this repo has no description
0
fork

Configure Feed

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

feat: user/owner view

+217 -157
+13 -3
src/app/[id]/page.tsx
··· 1 + import { headers } from "next/headers"; 1 2 import { redirect } from "next/navigation"; 2 - 3 3 import { Group } from "@/components/group"; 4 + import { auth } from "@/lib/auth"; 4 5 import { getGroup } from "@/lib/group"; 5 6 import { redeemInvite } from "@/lib/invite"; 6 7 import { getVouchers } from "@/lib/voucher"; ··· 12 13 params: Promise<{ id: string }>; 13 14 searchParams: Promise<{ invite?: string }>; 14 15 }) { 16 + const session = await auth.api.getSession({ 17 + headers: await headers(), 18 + }); 19 + 15 20 const { invite } = await searchParams; 16 21 if (invite) await redeemInvite(invite as string); 17 22 ··· 19 24 const group = await getGroup(Number(id)); 20 25 const vouchers = await getVouchers(Number(id)); 21 26 22 - if (!group) redirect("/"); 27 + if (!session || !group) redirect("/"); 23 28 24 - return <Group group={group} vouchers={vouchers} />; 29 + const user = { 30 + ...session.user, 31 + image: session.user.image || "", 32 + }; 33 + 34 + return <Group user={user} group={group} vouchers={vouchers} />; 25 35 }
+4 -2
src/app/page.tsx
··· 1 1 import { headers } from "next/headers"; 2 + import { redirect } from "next/navigation"; 2 3 import { Groups } from "@/components/groups"; 3 4 import { auth } from "@/lib/auth"; 4 5 import { getGroups } from "@/lib/group"; ··· 8 9 headers: await headers(), 9 10 }); 10 11 11 - if (!session) return null; 12 - const groups = await getGroups(); 12 + if (!session) redirect("/"); 13 13 14 14 const user = { 15 15 ...session.user, 16 16 image: session.user.image || "", 17 17 }; 18 + 19 + const groups = await getGroups(); 18 20 19 21 return <Groups user={user} groups={groups} />; 20 22 }
+198 -152
src/components/group.tsx
··· 53 53 ItemTitle, 54 54 } from "@/components/ui/item"; 55 55 import type { voucher } from "@/db/schema"; 56 + import { sendEmail } from "@/lib/email"; 56 57 import type { GroupWithMembers } from "@/lib/group"; 57 58 import { deleteMember } from "@/lib/member"; 58 59 import { createVoucher, deleteVoucher, updateVoucher } from "@/lib/voucher"; 59 60 60 61 type Voucher = typeof voucher.$inferSelect; 62 + type User = { 63 + id: string; 64 + name: string; 65 + image: string; 66 + email: string; 67 + }; 61 68 62 69 export function Group({ 70 + user, 63 71 group, 64 72 vouchers: initialVouchers, 65 73 }: { 74 + user: User; 66 75 group: GroupWithMembers; 67 76 vouchers: Voucher[]; 68 77 }) { 69 78 const [vouchers, setVouchers] = useState(initialVouchers); 70 - const users = [group.owner, ...group.members.map((m) => m.user)]; 79 + const [users, setUsers] = useState([ 80 + group.owner, 81 + ...group.members.map((m) => m.user), 82 + ]); 71 83 72 84 return ( 73 85 <Card> ··· 96 108 <DialogTitle>Members</DialogTitle> 97 109 </DialogHeader> 98 110 <ItemGroup> 99 - {users.map((user) => ( 100 - <Item key={user.id}> 111 + {users.map((current) => ( 112 + <Item key={current.id}> 101 113 <ItemMedia> 102 114 <CustomAvatar 103 - name={user.name} 104 - image={user.image as string} 115 + name={current.name} 116 + image={current.image as string} 105 117 /> 106 118 </ItemMedia> 107 119 <ItemContent> 108 - <ItemTitle>{user.name}</ItemTitle> 120 + <ItemTitle>{current.name}</ItemTitle> 109 121 <ItemDescription> 110 - {user.id === group.userId ? "Owner" : "Member"} 122 + {current.id === group.userId ? "Owner" : "Member"} 111 123 </ItemDescription> 112 124 </ItemContent> 113 - {user.id !== group.owner.id && ( 114 - <ItemActions> 115 - <Button 116 - variant="ghost" 117 - onClick={() => deleteMember(group.id, user.id)} 118 - > 119 - <HugeiconsIcon icon={Delete01Icon} /> 120 - </Button> 121 - </ItemActions> 122 - )} 125 + {user.id === group.owner.id && 126 + current.id !== group.owner.id && ( 127 + <ItemActions> 128 + <Button 129 + variant="ghost" 130 + onClick={() => { 131 + deleteMember(group.id, current.id).then( 132 + (res) => { 133 + if (!res) return; 134 + setUsers((prevUsers) => 135 + prevUsers.filter( 136 + (u) => u.id !== current.id, 137 + ), 138 + ); 139 + }, 140 + ); 141 + }} 142 + > 143 + <HugeiconsIcon icon={Delete01Icon} /> 144 + </Button> 145 + </ItemActions> 146 + )} 123 147 </Item> 124 148 ))} 125 149 </ItemGroup> 126 150 127 151 <InputGroup> 128 152 <InputGroupInput 129 - placeholder={`${window.location}?invite=${group.invite}`} 153 + placeholder={`${window.location.origin}/${group.id}?invite=${group.invite}`} 130 154 readOnly 131 155 /> 132 156 <InputGroupAddon align="inline-end"> 133 157 <InputGroupButton 134 158 onClick={() => 135 159 navigator.clipboard.writeText( 136 - `${window.location}?invite=${group.invite}`, 160 + `${window.location.origin}/${group.id}?invite=${group.invite}`, 137 161 ) 138 162 } 139 163 > ··· 155 179 <ItemDescription>{voucher.description}</ItemDescription> 156 180 </ItemContent> 157 181 <ItemActions> 158 - <Dialog> 159 - <DialogTrigger 160 - render={<Button className="w-full">Edit</Button>} 161 - /> 162 - <DialogContent> 163 - <form 164 - onSubmit={(e) => { 165 - e.preventDefault(); 166 - const formData = new FormData(e.currentTarget); 167 - const name = formData.get("name") as string; 168 - const description = formData.get( 169 - "description", 170 - ) as string; 171 - updateVoucher(voucher.id, name, description).then( 172 - (res) => { 173 - if (!res) return; 174 - setVouchers((prevVouchers) => 175 - prevVouchers.map((v) => 176 - v.id === voucher.id ? res : v, 177 - ), 178 - ); 179 - }, 180 - ); 181 - }} 182 - > 183 - <FieldSet> 184 - <FieldLegend>Edit the voucher</FieldLegend> 185 - <FieldDescription>Are you happy now ?</FieldDescription> 186 - <FieldGroup> 187 - <Field> 188 - <FieldLabel htmlFor="name">Name</FieldLabel> 189 - <Input 190 - id="name" 191 - name="name" 192 - defaultValue={voucher.name} 193 - required 194 - /> 195 - </Field> 196 - <Field> 197 - <FieldLabel htmlFor="description"> 198 - Description 199 - </FieldLabel> 200 - <Input 201 - id="description" 202 - name="description" 203 - defaultValue={voucher.description || ""} 204 - required 205 - /> 206 - </Field> 207 - <Field orientation="horizontal"> 208 - <DialogClose 209 - render={ 210 - <Button 211 - variant="destructive" 212 - onClick={(e) => { 213 - e.preventDefault(); 214 - deleteVoucher(voucher.id).then((res) => { 215 - if (!res) return; 216 - setVouchers((prevVouchers) => 217 - prevVouchers.filter( 218 - (v) => v.id !== voucher.id, 219 - ), 220 - ); 221 - }); 222 - }} 223 - > 224 - <HugeiconsIcon icon={Delete01Icon} /> 225 - </Button> 226 - } 227 - /> 228 - <DialogClose 229 - render={ 230 - <Button type="submit" className="flex-1"> 231 - Update 232 - </Button> 233 - } 234 - /> 235 - </Field> 236 - </FieldGroup> 237 - </FieldSet> 238 - </form> 239 - </DialogContent> 240 - </Dialog> 182 + {(group.owner.id === user.id && ( 183 + <Dialog> 184 + <DialogTrigger 185 + render={<Button className="w-full">Edit</Button>} 186 + /> 187 + <DialogContent> 188 + <form 189 + onSubmit={(e) => { 190 + e.preventDefault(); 191 + const formData = new FormData(e.currentTarget); 192 + const name = formData.get("name") as string; 193 + const description = formData.get( 194 + "description", 195 + ) as string; 196 + updateVoucher(voucher.id, name, description).then( 197 + (res) => { 198 + if (!res) return; 199 + setVouchers((prevVouchers) => 200 + prevVouchers.map((v) => 201 + v.id === voucher.id ? res : v, 202 + ), 203 + ); 204 + }, 205 + ); 206 + }} 207 + > 208 + <FieldSet> 209 + <FieldLegend>Edit the voucher</FieldLegend> 210 + <FieldDescription> 211 + Are you happy now ? 212 + </FieldDescription> 213 + <FieldGroup> 214 + <Field> 215 + <FieldLabel htmlFor="name">Name</FieldLabel> 216 + <Input 217 + id="name" 218 + name="name" 219 + defaultValue={voucher.name} 220 + required 221 + /> 222 + </Field> 223 + <Field> 224 + <FieldLabel htmlFor="description"> 225 + Description 226 + </FieldLabel> 227 + <Input 228 + id="description" 229 + name="description" 230 + defaultValue={voucher.description || ""} 231 + required 232 + /> 233 + </Field> 234 + <Field orientation="horizontal"> 235 + <DialogClose 236 + render={ 237 + <Button 238 + variant="destructive" 239 + onClick={(e) => { 240 + e.preventDefault(); 241 + deleteVoucher(voucher.id).then((res) => { 242 + if (!res) return; 243 + setVouchers((prevVouchers) => 244 + prevVouchers.filter( 245 + (v) => v.id !== voucher.id, 246 + ), 247 + ); 248 + }); 249 + }} 250 + > 251 + <HugeiconsIcon icon={Delete01Icon} /> 252 + </Button> 253 + } 254 + /> 255 + <DialogClose 256 + render={ 257 + <Button type="submit" className="flex-1"> 258 + Update 259 + </Button> 260 + } 261 + /> 262 + </Field> 263 + </FieldGroup> 264 + </FieldSet> 265 + </form> 266 + </DialogContent> 267 + </Dialog> 268 + )) || ( 269 + <Button 270 + onClick={() => 271 + sendEmail({ 272 + to: group.owner.email, 273 + subject: `Voucher Claimed: ${voucher.name}`, 274 + html: `<p>New activity in <strong>${group.name}</strong>:</p> 275 + <p><strong>${user.name}</strong> has just claimed the <strong>${voucher.name}</strong> voucher.</p> 276 + <p>You can reach out to them at ${user.email} to finalize the details.</p>`, 277 + }) 278 + } 279 + > 280 + Redeem 281 + </Button> 282 + )} 241 283 </ItemActions> 242 284 </Item> 243 285 ))} 244 - <Dialog> 245 - <DialogTrigger render={<Button>Create Voucher</Button>} /> 246 - <DialogContent> 247 - <form 248 - onSubmit={(e) => { 249 - e.preventDefault(); 250 - const formData = new FormData(e.currentTarget); 251 - const name = formData.get("name") as string; 252 - const description = formData.get("description") as string; 253 - createVoucher(group.id, name, description).then((res) => { 254 - if (!res) return; 255 - setVouchers((prevVouchers) => [...prevVouchers, res]); 256 - }); 257 - }} 258 - > 259 - <FieldSet> 260 - <FieldLegend>New voucher</FieldLegend> 261 - <FieldDescription> 262 - What's the name and description of your new voucher? You can 263 - always change this later. 264 - </FieldDescription> 265 - <FieldGroup> 266 - <Field> 267 - <FieldLabel htmlFor="name">Name</FieldLabel> 268 - <Input 269 - id="name" 270 - name="name" 271 - placeholder="My voucher" 272 - required 273 - /> 274 - </Field> 275 - <Field> 276 - <FieldLabel htmlFor="description">Description</FieldLabel> 277 - <Input 278 - id="description" 279 - name="description" 280 - placeholder="This voucher can be used to redeem 1 free coffee." 281 - required 282 - /> 283 - </Field> 284 - <Field> 285 - <DialogClose 286 - render={<Button type="submit">Create</Button>} 287 - /> 288 - </Field> 289 - </FieldGroup> 290 - </FieldSet> 291 - </form> 292 - </DialogContent> 293 - </Dialog> 286 + {group.owner.id === user.id && ( 287 + <Dialog> 288 + <DialogTrigger render={<Button>Create Voucher</Button>} /> 289 + <DialogContent> 290 + <form 291 + onSubmit={(e) => { 292 + e.preventDefault(); 293 + const formData = new FormData(e.currentTarget); 294 + const name = formData.get("name") as string; 295 + const description = formData.get("description") as string; 296 + createVoucher(group.id, name, description).then((res) => { 297 + if (!res) return; 298 + setVouchers((prevVouchers) => [...prevVouchers, res]); 299 + }); 300 + }} 301 + > 302 + <FieldSet> 303 + <FieldLegend>New voucher</FieldLegend> 304 + <FieldDescription> 305 + What's the name and description of your new voucher? You 306 + can always change this later. 307 + </FieldDescription> 308 + <FieldGroup> 309 + <Field> 310 + <FieldLabel htmlFor="name">Name</FieldLabel> 311 + <Input 312 + id="name" 313 + name="name" 314 + placeholder="My voucher" 315 + required 316 + /> 317 + </Field> 318 + <Field> 319 + <FieldLabel htmlFor="description"> 320 + Description 321 + </FieldLabel> 322 + <Input 323 + id="description" 324 + name="description" 325 + placeholder="This voucher can be used to redeem 1 free coffee." 326 + required 327 + /> 328 + </Field> 329 + <Field> 330 + <DialogClose 331 + render={<Button type="submit">Create</Button>} 332 + /> 333 + </Field> 334 + </FieldGroup> 335 + </FieldSet> 336 + </form> 337 + </DialogContent> 338 + </Dialog> 339 + )} 294 340 </ItemGroup> 295 341 </CardContent> 296 342 </Card>
+2
src/lib/email.ts
··· 1 + "use server"; 2 + 1 3 import nodemailer from "nodemailer"; 2 4 3 5 interface SendEmailParams {