create-appystack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/bin/index.js +243 -0
- package/package.json +39 -0
- package/template/.claude/skills/recipe/SKILL.md +71 -0
- package/template/.claude/skills/recipe/domains/care-provider-operations.md +185 -0
- package/template/.claude/skills/recipe/domains/youtube-launch-optimizer.md +154 -0
- package/template/.claude/skills/recipe/references/file-crud.md +295 -0
- package/template/.claude/skills/recipe/references/nav-shell.md +233 -0
- package/template/.dockerignore +39 -0
- package/template/.env.example +13 -0
- package/template/.github/workflows/ci.yml +43 -0
- package/template/.husky/pre-commit +1 -0
- package/template/.prettierignore +7 -0
- package/template/.prettierrc +8 -0
- package/template/.vscode/launch.json +59 -0
- package/template/CLAUDE.md +114 -0
- package/template/Dockerfile +56 -0
- package/template/README.md +219 -0
- package/template/client/index.html +13 -0
- package/template/client/package.json +43 -0
- package/template/client/src/App.test.tsx +67 -0
- package/template/client/src/App.tsx +11 -0
- package/template/client/src/components/ErrorFallback.test.tsx +64 -0
- package/template/client/src/components/ErrorFallback.tsx +18 -0
- package/template/client/src/config/env.test.ts +64 -0
- package/template/client/src/config/env.ts +34 -0
- package/template/client/src/contexts/AppContext.test.tsx +81 -0
- package/template/client/src/contexts/AppContext.tsx +52 -0
- package/template/client/src/demo/ContactForm.test.tsx +97 -0
- package/template/client/src/demo/ContactForm.tsx +100 -0
- package/template/client/src/demo/DemoPage.tsx +56 -0
- package/template/client/src/demo/SocketDemo.test.tsx +160 -0
- package/template/client/src/demo/SocketDemo.tsx +65 -0
- package/template/client/src/demo/StatusGrid.test.tsx +181 -0
- package/template/client/src/demo/StatusGrid.tsx +77 -0
- package/template/client/src/demo/TechStackDisplay.test.tsx +63 -0
- package/template/client/src/demo/TechStackDisplay.tsx +75 -0
- package/template/client/src/hooks/useServerStatus.test.ts +133 -0
- package/template/client/src/hooks/useServerStatus.ts +67 -0
- package/template/client/src/hooks/useSocket.test.ts +152 -0
- package/template/client/src/hooks/useSocket.ts +43 -0
- package/template/client/src/lib/utils.test.ts +33 -0
- package/template/client/src/lib/utils.ts +14 -0
- package/template/client/src/main.test.tsx +113 -0
- package/template/client/src/main.tsx +14 -0
- package/template/client/src/pages/LandingPage.test.tsx +30 -0
- package/template/client/src/pages/LandingPage.tsx +29 -0
- package/template/client/src/styles/index.css +50 -0
- package/template/client/src/test/msw/browser.ts +4 -0
- package/template/client/src/test/msw/handlers.ts +12 -0
- package/template/client/src/test/msw/msw-example.test.ts +69 -0
- package/template/client/src/test/msw/server.ts +14 -0
- package/template/client/src/test/setup.ts +10 -0
- package/template/client/src/utils/api.test.ts +79 -0
- package/template/client/src/utils/api.ts +42 -0
- package/template/client/src/vite-env.d.ts +13 -0
- package/template/client/tsconfig.json +17 -0
- package/template/client/vite.config.ts +38 -0
- package/template/client/vitest.config.ts +36 -0
- package/template/docker-compose.yml +19 -0
- package/template/e2e/smoke.test.ts +95 -0
- package/template/e2e/socket.test.ts +96 -0
- package/template/eslint.config.js +2 -0
- package/template/package.json +50 -0
- package/template/playwright.config.ts +14 -0
- package/template/scripts/customize.ts +175 -0
- package/template/server/nodemon.json +5 -0
- package/template/server/package.json +45 -0
- package/template/server/src/app.test.ts +103 -0
- package/template/server/src/config/env.test.ts +97 -0
- package/template/server/src/config/env.ts +29 -0
- package/template/server/src/config/logger.test.ts +58 -0
- package/template/server/src/config/logger.ts +17 -0
- package/template/server/src/helpers/response.test.ts +53 -0
- package/template/server/src/helpers/response.ts +17 -0
- package/template/server/src/index.ts +118 -0
- package/template/server/src/middleware/errorHandler.test.ts +84 -0
- package/template/server/src/middleware/errorHandler.ts +27 -0
- package/template/server/src/middleware/rateLimiter.test.ts +68 -0
- package/template/server/src/middleware/rateLimiter.ts +8 -0
- package/template/server/src/middleware/requestLogger.test.ts +111 -0
- package/template/server/src/middleware/requestLogger.ts +17 -0
- package/template/server/src/middleware/validate.test.ts +213 -0
- package/template/server/src/middleware/validate.ts +23 -0
- package/template/server/src/routes/health.test.ts +17 -0
- package/template/server/src/routes/health.ts +12 -0
- package/template/server/src/routes/info.test.ts +20 -0
- package/template/server/src/routes/info.ts +19 -0
- package/template/server/src/shared.test.ts +53 -0
- package/template/server/src/shutdown.test.ts +98 -0
- package/template/server/src/socket.test.ts +185 -0
- package/template/server/src/static.test.ts +166 -0
- package/template/server/tsconfig.json +16 -0
- package/template/server/vitest.config.ts +22 -0
- package/template/shared/package.json +19 -0
- package/template/shared/src/constants.ts +11 -0
- package/template/shared/src/index.ts +8 -0
- package/template/shared/src/types.ts +33 -0
- package/template/shared/tsconfig.json +10 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Sample: YouTube Launch Optimizer
|
|
2
|
+
|
|
3
|
+
A content production tool for YouTube creators. Manages the full lifecycle of a video from idea through launch — scripts, thumbnails, SEO, and launch checklist tasks.
|
|
4
|
+
|
|
5
|
+
**Use with**: `file-crud` recipe. Optionally combine with `nav-shell` recipe.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Entities
|
|
10
|
+
|
|
11
|
+
### Channel
|
|
12
|
+
A YouTube channel being managed. Most users will have one; power users may manage several.
|
|
13
|
+
|
|
14
|
+
| Field | Type | Notes |
|
|
15
|
+
|-------|------|-------|
|
|
16
|
+
| `name` | string | **namish field** — channel display name |
|
|
17
|
+
| `handle` | string | e.g. `@AppyDave` |
|
|
18
|
+
| `niche` | string | e.g. 'AI Tools', 'Coding Tutorials' |
|
|
19
|
+
| `targetAudience` | string | brief description |
|
|
20
|
+
| `contentPillars` | string[] | e.g. ['Claude Code', 'BMAD Method', 'AppyStack'] |
|
|
21
|
+
|
|
22
|
+
Namish field: `name`
|
|
23
|
+
Example filename: `appydave-a1b2c.json`
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
### Video
|
|
28
|
+
A video being produced. Central entity — most other entities link to a video.
|
|
29
|
+
|
|
30
|
+
| Field | Type | Notes |
|
|
31
|
+
|-------|------|-------|
|
|
32
|
+
| `title` | string | **namish field** — working title |
|
|
33
|
+
| `channelId` | string | FK → Channel |
|
|
34
|
+
| `status` | string | 'idea' / 'scripting' / 'recorded' / 'editing' / 'ready' / 'published' |
|
|
35
|
+
| `publishDate` | string | ISO date, optional |
|
|
36
|
+
| `youtubeId` | string | once published |
|
|
37
|
+
| `hooks` | string[] | attention-grabbing opening lines |
|
|
38
|
+
| `seoKeyword` | string | primary keyword |
|
|
39
|
+
| `description` | string | YouTube description |
|
|
40
|
+
| `tags` | string[] | |
|
|
41
|
+
|
|
42
|
+
Namish field: `title`
|
|
43
|
+
Example filename: `how-to-build-your-first-appystack-app-k3p7r.json`
|
|
44
|
+
Relationship: `channelId` → Channel
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
### Script
|
|
49
|
+
The script or detailed outline for a video.
|
|
50
|
+
|
|
51
|
+
| Field | Type | Notes |
|
|
52
|
+
|-------|------|-------|
|
|
53
|
+
| `title` | string | **namish field** — usually matches video title |
|
|
54
|
+
| `videoId` | string | FK → Video |
|
|
55
|
+
| `version` | number | allows multiple script iterations |
|
|
56
|
+
| `hook` | string | opening line |
|
|
57
|
+
| `sections` | object[] | array of `{ heading, content, duration }` |
|
|
58
|
+
| `cta` | string | call to action |
|
|
59
|
+
| `totalDuration` | number | estimated minutes |
|
|
60
|
+
|
|
61
|
+
Namish field: `title` + version composite → `how-to-build-appystack-v2-m9x4k.json`
|
|
62
|
+
Relationship: `videoId` → Video
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### ThumbnailVariant
|
|
67
|
+
A thumbnail concept or A/B test variant for a video.
|
|
68
|
+
|
|
69
|
+
| Field | Type | Notes |
|
|
70
|
+
|-------|------|-------|
|
|
71
|
+
| `label` | string | **namish field** — e.g. 'Face + text', 'Curiosity gap', 'Before/After' |
|
|
72
|
+
| `videoId` | string | FK → Video |
|
|
73
|
+
| `concept` | string | description of the visual idea |
|
|
74
|
+
| `mainText` | string | large text on thumbnail |
|
|
75
|
+
| `subText` | string | smaller supporting text |
|
|
76
|
+
| `emotion` | string | the feeling to convey |
|
|
77
|
+
| `selected` | boolean | which variant was used |
|
|
78
|
+
| `ctrEstimate` | number | optional estimated CTR % |
|
|
79
|
+
|
|
80
|
+
Namish field: `label`
|
|
81
|
+
Example filename: `face-plus-text-variant-p2m8n.json`
|
|
82
|
+
Relationship: `videoId` → Video
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### LaunchTask
|
|
87
|
+
A checklist item to complete before or after publishing a video.
|
|
88
|
+
|
|
89
|
+
| Field | Type | Notes |
|
|
90
|
+
|-------|------|-------|
|
|
91
|
+
| `title` | string | **namish field** — task description |
|
|
92
|
+
| `videoId` | string | FK → Video |
|
|
93
|
+
| `category` | string | 'pre-launch' / 'day-of' / 'post-launch' |
|
|
94
|
+
| `completed` | boolean | |
|
|
95
|
+
| `dueDate` | string | optional |
|
|
96
|
+
| `notes` | string | optional |
|
|
97
|
+
|
|
98
|
+
Namish field: `title`
|
|
99
|
+
Example filename: `upload-thumbnail-to-youtube-studio-r7k2m.json`
|
|
100
|
+
Relationship: `videoId` → Video
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Entity Classification
|
|
105
|
+
|
|
106
|
+
| Entity | Type | Notes |
|
|
107
|
+
|--------|------|-------|
|
|
108
|
+
| Channel | System / configuration | Set up once |
|
|
109
|
+
| Video | Domain / operational | Core working entity |
|
|
110
|
+
| Script | Domain / operational | Created per video, iterated |
|
|
111
|
+
| ThumbnailVariant | Domain / operational | Multiple per video for A/B |
|
|
112
|
+
| LaunchTask | Domain / operational | Checklist items per video |
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Suggested Nav Mapping (for nav-shell recipe)
|
|
117
|
+
|
|
118
|
+
| Nav Item | View Key | Entity | Tier |
|
|
119
|
+
|----------|----------|--------|------|
|
|
120
|
+
| Dashboard | `dashboard` | — (videos by status) | primary |
|
|
121
|
+
| Videos | `videos` | Video | primary |
|
|
122
|
+
| Scripts | `scripts` | Script | primary |
|
|
123
|
+
| Thumbnails | `thumbnails` | ThumbnailVariant | primary |
|
|
124
|
+
| Launch | `launch` | LaunchTask | primary |
|
|
125
|
+
| Channels | `channels` | Channel | secondary |
|
|
126
|
+
|
|
127
|
+
**Context-aware nav suggestion**: When viewing a specific Video, the sidebar could switch to show Script, Thumbnails, and LaunchTasks for that video (context-aware menu pattern from nav-shell recipe).
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Data Folder Structure
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
data/
|
|
135
|
+
├── channels/
|
|
136
|
+
│ └── appydave-a1b2c.json
|
|
137
|
+
├── videos/
|
|
138
|
+
│ └── how-to-build-your-first-appystack-app-k3p7r.json
|
|
139
|
+
├── scripts/
|
|
140
|
+
│ └── how-to-build-appystack-v2-m9x4k.json
|
|
141
|
+
├── thumbnail-variants/
|
|
142
|
+
│ └── face-plus-text-variant-p2m8n.json
|
|
143
|
+
└── launch-tasks/
|
|
144
|
+
└── upload-thumbnail-to-youtube-studio-r7k2m.json
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Notes
|
|
150
|
+
|
|
151
|
+
- Videos are the central entity. Most other entities have a `videoId` FK — consider always showing which video context you're in.
|
|
152
|
+
- The context-aware menu pattern from `nav-shell` is especially useful here: when you open a Video's detail view, the sidebar can switch to show that video's Scripts, Thumbnails, and LaunchTasks directly.
|
|
153
|
+
- `LaunchTask` items could be seeded from a template for every new video (same checklist each time). This is a developer-implemented pattern; the recipe scaffolds the entity, not the seeding logic.
|
|
154
|
+
- `ThumbnailVariant` with `selected: true` indicates which variant was actually used. Only one should be selected per video.
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# Recipe: File-Based JSON Persistence
|
|
2
|
+
|
|
3
|
+
Multiple entities stored as individual JSON files on the server filesystem. Socket.IO bridges client actions to the filesystem and broadcasts changes back to all connected clients in real time. No database required — the `data/` folder IS the database.
|
|
4
|
+
|
|
5
|
+
This recipe suits local tools, small-team apps, and rapid prototyping. It also enables a **local-first workflow**: users work with local files, then publish to a central database when ready (see Local-First Pattern below).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Recipe Anatomy
|
|
10
|
+
|
|
11
|
+
**Intent**
|
|
12
|
+
Scaffold server-side JSON file persistence for one or more domain entities, with real-time Socket.io sync to connected clients. Each record is a human-readable JSON file. All clients see updates live.
|
|
13
|
+
|
|
14
|
+
**Type**: Seed for initial entities. Migration-friendly for adding new entities (additive, non-destructive). Run once with all entities upfront, or entity-by-entity.
|
|
15
|
+
|
|
16
|
+
**Stack Assumptions**
|
|
17
|
+
- Express 5, Socket.io, TypeScript, Node.js `fs/promises`
|
|
18
|
+
- chokidar (verify in `server/package.json`; add if missing)
|
|
19
|
+
- `data/` folder at repo root
|
|
20
|
+
|
|
21
|
+
**Idempotency Check**
|
|
22
|
+
Does `server/src/data/fileStore.ts` exist? If yes → infrastructure is already installed. Only generate entity-specific route and type files for new entities.
|
|
23
|
+
|
|
24
|
+
**Does Not Touch**
|
|
25
|
+
- `client/` — client-side views are the shell recipe's or developer's concern
|
|
26
|
+
- `server/src/index.ts` — instructs developer to mount the generated routes; does not rewrite it
|
|
27
|
+
- Authentication or authorization
|
|
28
|
+
- Entity relationships (foreign keys, join queries) — flagged as TODOs in generated type stubs
|
|
29
|
+
- Contents of `data/` — creates folder structure only
|
|
30
|
+
|
|
31
|
+
**Composes With**
|
|
32
|
+
- `nav-shell` recipe — each entity can have a nav item; views switch to entity CRUD screens
|
|
33
|
+
- `domain-preparation` step — before combining shell + persistence, collect entity definitions once
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Folder Structure
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
project-root/
|
|
41
|
+
├── data/ ← at repo root (not inside client or server)
|
|
42
|
+
│ └── {entity-plural}/ ← one folder per entity (e.g. companies/, sites/)
|
|
43
|
+
│ └── {name-slug}-{5char}.json ← individual records
|
|
44
|
+
├── server/src/
|
|
45
|
+
│ ├── data/
|
|
46
|
+
│ │ ├── fileStore.ts ← read/write/delete individual record files
|
|
47
|
+
│ │ ├── idgen.ts ← 5-char alphanum ID generator
|
|
48
|
+
│ │ └── watcher.ts ← chokidar watcher, emits Socket.io events on change
|
|
49
|
+
│ ├── routes/
|
|
50
|
+
│ │ └── {entity}.ts ← REST endpoints (initial loads + server-to-server)
|
|
51
|
+
│ └── sockets/
|
|
52
|
+
│ └── {entity}Handlers.ts ← Socket.io event handlers per entity
|
|
53
|
+
└── shared/src/types/
|
|
54
|
+
├── entity.ts ← base EntityRecord, EntityIndex types
|
|
55
|
+
└── {entity}.ts ← entity-specific field types (generated stubs)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## File Naming Convention and Primary Key
|
|
61
|
+
|
|
62
|
+
**The 5-char alphanumeric ID is the record's primary key.** It is embedded in the filename, generated once at creation, and never changes — even when the record's name changes.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
File name pattern: {name-slug}-{5char-id}.json
|
|
66
|
+
|
|
67
|
+
Examples:
|
|
68
|
+
name = "NERO Banana" → nero-banana-a7k3p.json
|
|
69
|
+
name = "David Kruwys" → david-kruwys-a1f4q.json (ID: a1f4q)
|
|
70
|
+
name corrected to "David Cruwys" → david-cruwys-a1f4q.json (same ID, new slug)
|
|
71
|
+
name = "Acme Corp" → acme-corp-x9q2m.json
|
|
72
|
+
name = "St. Mary's Group Home" → st-marys-group-home-k3p7r.json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Why this matters**: If the name changes, the slug prefix in the filename changes but the 5-char suffix stays constant. The ID is how you find, update, and delete a record regardless of what the name is. This is the stable identity; the slug is the human label.
|
|
76
|
+
|
|
77
|
+
**Why this avoids Git merge conflicts**: Two users creating different records produce two different files. No shared file is written per-operation (unlike a stored `index.json`). Two users can create, rename, or delete different records simultaneously and push to the same Git branch with zero conflicts.
|
|
78
|
+
|
|
79
|
+
### Slug Rules
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
- Take the entity's "namish" field (usually `name`; sometimes a composite or alternative field)
|
|
83
|
+
- Lowercase
|
|
84
|
+
- Replace spaces and special characters with hyphens
|
|
85
|
+
- Collapse multiple hyphens to one
|
|
86
|
+
- Strip leading/trailing hyphens
|
|
87
|
+
- Maximum 40 characters (truncate without breaking mid-word where possible)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### ID Generation
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// server/src/data/idgen.ts
|
|
94
|
+
export function generateId(): string {
|
|
95
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
|
96
|
+
return Array.from({ length: 5 }, () =>
|
|
97
|
+
chars[Math.floor(Math.random() * chars.length)]
|
|
98
|
+
).join('')
|
|
99
|
+
}
|
|
100
|
+
// Collision probability at 1,000 records: ~0.015% — acceptable for local/small-team use
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Index Strategy: Inferred (Not Stored)
|
|
106
|
+
|
|
107
|
+
**There is no `index.json` file.** The list of records is built by reading the entity folder at request time and optionally reading each file for summary fields. This is inferred, not stored.
|
|
108
|
+
|
|
109
|
+
**Why**: A stored `index.json` is written on every create, rename, and delete — it becomes a conflict magnet when multiple users work simultaneously on Git. With inferred indexing, each user's changes touch only their own files.
|
|
110
|
+
|
|
111
|
+
**Performance**: Folder scan at 1,000 records = ~5-20ms (filenames only). Reading all files for summary fields = ~50-200ms. Acceptable for local/small-team tools. Cache mitigates repeated calls.
|
|
112
|
+
|
|
113
|
+
### In-Memory Cache
|
|
114
|
+
|
|
115
|
+
The server maintains a per-entity index cache. chokidar invalidates it on any file change.
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// server/src/data/fileStore.ts
|
|
119
|
+
|
|
120
|
+
const indexCache = new Map<string, EntityIndex[]>()
|
|
121
|
+
|
|
122
|
+
export async function listRecords(entity: string): Promise<EntityIndex[]> {
|
|
123
|
+
if (indexCache.has(entity)) return indexCache.get(entity)!
|
|
124
|
+
|
|
125
|
+
const folder = path.join('./data', entity)
|
|
126
|
+
const files = (await fs.readdir(folder)).filter(f => f.endsWith('.json'))
|
|
127
|
+
|
|
128
|
+
const index = await Promise.all(files.map(async (filename) => {
|
|
129
|
+
const id = extractId(filename) // last segment before .json
|
|
130
|
+
const slug = extractSlug(filename) // everything before the last -xxxxx
|
|
131
|
+
const content = JSON.parse(await fs.readFile(path.join(folder, filename), 'utf-8'))
|
|
132
|
+
return { id, name: content.name ?? slugToName(slug), filename }
|
|
133
|
+
}))
|
|
134
|
+
|
|
135
|
+
indexCache.set(entity, index)
|
|
136
|
+
return index
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Called by chokidar on any change in data/
|
|
140
|
+
export function invalidateCache(entity: string) {
|
|
141
|
+
indexCache.delete(entity)
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Shared Types
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// shared/src/types/entity.ts
|
|
151
|
+
|
|
152
|
+
export interface EntityRecord {
|
|
153
|
+
id: string // extracted from filename (primary key), NOT stored in the file body
|
|
154
|
+
name: string // the "namish" field — used to derive the filename slug
|
|
155
|
+
[key: string]: unknown
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface EntityIndex {
|
|
159
|
+
id: string
|
|
160
|
+
name: string
|
|
161
|
+
filename: string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function extractId(filename: string): string {
|
|
165
|
+
return filename.replace(/\.json$/, '').split('-').pop() ?? ''
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Entity-specific types extend `EntityRecord`:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// shared/src/types/company.ts
|
|
173
|
+
export interface Company extends EntityRecord {
|
|
174
|
+
name: string
|
|
175
|
+
// TODO: add domain-specific fields
|
|
176
|
+
// TODO: add relationship FKs if needed (e.g. siteIds: string[])
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Socket.io Events
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// Client → Server (commands)
|
|
186
|
+
socket.emit('entity:list', { entity: 'company' })
|
|
187
|
+
socket.emit('entity:get', { entity: 'company', id: 'a7k3p' })
|
|
188
|
+
socket.emit('entity:save', { entity: 'company', record: { name: 'NERO Banana', ...fields } })
|
|
189
|
+
// record.id present → update; record.id absent → create
|
|
190
|
+
socket.emit('entity:delete', { entity: 'company', id: 'a7k3p' })
|
|
191
|
+
|
|
192
|
+
// Server → requesting client
|
|
193
|
+
socket.emit('entity:list:result', { entity: 'company', records: EntityIndex[] })
|
|
194
|
+
socket.emit('entity:get:result', { entity: 'company', record: EntityRecord })
|
|
195
|
+
|
|
196
|
+
// Server → ALL clients (broadcast on state change)
|
|
197
|
+
io.emit('entity:created', { entity: 'company', record: EntityRecord, index: EntityIndex })
|
|
198
|
+
io.emit('entity:updated', { entity: 'company', record: EntityRecord, index: EntityIndex })
|
|
199
|
+
io.emit('entity:deleted', { entity: 'company', id: string })
|
|
200
|
+
|
|
201
|
+
// Server → ALL clients (chokidar: external file change detected)
|
|
202
|
+
io.emit('entity:external-change', { entity: 'company', changeType: 'add'|'change'|'unlink', id: string })
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**`entity:created` vs `entity:updated` are separate events.** The client UI often treats them differently — scroll to new record, highlight on update, different toast messages.
|
|
206
|
+
|
|
207
|
+
**REST endpoints** (`server/src/routes/{entity}.ts`) are generated alongside Socket.io handlers. REST is useful for initial page loads, debugging, health checks, and server-to-server calls. Socket.io is the primary real-time path.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## CRUD Operations Summary
|
|
212
|
+
|
|
213
|
+
| Operation | Client emits | Server does | Server broadcasts |
|
|
214
|
+
|-----------|-------------|-------------|-------------------|
|
|
215
|
+
| List | `entity:list` | reads folder, builds index (cached) | `entity:list:result` to requester |
|
|
216
|
+
| Get | `entity:get` | reads record file | `entity:get:result` to requester |
|
|
217
|
+
| Create | `entity:save` (no id) | generates id, writes file, invalidates cache | `entity:created` to all |
|
|
218
|
+
| Update | `entity:save` (with id) | renames file if name changed, writes, invalidates cache | `entity:updated` to all |
|
|
219
|
+
| Delete | `entity:delete` | deletes file, invalidates cache | `entity:deleted` to all |
|
|
220
|
+
| External change | — | chokidar detects file change | `entity:external-change` to all |
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Multi-Entity Support
|
|
225
|
+
|
|
226
|
+
When the recipe is applied for multiple entities (e.g. Company, Site, User, Incident):
|
|
227
|
+
|
|
228
|
+
**What the recipe generates:**
|
|
229
|
+
- `server/src/data/fileStore.ts` — entity-agnostic (handles any entity name as a string param)
|
|
230
|
+
- `server/src/data/idgen.ts` — shared ID generator
|
|
231
|
+
- `server/src/data/watcher.ts` — watches `data/` folder, entity-agnostic
|
|
232
|
+
- Per entity: `server/src/sockets/{entity}Handlers.ts`
|
|
233
|
+
- Per entity: `server/src/routes/{entity}.ts`
|
|
234
|
+
- Per entity: `shared/src/types/{entity}.ts` — stub with TODO comments
|
|
235
|
+
|
|
236
|
+
**What remains the developer's job:**
|
|
237
|
+
- Define actual field shapes in entity type stubs
|
|
238
|
+
- Implement entity relationship lookups (foreign key references)
|
|
239
|
+
- Mount generated routes in `server/src/index.ts`
|
|
240
|
+
- Add validation rules for entity-specific business logic
|
|
241
|
+
- Build client-side view components (`useEntity` hook is generated; views are not — unless shell recipe ran first)
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Local-First Pattern
|
|
246
|
+
|
|
247
|
+
The file-based persistence layer also enables a local-first workflow:
|
|
248
|
+
|
|
249
|
+
**The pattern:**
|
|
250
|
+
1. Users run the app locally — all data lives in `data/` as JSON files
|
|
251
|
+
2. Multiple users can work simultaneously (different records = different files = no conflicts)
|
|
252
|
+
3. When ready, a **publish** action pushes the local JSON data to a central database or API
|
|
253
|
+
4. This can be triggered manually (publish button) or automatically on save
|
|
254
|
+
|
|
255
|
+
**The use case:** Build all application screens and let stakeholders interact with real data shapes before the backend database exists. When the backend is ready, wire up a publish step that sends the local JSON records to it. The frontend never changes — only the persistence layer swaps out.
|
|
256
|
+
|
|
257
|
+
**What the recipe provides:** The local file layer. The publish-to-database step is a separate recipe (`publish-to-db`) that wraps the file layer and adds the sync mechanism. Not included in this recipe.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Sample Data Patterns
|
|
262
|
+
|
|
263
|
+
Domain-specific entity setups for common application types. See the `samples/` folder:
|
|
264
|
+
|
|
265
|
+
- [`samples/support-signal.md`](../samples/support-signal.md) — NDIS/disability support: Company, Site, User, Incident, MomentThatMatters
|
|
266
|
+
- [`samples/youtube-launch-optimizer.md`](../samples/youtube-launch-optimizer.md) — YouTube content: Channel, Video, Script, ThumbnailVariant, LaunchTask
|
|
267
|
+
|
|
268
|
+
Each sample defines: entity names, namish fields, key domain fields, relationships, and suggested nav mapping. Use a sample as the input to the recipe's entity questions.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Alternative Persistence Recipes
|
|
273
|
+
|
|
274
|
+
These are separate recipes for when file-based JSON is not the right fit:
|
|
275
|
+
|
|
276
|
+
| Recipe | When to Use |
|
|
277
|
+
|--------|-------------|
|
|
278
|
+
| `file-csv` | Humans need to edit data in Excel/Numbers; export to external systems; compliance reporting |
|
|
279
|
+
| `sqlite-drizzle` | Entity count grows large (thousands+); complex queries; referential integrity required |
|
|
280
|
+
| `in-memory` | Development and testing only; full Socket.io loop without touching the filesystem |
|
|
281
|
+
|
|
282
|
+
The Socket.io event spec is identical across all persistence recipes. The client never knows whether the server is reading files, a database, or memory. Persistence is swappable.
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## What to Generate in the Build Prompt
|
|
287
|
+
|
|
288
|
+
When generating the prompt for this recipe, collect:
|
|
289
|
+
|
|
290
|
+
1. **Entity names** — what entities does the app need? (e.g. Company, Site, User)
|
|
291
|
+
2. **Namish field** — for each entity, what field is used to name/slug the file? (usually `name`; sometimes a composite)
|
|
292
|
+
3. **Key fields** — domain-specific fields per entity
|
|
293
|
+
4. **Relationships** — which entities reference others? → adds TODO FK comments to type stubs
|
|
294
|
+
5. **Status field?** — does each entity need an active/inactive or draft/published status?
|
|
295
|
+
6. **Gitignore `data/`?** — recommended: yes for production data, no for seeded sample data
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Recipe: Visual Shell
|
|
2
|
+
|
|
3
|
+
A collapsible left-sidebar navigation shell with header, main content area, and optional footer/status bar. Clicking a nav item switches the tool rendered in the content area. Menus can change dynamically when a sub-tool is active. Domain-agnostic — the shell knows about layout and navigation only.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Recipe Anatomy
|
|
8
|
+
|
|
9
|
+
**Intent**
|
|
10
|
+
Scaffold the application's structural container. The shell provides layout, navigation state, and view switching. It has no knowledge of data, entities, or business logic — those are filled in later.
|
|
11
|
+
|
|
12
|
+
**Type**: Seed — apply once to a new project. Re-applying is guarded by idempotency check.
|
|
13
|
+
|
|
14
|
+
**Stack Assumptions**
|
|
15
|
+
- React 19, TypeScript, TailwindCSS v4
|
|
16
|
+
- No additional libraries required beyond what AppyStack ships with
|
|
17
|
+
|
|
18
|
+
**Idempotency Check**
|
|
19
|
+
Does `client/src/components/AppShell.tsx` exist? If yes → PRESENT, skip unless `--force`.
|
|
20
|
+
|
|
21
|
+
**Does Not Touch**
|
|
22
|
+
- `client/src/App.tsx` beyond mounting AppShell
|
|
23
|
+
- `server/` — this is client-only
|
|
24
|
+
- `data/` folder
|
|
25
|
+
- Domain logic inside view components (stubs only)
|
|
26
|
+
- Socket.io wiring (belongs to a data or feature recipe)
|
|
27
|
+
|
|
28
|
+
**Composes With**
|
|
29
|
+
- `file-crud` recipe — persistence recipe populates view stubs with real data
|
|
30
|
+
- `domain-preparation` step — before combining shell + persistence, collect domain context once
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Layout Structure
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
┌────────────────────────────────────────────────────────┐
|
|
38
|
+
│ Header (app name left │ right: actions, settings cog) │
|
|
39
|
+
├──────────────────┬─────────────────────────────────────┤
|
|
40
|
+
│ [≡] Sidebar │ │
|
|
41
|
+
│ │ Content Panel │
|
|
42
|
+
│ ▼ Group Label │ (active view renders here) │
|
|
43
|
+
│ ● Primary │ │
|
|
44
|
+
│ ● Primary │ │
|
|
45
|
+
│ · Secondary │ │
|
|
46
|
+
│ │ │
|
|
47
|
+
│ ▼ Group Label │ │
|
|
48
|
+
│ ● Primary │ │
|
|
49
|
+
│ │ │
|
|
50
|
+
│ [collapse ◀] │ │
|
|
51
|
+
├──────────────────┴─────────────────────────────────────┤
|
|
52
|
+
│ Footer / Status Bar (optional) │
|
|
53
|
+
└────────────────────────────────────────────────────────┘
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
When the sidebar is collapsed, it shrinks to an icon strip (or zero width). A toggle handle is always visible.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Component Structure
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
client/src/
|
|
64
|
+
├── components/
|
|
65
|
+
│ ├── AppShell.tsx ← outer layout, composes header + sidebar + content
|
|
66
|
+
│ ├── Header.tsx ← app title left, actions right (cog, user, etc.)
|
|
67
|
+
│ ├── Sidebar.tsx ← collapsible, renders nav groups and items
|
|
68
|
+
│ ├── SidebarGroup.tsx ← one group: primary items + optional secondary items
|
|
69
|
+
│ └── ContentPanel.tsx ← renders the active view via viewMap
|
|
70
|
+
├── views/ ← one stub per nav item destination
|
|
71
|
+
│ └── [ViewName]View.tsx
|
|
72
|
+
├── config/
|
|
73
|
+
│ └── nav.ts ← static nav config (root-level default nav)
|
|
74
|
+
└── contexts/
|
|
75
|
+
└── NavContext.tsx ← shell state: activeView, collapsed, contextNav
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Nav Config Shape
|
|
81
|
+
|
|
82
|
+
Define nav structure as data, not hardcoded JSX:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// client/src/config/nav.ts
|
|
86
|
+
|
|
87
|
+
export type NavItemTier = 'primary' | 'secondary'
|
|
88
|
+
|
|
89
|
+
export interface NavItem {
|
|
90
|
+
key: string // unique view key, used by ContentPanel
|
|
91
|
+
label: string // display text
|
|
92
|
+
icon?: string // optional icon identifier
|
|
93
|
+
tier?: NavItemTier // defaults to 'primary' if omitted
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface NavGroup {
|
|
97
|
+
label: string
|
|
98
|
+
items: NavItem[]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type NavConfig = NavGroup[]
|
|
102
|
+
|
|
103
|
+
export const navConfig: NavConfig = [
|
|
104
|
+
{
|
|
105
|
+
label: 'Main',
|
|
106
|
+
items: [
|
|
107
|
+
{ key: 'dashboard', label: 'Dashboard', tier: 'primary' },
|
|
108
|
+
{ key: 'settings', label: 'Settings', tier: 'secondary' },
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Primary items** are the main actions — rendered prominently. **Secondary items** render smaller or in a visual sub-cluster within the group, for less-frequently-used actions.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## State Model
|
|
119
|
+
|
|
120
|
+
All shell state lives in `NavContext`:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// client/src/contexts/NavContext.tsx
|
|
124
|
+
|
|
125
|
+
interface NavContextValue {
|
|
126
|
+
activeView: string // which view is currently rendering
|
|
127
|
+
collapsed: boolean // is the sidebar collapsed?
|
|
128
|
+
contextNav: NavConfig | null // sub-tool override nav; null = use static navConfig
|
|
129
|
+
|
|
130
|
+
navigate: (viewKey: string) => void
|
|
131
|
+
toggleCollapsed: () => void
|
|
132
|
+
setContextNav: (nav: NavConfig | null) => void
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The `collapsed` state controls sidebar width: `collapsed ? 'w-12' : 'w-56'`. Use Tailwind transition utilities for smooth animation. A collapse toggle handle renders at the sidebar bottom (or top) — always visible even when collapsed.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Content Panel Switching
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// ContentPanel.tsx
|
|
144
|
+
const viewMap: Record<string, React.ComponentType> = {
|
|
145
|
+
dashboard: DashboardView,
|
|
146
|
+
settings: SettingsView,
|
|
147
|
+
// one entry per nav item key
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const View = viewMap[activeView] ?? viewMap['dashboard']
|
|
151
|
+
return <View />
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
View stubs are generated empty. Domain logic is filled in separately (by the developer, a data recipe, or a compound recipe step).
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Context-Aware Menus
|
|
159
|
+
|
|
160
|
+
When a sub-tool needs its own nav items, it replaces the sidebar temporarily using `setContextNav`. When the user navigates away, the view unmounts and clears the context nav automatically.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
// Inside a sub-tool view component
|
|
164
|
+
const { setContextNav } = useNav()
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
setContextNav([
|
|
168
|
+
{
|
|
169
|
+
label: 'Incident Tools',
|
|
170
|
+
items: [
|
|
171
|
+
{ key: 'incident-list', label: 'All Incidents', tier: 'primary' },
|
|
172
|
+
{ key: 'incident-create', label: 'New Incident', tier: 'primary' },
|
|
173
|
+
{ key: 'incident-export', label: 'Export', tier: 'secondary' },
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
])
|
|
177
|
+
return () => setContextNav(null) // cleanup: restore static nav on unmount
|
|
178
|
+
}, [])
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The Sidebar renders `contextNav ?? navConfig` — context nav wins when set, falls back to static config when cleared.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Optional Footer / Status Bar
|
|
186
|
+
|
|
187
|
+
If the app has a footer (connection status, version number, last-sync time, etc.), add a `Footer.tsx` component and include it in `AppShell`. The recipe generates a stub if requested. If not needed, `AppShell` simply omits it.
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
client/src/components/Footer.tsx ← optional; only generated if requested
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## What the Recipe Asks at Use-Time
|
|
196
|
+
|
|
197
|
+
Before generating the build prompt, collect:
|
|
198
|
+
|
|
199
|
+
1. **App name** — displayed in the Header
|
|
200
|
+
2. **Main tools / pages** (2–6) — generates view stubs and initial nav items
|
|
201
|
+
3. **Nav groups** — which items group together, what each group is called
|
|
202
|
+
4. **Primary vs secondary** — for each item, is it a primary action or secondary?
|
|
203
|
+
5. **Default view** — which view loads on startup
|
|
204
|
+
6. **Context-aware nav?** — does any tool need its own sidebar menu when active? (Yes → adds `setContextNav` scaffolding to that view stub)
|
|
205
|
+
7. **Footer / status bar?** — yes or no → generates Footer stub if yes
|
|
206
|
+
|
|
207
|
+
The recipe does **not** ask about data models, API calls, or business logic. Those are not its concern.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Other Shell Patterns (Future Recipe Ideas)
|
|
212
|
+
|
|
213
|
+
These are identified but not yet built. Noted here so they're visible when planning.
|
|
214
|
+
|
|
215
|
+
**Top-Nav Shell**
|
|
216
|
+
Horizontal tab or link bar across the top. No sidebar. For apps with 3–5 flat sections of equal weight. Navigation is shallow; no hierarchy needed.
|
|
217
|
+
|
|
218
|
+
**Workspace Shell** (VS Code style)
|
|
219
|
+
Activity bar (icon strip, far left) selects a panel type. The panel area changes completely per activity — not just nav items but full tool panels, file trees, form widgets. Two levels of state: `activeActivity` + `activeView`. Best for tool-heavy apps where each "mode" has its own set of controls (e.g. FliHub's recording workflow vs settings vs file browser).
|
|
220
|
+
|
|
221
|
+
**Dashboard Shell**
|
|
222
|
+
A fixed or responsive grid of widget panels. Each widget independently fetches and displays data. No single `activeView` — multiple views live simultaneously. Per-widget Socket.io subscriptions. Best for monitoring, ops tools, reporting apps where seeing multiple things at once is the goal.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Styling Notes
|
|
227
|
+
|
|
228
|
+
- Sidebar width: `w-56` expanded, `w-12` collapsed (icon-only or just toggle handle)
|
|
229
|
+
- Header height: `h-14`
|
|
230
|
+
- Content panel: fills remaining space, independently scrollable
|
|
231
|
+
- Active nav item: distinct background highlight (CSS variable for theming)
|
|
232
|
+
- Transitions: `transition-all duration-200` on sidebar width change
|
|
233
|
+
- Use TailwindCSS v4 CSS variables for sidebar/header colours — define in `client/src/styles/index.css`
|