umpordez 1.1.0 → 1.3.1
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/.claude/settings.local.json +3 -1
- package/README.md +42 -1
- package/cli.mjs +4 -2
- package/create.mjs +72 -6
- package/package.json +1 -1
- package/template/AGENTS.md +80 -1
- package/template/CLAUDE.md +1 -1
- package/template/misc/build-local.sh +82 -0
- package/template/mobile/{{PROJECT_SLUG}}/App.tsx +29 -13
- package/template/mobile/{{PROJECT_SLUG}}/CHANGELOG.md +28 -0
- package/template/mobile/{{PROJECT_SLUG}}/CLAUDE.md +21 -5
- package/template/mobile/{{PROJECT_SLUG}}/app.json +3 -0
- package/template/mobile/{{PROJECT_SLUG}}/docs/releases.md +78 -0
- package/template/mobile/{{PROJECT_SLUG}}/docs/screenshots.md +84 -0
- package/template/mobile/{{PROJECT_SLUG}}/docs/store-copy.md +122 -0
- package/template/mobile/{{PROJECT_SLUG}}/docs/store-listing.md +107 -0
- package/template/mobile/{{PROJECT_SLUG}}/index.ts +47 -1
- package/template/mobile/{{PROJECT_SLUG}}/package.json +21 -17
- package/template/mobile/{{PROJECT_SLUG}}/src/components/AnimatedSplash.tsx +10 -0
- package/template/mobile/{{PROJECT_SLUG}}/src/config.ts +6 -0
- package/template/mobile/{{PROJECT_SLUG}}/src/contexts/AuthContext.tsx +2 -3
- package/template/scripts/README.md +18 -1
- package/template/scripts/appstore-resize.sh +70 -0
- package/template/scripts/frame-shots.mjs +246 -0
- package/template/scripts/gen-mockups.mjs +120 -0
- package/template/scripts/screenshots-capture.sh +50 -0
- package/template/scripts/screenshots.flow.yaml +54 -0
- package/template/ui/admin/CLAUDE.md +2 -2
- package/template/ui/admin/src/lib/fetch-api.ts +19 -7
- package/template/ui/site/_variants/app/en.ts +131 -0
- package/template/ui/site/_variants/app/footer.ejs +46 -0
- package/template/ui/site/_variants/app/header.ejs +102 -0
- package/template/ui/site/_variants/app/home.ejs +237 -0
- package/template/ui/site/_variants/app/not-found.ejs +22 -0
- package/template/ui/site/_variants/app/pt-BR.ts +203 -0
- package/template/ui/site/tailwind.config.js +9 -1
- package/template-variables.mjs +34 -0
- package/update.mjs +30 -1
|
@@ -46,7 +46,9 @@
|
|
|
46
46
|
"Bash(md5 /tmp/umptest/everything/mobile/testapp/assets/icon.png /Users/ligeiro/dev/tranqs/app/mobile/tranqs/assets/icon.png)",
|
|
47
47
|
"Bash(git merge *)",
|
|
48
48
|
"Bash(git tag *)",
|
|
49
|
-
"Bash(npm pack *)"
|
|
49
|
+
"Bash(npm pack *)",
|
|
50
|
+
"Read(//Users/ligeiro/dev/dropa/**)",
|
|
51
|
+
"Read(//Users/ligeiro/dev/growerheleper-v2/app/**)"
|
|
50
52
|
]
|
|
51
53
|
}
|
|
52
54
|
}
|
package/README.md
CHANGED
|
@@ -98,11 +98,19 @@ It asks **what to generate** first:
|
|
|
98
98
|
|---|---|
|
|
99
99
|
| everything | server + admin UI + site + mobile |
|
|
100
100
|
| web-only | server + admin UI + site (no mobile) |
|
|
101
|
+
| mobile-and-site | mobile app + app-store landing page (no server, no admin) |
|
|
101
102
|
| mobile-and-server | server + mobile (no UI) |
|
|
102
103
|
| just-mobile | mobile only (assumes external API) |
|
|
103
104
|
|
|
105
|
+
The **public site** ships two landing flavors and the CLI picks by
|
|
106
|
+
preset: a **SaaS landing** (CTAs → admin signup) for server/admin
|
|
107
|
+
presets, and an **app-store landing** (App Store / Play buttons,
|
|
108
|
+
pricing, download) for `mobile-and-site`, branded from your primary
|
|
109
|
+
color.
|
|
110
|
+
|
|
104
111
|
Then a few questions (name, domain, ports, primary color, mobile
|
|
105
|
-
bundle ID
|
|
112
|
+
bundle ID + **splash style** — instant show-up or animated — if
|
|
113
|
+
applicable). After scaffold:
|
|
106
114
|
|
|
107
115
|
```bash
|
|
108
116
|
cd my-project
|
|
@@ -198,6 +206,39 @@ umpordez --version check version
|
|
|
198
206
|
| Uploads | AWS S3 with signed URLs |
|
|
199
207
|
| Email | Nodemailer + HTML templates |
|
|
200
208
|
|
|
209
|
+
## What changed in 1.2
|
|
210
|
+
|
|
211
|
+
- **Non-Negotiables** section in AGENTS.md — codifies rules per layer
|
|
212
|
+
(server, admin UI, site, mobile) so teams don't have to rediscover
|
|
213
|
+
them.
|
|
214
|
+
- **Key Patterns (TL;DR)** — quick reference for adding entities,
|
|
215
|
+
context DI, route handlers, and transactions.
|
|
216
|
+
- **i18n Coverage** table — shows which layers speak which
|
|
217
|
+
languages at a glance.
|
|
218
|
+
- **Anonymous-first mobile** — synthetic device-account on first
|
|
219
|
+
boot, never gate behind sign-in.
|
|
220
|
+
- **fetch-api** now supports PUT and PATCH methods plus safer
|
|
221
|
+
URL parameter filtering (skips null/undefined values).
|
|
222
|
+
|
|
223
|
+
## What's new in 1.3
|
|
224
|
+
|
|
225
|
+
Ship a mobile app + its marketing site in one shot:
|
|
226
|
+
|
|
227
|
+
- **`mobile-and-site` preset** — mobile app + app-store landing page,
|
|
228
|
+
no server or admin. The "indie mobile app" shape.
|
|
229
|
+
- **App-store landing** — App Store / Play buttons, phone mockup,
|
|
230
|
+
pricing, download CTAs; branded from your primary color. The CLI
|
|
231
|
+
picks SaaS vs app landing by preset (and `umpordez update` keeps
|
|
232
|
+
the right flavor).
|
|
233
|
+
- **Splash choice** — instant show-up (default; native splash hides on
|
|
234
|
+
font load) or the animated splash, chosen at scaffold time.
|
|
235
|
+
- **Store launch kit** (every mobile preset) — `CHANGELOG.md` +
|
|
236
|
+
`docs/store-listing.md`, `store-copy.md`, `releases.md`,
|
|
237
|
+
`screenshots.md`.
|
|
238
|
+
- **Screenshot + release automation** — `frame-shots.mjs`,
|
|
239
|
+
`gen-mockups.mjs`, `screenshots-capture.sh` + Maestro flow,
|
|
240
|
+
`appstore-resize.sh`, and `misc/build-local.sh`.
|
|
241
|
+
|
|
201
242
|
## What changed in 1.1
|
|
202
243
|
|
|
203
244
|
The big one. v1.0.x was server + web only. v1.1 adds:
|
package/cli.mjs
CHANGED
|
@@ -41,13 +41,15 @@ function showHelp() {
|
|
|
41
41
|
console.log(` umpordez --version ${dim('Show version')}`);
|
|
42
42
|
console.log('');
|
|
43
43
|
console.log(green.bold('What you get:'));
|
|
44
|
+
console.log(` ${green('>')} Presets: everything / web-only / mobile+site / mobile+server / just-mobile`);
|
|
44
45
|
console.log(` ${green('>')} Multi-tenant auth (cookie-based JWT)`);
|
|
45
46
|
console.log(` ${green('>')} Admin API + Site API (Express)`);
|
|
46
47
|
console.log(` ${green('>')} React admin panel (Vite + shadcn/ui)`);
|
|
47
48
|
console.log(` ${green('>')} PostgreSQL + Knex.js migrations`);
|
|
48
49
|
console.log(` ${green('>')} S3 file uploads with signed URLs`);
|
|
49
|
-
console.log(` ${green('>')}
|
|
50
|
-
console.log(` ${green('>')}
|
|
50
|
+
console.log(` ${green('>')} Public site — SaaS or app-store landing (EJS + i18n + SEO)`);
|
|
51
|
+
console.log(` ${green('>')} Mobile app (Expo + TS) — instant or animated splash`);
|
|
52
|
+
console.log(` ${green('>')} Store launch kit (listing, copy, changelog, screenshots)`);
|
|
51
53
|
console.log(` ${green('>')} Role-based access (admin/owner/manager/user)`);
|
|
52
54
|
console.log(` ${green('>')} Production build system with versioning`);
|
|
53
55
|
console.log('');
|
package/create.mjs
CHANGED
|
@@ -4,7 +4,11 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { execSync } from 'node:child_process';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
buildReplacements,
|
|
9
|
+
APP_LANDING_SWAPS,
|
|
10
|
+
SITE_VARIANT_DIR
|
|
11
|
+
} from './template-variables.mjs';
|
|
8
12
|
import { generateIcons } from './icons.mjs';
|
|
9
13
|
import { writeInitialManifest } from './update.mjs';
|
|
10
14
|
|
|
@@ -45,6 +49,17 @@ const PRESETS = {
|
|
|
45
49
|
wantsAdminUi: true,
|
|
46
50
|
wantsSite: true,
|
|
47
51
|
},
|
|
52
|
+
'mobile-and-site': {
|
|
53
|
+
label: 'Mobile app + landing page (no server, no admin)',
|
|
54
|
+
includes: ['mobile', 'ui/site', 'scripts', 'doc'],
|
|
55
|
+
wantsMobile: true,
|
|
56
|
+
wantsServer: false,
|
|
57
|
+
wantsAdminUi: false,
|
|
58
|
+
wantsSite: true,
|
|
59
|
+
// Swaps the SaaS landing for the app-marketing landing
|
|
60
|
+
// (App Store / Play buttons, pricing, download CTA).
|
|
61
|
+
appLanding: true,
|
|
62
|
+
},
|
|
48
63
|
'mobile-and-server': {
|
|
49
64
|
label: 'Mobile + server only (no admin UI, no site)',
|
|
50
65
|
includes: ['server', 'mobile', 'misc', 'scripts', 'doc'],
|
|
@@ -70,6 +85,8 @@ const ROOT_FILES_BY_PRESET = {
|
|
|
70
85
|
'web-only': ['CLAUDE.md', 'AGENTS.md', 'README.md',
|
|
71
86
|
'architecture.md', 'code.md',
|
|
72
87
|
'install.sh', 'seed.sh', 'dev.sh'],
|
|
88
|
+
'mobile-and-site': ['CLAUDE.md', 'AGENTS.md', 'README.md',
|
|
89
|
+
'code.md', 'install.sh', 'dev.sh'],
|
|
73
90
|
'mobile-and-server': ['CLAUDE.md', 'AGENTS.md', 'README.md',
|
|
74
91
|
'architecture.md', 'code.md',
|
|
75
92
|
'install.sh', 'seed.sh', 'dev.sh'],
|
|
@@ -266,6 +283,16 @@ async function promptProjectDetails() {
|
|
|
266
283
|
? true
|
|
267
284
|
: 'Lowercase alphanumeric, dashes allowed',
|
|
268
285
|
},
|
|
286
|
+
{
|
|
287
|
+
type: 'list',
|
|
288
|
+
name: 'splashStyle',
|
|
289
|
+
message: 'Splash screen:',
|
|
290
|
+
choices: [
|
|
291
|
+
{ name: 'Instant show-up (recommended)', value: 'instant' },
|
|
292
|
+
{ name: 'Animated (glow + logo + pulse)', value: 'animated' },
|
|
293
|
+
],
|
|
294
|
+
default: 'instant',
|
|
295
|
+
},
|
|
269
296
|
];
|
|
270
297
|
|
|
271
298
|
const answers = await inquirer.prompt([
|
|
@@ -296,6 +323,7 @@ async function promptProjectDetails() {
|
|
|
296
323
|
androidPackage: answers.androidPackage
|
|
297
324
|
?? toReverseDomain(answers.domain),
|
|
298
325
|
mobileScheme: answers.mobileScheme ?? answers.slug,
|
|
326
|
+
splashStyle: answers.splashStyle ?? 'instant',
|
|
299
327
|
...answers,
|
|
300
328
|
};
|
|
301
329
|
}
|
|
@@ -331,17 +359,28 @@ function showSummary(details) {
|
|
|
331
359
|
}
|
|
332
360
|
console.log(` ${chalk.bold('Color:')} ${chalk.hex(details.primaryColor)('██')} ${details.primaryColor}`);
|
|
333
361
|
console.log(` ${chalk.bold('Theme:')} ${details.defaultTheme}`);
|
|
362
|
+
if (presetDef.wantsSite) {
|
|
363
|
+
console.log(` ${chalk.bold('Landing:')} ${cyan(
|
|
364
|
+
presetDef.appLanding ? 'app store (download)' : 'SaaS (signup)'
|
|
365
|
+
)}`);
|
|
366
|
+
}
|
|
334
367
|
if (presetDef.wantsMobile) {
|
|
335
368
|
console.log(` ${chalk.bold('iOS:')} ${details.iosBundleId}`);
|
|
336
369
|
console.log(` ${chalk.bold('Android:')} ${details.androidPackage}`);
|
|
337
370
|
console.log(` ${chalk.bold('Scheme:')} ${details.mobileScheme}://`);
|
|
371
|
+
console.log(` ${chalk.bold('Splash:')} ${details.splashStyle === 'animated'
|
|
372
|
+
? 'animated (glow + logo)'
|
|
373
|
+
: 'instant show-up'}`);
|
|
338
374
|
}
|
|
339
375
|
console.log('');
|
|
340
376
|
console.log(dim(' Generates:'));
|
|
341
377
|
if (presetDef.wantsServer) console.log(dim(` ${green('>')} Admin API + Site API (Express + TS)`));
|
|
342
378
|
if (presetDef.wantsAdminUi) console.log(dim(` ${green('>')} React admin panel (Vite + shadcn/ui)`));
|
|
343
|
-
if (presetDef.wantsSite) console.log(dim(` ${green('>')}
|
|
379
|
+
if (presetDef.wantsSite) console.log(dim(` ${green('>')} ${presetDef.appLanding
|
|
380
|
+
? 'App landing page (store buttons + pricing)'
|
|
381
|
+
: 'Public site (EJS + Tailwind)'}`));
|
|
344
382
|
if (presetDef.wantsMobile) console.log(dim(` ${green('>')} Mobile app (Expo + TS + auth + theme)`));
|
|
383
|
+
if (presetDef.wantsMobile) console.log(dim(` ${green('>')} Store launch kit (listing, copy, changelog, screenshots)`));
|
|
345
384
|
if (presetDef.wantsServer) console.log(dim(` ${green('>')} PostgreSQL migrations + seed`));
|
|
346
385
|
console.log('');
|
|
347
386
|
}
|
|
@@ -383,7 +422,7 @@ function replacePlaceholders(content, replacements) {
|
|
|
383
422
|
// config, git ignore) doesn't belong to any single sub-app.
|
|
384
423
|
const ALWAYS_KEEP = new Set(['.claude']);
|
|
385
424
|
|
|
386
|
-
async function copyTemplate(outputDir, preset) {
|
|
425
|
+
export async function copyTemplate(outputDir, preset) {
|
|
387
426
|
const presetDef = PRESETS[preset];
|
|
388
427
|
await fs.promises.cp(TEMPLATE_DIR, outputDir, { recursive: true });
|
|
389
428
|
|
|
@@ -442,6 +481,25 @@ async function copyTemplate(outputDir, preset) {
|
|
|
442
481
|
await fs.promises.rm(path.join(outputDir, entry.name));
|
|
443
482
|
}
|
|
444
483
|
}
|
|
484
|
+
|
|
485
|
+
// Landing-page variant swap. App-focused presets replace the SaaS
|
|
486
|
+
// landing with the app-marketing one before placeholder replacement
|
|
487
|
+
// runs (so the swapped files get their placeholders filled too). The
|
|
488
|
+
// _variants staging dir is always removed — it never ships.
|
|
489
|
+
if (presetDef.appLanding) {
|
|
490
|
+
for (const [variant, canonical] of APP_LANDING_SWAPS) {
|
|
491
|
+
const from = path.join(outputDir, variant);
|
|
492
|
+
const to = path.join(outputDir, canonical);
|
|
493
|
+
if (fs.existsSync(from)) {
|
|
494
|
+
await fs.promises.rm(to, { force: true });
|
|
495
|
+
await fs.promises.rename(from, to);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
await fs.promises.rm(
|
|
500
|
+
path.join(outputDir, SITE_VARIANT_DIR),
|
|
501
|
+
{ recursive: true, force: true }
|
|
502
|
+
);
|
|
445
503
|
}
|
|
446
504
|
|
|
447
505
|
export async function createProject() {
|
|
@@ -565,9 +623,14 @@ export async function createProject() {
|
|
|
565
623
|
'scripts/run-ios.sh',
|
|
566
624
|
'scripts/release-android.sh',
|
|
567
625
|
'scripts/generate-icons.sh',
|
|
568
|
-
'scripts/install-native.sh'
|
|
626
|
+
'scripts/install-native.sh',
|
|
627
|
+
'scripts/appstore-resize.sh',
|
|
628
|
+
'scripts/screenshots-capture.sh'
|
|
569
629
|
);
|
|
570
630
|
}
|
|
631
|
+
if (details.presetDef.wantsServer) {
|
|
632
|
+
scripts.push('misc/build-local.sh');
|
|
633
|
+
}
|
|
571
634
|
|
|
572
635
|
for (const script of scripts) {
|
|
573
636
|
const scriptPath = path.join(outputDir, script);
|
|
@@ -638,8 +701,10 @@ function showNextSteps(details) {
|
|
|
638
701
|
console.log(` ${green('3.')} ./seed.sh`);
|
|
639
702
|
console.log(` ${green('4.')} ./dev.sh`);
|
|
640
703
|
} else {
|
|
641
|
-
|
|
642
|
-
|
|
704
|
+
// No DB to seed, but install.sh + dev.sh still orchestrate the
|
|
705
|
+
// site + mobile (they guard on directory existence).
|
|
706
|
+
console.log(` ${green('2.')} ./install.sh`);
|
|
707
|
+
console.log(` ${green('3.')} ./dev.sh`);
|
|
643
708
|
}
|
|
644
709
|
console.log('');
|
|
645
710
|
if (presetDef.wantsAdminUi) {
|
|
@@ -653,6 +718,7 @@ function showNextSteps(details) {
|
|
|
653
718
|
}
|
|
654
719
|
if (presetDef.wantsMobile) {
|
|
655
720
|
console.log(dim(` Mobile: cd mobile/${details.slug} && npm run ios | android | web`));
|
|
721
|
+
console.log(dim(` Store kit: mobile/${details.slug}/docs/ (listing, copy, screenshots) + CHANGELOG.md`));
|
|
656
722
|
}
|
|
657
723
|
console.log('');
|
|
658
724
|
console.log(dim(' Happy hacking! - ') + cyan('youtube.com/ligeiro'));
|
package/package.json
CHANGED
package/template/AGENTS.md
CHANGED
|
@@ -276,7 +276,86 @@ const ok = await confirm({
|
|
|
276
276
|
if (!ok) return;
|
|
277
277
|
```
|
|
278
278
|
|
|
279
|
-
##
|
|
279
|
+
## Non-Negotiables
|
|
280
|
+
|
|
281
|
+
### Code Style
|
|
282
|
+
- 4-space indent, semicolons, max 80 cols, LF, always braces
|
|
283
|
+
- TypeScript strict (no `any`, use `unknown`)
|
|
284
|
+
- Code in English; user-facing strings in Portuguese (pt-BR)
|
|
285
|
+
- Path aliases: `#core/*`, `#shared/*`, `#api/*` (server), `@/*` (admin UI / mobile)
|
|
286
|
+
- No relative paths beyond single `../`
|
|
287
|
+
|
|
288
|
+
### Server
|
|
289
|
+
- Routes go through models (never `req.context.knex()` in routes)
|
|
290
|
+
- `buildHandler()` wraps every async handler; Zod validates input
|
|
291
|
+
- Raw SQL in migrations (`knex.schema.raw()`)
|
|
292
|
+
- Models extend `BaseModel` — get `this.knex`, `this.context`, `this.db`
|
|
293
|
+
- Errors: `ValidationError` (400), `NotFoundError` (404), `ConflictError` (409), `ForbiddenError` (403)
|
|
294
|
+
- Pagination: `{ rows, total, page, limit, totalPages }`
|
|
295
|
+
- Every table: `id UUID`, `utc_created_on`, `utc_updated_on`; multi-tenant tables also `account_id`
|
|
296
|
+
|
|
297
|
+
### Admin UI
|
|
298
|
+
- `fetchApi()` for every API call (handles cookies, JSON, FormData)
|
|
299
|
+
- React Query for all server state; `keepPreviousData` on paginated queries
|
|
300
|
+
- Query keys centralized in `lib/query-keys.ts`
|
|
301
|
+
- shadcn/ui primitives under `components/ui/` (compose, don't fork)
|
|
302
|
+
- `useConfirm()` instead of `window.confirm()`, `toast` from sonner
|
|
303
|
+
|
|
304
|
+
### Site
|
|
305
|
+
- Every string through `t()` — no bare text in templates
|
|
306
|
+
- URL strategy: `/` → pt-BR, `/en/...` → English (middleware strips prefix)
|
|
307
|
+
- Hreflang alternates + canonical + sitemap.xml on every page
|
|
308
|
+
|
|
309
|
+
### Mobile
|
|
310
|
+
- **Free-first**: never gate behind sign-in; anonymous usage is the default
|
|
311
|
+
- **Anonymous-first**: synthetic UUID device-account on first boot, never gate behind login
|
|
312
|
+
- **Services own data access** — pages never call `getDb()` directly
|
|
313
|
+
- **User-scoped writes check `demandSigned()`** — reads always work, writes no-op when gate closed
|
|
314
|
+
- Every string through `t()` — no bare text in JSX
|
|
315
|
+
- NitroModules crash in Expo Go — lazy-load with `try/require`
|
|
316
|
+
- Never `expo prebuild --clean` (wipes native code; use `scripts/prebuild.sh`)
|
|
317
|
+
|
|
318
|
+
### i18n Coverage
|
|
319
|
+
| Layer | pt-BR | en | Additional |
|
|
320
|
+
|-------|-------|----|------------|
|
|
321
|
+
| Server | ✓ | ✓ | Extensible via i18next |
|
|
322
|
+
| Admin UI | ✓ | ✗ | pt-BR only (Brazil market) |
|
|
323
|
+
| Site | ✓ | ✓ | URL-prefixed (/ vs /en/) |
|
|
324
|
+
| Mobile | ✓ | ✓ | Extensible per project |
|
|
325
|
+
|
|
326
|
+
## Key Patterns (TL;DR)
|
|
327
|
+
|
|
328
|
+
### Adding a domain entity
|
|
329
|
+
1. `migrations/YYYYMMDDHHMMSS_create-thing.ts` — raw SQL
|
|
330
|
+
2. `core/models/thing.ts` — extends `BaseModel`
|
|
331
|
+
3. Register on `Context` in `core/context.ts`
|
|
332
|
+
4. `apps/api/routes/thing.ts` — `buildHandler()` + Zod
|
|
333
|
+
5. Admin UI: page + `hooks/queries/use-thing.ts`
|
|
334
|
+
6. Mobile (optional): `src/services/thing/`
|
|
335
|
+
|
|
336
|
+
### Context DI
|
|
337
|
+
```typescript
|
|
338
|
+
const users = await req.context.user.list({ page: 1 });
|
|
339
|
+
this.user = new UserModel(this); // in context.ts
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Route handler
|
|
343
|
+
```typescript
|
|
344
|
+
router.post('/things', buildHandler(async (req, res) => {
|
|
345
|
+
const data = z.object({ name: z.string().min(1) }).parse(req.body);
|
|
346
|
+
const result = await req.context.thing.create(req.accountId!, data);
|
|
347
|
+
res.json({ ok: true, result });
|
|
348
|
+
}));
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Transaction
|
|
352
|
+
```typescript
|
|
353
|
+
await req.context.knexTransaction(async () => {
|
|
354
|
+
const user = await req.context.user.create(data);
|
|
355
|
+
await req.context.account.addMember(accountId, { ... });
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
280
359
|
|
|
281
360
|
```
|
|
282
361
|
users (global identity) → user_in_accounts (many-to-many + role) → accounts (tenant) → [domain entities]
|
package/template/CLAUDE.md
CHANGED
|
@@ -58,7 +58,7 @@ URLs:
|
|
|
58
58
|
Folders that the preset didn't ship are simply absent. `dev.sh`
|
|
59
59
|
and `install.sh` guard on existence per directory, so the same
|
|
60
60
|
script works for every preset (everything / web-only /
|
|
61
|
-
mobile-and-server / just-mobile).
|
|
61
|
+
mobile-and-site / mobile-and-server / just-mobile).
|
|
62
62
|
|
|
63
63
|
## Multi-tenancy model
|
|
64
64
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# build-local — produce a production build of every web sub-project this
|
|
4
|
+
# repo ships (server, admin SPA, public site) into a sibling `builds/`
|
|
5
|
+
# directory, bump a build number and (optionally) commit + push it.
|
|
6
|
+
#
|
|
7
|
+
# Layout produced (matches CLAUDE.md → Deployment):
|
|
8
|
+
# ../builds/server/ server `npm run build` output + deps
|
|
9
|
+
# ../builds/ui/admin/dist/ Vite build (nginx serves this)
|
|
10
|
+
# ../builds/ui/site/ compiled site + views + public
|
|
11
|
+
#
|
|
12
|
+
# Usage (from the project root):
|
|
13
|
+
# ./misc/build-local.sh # build into ../builds
|
|
14
|
+
# ./misc/build-local.sh --push # also git commit + tag + push builds
|
|
15
|
+
#
|
|
16
|
+
# Each block is guarded on directory existence, so the same script works
|
|
17
|
+
# whatever preset generated this project.
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
21
|
+
cd "$ROOT"
|
|
22
|
+
|
|
23
|
+
BUILD_DIR="${BUILD_DIR:-../builds}"
|
|
24
|
+
PUSH=0
|
|
25
|
+
for arg in "$@"; do
|
|
26
|
+
case "$arg" in
|
|
27
|
+
--push) PUSH=1 ;;
|
|
28
|
+
esac
|
|
29
|
+
done
|
|
30
|
+
|
|
31
|
+
mkdir -p "$BUILD_DIR"
|
|
32
|
+
|
|
33
|
+
# ── Build number ────────────────────────────────────────────────────
|
|
34
|
+
NUM_FILE="$BUILD_DIR/build-number"
|
|
35
|
+
number=$(cat "$NUM_FILE" 2>/dev/null || echo 0)
|
|
36
|
+
next=$((number + 1))
|
|
37
|
+
|
|
38
|
+
# ── Server ──────────────────────────────────────────────────────────
|
|
39
|
+
if [ -d server ]; then
|
|
40
|
+
echo "==> Building server…"
|
|
41
|
+
(cd server && npm run build)
|
|
42
|
+
mkdir -p "$BUILD_DIR/server"
|
|
43
|
+
rsync -a --delete \
|
|
44
|
+
--exclude node_modules --exclude .env \
|
|
45
|
+
server/dist/ "$BUILD_DIR/server/dist/" 2>/dev/null || true
|
|
46
|
+
cp server/package.json server/package-lock.json "$BUILD_DIR/server/" 2>/dev/null || true
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# ── Admin SPA ───────────────────────────────────────────────────────
|
|
50
|
+
if [ -d ui/admin ]; then
|
|
51
|
+
echo "==> Building admin SPA…"
|
|
52
|
+
(cd ui/admin && npm run build)
|
|
53
|
+
mkdir -p "$BUILD_DIR/ui/admin"
|
|
54
|
+
rsync -a --delete ui/admin/dist/ "$BUILD_DIR/ui/admin/dist/"
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# ── Public site ─────────────────────────────────────────────────────
|
|
58
|
+
if [ -d ui/site ]; then
|
|
59
|
+
echo "==> Building public site…"
|
|
60
|
+
(cd ui/site && npm run build)
|
|
61
|
+
mkdir -p "$BUILD_DIR/ui/site"
|
|
62
|
+
rsync -a --delete \
|
|
63
|
+
--exclude node_modules --exclude .env \
|
|
64
|
+
ui/site/build/ "$BUILD_DIR/ui/site/build/" 2>/dev/null || true
|
|
65
|
+
cp ui/site/package.json "$BUILD_DIR/ui/site/" 2>/dev/null || true
|
|
66
|
+
cp -R ui/site/views "$BUILD_DIR/ui/site/" 2>/dev/null || true
|
|
67
|
+
cp -R ui/site/public "$BUILD_DIR/ui/site/" 2>/dev/null || true
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
echo "$next" > "$NUM_FILE"
|
|
71
|
+
echo ""
|
|
72
|
+
echo "Build #$next written to $BUILD_DIR"
|
|
73
|
+
|
|
74
|
+
# ── Optional: commit + push the builds repo ─────────────────────────
|
|
75
|
+
if [ "$PUSH" = "1" ] && [ -d "$BUILD_DIR/.git" ]; then
|
|
76
|
+
echo "==> Committing + pushing builds…"
|
|
77
|
+
(cd "$BUILD_DIR" \
|
|
78
|
+
&& git add . \
|
|
79
|
+
&& git commit -m "build: #$next" \
|
|
80
|
+
&& git tag -a "$next" -m "#$next" \
|
|
81
|
+
&& git push && git push origin --tags)
|
|
82
|
+
fi
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { AppState } from 'react-native';
|
|
3
3
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|
4
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
4
5
|
import Toast from 'react-native-toast-message';
|
|
5
6
|
import * as SplashScreen from 'expo-splash-screen';
|
|
6
7
|
|
|
@@ -14,13 +15,16 @@ import AnimatedSplash from './src/components/AnimatedSplash';
|
|
|
14
15
|
import { toastConfig } from './src/components/ToastConfig';
|
|
15
16
|
import { initDb } from './src/services/db';
|
|
16
17
|
import { syncAllContent } from './src/services/content';
|
|
18
|
+
import { SPLASH_ANIMATED } from './src/config';
|
|
17
19
|
|
|
18
20
|
// Hide the native splash screen so our AnimatedSplash takes over the
|
|
19
21
|
// transition. Best-effort — if the call fails (rare), we just continue.
|
|
20
22
|
SplashScreen.preventAutoHideAsync().catch(() => { /* best-effort */ });
|
|
21
23
|
|
|
22
24
|
export default function App() {
|
|
23
|
-
|
|
25
|
+
// Instant show-up (SPLASH_ANIMATED = false) starts already "done" so
|
|
26
|
+
// no overlay ever mounts — the app appears the moment fonts load.
|
|
27
|
+
const [splashDone, setSplashDone] = useState<boolean>(!SPLASH_ANIMATED);
|
|
24
28
|
|
|
25
29
|
useEffect(() => {
|
|
26
30
|
// Init runs in the background. App rendering doesn't wait on
|
|
@@ -28,6 +32,13 @@ export default function App() {
|
|
|
28
32
|
// `splashDone` flips, and screens read from SQLite which
|
|
29
33
|
// serves cached data while the network sync runs.
|
|
30
34
|
SplashScreen.hideAsync().catch(() => { /* best-effort */ });
|
|
35
|
+
|
|
36
|
+
// Force-hide the splash after 5s no matter what, so a stuck
|
|
37
|
+
// animation never keeps the user out of the app. In Release
|
|
38
|
+
// there's no red box — without this timeout a render-time
|
|
39
|
+
// crash is invisible.
|
|
40
|
+
const fallback = setTimeout(() => setSplashDone(true), 5000);
|
|
41
|
+
|
|
31
42
|
(async () => {
|
|
32
43
|
try {
|
|
33
44
|
await initDb();
|
|
@@ -55,21 +66,26 @@ export default function App() {
|
|
|
55
66
|
});
|
|
56
67
|
}
|
|
57
68
|
});
|
|
58
|
-
return () =>
|
|
69
|
+
return () => {
|
|
70
|
+
sub.remove();
|
|
71
|
+
clearTimeout(fallback);
|
|
72
|
+
};
|
|
59
73
|
}, []);
|
|
60
74
|
|
|
61
75
|
return (
|
|
62
|
-
<
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
77
|
+
<SafeAreaProvider>
|
|
78
|
+
<ThemeProvider>
|
|
79
|
+
<AuthProvider>
|
|
80
|
+
<Routes />
|
|
81
|
+
<Toast config={toastConfig} />
|
|
82
|
+
</AuthProvider>
|
|
83
|
+
</ThemeProvider>
|
|
69
84
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
85
|
+
{SPLASH_ANIMATED && !splashDone && (
|
|
86
|
+
<AnimatedSplash onDone={() => setSplashDone(true)} />
|
|
87
|
+
)}
|
|
88
|
+
</SafeAreaProvider>
|
|
89
|
+
</GestureHandlerRootView>
|
|
74
90
|
);
|
|
75
91
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **{{PROJECT_NAME}}** (the mobile app). This is the
|
|
4
|
+
source of truth for what shipped; store "What's New" copy is derived from
|
|
5
|
+
it in [`docs/releases.md`](./docs/releases.md).
|
|
6
|
+
|
|
7
|
+
Format loosely follows [Keep a Changelog](https://keepachangelog.com);
|
|
8
|
+
versions match `app.json` (`version` / `ios.buildNumber` /
|
|
9
|
+
`android.versionCode`).
|
|
10
|
+
|
|
11
|
+
## [Unreleased]
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- First public build.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
-
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
-
|
|
21
|
+
|
|
22
|
+
<!--
|
|
23
|
+
When you cut a release:
|
|
24
|
+
1. `../../scripts/bump-version.sh patch|minor|major`
|
|
25
|
+
2. Move the items above under a new `## [x.y.z] - YYYY-MM-DD` heading.
|
|
26
|
+
3. Reset [Unreleased] to empty Added/Changed/Fixed.
|
|
27
|
+
4. Translate the highlights into store copy in docs/releases.md.
|
|
28
|
+
-->
|
|
@@ -6,7 +6,7 @@ auth, 5-tab navigator, offline SQLite, custom-native modules.
|
|
|
6
6
|
```
|
|
7
7
|
src/
|
|
8
8
|
├── App.tsx, index.ts
|
|
9
|
-
├── config.ts API base URL +
|
|
9
|
+
├── config.ts API base URL + client IDs + SPLASH_ANIMATED
|
|
10
10
|
├── theme/colors.ts Palette generated from primary color
|
|
11
11
|
├── i18n/ pt-BR default + en (react-i18next)
|
|
12
12
|
├── contexts/
|
|
@@ -63,10 +63,17 @@ hasCompletedOnboarding
|
|
|
63
63
|
render an "auth required" screen at the entry point. Wrap
|
|
64
64
|
premium features in `<PremiumGate onUpgrade={openAuth} />`.
|
|
65
65
|
|
|
66
|
-
- **
|
|
66
|
+
- **Anonymous-first.** `AuthContext` fabricates a synthetic device-
|
|
67
|
+
account on first boot (`services/auth/anonymous.ts` returns a
|
|
68
|
+
UUID v4 stored in AsyncStorage). `signed: true` always;
|
|
69
|
+
`isAnonymous: true` for the default state. Never gate app entry
|
|
70
|
+
behind sign-in. The opt-in AuthStack lives in Profile's footer
|
|
71
|
+
link only.
|
|
72
|
+
|
|
73
|
+
- **Services own data access** — same rule as server. Mobile
|
|
67
74
|
doesn't have models; the equivalent is: **services own data
|
|
68
|
-
access**. Pages call `
|
|
69
|
-
`getDb().
|
|
75
|
+
access**. Pages call `services/crops`, `services/events`, … —
|
|
76
|
+
never `getDb()` directly. New SQLite reads/writes go in the
|
|
70
77
|
matching `services/<thing>/` folder.
|
|
71
78
|
|
|
72
79
|
- **User-scoped writes check `demandSigned()`** at the top of the
|
|
@@ -88,6 +95,15 @@ hasCompletedOnboarding
|
|
|
88
95
|
Use `./scripts/prebuild.sh` (refuses --clean). To genuinely
|
|
89
96
|
reset, move customizations into `plugins/` first.
|
|
90
97
|
|
|
98
|
+
## Splash
|
|
99
|
+
|
|
100
|
+
`config.ts` exports `SPLASH_ANIMATED` (set at scaffold time). When
|
|
101
|
+
`false` (instant show-up, the default) the native splash hides the
|
|
102
|
+
moment fonts load and the app appears immediately — `AnimatedSplash`
|
|
103
|
+
never mounts. When `true`, `App.tsx` overlays `AnimatedSplash`
|
|
104
|
+
(glow + logo + pulse ring, ~2s) until it finishes. The component ships
|
|
105
|
+
either way; flip the flag to switch behavior.
|
|
106
|
+
|
|
91
107
|
## Adding a screen
|
|
92
108
|
|
|
93
109
|
1. **Page** — `src/pages/<scope>/<Name>.tsx`. Typed via
|
|
@@ -150,7 +166,7 @@ Client → /api/auth/google/app
|
|
|
150
166
|
```
|
|
151
167
|
|
|
152
168
|
`signOut()` clears auth + onboarding state so the next user starts
|
|
153
|
-
from Welcome (
|
|
169
|
+
from Welcome (onboarding is per-account).
|
|
154
170
|
|
|
155
171
|
## Premium
|
|
156
172
|
|