Skip to content

Commit

Permalink
feat: added edit testimonial and share copy to clipborad
Browse files Browse the repository at this point in the history
  • Loading branch information
Shashivadan committed Nov 11, 2024
1 parent 8a4d1f0 commit 42f7110
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 29 deletions.
13 changes: 12 additions & 1 deletion apps/www/src/actions/create-testimonial.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"use server";

import type { z } from "zod";
import { revalidatePath } from "next/cache";

import { eq } from "@acme/db";
import { db } from "@acme/db/client";
import { testimonialTable } from "@acme/db/schema";
import { organizationTable, testimonialTable } from "@acme/db/schema";

import type { testimonialFormSchema } from "~/components/testimonial/create-testimonial-form";
import { getCurrentUser } from "~/utils/get-current-user";
Expand All @@ -17,6 +19,13 @@ export const createTestimonial = async (
throw new Error("Unauthorized: You must be logged in");
}
try {
const orgData = await db
.select({
orgName: organizationTable.organizationName,
})
.from(organizationTable)
.where(eq(organizationTable.id, orgId));

const result = await db
.insert(testimonialTable)
.values({
Expand All @@ -33,6 +42,8 @@ export const createTestimonial = async (
})
.returning();

revalidatePath(`/products/${orgData[0]?.orgName}/**`);

return result;
} catch (error) {
return (error as Error).message;
Expand Down
2 changes: 1 addition & 1 deletion apps/www/src/actions/edit-space.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export async function editSpace(
)
.returning();

revalidatePath(`/products/${values.organizationName}`);
revalidatePath(`/products/${values.organizationName}/**`);

return result;
} catch (error) {
Expand Down
57 changes: 57 additions & 0 deletions apps/www/src/actions/edit-testimonial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use server";

import type { z } from "zod";
import { revalidatePath } from "next/cache";

import { and, eq } from "@acme/db";
import { db } from "@acme/db/client";
import { organizationTable, testimonialTable } from "@acme/db/schema";

import type { testimonialEditFormSchema } from "~/components/testimonial/edit-testimonial";
import { getCurrentUser } from "~/utils/get-current-user";

export const editTestimonial = async (
data: z.infer<typeof testimonialEditFormSchema>,
testimonialId: string,
orgId: string,
) => {
const user = await getCurrentUser();
if (!user) {
throw new Error("Unauthorized: You must be logged in");
}

try {
const orgData = await db
.select({
orgName: organizationTable.organizationName,
})
.from(organizationTable)
.where(eq(organizationTable.id, orgId));

const result = await db
.update(testimonialTable)
.set({
profileImages:
data.profileImages === "" || !data.profileImages?.startsWith("https")
? `https://robohash.org/${data.authorEmail}`
: data.profileImages,
authorEmail: data.authorEmail,
reviewImages: data.reviewImages ?? "",
message: data.message,
rating: data.rating,
authorName: data.authorName,
})
.where(
and(
eq(testimonialTable.organizationId, orgId),
eq(testimonialTable.id, testimonialId),
),
);

revalidatePath(`/products/${orgData[0]?.orgName}/**`);

return result;
} catch (error) {
return (error as Error).message;
}
};
16 changes: 9 additions & 7 deletions apps/www/src/app/(share)/share/[testimonial]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,15 @@ export default async function page({
</div>
</div>
{testimonialDetails.reviewImages && (
<Image
src={testimonialDetails.reviewImages}
alt="Review image"
width={500}
height={300}
className="aspect-video w-full rounded-lg object-cover"
/>
<div className="flex items-center justify-center">
<Image
width={300}
height={300}
src={testimonialDetails.reviewImages}
alt="Review"
className="mt-4 rounded-md object-cover object-center"
/>
</div>
)}
{testimonialDetails.type === "video" && (
<p className="text-center text-muted-foreground">
Expand Down
211 changes: 211 additions & 0 deletions apps/www/src/components/testimonial/edit-testimonial.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import React from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { Edit, Star } from "lucide-react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";

import { Button } from "@acme/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
} from "@acme/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@acme/ui/form";
import { Input } from "@acme/ui/input";
import { Textarea } from "@acme/ui/textarea";

import type { TestimonialType } from "~/types";
import { editTestimonial } from "~/actions/edit-testimonial";

const imageRegex = /^(https?:\/\/[^\s]+)$/g;
export const testimonialEditFormSchema = z.object({
authorName: z.string().min(4, "Name must be at least 4 characters"),
profileImages: z.string().regex(imageRegex).optional(),
authorEmail: z.string().email(),
reviewImages: z.string().regex(imageRegex).optional(),
message: z.string().min(10, "Review must be at least 30 characters"),
rating: z.number().min(1).max(5),
});

export default function EditTestimonial({ data }: { data: TestimonialType }) {
const form = useForm<z.infer<typeof testimonialEditFormSchema>>({
resolver: zodResolver(testimonialEditFormSchema),
defaultValues: {
authorName: data.authorName,
profileImages: data.profileImages ?? "",
authorEmail: data.authorEmail,
reviewImages: data.reviewImages ?? "",
message: data.message,
rating: data.rating,
},
});
async function onSubmit(values: z.infer<typeof testimonialEditFormSchema>) {
try {
await editTestimonial(values, data.id, data.organizationId);
toast.success(`Successfully saved changes`);
} catch (error) {
toast.error((error as Error).message);
}
}
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" size="sm" className="rounded-sm">
Edit <Edit size={16} className="ml-2" />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] md:min-w-[500px]">
<DialogTitle>Edit Testimonial</DialogTitle>
<div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 p-4"
>
<FormField
control={form.control}
name="rating"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex items-center space-x-1">
{[1, 2, 3, 4, 5].map((value) => (
<button
key={value}
type="button"
className="transition-transform hover:scale-110 focus:outline-none"
onClick={() => field.onChange(value)}
onMouseEnter={() => field.onChange(value)}
>
<Star
className={`h-8 w-8 transition-colors ${field.value >= value ? "fill-yellow-400 text-yellow-500" : "text-zinc-200"}`}
/>
</button>
))}
</div>
</FormControl>
<div className="mt-1 text-sm text-gray-500">
{field.value === 1 && "Poor"}
{field.value === 2 && "Fair"}
{field.value === 3 && "Good"}
{field.value === 4 && "Very Good"}
{field.value === 5 && "Excellent"}
</div>
<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormControl>
<Textarea
placeholder="write your experience with us"
className="h-32 resize-none"
{...field}
/>
</FormControl>

<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="authorName"
render={({ field }) => (
<FormItem>
<FormLabel className="mb-1 flex justify-start">
Name <span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<Input placeholder="Name" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="authorEmail"
render={({ field }) => (
<FormItem>
<FormLabel className="mb-1 flex justify-start">
Email <span className="text-red-500">*</span>
</FormLabel>
<FormControl>
<Input placeholder="[email protected]" {...field} />
</FormControl>
<FormDescription>
We won't spam you, we promise.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="profileImages"
render={({ field }) => (
<FormItem>
<FormLabel className="mb-1 flex justify-start">
Profile-image{" "}
<span className="text-xs text-zinc-500">(optional)</span>
</FormLabel>
<FormControl>
<Input placeholder="profile image-URL" {...field} />
</FormControl>

<FormMessage />
</FormItem>
)}
/>

<FormField
control={form.control}
name="reviewImages"
render={({ field }) => (
<FormItem>
<FormLabel className="mb-1 flex justify-start">
Review-images{" "}
<span className="text-xs text-zinc-500">(optional)</span>
</FormLabel>
<FormControl>
<Input placeholder="https://image.png-URL" {...field} />
</FormControl>

<FormMessage />
</FormItem>
)}
/>

<DialogClose asChild>
<Button type="submit">Save changes</Button>
</DialogClose>
</form>
</Form>
</div>
<DialogDescription className="m-0 hidden p-0"></DialogDescription>
</DialogContent>
</Dialog>
);
}
19 changes: 12 additions & 7 deletions apps/www/src/components/testimonial/share-testimonial.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React from "react";
import Link from "next/link";
import { CopyIcon, LucideLinkedin, Share, Twitter } from "lucide-react";

import { Button } from "@acme/ui/button";
Expand All @@ -9,29 +8,35 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@acme/ui/dropdown-menu";
import { toast } from "@acme/ui/toast";

import type { TestimonialType } from "~/types";

export default function ShareTestimonial({ data }: { data: TestimonialType }) {
const domain = document.location.origin;

const getLinkOnClick = async () => {
await navigator.clipboard.writeText(`${domain}/share/${data.id}`);
toast.success("successfully copied to clipboard");
};
const twitterShareUrl = `https://twitter.com/intent/tweet?url=${encodeURIComponent(`${domain}/share/${data.id}`)}&text=${encodeURIComponent("Check out these amazing customer testimonial! 🌟")}`;
const linkedinShareUrl = `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(`${domain}/share/${data.id}`)}&title=${encodeURIComponent("Real stories from real users ⭐️ See what everyone's saying about us!")}&summary=${encodeURIComponent("Check out these amazing customer testimonial! 🌟")}&source=${encodeURIComponent(`${domain}/share/${data.id}`)}`;
return (
<DropdownMenu>
<DropdownMenuTrigger className="flex items-center">
<DropdownMenuTrigger className="flex items-center" asChild>
<Button variant="outline" size="sm" className="rounded-sm">
Share <Share size={16} className="ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Link
target="_blank"
<DropdownMenuItem asChild>
<Button
variant={"ghost"}
onClick={getLinkOnClick}
className="flex items-center justify-between gap-2"
href={`/share/${data.id}`}
>
Get the link <CopyIcon size={16} />
</Link>
</Button>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<a
Expand Down
Loading

0 comments on commit 42f7110

Please sign in to comment.