feat: workout + exercise route with store, front end fetching with suspend

This commit is contained in:
Yehoshua Sandler 2025-05-19 19:00:24 -05:00
parent 553ca30d12
commit db89c3e551
13 changed files with 289 additions and 51 deletions

View File

@ -0,0 +1,45 @@
'use client'
import { SidebarLeft } from '@/components/sidebar-left'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
} from '@/components/ui/breadcrumb'
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
import { Separator } from '@radix-ui/react-separator'
import { SidebarRight } from '@/components/sidebar-right'
import { DashboardContent } from '@/components/Dashboard'
import { ReactNode } from 'react'
type Props = {
children: ReactNode
}
// TODO: invesigate error blocking this from being serverside
const DashboardLayout = ({ children }: Props) => {
return (
<SidebarProvider>
<SidebarLeft />
<SidebarInset>
<header className="sticky top-0 flex h-14 shrink-0 items-center gap-2 bg-background">
<div className="flex flex-1 items-center gap-2 px-3">
<SidebarTrigger />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbPage className="line-clamp-1">Dashboard</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
{children}
</SidebarInset>
<SidebarRight />
</SidebarProvider>
)
}
export default DashboardLayout

View File

@ -1,21 +1,19 @@
'use client'
import { DashboardContentSection } from '@/components/Dashboard'
import { Tenant, User } from '@/payload-types'
import useGlobal from '@/stores'
type Props = {
user?: User
tenant?: Tenant
}
const DashboardPageClient = (props?: Props) => {
const DashboardPageClient = () => {
const { user, tenant } = useGlobal()
return (
<div className="mx-auto h-24 w-full max-w-3xl rounded-xl bg-muted/50">
<p>Testing Dashboard Zustand Data Here</p>
<p>{user?.email}</p>
<p>{tenant?.name}</p>
</div>
<DashboardContentSection>
<p>Dashboard</p>
<p>Username: {user?.username}</p>
<p>User Email: {user?.email}</p>
<p>Tenant: {tenant?.name}</p>
<p>Tenant Slug: {tenant?.slug}</p>
</DashboardContentSection>
)
}

View File

@ -1,41 +1,5 @@
import { SidebarLeft } from '@/components/sidebar-left'
import { SidebarRight } from '@/components/sidebar-right'
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
} from '@/components/ui/breadcrumb'
import { Separator } from '@/components/ui/separator'
import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/components/ui/sidebar'
import DashboardPageClient from './page.client'
export default function Page() {
return (
<SidebarProvider>
<SidebarLeft />
<SidebarInset>
<header className="sticky top-0 flex h-14 shrink-0 items-center gap-2 bg-background">
<div className="flex flex-1 items-center gap-2 px-3">
<SidebarTrigger />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbPage className="line-clamp-1">Dashboard</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<section className="flex flex-1 flex-col gap-4 p-4">
<DashboardPageClient />
<div className="mx-auto h-24 w-full max-w-3xl rounded-xl bg-muted/50" />
<div className="mx-auto h-[100vh] w-full max-w-3xl rounded-xl bg-muted/50" />
</section>
</SidebarInset>
<SidebarRight />
</SidebarProvider>
)
return <DashboardPageClient />
}

View File

@ -0,0 +1,43 @@
'use client'
import { Exercise, ExerciseType, Workout } from '@/payload-types'
import useWorkouts from '@/stores/Workouts'
import { PaginatedDocs } from 'payload'
import { ReactNode, use, useEffect } from 'react'
type Props = {
children: ReactNode
getTenantWorkoutsPromise: Promise<PaginatedDocs<Workout>>
getTenantExercisesPromise: Promise<PaginatedDocs<Exercise>>
getTenantExerciseTypesPromise: Promise<PaginatedDocs<ExerciseType>>
}
const WorkoutsLayoutSuspendedFrontend = (props: Props) => {
const {
children,
getTenantExerciseTypesPromise,
getTenantExercisesPromise,
getTenantWorkoutsPromise,
} = props
const exerciseTypeResponse = use(getTenantExerciseTypesPromise)
const exerciseResponse = use(getTenantExercisesPromise)
const workoutResponse = use(getTenantWorkoutsPromise)
const { setExerciseTypes, setExercises, setWorkouts } = useWorkouts()
useEffect(() => {
if (exerciseTypeResponse?.docs?.length) setExerciseTypes(exerciseTypeResponse)
}, [exerciseTypeResponse])
useEffect(() => {
if (exerciseResponse?.docs?.length) setExercises(exerciseResponse)
}, [exerciseResponse])
useEffect(() => {
if (workoutResponse?.docs?.length) setWorkouts(workoutResponse)
}, [workoutResponse])
return <>{children}</>
}
export default WorkoutsLayoutSuspendedFrontend

View File

@ -0,0 +1,84 @@
import { ReactNode } from 'react'
import configPromise from '@payload-config'
import { getPayload, PaginatedDocs } from 'payload'
import WorkoutsLayoutSuspendedFrontend from './layout.client'
import { Exercise, ExerciseType } from '@/payload-types'
type Props = {
params: Promise<{
tenant?: string
}>
children: ReactNode
}
const WorkoutsLayout = async (props: Props) => {
const { params, children } = props
const { tenant: tenantSlug } = await params
const payload = await getPayload({ config: configPromise })
const getExerciseTypesPromise = payload.find({
collection: 'exerciseTypes',
limit: 50,
depth: 0,
select: {
id: true,
name: true,
},
where: {
'tenant.slug': {
equals: tenantSlug,
},
},
}) as Promise<PaginatedDocs<ExerciseType>>
const getTenantExercisesPromise = payload.find({
collection: 'exercises',
limit: 20,
depth: 0,
select: {
id: true,
name: true,
type: true,
muscleGroup: true,
difficulty: true,
equipmentNeeded: true,
instructions: true,
},
where: {
'tenant.slug': {
equals: tenantSlug,
},
},
}) as Promise<PaginatedDocs<Exercise>>
const getWorkoutsPromise = payload.find({
collection: 'workouts',
limit: 20,
depth: 0,
select: {
id: true,
name: true,
type: true,
difficulty: true,
description: true,
durationMinutes: true,
},
where: {
'tenant.slug': {
equals: tenantSlug,
},
},
}) as Promise<PaginatedDocs<ExerciseType>>
return (
<WorkoutsLayoutSuspendedFrontend
getTenantExerciseTypesPromise={getExerciseTypesPromise}
getTenantExercisesPromise={getTenantExercisesPromise}
getTenantWorkoutsPromise={getWorkoutsPromise}
>
{children}
</WorkoutsLayoutSuspendedFrontend>
)
}
export default WorkoutsLayout

View File

@ -0,0 +1,15 @@
'use client'
import useWorkouts from '@/stores/Workouts'
const WorkoutsPageClient = () => {
const { exerciseTypes, exercises, workouts } = useWorkouts()
console.log({
exerciseTypes,
exercises,
workouts,
})
return <div>Workout page client</div>
}
export default WorkoutsPageClient

View File

@ -0,0 +1,20 @@
import { DashboardContent, DashboardContentSection } from '@/components/Dashboard'
// import WorkoutsPageClient from './page.client'
const WorkoutsPage = () => {
return (
<DashboardContent className="grid grid-cols-2">
<DashboardContentSection className="col-span-1">
<h1>Workouts</h1>
</DashboardContentSection>
<DashboardContentSection className="col-span-1">
<h1>Exercises</h1>
</DashboardContentSection>
<DashboardContentSection className="col-span-2">
<h1>Clients</h1>
</DashboardContentSection>
</DashboardContent>
)
}
export default WorkoutsPage

View File

@ -2,12 +2,12 @@ import configPromise from '@payload-config'
import { getPayload, PaginatedDocs } from 'payload'
import { headers as getHeaders } from 'next/headers.js'
import { ReactNode, Suspense } from 'react'
import RootLayoutSuspenseFrontend from './layout.suspense'
import RootLayoutSuspenseFrontend from './layout.client'
import { Tenant } from '@/payload-types'
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
title: 'Biotracker',
description: 'Developed by Beitzah.Tech',
}
type Props = {

View File

@ -0,0 +1,18 @@
import { cn } from '@/utilities/ui'
import { ClassValue } from 'clsx'
import { ReactNode } from 'react'
type Props = {
children: ReactNode
className?: ClassValue
}
const DashboardContent = (props: Props) => {
const { children, className } = props
return (
<div className={cn('flex flex-1 flex-col gap-4 p-4 mx-auto w-full max-w-5xl', className || '')}>
{children}
</div>
)
}
export default DashboardContent

View File

@ -0,0 +1,19 @@
import { cn } from '@/utilities/ui'
import { ClassValue } from 'clsx'
import { ReactNode } from 'react'
type Props = {
children: ReactNode
className?: ClassValue
}
const DashboardContentSection = (props: Props) => {
const { children, className } = props
return (
<section className={cn('mx-auto w-full p-6 rounded-xl bg-muted/50', className || '')}>
{children}
</section>
)
}
export default DashboardContentSection

View File

@ -0,0 +1,4 @@
import DashboardContent from './DashboardContent'
import DashboardContentSection from './DashboardContentSection'
export { DashboardContent, DashboardContentSection }

28
src/stores/Workouts.ts Normal file
View File

@ -0,0 +1,28 @@
import { create } from 'zustand'
import { Exercise, ExerciseType, Workout } from '@/payload-types'
import { PaginatedDocs } from 'payload'
export type WorkoutsProps = {
exercises?: PaginatedDocs<Exercise>,
exerciseTypes?: PaginatedDocs<ExerciseType>,
workouts?: PaginatedDocs<Workout>,
}
export type WorkoutsMethods = {
setExercises: (exercises?: PaginatedDocs<Exercise>) => void,
setExerciseTypes: (exerciseTypes?: PaginatedDocs<ExerciseType>) => void,
setWorkouts: (workouts?: PaginatedDocs<Workout>) => void,
}
export type WorkoutsStore = WorkoutsProps & WorkoutsMethods
const useWorkouts = create<WorkoutsStore>((set) => ({
exercises: undefined,
exerciseTypes: undefined,
workouts: undefined,
setExercises: (exercises?: PaginatedDocs<Exercise>) => set(() => ({ exercises: exercises })),
setExerciseTypes: (exerciseTypes?: PaginatedDocs<ExerciseType>) => set(() => ({ exerciseTypes: exerciseTypes })),
setWorkouts: (workouts?: PaginatedDocs<Workout>) => set(() => ({ workouts: workouts })),
}))
export default useWorkouts