this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: settings

+233 -117
+13 -103
src/app/settings/page.tsx
··· 1 - "use client"; 2 - 3 - import { 4 - ArrowLeft01Icon, 5 - Bookmark03Icon, 6 - Logout01Icon, 7 - } from "@hugeicons/core-free-icons"; 8 - import { HugeiconsIcon } from "@hugeicons/react"; 9 - import Link from "next/link"; 10 - import { Button } from "@/components/ui/button"; 11 - import { ButtonGroup } from "@/components/ui/button-group"; 12 - import { 13 - Card, 14 - CardAction, 15 - CardContent, 16 - CardDescription, 17 - CardHeader, 18 - CardTitle, 19 - } from "@/components/ui/card"; 20 - import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; 21 - import { Input } from "@/components/ui/input"; 22 - import { authClient } from "@/lib/auth-client"; 23 - import { uploadImage } from "@/lib/image"; 24 - 25 - export default function Page() { 26 - const { data: session } = authClient.useSession(); 27 - const user = session?.user; 28 - 29 - if (!user) return null; 1 + import { headers } from "next/headers"; 2 + import { redirect } from "next/navigation"; 3 + import { Settings } from "@/components/settings"; 4 + import { auth } from "@/lib/auth"; 30 5 31 - const handleSave = async (e: React.SubmitEvent<HTMLFormElement>) => { 32 - e.preventDefault(); 33 - const formData = new FormData(e.currentTarget); 6 + export default async function Page() { 7 + const session = await auth.api.getSession({ 8 + headers: await headers(), 9 + }); 34 10 35 - const picture = formData.get("picture"); 36 - const name = formData.get("name"); 37 - const email = formData.get("email"); 11 + if (!session) redirect("/"); 38 12 39 - if (picture instanceof File && picture.size > 0) { 40 - const { url } = await uploadImage(picture); 41 - await authClient.updateUser({ 42 - image: url, 43 - }); 44 - } 45 - 46 - if (name && name !== user.name) 47 - await authClient.updateUser({ 48 - name: name as string, 49 - }); 50 - 51 - if (email && email !== user.email) 52 - await authClient.changeEmail({ 53 - newEmail: email as string, 54 - callbackURL: "/", 55 - }); 13 + const user = { 14 + ...session.user, 15 + image: session.user.image || "", 56 16 }; 57 17 58 - return ( 59 - <Card> 60 - <CardHeader> 61 - <CardTitle>Settings</CardTitle> 62 - <CardDescription> 63 - Manage your account settings and preferences. 64 - </CardDescription> 65 - <CardAction> 66 - <ButtonGroup orientation="vertical"> 67 - <Button render={<Link href="/" />} nativeButton={false}> 68 - <HugeiconsIcon icon={ArrowLeft01Icon} /> 69 - Back 70 - </Button> 71 - <Button variant="destructive" onClick={() => authClient.signOut()}> 72 - <HugeiconsIcon icon={Logout01Icon} /> 73 - Logout 74 - </Button> 75 - </ButtonGroup> 76 - </CardAction> 77 - </CardHeader> 78 - <CardContent> 79 - <form onSubmit={handleSave}> 80 - <FieldGroup> 81 - <Field> 82 - <FieldLabel htmlFor="avatar">Avatar</FieldLabel> 83 - <Input id="avatar" type="file" name="avatar" accept="image/*" /> 84 - </Field> 85 - <Field> 86 - <FieldLabel>Name</FieldLabel> 87 - <Input defaultValue={user.name} name="name" required /> 88 - </Field> 89 - <Field> 90 - <FieldLabel>Email</FieldLabel> 91 - <Input 92 - defaultValue={user.email} 93 - name="email" 94 - type="email" 95 - required 96 - /> 97 - </Field> 98 - <Field> 99 - <Button type="submit"> 100 - <HugeiconsIcon icon={Bookmark03Icon} /> 101 - Save Changes 102 - </Button> 103 - </Field> 104 - </FieldGroup> 105 - </form> 106 - </CardContent> 107 - </Card> 108 - ); 18 + return <Settings user={user} />; 109 19 }
+125
src/components/settings.tsx
··· 1 + "use client"; 2 + 3 + import { 4 + ArrowLeft01Icon, 5 + Bookmark03Icon, 6 + Logout01Icon, 7 + } from "@hugeicons/core-free-icons"; 8 + import { HugeiconsIcon } from "@hugeicons/react"; 9 + import Link from "next/link"; 10 + import { Button } from "@/components/ui/button"; 11 + import { ButtonGroup } from "@/components/ui/button-group"; 12 + import { 13 + Card, 14 + CardAction, 15 + CardContent, 16 + CardDescription, 17 + CardHeader, 18 + CardTitle, 19 + } from "@/components/ui/card"; 20 + import { 21 + Field, 22 + FieldContent, 23 + FieldDescription, 24 + FieldGroup, 25 + FieldLabel, 26 + } from "@/components/ui/field"; 27 + import { Input } from "@/components/ui/input"; 28 + import { Switch } from "@/components/ui/switch"; 29 + import { authClient } from "@/lib/auth-client"; 30 + import { uploadImage } from "@/lib/image"; 31 + import { updateSetting } from "@/lib/setting"; 32 + 33 + type User = { 34 + name: string; 35 + email: string; 36 + }; 37 + 38 + export function Settings({ user }: { user: User }) { 39 + const handleSave = async (e: React.SubmitEvent<HTMLFormElement>) => { 40 + e.preventDefault(); 41 + const formData = new FormData(e.currentTarget); 42 + 43 + const picture = formData.get("picture") as File | null; 44 + const name = formData.get("name") as string; 45 + const email = formData.get("email") as string; 46 + const notify = formData.get("notify") === "on"; 47 + 48 + if (picture && picture.size > 0) { 49 + const { url } = await uploadImage(picture); 50 + await authClient.updateUser({ 51 + image: url, 52 + }); 53 + } 54 + 55 + if (email && email !== user.email) 56 + await authClient.changeEmail({ 57 + newEmail: email, 58 + callbackURL: "/", 59 + }); 60 + 61 + await authClient.updateUser({ name }); 62 + await updateSetting(notify); 63 + }; 64 + 65 + return ( 66 + <Card> 67 + <CardHeader> 68 + <CardTitle>Settings</CardTitle> 69 + <CardDescription> 70 + Manage your account settings and preferences. 71 + </CardDescription> 72 + <CardAction> 73 + <ButtonGroup orientation="vertical"> 74 + <Button render={<Link href="/" />} nativeButton={false}> 75 + <HugeiconsIcon icon={ArrowLeft01Icon} /> 76 + Back 77 + </Button> 78 + <Button variant="destructive" onClick={() => authClient.signOut()}> 79 + <HugeiconsIcon icon={Logout01Icon} /> 80 + Logout 81 + </Button> 82 + </ButtonGroup> 83 + </CardAction> 84 + </CardHeader> 85 + <CardContent> 86 + <form onSubmit={handleSave}> 87 + <FieldGroup> 88 + <Field> 89 + <FieldLabel htmlFor="avatar">Avatar</FieldLabel> 90 + <Input id="avatar" type="file" name="avatar" accept="image/*" /> 91 + </Field> 92 + <Field> 93 + <FieldLabel>Name</FieldLabel> 94 + <Input defaultValue={user.name} name="name" required /> 95 + </Field> 96 + <Field> 97 + <FieldLabel>Email</FieldLabel> 98 + <Input 99 + defaultValue={user.email} 100 + name="email" 101 + type="email" 102 + required 103 + /> 104 + </Field> 105 + <Field orientation="horizontal"> 106 + <FieldContent> 107 + <FieldLabel htmlFor="notify">Notification</FieldLabel> 108 + <FieldDescription> 109 + Get notified when someone claims a voucher from your groups. 110 + </FieldDescription> 111 + </FieldContent> 112 + <Switch id="notify" name="notify" defaultChecked /> 113 + </Field> 114 + <Field> 115 + <Button type="submit"> 116 + <HugeiconsIcon icon={Bookmark03Icon} /> 117 + Save Changes 118 + </Button> 119 + </Field> 120 + </FieldGroup> 121 + </form> 122 + </CardContent> 123 + </Card> 124 + ); 125 + }
+32
src/components/ui/switch.tsx
··· 1 + "use client"; 2 + 3 + import { Switch as SwitchPrimitive } from "@base-ui/react/switch"; 4 + 5 + import { cn } from "@/lib/utils"; 6 + 7 + function Switch({ 8 + className, 9 + size = "default", 10 + ...props 11 + }: SwitchPrimitive.Root.Props & { 12 + size?: "sm" | "default"; 13 + }) { 14 + return ( 15 + <SwitchPrimitive.Root 16 + data-slot="switch" 17 + data-size={size} 18 + className={cn( 19 + "data-checked:bg-primary data-unchecked:bg-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 shrink-0 rounded-full border border-transparent focus-visible:ring-[3px] aria-invalid:ring-[3px] data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] peer group/switch relative inline-flex items-center transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 data-disabled:cursor-not-allowed data-disabled:opacity-50", 20 + className, 21 + )} 22 + {...props} 23 + > 24 + <SwitchPrimitive.Thumb 25 + data-slot="switch-thumb" 26 + className="bg-background dark:data-unchecked:bg-foreground dark:data-checked:bg-primary-foreground rounded-full group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 pointer-events-none block ring-0 transition-transform" 27 + /> 28 + </SwitchPrimitive.Root> 29 + ); 30 + } 31 + 32 + export { Switch };
+8
src/db/schema.ts
··· 1 1 import { relations } from "drizzle-orm"; 2 2 import { 3 + boolean, 3 4 index, 4 5 integer, 5 6 pgTable, ··· 50 51 index("member_user_idx").on(t.userId), 51 52 ], 52 53 ); 54 + 55 + export const setting = pgTable("setting", { 56 + userId: text() 57 + .primaryKey() 58 + .references(() => user.id, { onDelete: "cascade" }), 59 + notify: boolean().notNull().default(true), 60 + }); 53 61 54 62 export const groupRelations = relations(group, ({ many, one }) => ({ 55 63 members: many(member),
+14 -6
src/lib/auth.ts
··· 4 4 import * as schema from "@/db/schema"; 5 5 import { db } from "@/lib/db"; 6 6 import { sendEmail } from "@/lib/email"; 7 + import { createSetting } from "./setting"; 7 8 8 9 export const auth = betterAuth({ 9 10 database: drizzleAdapter(db, { ··· 15 16 sendVerificationEmail: async ({ user, url, token }) => { 16 17 await sendEmail({ 17 18 to: user.email, 18 - subject: "Verify your email for Vouch", 19 - html: ` 20 - <h1>Welcome to Vouch</h1> 21 - <p>Click the link below to verify your email address:</p> 22 - <a href="${url}">Verify Email</a> 23 - `, 19 + subject: "Confirm your email - Vouch", 20 + html: `<p>Please confirm your email address by clicking the link below:</p> 21 + <p><a href="${url}">${url}</a></p> 22 + <p>If you did not request this, you can ignore this email.</p>`, 24 23 }); 25 24 }, 26 25 }, ··· 28 27 google: { 29 28 clientId: process.env.GOOGLE_CLIENT_ID as string, 30 29 clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, 30 + }, 31 + }, 32 + databaseHooks: { 33 + user: { 34 + create: { 35 + after: async (user) => { 36 + await createSetting(user.id); 37 + }, 38 + }, 31 39 }, 32 40 }, 33 41 });
+3 -3
src/lib/group.ts
··· 1 1 "use server"; 2 2 3 - import { and, eq } from "drizzle-orm"; 3 + import { eq } from "drizzle-orm"; 4 4 5 5 import { group, member } from "@/db/schema"; 6 6 import { getUserId } from "@/lib/auth"; ··· 65 65 66 66 export async function deleteGroup(id: number) { 67 67 if (!(await isGroupOwner(id))) return false; 68 - const res = await db.delete(group).where(and(eq(group.id, id))); 68 + const res = await db.delete(group).where(eq(group.id, id)); 69 69 return res.rowCount > 0; 70 70 } 71 71 ··· 79 79 const [res] = await db 80 80 .update(group) 81 81 .set({ name, description }) 82 - .where(and(eq(group.id, id))) 82 + .where(eq(group.id, id)) 83 83 .returning(); 84 84 85 85 return res;
+2 -2
src/lib/invite.ts
··· 1 1 "use server"; 2 2 3 - import { and, eq } from "drizzle-orm"; 3 + import { eq } from "drizzle-orm"; 4 4 5 5 import { group } from "@/db/schema"; 6 6 import { db } from "@/lib/db"; ··· 18 18 const [res] = await db 19 19 .update(group) 20 20 .set({ invite }) 21 - .where(and(eq(group.id, id))) 21 + .where(eq(group.id, id)) 22 22 .returning(); 23 23 24 24 return res;
+33
src/lib/setting.ts
··· 1 + "use server"; 2 + 3 + import { eq } from "drizzle-orm"; 4 + import { setting } from "@/db/schema"; 5 + import { getUserId } from "@/lib/auth"; 6 + import { db } from "@/lib/db"; 7 + 8 + export async function createSetting(userId: string) { 9 + const [res] = await db.insert(setting).values({ userId: userId }).returning(); 10 + return res; 11 + } 12 + 13 + export async function updateSetting(notify: boolean) { 14 + const userId = await getUserId(); 15 + if (!userId) return null; 16 + 17 + const [res] = await db 18 + .update(setting) 19 + .set({ notify }) 20 + .where(eq(setting.userId, userId)) 21 + .returning(); 22 + 23 + return res; 24 + } 25 + 26 + export async function getSetting() { 27 + const userId = await getUserId(); 28 + if (!userId) return null; 29 + 30 + return await db.query.setting.findFirst({ 31 + where: eq(setting.userId, userId), 32 + }); 33 + }
+3 -3
src/lib/voucher.ts
··· 1 1 "use server"; 2 2 3 - import { and, eq, inArray } from "drizzle-orm"; 3 + import { eq, inArray } from "drizzle-orm"; 4 4 import { voucher } from "@/db/schema"; 5 5 import { db } from "@/lib/db"; 6 6 import { getGroups, isGroupOwner } from "@/lib/group"; ··· 50 50 51 51 export async function deleteVoucher(id: number) { 52 52 if (!(await isVoucherOwner(id))) return false; 53 - const res = await db.delete(voucher).where(and(eq(voucher.id, id))); 53 + const res = await db.delete(voucher).where(eq(voucher.id, id)); 54 54 return res.rowCount > 0; 55 55 } 56 56 ··· 64 64 const [res] = await db 65 65 .update(voucher) 66 66 .set({ name, description }) 67 - .where(and(eq(voucher.id, id))) 67 + .where(eq(voucher.id, id)) 68 68 .returning(); 69 69 70 70 return res;