create-100x-mobile 0.4.4 → 0.4.6
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/dist/cli.js +3 -1
- package/dist/commands/new/args.js +18 -0
- package/dist/commands/new/scaffold.js +6 -6
- package/dist/commands/new/steps.js +99 -19
- package/dist/commands/new.js +108 -36
- package/dist/templates/config/envExample.js +6 -1
- package/dist/templates/config/packageJson.js +13 -3
- package/dist/templates/config/readme.js +45 -1
- package/dist/templates/instant/instantLib.js +28 -0
- package/dist/templates/instant/rootLayout.js +23 -0
- package/dist/templates/instant/settingsScreen.js +83 -0
- package/dist/templates/instant/tabsLayout.js +54 -0
- package/dist/templates/instant/todosScreen.js +408 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -16,11 +16,13 @@ Options:
|
|
|
16
16
|
--yes Run without prompts (defaults name to "my-app")
|
|
17
17
|
--no-install Skip dependency installation
|
|
18
18
|
--skip-git Skip git init/commit
|
|
19
|
+
--backend <provider> Backend: "convex" or "instantdb"
|
|
19
20
|
--sdk <value> Use "latest", "previous", or a specific version (e.g. 54.0.33)
|
|
20
21
|
--clerk-pk <key> Pre-fill Clerk publishable key
|
|
21
22
|
--clerk-domain <domain> Pre-fill Clerk issuer domain
|
|
23
|
+
--instant-app-id <id> Pre-fill InstantDB app id
|
|
22
24
|
|
|
23
|
-
Scaffolds a
|
|
25
|
+
Scaffolds a mobile app with Expo + Convex/Clerk or Expo + InstantDB.
|
|
24
26
|
`);
|
|
25
27
|
}
|
|
26
28
|
async function main() {
|
|
@@ -22,8 +22,10 @@ function parseNewCommandArgs(args) {
|
|
|
22
22
|
installDependencies: true,
|
|
23
23
|
initializeGit: true,
|
|
24
24
|
sdk: null,
|
|
25
|
+
backend: null,
|
|
25
26
|
clerkPublishableKey: null,
|
|
26
27
|
clerkDomain: null,
|
|
28
|
+
instantAppId: null,
|
|
27
29
|
};
|
|
28
30
|
let projectName = null;
|
|
29
31
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -55,6 +57,16 @@ function parseNewCommandArgs(args) {
|
|
|
55
57
|
i = nextIndex;
|
|
56
58
|
continue;
|
|
57
59
|
}
|
|
60
|
+
if (arg === "--backend" || arg.startsWith("--backend=")) {
|
|
61
|
+
const { value, nextIndex } = getInlineOrNextValue(arg, args, i);
|
|
62
|
+
const normalized = value.toLowerCase();
|
|
63
|
+
if (normalized !== "convex" && normalized !== "instantdb") {
|
|
64
|
+
throw new Error(`Invalid --backend value "${value}". Use "convex" or "instantdb".`);
|
|
65
|
+
}
|
|
66
|
+
options.backend = normalized;
|
|
67
|
+
i = nextIndex;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
58
70
|
if (arg === "--clerk-pk" || arg.startsWith("--clerk-pk=")) {
|
|
59
71
|
const { value, nextIndex } = getInlineOrNextValue(arg, args, i);
|
|
60
72
|
options.clerkPublishableKey = value;
|
|
@@ -67,6 +79,12 @@ function parseNewCommandArgs(args) {
|
|
|
67
79
|
i = nextIndex;
|
|
68
80
|
continue;
|
|
69
81
|
}
|
|
82
|
+
if (arg === "--instant-app-id" || arg.startsWith("--instant-app-id=")) {
|
|
83
|
+
const { value, nextIndex } = getInlineOrNextValue(arg, args, i);
|
|
84
|
+
options.instantAppId = value;
|
|
85
|
+
i = nextIndex;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
70
88
|
throw new Error(`Unknown flag: ${arg}`);
|
|
71
89
|
}
|
|
72
90
|
if (!projectName && !options.interactive) {
|
|
@@ -7,16 +7,16 @@ const node_path_1 = require("node:path");
|
|
|
7
7
|
const fs_1 = require("../../lib/fs");
|
|
8
8
|
// 1x1 transparent PNG placeholder.
|
|
9
9
|
const TRANSPARENT_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7+P5kAAAAASUVORK5CYII=";
|
|
10
|
-
async function createProjectDirectories(projectDir) {
|
|
11
|
-
const
|
|
12
|
-
"app/(auth)",
|
|
10
|
+
async function createProjectDirectories(projectDir, backend) {
|
|
11
|
+
const commonDirs = [
|
|
13
12
|
"app/(tabs)",
|
|
14
|
-
"app/providers",
|
|
15
|
-
"components",
|
|
16
|
-
"convex",
|
|
17
13
|
"hooks",
|
|
18
14
|
"assets/images",
|
|
19
15
|
];
|
|
16
|
+
const backendDirs = backend === "convex"
|
|
17
|
+
? ["app/(auth)", "app/providers", "components", "convex"]
|
|
18
|
+
: ["lib"];
|
|
19
|
+
const dirs = [...commonDirs, ...backendDirs];
|
|
20
20
|
for (const dir of dirs) {
|
|
21
21
|
await (0, fs_1.ensureDir)((0, node_path_1.join)(projectDir, dir));
|
|
22
22
|
}
|
|
@@ -5,11 +5,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.normalizeClerkDomain = normalizeClerkDomain;
|
|
7
7
|
exports.toErrorMessage = toErrorMessage;
|
|
8
|
+
exports.resolveBackendChoice = resolveBackendChoice;
|
|
8
9
|
exports.resolveExpoVersion = resolveExpoVersion;
|
|
9
10
|
exports.installDependencies = installDependencies;
|
|
10
11
|
exports.resolveExpoDependencies = resolveExpoDependencies;
|
|
11
12
|
exports.promptClerkSetup = promptClerkSetup;
|
|
13
|
+
exports.promptInstantSetup = promptInstantSetup;
|
|
12
14
|
exports.writeAuthEnvironment = writeAuthEnvironment;
|
|
15
|
+
exports.writeInstantEnvironment = writeInstantEnvironment;
|
|
13
16
|
exports.initializeConvex = initializeConvex;
|
|
14
17
|
exports.ensureExpoPublicConvexUrl = ensureExpoPublicConvexUrl;
|
|
15
18
|
exports.setConvexClerkEnv = setConvexClerkEnv;
|
|
@@ -58,6 +61,34 @@ function normalizeSdkArg(sdk) {
|
|
|
58
61
|
}
|
|
59
62
|
throw new Error(`Invalid --sdk value "${value}". Use "latest", "previous", or a version like "54.0.33".`);
|
|
60
63
|
}
|
|
64
|
+
async function resolveBackendChoice(options) {
|
|
65
|
+
if (options.backend) {
|
|
66
|
+
return options.backend;
|
|
67
|
+
}
|
|
68
|
+
if (!options.interactive) {
|
|
69
|
+
return "convex";
|
|
70
|
+
}
|
|
71
|
+
const choice = await (0, prompts_1.select)({
|
|
72
|
+
message: "Which backend do you want?",
|
|
73
|
+
options: [
|
|
74
|
+
{
|
|
75
|
+
value: "convex",
|
|
76
|
+
label: "Convex + Clerk",
|
|
77
|
+
hint: "Auth + realtime backend",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
value: "instantdb",
|
|
81
|
+
label: "InstantDB",
|
|
82
|
+
hint: "Client-first realtime backend",
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
initialValue: "convex",
|
|
86
|
+
});
|
|
87
|
+
if ((0, prompts_1.isCancel)(choice)) {
|
|
88
|
+
return "convex";
|
|
89
|
+
}
|
|
90
|
+
return choice;
|
|
91
|
+
}
|
|
61
92
|
async function resolveExpoVersion(options) {
|
|
62
93
|
if (options.sdk) {
|
|
63
94
|
const normalized = normalizeSdkArg(options.sdk);
|
|
@@ -144,24 +175,21 @@ async function installDependencies(projectDir) {
|
|
|
144
175
|
}
|
|
145
176
|
}
|
|
146
177
|
}
|
|
147
|
-
async function resolveExpoDependencies(projectDir) {
|
|
148
|
-
const
|
|
178
|
+
async function resolveExpoDependencies(projectDir, backend) {
|
|
179
|
+
const commonExpoPackages = [
|
|
149
180
|
"react",
|
|
150
181
|
"react-dom",
|
|
151
182
|
"react-native",
|
|
152
183
|
"expo-router",
|
|
153
184
|
"@expo/vector-icons",
|
|
154
|
-
"expo-auth-session",
|
|
155
185
|
"expo-blur",
|
|
156
186
|
"expo-constants",
|
|
157
187
|
"expo-font",
|
|
158
188
|
"expo-haptics",
|
|
159
189
|
"expo-linking",
|
|
160
|
-
"expo-secure-store",
|
|
161
190
|
"expo-splash-screen",
|
|
162
191
|
"expo-status-bar",
|
|
163
192
|
"expo-system-ui",
|
|
164
|
-
"expo-web-browser",
|
|
165
193
|
"react-native-gesture-handler",
|
|
166
194
|
"react-native-reanimated",
|
|
167
195
|
"react-native-safe-area-context",
|
|
@@ -169,6 +197,10 @@ async function resolveExpoDependencies(projectDir) {
|
|
|
169
197
|
"react-native-svg",
|
|
170
198
|
"react-native-web",
|
|
171
199
|
];
|
|
200
|
+
const backendExpoPackages = backend === "convex"
|
|
201
|
+
? ["expo-auth-session", "expo-secure-store", "expo-web-browser"]
|
|
202
|
+
: ["@react-native-async-storage/async-storage", "@react-native-community/netinfo"];
|
|
203
|
+
const expoPackages = [...commonExpoPackages, ...backendExpoPackages];
|
|
172
204
|
try {
|
|
173
205
|
await (0, run_1.run)("npx", ["expo", "install", ...expoPackages], { cwd: projectDir });
|
|
174
206
|
return true;
|
|
@@ -240,6 +272,24 @@ async function promptClerkSetup(options) {
|
|
|
240
272
|
}
|
|
241
273
|
return { publishableKey, domain, jwtCreated };
|
|
242
274
|
}
|
|
275
|
+
async function promptInstantSetup(options) {
|
|
276
|
+
let appId = options.instantAppId?.trim() ?? "";
|
|
277
|
+
if (!appId && options.interactive) {
|
|
278
|
+
prompts_1.log.info("");
|
|
279
|
+
prompts_1.log.info(picocolors_1.default.bold("InstantDB Setup"));
|
|
280
|
+
prompts_1.log.info(picocolors_1.default.dim("Create an app id with: npx instant-cli init-without-files --title my-app"));
|
|
281
|
+
prompts_1.log.info(picocolors_1.default.dim("Press Enter to skip and configure later."));
|
|
282
|
+
const appIdInput = await (0, prompts_1.text)({
|
|
283
|
+
message: "InstantDB App ID",
|
|
284
|
+
placeholder: "your-instant-app-id",
|
|
285
|
+
defaultValue: "",
|
|
286
|
+
});
|
|
287
|
+
if (!(0, prompts_1.isCancel)(appIdInput) && appIdInput) {
|
|
288
|
+
appId = appIdInput.trim();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { appId };
|
|
292
|
+
}
|
|
243
293
|
async function writeAuthEnvironment(projectDir, clerk) {
|
|
244
294
|
const envLocalPath = (0, node_path_1.join)(projectDir, ".env.local");
|
|
245
295
|
let envContents = "";
|
|
@@ -265,6 +315,23 @@ async function writeAuthEnvironment(projectDir, clerk) {
|
|
|
265
315
|
await (0, fs_1.writeTextFile)((0, node_path_1.join)(projectDir, "convex/auth.config.ts"), `export default {\n providers: [\n {\n domain: ${serializedDomain},\n applicationID: "convex",\n },\n ],\n};\n`);
|
|
266
316
|
}
|
|
267
317
|
}
|
|
318
|
+
async function writeInstantEnvironment(projectDir, instant) {
|
|
319
|
+
if (!instant.appId) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const envLocalPath = (0, node_path_1.join)(projectDir, ".env.local");
|
|
323
|
+
let envContents = "";
|
|
324
|
+
try {
|
|
325
|
+
envContents = await (0, fs_1.readTextFile)(envLocalPath);
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
if (isErrnoException(error) && error.code !== "ENOENT") {
|
|
329
|
+
prompts_1.log.warn(`Could not read .env.local: ${toErrorMessage(error)}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
envContents = (0, dotenv_1.upsertDotenvVar)(envContents, "EXPO_PUBLIC_INSTANT_APP_ID", instant.appId);
|
|
333
|
+
await (0, fs_1.writeTextFile)(envLocalPath, envContents);
|
|
334
|
+
}
|
|
268
335
|
async function initializeConvex(projectDir) {
|
|
269
336
|
try {
|
|
270
337
|
await (0, run_1.run)("bunx", ["convex", "dev", "--once"], { cwd: projectDir });
|
|
@@ -343,18 +410,23 @@ async function initializeGit(projectDir) {
|
|
|
343
410
|
return false;
|
|
344
411
|
}
|
|
345
412
|
}
|
|
346
|
-
async function runHealthChecks(projectDir, clerkPublishableKey) {
|
|
413
|
+
async function runHealthChecks(projectDir, backend, clerkPublishableKey) {
|
|
347
414
|
const envLocalPath = (0, node_path_1.join)(projectDir, ".env.local");
|
|
348
415
|
const checks = [];
|
|
349
416
|
const envExists = await (0, fs_1.pathExists)(envLocalPath);
|
|
350
417
|
checks.push({ label: ".env.local exists", ok: envExists });
|
|
351
418
|
if (!envExists) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
419
|
+
if (backend === "instantdb") {
|
|
420
|
+
checks.push({ label: "EXPO_PUBLIC_INSTANT_APP_ID is set", ok: false });
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
checks.push({ label: "EXPO_PUBLIC_CONVEX_URL is set", ok: false });
|
|
424
|
+
if (clerkPublishableKey) {
|
|
425
|
+
checks.push({
|
|
426
|
+
label: "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY is set",
|
|
427
|
+
ok: false,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
358
430
|
}
|
|
359
431
|
return checks;
|
|
360
432
|
}
|
|
@@ -365,15 +437,23 @@ async function runHealthChecks(projectDir, clerkPublishableKey) {
|
|
|
365
437
|
catch (error) {
|
|
366
438
|
prompts_1.log.warn(`Could not read .env.local during health checks: ${toErrorMessage(error)}`);
|
|
367
439
|
}
|
|
368
|
-
|
|
369
|
-
label: "EXPO_PUBLIC_CONVEX_URL is set",
|
|
370
|
-
ok: envContent.includes("EXPO_PUBLIC_CONVEX_URL"),
|
|
371
|
-
});
|
|
372
|
-
if (clerkPublishableKey) {
|
|
440
|
+
if (backend === "instantdb") {
|
|
373
441
|
checks.push({
|
|
374
|
-
label: "
|
|
375
|
-
ok: envContent.includes("
|
|
442
|
+
label: "EXPO_PUBLIC_INSTANT_APP_ID is set",
|
|
443
|
+
ok: envContent.includes("EXPO_PUBLIC_INSTANT_APP_ID"),
|
|
376
444
|
});
|
|
377
445
|
}
|
|
446
|
+
else {
|
|
447
|
+
checks.push({
|
|
448
|
+
label: "EXPO_PUBLIC_CONVEX_URL is set",
|
|
449
|
+
ok: envContent.includes("EXPO_PUBLIC_CONVEX_URL"),
|
|
450
|
+
});
|
|
451
|
+
if (clerkPublishableKey) {
|
|
452
|
+
checks.push({
|
|
453
|
+
label: "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY is set",
|
|
454
|
+
ok: envContent.includes("EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY"),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
378
458
|
return checks;
|
|
379
459
|
}
|
package/dist/commands/new.js
CHANGED
|
@@ -36,6 +36,12 @@ const settingsScreen_1 = require("../templates/app/settingsScreen");
|
|
|
36
36
|
const signIn_1 = require("../templates/app/signIn");
|
|
37
37
|
const tabsLayout_1 = require("../templates/app/tabsLayout");
|
|
38
38
|
const todosScreen_1 = require("../templates/app/todosScreen");
|
|
39
|
+
// Instant templates
|
|
40
|
+
const instantLib_1 = require("../templates/instant/instantLib");
|
|
41
|
+
const rootLayout_2 = require("../templates/instant/rootLayout");
|
|
42
|
+
const settingsScreen_2 = require("../templates/instant/settingsScreen");
|
|
43
|
+
const tabsLayout_2 = require("../templates/instant/tabsLayout");
|
|
44
|
+
const todosScreen_2 = require("../templates/instant/todosScreen");
|
|
39
45
|
// Component templates
|
|
40
46
|
const addTodoForm_1 = require("../templates/components/addTodoForm");
|
|
41
47
|
const emptyState_1 = require("../templates/components/emptyState");
|
|
@@ -43,18 +49,42 @@ const filterTabs_1 = require("../templates/components/filterTabs");
|
|
|
43
49
|
const todoItem_1 = require("../templates/components/todoItem");
|
|
44
50
|
// Hook templates
|
|
45
51
|
const useFrameworkReady_1 = require("../templates/hooks/useFrameworkReady");
|
|
46
|
-
function buildTemplateFiles(projectName, expoVersion) {
|
|
52
|
+
function buildTemplateFiles(projectName, expoVersion, backend) {
|
|
53
|
+
if (backend === "instantdb") {
|
|
54
|
+
const files = [
|
|
55
|
+
// Config
|
|
56
|
+
["package.json", (0, packageJson_1.packageJsonTemplate)(projectName, expoVersion, backend)],
|
|
57
|
+
["app.json", (0, appJson_1.appJsonTemplate)(projectName)],
|
|
58
|
+
["tsconfig.json", (0, tsconfig_1.tsconfigTemplate)()],
|
|
59
|
+
[".gitignore", (0, gitignore_1.gitignoreTemplate)()],
|
|
60
|
+
[".env.example", (0, envExample_1.envExampleTemplate)(backend)],
|
|
61
|
+
["expo-env.d.ts", (0, tsconfig_1.expoEnvDtsTemplate)()],
|
|
62
|
+
[".prettierrc", (0, prettierrc_1.prettierrcTemplate)()],
|
|
63
|
+
["eas.json", (0, easJson_1.easJsonTemplate)()],
|
|
64
|
+
["README.md", (0, readme_1.readmeTemplate)(projectName, backend)],
|
|
65
|
+
// Instant app
|
|
66
|
+
["app/_layout.tsx", (0, rootLayout_2.instantRootLayoutTemplate)()],
|
|
67
|
+
["app/+not-found.tsx", (0, notFound_1.notFoundTemplate)()],
|
|
68
|
+
["app/(tabs)/_layout.tsx", (0, tabsLayout_2.instantTabsLayoutTemplate)()],
|
|
69
|
+
["app/(tabs)/index.tsx", (0, todosScreen_2.instantTodosScreenTemplate)()],
|
|
70
|
+
["app/(tabs)/settings.tsx", (0, settingsScreen_2.instantSettingsScreenTemplate)()],
|
|
71
|
+
["lib/instant.ts", (0, instantLib_1.instantLibTemplate)()],
|
|
72
|
+
// Hooks
|
|
73
|
+
["hooks/useFrameworkReady.ts", (0, useFrameworkReady_1.useFrameworkReadyTemplate)()],
|
|
74
|
+
];
|
|
75
|
+
return files.map(([path, content]) => ({ path, content }));
|
|
76
|
+
}
|
|
47
77
|
const files = [
|
|
48
78
|
// Config
|
|
49
|
-
["package.json", (0, packageJson_1.packageJsonTemplate)(projectName, expoVersion)],
|
|
79
|
+
["package.json", (0, packageJson_1.packageJsonTemplate)(projectName, expoVersion, backend)],
|
|
50
80
|
["app.json", (0, appJson_1.appJsonTemplate)(projectName)],
|
|
51
81
|
["tsconfig.json", (0, tsconfig_1.tsconfigTemplate)()],
|
|
52
82
|
[".gitignore", (0, gitignore_1.gitignoreTemplate)()],
|
|
53
|
-
[".env.example", (0, envExample_1.envExampleTemplate)()],
|
|
83
|
+
[".env.example", (0, envExample_1.envExampleTemplate)(backend)],
|
|
54
84
|
["expo-env.d.ts", (0, tsconfig_1.expoEnvDtsTemplate)()],
|
|
55
85
|
[".prettierrc", (0, prettierrc_1.prettierrcTemplate)()],
|
|
56
86
|
["eas.json", (0, easJson_1.easJsonTemplate)()],
|
|
57
|
-
["README.md", (0, readme_1.readmeTemplate)(projectName)],
|
|
87
|
+
["README.md", (0, readme_1.readmeTemplate)(projectName, backend)],
|
|
58
88
|
// Convex
|
|
59
89
|
["convex/schema.ts", (0, schema_1.schemaTemplate)()],
|
|
60
90
|
["convex/todos.ts", (0, todos_1.todosTemplate)()],
|
|
@@ -128,14 +158,20 @@ async function cmdNew(rawArgs) {
|
|
|
128
158
|
}
|
|
129
159
|
await (0, fs_1.removeDir)(projectDir);
|
|
130
160
|
}
|
|
131
|
-
|
|
161
|
+
const backend = await (0, steps_1.resolveBackendChoice)(options);
|
|
162
|
+
if (backend === "convex" && options.clerkDomain) {
|
|
132
163
|
options.clerkDomain = (0, steps_1.normalizeClerkDomain)(options.clerkDomain);
|
|
133
164
|
}
|
|
134
165
|
const expoVersion = await (0, steps_1.resolveExpoVersion)(options);
|
|
135
|
-
const clerk =
|
|
136
|
-
|
|
166
|
+
const clerk = backend === "convex"
|
|
167
|
+
? await (0, steps_1.promptClerkSetup)(options)
|
|
168
|
+
: { publishableKey: "", domain: "", jwtCreated: false };
|
|
169
|
+
const instant = backend === "instantdb"
|
|
170
|
+
? await (0, steps_1.promptInstantSetup)(options)
|
|
171
|
+
: { appId: "" };
|
|
172
|
+
const totalSteps = 1 + // project generation
|
|
137
173
|
(options.installDependencies ? 2 : 0) +
|
|
138
|
-
(clerk.domain ? 1 : 0) +
|
|
174
|
+
(backend === "convex" ? 2 + (clerk.domain ? 1 : 0) : 1) +
|
|
139
175
|
(options.initializeGit ? 1 : 0) +
|
|
140
176
|
1; // health check
|
|
141
177
|
let currentStep = 0;
|
|
@@ -143,11 +179,26 @@ async function cmdNew(rawArgs) {
|
|
|
143
179
|
const structureSpinner = (0, prompts_1.spinner)();
|
|
144
180
|
currentStep++;
|
|
145
181
|
structureSpinner.start(`Creating project structure ${stepLabel()}`);
|
|
146
|
-
await (0, scaffold_1.createProjectDirectories)(projectDir);
|
|
147
|
-
await (0, scaffold_1.writeScaffoldFiles)(projectDir, buildTemplateFiles(projectName, expoVersion));
|
|
182
|
+
await (0, scaffold_1.createProjectDirectories)(projectDir, backend);
|
|
183
|
+
await (0, scaffold_1.writeScaffoldFiles)(projectDir, buildTemplateFiles(projectName, expoVersion, backend));
|
|
148
184
|
await (0, scaffold_1.writeDefaultAssetFiles)(projectDir);
|
|
149
|
-
await (0, steps_1.writeAuthEnvironment)(projectDir, clerk);
|
|
150
185
|
structureSpinner.stop("Project files created.");
|
|
186
|
+
currentStep++;
|
|
187
|
+
if (backend === "convex") {
|
|
188
|
+
prompts_1.log.step(`Configuring Convex + Clerk ${stepLabel()}`);
|
|
189
|
+
await (0, steps_1.writeAuthEnvironment)(projectDir, clerk);
|
|
190
|
+
prompts_1.log.success("Convex and Clerk environment configured.");
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
prompts_1.log.step(`Configuring InstantDB ${stepLabel()}`);
|
|
194
|
+
await (0, steps_1.writeInstantEnvironment)(projectDir, instant);
|
|
195
|
+
if (instant.appId) {
|
|
196
|
+
prompts_1.log.success("InstantDB environment configured.");
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
prompts_1.log.info(picocolors_1.default.dim("Set EXPO_PUBLIC_INSTANT_APP_ID in .env.local to connect InstantDB."));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
151
202
|
if (options.installDependencies) {
|
|
152
203
|
currentStep++;
|
|
153
204
|
prompts_1.log.step(`Installing dependencies ${stepLabel()}`);
|
|
@@ -160,7 +211,7 @@ async function cmdNew(rawArgs) {
|
|
|
160
211
|
}
|
|
161
212
|
currentStep++;
|
|
162
213
|
prompts_1.log.step(`Installing Expo dependencies ${stepLabel()}`);
|
|
163
|
-
const resolved = await (0, steps_1.resolveExpoDependencies)(projectDir);
|
|
214
|
+
const resolved = await (0, steps_1.resolveExpoDependencies)(projectDir, backend);
|
|
164
215
|
if (resolved) {
|
|
165
216
|
prompts_1.log.success("Expo dependencies resolved.");
|
|
166
217
|
}
|
|
@@ -168,31 +219,33 @@ async function cmdNew(rawArgs) {
|
|
|
168
219
|
else {
|
|
169
220
|
prompts_1.log.info(picocolors_1.default.dim("Skipping dependency installation (--no-install)."));
|
|
170
221
|
}
|
|
171
|
-
|
|
172
|
-
prompts_1.log.step(`Setting up Convex ${stepLabel()}`);
|
|
173
|
-
const convexInitialized = await (0, steps_1.initializeConvex)(projectDir);
|
|
174
|
-
if (convexInitialized) {
|
|
175
|
-
prompts_1.log.success("Convex initialized.");
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
prompts_1.log.info(picocolors_1.default.dim(`Run manually: cd ${projectName} && bunx convex dev --once`));
|
|
179
|
-
}
|
|
180
|
-
const convexUrlReady = await (0, steps_1.ensureExpoPublicConvexUrl)(projectDir);
|
|
181
|
-
if (convexUrlReady) {
|
|
182
|
-
prompts_1.log.success("Convex URL configured for Expo.");
|
|
183
|
-
}
|
|
184
|
-
else {
|
|
185
|
-
prompts_1.log.info(picocolors_1.default.dim("Add EXPO_PUBLIC_CONVEX_URL to .env.local with your Convex deployment URL."));
|
|
186
|
-
}
|
|
187
|
-
if (clerk.domain) {
|
|
222
|
+
if (backend === "convex") {
|
|
188
223
|
currentStep++;
|
|
189
|
-
prompts_1.log.step(`Setting Convex
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
prompts_1.log.success("Convex
|
|
224
|
+
prompts_1.log.step(`Setting up Convex ${stepLabel()}`);
|
|
225
|
+
const convexInitialized = await (0, steps_1.initializeConvex)(projectDir);
|
|
226
|
+
if (convexInitialized) {
|
|
227
|
+
prompts_1.log.success("Convex initialized.");
|
|
193
228
|
}
|
|
194
229
|
else {
|
|
195
|
-
prompts_1.log.info(picocolors_1.default.dim(`Run manually: bunx convex
|
|
230
|
+
prompts_1.log.info(picocolors_1.default.dim(`Run manually: cd ${projectName} && bunx convex dev --once`));
|
|
231
|
+
}
|
|
232
|
+
const convexUrlReady = await (0, steps_1.ensureExpoPublicConvexUrl)(projectDir);
|
|
233
|
+
if (convexUrlReady) {
|
|
234
|
+
prompts_1.log.success("Convex URL configured for Expo.");
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
prompts_1.log.info(picocolors_1.default.dim("Add EXPO_PUBLIC_CONVEX_URL to .env.local with your Convex deployment URL."));
|
|
238
|
+
}
|
|
239
|
+
if (clerk.domain) {
|
|
240
|
+
currentStep++;
|
|
241
|
+
prompts_1.log.step(`Setting Convex environment variable ${stepLabel()}`);
|
|
242
|
+
const setEnvOk = await (0, steps_1.setConvexClerkEnv)(projectDir, clerk.domain);
|
|
243
|
+
if (setEnvOk) {
|
|
244
|
+
prompts_1.log.success("Convex environment variable set.");
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
prompts_1.log.info(picocolors_1.default.dim(`Run manually: bunx convex env set CLERK_JWT_ISSUER_DOMAIN ${clerk.domain}`));
|
|
248
|
+
}
|
|
196
249
|
}
|
|
197
250
|
}
|
|
198
251
|
if (options.initializeGit) {
|
|
@@ -208,7 +261,7 @@ async function cmdNew(rawArgs) {
|
|
|
208
261
|
}
|
|
209
262
|
currentStep++;
|
|
210
263
|
prompts_1.log.step(`Running health check ${stepLabel()}`);
|
|
211
|
-
const healthChecks = await (0, steps_1.runHealthChecks)(projectDir, clerk.publishableKey);
|
|
264
|
+
const healthChecks = await (0, steps_1.runHealthChecks)(projectDir, backend, clerk.publishableKey);
|
|
212
265
|
for (const check of healthChecks) {
|
|
213
266
|
if (check.ok) {
|
|
214
267
|
prompts_1.log.info(` ${picocolors_1.default.green("✓")} ${check.label}`);
|
|
@@ -218,7 +271,26 @@ async function cmdNew(rawArgs) {
|
|
|
218
271
|
}
|
|
219
272
|
}
|
|
220
273
|
prompts_1.log.info("");
|
|
221
|
-
if (
|
|
274
|
+
if (backend === "instantdb") {
|
|
275
|
+
prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Project scaffolded with InstantDB!")));
|
|
276
|
+
prompts_1.log.info("");
|
|
277
|
+
prompts_1.log.info(" Next steps:");
|
|
278
|
+
prompts_1.log.info(` ${picocolors_1.default.cyan("cd")} ${projectName}`);
|
|
279
|
+
if (options.installDependencies) {
|
|
280
|
+
prompts_1.log.info(` ${picocolors_1.default.cyan("bunx expo start")}`);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
prompts_1.log.info(` ${picocolors_1.default.cyan("bun install")} ${picocolors_1.default.dim("# or npm install")}`);
|
|
284
|
+
prompts_1.log.info(` ${picocolors_1.default.cyan("bunx expo start")}`);
|
|
285
|
+
}
|
|
286
|
+
if (!instant.appId) {
|
|
287
|
+
prompts_1.log.info("");
|
|
288
|
+
prompts_1.log.info(picocolors_1.default.dim(" Add EXPO_PUBLIC_INSTANT_APP_ID to .env.local first."));
|
|
289
|
+
prompts_1.log.info(picocolors_1.default.dim(" Create one with: npx instant-cli init-without-files --title " +
|
|
290
|
+
projectName));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else if (clerk.publishableKey && clerk.domain) {
|
|
222
294
|
prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Your app is ready!")));
|
|
223
295
|
prompts_1.log.info("");
|
|
224
296
|
prompts_1.log.info(" Next steps:");
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.envExampleTemplate = envExampleTemplate;
|
|
4
|
-
function envExampleTemplate() {
|
|
4
|
+
function envExampleTemplate(backend) {
|
|
5
|
+
if (backend === "instantdb") {
|
|
6
|
+
return `# InstantDB
|
|
7
|
+
EXPO_PUBLIC_INSTANT_APP_ID=
|
|
8
|
+
`;
|
|
9
|
+
}
|
|
5
10
|
return `# Convex
|
|
6
11
|
EXPO_PUBLIC_CONVEX_URL=
|
|
7
12
|
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.packageJsonTemplate = packageJsonTemplate;
|
|
4
|
-
function packageJsonTemplate(appName, expoVersion = "latest") {
|
|
4
|
+
function packageJsonTemplate(appName, expoVersion = "latest", backend = "convex") {
|
|
5
|
+
const backendDependencies = backend === "instantdb"
|
|
6
|
+
? {
|
|
7
|
+
"@instantdb/react-native": "^0.20.0",
|
|
8
|
+
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
9
|
+
"@react-native-community/netinfo": "^11.4.1",
|
|
10
|
+
"react-native-get-random-values": "^1.11.0",
|
|
11
|
+
}
|
|
12
|
+
: {
|
|
13
|
+
"@clerk/clerk-expo": "^2.14.24",
|
|
14
|
+
convex: "^1.26.1",
|
|
15
|
+
};
|
|
5
16
|
const pkg = {
|
|
6
17
|
name: appName,
|
|
7
18
|
main: "expo-router/entry",
|
|
@@ -14,12 +25,11 @@ function packageJsonTemplate(appName, expoVersion = "latest") {
|
|
|
14
25
|
format: "prettier --write .",
|
|
15
26
|
},
|
|
16
27
|
dependencies: {
|
|
17
|
-
"@clerk/clerk-expo": "^2.14.24",
|
|
18
28
|
"@react-navigation/bottom-tabs": "^7.3.10",
|
|
19
29
|
"@react-navigation/native": "^7.1.6",
|
|
20
|
-
convex: "^1.26.1",
|
|
21
30
|
expo: expoVersion,
|
|
22
31
|
"lucide-react-native": "^0.542.0",
|
|
32
|
+
...backendDependencies,
|
|
23
33
|
},
|
|
24
34
|
devDependencies: {
|
|
25
35
|
"@babel/core": "^7.25.2",
|
|
@@ -1,7 +1,51 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.readmeTemplate = readmeTemplate;
|
|
4
|
-
function readmeTemplate(projectName) {
|
|
4
|
+
function readmeTemplate(projectName, backend) {
|
|
5
|
+
if (backend === "instantdb") {
|
|
6
|
+
return `# ${projectName}
|
|
7
|
+
|
|
8
|
+
A mobile app built with **Expo** and **InstantDB**.
|
|
9
|
+
|
|
10
|
+
## Tech Stack
|
|
11
|
+
|
|
12
|
+
- **[Expo](https://expo.dev)** — React Native framework
|
|
13
|
+
- **[InstantDB](https://instantdb.com)** — Real-time backend and sync
|
|
14
|
+
- **[Expo Router](https://docs.expo.dev/router/introduction/)** — File-based navigation
|
|
15
|
+
|
|
16
|
+
## Getting Started
|
|
17
|
+
|
|
18
|
+
\`\`\`bash
|
|
19
|
+
# Install dependencies
|
|
20
|
+
bun install
|
|
21
|
+
|
|
22
|
+
# Add your Instant app id
|
|
23
|
+
cp .env.example .env.local
|
|
24
|
+
|
|
25
|
+
# Start Expo
|
|
26
|
+
bunx expo start
|
|
27
|
+
\`\`\`
|
|
28
|
+
|
|
29
|
+
Set \`EXPO_PUBLIC_INSTANT_APP_ID\` in \`.env.local\`. You can create an Instant app id via:
|
|
30
|
+
|
|
31
|
+
\`\`\`bash
|
|
32
|
+
npx instant-cli init-without-files --title ${projectName}
|
|
33
|
+
\`\`\`
|
|
34
|
+
|
|
35
|
+
## Scripts
|
|
36
|
+
|
|
37
|
+
| Command | Description |
|
|
38
|
+
|---------|-------------|
|
|
39
|
+
| \`bun run dev\` | Start Expo dev server |
|
|
40
|
+
| \`bun run lint\` | Run ESLint |
|
|
41
|
+
| \`bun run format\` | Format code with Prettier |
|
|
42
|
+
|
|
43
|
+
## Learn More
|
|
44
|
+
|
|
45
|
+
- [Expo Docs](https://docs.expo.dev)
|
|
46
|
+
- [InstantDB Docs](https://instantdb.com/docs)
|
|
47
|
+
`;
|
|
48
|
+
}
|
|
5
49
|
return `# ${projectName}
|
|
6
50
|
|
|
7
51
|
A full-stack mobile app built with **Expo**, **Convex**, and **Clerk**.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.instantLibTemplate = instantLibTemplate;
|
|
4
|
+
function instantLibTemplate() {
|
|
5
|
+
return `import "react-native-get-random-values";
|
|
6
|
+
import { init, i, id, type InstaQLEntity } from "@instantdb/react-native";
|
|
7
|
+
|
|
8
|
+
const appId = process.env.EXPO_PUBLIC_INSTANT_APP_ID;
|
|
9
|
+
|
|
10
|
+
export const schema = i.schema({
|
|
11
|
+
entities: {
|
|
12
|
+
todos: i.entity({
|
|
13
|
+
text: i.string(),
|
|
14
|
+
completed: i.boolean(),
|
|
15
|
+
createdAt: i.number().indexed(),
|
|
16
|
+
}),
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type InstantTodo = InstaQLEntity<typeof schema, "todos">;
|
|
21
|
+
export const createId = id;
|
|
22
|
+
|
|
23
|
+
export const isInstantConfigured = Boolean(appId);
|
|
24
|
+
export const instantDb = isInstantConfigured
|
|
25
|
+
? init({ appId: appId as string, schema })
|
|
26
|
+
: null;
|
|
27
|
+
`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.instantRootLayoutTemplate = instantRootLayoutTemplate;
|
|
4
|
+
function instantRootLayoutTemplate() {
|
|
5
|
+
return `import { Stack } from "expo-router";
|
|
6
|
+
import { StatusBar } from "expo-status-bar";
|
|
7
|
+
import { useFrameworkReady } from "@/hooks/useFrameworkReady";
|
|
8
|
+
|
|
9
|
+
export default function RootLayout() {
|
|
10
|
+
useFrameworkReady();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
<Stack screenOptions={{ headerShown: false }}>
|
|
15
|
+
<Stack.Screen name="(tabs)" />
|
|
16
|
+
<Stack.Screen name="+not-found" />
|
|
17
|
+
</Stack>
|
|
18
|
+
<StatusBar style="auto" />
|
|
19
|
+
</>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.instantSettingsScreenTemplate = instantSettingsScreenTemplate;
|
|
4
|
+
function instantSettingsScreenTemplate() {
|
|
5
|
+
return `import React from "react";
|
|
6
|
+
import { View, Text, StyleSheet, StatusBar } from "react-native";
|
|
7
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
8
|
+
|
|
9
|
+
export default function SettingsScreen() {
|
|
10
|
+
const instantAppId = process.env.EXPO_PUBLIC_INSTANT_APP_ID;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<SafeAreaView style={styles.container}>
|
|
14
|
+
<StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
|
|
15
|
+
<View style={styles.header}>
|
|
16
|
+
<Text style={styles.title}>Settings</Text>
|
|
17
|
+
</View>
|
|
18
|
+
|
|
19
|
+
<View style={styles.content}>
|
|
20
|
+
<View style={styles.card}>
|
|
21
|
+
<Text style={styles.sectionTitle}>Backend</Text>
|
|
22
|
+
<Text style={styles.sectionText}>InstantDB</Text>
|
|
23
|
+
</View>
|
|
24
|
+
|
|
25
|
+
<View style={styles.card}>
|
|
26
|
+
<Text style={styles.sectionTitle}>Instant App ID</Text>
|
|
27
|
+
<Text style={styles.sectionText}>
|
|
28
|
+
{instantAppId || "Not configured (set EXPO_PUBLIC_INSTANT_APP_ID)"}
|
|
29
|
+
</Text>
|
|
30
|
+
</View>
|
|
31
|
+
|
|
32
|
+
<View style={styles.card}>
|
|
33
|
+
<Text style={styles.sectionTitle}>About</Text>
|
|
34
|
+
<Text style={styles.sectionText}>Version 1.0.0</Text>
|
|
35
|
+
<Text style={styles.sectionText}>Built with Expo + InstantDB</Text>
|
|
36
|
+
</View>
|
|
37
|
+
</View>
|
|
38
|
+
</SafeAreaView>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const styles = StyleSheet.create({
|
|
43
|
+
container: {
|
|
44
|
+
flex: 1,
|
|
45
|
+
backgroundColor: "#FFFFFF",
|
|
46
|
+
},
|
|
47
|
+
header: {
|
|
48
|
+
paddingHorizontal: 16,
|
|
49
|
+
paddingTop: 24,
|
|
50
|
+
paddingBottom: 16,
|
|
51
|
+
},
|
|
52
|
+
title: {
|
|
53
|
+
fontSize: 28,
|
|
54
|
+
fontWeight: "700",
|
|
55
|
+
color: "#1F2937",
|
|
56
|
+
},
|
|
57
|
+
content: {
|
|
58
|
+
paddingHorizontal: 16,
|
|
59
|
+
paddingBottom: 24,
|
|
60
|
+
gap: 12,
|
|
61
|
+
},
|
|
62
|
+
card: {
|
|
63
|
+
backgroundColor: "#F8F9FA",
|
|
64
|
+
borderRadius: 14,
|
|
65
|
+
padding: 16,
|
|
66
|
+
},
|
|
67
|
+
sectionTitle: {
|
|
68
|
+
fontSize: 14,
|
|
69
|
+
fontWeight: "600",
|
|
70
|
+
color: "#374151",
|
|
71
|
+
marginBottom: 8,
|
|
72
|
+
textTransform: "uppercase",
|
|
73
|
+
letterSpacing: 0.4,
|
|
74
|
+
},
|
|
75
|
+
sectionText: {
|
|
76
|
+
fontSize: 15,
|
|
77
|
+
color: "#1F2937",
|
|
78
|
+
marginBottom: 4,
|
|
79
|
+
lineHeight: 22,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.instantTabsLayoutTemplate = instantTabsLayoutTemplate;
|
|
4
|
+
function instantTabsLayoutTemplate() {
|
|
5
|
+
return `import { Tabs } from "expo-router";
|
|
6
|
+
import { SquareCheck as CheckSquare, Settings } from "lucide-react-native";
|
|
7
|
+
import { Platform } from "react-native";
|
|
8
|
+
|
|
9
|
+
export default function TabLayout() {
|
|
10
|
+
return (
|
|
11
|
+
<Tabs
|
|
12
|
+
screenOptions={{
|
|
13
|
+
headerShown: false,
|
|
14
|
+
tabBarStyle: {
|
|
15
|
+
backgroundColor: "#FFFFFF",
|
|
16
|
+
borderTopWidth: 0,
|
|
17
|
+
elevation: 0,
|
|
18
|
+
shadowOpacity: 0,
|
|
19
|
+
height: Platform.OS === "ios" ? 88 : 72,
|
|
20
|
+
paddingBottom: Platform.OS === "ios" ? 32 : 12,
|
|
21
|
+
paddingTop: 12,
|
|
22
|
+
},
|
|
23
|
+
tabBarActiveTintColor: "#1F2937",
|
|
24
|
+
tabBarInactiveTintColor: "#9CA3AF",
|
|
25
|
+
tabBarLabelStyle: {
|
|
26
|
+
fontSize: 13,
|
|
27
|
+
fontWeight: "500",
|
|
28
|
+
marginTop: 4,
|
|
29
|
+
},
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<Tabs.Screen
|
|
33
|
+
name="index"
|
|
34
|
+
options={{
|
|
35
|
+
title: "Todos",
|
|
36
|
+
tabBarIcon: ({ size, color }) => (
|
|
37
|
+
<CheckSquare size={size} color={color} strokeWidth={2} />
|
|
38
|
+
),
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
<Tabs.Screen
|
|
42
|
+
name="settings"
|
|
43
|
+
options={{
|
|
44
|
+
title: "Settings",
|
|
45
|
+
tabBarIcon: ({ size, color }) => (
|
|
46
|
+
<Settings size={size} color={color} strokeWidth={2} />
|
|
47
|
+
),
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
</Tabs>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.instantTodosScreenTemplate = instantTodosScreenTemplate;
|
|
4
|
+
function instantTodosScreenTemplate() {
|
|
5
|
+
return `import React, { useMemo, useState } from "react";
|
|
6
|
+
import {
|
|
7
|
+
Alert,
|
|
8
|
+
FlatList,
|
|
9
|
+
KeyboardAvoidingView,
|
|
10
|
+
Platform,
|
|
11
|
+
Pressable,
|
|
12
|
+
StatusBar,
|
|
13
|
+
StyleSheet,
|
|
14
|
+
Text,
|
|
15
|
+
TextInput,
|
|
16
|
+
View,
|
|
17
|
+
} from "react-native";
|
|
18
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
19
|
+
import { Check, Plus, Trash2 } from "lucide-react-native";
|
|
20
|
+
import { createId, instantDb, type InstantTodo } from "@/lib/instant";
|
|
21
|
+
|
|
22
|
+
type FilterType = "all" | "active" | "completed";
|
|
23
|
+
|
|
24
|
+
function SetupRequired() {
|
|
25
|
+
return (
|
|
26
|
+
<View style={styles.setupContainer}>
|
|
27
|
+
<Text style={styles.setupTitle}>InstantDB Setup Required</Text>
|
|
28
|
+
<Text style={styles.setupText}>
|
|
29
|
+
Add EXPO_PUBLIC_INSTANT_APP_ID to your .env.local file.
|
|
30
|
+
</Text>
|
|
31
|
+
<Text style={styles.setupHint}>
|
|
32
|
+
You can create an app id with:\\n
|
|
33
|
+
npx instant-cli init-without-files --title my-app
|
|
34
|
+
</Text>
|
|
35
|
+
</View>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function InstantTodosScreen() {
|
|
40
|
+
const db = instantDb!;
|
|
41
|
+
const [text, setText] = useState("");
|
|
42
|
+
const [filter, setFilter] = useState<FilterType>("all");
|
|
43
|
+
|
|
44
|
+
const { isLoading, error, data } = db.useQuery({
|
|
45
|
+
todos: {
|
|
46
|
+
$: {
|
|
47
|
+
order: { createdAt: "desc" },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const todos = data?.todos ?? [];
|
|
53
|
+
|
|
54
|
+
const filteredTodos = useMemo(() => {
|
|
55
|
+
switch (filter) {
|
|
56
|
+
case "active":
|
|
57
|
+
return todos.filter((todo) => !todo.completed);
|
|
58
|
+
case "completed":
|
|
59
|
+
return todos.filter((todo) => todo.completed);
|
|
60
|
+
default:
|
|
61
|
+
return todos;
|
|
62
|
+
}
|
|
63
|
+
}, [filter, todos]);
|
|
64
|
+
|
|
65
|
+
const activeTodosCount = todos.filter((todo) => !todo.completed).length;
|
|
66
|
+
const completedTodosCount = todos.filter((todo) => todo.completed).length;
|
|
67
|
+
|
|
68
|
+
const addTodo = async () => {
|
|
69
|
+
const trimmed = text.trim();
|
|
70
|
+
if (!trimmed) return;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
await db.transact(
|
|
74
|
+
db.tx.todos[createId()].create({
|
|
75
|
+
text: trimmed,
|
|
76
|
+
completed: false,
|
|
77
|
+
createdAt: Date.now(),
|
|
78
|
+
})
|
|
79
|
+
);
|
|
80
|
+
setText("");
|
|
81
|
+
} catch (txnError) {
|
|
82
|
+
console.error("Failed to add todo:", txnError);
|
|
83
|
+
Alert.alert("Error", "Could not add todo.");
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const toggleTodo = async (todo: InstantTodo) => {
|
|
88
|
+
try {
|
|
89
|
+
await db.transact(
|
|
90
|
+
db.tx.todos[todo.id].update({ completed: !todo.completed })
|
|
91
|
+
);
|
|
92
|
+
} catch (txnError) {
|
|
93
|
+
console.error("Failed to toggle todo:", txnError);
|
|
94
|
+
Alert.alert("Error", "Could not update todo.");
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const removeTodo = async (todo: InstantTodo) => {
|
|
99
|
+
try {
|
|
100
|
+
await db.transact(db.tx.todos[todo.id].delete());
|
|
101
|
+
} catch (txnError) {
|
|
102
|
+
console.error("Failed to delete todo:", txnError);
|
|
103
|
+
Alert.alert("Error", "Could not delete todo.");
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (isLoading) {
|
|
108
|
+
return (
|
|
109
|
+
<View style={styles.loadingContainer}>
|
|
110
|
+
<Text style={styles.loadingText}>Loading todos...</Text>
|
|
111
|
+
</View>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (error) {
|
|
116
|
+
return (
|
|
117
|
+
<View style={styles.loadingContainer}>
|
|
118
|
+
<Text style={styles.errorText}>Error: {error.message}</Text>
|
|
119
|
+
</View>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<SafeAreaView style={styles.container}>
|
|
125
|
+
<StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
|
|
126
|
+
<KeyboardAvoidingView
|
|
127
|
+
style={styles.keyboardAvoidingView}
|
|
128
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
129
|
+
>
|
|
130
|
+
<View style={styles.header}>
|
|
131
|
+
<Text style={styles.title}>My Tasks</Text>
|
|
132
|
+
<Text style={styles.subtitle}>
|
|
133
|
+
{activeTodosCount} active, {completedTodosCount} completed
|
|
134
|
+
</Text>
|
|
135
|
+
</View>
|
|
136
|
+
|
|
137
|
+
<View style={styles.addRow}>
|
|
138
|
+
<TextInput
|
|
139
|
+
style={styles.input}
|
|
140
|
+
placeholder="Add a new task..."
|
|
141
|
+
placeholderTextColor="#9CA3AF"
|
|
142
|
+
value={text}
|
|
143
|
+
onChangeText={setText}
|
|
144
|
+
onSubmitEditing={addTodo}
|
|
145
|
+
returnKeyType="done"
|
|
146
|
+
/>
|
|
147
|
+
<Pressable
|
|
148
|
+
style={[styles.addButton, !text.trim() && styles.addButtonDisabled]}
|
|
149
|
+
onPress={addTodo}
|
|
150
|
+
disabled={!text.trim()}
|
|
151
|
+
>
|
|
152
|
+
<Plus size={18} color={text.trim() ? "#FFFFFF" : "#9CA3AF"} />
|
|
153
|
+
</Pressable>
|
|
154
|
+
</View>
|
|
155
|
+
|
|
156
|
+
<View style={styles.filters}>
|
|
157
|
+
{(["all", "active", "completed"] as const).map((key) => (
|
|
158
|
+
<Pressable
|
|
159
|
+
key={key}
|
|
160
|
+
style={[styles.filterPill, filter === key && styles.filterPillActive]}
|
|
161
|
+
onPress={() => setFilter(key)}
|
|
162
|
+
>
|
|
163
|
+
<Text
|
|
164
|
+
style={[
|
|
165
|
+
styles.filterText,
|
|
166
|
+
filter === key && styles.filterTextActive,
|
|
167
|
+
]}
|
|
168
|
+
>
|
|
169
|
+
{key === "completed" ? "done" : key}
|
|
170
|
+
</Text>
|
|
171
|
+
</Pressable>
|
|
172
|
+
))}
|
|
173
|
+
</View>
|
|
174
|
+
|
|
175
|
+
<FlatList
|
|
176
|
+
data={filteredTodos}
|
|
177
|
+
keyExtractor={(item) => item.id}
|
|
178
|
+
contentContainerStyle={styles.listContent}
|
|
179
|
+
ListEmptyComponent={
|
|
180
|
+
<View style={styles.emptyState}>
|
|
181
|
+
<Text style={styles.emptyTitle}>No tasks yet</Text>
|
|
182
|
+
<Text style={styles.emptySubtitle}>
|
|
183
|
+
Add your first task above to get started.
|
|
184
|
+
</Text>
|
|
185
|
+
</View>
|
|
186
|
+
}
|
|
187
|
+
renderItem={({ item }) => (
|
|
188
|
+
<View style={styles.todoRow}>
|
|
189
|
+
<Pressable
|
|
190
|
+
style={[
|
|
191
|
+
styles.checkbox,
|
|
192
|
+
item.completed && styles.checkboxCompleted,
|
|
193
|
+
]}
|
|
194
|
+
onPress={() => toggleTodo(item)}
|
|
195
|
+
>
|
|
196
|
+
{item.completed ? (
|
|
197
|
+
<Check size={14} color="#FFFFFF" strokeWidth={3} />
|
|
198
|
+
) : null}
|
|
199
|
+
</Pressable>
|
|
200
|
+
<Text
|
|
201
|
+
style={[
|
|
202
|
+
styles.todoText,
|
|
203
|
+
item.completed && styles.todoTextCompleted,
|
|
204
|
+
]}
|
|
205
|
+
>
|
|
206
|
+
{item.text}
|
|
207
|
+
</Text>
|
|
208
|
+
<Pressable style={styles.deleteButton} onPress={() => removeTodo(item)}>
|
|
209
|
+
<Trash2 size={18} color="#9CA3AF" />
|
|
210
|
+
</Pressable>
|
|
211
|
+
</View>
|
|
212
|
+
)}
|
|
213
|
+
/>
|
|
214
|
+
</KeyboardAvoidingView>
|
|
215
|
+
</SafeAreaView>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export default function TodosScreen() {
|
|
220
|
+
if (!instantDb) {
|
|
221
|
+
return (
|
|
222
|
+
<SafeAreaView style={styles.container}>
|
|
223
|
+
<SetupRequired />
|
|
224
|
+
</SafeAreaView>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return <InstantTodosScreen />;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const styles = StyleSheet.create({
|
|
231
|
+
container: {
|
|
232
|
+
flex: 1,
|
|
233
|
+
backgroundColor: "#FFFFFF",
|
|
234
|
+
},
|
|
235
|
+
keyboardAvoidingView: {
|
|
236
|
+
flex: 1,
|
|
237
|
+
},
|
|
238
|
+
header: {
|
|
239
|
+
paddingHorizontal: 16,
|
|
240
|
+
paddingTop: 24,
|
|
241
|
+
paddingBottom: 16,
|
|
242
|
+
},
|
|
243
|
+
title: {
|
|
244
|
+
fontSize: 28,
|
|
245
|
+
fontWeight: "700",
|
|
246
|
+
color: "#1F2937",
|
|
247
|
+
marginBottom: 4,
|
|
248
|
+
},
|
|
249
|
+
subtitle: {
|
|
250
|
+
fontSize: 14,
|
|
251
|
+
color: "#6B7280",
|
|
252
|
+
fontWeight: "500",
|
|
253
|
+
},
|
|
254
|
+
addRow: {
|
|
255
|
+
paddingHorizontal: 16,
|
|
256
|
+
flexDirection: "row",
|
|
257
|
+
alignItems: "center",
|
|
258
|
+
gap: 8,
|
|
259
|
+
},
|
|
260
|
+
input: {
|
|
261
|
+
flex: 1,
|
|
262
|
+
height: 52,
|
|
263
|
+
borderWidth: 1,
|
|
264
|
+
borderColor: "#E5E7EB",
|
|
265
|
+
borderRadius: 12,
|
|
266
|
+
paddingHorizontal: 14,
|
|
267
|
+
fontSize: 16,
|
|
268
|
+
color: "#1F2937",
|
|
269
|
+
},
|
|
270
|
+
addButton: {
|
|
271
|
+
width: 44,
|
|
272
|
+
height: 44,
|
|
273
|
+
borderRadius: 12,
|
|
274
|
+
backgroundColor: "#1F2937",
|
|
275
|
+
alignItems: "center",
|
|
276
|
+
justifyContent: "center",
|
|
277
|
+
},
|
|
278
|
+
addButtonDisabled: {
|
|
279
|
+
backgroundColor: "#E5E7EB",
|
|
280
|
+
},
|
|
281
|
+
filters: {
|
|
282
|
+
flexDirection: "row",
|
|
283
|
+
paddingHorizontal: 16,
|
|
284
|
+
paddingTop: 14,
|
|
285
|
+
paddingBottom: 10,
|
|
286
|
+
gap: 8,
|
|
287
|
+
},
|
|
288
|
+
filterPill: {
|
|
289
|
+
paddingHorizontal: 12,
|
|
290
|
+
paddingVertical: 8,
|
|
291
|
+
borderRadius: 999,
|
|
292
|
+
backgroundColor: "#F3F4F6",
|
|
293
|
+
},
|
|
294
|
+
filterPillActive: {
|
|
295
|
+
backgroundColor: "#111827",
|
|
296
|
+
},
|
|
297
|
+
filterText: {
|
|
298
|
+
color: "#6B7280",
|
|
299
|
+
fontWeight: "600",
|
|
300
|
+
textTransform: "capitalize",
|
|
301
|
+
},
|
|
302
|
+
filterTextActive: {
|
|
303
|
+
color: "#FFFFFF",
|
|
304
|
+
},
|
|
305
|
+
listContent: {
|
|
306
|
+
paddingHorizontal: 16,
|
|
307
|
+
paddingBottom: 96,
|
|
308
|
+
flexGrow: 1,
|
|
309
|
+
},
|
|
310
|
+
todoRow: {
|
|
311
|
+
flexDirection: "row",
|
|
312
|
+
alignItems: "center",
|
|
313
|
+
minHeight: 56,
|
|
314
|
+
borderRadius: 12,
|
|
315
|
+
borderWidth: 1,
|
|
316
|
+
borderColor: "#E5E7EB",
|
|
317
|
+
marginBottom: 8,
|
|
318
|
+
paddingHorizontal: 8,
|
|
319
|
+
backgroundColor: "#FFFFFF",
|
|
320
|
+
},
|
|
321
|
+
checkbox: {
|
|
322
|
+
width: 24,
|
|
323
|
+
height: 24,
|
|
324
|
+
borderRadius: 6,
|
|
325
|
+
borderWidth: 2,
|
|
326
|
+
borderColor: "#D1D5DB",
|
|
327
|
+
alignItems: "center",
|
|
328
|
+
justifyContent: "center",
|
|
329
|
+
marginRight: 10,
|
|
330
|
+
},
|
|
331
|
+
checkboxCompleted: {
|
|
332
|
+
backgroundColor: "#10B981",
|
|
333
|
+
borderColor: "#10B981",
|
|
334
|
+
},
|
|
335
|
+
todoText: {
|
|
336
|
+
flex: 1,
|
|
337
|
+
color: "#1F2937",
|
|
338
|
+
fontSize: 16,
|
|
339
|
+
},
|
|
340
|
+
todoTextCompleted: {
|
|
341
|
+
color: "#9CA3AF",
|
|
342
|
+
textDecorationLine: "line-through",
|
|
343
|
+
},
|
|
344
|
+
deleteButton: {
|
|
345
|
+
width: 40,
|
|
346
|
+
height: 40,
|
|
347
|
+
borderRadius: 10,
|
|
348
|
+
alignItems: "center",
|
|
349
|
+
justifyContent: "center",
|
|
350
|
+
},
|
|
351
|
+
setupContainer: {
|
|
352
|
+
flex: 1,
|
|
353
|
+
justifyContent: "center",
|
|
354
|
+
paddingHorizontal: 24,
|
|
355
|
+
},
|
|
356
|
+
setupTitle: {
|
|
357
|
+
fontSize: 24,
|
|
358
|
+
fontWeight: "700",
|
|
359
|
+
color: "#111827",
|
|
360
|
+
marginBottom: 12,
|
|
361
|
+
textAlign: "center",
|
|
362
|
+
},
|
|
363
|
+
setupText: {
|
|
364
|
+
fontSize: 15,
|
|
365
|
+
color: "#4B5563",
|
|
366
|
+
textAlign: "center",
|
|
367
|
+
lineHeight: 22,
|
|
368
|
+
},
|
|
369
|
+
setupHint: {
|
|
370
|
+
marginTop: 16,
|
|
371
|
+
fontSize: 13,
|
|
372
|
+
color: "#6B7280",
|
|
373
|
+
textAlign: "center",
|
|
374
|
+
lineHeight: 20,
|
|
375
|
+
},
|
|
376
|
+
loadingContainer: {
|
|
377
|
+
flex: 1,
|
|
378
|
+
justifyContent: "center",
|
|
379
|
+
alignItems: "center",
|
|
380
|
+
paddingHorizontal: 24,
|
|
381
|
+
},
|
|
382
|
+
loadingText: {
|
|
383
|
+
color: "#4B5563",
|
|
384
|
+
fontSize: 16,
|
|
385
|
+
},
|
|
386
|
+
errorText: {
|
|
387
|
+
color: "#DC2626",
|
|
388
|
+
fontSize: 15,
|
|
389
|
+
textAlign: "center",
|
|
390
|
+
},
|
|
391
|
+
emptyState: {
|
|
392
|
+
marginTop: 48,
|
|
393
|
+
alignItems: "center",
|
|
394
|
+
},
|
|
395
|
+
emptyTitle: {
|
|
396
|
+
fontSize: 18,
|
|
397
|
+
fontWeight: "600",
|
|
398
|
+
color: "#1F2937",
|
|
399
|
+
},
|
|
400
|
+
emptySubtitle: {
|
|
401
|
+
marginTop: 8,
|
|
402
|
+
color: "#6B7280",
|
|
403
|
+
fontSize: 14,
|
|
404
|
+
textAlign: "center",
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
`;
|
|
408
|
+
}
|