this repo has no description
0
fork

Configure Feed

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

feat: redemption

+93 -26
+5 -2
src/app/settings/page.tsx
··· 2 2 import { redirect } from "next/navigation"; 3 3 import { Settings } from "@/components/settings"; 4 4 import { auth } from "@/lib/auth"; 5 + import { getSetting } from "@/lib/setting"; 5 6 6 7 export default async function Page() { 7 8 const session = await auth.api.getSession({ 8 9 headers: await headers(), 9 10 }); 10 11 11 - if (!session) redirect("/"); 12 + const setting = await getSetting(); 13 + 14 + if (!session || !setting) redirect("/"); 12 15 13 16 const user = { 14 17 ...session.user, 15 18 image: session.user.image || "", 16 19 }; 17 20 18 - return <Settings user={user} />; 21 + return <Settings user={user} setting={setting} />; 19 22 }
+7 -14
src/components/group.tsx
··· 20 20 CardAction, 21 21 CardContent, 22 22 CardDescription, 23 - CardFooter, 24 23 CardHeader, 25 24 CardTitle, 26 25 } from "@/components/ui/card"; ··· 57 56 ItemTitle, 58 57 } from "@/components/ui/item"; 59 58 import type { voucher } from "@/db/schema"; 60 - import { sendEmail } from "@/lib/email"; 61 59 import type { GroupWithMembers } from "@/lib/group"; 62 60 import { deleteMember } from "@/lib/member"; 63 - import { createVoucher, deleteVoucher, updateVoucher } from "@/lib/voucher"; 61 + import { 62 + createVoucher, 63 + deleteVoucher, 64 + redeemVoucher, 65 + updateVoucher, 66 + } from "@/lib/voucher"; 64 67 65 68 type Voucher = typeof voucher.$inferSelect; 66 69 type User = { ··· 280 283 </DialogContent> 281 284 </Dialog> 282 285 )) || ( 283 - <Button 284 - onClick={() => 285 - sendEmail({ 286 - to: group.owner.email, 287 - subject: `Voucher Claimed: ${voucher.name}`, 288 - html: `<p>New activity in <strong>${group.name}</strong>:</p> 289 - <p><strong>${user.name}</strong> has just claimed the <strong>${voucher.name}</strong> voucher.</p> 290 - <p>You can reach out to them at ${user.email} to finalize the details.</p>`, 291 - }) 292 - } 293 - > 286 + <Button onClick={() => redeemVoucher(voucher.id)}> 294 287 <HugeiconsIcon icon={Ticket02Icon} /> 295 288 Redeem 296 289 </Button>
+8 -2
src/components/settings.tsx
··· 26 26 } from "@/components/ui/field"; 27 27 import { Input } from "@/components/ui/input"; 28 28 import { Switch } from "@/components/ui/switch"; 29 + import type { setting } from "@/db/schema"; 29 30 import { authClient } from "@/lib/auth-client"; 30 31 import { uploadImage } from "@/lib/image"; 31 32 import { updateSetting } from "@/lib/setting"; 32 33 34 + type Setting = typeof setting.$inferSelect; 33 35 type User = { 34 36 name: string; 35 37 email: string; 36 38 }; 37 39 38 - export function Settings({ user }: { user: User }) { 40 + export function Settings({ user, setting }: { user: User; setting: Setting }) { 39 41 const handleSave = async (e: React.SubmitEvent<HTMLFormElement>) => { 40 42 e.preventDefault(); 41 43 const formData = new FormData(e.currentTarget); ··· 109 111 Get notified when someone claims a voucher from your groups. 110 112 </FieldDescription> 111 113 </FieldContent> 112 - <Switch id="notify" name="notify" defaultChecked /> 114 + <Switch 115 + id="notify" 116 + name="notify" 117 + defaultChecked={setting.notify} 118 + /> 113 119 </Field> 114 120 <Field> 115 121 <Button type="submit">
+29 -1
src/db/schema.ts
··· 6 6 pgTable, 7 7 primaryKey, 8 8 text, 9 + timestamp, 9 10 varchar, 10 11 } from "drizzle-orm/pg-core"; 11 12 ··· 59 60 notify: boolean().notNull().default(true), 60 61 }); 61 62 63 + export const redemption = pgTable( 64 + "redemption", 65 + { 66 + id: integer().primaryKey().generatedAlwaysAsIdentity(), 67 + voucherId: integer() 68 + .notNull() 69 + .references(() => voucher.id, { onDelete: "cascade" }), 70 + userId: text() 71 + .notNull() 72 + .references(() => user.id, { onDelete: "cascade" }), 73 + date: timestamp().defaultNow().notNull(), 74 + }, 75 + (t) => [index("redemption_user_idx").on(t.userId)], 76 + ); 77 + 62 78 export const groupRelations = relations(group, ({ many, one }) => ({ 63 79 members: many(member), 64 80 owner: one(user, { ··· 67 83 }), 68 84 })); 69 85 70 - export const memberRelations = relations(member, ({ one }) => ({ 86 + export const memberRelations = relations(member, ({ one, many }) => ({ 71 87 group: one(group, { 72 88 fields: [member.groupId], 73 89 references: [group.id], ··· 75 91 user: one(user, { 76 92 fields: [member.userId], 77 93 references: [user.id], 94 + }), 95 + redemptions: many(redemption), 96 + })); 97 + 98 + export const redemptionRelations = relations(redemption, ({ one }) => ({ 99 + voucher: one(voucher, { 100 + fields: [redemption.voucherId], 101 + references: [voucher.id], 102 + }), 103 + member: one(member, { 104 + fields: [redemption.userId], 105 + references: [member.userId], 78 106 }), 79 107 }));
+7 -2
src/lib/auth.ts
··· 40 40 }, 41 41 }); 42 42 43 + export async function getUser() { 44 + const data = await auth.api.getSession({ headers: await headers() }); 45 + return data?.user || null; 46 + } 47 + 43 48 export async function getUserId() { 44 - const data = await auth.api.getSession({ headers: await headers() }); 45 - return data?.user.id ?? null; 49 + const user = await getUser(); 50 + return user?.id || null; 46 51 }
+1
src/lib/group.ts
··· 44 44 members: { 45 45 with: { 46 46 user: true, 47 + redemptions: true, 47 48 }, 48 49 }, 49 50 },
+5 -3
src/lib/setting.ts
··· 23 23 return res; 24 24 } 25 25 26 - export async function getSetting() { 27 - const userId = await getUserId(); 28 - if (!userId) return null; 26 + export async function getSetting(userId?: string) { 27 + if (!userId) { 28 + userId = (await getUserId()) || undefined; 29 + if (!userId) return null; 30 + } 29 31 30 32 return await db.query.setting.findFirst({ 31 33 where: eq(setting.userId, userId),
+31 -2
src/lib/voucher.ts
··· 1 1 "use server"; 2 2 3 3 import { eq, inArray } from "drizzle-orm"; 4 - import { voucher } from "@/db/schema"; 4 + import { redemption, voucher } from "@/db/schema"; 5 + import { getUser } from "@/lib/auth"; 5 6 import { db } from "@/lib/db"; 6 - import { getGroups, isGroupOwner } from "@/lib/group"; 7 + import { sendEmail } from "@/lib/email"; 8 + import { getGroup, getGroups, isGroupOwner } from "@/lib/group"; 9 + import { getSetting } from "@/lib/setting"; 7 10 8 11 export async function createVoucher( 9 12 groupId: number, ··· 69 72 70 73 return res; 71 74 } 75 + 76 + export async function redeemVoucher(id: number) { 77 + const user = await getUser(); 78 + const voucher = await getVoucher(id); 79 + 80 + const setting = await getSetting(user?.id || ""); 81 + const group = await getGroup(voucher?.groupId || 0); 82 + 83 + if (!user || !setting || !voucher || !group) return null; 84 + 85 + const [res] = await db 86 + .insert(redemption) 87 + .values({ voucherId: id, userId: user.id }) 88 + .returning(); 89 + 90 + if (setting.notify) 91 + sendEmail({ 92 + to: group.owner.email, 93 + subject: `Voucher Claimed: ${voucher.name}`, 94 + html: `<p>New activity in <strong>${group.name}</strong>:</p> 95 + <p><strong>${user.name}</strong> has just claimed the <strong>${voucher.name}</strong> voucher.</p> 96 + <p>You can reach out to them at ${user.email} to finalize the details.</p>`, 97 + }); 98 + 99 + return res; 100 + }