create-100x-mobile 0.2.3 → 0.3.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/dist/commands/new.js +91 -10
- package/dist/templates/app/todosScreen.js +59 -39
- package/dist/templates/components/addTodoForm.js +30 -13
- package/dist/templates/components/filterTabs.js +7 -1
- package/dist/templates/components/todoItem.js +58 -26
- package/dist/templates/config/easJson.js +24 -0
- package/dist/templates/config/packageJson.js +3 -0
- package/dist/templates/config/prettierrc.js +12 -0
- package/dist/templates/config/readme.js +78 -0
- package/package.json +1 -1
package/dist/commands/new.js
CHANGED
|
@@ -19,6 +19,9 @@ const appJson_1 = require("../templates/config/appJson");
|
|
|
19
19
|
const tsconfig_1 = require("../templates/config/tsconfig");
|
|
20
20
|
const gitignore_1 = require("../templates/config/gitignore");
|
|
21
21
|
const envExample_1 = require("../templates/config/envExample");
|
|
22
|
+
const prettierrc_1 = require("../templates/config/prettierrc");
|
|
23
|
+
const easJson_1 = require("../templates/config/easJson");
|
|
24
|
+
const readme_1 = require("../templates/config/readme");
|
|
22
25
|
// Convex templates
|
|
23
26
|
const schema_1 = require("../templates/convex/schema");
|
|
24
27
|
const todos_1 = require("../templates/convex/todos");
|
|
@@ -84,9 +87,19 @@ async function cmdNew(args) {
|
|
|
84
87
|
}
|
|
85
88
|
await (0, fs_1.removeDir)(projectDir);
|
|
86
89
|
}
|
|
87
|
-
// ──
|
|
90
|
+
// ── Determine total steps ────────────────────────────────
|
|
91
|
+
// Steps: 1=Project structure, 2=Dependencies, 3=Convex setup,
|
|
92
|
+
// 4=Convex env (if clerkDomain), 5=Git init, 6=Health check
|
|
93
|
+
// We'll calculate totalSteps after Clerk prompts, but we create the
|
|
94
|
+
// counter now. We'll adjust totalSteps after Clerk prompts.
|
|
95
|
+
let currentStep = 0;
|
|
96
|
+
let totalSteps = 5; // base: structure, deps, convex, git, health check
|
|
97
|
+
// totalSteps += 1 if clerkDomain (convex env step) — adjusted after Clerk prompts
|
|
98
|
+
const stepLabel = () => `(${currentStep}/${totalSteps})`;
|
|
99
|
+
// ── Step: Create directories ─────────────────────────
|
|
88
100
|
const s = (0, prompts_1.spinner)();
|
|
89
|
-
|
|
101
|
+
currentStep++;
|
|
102
|
+
s.start(`Creating project structure ${stepLabel()}`);
|
|
90
103
|
const dirs = [
|
|
91
104
|
"app/(auth)",
|
|
92
105
|
"app/(tabs)",
|
|
@@ -108,6 +121,9 @@ async function cmdNew(args) {
|
|
|
108
121
|
[".gitignore", (0, gitignore_1.gitignoreTemplate)()],
|
|
109
122
|
[".env.example", (0, envExample_1.envExampleTemplate)()],
|
|
110
123
|
["expo-env.d.ts", (0, tsconfig_1.expoEnvDtsTemplate)()],
|
|
124
|
+
[".prettierrc", (0, prettierrc_1.prettierrcTemplate)()],
|
|
125
|
+
["eas.json", (0, easJson_1.easJsonTemplate)()],
|
|
126
|
+
["README.md", (0, readme_1.readmeTemplate)(projectName)],
|
|
111
127
|
// Convex
|
|
112
128
|
["convex/schema.ts", (0, schema_1.schemaTemplate)()],
|
|
113
129
|
["convex/todos.ts", (0, todos_1.todosTemplate)()],
|
|
@@ -136,8 +152,9 @@ async function cmdNew(args) {
|
|
|
136
152
|
await (0, fs_1.writeTextFile)((0, node_path_1.join)(projectDir, filePath), content);
|
|
137
153
|
}
|
|
138
154
|
s.stop("Project files created.");
|
|
139
|
-
// ── Step
|
|
140
|
-
|
|
155
|
+
// ── Step: Install dependencies ───────────────────────
|
|
156
|
+
currentStep++;
|
|
157
|
+
prompts_1.log.step(`Installing dependencies ${stepLabel()}`);
|
|
141
158
|
try {
|
|
142
159
|
await (0, run_1.run)("bun", ["install"], { cwd: projectDir });
|
|
143
160
|
}
|
|
@@ -206,7 +223,11 @@ async function cmdNew(args) {
|
|
|
206
223
|
}
|
|
207
224
|
}
|
|
208
225
|
}
|
|
209
|
-
//
|
|
226
|
+
// Adjust total steps if Clerk domain is configured
|
|
227
|
+
if (clerkDomain) {
|
|
228
|
+
totalSteps = 6; // adds convex env step
|
|
229
|
+
}
|
|
230
|
+
// ── Write Clerk env vars to .env.local ─────────
|
|
210
231
|
const envLocalPath = (0, node_path_1.join)(projectDir, ".env.local");
|
|
211
232
|
let envContents = "";
|
|
212
233
|
try {
|
|
@@ -230,8 +251,9 @@ async function cmdNew(args) {
|
|
|
230
251
|
if (clerkDomain) {
|
|
231
252
|
await (0, fs_1.writeTextFile)((0, node_path_1.join)(projectDir, "convex/auth.config.ts"), `export default {\n providers: [\n {\n domain: "${clerkDomain}",\n applicationID: "convex",\n },\n ],\n};\n`);
|
|
232
253
|
}
|
|
233
|
-
// ── Step
|
|
234
|
-
|
|
254
|
+
// ── Step: Initialize Convex (single run) ─────────────
|
|
255
|
+
currentStep++;
|
|
256
|
+
prompts_1.log.step(`Setting up Convex ${stepLabel()}`);
|
|
235
257
|
try {
|
|
236
258
|
await (0, run_1.run)("bunx", ["convex", "dev", "--once"], { cwd: projectDir });
|
|
237
259
|
prompts_1.log.success("Convex initialized.");
|
|
@@ -277,9 +299,10 @@ async function cmdNew(args) {
|
|
|
277
299
|
catch {
|
|
278
300
|
// Non-critical — user can set manually
|
|
279
301
|
}
|
|
280
|
-
// ── Step
|
|
302
|
+
// ── Step: Set Convex env var for Clerk ───────────────
|
|
281
303
|
if (clerkDomain) {
|
|
282
|
-
|
|
304
|
+
currentStep++;
|
|
305
|
+
prompts_1.log.step(`Setting Convex environment variable ${stepLabel()}`);
|
|
283
306
|
try {
|
|
284
307
|
await (0, run_1.run)("bunx", ["convex", "env", "set", "CLERK_JWT_ISSUER_DOMAIN", clerkDomain], { cwd: projectDir });
|
|
285
308
|
prompts_1.log.success("Convex environment variable set.");
|
|
@@ -288,7 +311,65 @@ async function cmdNew(args) {
|
|
|
288
311
|
prompts_1.log.info(picocolors_1.default.dim(` Note: Run manually: bunx convex env set CLERK_JWT_ISSUER_DOMAIN ${clerkDomain}`));
|
|
289
312
|
}
|
|
290
313
|
}
|
|
291
|
-
// ── Step
|
|
314
|
+
// ── Step: Git init ───────────────────────────────────
|
|
315
|
+
currentStep++;
|
|
316
|
+
prompts_1.log.step(`Initializing git repository ${stepLabel()}`);
|
|
317
|
+
try {
|
|
318
|
+
await (0, run_1.run)("git", ["init"], { cwd: projectDir });
|
|
319
|
+
await (0, run_1.run)("git", ["add", "-A"], { cwd: projectDir });
|
|
320
|
+
await (0, run_1.run)("git", ["commit", "-m", "Initial commit from create-100x-mobile"], {
|
|
321
|
+
cwd: projectDir,
|
|
322
|
+
});
|
|
323
|
+
prompts_1.log.success("Git repository initialized.");
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
prompts_1.log.info(picocolors_1.default.dim(" Note: git is not available. You can initialize a repo later."));
|
|
327
|
+
}
|
|
328
|
+
// ── Step: Health check ──────────────────────────────
|
|
329
|
+
currentStep++;
|
|
330
|
+
prompts_1.log.step(`Running health check ${stepLabel()}`);
|
|
331
|
+
const healthChecks = [];
|
|
332
|
+
// Check .env.local exists
|
|
333
|
+
const envExists = await (0, fs_1.pathExists)(envLocalPath);
|
|
334
|
+
healthChecks.push({ label: ".env.local exists", ok: envExists });
|
|
335
|
+
// Check EXPO_PUBLIC_CONVEX_URL is set
|
|
336
|
+
if (envExists) {
|
|
337
|
+
let envContent = "";
|
|
338
|
+
try {
|
|
339
|
+
envContent = await (0, fs_2.readTextFile)(envLocalPath);
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// ignore
|
|
343
|
+
}
|
|
344
|
+
healthChecks.push({
|
|
345
|
+
label: "EXPO_PUBLIC_CONVEX_URL is set",
|
|
346
|
+
ok: envContent.includes("EXPO_PUBLIC_CONVEX_URL"),
|
|
347
|
+
});
|
|
348
|
+
if (clerkKeyValue) {
|
|
349
|
+
healthChecks.push({
|
|
350
|
+
label: "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY is set",
|
|
351
|
+
ok: envContent.includes("EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY"),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
healthChecks.push({ label: "EXPO_PUBLIC_CONVEX_URL is set", ok: false });
|
|
357
|
+
if (clerkKeyValue) {
|
|
358
|
+
healthChecks.push({
|
|
359
|
+
label: "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY is set",
|
|
360
|
+
ok: false,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
for (const check of healthChecks) {
|
|
365
|
+
if (check.ok) {
|
|
366
|
+
prompts_1.log.info(` ${picocolors_1.default.green("✓")} ${check.label}`);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
prompts_1.log.info(` ${picocolors_1.default.yellow("⚠")} ${check.label}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
// ── Success message ───────────────────────────────
|
|
292
373
|
prompts_1.log.info("");
|
|
293
374
|
if (clerkKeyValue && clerkDomain) {
|
|
294
375
|
// Clerk is configured — app is ready
|
|
@@ -2,8 +2,17 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.todosScreenTemplate = todosScreenTemplate;
|
|
4
4
|
function todosScreenTemplate() {
|
|
5
|
-
return `import React, { useState } from "react";
|
|
6
|
-
import {
|
|
5
|
+
return `import React, { useState, useCallback } from "react";
|
|
6
|
+
import {
|
|
7
|
+
View,
|
|
8
|
+
Text,
|
|
9
|
+
FlatList,
|
|
10
|
+
KeyboardAvoidingView,
|
|
11
|
+
Platform,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
StatusBar,
|
|
14
|
+
} from "react-native";
|
|
15
|
+
import Animated, { LinearTransition } from "react-native-reanimated";
|
|
7
16
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
8
17
|
import { AddTodoForm } from "@/components/AddTodoForm";
|
|
9
18
|
import { TodoItem } from "@/components/TodoItem";
|
|
@@ -23,6 +32,8 @@ export interface Todo {
|
|
|
23
32
|
|
|
24
33
|
type FilterType = "all" | "active" | "completed";
|
|
25
34
|
|
|
35
|
+
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList<Todo>);
|
|
36
|
+
|
|
26
37
|
export default function TodosScreen() {
|
|
27
38
|
const [filter, setFilter] = useState<FilterType>("all");
|
|
28
39
|
|
|
@@ -73,47 +84,53 @@ export default function TodosScreen() {
|
|
|
73
84
|
(todo: Todo) => todo.completed
|
|
74
85
|
).length;
|
|
75
86
|
|
|
87
|
+
const renderItem = useCallback(
|
|
88
|
+
({ item }: { item: Todo }) => (
|
|
89
|
+
<TodoItem todo={item} onToggle={toggleTodo} onDelete={deleteTodo} />
|
|
90
|
+
),
|
|
91
|
+
[toggleTodo, deleteTodo]
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const keyExtractor = useCallback((item: Todo) => item._id, []);
|
|
95
|
+
|
|
76
96
|
return (
|
|
77
97
|
<SafeAreaView style={styles.container}>
|
|
78
98
|
<StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<Text style={styles.subtitle}>
|
|
83
|
-
{activeTodosCount} active, {completedTodosCount} completed
|
|
84
|
-
</Text>
|
|
85
|
-
</View>
|
|
86
|
-
|
|
87
|
-
<AddTodoForm onAddTodo={addTodo} />
|
|
88
|
-
|
|
89
|
-
<FilterTabs
|
|
90
|
-
filter={filter}
|
|
91
|
-
onFilterChange={setFilter}
|
|
92
|
-
counts={{
|
|
93
|
-
all: todos.length,
|
|
94
|
-
active: activeTodosCount,
|
|
95
|
-
completed: completedTodosCount,
|
|
96
|
-
}}
|
|
97
|
-
/>
|
|
98
|
-
|
|
99
|
-
<ScrollView
|
|
100
|
-
style={styles.todosContainer}
|
|
101
|
-
contentContainerStyle={styles.todosContent}
|
|
102
|
-
showsVerticalScrollIndicator={false}
|
|
99
|
+
<KeyboardAvoidingView
|
|
100
|
+
style={styles.keyboardAvoidingView}
|
|
101
|
+
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
103
102
|
>
|
|
104
|
-
{
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
103
|
+
<View style={styles.header}>
|
|
104
|
+
<Text style={styles.title}>My Tasks</Text>
|
|
105
|
+
<Text style={styles.subtitle}>
|
|
106
|
+
{activeTodosCount} active, {completedTodosCount} completed
|
|
107
|
+
</Text>
|
|
108
|
+
</View>
|
|
109
|
+
|
|
110
|
+
<AddTodoForm onAddTodo={addTodo} />
|
|
111
|
+
|
|
112
|
+
<FilterTabs
|
|
113
|
+
filter={filter}
|
|
114
|
+
onFilterChange={setFilter}
|
|
115
|
+
counts={{
|
|
116
|
+
all: todos.length,
|
|
117
|
+
active: activeTodosCount,
|
|
118
|
+
completed: completedTodosCount,
|
|
119
|
+
}}
|
|
120
|
+
/>
|
|
121
|
+
|
|
122
|
+
<AnimatedFlatList
|
|
123
|
+
data={filteredTodos}
|
|
124
|
+
renderItem={renderItem}
|
|
125
|
+
keyExtractor={keyExtractor}
|
|
126
|
+
style={styles.todosContainer}
|
|
127
|
+
contentContainerStyle={styles.todosContent}
|
|
128
|
+
showsVerticalScrollIndicator={false}
|
|
129
|
+
itemLayoutAnimation={LinearTransition.springify()}
|
|
130
|
+
ListEmptyComponent={<EmptyState filter={filter} />}
|
|
131
|
+
keyboardShouldPersistTaps="handled"
|
|
132
|
+
/>
|
|
133
|
+
</KeyboardAvoidingView>
|
|
117
134
|
</SafeAreaView>
|
|
118
135
|
);
|
|
119
136
|
}
|
|
@@ -123,6 +140,9 @@ const styles = StyleSheet.create({
|
|
|
123
140
|
flex: 1,
|
|
124
141
|
backgroundColor: "#FFFFFF",
|
|
125
142
|
},
|
|
143
|
+
keyboardAvoidingView: {
|
|
144
|
+
flex: 1,
|
|
145
|
+
},
|
|
126
146
|
header: {
|
|
127
147
|
paddingHorizontal: 16,
|
|
128
148
|
paddingTop: 24,
|
|
@@ -4,6 +4,12 @@ exports.addTodoFormTemplate = addTodoFormTemplate;
|
|
|
4
4
|
function addTodoFormTemplate() {
|
|
5
5
|
return `import React, { useState } from "react";
|
|
6
6
|
import { View, TextInput, TouchableOpacity, StyleSheet } from "react-native";
|
|
7
|
+
import Animated, {
|
|
8
|
+
useSharedValue,
|
|
9
|
+
useAnimatedStyle,
|
|
10
|
+
withSpring,
|
|
11
|
+
} from "react-native-reanimated";
|
|
12
|
+
import * as Haptics from "expo-haptics";
|
|
7
13
|
import { Plus } from "lucide-react-native";
|
|
8
14
|
|
|
9
15
|
interface AddTodoFormProps {
|
|
@@ -12,9 +18,18 @@ interface AddTodoFormProps {
|
|
|
12
18
|
|
|
13
19
|
export function AddTodoForm({ onAddTodo }: AddTodoFormProps) {
|
|
14
20
|
const [text, setText] = useState("");
|
|
21
|
+
const buttonScale = useSharedValue(1);
|
|
22
|
+
|
|
23
|
+
const buttonAnimatedStyle = useAnimatedStyle(() => ({
|
|
24
|
+
transform: [{ scale: buttonScale.value }],
|
|
25
|
+
}));
|
|
15
26
|
|
|
16
27
|
const handleSubmit = () => {
|
|
17
28
|
if (text.trim()) {
|
|
29
|
+
buttonScale.value = withSpring(0.9, { damping: 15 }, () => {
|
|
30
|
+
buttonScale.value = withSpring(1, { damping: 15 });
|
|
31
|
+
});
|
|
32
|
+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
18
33
|
onAddTodo(text);
|
|
19
34
|
setText("");
|
|
20
35
|
}
|
|
@@ -25,7 +40,7 @@ export function AddTodoForm({ onAddTodo }: AddTodoFormProps) {
|
|
|
25
40
|
<View style={styles.inputContainer}>
|
|
26
41
|
<TextInput
|
|
27
42
|
style={styles.input}
|
|
28
|
-
placeholder="Add a new task
|
|
43
|
+
placeholder="Add a new task\u2026"
|
|
29
44
|
placeholderTextColor="#9CA3AF"
|
|
30
45
|
value={text}
|
|
31
46
|
onChangeText={setText}
|
|
@@ -33,18 +48,20 @@ export function AddTodoForm({ onAddTodo }: AddTodoFormProps) {
|
|
|
33
48
|
returnKeyType="done"
|
|
34
49
|
multiline={false}
|
|
35
50
|
/>
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
<Animated.View style={buttonAnimatedStyle}>
|
|
52
|
+
<TouchableOpacity
|
|
53
|
+
style={[styles.addButton, !text.trim() && styles.addButtonDisabled]}
|
|
54
|
+
onPress={handleSubmit}
|
|
55
|
+
disabled={!text.trim()}
|
|
56
|
+
activeOpacity={0.7}
|
|
57
|
+
>
|
|
58
|
+
<Plus
|
|
59
|
+
size={20}
|
|
60
|
+
color={text.trim() ? "#FFFFFF" : "#9CA3AF"}
|
|
61
|
+
strokeWidth={2.5}
|
|
62
|
+
/>
|
|
63
|
+
</TouchableOpacity>
|
|
64
|
+
</Animated.View>
|
|
48
65
|
</View>
|
|
49
66
|
</View>
|
|
50
67
|
);
|
|
@@ -4,6 +4,7 @@ exports.filterTabsTemplate = filterTabsTemplate;
|
|
|
4
4
|
function filterTabsTemplate() {
|
|
5
5
|
return `import React from "react";
|
|
6
6
|
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
|
|
7
|
+
import * as Haptics from "expo-haptics";
|
|
7
8
|
|
|
8
9
|
type FilterType = "all" | "active" | "completed";
|
|
9
10
|
|
|
@@ -24,13 +25,18 @@ export function FilterTabs({ filter, onFilterChange, counts }: FilterTabsProps)
|
|
|
24
25
|
{ key: "completed", label: "Done" },
|
|
25
26
|
];
|
|
26
27
|
|
|
28
|
+
const handlePress = (key: FilterType) => {
|
|
29
|
+
Haptics.selectionAsync();
|
|
30
|
+
onFilterChange(key);
|
|
31
|
+
};
|
|
32
|
+
|
|
27
33
|
return (
|
|
28
34
|
<View style={styles.container}>
|
|
29
35
|
{tabs.map((tab) => (
|
|
30
36
|
<TouchableOpacity
|
|
31
37
|
key={tab.key}
|
|
32
38
|
style={[styles.tab, filter === tab.key && styles.tabActive]}
|
|
33
|
-
onPress={() =>
|
|
39
|
+
onPress={() => handlePress(tab.key)}
|
|
34
40
|
activeOpacity={0.7}
|
|
35
41
|
>
|
|
36
42
|
<Text
|
|
@@ -4,6 +4,13 @@ exports.todoItemTemplate = todoItemTemplate;
|
|
|
4
4
|
function todoItemTemplate() {
|
|
5
5
|
return `import React from "react";
|
|
6
6
|
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
|
|
7
|
+
import Animated, {
|
|
8
|
+
FadeInDown,
|
|
9
|
+
useSharedValue,
|
|
10
|
+
useAnimatedStyle,
|
|
11
|
+
withSpring,
|
|
12
|
+
} from "react-native-reanimated";
|
|
13
|
+
import * as Haptics from "expo-haptics";
|
|
7
14
|
import { Check, X } from "lucide-react-native";
|
|
8
15
|
import type { Todo } from "@/app/(tabs)/index";
|
|
9
16
|
import { Id } from "../convex/_generated/dataModel";
|
|
@@ -15,38 +22,63 @@ interface TodoItemProps {
|
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
export function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
|
|
25
|
+
const checkboxScale = useSharedValue(1);
|
|
26
|
+
|
|
27
|
+
const checkboxAnimatedStyle = useAnimatedStyle(() => ({
|
|
28
|
+
transform: [{ scale: checkboxScale.value }],
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
const handleToggle = () => {
|
|
32
|
+
checkboxScale.value = withSpring(0.85, { damping: 15 }, () => {
|
|
33
|
+
checkboxScale.value = withSpring(1, { damping: 15 });
|
|
34
|
+
});
|
|
35
|
+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
36
|
+
onToggle(todo._id);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleDelete = () => {
|
|
40
|
+
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
|
|
41
|
+
onDelete(todo._id);
|
|
42
|
+
};
|
|
43
|
+
|
|
18
44
|
return (
|
|
19
|
-
<View
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<View
|
|
26
|
-
style={[styles.checkbox, todo.completed && styles.checkboxCompleted]}
|
|
45
|
+
<Animated.View entering={FadeInDown.duration(300).springify()}>
|
|
46
|
+
<View style={styles.container}>
|
|
47
|
+
<TouchableOpacity
|
|
48
|
+
style={styles.checkboxHitArea}
|
|
49
|
+
onPress={handleToggle}
|
|
50
|
+
activeOpacity={0.7}
|
|
27
51
|
>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
52
|
+
<Animated.View
|
|
53
|
+
style={[
|
|
54
|
+
styles.checkbox,
|
|
55
|
+
todo.completed && styles.checkboxCompleted,
|
|
56
|
+
checkboxAnimatedStyle,
|
|
57
|
+
]}
|
|
58
|
+
>
|
|
59
|
+
{todo.completed && (
|
|
60
|
+
<Check size={14} color="#FFFFFF" strokeWidth={3} />
|
|
61
|
+
)}
|
|
62
|
+
</Animated.View>
|
|
63
|
+
</TouchableOpacity>
|
|
64
|
+
|
|
65
|
+
<View style={styles.content}>
|
|
66
|
+
<Text
|
|
67
|
+
style={[styles.text, todo.completed && styles.textCompleted]}
|
|
68
|
+
>
|
|
69
|
+
{todo.text}
|
|
70
|
+
</Text>
|
|
31
71
|
</View>
|
|
32
|
-
</TouchableOpacity>
|
|
33
72
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
73
|
+
<TouchableOpacity
|
|
74
|
+
style={styles.deleteHitArea}
|
|
75
|
+
onPress={handleDelete}
|
|
76
|
+
activeOpacity={0.7}
|
|
37
77
|
>
|
|
38
|
-
{
|
|
39
|
-
</
|
|
78
|
+
<X size={18} color="#9CA3AF" strokeWidth={2} />
|
|
79
|
+
</TouchableOpacity>
|
|
40
80
|
</View>
|
|
41
|
-
|
|
42
|
-
<TouchableOpacity
|
|
43
|
-
style={styles.deleteHitArea}
|
|
44
|
-
onPress={() => onDelete(todo._id)}
|
|
45
|
-
activeOpacity={0.7}
|
|
46
|
-
>
|
|
47
|
-
<X size={18} color="#9CA3AF" strokeWidth={2} />
|
|
48
|
-
</TouchableOpacity>
|
|
49
|
-
</View>
|
|
81
|
+
</Animated.View>
|
|
50
82
|
);
|
|
51
83
|
}
|
|
52
84
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.easJsonTemplate = easJsonTemplate;
|
|
4
|
+
function easJsonTemplate() {
|
|
5
|
+
const config = {
|
|
6
|
+
cli: {
|
|
7
|
+
version: ">= 15.0.0",
|
|
8
|
+
},
|
|
9
|
+
build: {
|
|
10
|
+
development: {
|
|
11
|
+
developmentClient: true,
|
|
12
|
+
distribution: "internal",
|
|
13
|
+
},
|
|
14
|
+
preview: {
|
|
15
|
+
distribution: "internal",
|
|
16
|
+
},
|
|
17
|
+
production: {},
|
|
18
|
+
},
|
|
19
|
+
submit: {
|
|
20
|
+
production: {},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
24
|
+
}
|
|
@@ -11,6 +11,7 @@ function packageJsonTemplate(appName) {
|
|
|
11
11
|
dev: "EXPO_NO_TELEMETRY=1 expo start",
|
|
12
12
|
"build:web": "expo export --platform web",
|
|
13
13
|
lint: "expo lint",
|
|
14
|
+
format: "prettier --write .",
|
|
14
15
|
},
|
|
15
16
|
dependencies: {
|
|
16
17
|
"@clerk/clerk-expo": "^2.14.24",
|
|
@@ -23,6 +24,7 @@ function packageJsonTemplate(appName) {
|
|
|
23
24
|
"expo-blur": "~15.0.8",
|
|
24
25
|
"expo-constants": "~18.0.13",
|
|
25
26
|
"expo-font": "~14.0.10",
|
|
27
|
+
"expo-haptics": "~14.0.4",
|
|
26
28
|
"expo-linking": "~8.0.11",
|
|
27
29
|
"expo-router": "~6.0.21",
|
|
28
30
|
"expo-secure-store": "~15.0.8",
|
|
@@ -46,6 +48,7 @@ function packageJsonTemplate(appName) {
|
|
|
46
48
|
"@types/react": "~19.1.10",
|
|
47
49
|
eslint: "^9.0.0",
|
|
48
50
|
"eslint-config-expo": "~10.0.0",
|
|
51
|
+
prettier: "^3.4.2",
|
|
49
52
|
typescript: "~5.9.2",
|
|
50
53
|
},
|
|
51
54
|
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.prettierrcTemplate = prettierrcTemplate;
|
|
4
|
+
function prettierrcTemplate() {
|
|
5
|
+
const config = {
|
|
6
|
+
semi: true,
|
|
7
|
+
singleQuote: false,
|
|
8
|
+
tabWidth: 2,
|
|
9
|
+
trailingComma: "es5",
|
|
10
|
+
};
|
|
11
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
12
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readmeTemplate = readmeTemplate;
|
|
4
|
+
function readmeTemplate(projectName) {
|
|
5
|
+
return `# ${projectName}
|
|
6
|
+
|
|
7
|
+
A full-stack mobile app built with **Expo**, **Convex**, and **Clerk**.
|
|
8
|
+
|
|
9
|
+
## Tech Stack
|
|
10
|
+
|
|
11
|
+
- **[Expo](https://expo.dev)** — React Native framework
|
|
12
|
+
- **[Convex](https://convex.dev)** — Backend & real-time database
|
|
13
|
+
- **[Clerk](https://clerk.com)** — Authentication (Google & Apple OAuth)
|
|
14
|
+
- **[Expo Router](https://docs.expo.dev/router/introduction/)** — File-based navigation
|
|
15
|
+
- **[React Native Reanimated](https://docs.swmansion.com/react-native-reanimated/)** — Animations
|
|
16
|
+
|
|
17
|
+
## Getting Started
|
|
18
|
+
|
|
19
|
+
\`\`\`bash
|
|
20
|
+
# Install dependencies
|
|
21
|
+
bun install
|
|
22
|
+
|
|
23
|
+
# Start the Convex dev server
|
|
24
|
+
bunx convex dev
|
|
25
|
+
|
|
26
|
+
# Start Expo (in a separate terminal)
|
|
27
|
+
bunx expo start
|
|
28
|
+
\`\`\`
|
|
29
|
+
|
|
30
|
+
## Project Structure
|
|
31
|
+
|
|
32
|
+
\`\`\`
|
|
33
|
+
├── app/
|
|
34
|
+
│ ├── _layout.tsx # Root layout with providers
|
|
35
|
+
│ ├── +not-found.tsx # 404 screen
|
|
36
|
+
│ ├── (auth)/ # Auth screens (sign-in)
|
|
37
|
+
│ ├── (tabs)/ # Tab navigation (todos, settings)
|
|
38
|
+
│ └── providers/ # Auth provider
|
|
39
|
+
├── components/ # Reusable UI components
|
|
40
|
+
├── convex/ # Backend functions & schema
|
|
41
|
+
├── hooks/ # Custom React hooks
|
|
42
|
+
└── assets/ # Images and static assets
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
## Scripts
|
|
46
|
+
|
|
47
|
+
| Command | Description |
|
|
48
|
+
|---------|-------------|
|
|
49
|
+
| \`bun run dev\` | Start Expo dev server |
|
|
50
|
+
| \`bun run lint\` | Run ESLint |
|
|
51
|
+
| \`bun run format\` | Format code with Prettier |
|
|
52
|
+
| \`bunx convex dev\` | Start Convex dev server |
|
|
53
|
+
|
|
54
|
+
## Deployment
|
|
55
|
+
|
|
56
|
+
### EAS Build
|
|
57
|
+
|
|
58
|
+
\`\`\`bash
|
|
59
|
+
# Development build
|
|
60
|
+
bunx eas build --profile development --platform ios
|
|
61
|
+
|
|
62
|
+
# Production build
|
|
63
|
+
bunx eas build --profile production --platform all
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
### Convex
|
|
67
|
+
|
|
68
|
+
\`\`\`bash
|
|
69
|
+
bunx convex deploy
|
|
70
|
+
\`\`\`
|
|
71
|
+
|
|
72
|
+
## Learn More
|
|
73
|
+
|
|
74
|
+
- [Expo Docs](https://docs.expo.dev)
|
|
75
|
+
- [Convex Docs](https://docs.convex.dev)
|
|
76
|
+
- [Clerk Docs](https://clerk.com/docs)
|
|
77
|
+
`;
|
|
78
|
+
}
|