Skip to content

Commit bc1a60e

Browse files
committed
feat: add join workshop with access code
1 parent 375556a commit bc1a60e

File tree

12 files changed

+271
-19
lines changed

12 files changed

+271
-19
lines changed

README.md

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# [Byte](https://byte-liart.vercel.app)
1+
# [Byte](https://byte.raphico.tech)
22

33
A platform for developers to plan and manage virtual workshops
44

@@ -32,12 +32,11 @@ The architecture of this project revolves around a streamlined development and d
3232
- [x] Add auth with lucia
3333
- [x] Workshop creation
3434
- [x] Edit workshop
35-
- [x] Complete workshop page
3635
- [ ] Interactive workshop session(e.g video conferencing, live chat code editor)
3736
- [x] Delete workshop
3837
- [x] Participant registration
39-
- [ ] Join workshops with workshop code
40-
- [ ] Complete workshop & dashboard page
38+
- [x] Join workshops with workshop code
39+
- [x] Complete workshop & dashboard page
4140

4241
## Running locally
4342

src/app/(app)/dashboard/page.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export default async function DashboardPage() {
3434
<PageHeaderHeading>Upcoming</PageHeaderHeading>
3535
</PageHeader>
3636

37-
<CreateJoinWorkshopDropdown />
37+
<CreateJoinWorkshopDropdown userId={user.id} />
3838
</div>
3939

4040
{workshops.length ? (

src/app/(app)/explore/page.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { type Metadata } from "next"
2+
import { redirect } from "next/navigation"
23
import { env } from "@/env"
34

5+
import { redirects } from "@/config/constants"
6+
import { getUserSession } from "@/server/data/user"
47
import { getWorkshops } from "@/server/data/workshop"
58
import { EmptyShell } from "@/components/empty-shell"
69
import { PageHeader, PageHeaderHeading } from "@/components/page-header"
@@ -16,6 +19,12 @@ export const metadata: Metadata = {
1619
}
1720

1821
export default async function DashboardPage() {
22+
const { user } = await getUserSession()
23+
24+
if (!user) {
25+
redirect(redirects.toLogin)
26+
}
27+
1928
const workshops = await getWorkshops()
2029

2130
return (
@@ -25,7 +34,7 @@ export default async function DashboardPage() {
2534
<PageHeaderHeading>Explore</PageHeaderHeading>
2635
</PageHeader>
2736

28-
<CreateJoinWorkshopDropdown />
37+
<CreateJoinWorkshopDropdown userId={user.id} />
2938
</div>
3039
{workshops.length ? (
3140
<section className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">

src/app/(app)/workshop/[workshopId]/_components/workshop-registrants.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export function WorkshopRegistrants({ registrants }: WorkshopRegistrantsProps) {
1616
return (
1717
<div className="flex -space-x-4 rtl:space-x-reverse">
1818
{displayedRegistrants.map((registrant) => (
19-
<Avatar key={registrant.id}>
19+
<Avatar className="size-8" key={registrant.id}>
2020
<AvatarImage
2121
src={registrant.image ?? ""}
2222
alt={`@${registrant.username}`}
@@ -27,7 +27,7 @@ export function WorkshopRegistrants({ registrants }: WorkshopRegistrantsProps) {
2727
</Avatar>
2828
))}
2929

30-
{extraRegistrantsCount && (
30+
{extraRegistrantsCount !== 0 && (
3131
<div className="z-50 flex size-10 items-center justify-center rounded-full bg-muted text-xs font-medium text-white">
3232
+{extraRegistrantsCount}
3333
</div>

src/app/(app)/workshop/[workshopId]/loading.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export default function WorkshopLoading() {
3434
</div>
3535
<div className="flex -space-x-4 rtl:space-x-reverse">
3636
{Array.from({ length: 4 }).map((_, i) => (
37-
<Skeleton key={i} className="size-10 rounded-full" />
37+
<Skeleton key={i} className="size-6 rounded-full" />
3838
))}
3939
</div>
4040
</div>

src/components/workshops/create-join-workshop-dropdown.tsx

+20-2
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,30 @@ import {
1313
import { Icons } from "@/components/icons"
1414

1515
import { useCreateEditWorkshopModal } from "./create-edit-workshop-modal"
16+
import { useJoinWorkshopWithCodeModal } from "./join-workshop-with-code-modal"
1617

17-
export function CreateJoinWorkshopDropdown() {
18+
interface CreateJoinWorkshopDropdownProps {
19+
userId: string
20+
}
21+
22+
export function CreateJoinWorkshopDropdown({
23+
userId,
24+
}: CreateJoinWorkshopDropdownProps) {
1825
const [open, setOpen] = React.useState(false)
1926
const { setShowCreateEditWorkshopModal, CreateEditWorkshopModal } =
2027
useCreateEditWorkshopModal({
2128
text: "Create",
2229
})
2330

31+
const { JoinWorkshopWithCodeModal, setShowJoinWorkshopWithCodeModal } =
32+
useJoinWorkshopWithCodeModal({
33+
userId,
34+
})
35+
2436
return (
2537
<>
2638
<CreateEditWorkshopModal />
39+
<JoinWorkshopWithCodeModal />
2740
<DropdownMenu open={open} onOpenChange={setOpen}>
2841
<DropdownMenuTrigger asChild>
2942
<Button
@@ -47,7 +60,12 @@ export function CreateJoinWorkshopDropdown() {
4760
Create Workshop
4861
</DropdownMenuItem>
4962
<DropdownMenuSeparator />
50-
<DropdownMenuItem>
63+
<DropdownMenuItem
64+
onClick={() => {
65+
setOpen(false)
66+
setShowJoinWorkshopWithCodeModal(true)
67+
}}
68+
>
5169
<Icons.video className="mr-2 size-4" aria-hidden="true" />
5270
Join Workshop
5371
</DropdownMenuItem>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as React from "react"
2+
import { type UseFormReturn } from "react-hook-form"
3+
4+
import { cn } from "@/lib/utils"
5+
import { type JoinWorkshopWithCodeSchema } from "@/lib/zod/schemas/workshops"
6+
7+
import { Form, FormControl, FormField, FormItem, FormMessage } from "../ui/form"
8+
import { Input } from "../ui/input"
9+
10+
interface JoinWorkshopWithCodeFormProps
11+
extends Omit<React.ComponentPropsWithoutRef<"form">, "onSubmit"> {
12+
form: UseFormReturn<JoinWorkshopWithCodeSchema>
13+
onSubmit: (values: JoinWorkshopWithCodeSchema) => void
14+
}
15+
16+
export function JoinWorkshopWithCodeForm({
17+
form,
18+
onSubmit,
19+
children,
20+
className,
21+
...props
22+
}: JoinWorkshopWithCodeFormProps) {
23+
return (
24+
<Form {...form}>
25+
<form
26+
className={cn("grid gap-4", className)}
27+
onSubmit={form.handleSubmit(onSubmit)}
28+
{...props}
29+
>
30+
<FormField
31+
control={form.control}
32+
name="accessCode"
33+
render={({ field }) => (
34+
<FormItem>
35+
<FormControl>
36+
<Input placeholder="Workshop access code" {...field} />
37+
</FormControl>
38+
<FormMessage />
39+
</FormItem>
40+
)}
41+
/>
42+
43+
{children}
44+
</form>
45+
</Form>
46+
)
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import * as React from "react"
2+
import { zodResolver } from "@hookform/resolvers/zod"
3+
import { useForm } from "react-hook-form"
4+
import { toast } from "sonner"
5+
6+
import { registerUserWithAccessCode } from "@/server/actions/registration"
7+
import {
8+
joinWorkshopWithCodeSchema,
9+
type JoinWorkshopWithCodeSchema,
10+
} from "@/lib/zod/schemas/workshops"
11+
import { showErrorToast } from "@/utils/handle-error"
12+
13+
import { Icons } from "../icons"
14+
import { Button } from "../ui/button"
15+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog"
16+
import { JoinWorkshopWithCodeForm } from "./join-workshop-with-code-form"
17+
18+
interface JoinWorkshopWithCodeModalProps {
19+
userId: string
20+
}
21+
22+
export function JoinWorkshopWithCodeModal({
23+
showJoinWorkshopWithCodeModal,
24+
setShowJoinWorkshopWithCodeModal,
25+
props,
26+
}: {
27+
showJoinWorkshopWithCodeModal: boolean
28+
setShowJoinWorkshopWithCodeModal: React.Dispatch<
29+
React.SetStateAction<boolean>
30+
>
31+
props: JoinWorkshopWithCodeModalProps
32+
}) {
33+
const [isPending, startTransition] = React.useTransition()
34+
35+
const form = useForm<JoinWorkshopWithCodeSchema>({
36+
resolver: zodResolver(joinWorkshopWithCodeSchema),
37+
defaultValues: {
38+
accessCode: "",
39+
},
40+
})
41+
42+
const onSubmit = (values: JoinWorkshopWithCodeSchema) => {
43+
startTransition(async () => {
44+
const { error } = await registerUserWithAccessCode({
45+
userId: props.userId,
46+
accessCode: values.accessCode,
47+
})
48+
49+
if (error) {
50+
showErrorToast(error)
51+
}
52+
53+
setShowJoinWorkshopWithCodeModal(false)
54+
toast.success("Registration Successful")
55+
form.reset()
56+
})
57+
}
58+
59+
return (
60+
<Dialog
61+
open={showJoinWorkshopWithCodeModal}
62+
onOpenChange={setShowJoinWorkshopWithCodeModal}
63+
>
64+
<DialogContent className="max-w-sm">
65+
<DialogHeader className="items-center">
66+
<DialogTitle className="text-base">Join Workshop</DialogTitle>
67+
</DialogHeader>
68+
<JoinWorkshopWithCodeForm onSubmit={onSubmit} form={form}>
69+
<Button disabled={isPending}>
70+
{isPending && (
71+
<Icons.spinner
72+
className="mr-2 size-4 animate-spin"
73+
aria-hidden="true"
74+
/>
75+
)}
76+
Join
77+
</Button>
78+
</JoinWorkshopWithCodeForm>
79+
</DialogContent>
80+
</Dialog>
81+
)
82+
}
83+
84+
export function useJoinWorkshopWithCodeModal(
85+
props: JoinWorkshopWithCodeModalProps
86+
) {
87+
const [showJoinWorkshopWithCodeModal, setShowJoinWorkshopWithCodeModal] =
88+
React.useState(false)
89+
90+
const JoinWorkshopWithCodeModalCallback = React.useCallback(
91+
() => (
92+
<JoinWorkshopWithCodeModal
93+
showJoinWorkshopWithCodeModal={showJoinWorkshopWithCodeModal}
94+
setShowJoinWorkshopWithCodeModal={setShowJoinWorkshopWithCodeModal}
95+
props={props}
96+
/>
97+
),
98+
[showJoinWorkshopWithCodeModal, props]
99+
)
100+
101+
return React.useMemo(
102+
() => ({
103+
showJoinWorkshopWithCodeModal,
104+
setShowJoinWorkshopWithCodeModal,
105+
JoinWorkshopWithCodeModal: JoinWorkshopWithCodeModalCallback,
106+
}),
107+
[JoinWorkshopWithCodeModalCallback, showJoinWorkshopWithCodeModal]
108+
)
109+
}

src/lib/zod/schemas/registration.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { z } from "zod"
2+
3+
export const registrationSchema = z.object({
4+
userId: z.string(),
5+
workshopId: z.string(),
6+
})
7+
8+
export const registerUserWithAccessCode = z.object({
9+
userId: registrationSchema.shape.userId,
10+
accessCode: z
11+
.string()
12+
.min(8)
13+
.max(8)
14+
.describe("The access code to the workshop"),
15+
})
16+
17+
export type RegisterUserWithAccessCode = z.infer<
18+
typeof registerUserWithAccessCode
19+
>
20+
export type RegistrationSchema = z.infer<typeof registrationSchema>

src/lib/zod/schemas/workshops.ts

+7
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,11 @@ export const createEditWorkshopSchema = z.object({
4141
.describe("The workshop scheduled date"),
4242
})
4343

44+
export const joinWorkshopWithCodeSchema = z.object({
45+
accessCode: createEditWorkshopSchema.shape.accessCode,
46+
})
47+
48+
export type JoinWorkshopWithCodeSchema = z.infer<
49+
typeof joinWorkshopWithCodeSchema
50+
>
4451
export type CreateEditWorkshopSchema = z.infer<typeof createEditWorkshopSchema>

src/server/actions/registration.ts

+44-6
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,55 @@
33
import { revalidateTag } from "next/cache"
44
import { eq } from "drizzle-orm"
55

6+
import {
7+
type RegisterUserWithAccessCode,
8+
type RegistrationSchema,
9+
} from "@/lib/zod/schemas/registration"
610
import { getErrorMessage } from "@/utils/handle-error"
711

812
import { db } from "../db"
9-
import { registrations } from "../db/schema"
13+
import { registrations, workshops } from "../db/schema"
1014

11-
interface RegistrationProps {
12-
workshopId: string
13-
userId: string
15+
export async function registerUserWithAccessCode(
16+
input: RegisterUserWithAccessCode
17+
) {
18+
try {
19+
const workshop = await db.query.workshops.findFirst({
20+
columns: {
21+
id: true,
22+
organizerId: true,
23+
},
24+
where: eq(workshops.accessCode, input.accessCode),
25+
})
26+
27+
if (!workshop) {
28+
throw new Error("Workshop not found")
29+
}
30+
31+
if ((workshop.organizerId = input.userId)) {
32+
throw new Error("You cannot register for your own workshop")
33+
}
34+
35+
const { error } = await registerUserAction({
36+
userId: input.userId,
37+
workshopId: workshop.id,
38+
})
39+
40+
if (error) {
41+
throw new Error(error)
42+
}
43+
44+
return {
45+
error: null,
46+
}
47+
} catch (err) {
48+
return {
49+
error: getErrorMessage(err),
50+
}
51+
}
1452
}
1553

16-
export async function registerUserAction(input: RegistrationProps) {
54+
export async function registerUserAction(input: RegistrationSchema) {
1755
try {
1856
await db.insert(registrations).values({
1957
registrantId: input.userId,
@@ -32,7 +70,7 @@ export async function registerUserAction(input: RegistrationProps) {
3270
}
3371
}
3472

35-
export async function cancelRegistrationAction(input: RegistrationProps) {
73+
export async function cancelRegistrationAction(input: RegistrationSchema) {
3674
try {
3775
await db
3876
.delete(registrations)

0 commit comments

Comments
 (0)