Skip to content

Commit 376a4d2

Browse files
committed
refactor: restructure workout-structure admin module
- Extract all payload queries and data transforms into loader.ts - Slim workout-structure.tsx down to guard + render (~20 lines) - Move editor.tsx into components/editor/ folder (Medusa folder-per-component pattern) - Move exercise-form and group-form into their own component folders with index.ts barrels - Extract delete mutation handlers into components/editor/hooks/use-workout-mutations.ts - Add WorkoutStructureData type to types.ts - Move shared textField helper to src/app/(payload)/admin/utils/fields.ts
1 parent b5de61c commit 376a4d2

11 files changed

Lines changed: 180 additions & 152 deletions

File tree

src/app/(payload)/admin/modules/workout-structure/editor.tsx renamed to src/app/(payload)/admin/modules/workout-structure/components/editor/editor.tsx

Lines changed: 13 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
'use client'
22

33
import React, { useState } from 'react'
4-
import { Button, toast } from '@payloadcms/ui'
5-
import { sdk } from '@/lib/sdk'
6-
import type { ExerciseRow, Group, Section } from './types'
7-
import { PROTOCOL_LABEL } from './constants'
8-
import { exerciseLabel, exerciseMeta, groupLabel } from './utils'
9-
import { s } from './styles'
10-
import { GroupForm } from './components/group-form'
11-
import { ExerciseForm } from './components/exercise-form'
4+
import { Button } from '@payloadcms/ui'
5+
import type { ExerciseRow, Group, Section } from '../../types'
6+
import { PROTOCOL_LABEL } from '../../constants'
7+
import { exerciseLabel, exerciseMeta, groupLabel } from '../../utils'
8+
import { s } from '../../styles'
9+
import { GroupForm } from '../group-form'
10+
import { ExerciseForm } from '../exercise-form'
11+
import { useWorkoutMutations } from './hooks/use-workout-mutations'
1212

1313
type Props = {
1414
sections: Section[]
@@ -33,8 +33,9 @@ export function WorkoutStructureEditor({
3333
const [editingGroup, setEditingGroup] = useState<number | null>(null)
3434
const [addingExerciseFor, setAddingExerciseFor] = useState<number | null>(null)
3535
const [editingExercise, setEditingExercise] = useState<number | null>(null)
36-
const [deletingGroup, setDeletingGroup] = useState<number | null>(null)
37-
const [deletingExercise, setDeletingExercise] = useState<number | null>(null)
36+
37+
const { deleteGroup, deleteExercise, deletingGroup, deletingExercise } =
38+
useWorkoutMutations(setGroups, setExerciseRows)
3839

3940
const sectionsWithFallback: Section[] =
4041
sections.length > 0 ? sections : [{ id: undefined, title: null, subtitle: null }]
@@ -45,33 +46,6 @@ export function WorkoutStructureEditor({
4546
const rowsForGroup = (groupId: number) =>
4647
exerciseRows.filter((r) => r.group === groupId).sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
4748

48-
const handleDeleteGroup = async (groupId: number) => {
49-
setDeletingGroup(groupId)
50-
try {
51-
await sdk.delete({ collection: 'workout-groups', id: groupId })
52-
setGroups((prev) => prev.filter((g) => g.id !== groupId))
53-
setExerciseRows((prev) => prev.filter((r) => r.group !== groupId))
54-
toast.success('Grupa usunięta')
55-
} catch (e) {
56-
toast.error(e instanceof Error ? e.message : 'Nie można usunąć grupy')
57-
} finally {
58-
setDeletingGroup(null)
59-
}
60-
}
61-
62-
const handleDeleteExercise = async (rowId: number) => {
63-
setDeletingExercise(rowId)
64-
try {
65-
await sdk.delete({ collection: 'workout-exercise-rows', id: rowId })
66-
setExerciseRows((prev) => prev.filter((r) => r.id !== rowId))
67-
toast.success('Ćwiczenie usunięte')
68-
} catch (e) {
69-
toast.error(e instanceof Error ? e.message : 'Nie można usunąć ćwiczenia')
70-
} finally {
71-
setDeletingExercise(null)
72-
}
73-
}
74-
7549
return (
7650
<div style={s.container}>
7751

@@ -139,7 +113,7 @@ export function WorkoutStructureEditor({
139113
disabled={deletingGroup === group.id}
140114
onClick={() => {
141115
if (confirm('Usunąć grupę i wszystkie jej ćwiczenia?')) {
142-
handleDeleteGroup(group.id)
116+
deleteGroup(group.id)
143117
}
144118
}}
145119
>
@@ -192,7 +166,7 @@ export function WorkoutStructureEditor({
192166
disabled={deletingExercise === row.id}
193167
onClick={() => {
194168
if (confirm(`Usunąć ćwiczenie "${exerciseLabel(row)}"?`)) {
195-
handleDeleteExercise(row.id)
169+
deleteExercise(row.id)
196170
}
197171
}}
198172
>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { toast } from '@payloadcms/ui'
5+
import { sdk } from '@/lib/sdk'
6+
import type { ExerciseRow, Group } from '../../../types'
7+
8+
export function useWorkoutMutations(
9+
setGroups: React.Dispatch<React.SetStateAction<Group[]>>,
10+
setExerciseRows: React.Dispatch<React.SetStateAction<ExerciseRow[]>>,
11+
) {
12+
const [deletingGroup, setDeletingGroup] = useState<number | null>(null)
13+
const [deletingExercise, setDeletingExercise] = useState<number | null>(null)
14+
15+
const deleteGroup = async (groupId: number) => {
16+
setDeletingGroup(groupId)
17+
try {
18+
await sdk.delete({ collection: 'workout-groups', id: groupId })
19+
setGroups((prev) => prev.filter((g) => g.id !== groupId))
20+
setExerciseRows((prev) => prev.filter((r) => r.group !== groupId))
21+
toast.success('Grupa usunięta')
22+
} catch (e) {
23+
toast.error(e instanceof Error ? e.message : 'Nie można usunąć grupy')
24+
} finally {
25+
setDeletingGroup(null)
26+
}
27+
}
28+
29+
const deleteExercise = async (rowId: number) => {
30+
setDeletingExercise(rowId)
31+
try {
32+
await sdk.delete({ collection: 'workout-exercise-rows', id: rowId })
33+
setExerciseRows((prev) => prev.filter((r) => r.id !== rowId))
34+
toast.success('Ćwiczenie usunięte')
35+
} catch (e) {
36+
toast.error(e instanceof Error ? e.message : 'Nie można usunąć ćwiczenia')
37+
} finally {
38+
setDeletingExercise(null)
39+
}
40+
}
41+
42+
return { deleteGroup, deleteExercise, deletingGroup, deletingExercise }
43+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './editor'

src/app/(payload)/admin/modules/workout-structure/components/exercise-form.tsx renamed to src/app/(payload)/admin/modules/workout-structure/components/exercise-form/exercise-form.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
'use client'
22

33
import { Button, Form, RelationshipField, TextField, toast, useFormProcessing } from '@payloadcms/ui'
4-
import type { FormState, SingleRelationshipFieldClient, TextFieldClient, ValueWithRelation } from 'payload'
5-
import type { ExerciseRow } from '../types'
6-
import { s } from '../styles'
4+
import type { FormState, SingleRelationshipFieldClient, ValueWithRelation } from 'payload'
5+
import type { ExerciseRow } from '../../types'
6+
import { s } from '../../styles'
77
import { sdk } from '@/lib/sdk'
8-
import { validateKgOrReps, validateRepsOrKg, validateRounds } from '../utils'
8+
import { textField } from '@/app/(payload)/admin/utils/fields'
9+
import { validateKgOrReps, validateRepsOrKg, validateRounds } from '../../utils'
910

1011
type Props = {
1112
groupId: number
@@ -15,9 +16,6 @@ type Props = {
1516
onCancel: () => void
1617
}
1718

18-
const textField = (name: string, label: string, placeholder?: string): TextFieldClient =>
19-
({ name, label, type: 'text', admin: { placeholder } }) as TextFieldClient
20-
2119
const exerciseRelField: SingleRelationshipFieldClient = {
2220
name: 'exercise',
2321
type: 'relationship',
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './exercise-form'

src/app/(payload)/admin/modules/workout-structure/components/group-form.tsx renamed to src/app/(payload)/admin/modules/workout-structure/components/group-form/group-form.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
'use client'
22

33
import { Button, Form, SelectField, TextField, toast, useDocumentInfo, useFormFields, useFormProcessing } from '@payloadcms/ui'
4-
import type { FormState, SelectFieldClient, TextFieldClient } from 'payload'
5-
import type { Group } from '../types'
6-
import { PROTOCOLS } from '../constants'
7-
import { s } from '../styles'
4+
import type { FormState, SelectFieldClient } from 'payload'
5+
import type { Group } from '../../types'
6+
import { PROTOCOLS } from '../../constants'
7+
import { s } from '../../styles'
88
import type { WorkoutGroup } from '@/payload-types'
99
import { sdk } from '@/lib/sdk'
10+
import { textField } from '@/app/(payload)/admin/utils/fields'
1011
import {
1112
validateDurationMinutes,
1213
validateIntervalSeconds,
1314
validateRestSeconds,
1415
validateRounds,
1516
validateWorkSeconds,
16-
} from '../utils'
17-
18-
const textField = (name: string, label: string, placeholder?: string): TextFieldClient =>
19-
({ name, label, type: 'text', admin: { placeholder } }) as TextFieldClient
17+
} from '../../utils'
2018

2119
const protocolField: SelectFieldClient = {
2220
name: 'protocol',
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './group-form'
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { ExerciseRow, Group, RawExerciseRow, Section, WorkoutStructureData } from './types'
2+
3+
export async function loadWorkoutStructure(
4+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5+
payload: any,
6+
docId: number | string,
7+
): Promise<WorkoutStructureData> {
8+
const workout = await payload.findByID({ collection: 'workouts', id: docId, depth: 0 })
9+
10+
const groupsResult = await payload.find({
11+
collection: 'workout-groups',
12+
where: { workout: { equals: docId } },
13+
sort: 'order',
14+
limit: 500,
15+
depth: 0,
16+
})
17+
18+
const groupIds = groupsResult.docs.map((g: Group) => g.id)
19+
20+
const exerciseRowsResult = groupIds.length
21+
? await payload.find({
22+
collection: 'workout-exercise-rows',
23+
where: { group: { in: groupIds } },
24+
sort: 'order',
25+
limit: 5000,
26+
depth: 1,
27+
})
28+
: { docs: [] }
29+
30+
const exerciseRowIds = exerciseRowsResult.docs.map((r: RawExerciseRow) => r.id)
31+
32+
const [roundLogsResult, setLogsResult] = await Promise.all([
33+
groupIds.length
34+
? payload.find({ collection: 'round-logs', where: { group: { in: groupIds } }, limit: 5000, depth: 0 })
35+
: { docs: [] },
36+
exerciseRowIds.length
37+
? payload.find({ collection: 'set-logs', where: { exerciseRow: { in: exerciseRowIds } }, limit: 5000, depth: 0 })
38+
: { docs: [] },
39+
])
40+
41+
const groupIdsWithLogs: number[] = [
42+
...new Set(
43+
roundLogsResult.docs.map((r: { group: number | { id: number } }) =>
44+
typeof r.group === 'object' ? r.group.id : r.group
45+
) as number[],
46+
),
47+
]
48+
49+
const exerciseRowIdsWithLogs: number[] = [
50+
...new Set(
51+
setLogsResult.docs.map((r: { exerciseRow: number | { id: number } }) =>
52+
typeof r.exerciseRow === 'object' ? r.exerciseRow.id : r.exerciseRow
53+
) as number[],
54+
),
55+
]
56+
57+
const sections: Section[] = (workout.sections ?? []) as Section[]
58+
59+
const initialGroups: Group[] = groupsResult.docs.map((g: Group) => ({
60+
id: g.id,
61+
sectionRowId: g.sectionRowId ?? null,
62+
order: g.order ?? 0,
63+
label: g.label ?? null,
64+
protocol: g.protocol ?? 'standard',
65+
rounds: g.rounds ?? null,
66+
durationMinutes: g.durationMinutes ?? null,
67+
intervalSeconds: g.intervalSeconds ?? null,
68+
workSeconds: g.workSeconds ?? null,
69+
restSeconds: g.restSeconds ?? null,
70+
restBetweenRounds: g.restBetweenRounds ?? null,
71+
}))
72+
73+
const initialExerciseRows: ExerciseRow[] = exerciseRowsResult.docs.map((r: RawExerciseRow) => ({
74+
id: r.id,
75+
group: typeof r.group === 'object' && r.group !== null ? r.group.id : (r.group ?? null),
76+
order: r.order ?? 0,
77+
numer: r.numer ?? null,
78+
rounds: r.rounds ?? null,
79+
exercise:
80+
r.exercise && typeof r.exercise === 'object'
81+
? { id: (r.exercise as { id: number }).id, name: (r.exercise as { name?: string | null }).name ?? null }
82+
: null,
83+
note: r.note ?? null,
84+
reps: r.reps ?? null,
85+
kg: r.kg ?? null,
86+
tut: r.tut ?? null,
87+
rir: r.rir ?? null,
88+
rest: r.rest ?? null,
89+
durationMin: r.durationMin ?? null,
90+
durationSec: r.durationSec ?? null,
91+
}))
92+
93+
return { sections, initialGroups, initialExerciseRows, groupIdsWithLogs, exerciseRowIdsWithLogs }
94+
}

src/app/(payload)/admin/modules/workout-structure/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,11 @@ export type RawExerciseRow = Omit<ExerciseRow, 'group' | 'exercise'> & {
3535
group?: number | { id: number } | null
3636
exercise?: { id: number; name?: string | null } | number | null
3737
}
38+
39+
export type WorkoutStructureData = {
40+
sections: Section[]
41+
initialGroups: Group[]
42+
initialExerciseRows: ExerciseRow[]
43+
groupIdsWithLogs: number[]
44+
exerciseRowIdsWithLogs: number[]
45+
}

0 commit comments

Comments
 (0)