Skip to content

Commit c6937e9

Browse files
committed
Refactor code structure for improved readability and maintainability
1 parent f7f3401 commit c6937e9

18 files changed

Lines changed: 101 additions & 50 deletions

File tree

.claude/agents/app-builder.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ import { createId } from '@paralleldrive/cuid2'
206206

207207
// Next.js
208208
import { revalidatePath } from 'next/cache'
209-
import { notFound } from 'next/navigation'
209+
import { notFound, redirect } from 'next/navigation'
210210
import Link from 'next/link'
211211

212212
// UI — always use the barrel import, never sub-paths

.claude/rules/server-actions.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,29 @@ import { myTable } from '../../../apps/<appId>/db/schema' // ✓
3636
import { myTable } from '@/apps/<appId>/db/schema' // ✗ no alias
3737
```
3838
39+
## Delete and update actions
40+
41+
Both delete and update actions must redirect after `revalidatePath`, otherwise the deleted item's detail page re-renders and triggers `notFound()` (delete), or the user stays stuck on the edit form with no feedback (update):
42+
43+
```typescript
44+
export async function deleteItem(id: string) {
45+
const db = getDb()
46+
await db.delete(myTable).where(eq(myTable.id, id))
47+
revalidatePath('/apps/<appId>')
48+
redirect('/apps/<appId>')
49+
}
50+
51+
export async function updateItem(id: string, formData: FormData) {
52+
const db = getDb()
53+
await db.update(myTable)...
54+
revalidatePath('/apps/<appId>')
55+
revalidatePath(`/apps/<appId>/${id}`)
56+
redirect('/apps/<appId>')
57+
}
58+
```
59+
60+
Import `redirect` from `'next/navigation'`.
61+
3962
## FormData conventions
4063

4164
- Use `formData.get('fieldName') as string` — field name must match the HTML `name` attribute exactly.

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
<div align="center">
22

3-
# 🧩 AppAtelier
3+
<picture>
4+
<source media="(prefers-color-scheme: dark)" srcset="screenshots/logo.png">
5+
<img src="screenshots/logo.png" alt="AppAtelier" width="140" />
6+
</picture>
7+
8+
# AppAtelier
49

510
**Build your own private app store. One repo, one deploy, infinite installable PWAs — described in plain language to a team of AI agents.**
611

@@ -25,6 +30,22 @@ One repo. One Vercel project. One wildcard DNS record. Every app you'll ever cre
2530

2631
---
2732

33+
## Preview
34+
35+
<p align="center">
36+
<img src="screenshots/hub.png" alt="AppAtelier Hub — web launcher showing all your installed PWAs as an icon grid" width="90%" />
37+
</p>
38+
39+
<p align="center">
40+
<img src="screenshots/tasks.png" alt="Tasks PWA — task manager app with priorities and due dates" width="45%" />
41+
&nbsp;&nbsp;
42+
<img src="screenshots/habits.png" alt="Habits PWA — habit tracker with daily streak counting" width="45%" />
43+
</p>
44+
45+
*The hub (top) shows your launcher at `yourdomain.com`. Below: Tasks and Habits — two of the four included example PWAs, each at its own subdomain and individually installable. All shown on mobile where they're designed to be installed and used.*
46+
47+
---
48+
2849
## Quick start
2950

3051
> **Requirements:** Node.js ≥20, pnpm ≥10, [Claude Code](https://claude.com/claude-code) (for the AI studio).

app/apps/_template/actions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use server'
22

33
import { revalidatePath } from 'next/cache'
4+
import { redirect } from 'next/navigation'
45
import { getDb } from '@hub/db'
56
import { items } from '../../../apps/__APP_ID__/db/schema'
67
import { eq } from 'drizzle-orm'
@@ -19,10 +20,12 @@ export async function updateItem(id: string, formData: FormData) {
1920
await db.update(items).set({ title, updatedAt: new Date() }).where(eq(items.id, id))
2021
revalidatePath('/apps/__APP_ID__')
2122
revalidatePath(`/apps/__APP_ID__/${id}`)
23+
redirect('/apps/__APP_ID__')
2224
}
2325

2426
export async function deleteItem(id: string) {
2527
const db = getDb()
2628
await db.delete(items).where(eq(items.id, id))
2729
revalidatePath('/apps/__APP_ID__')
30+
redirect('/apps/__APP_ID__')
2831
}

app/apps/habits/[id]/page.tsx

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export default async function HabitPage({ params }: { params: Promise<{ id: stri
5858
<AppContainer>
5959
<PageHeader backHref="/apps/habits" />
6060

61-
<form action={updateHabitWithId} className="space-y-4" id="edit-form">
61+
<form action={updateHabitWithId} className="space-y-4">
6262
<div
6363
className="w-4 h-4 rounded-full"
6464
style={{ backgroundColor: habit.color }}
@@ -107,22 +107,21 @@ export default async function HabitPage({ params }: { params: Promise<{ id: stri
107107
))}
108108
</div>
109109
</div>
110-
</form>
111110

112-
<div className="flex items-center justify-between pt-4 border-t border-zinc-800 mt-4">
113-
<DeleteButton
114-
formAction={deleteHabit.bind(null, id)}
115-
confirmMessage="Delete this habit and all its entries?"
116-
/>
117-
<button
118-
type="submit"
119-
form="edit-form"
120-
className="text-zinc-950 text-sm font-semibold px-4 py-1.5 rounded-lg transition-colors hover:opacity-90"
121-
style={{ backgroundColor: habit.color }}
122-
>
123-
Save
124-
</button>
125-
</div>
111+
<div className="flex items-center justify-between pt-4 border-t border-zinc-800 mt-4">
112+
<DeleteButton
113+
formAction={deleteHabit.bind(null, id)}
114+
confirmMessage="Delete this habit and all its entries?"
115+
/>
116+
<button
117+
type="submit"
118+
className="text-zinc-950 text-sm font-semibold px-4 py-1.5 rounded-lg transition-colors hover:opacity-90"
119+
style={{ backgroundColor: habit.color }}
120+
>
121+
Save
122+
</button>
123+
</div>
124+
</form>
126125
</AppContainer>
127126
)
128127
}

app/apps/habits/actions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use server'
22

33
import { revalidatePath } from 'next/cache'
4+
import { redirect } from 'next/navigation'
45
import { getDb } from '@hub/db'
56
import { habits, entries } from '../../../apps/habits/db/schema'
67
import { eq, and } from 'drizzle-orm'
@@ -37,13 +38,15 @@ export async function updateHabit(id: string, formData: FormData) {
3738

3839
revalidatePath('/apps/habits')
3940
revalidatePath(`/apps/habits/${id}`)
41+
redirect('/apps/habits')
4042
}
4143

4244
export async function deleteHabit(id: string) {
4345
const db = getDb()
4446
await db.delete(entries).where(eq(entries.habitId, id))
4547
await db.delete(habits).where(eq(habits.id, id))
4648
revalidatePath('/apps/habits')
49+
redirect('/apps/habits')
4750
}
4851

4952
export async function toggleEntry(habitId: string, date: string) {

app/apps/notes/actions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use server'
22

33
import { revalidatePath } from 'next/cache'
4+
import { redirect } from 'next/navigation'
45
import { getDb } from '@hub/db'
56
import { notes } from '../../../apps/notes/db/schema'
67
import { eq } from 'drizzle-orm'
@@ -34,10 +35,12 @@ export async function updateNote(id: string, formData: FormData) {
3435

3536
revalidatePath('/apps/notes')
3637
revalidatePath(`/apps/notes/${id}`)
38+
redirect('/apps/notes')
3739
}
3840

3941
export async function deleteNote(id: string) {
4042
const db = getDb()
4143
await db.delete(notes).where(eq(notes.id, id))
4244
revalidatePath('/apps/notes')
45+
redirect('/apps/notes')
4346
}

app/apps/tasks/actions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use server'
22

33
import { revalidatePath } from 'next/cache'
4+
import { redirect } from 'next/navigation'
45
import { getDb } from '@hub/db'
56
import { tasks } from '../../../apps/tasks/db/schema'
67
import { eq, sql } from 'drizzle-orm'
@@ -55,10 +56,12 @@ export async function updateTask(id: string, formData: FormData) {
5556

5657
revalidatePath('/apps/tasks')
5758
revalidatePath(`/apps/tasks/${id}`)
59+
redirect('/apps/tasks')
5860
}
5961

6062
export async function deleteTask(id: string) {
6163
const db = getDb()
6264
await db.delete(tasks).where(eq(tasks.id, id))
6365
revalidatePath('/apps/tasks')
66+
redirect('/apps/tasks')
6467
}

packages/db/src/adapters/d1.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
/// <reference types="@cloudflare/workers-types" />
2-
import { drizzle } from 'drizzle-orm/d1'
32

4-
// D1 requires the CF Worker binding passed per-request (from CF Pages/Worker env).
5-
// In a Next.js on Cloudflare Pages context, set globalThis.__D1_BINDING__ = env.DB
6-
// in your CF Pages Function entry point before calling the Next.js handler.
73
export function getDb(d1Binding?: D1Database) {
4+
const { drizzle } = require('drizzle-orm/d1') as typeof import('drizzle-orm/d1')
5+
86
const binding =
97
d1Binding ?? (globalThis as unknown as { __D1_BINDING__?: D1Database }).__D1_BINDING__
108
if (!binding) {

packages/db/src/adapters/mysql.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { drizzle } from 'drizzle-orm/mysql2'
2-
import mysql from 'mysql2/promise'
1+
import type { MySql2Database } from 'drizzle-orm/mysql2'
32

4-
let _db: ReturnType<typeof drizzle> | null = null
3+
let _db: MySql2Database<Record<string, never>> | null = null
54

65
export function getDb() {
76
if (!_db) {
7+
const mysql = require('mysql2/promise')
8+
const { drizzle } = require('drizzle-orm/mysql2') as typeof import('drizzle-orm/mysql2')
89
const pool = mysql.createPool(process.env.DATABASE_URL!)
910
_db = drizzle(pool as any)
1011
}

0 commit comments

Comments
 (0)