Skip to content

Commit 910f0d0

Browse files
committed
refactor: restructure frontend code out of app/ into src/
Move all shared frontend code from app/[locale]/(frontend)/ into src/: - src/types/ — domain types (workout, plan, constants) - src/lib/ — class-names, date, metrics (fix trackingTypes shim import) - src/loaders/ — training-plan-loader extracted from page.tsx - src/components/ — all components in kebab-case folders with index barrels page.tsx reduced from ~250 to ~30 lines. Delete src/trackingTypes.ts shim.
1 parent 1bdaff1 commit 910f0d0

33 files changed

Lines changed: 325 additions & 302 deletions

File tree

src/app/[locale]/(frontend)/login/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import { useRouter } from 'next/navigation'
44
import { useTranslations } from 'next-intl'
55
import React, { useState } from 'react'
6-
import { errorBannerClass } from '../ui'
7-
import { Button } from '../components/ui/Button'
8-
import { Input } from '../components/ui/Input'
9-
import { Surface } from '../components/ui/Surface'
6+
import { errorBannerClass } from '@/lib/class-names'
7+
import { Button } from '@/components/ui/button'
8+
import { Input } from '@/components/ui/input'
9+
import { Surface } from '@/components/ui/surface'
1010

1111
export default function LoginPage() {
1212
const t = useTranslations('login')
Lines changed: 11 additions & 222 deletions
Original file line numberDiff line numberDiff line change
@@ -1,245 +1,34 @@
1-
import { headers as getHeaders } from 'next/headers.js'
21
import { redirect } from 'next/navigation'
32
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'
126

137
export default async function HomePage() {
148
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()
10010

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')
22212

22313
return (
22414
<div className="mx-auto max-w-3xl px-5 py-6 sm:px-6 sm:py-8">
22515
<div className="mb-4 flex items-center justify-between gap-3 sm:mb-7 sm:gap-4">
22616
<div className="flex items-center gap-3">
22717
<img src="/images/logo.svg" alt="Logo" className="h-7 w-auto sm:h-8" />
22818
<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>
23022
<span className="mt-0.5 block text-xs text-ui-fg-muted sm:mt-1">{t('yourTrainingPlans')}</span>
23123
</div>
23224
</div>
23325
<LogoutButton />
23426
</div>
23527

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>
24030
)}
241-
242-
{plans.docs.length > 0 && <WorkoutPlansAccordion plans={accordionPlans} />}
31+
{result.plans.length > 0 && <WorkoutPlansAccordion plans={result.plans} />}
24332
</div>
24433
)
24534
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { LogoutButton } from './logout-button'

src/app/[locale]/(frontend)/LogoutButton.tsx renamed to src/components/common/logout-button/logout-button.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import { useRouter } from 'next/navigation'
44
import { useTranslations } from 'next-intl'
55
import React, { useState } from 'react'
6-
import { Button } from './components/ui/Button'
6+
import { Button } from '@/components/ui/button'
77

8-
export default function LogoutButton() {
8+
export function LogoutButton() {
99
const t = useTranslations('nav')
1010
const router = useRouter()
1111
const [loading, setLoading] = useState(false)

src/app/[locale]/(frontend)/components/ui/Button.tsx renamed to src/components/ui/button/button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
joinClasses,
99
primaryButtonClass,
1010
secondaryButtonClass,
11-
} from '../../ui'
11+
} from '@/lib/class-names'
1212

1313
type Variant = 'primary' | 'secondary' | 'dashed' | 'icon' | 'danger' | 'ghost'
1414
type Size = 'md' | 'sm'

src/components/ui/button/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Button } from './button'

src/components/ui/input/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { Input, Select } from './input'

src/app/[locale]/(frontend)/components/ui/Input.tsx renamed to src/components/ui/input/input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
inputClass,
88
joinClasses,
99
selectClass,
10-
} from '../../ui'
10+
} from '@/lib/class-names'
1111

1212
type InputVariant = 'default' | 'compact' | 'compact-unit'
1313

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { StatusBadge } from './status-badge'

src/app/[locale]/(frontend)/components/ui/StatusBadge.tsx renamed to src/components/ui/status-badge/status-badge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react'
2-
import { statusBadgeClass } from '../../ui'
2+
import { statusBadgeClass } from '@/lib/class-names'
33

44
export function StatusBadge({ children, status }: { children: React.ReactNode; status: string }) {
55
return <span className={statusBadgeClass(status)}>{children}</span>

0 commit comments

Comments
 (0)