Skip to content

Commit d8e4d41

Browse files
committed
Initial release
Open-source coaching app built with Payload CMS 3 and Next.js 16. Features: - Workout plan hierarchy: Plan → Microcycle → Workout → Group → ExerciseRow - Per-set logging (reps, weight, RIR, time, rounds) - Coach admin panel via Payload CMS - Client-facing mobile-friendly web interface - i18n: Polish and English (next-intl) - PostgreSQL via @payloadcms/db-postgres
0 parents  commit d8e4d41

162 files changed

Lines changed: 43489 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.ai/skills/code-style

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/Applications/XAMPP/xamppfiles/htdocs/loyal/audo-medusa/.ai/skills/code-style
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
---
2+
name: payload-build-collections
3+
description: Step-by-step guide for adding a new Payload CMS collection to this project. Use when asked to create a new collection, add a new data type, or extend the backend schema.
4+
---
5+
6+
# Payload — creating a new collection
7+
8+
Every collection lives in its own folder under `src/collections/{kebab-case}/`. Never create collection files directly inside `src/collections/`.
9+
10+
## Steps
11+
12+
### 1. Create the folder and collection file
13+
14+
```
15+
src/collections/{kebab-case-name}/
16+
├── index.ts # CollectionConfig — always required
17+
└── types.ts # optional: only if the collection has its own domain types
18+
```
19+
20+
Minimal `index.ts`:
21+
22+
```ts
23+
import type { CollectionConfig } from 'payload'
24+
import { isAdmin, isAuthenticated } from '../../access'
25+
26+
export const MyCollection: CollectionConfig = {
27+
slug: 'my-collection',
28+
admin: {
29+
useAsTitle: 'name',
30+
defaultColumns: ['name', 'updatedAt'],
31+
group: 'Panel group name',
32+
},
33+
access: {
34+
create: isAdmin,
35+
read: isAuthenticated,
36+
update: isAdmin,
37+
delete: isAdmin,
38+
},
39+
fields: [
40+
{
41+
name: 'name',
42+
type: 'text',
43+
required: true,
44+
label: 'Name',
45+
},
46+
],
47+
}
48+
```
49+
50+
### 2. Register in the barrel `src/collections/index.ts`
51+
52+
Add one line at the end of the export list:
53+
54+
```ts
55+
export { MyCollection } from './my-collection'
56+
```
57+
58+
`payload.config.ts` imports from this barrel — nothing else changes there.
59+
60+
### 3. Add to the collections array in `payload.config.ts`
61+
62+
```ts
63+
import {
64+
// ... existing
65+
MyCollection,
66+
} from './collections'
67+
68+
export default buildConfig({
69+
collections: [
70+
// ... existing
71+
MyCollection,
72+
],
73+
})
74+
```
75+
76+
---
77+
78+
## Naming conventions
79+
80+
| What | How |
81+
|---|---|
82+
| Folder name | `kebab-case` — must match the collection slug |
83+
| Export name | `PascalCase``export const WorkoutLogs` |
84+
| Collection slug | `kebab-case``'workout-logs'` |
85+
| Collection file | always `index.ts` |
86+
| Domain types | `types.ts` in the same folder |
87+
| Access import | always from `'../../access'` (two levels up) |
88+
89+
---
90+
91+
## Domain types — when and how
92+
93+
Create `types.ts` when the collection has:
94+
- its own enums / select options used in multiple places
95+
- types that the frontend needs to import
96+
97+
```ts
98+
// src/collections/exercises/types.ts
99+
export type TrackingType = 'strength' | 'cardio'
100+
export const TRACKING = { ... }
101+
export const trackingOptions = [...]
102+
```
103+
104+
Import inside the collection's `index.ts`:
105+
```ts
106+
import { trackingOptions, DEFAULT_TRACKING } from './types'
107+
```
108+
109+
Import from the frontend:
110+
```ts
111+
import type { TrackingType } from '@/collections/exercises/types'
112+
```
113+
114+
---
115+
116+
## Access — available functions from `src/access/index.ts`
117+
118+
| Function | When to use |
119+
|---|---|
120+
| `isAdmin` | only trainer/admin can create, edit, delete |
121+
| `isAuthenticated` | any logged-in user can read (admin + client) |
122+
| `adminOrSelf` | admin sees everyone, client sees only themselves (use for the `clients` collection) |
123+
| `adminOrOwnByClient` | admin sees everything, client sees only records where the `client` field points to them |
124+
125+
---
126+
127+
## Payload hooks — where to put them
128+
129+
Simple hooks (1-2 lines of logic) — inline in `index.ts`:
130+
131+
```ts
132+
hooks: {
133+
beforeChange: [
134+
({ data, req }) => {
135+
if (req.user?.collection === 'clients') data.client = req.user.id
136+
return data
137+
},
138+
],
139+
},
140+
```
141+
142+
Complex hooks (custom queries, validations, side effects) — extract to `hooks.ts` in the same folder:
143+
144+
```ts
145+
// src/collections/workout-logs/hooks.ts
146+
export const autoAssignClient: CollectionBeforeChangeHook = async ({ data, req, operation }) => {
147+
// ...
148+
}
149+
```
150+
151+
```ts
152+
// src/collections/workout-logs/index.ts
153+
import { autoAssignClient } from './hooks'
154+
155+
hooks: {
156+
beforeChange: [autoAssignClient],
157+
},
158+
```
159+
160+
---
161+
162+
## Relationships between collections
163+
164+
```ts
165+
{
166+
name: 'client',
167+
type: 'relationship',
168+
relationTo: 'clients', // slug of the target collection
169+
required: true,
170+
label: 'Client',
171+
}
172+
```
173+
174+
Reverse relation (join) — on the parent collection's side:
175+
```ts
176+
{
177+
name: 'plans',
178+
type: 'join',
179+
collection: 'plans',
180+
on: 'client', // the field in `plans` pointing back to this collection
181+
label: 'Client plans',
182+
}
183+
```
184+
185+
---
186+
187+
## Full example — collection with hook, types, and relationships
188+
189+
```
190+
src/collections/progress-photos/
191+
├── index.ts
192+
└── types.ts
193+
```
194+
195+
```ts
196+
// types.ts
197+
export type PhotoAngle = 'front' | 'side' | 'back'
198+
export const PHOTO_ANGLE_OPTIONS: { label: string; value: PhotoAngle }[] = [
199+
{ label: 'Front', value: 'front' },
200+
{ label: 'Side', value: 'side' },
201+
{ label: 'Back', value: 'back' },
202+
]
203+
```
204+
205+
```ts
206+
// index.ts
207+
import type { CollectionConfig } from 'payload'
208+
import { isAdmin, adminOrOwnByClient } from '../../access'
209+
import { PHOTO_ANGLE_OPTIONS } from './types'
210+
211+
export const ProgressPhotos: CollectionConfig = {
212+
slug: 'progress-photos',
213+
admin: {
214+
useAsTitle: 'id',
215+
defaultColumns: ['client', 'angle', 'takenAt'],
216+
group: 'Clients',
217+
},
218+
access: {
219+
create: ({ req: { user } }) => Boolean(user),
220+
read: adminOrOwnByClient,
221+
update: isAdmin,
222+
delete: isAdmin,
223+
},
224+
hooks: {
225+
beforeChange: [
226+
({ data, req }) => {
227+
if (req.user?.collection === 'clients') data.client = req.user.id
228+
return data
229+
},
230+
],
231+
},
232+
fields: [
233+
{
234+
name: 'client',
235+
type: 'relationship',
236+
relationTo: 'clients',
237+
label: 'Client',
238+
defaultValue: ({ user }) => (user?.collection === 'clients' ? user.id : undefined),
239+
},
240+
{
241+
name: 'photo',
242+
type: 'upload',
243+
relationTo: 'media',
244+
required: true,
245+
label: 'Photo',
246+
},
247+
{
248+
name: 'angle',
249+
type: 'select',
250+
label: 'Angle',
251+
options: PHOTO_ANGLE_OPTIONS,
252+
},
253+
{
254+
name: 'takenAt',
255+
type: 'date',
256+
label: 'Date',
257+
admin: { date: { pickerAppearance: 'dayOnly' } },
258+
},
259+
],
260+
}
261+
```
262+
263+
Register in `src/collections/index.ts`:
264+
```ts
265+
export { ProgressPhotos } from './progress-photos'
266+
```
267+
268+
Add to `payload.config.ts`:
269+
```ts
270+
import { ..., ProgressPhotos } from './collections'
271+
272+
collections: [..., ProgressPhotos],
273+
```

0 commit comments

Comments
 (0)