Skip to content

Commit

Permalink
feat: add workshop page
Browse files Browse the repository at this point in the history
  • Loading branch information
Raphico committed Jun 16, 2024
1 parent 8763a55 commit d8d3532
Show file tree
Hide file tree
Showing 15 changed files with 390 additions and 346 deletions.
5 changes: 2 additions & 3 deletions src/app/(app)/dashboard/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Skeleton } from "@/components/ui/skeleton"
import { PageHeader, PageHeaderHeading } from "@/components/page-header"
import { Shell } from "@/components/shell"
import { WorkshopSkeletons } from "@/components/workshops/workshop-skeletons"

export default function DashboardLoading() {
return (
Expand All @@ -14,9 +15,7 @@ export default function DashboardLoading() {
</div>

<section className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
<Skeleton className="h-40 rounded-lg" />
<Skeleton className="h-40 rounded-lg" />
<Skeleton className="h-40 rounded-lg" />
<WorkshopSkeletons />
</section>
</Shell>
)
Expand Down
5 changes: 2 additions & 3 deletions src/app/(app)/explore/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Skeleton } from "@/components/ui/skeleton"
import { PageHeader, PageHeaderHeading } from "@/components/page-header"
import { Shell } from "@/components/shell"
import { WorkshopSkeletons } from "@/components/workshops/workshop-skeletons"

export default function ExploreLoading() {
return (
Expand All @@ -14,9 +15,7 @@ export default function ExploreLoading() {
</div>

<section className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
<Skeleton className="h-40 rounded-lg" />
<Skeleton className="h-40 rounded-lg" />
<Skeleton className="h-40 rounded-lg" />
<WorkshopSkeletons />
</section>
</Shell>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Skeleton } from "@/components/ui/skeleton"

export function OrganizerSectionSkeleton() {
return (
<div className="space-y-1">
<h4 className="font-medium sm:text-lg">Organizer</h4>

<div className="flex items-center gap-2">
<Skeleton className="size-8 rounded-full" />

<Skeleton className="h-5 w-10" />
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { getWorkshopOrganizer } from "@/server/data/workshop"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"

interface OrganizerSectionProps {
organizerId: string
}

export async function OrganizerSection({ organizerId }: OrganizerSectionProps) {
const organizer = await getWorkshopOrganizer(organizerId)

const organizerUsernameInitial = organizer?.username.charAt(0)

return (
<div className="space-y-1">
<h4 className="font-medium sm:text-lg">Organizer</h4>

<div className="flex items-center gap-2">
<Avatar className="size-8 bg-muted">
{organizer?.image ? (
<AvatarImage src={organizer.image} />
) : (
<AvatarFallback>{organizerUsernameInitial}</AvatarFallback>
)}
</Avatar>

<p className="text-sm text-muted-foreground">{organizer?.username}</p>
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"use client"

import * as React from "react"

import { type getWorkshop } from "@/server/data/workshop"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Icons } from "@/components/icons"
import { useCreateEditWorkshopModal } from "@/components/workshops/create-edit-workshop-modal"
import { useDeleteWorkshopAlert } from "@/components/workshops/delete-workshop-alert"

interface WorkshopSettingsProps {
workshop: NonNullable<Awaited<ReturnType<typeof getWorkshop>>>
}

export function WorkshopSettings({ workshop }: WorkshopSettingsProps) {
const [open, setOpen] = React.useState(false)

const { setShowCreateEditWorkshopModal, CreateEditWorkshopModal } =
useCreateEditWorkshopModal({
text: "Update",
workshop,
})

const { setShowDeleteWorkshopAlert, DeleteWorkshopAlert } =
useDeleteWorkshopAlert({
id: workshop.id,
})

return (
<>
<CreateEditWorkshopModal />
<DeleteWorkshopAlert />
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="ghost">
<Icons.more className="size-5" aria-hidden="true" />
<span className="sr-only">Workshop Settings Dropdown</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setOpen(false)
setShowCreateEditWorkshopModal(true)
}}
aria-label="Edit Workshop"
>
<Icons.pen className="mr-2 size-4" aria-hidden="true" />
Edit
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
aria-label="Delete Workshop"
onClick={() => {
setOpen(false)
setShowDeleteWorkshopAlert(true)
}}
className="text-red-400"
>
<Icons.trash className="mr-2 size-4" aria-hidden="true" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
)
}
50 changes: 50 additions & 0 deletions src/app/(app)/workshop/[workshopId]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Separator } from "@/components/ui/separator"
import { Skeleton } from "@/components/ui/skeleton"
import { Icons } from "@/components/icons"
import { Shell } from "@/components/shell"

import { OrganizerSectionSkeleton } from "./_components/organizer-section-skeleton"

export default function WorkshopLoading() {
return (
<Shell className="max-w-xl gap-4 sm:mt-8">
<div className="flex w-full flex-col items-start space-y-2">
<div className="flex w-full items-start justify-between">
<Skeleton className="h-7 w-52" />

<div className="flex items-center gap-1">
<Skeleton className="size-4" />
<Skeleton className="size-4" />
</div>
</div>

<div className="flex items-center gap-2">
<Icons.clock
className="size-4 text-muted-foreground"
aria-hidden="true"
/>
<Skeleton className="h-5 w-40" />
</div>
<div className="flex items-center gap-2">
<Icons.watch
className="size-4 text-muted-foreground"
aria-hidden="true"
/>
<Skeleton className="h-5 w-10" />
</div>
</div>

<Separator />

<div className="flex w-full flex-col items-start space-y-4">
<div className="w-full space-y-1">
<h4 className="font-medium sm:text-lg">About</h4>

<Skeleton className="h-20 w-full" />
</div>

<OrganizerSectionSkeleton />
</div>
</Shell>
)
}
15 changes: 15 additions & 0 deletions src/app/(app)/workshop/[workshopId]/not-found.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ErrorShell } from "@/components/error-shell"
import { Shell } from "@/components/shell"

export default function WorkshopNotFound() {
return (
<Shell variant="centered" className="max-w-2xl">
<ErrorShell
title="Workshop not found"
description="Oops! We can't seem to find the workshop you're looking for"
retryLinkText="Explore workshops"
retryLink="/explore"
/>
</Shell>
)
}
113 changes: 113 additions & 0 deletions src/app/(app)/workshop/[workshopId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import * as React from "react"
import { notFound, redirect } from "next/navigation"

import { redirects } from "@/config/constants"
import { getUserSession } from "@/server/data/user"
import { getWorkshop } from "@/server/data/workshop"
import { getExactScheduled } from "@/utils/format-scheduled-date"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { CopyButton } from "@/components/copy-button"
import { Icons } from "@/components/icons"
import { PageHeader, PageHeaderHeading } from "@/components/page-header"
import { Shell } from "@/components/shell"

import { OrganizerSection } from "./_components/organizer-section"
import { OrganizerSectionSkeleton } from "./_components/organizer-section-skeleton"
import { WorkshopSettings } from "./_components/workshop-settings"

interface WorkshopPageProps {
params: {
workshopId: string
}
}

export default async function WorkshopPage({ params }: WorkshopPageProps) {
const workshopId = decodeURIComponent(params.workshopId)

const workshop = await getWorkshop(workshopId)

if (!workshop) {
notFound()
}

const { user } = await getUserSession()

if (!user) {
redirect(redirects.toLogin)
}

const isCurrentUserWorkshop = workshop.organizerId === user.id

return (
<Shell className="max-w-xl gap-4 sm:mt-8">
<div className="flex w-full flex-col items-start space-y-1">
<div className="flex w-full items-start justify-between">
<PageHeader>
<PageHeaderHeading>{workshop.title}</PageHeaderHeading>
</PageHeader>

<div className="flex items-center gap-1">
<CopyButton
value={workshop.accessCode}
size="icon"
className="rounded-full"
/>
{isCurrentUserWorkshop && <WorkshopSettings workshop={workshop} />}
</div>
</div>

<div className="flex items-center gap-2">
<Icons.clock
className="size-4 text-muted-foreground"
aria-hidden="true"
/>
<p className="text-sm text-muted-foreground">
{getExactScheduled(workshop.scheduled)}
</p>
</div>
<div className="flex items-center gap-2">
<Icons.watch
className="size-4 text-muted-foreground"
aria-hidden="true"
/>
<p className="text-sm text-muted-foreground">
{workshop.duration} mins
</p>
</div>
</div>

<Separator />

<div className="flex flex-col items-start space-y-4">
<div className="space-y-1">
<h4 className="font-medium sm:text-lg">About</h4>

<p className="max-w-lg text-sm text-muted-foreground">
{workshop.description}
</p>
</div>

<React.Suspense fallback={<OrganizerSectionSkeleton />}>
<OrganizerSection organizerId={workshop.organizerId} />
</React.Suspense>

<div className="flex w-full justify-end">
{!isCurrentUserWorkshop ? (
<Button size="sm">
{/* {isPending && (
<Icons.spinner
className="mr-2 size-4 animate-spin"
aria-hidden="true"
/>
)} */}
Register
</Button>
) : (
<Button size="sm">Start</Button>
)}
</div>
</div>
</Shell>
)
}
35 changes: 11 additions & 24 deletions src/app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"use client"

import { Button } from "@/components/ui/button"
import { DotBg } from "@/components/dot-bg"
import { Icons } from "@/components/icons"
import { unknownError } from "@/config/constants"
import { ErrorShell } from "@/components/error-shell"
import { Shell } from "@/components/shell"

export default function GlobalError({
error,
Expand All @@ -14,28 +14,15 @@ export default function GlobalError({
return (
<html>
<body>
<main
role="alert"
className="container grid min-h-screen max-w-5xl items-center justify-center gap-6 bg-background pb-8 pt-6 font-sans antialiased md:pb-12 md:pt-10 lg:pb-24 lg:pt-16"
>
<DotBg />
<div className="flex size-full flex-col items-center justify-center space-y-4">
<Icons.warning
className="size-28 text-red-500 dark:text-red-500"
aria-hidden="true"
<main role="alert" className="bg-background font-sans antialiased">
<Shell variant="centered" className="max-w-2xl">
<ErrorShell
title=""
description={error.message ?? unknownError}
icon="warning"
reset={reset}
/>
<h1 className="text-center text-2xl font-bold text-red-500 dark:text-red-500 sm:text-2xl lg:text-3xl">
{error.message ?? "Something went wrong!"}
</h1>
<Button
aria-label="Retry"
variant="outline"
onClick={() => reset()}
>
<Icons.refresh className="mr-2 size-4" aria-hidden="true" />
Retry
</Button>
</div>
</Shell>
</main>
</body>
</html>
Expand Down
Loading

0 comments on commit d8d3532

Please sign in to comment.