|
1 | | -import { headers as getHeaders } from 'next/headers.js' |
2 | 1 | import { redirect } from 'next/navigation' |
3 | 2 | import { getTranslations } from 'next-intl/server' |
4 | | -import { getPayload } from 'payload' |
5 | | -import React from 'react' |
6 | | - |
7 | | -import config from '@/payload.config' |
8 | | -import LogoutButton from './LogoutButton' |
9 | | -import type { TWorkout } from './WorkoutTracker' |
10 | | -import { WorkoutPlansAccordion, type TPlanAccordionItem } from './components/WorkoutPlansAccordion' |
11 | | -import { STATUS_LABEL } from './types/constants' |
| 3 | +import { loadTrainingPlans } from '@/loaders/training-plan-loader' |
| 4 | +import { WorkoutPlansAccordion } from '@/components/workout/workout-plans-accordion' |
| 5 | +import { LogoutButton } from '@/components/common/logout-button' |
12 | 6 |
|
13 | 7 | export default async function HomePage() { |
14 | 8 | const t = await getTranslations('home') |
15 | | - const headers = await getHeaders() |
16 | | - const payload = await getPayload({ config: await config }) |
17 | | - const { user } = await payload.auth({ headers }) |
18 | | - |
19 | | - if (!user || user.collection !== 'clients') { |
20 | | - redirect('/login') |
21 | | - } |
22 | | - |
23 | | - const plans = await payload.find({ |
24 | | - collection: 'plans', |
25 | | - where: { client: { equals: user.id } }, |
26 | | - sort: '-createdAt', |
27 | | - depth: 0, |
28 | | - limit: 100, |
29 | | - }) |
30 | | - |
31 | | - const planIds = plans.docs.map((p) => p.id) |
32 | | - |
33 | | - const microcycles = planIds.length |
34 | | - ? await payload.find({ |
35 | | - collection: 'microcycles', |
36 | | - where: { plan: { in: planIds } }, |
37 | | - sort: 'order', |
38 | | - depth: 0, |
39 | | - limit: 500, |
40 | | - }) |
41 | | - : { docs: [] } |
42 | | - |
43 | | - const mcIds = microcycles.docs.map((m) => m.id) |
44 | | - |
45 | | - const workouts = mcIds.length |
46 | | - ? await payload.find({ |
47 | | - collection: 'workouts', |
48 | | - where: { microcycle: { in: mcIds } }, |
49 | | - sort: 'order', |
50 | | - depth: 0, |
51 | | - limit: 1000, |
52 | | - }) |
53 | | - : { docs: [] } |
54 | | - |
55 | | - const workoutIds = workouts.docs.map((w) => w.id) |
56 | | - |
57 | | - const workoutGroups = workoutIds.length |
58 | | - ? await payload.find({ |
59 | | - collection: 'workout-groups', |
60 | | - where: { workout: { in: workoutIds } }, |
61 | | - sort: 'order', |
62 | | - depth: 0, |
63 | | - limit: 10000, |
64 | | - }) |
65 | | - : { docs: [] } |
66 | | - |
67 | | - const groupIds = workoutGroups.docs.map((g) => g.id) |
68 | | - |
69 | | - const exerciseRows = groupIds.length |
70 | | - ? await payload.find({ |
71 | | - collection: 'workout-exercise-rows', |
72 | | - where: { group: { in: groupIds } }, |
73 | | - sort: 'order', |
74 | | - depth: 1, |
75 | | - limit: 10000, |
76 | | - }) |
77 | | - : { docs: [] } |
78 | | - |
79 | | - const relId = (rel: unknown): number | string | undefined => |
80 | | - rel && typeof rel === 'object' ? (rel as { id: number | string }).id : (rel as number | string) |
81 | | - |
82 | | - const mcByPlan = (planId: number | string) => |
83 | | - microcycles.docs.filter((m) => relId(m.plan) === planId) |
84 | | - const woByMc = (mcId: number | string) => |
85 | | - workouts.docs.filter((w) => relId(w.microcycle) === mcId) |
86 | | - const groupsByWorkout = (workoutId: number | string) => |
87 | | - workoutGroups.docs.filter((g) => relId(g.workout) === workoutId) |
88 | | - const rowsByGroup = (groupId: number | string) => |
89 | | - exerciseRows.docs.filter((r) => relId(r.group) === groupId) |
90 | | - |
91 | | - const fmtDuration = (min?: number | null, sec?: number | null): string | null => { |
92 | | - const m = min ?? 0 |
93 | | - const s = sec ?? 0 |
94 | | - if (!m && !s) return null |
95 | | - const p: string[] = [] |
96 | | - if (m) p.push(`${m} min`) |
97 | | - if (s) p.push(`${s} sek`) |
98 | | - return p.join(' ') |
99 | | - } |
| 9 | + const result = await loadTrainingPlans() |
100 | 10 |
|
101 | | - const ok = (v?: string | null) => v && v.trim() !== '' && v.trim().toLowerCase() !== 'x' |
102 | | - |
103 | | - const exMeta = (ex: { |
104 | | - rounds?: string | null |
105 | | - reps?: string | null |
106 | | - durationMin?: number | null |
107 | | - durationSec?: number | null |
108 | | - rest?: string | null |
109 | | - tut?: string | null |
110 | | - rir?: string | null |
111 | | - kg?: string | null |
112 | | - }): string[] => { |
113 | | - const parts: string[] = [] |
114 | | - if (ok(ex.rounds)) parts.push(`${t('seriesPrefix')}: ${ex.rounds}`) |
115 | | - if (ok(ex.reps)) parts.push(`${t('repsPrefix')}: ${ex.reps}`) |
116 | | - const dur = fmtDuration(ex.durationMin, ex.durationSec) |
117 | | - if (dur) parts.push(`${t('durationPrefix')}: ${dur}`) |
118 | | - if (ok(ex.rest)) parts.push(`${t('restPrefix')}: ${ex.rest}`) |
119 | | - if (ok(ex.tut)) parts.push(`TUT: ${ex.tut}`) |
120 | | - if (ok(ex.rir)) parts.push(`RIR: ${ex.rir}`) |
121 | | - if (ok(ex.kg)) parts.push(`${ex.kg} kg`) |
122 | | - return parts |
123 | | - } |
124 | | - |
125 | | - const groupLabel = (g: (typeof workoutGroups.docs)[number]): string => { |
126 | | - if (g.label) return g.label as string |
127 | | - const protocol = g.protocol as string |
128 | | - const rounds = g.rounds as string | null | undefined |
129 | | - const durationMinutes = g.durationMinutes as number | null | undefined |
130 | | - |
131 | | - if (protocol === 'emom') return rounds ? `EMOM · ${rounds} min` : 'EMOM' |
132 | | - if (protocol === 'amrap') return durationMinutes ? `AMRAP · ${durationMinutes} min` : 'AMRAP' |
133 | | - if (protocol === 'for_time') return rounds ? `For Time · ${rounds} rund` : 'For Time' |
134 | | - if (protocol === 'tabata') return 'Tabata' |
135 | | - return rounds ? `${rounds} serie` : '' |
136 | | - } |
137 | | - |
138 | | - const serializeWorkout = (w: (typeof workouts.docs)[number]): TWorkout => { |
139 | | - const sections = (w.sections ?? []) as Array<{ |
140 | | - id?: string |
141 | | - title?: string | null |
142 | | - subtitle?: string | null |
143 | | - }> |
144 | | - |
145 | | - const groups = groupsByWorkout(w.id) |
146 | | - |
147 | | - return { |
148 | | - id: w.id, |
149 | | - title: w.title, |
150 | | - rpe: w.rpe ?? null, |
151 | | - sections: sections.map((section) => { |
152 | | - const sectionGroups = groups.filter( |
153 | | - (g) => (g.sectionRowId as string | null | undefined) === section.id, |
154 | | - ) |
155 | | - |
156 | | - return { |
157 | | - title: section.title ?? null, |
158 | | - subtitle: section.subtitle ?? null, |
159 | | - groups: sectionGroups.map((group) => { |
160 | | - const rows = rowsByGroup(group.id) |
161 | | - |
162 | | - return { |
163 | | - protocol: (group.protocol as string) ?? 'standard', |
164 | | - label: groupLabel(group), |
165 | | - exercises: rows.map((ex) => { |
166 | | - const cat = |
167 | | - ex.exercise && typeof ex.exercise === 'object' |
168 | | - ? (ex.exercise as { id: number; name?: string; trackingType?: string; videoUrl?: string }) |
169 | | - : null |
170 | | - const name = cat?.name || ex.note || '' |
171 | | - const extraNote = cat && ex.note && ex.note !== cat.name ? ex.note : null |
172 | | - |
173 | | - return { |
174 | | - rowId: String(ex.id), |
175 | | - numer: (ex.numer as string | null) ?? null, |
176 | | - name, |
177 | | - note: extraNote as string | null, |
178 | | - exerciseId: cat?.id ?? null, |
179 | | - exerciseName: name, |
180 | | - trackingType: cat?.trackingType ?? null, |
181 | | - videoUrl: (cat?.videoUrl as string | null | undefined) ?? null, |
182 | | - rounds: (ex.rounds as string | null) ?? null, |
183 | | - meta: exMeta(ex as Parameters<typeof exMeta>[0]), |
184 | | - prefill: { |
185 | | - reps: (ex.reps as string | null) ?? null, |
186 | | - rir: (ex.rir as string | null) ?? null, |
187 | | - }, |
188 | | - setParameters: |
189 | | - (ex.setParameters as Array<{ setNumber: number; reps?: string | null; kg?: string | null }> | null | undefined) ?? null, |
190 | | - } |
191 | | - }), |
192 | | - } |
193 | | - }), |
194 | | - } |
195 | | - }), |
196 | | - } |
197 | | - } |
198 | | - |
199 | | - const accordionPlans: TPlanAccordionItem[] = plans.docs.map((plan) => { |
200 | | - const status = (plan.status as string) || 'active' |
201 | | - |
202 | | - return { |
203 | | - id: plan.id, |
204 | | - title: plan.title, |
205 | | - status, |
206 | | - statusLabel: STATUS_LABEL[status] || status, |
207 | | - dateRange: |
208 | | - plan.startDate || plan.endDate |
209 | | - ? [plan.startDate, plan.endDate] |
210 | | - .map((d) => (d ? new Date(d).toLocaleDateString('pl-PL') : '…')) |
211 | | - .join(' – ') |
212 | | - : null, |
213 | | - description: plan.description ?? null, |
214 | | - microcycles: mcByPlan(plan.id).map((mc) => ({ |
215 | | - id: mc.id, |
216 | | - title: mc.title, |
217 | | - rpe: mc.rpe ?? null, |
218 | | - workouts: woByMc(mc.id).map((w) => serializeWorkout(w)), |
219 | | - })), |
220 | | - } |
221 | | - }) |
| 11 | + if (!result.user) redirect('/login') |
222 | 12 |
|
223 | 13 | return ( |
224 | 14 | <div className="mx-auto max-w-3xl px-5 py-6 sm:px-6 sm:py-8"> |
225 | 15 | <div className="mb-4 flex items-center justify-between gap-3 sm:mb-7 sm:gap-4"> |
226 | 16 | <div className="flex items-center gap-3"> |
227 | 17 | <img src="/images/logo.svg" alt="Logo" className="h-7 w-auto sm:h-8" /> |
228 | 18 | <div> |
229 | | - <h1 className="text-sm font-semibold text-ui-fg-base sm:text-xl">{t('greeting', { name: user.name || user.email })}</h1> |
| 19 | + <h1 className="text-sm font-semibold text-ui-fg-base sm:text-xl"> |
| 20 | + {t('greeting', { name: result.user.name || result.user.email })} |
| 21 | + </h1> |
230 | 22 | <span className="mt-0.5 block text-xs text-ui-fg-muted sm:mt-1">{t('yourTrainingPlans')}</span> |
231 | 23 | </div> |
232 | 24 | </div> |
233 | 25 | <LogoutButton /> |
234 | 26 | </div> |
235 | 27 |
|
236 | | - {plans.docs.length === 0 && ( |
237 | | - <div className="py-10 text-center text-sm text-ui-fg-muted"> |
238 | | - {t('noPlans')} |
239 | | - </div> |
| 28 | + {result.plans.length === 0 && ( |
| 29 | + <div className="py-10 text-center text-sm text-ui-fg-muted">{t('noPlans')}</div> |
240 | 30 | )} |
241 | | - |
242 | | - {plans.docs.length > 0 && <WorkoutPlansAccordion plans={accordionPlans} />} |
| 31 | + {result.plans.length > 0 && <WorkoutPlansAccordion plans={result.plans} />} |
243 | 32 | </div> |
244 | 33 | ) |
245 | 34 | } |
0 commit comments