create-100x-mobile 0.1.1 → 0.2.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 +65 -16
- package/dist/lib/clerk.js +125 -0
- package/dist/templates/app/authLayout.js +2 -15
- package/dist/templates/app/rootLayout.js +2 -0
- package/dist/templates/app/settingsScreen.js +20 -34
- package/dist/templates/app/signIn.js +137 -129
- package/dist/templates/app/tabsLayout.js +2 -7
- package/dist/templates/app/todosScreen.js +6 -12
- package/dist/templates/components/addTodoForm.js +11 -9
- package/dist/templates/components/emptyState.js +6 -9
- package/dist/templates/components/filterTabs.js +13 -14
- package/dist/templates/components/todoItem.js +21 -13
- package/package.json +1 -1
package/dist/commands/new.js
CHANGED
|
@@ -12,6 +12,7 @@ const projectName_1 = require("../lib/projectName");
|
|
|
12
12
|
const run_1 = require("../lib/run");
|
|
13
13
|
const dotenv_1 = require("../lib/dotenv");
|
|
14
14
|
const fs_2 = require("../lib/fs");
|
|
15
|
+
const clerk_1 = require("../lib/clerk");
|
|
15
16
|
// Config templates
|
|
16
17
|
const packageJson_1 = require("../templates/config/packageJson");
|
|
17
18
|
const appJson_1 = require("../templates/config/appJson");
|
|
@@ -29,7 +30,6 @@ const notFound_1 = require("../templates/app/notFound");
|
|
|
29
30
|
const authProvider_1 = require("../templates/app/authProvider");
|
|
30
31
|
const authLayout_1 = require("../templates/app/authLayout");
|
|
31
32
|
const signIn_1 = require("../templates/app/signIn");
|
|
32
|
-
const signUp_1 = require("../templates/app/signUp");
|
|
33
33
|
const tabsLayout_1 = require("../templates/app/tabsLayout");
|
|
34
34
|
const todosScreen_1 = require("../templates/app/todosScreen");
|
|
35
35
|
const settingsScreen_1 = require("../templates/app/settingsScreen");
|
|
@@ -121,7 +121,6 @@ async function cmdNew(args) {
|
|
|
121
121
|
["app/providers/AuthProvider.tsx", (0, authProvider_1.authProviderTemplate)()],
|
|
122
122
|
["app/(auth)/_layout.tsx", (0, authLayout_1.authLayoutTemplate)()],
|
|
123
123
|
["app/(auth)/sign-in.tsx", (0, signIn_1.signInTemplate)()],
|
|
124
|
-
["app/(auth)/sign-up.tsx", (0, signUp_1.signUpTemplate)()],
|
|
125
124
|
["app/(tabs)/_layout.tsx", (0, tabsLayout_1.tabsLayoutTemplate)()],
|
|
126
125
|
["app/(tabs)/index.tsx", (0, todosScreen_1.todosScreenTemplate)()],
|
|
127
126
|
["app/(tabs)/settings.tsx", (0, settingsScreen_1.settingsScreenTemplate)()],
|
|
@@ -173,19 +172,38 @@ async function cmdNew(args) {
|
|
|
173
172
|
});
|
|
174
173
|
const clerkKeyValue = (0, prompts_1.isCancel)(clerkKey) || !clerkKey ? "" : clerkKey.trim();
|
|
175
174
|
let clerkDomain = "";
|
|
175
|
+
let clerkSecretKey = "";
|
|
176
176
|
if (clerkKeyValue) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
// Try to extract domain from publishable key
|
|
178
|
+
const derivedDomain = (0, clerk_1.extractDomainFromPublishableKey)(clerkKeyValue);
|
|
179
|
+
if (derivedDomain) {
|
|
180
|
+
clerkDomain = derivedDomain;
|
|
181
|
+
prompts_1.log.info(` Detected issuer domain: ${picocolors_1.default.cyan(clerkDomain)}`);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
const domainInput = await (0, prompts_1.text)({
|
|
185
|
+
message: "Clerk JWT Issuer Domain",
|
|
186
|
+
placeholder: "https://your-app.clerk.accounts.dev",
|
|
187
|
+
defaultValue: "",
|
|
188
|
+
});
|
|
189
|
+
clerkDomain =
|
|
190
|
+
(0, prompts_1.isCancel)(domainInput) || !domainInput ? "" : domainInput.trim();
|
|
191
|
+
}
|
|
192
|
+
// Ask for secret key to auto-create JWT template
|
|
193
|
+
prompts_1.log.info("");
|
|
194
|
+
prompts_1.log.info(picocolors_1.default.dim('Provide your Clerk Secret Key to auto-create the "convex" JWT template.'));
|
|
195
|
+
prompts_1.log.info(picocolors_1.default.dim("Press Enter to skip (you can create it manually later)."));
|
|
196
|
+
const secretInput = await (0, prompts_1.text)({
|
|
197
|
+
message: "Clerk Secret Key",
|
|
198
|
+
placeholder: "sk_test_...",
|
|
180
199
|
defaultValue: "",
|
|
181
200
|
});
|
|
182
|
-
|
|
183
|
-
(0, prompts_1.isCancel)(
|
|
201
|
+
clerkSecretKey =
|
|
202
|
+
(0, prompts_1.isCancel)(secretInput) || !secretInput ? "" : secretInput.trim();
|
|
184
203
|
}
|
|
185
204
|
// Step 7: Write env vars
|
|
186
205
|
const envLocalPath = (0, node_path_1.join)(projectDir, ".env.local");
|
|
187
206
|
let envContents = "";
|
|
188
|
-
// Read existing .env.local (Convex may have written EXPO_PUBLIC_CONVEX_URL)
|
|
189
207
|
try {
|
|
190
208
|
envContents = await (0, fs_2.readTextFile)(envLocalPath);
|
|
191
209
|
}
|
|
@@ -212,6 +230,22 @@ async function cmdNew(args) {
|
|
|
212
230
|
prompts_1.log.warn(`Run manually: bunx convex env set CLERK_JWT_ISSUER_DOMAIN ${clerkDomain}`);
|
|
213
231
|
}
|
|
214
232
|
}
|
|
233
|
+
// Step 8b: Auto-create JWT template if secret key provided
|
|
234
|
+
let jwtCreated = false;
|
|
235
|
+
if (clerkSecretKey) {
|
|
236
|
+
const jwtSpinner = (0, prompts_1.spinner)();
|
|
237
|
+
jwtSpinner.start('Creating "convex" JWT template in Clerk');
|
|
238
|
+
const result = await (0, clerk_1.createConvexJwtTemplate)(clerkSecretKey);
|
|
239
|
+
if (result.success) {
|
|
240
|
+
jwtSpinner.stop('JWT template "convex" created in Clerk.');
|
|
241
|
+
jwtCreated = true;
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
jwtSpinner.stop("Could not create JWT template.");
|
|
245
|
+
prompts_1.log.warn(`Clerk API error: ${result.error}`);
|
|
246
|
+
prompts_1.log.warn('Create a JWT template named "convex" manually in Clerk dashboard.');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
215
249
|
// Step 9: Push Convex functions (if Clerk was configured)
|
|
216
250
|
if (clerkKeyValue && clerkDomain) {
|
|
217
251
|
prompts_1.log.step("Deploying Convex functions");
|
|
@@ -225,29 +259,44 @@ async function cmdNew(args) {
|
|
|
225
259
|
}
|
|
226
260
|
// Step 10: Success message
|
|
227
261
|
prompts_1.log.info("");
|
|
228
|
-
if (clerkKeyValue && clerkDomain) {
|
|
262
|
+
if (clerkKeyValue && clerkDomain && jwtCreated) {
|
|
263
|
+
// Fully automated — everything is set up
|
|
229
264
|
prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Your app is ready!")));
|
|
230
265
|
prompts_1.log.info("");
|
|
231
266
|
prompts_1.log.info(" Next steps:");
|
|
232
267
|
prompts_1.log.info(` ${picocolors_1.default.cyan("cd")} ${projectName}`);
|
|
233
268
|
prompts_1.log.info(` ${picocolors_1.default.cyan("bunx expo start")}`);
|
|
234
269
|
prompts_1.log.info("");
|
|
235
|
-
prompts_1.log.info(picocolors_1.default.dim(
|
|
236
|
-
|
|
270
|
+
prompts_1.log.info(picocolors_1.default.dim("Remember to enable Google and Apple OAuth providers in your Clerk dashboard."));
|
|
271
|
+
}
|
|
272
|
+
else if (clerkKeyValue && clerkDomain) {
|
|
273
|
+
// Keys set but JWT template not auto-created
|
|
274
|
+
prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Almost ready!")));
|
|
275
|
+
prompts_1.log.info("");
|
|
276
|
+
prompts_1.log.info(" One more step:");
|
|
277
|
+
prompts_1.log.info(` 1. Create a JWT template named ${picocolors_1.default.bold('"convex"')} in Clerk dashboard`);
|
|
278
|
+
prompts_1.log.info(picocolors_1.default.dim(" Dashboard → JWT Templates → New template → Name: convex"));
|
|
279
|
+
prompts_1.log.info(` 2. Enable Google and Apple OAuth in Clerk dashboard`);
|
|
280
|
+
prompts_1.log.info("");
|
|
281
|
+
prompts_1.log.info(" Then start your app:");
|
|
282
|
+
prompts_1.log.info(` ${picocolors_1.default.cyan("cd")} ${projectName}`);
|
|
283
|
+
prompts_1.log.info(` ${picocolors_1.default.cyan("bunx expo start")}`);
|
|
237
284
|
}
|
|
238
285
|
else {
|
|
286
|
+
// No Clerk keys — manual setup needed
|
|
239
287
|
prompts_1.log.success(picocolors_1.default.bold(picocolors_1.default.green("Project scaffolded!")));
|
|
240
288
|
prompts_1.log.info("");
|
|
241
289
|
prompts_1.log.info(" To finish setup:");
|
|
242
290
|
prompts_1.log.info("");
|
|
243
|
-
prompts_1.log.info(` 1.
|
|
244
|
-
prompts_1.log.info(` 2.
|
|
291
|
+
prompts_1.log.info(` 1. Create a Clerk app at ${picocolors_1.default.cyan("https://dashboard.clerk.com")}`);
|
|
292
|
+
prompts_1.log.info(` 2. Enable Google and Apple OAuth providers`);
|
|
293
|
+
prompts_1.log.info(` 3. Add keys to ${picocolors_1.default.cyan(".env.local")}:`);
|
|
245
294
|
prompts_1.log.info(` ${picocolors_1.default.dim("EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...")}`);
|
|
246
295
|
prompts_1.log.info(` ${picocolors_1.default.dim("CLERK_JWT_ISSUER_DOMAIN=https://your-app.clerk.accounts.dev")}`);
|
|
247
|
-
prompts_1.log.info(`
|
|
296
|
+
prompts_1.log.info(` 4. Set the Convex env var:`);
|
|
248
297
|
prompts_1.log.info(` ${picocolors_1.default.dim("bunx convex env set CLERK_JWT_ISSUER_DOMAIN <domain>")}`);
|
|
249
|
-
prompts_1.log.info(`
|
|
250
|
-
prompts_1.log.info(`
|
|
298
|
+
prompts_1.log.info(` 5. Create a JWT template named ${picocolors_1.default.bold('"convex"')} in Clerk dashboard`);
|
|
299
|
+
prompts_1.log.info(` 6. Deploy Convex: ${picocolors_1.default.dim("bunx convex dev --once")}`);
|
|
251
300
|
prompts_1.log.info("");
|
|
252
301
|
prompts_1.log.info(" Then start your app:");
|
|
253
302
|
prompts_1.log.info(` ${picocolors_1.default.cyan("cd")} ${projectName}`);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.extractDomainFromPublishableKey = extractDomainFromPublishableKey;
|
|
37
|
+
exports.createConvexJwtTemplate = createConvexJwtTemplate;
|
|
38
|
+
const https = __importStar(require("node:https"));
|
|
39
|
+
function clerkApi(secretKey, method, path, body) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const data = body ? JSON.stringify(body) : undefined;
|
|
42
|
+
const req = https.request({
|
|
43
|
+
hostname: "api.clerk.com",
|
|
44
|
+
path: `/v1${path}`,
|
|
45
|
+
method,
|
|
46
|
+
headers: {
|
|
47
|
+
Authorization: `Bearer ${secretKey}`,
|
|
48
|
+
"Content-Type": "application/json",
|
|
49
|
+
...(data ? { "Content-Length": String(Buffer.byteLength(data)) } : {}),
|
|
50
|
+
},
|
|
51
|
+
}, (res) => {
|
|
52
|
+
let responseBody = "";
|
|
53
|
+
res.on("data", (chunk) => (responseBody += chunk));
|
|
54
|
+
res.on("end", () => {
|
|
55
|
+
try {
|
|
56
|
+
resolve({ status: res.statusCode ?? 0, data: JSON.parse(responseBody) });
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
resolve({ status: res.statusCode ?? 0, data: responseBody });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
req.on("error", reject);
|
|
64
|
+
if (data)
|
|
65
|
+
req.write(data);
|
|
66
|
+
req.end();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Extract the JWT issuer domain from a Clerk publishable key.
|
|
71
|
+
* Clerk keys encode the Frontend API domain in base64 after the prefix.
|
|
72
|
+
*/
|
|
73
|
+
function extractDomainFromPublishableKey(key) {
|
|
74
|
+
try {
|
|
75
|
+
const prefix = key.startsWith("pk_test_")
|
|
76
|
+
? "pk_test_"
|
|
77
|
+
: key.startsWith("pk_live_")
|
|
78
|
+
? "pk_live_"
|
|
79
|
+
: null;
|
|
80
|
+
if (!prefix)
|
|
81
|
+
return null;
|
|
82
|
+
const base64Part = key.slice(prefix.length);
|
|
83
|
+
const decoded = Buffer.from(base64Part, "base64").toString("utf-8");
|
|
84
|
+
// Remove trailing $ if present
|
|
85
|
+
const domain = decoded.replace(/\$$/, "");
|
|
86
|
+
if (domain.includes("clerk")) {
|
|
87
|
+
return `https://${domain}`;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Create a "convex" JWT template in Clerk via the Backend API.
|
|
97
|
+
* Returns success even if the template already exists.
|
|
98
|
+
*/
|
|
99
|
+
async function createConvexJwtTemplate(secretKey) {
|
|
100
|
+
try {
|
|
101
|
+
const res = await clerkApi(secretKey, "POST", "/jwt_templates", {
|
|
102
|
+
name: "convex",
|
|
103
|
+
claims: {},
|
|
104
|
+
lifetime: 60,
|
|
105
|
+
allowed_clock_skew: 5,
|
|
106
|
+
});
|
|
107
|
+
if (res.status >= 200 && res.status < 300) {
|
|
108
|
+
return { success: true };
|
|
109
|
+
}
|
|
110
|
+
// Template already exists — that's fine
|
|
111
|
+
if (res.status === 422 || res.status === 409) {
|
|
112
|
+
return { success: true };
|
|
113
|
+
}
|
|
114
|
+
const message = res.data?.errors?.[0]?.long_message ||
|
|
115
|
+
res.data?.errors?.[0]?.message ||
|
|
116
|
+
`HTTP ${res.status}`;
|
|
117
|
+
return { success: false, error: message };
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
error: err instanceof Error ? err.message : "Network error",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -6,21 +6,8 @@ function authLayoutTemplate() {
|
|
|
6
6
|
|
|
7
7
|
export default function AuthLayout() {
|
|
8
8
|
return (
|
|
9
|
-
<Stack>
|
|
10
|
-
<Stack.Screen
|
|
11
|
-
name="sign-in"
|
|
12
|
-
options={{
|
|
13
|
-
title: "Sign In",
|
|
14
|
-
headerBackTitle: "Back",
|
|
15
|
-
}}
|
|
16
|
-
/>
|
|
17
|
-
<Stack.Screen
|
|
18
|
-
name="sign-up"
|
|
19
|
-
options={{
|
|
20
|
-
title: "Sign Up",
|
|
21
|
-
headerBackTitle: "Back",
|
|
22
|
-
}}
|
|
23
|
-
/>
|
|
9
|
+
<Stack screenOptions={{ headerShown: false }}>
|
|
10
|
+
<Stack.Screen name="sign-in" />
|
|
24
11
|
</Stack>
|
|
25
12
|
);
|
|
26
13
|
}
|
|
@@ -13,6 +13,8 @@ export default function RootLayout() {
|
|
|
13
13
|
return (
|
|
14
14
|
<AuthProvider>
|
|
15
15
|
<Stack screenOptions={{ headerShown: false }}>
|
|
16
|
+
<Stack.Screen name="(tabs)" />
|
|
17
|
+
<Stack.Screen name="(auth)" />
|
|
16
18
|
<Stack.Screen name="+not-found" />
|
|
17
19
|
</Stack>
|
|
18
20
|
<StatusBar style="auto" />
|
|
@@ -32,17 +32,11 @@ export default function SettingsScreen() {
|
|
|
32
32
|
const handleSignOut = async () => {
|
|
33
33
|
if (Platform.OS === "web") {
|
|
34
34
|
const confirmed = window.confirm("Are you sure you want to sign out?");
|
|
35
|
-
if (confirmed)
|
|
36
|
-
await performSignOut();
|
|
37
|
-
}
|
|
35
|
+
if (confirmed) await performSignOut();
|
|
38
36
|
} else {
|
|
39
37
|
Alert.alert("Sign Out", "Are you sure you want to sign out?", [
|
|
40
38
|
{ text: "Cancel", style: "cancel" },
|
|
41
|
-
{
|
|
42
|
-
text: "Sign Out",
|
|
43
|
-
style: "destructive",
|
|
44
|
-
onPress: performSignOut,
|
|
45
|
-
},
|
|
39
|
+
{ text: "Sign Out", style: "destructive", onPress: performSignOut },
|
|
46
40
|
]);
|
|
47
41
|
}
|
|
48
42
|
};
|
|
@@ -56,18 +50,12 @@ export default function SettingsScreen() {
|
|
|
56
50
|
SecureStore.deleteItemAsync("__clerk_db_jwt"),
|
|
57
51
|
SecureStore.deleteItemAsync("__clerk_session_jwt"),
|
|
58
52
|
]);
|
|
59
|
-
} catch {
|
|
60
|
-
// Token cleanup errors are ignorable
|
|
61
|
-
}
|
|
53
|
+
} catch {}
|
|
62
54
|
|
|
63
55
|
await signOut();
|
|
64
56
|
} catch (error) {
|
|
65
57
|
console.error("Sign out error:", error);
|
|
66
|
-
|
|
67
|
-
window.alert("Failed to sign out. Please try again.");
|
|
68
|
-
} else {
|
|
69
|
-
Alert.alert("Error", "Failed to sign out. Please try again.");
|
|
70
|
-
}
|
|
58
|
+
Alert.alert("Error", "Failed to sign out. Please try again.");
|
|
71
59
|
} finally {
|
|
72
60
|
setIsSigningOut(false);
|
|
73
61
|
}
|
|
@@ -125,7 +113,7 @@ export default function SettingsScreen() {
|
|
|
125
113
|
<View style={styles.section}>
|
|
126
114
|
<Text style={styles.sectionTitle}>Account</Text>
|
|
127
115
|
|
|
128
|
-
<TouchableOpacity style={styles.menuItem}>
|
|
116
|
+
<TouchableOpacity style={styles.menuItem} activeOpacity={0.7}>
|
|
129
117
|
<Shield size={20} color="#6B7280" strokeWidth={2} />
|
|
130
118
|
<Text style={styles.menuText}>Privacy & Security</Text>
|
|
131
119
|
</TouchableOpacity>
|
|
@@ -158,9 +146,6 @@ export default function SettingsScreen() {
|
|
|
158
146
|
<Text style={styles.sectionText}>
|
|
159
147
|
Built with Expo, Convex, and Clerk
|
|
160
148
|
</Text>
|
|
161
|
-
<Text style={styles.sectionText}>
|
|
162
|
-
Real-time sync across all your devices
|
|
163
|
-
</Text>
|
|
164
149
|
</View>
|
|
165
150
|
</View>
|
|
166
151
|
</SafeAreaView>
|
|
@@ -178,9 +163,9 @@ const styles = StyleSheet.create({
|
|
|
178
163
|
alignItems: "center",
|
|
179
164
|
},
|
|
180
165
|
header: {
|
|
181
|
-
paddingHorizontal:
|
|
182
|
-
paddingTop:
|
|
183
|
-
paddingBottom:
|
|
166
|
+
paddingHorizontal: 16,
|
|
167
|
+
paddingTop: 24,
|
|
168
|
+
paddingBottom: 16,
|
|
184
169
|
},
|
|
185
170
|
title: {
|
|
186
171
|
fontSize: 28,
|
|
@@ -189,32 +174,32 @@ const styles = StyleSheet.create({
|
|
|
189
174
|
},
|
|
190
175
|
content: {
|
|
191
176
|
flex: 1,
|
|
192
|
-
paddingHorizontal:
|
|
193
|
-
paddingBottom:
|
|
177
|
+
paddingHorizontal: 16,
|
|
178
|
+
paddingBottom: 96,
|
|
194
179
|
},
|
|
195
180
|
profileSection: {
|
|
196
181
|
marginBottom: 32,
|
|
197
182
|
},
|
|
198
183
|
profileCard: {
|
|
199
184
|
backgroundColor: "#F8F9FA",
|
|
200
|
-
borderRadius:
|
|
185
|
+
borderRadius: 16,
|
|
201
186
|
padding: 16,
|
|
202
187
|
flexDirection: "row",
|
|
203
188
|
alignItems: "center",
|
|
204
189
|
},
|
|
205
190
|
avatarContainer: {
|
|
206
|
-
width:
|
|
207
|
-
height:
|
|
208
|
-
borderRadius:
|
|
191
|
+
width: 56,
|
|
192
|
+
height: 56,
|
|
193
|
+
borderRadius: 28,
|
|
209
194
|
backgroundColor: "#E5E7EB",
|
|
210
195
|
alignItems: "center",
|
|
211
196
|
justifyContent: "center",
|
|
212
197
|
marginRight: 16,
|
|
213
198
|
},
|
|
214
199
|
avatar: {
|
|
215
|
-
width:
|
|
216
|
-
height:
|
|
217
|
-
borderRadius:
|
|
200
|
+
width: 56,
|
|
201
|
+
height: 56,
|
|
202
|
+
borderRadius: 28,
|
|
218
203
|
},
|
|
219
204
|
profileInfo: {
|
|
220
205
|
flex: 1,
|
|
@@ -232,7 +217,7 @@ const styles = StyleSheet.create({
|
|
|
232
217
|
profileEmail: {
|
|
233
218
|
fontSize: 14,
|
|
234
219
|
color: "#6B7280",
|
|
235
|
-
marginLeft:
|
|
220
|
+
marginLeft: 8,
|
|
236
221
|
},
|
|
237
222
|
section: {
|
|
238
223
|
marginBottom: 24,
|
|
@@ -251,10 +236,11 @@ const styles = StyleSheet.create({
|
|
|
251
236
|
menuItem: {
|
|
252
237
|
flexDirection: "row",
|
|
253
238
|
alignItems: "center",
|
|
239
|
+
minHeight: 48,
|
|
254
240
|
paddingVertical: 12,
|
|
255
241
|
paddingHorizontal: 16,
|
|
256
242
|
backgroundColor: "#F8F9FA",
|
|
257
|
-
borderRadius:
|
|
243
|
+
borderRadius: 12,
|
|
258
244
|
marginBottom: 8,
|
|
259
245
|
},
|
|
260
246
|
menuText: {
|
|
@@ -6,195 +6,203 @@ function signInTemplate() {
|
|
|
6
6
|
import {
|
|
7
7
|
View,
|
|
8
8
|
Text,
|
|
9
|
-
TextInput,
|
|
10
9
|
TouchableOpacity,
|
|
11
10
|
StyleSheet,
|
|
12
|
-
KeyboardAvoidingView,
|
|
13
|
-
Platform,
|
|
14
11
|
Alert,
|
|
15
12
|
ActivityIndicator,
|
|
13
|
+
Platform,
|
|
16
14
|
} from "react-native";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
15
|
+
import { SafeAreaView } from "react-native-safe-area-context";
|
|
16
|
+
import { useOAuth } from "@clerk/clerk-expo";
|
|
17
|
+
import { useRouter } from "expo-router";
|
|
18
|
+
import { SquareCheck as CheckSquare } from "lucide-react-native";
|
|
19
|
+
import * as WebBrowser from "expo-web-browser";
|
|
20
|
+
|
|
21
|
+
WebBrowser.maybeCompleteAuthSession();
|
|
19
22
|
|
|
20
23
|
export default function SignInScreen() {
|
|
21
|
-
const { signIn, setActive, isLoaded } = useSignIn();
|
|
22
24
|
const router = useRouter();
|
|
25
|
+
const { startOAuthFlow: startGoogleOAuth } = useOAuth({
|
|
26
|
+
strategy: "oauth_google",
|
|
27
|
+
});
|
|
28
|
+
const { startOAuthFlow: startAppleOAuth } = useOAuth({
|
|
29
|
+
strategy: "oauth_apple",
|
|
30
|
+
});
|
|
23
31
|
|
|
24
|
-
const [
|
|
25
|
-
const [password, setPassword] = useState("");
|
|
26
|
-
const [loading, setLoading] = useState(false);
|
|
27
|
-
const [errorMessage, setErrorMessage] = useState("");
|
|
32
|
+
const [loading, setLoading] = useState<"google" | "apple" | null>(null);
|
|
28
33
|
|
|
29
|
-
const
|
|
30
|
-
|
|
34
|
+
const handleOAuth = async (
|
|
35
|
+
provider: "google" | "apple",
|
|
36
|
+
startFlow: typeof startGoogleOAuth
|
|
37
|
+
) => {
|
|
38
|
+
if (loading) return;
|
|
39
|
+
setLoading(provider);
|
|
31
40
|
|
|
32
|
-
setLoading(true);
|
|
33
|
-
setErrorMessage("");
|
|
34
41
|
try {
|
|
35
|
-
const
|
|
36
|
-
identifier: emailAddress,
|
|
37
|
-
password,
|
|
38
|
-
});
|
|
42
|
+
const { createdSessionId, setActive } = await startFlow();
|
|
39
43
|
|
|
40
|
-
if (
|
|
41
|
-
await setActive({ session:
|
|
44
|
+
if (createdSessionId && setActive) {
|
|
45
|
+
await setActive({ session: createdSessionId });
|
|
42
46
|
router.replace("/(tabs)");
|
|
43
|
-
} else {
|
|
44
|
-
setErrorMessage("Sign in failed. Please try again.");
|
|
45
47
|
}
|
|
46
48
|
} catch (err: any) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (errorCode === "form_identifier_not_found") {
|
|
51
|
-
message = "No account found with this email.";
|
|
52
|
-
} else if (errorCode === "form_password_incorrect") {
|
|
53
|
-
message = "Incorrect password. Please try again.";
|
|
54
|
-
} else if (err.errors?.[0]?.message) {
|
|
55
|
-
message = err.errors[0].message;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
setErrorMessage(message);
|
|
49
|
+
console.error(\`\${provider} OAuth error:\`, err);
|
|
50
|
+
const message =
|
|
51
|
+
err?.errors?.[0]?.message || "Sign in failed. Please try again.";
|
|
59
52
|
Alert.alert("Sign In Failed", message);
|
|
60
53
|
} finally {
|
|
61
|
-
setLoading(
|
|
54
|
+
setLoading(null);
|
|
62
55
|
}
|
|
63
56
|
};
|
|
64
57
|
|
|
65
58
|
return (
|
|
66
|
-
<
|
|
67
|
-
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
68
|
-
style={styles.container}
|
|
69
|
-
>
|
|
59
|
+
<SafeAreaView style={styles.container}>
|
|
70
60
|
<View style={styles.content}>
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
{errorMessage ? (
|
|
75
|
-
<View style={styles.errorContainer}>
|
|
76
|
-
<Text style={styles.errorText}>{errorMessage}</Text>
|
|
61
|
+
<View style={styles.branding}>
|
|
62
|
+
<View style={styles.logoContainer}>
|
|
63
|
+
<CheckSquare size={40} color="#1F2937" strokeWidth={2} />
|
|
77
64
|
</View>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
placeholderTextColor="#999"
|
|
84
|
-
value={emailAddress}
|
|
85
|
-
onChangeText={setEmailAddress}
|
|
86
|
-
autoCapitalize="none"
|
|
87
|
-
keyboardType="email-address"
|
|
88
|
-
/>
|
|
89
|
-
|
|
90
|
-
<TextInput
|
|
91
|
-
style={styles.input}
|
|
92
|
-
placeholder="Password"
|
|
93
|
-
placeholderTextColor="#999"
|
|
94
|
-
value={password}
|
|
95
|
-
onChangeText={setPassword}
|
|
96
|
-
secureTextEntry
|
|
97
|
-
/>
|
|
65
|
+
<Text style={styles.title}>TaskSync</Text>
|
|
66
|
+
<Text style={styles.subtitle}>
|
|
67
|
+
Your tasks, synced across all devices
|
|
68
|
+
</Text>
|
|
69
|
+
</View>
|
|
98
70
|
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
71
|
+
<View style={styles.buttons}>
|
|
72
|
+
<TouchableOpacity
|
|
73
|
+
style={[
|
|
74
|
+
styles.button,
|
|
75
|
+
styles.googleButton,
|
|
76
|
+
loading === "google" && styles.buttonLoading,
|
|
77
|
+
]}
|
|
78
|
+
onPress={() => handleOAuth("google", startGoogleOAuth)}
|
|
79
|
+
activeOpacity={0.7}
|
|
80
|
+
disabled={loading !== null}
|
|
81
|
+
>
|
|
82
|
+
{loading === "google" ? (
|
|
83
|
+
<ActivityIndicator size={20} color="#1F2937" />
|
|
84
|
+
) : (
|
|
85
|
+
<>
|
|
86
|
+
<Text style={styles.googleIcon}>G</Text>
|
|
87
|
+
<Text style={styles.googleText}>Continue with Google</Text>
|
|
88
|
+
</>
|
|
89
|
+
)}
|
|
90
|
+
</TouchableOpacity>
|
|
110
91
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
92
|
+
<TouchableOpacity
|
|
93
|
+
style={[
|
|
94
|
+
styles.button,
|
|
95
|
+
styles.appleButton,
|
|
96
|
+
loading === "apple" && styles.buttonLoading,
|
|
97
|
+
]}
|
|
98
|
+
onPress={() => handleOAuth("apple", startAppleOAuth)}
|
|
99
|
+
activeOpacity={0.7}
|
|
100
|
+
disabled={loading !== null}
|
|
101
|
+
>
|
|
102
|
+
{loading === "apple" ? (
|
|
103
|
+
<ActivityIndicator size={20} color="#FFFFFF" />
|
|
104
|
+
) : (
|
|
105
|
+
<>
|
|
106
|
+
<Text style={styles.appleIcon}>\u{F8FF}</Text>
|
|
107
|
+
<Text style={styles.appleText}>Continue with Apple</Text>
|
|
108
|
+
</>
|
|
109
|
+
)}
|
|
110
|
+
</TouchableOpacity>
|
|
118
111
|
</View>
|
|
112
|
+
|
|
113
|
+
<Text style={styles.terms}>
|
|
114
|
+
By continuing, you agree to our Terms of Service and Privacy Policy
|
|
115
|
+
</Text>
|
|
119
116
|
</View>
|
|
120
|
-
</
|
|
117
|
+
</SafeAreaView>
|
|
121
118
|
);
|
|
122
119
|
}
|
|
123
120
|
|
|
124
121
|
const styles = StyleSheet.create({
|
|
125
122
|
container: {
|
|
126
123
|
flex: 1,
|
|
127
|
-
backgroundColor: "#
|
|
124
|
+
backgroundColor: "#FFFFFF",
|
|
128
125
|
},
|
|
129
126
|
content: {
|
|
130
127
|
flex: 1,
|
|
131
128
|
justifyContent: "center",
|
|
132
|
-
paddingHorizontal:
|
|
129
|
+
paddingHorizontal: 24,
|
|
130
|
+
},
|
|
131
|
+
branding: {
|
|
132
|
+
alignItems: "center",
|
|
133
|
+
marginBottom: 64,
|
|
134
|
+
},
|
|
135
|
+
logoContainer: {
|
|
136
|
+
width: 80,
|
|
137
|
+
height: 80,
|
|
138
|
+
borderRadius: 20,
|
|
139
|
+
backgroundColor: "#F3F4F6",
|
|
140
|
+
alignItems: "center",
|
|
141
|
+
justifyContent: "center",
|
|
142
|
+
marginBottom: 24,
|
|
133
143
|
},
|
|
134
144
|
title: {
|
|
135
145
|
fontSize: 32,
|
|
136
|
-
fontWeight: "
|
|
137
|
-
color: "#
|
|
146
|
+
fontWeight: "700",
|
|
147
|
+
color: "#1F2937",
|
|
138
148
|
marginBottom: 8,
|
|
139
|
-
textAlign: "center",
|
|
140
149
|
},
|
|
141
150
|
subtitle: {
|
|
142
151
|
fontSize: 16,
|
|
143
|
-
color: "#
|
|
144
|
-
marginBottom: 40,
|
|
152
|
+
color: "#6B7280",
|
|
145
153
|
textAlign: "center",
|
|
154
|
+
lineHeight: 24,
|
|
146
155
|
},
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
borderRadius: 8,
|
|
150
|
-
paddingHorizontal: 16,
|
|
151
|
-
paddingVertical: 12,
|
|
152
|
-
fontSize: 16,
|
|
153
|
-
marginBottom: 16,
|
|
154
|
-
borderWidth: 1,
|
|
155
|
-
borderColor: "#e0e0e0",
|
|
156
|
+
buttons: {
|
|
157
|
+
gap: 12,
|
|
156
158
|
},
|
|
157
159
|
button: {
|
|
158
|
-
|
|
159
|
-
borderRadius: 8,
|
|
160
|
-
paddingVertical: 14,
|
|
160
|
+
flexDirection: "row",
|
|
161
161
|
alignItems: "center",
|
|
162
|
-
|
|
162
|
+
justifyContent: "center",
|
|
163
|
+
height: 56,
|
|
164
|
+
borderRadius: 12,
|
|
165
|
+
paddingHorizontal: 24,
|
|
163
166
|
},
|
|
164
|
-
|
|
167
|
+
buttonLoading: {
|
|
165
168
|
opacity: 0.7,
|
|
166
169
|
},
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
googleButton: {
|
|
171
|
+
backgroundColor: "#FFFFFF",
|
|
172
|
+
borderWidth: 1.5,
|
|
173
|
+
borderColor: "#E5E7EB",
|
|
174
|
+
},
|
|
175
|
+
googleIcon: {
|
|
176
|
+
fontSize: 20,
|
|
177
|
+
fontWeight: "700",
|
|
178
|
+
color: "#4285F4",
|
|
179
|
+
marginRight: 12,
|
|
180
|
+
},
|
|
181
|
+
googleText: {
|
|
182
|
+
fontSize: 16,
|
|
170
183
|
fontWeight: "600",
|
|
184
|
+
color: "#1F2937",
|
|
171
185
|
},
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
justifyContent: "center",
|
|
175
|
-
marginTop: 24,
|
|
186
|
+
appleButton: {
|
|
187
|
+
backgroundColor: "#000000",
|
|
176
188
|
},
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
189
|
+
appleIcon: {
|
|
190
|
+
fontSize: 20,
|
|
191
|
+
color: "#FFFFFF",
|
|
192
|
+
marginRight: 12,
|
|
180
193
|
},
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
fontSize: 14,
|
|
194
|
+
appleText: {
|
|
195
|
+
fontSize: 16,
|
|
184
196
|
fontWeight: "600",
|
|
197
|
+
color: "#FFFFFF",
|
|
185
198
|
},
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
padding: 12,
|
|
190
|
-
marginBottom: 16,
|
|
191
|
-
borderWidth: 1,
|
|
192
|
-
borderColor: "#ef5350",
|
|
193
|
-
},
|
|
194
|
-
errorText: {
|
|
195
|
-
color: "#c62828",
|
|
196
|
-
fontSize: 14,
|
|
199
|
+
terms: {
|
|
200
|
+
fontSize: 13,
|
|
201
|
+
color: "#9CA3AF",
|
|
197
202
|
textAlign: "center",
|
|
203
|
+
marginTop: 32,
|
|
204
|
+
lineHeight: 20,
|
|
205
|
+
paddingHorizontal: 16,
|
|
198
206
|
},
|
|
199
207
|
});
|
|
200
208
|
`;
|
|
@@ -27,10 +27,9 @@ export default function TabLayout() {
|
|
|
27
27
|
borderTopWidth: 0,
|
|
28
28
|
elevation: 0,
|
|
29
29
|
shadowOpacity: 0,
|
|
30
|
-
height: Platform.OS === "ios" ?
|
|
31
|
-
paddingBottom: Platform.OS === "ios" ?
|
|
30
|
+
height: Platform.OS === "ios" ? 88 : 72,
|
|
31
|
+
paddingBottom: Platform.OS === "ios" ? 32 : 12,
|
|
32
32
|
paddingTop: 12,
|
|
33
|
-
borderTopColor: "transparent",
|
|
34
33
|
},
|
|
35
34
|
tabBarActiveTintColor: "#1F2937",
|
|
36
35
|
tabBarInactiveTintColor: "#9CA3AF",
|
|
@@ -38,10 +37,6 @@ export default function TabLayout() {
|
|
|
38
37
|
fontSize: 13,
|
|
39
38
|
fontWeight: "500",
|
|
40
39
|
marginTop: 4,
|
|
41
|
-
marginBottom: Platform.OS === "ios" ? 0 : 2,
|
|
42
|
-
},
|
|
43
|
-
tabBarIconStyle: {
|
|
44
|
-
marginBottom: Platform.OS === "ios" ? 2 : 0,
|
|
45
40
|
},
|
|
46
41
|
}}
|
|
47
42
|
>
|
|
@@ -3,13 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.todosScreenTemplate = todosScreenTemplate;
|
|
4
4
|
function todosScreenTemplate() {
|
|
5
5
|
return `import React, { useState } from "react";
|
|
6
|
-
import {
|
|
7
|
-
View,
|
|
8
|
-
Text,
|
|
9
|
-
ScrollView,
|
|
10
|
-
StyleSheet,
|
|
11
|
-
StatusBar,
|
|
12
|
-
} from "react-native";
|
|
6
|
+
import { View, Text, ScrollView, StyleSheet, StatusBar } from "react-native";
|
|
13
7
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
14
8
|
import { AddTodoForm } from "@/components/AddTodoForm";
|
|
15
9
|
import { TodoItem } from "@/components/TodoItem";
|
|
@@ -130,9 +124,9 @@ const styles = StyleSheet.create({
|
|
|
130
124
|
backgroundColor: "#FFFFFF",
|
|
131
125
|
},
|
|
132
126
|
header: {
|
|
133
|
-
paddingHorizontal:
|
|
134
|
-
paddingTop:
|
|
135
|
-
paddingBottom:
|
|
127
|
+
paddingHorizontal: 16,
|
|
128
|
+
paddingTop: 24,
|
|
129
|
+
paddingBottom: 16,
|
|
136
130
|
},
|
|
137
131
|
title: {
|
|
138
132
|
fontSize: 28,
|
|
@@ -147,10 +141,10 @@ const styles = StyleSheet.create({
|
|
|
147
141
|
},
|
|
148
142
|
todosContainer: {
|
|
149
143
|
flex: 1,
|
|
150
|
-
paddingHorizontal:
|
|
144
|
+
paddingHorizontal: 16,
|
|
151
145
|
},
|
|
152
146
|
todosContent: {
|
|
153
|
-
paddingBottom:
|
|
147
|
+
paddingBottom: 96,
|
|
154
148
|
flexGrow: 1,
|
|
155
149
|
},
|
|
156
150
|
});
|
|
@@ -37,11 +37,12 @@ export function AddTodoForm({ onAddTodo }: AddTodoFormProps) {
|
|
|
37
37
|
style={[styles.addButton, !text.trim() && styles.addButtonDisabled]}
|
|
38
38
|
onPress={handleSubmit}
|
|
39
39
|
disabled={!text.trim()}
|
|
40
|
+
activeOpacity={0.7}
|
|
40
41
|
>
|
|
41
42
|
<Plus
|
|
42
43
|
size={20}
|
|
43
44
|
color={text.trim() ? "#FFFFFF" : "#9CA3AF"}
|
|
44
|
-
strokeWidth={2}
|
|
45
|
+
strokeWidth={2.5}
|
|
45
46
|
/>
|
|
46
47
|
</TouchableOpacity>
|
|
47
48
|
</View>
|
|
@@ -51,8 +52,8 @@ export function AddTodoForm({ onAddTodo }: AddTodoFormProps) {
|
|
|
51
52
|
|
|
52
53
|
const styles = StyleSheet.create({
|
|
53
54
|
container: {
|
|
54
|
-
paddingHorizontal:
|
|
55
|
-
marginBottom:
|
|
55
|
+
paddingHorizontal: 16,
|
|
56
|
+
marginBottom: 16,
|
|
56
57
|
},
|
|
57
58
|
inputContainer: {
|
|
58
59
|
flexDirection: "row",
|
|
@@ -61,21 +62,22 @@ const styles = StyleSheet.create({
|
|
|
61
62
|
borderRadius: 12,
|
|
62
63
|
borderWidth: 1,
|
|
63
64
|
borderColor: "#E5E7EB",
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
paddingLeft: 16,
|
|
66
|
+
paddingRight: 8,
|
|
67
|
+
minHeight: 56,
|
|
66
68
|
},
|
|
67
69
|
input: {
|
|
68
70
|
flex: 1,
|
|
69
71
|
fontSize: 16,
|
|
70
72
|
color: "#1F2937",
|
|
71
|
-
paddingVertical:
|
|
73
|
+
paddingVertical: 16,
|
|
72
74
|
paddingRight: 12,
|
|
73
75
|
},
|
|
74
76
|
addButton: {
|
|
75
77
|
backgroundColor: "#1F2937",
|
|
76
|
-
borderRadius:
|
|
77
|
-
width:
|
|
78
|
-
height:
|
|
78
|
+
borderRadius: 10,
|
|
79
|
+
width: 40,
|
|
80
|
+
height: 40,
|
|
79
81
|
alignItems: "center",
|
|
80
82
|
justifyContent: "center",
|
|
81
83
|
},
|
|
@@ -17,33 +17,30 @@ interface EmptyStateProps {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function EmptyState({ filter }: EmptyStateProps) {
|
|
20
|
-
const
|
|
20
|
+
const getContent = () => {
|
|
21
21
|
switch (filter) {
|
|
22
22
|
case "active":
|
|
23
23
|
return {
|
|
24
24
|
icon: <CheckCircle size={48} color="#D1D5DB" strokeWidth={1.5} />,
|
|
25
25
|
title: "All caught up!",
|
|
26
|
-
description:
|
|
27
|
-
"You have no active tasks. Time to add some new ones or take a well-deserved break.",
|
|
26
|
+
description: "No active tasks. Add some new ones or take a break.",
|
|
28
27
|
};
|
|
29
28
|
case "completed":
|
|
30
29
|
return {
|
|
31
30
|
icon: <Circle size={48} color="#D1D5DB" strokeWidth={1.5} />,
|
|
32
31
|
title: "No completed tasks",
|
|
33
|
-
description:
|
|
34
|
-
"Tasks you complete will appear here. Start checking off some items!",
|
|
32
|
+
description: "Tasks you complete will appear here.",
|
|
35
33
|
};
|
|
36
34
|
default:
|
|
37
35
|
return {
|
|
38
36
|
icon: <ListTodo size={48} color="#D1D5DB" strokeWidth={1.5} />,
|
|
39
37
|
title: "No tasks yet",
|
|
40
|
-
description:
|
|
41
|
-
"Add your first task above to get started with organizing your day.",
|
|
38
|
+
description: "Add your first task above to get started.",
|
|
42
39
|
};
|
|
43
40
|
}
|
|
44
41
|
};
|
|
45
42
|
|
|
46
|
-
const content =
|
|
43
|
+
const content = getContent();
|
|
47
44
|
|
|
48
45
|
return (
|
|
49
46
|
<View style={styles.container}>
|
|
@@ -59,7 +56,7 @@ const styles = StyleSheet.create({
|
|
|
59
56
|
flex: 1,
|
|
60
57
|
alignItems: "center",
|
|
61
58
|
justifyContent: "center",
|
|
62
|
-
paddingHorizontal:
|
|
59
|
+
paddingHorizontal: 48,
|
|
63
60
|
paddingVertical: 80,
|
|
64
61
|
},
|
|
65
62
|
iconContainer: {
|
|
@@ -55,11 +55,10 @@ export function FilterTabs({ filter, onFilterChange, counts }: FilterTabsProps)
|
|
|
55
55
|
const styles = StyleSheet.create({
|
|
56
56
|
container: {
|
|
57
57
|
flexDirection: "row",
|
|
58
|
-
paddingHorizontal: 24,
|
|
59
58
|
marginBottom: 16,
|
|
60
|
-
backgroundColor: "#
|
|
59
|
+
backgroundColor: "#F3F4F6",
|
|
61
60
|
borderRadius: 12,
|
|
62
|
-
marginHorizontal:
|
|
61
|
+
marginHorizontal: 16,
|
|
63
62
|
padding: 4,
|
|
64
63
|
},
|
|
65
64
|
tab: {
|
|
@@ -70,37 +69,37 @@ const styles = StyleSheet.create({
|
|
|
70
69
|
paddingVertical: 12,
|
|
71
70
|
paddingHorizontal: 8,
|
|
72
71
|
borderRadius: 8,
|
|
73
|
-
minHeight:
|
|
72
|
+
minHeight: 48,
|
|
74
73
|
},
|
|
75
74
|
tabActive: {
|
|
76
75
|
backgroundColor: "#FFFFFF",
|
|
77
76
|
shadowColor: "#000000",
|
|
78
77
|
shadowOffset: { width: 0, height: 1 },
|
|
79
|
-
shadowOpacity: 0.
|
|
80
|
-
shadowRadius:
|
|
81
|
-
elevation:
|
|
78
|
+
shadowOpacity: 0.06,
|
|
79
|
+
shadowRadius: 3,
|
|
80
|
+
elevation: 2,
|
|
82
81
|
},
|
|
83
82
|
tabText: {
|
|
84
|
-
fontSize:
|
|
83
|
+
fontSize: 14,
|
|
85
84
|
fontWeight: "500",
|
|
86
85
|
color: "#6B7280",
|
|
87
86
|
marginRight: 4,
|
|
88
|
-
flexShrink: 0,
|
|
89
87
|
},
|
|
90
88
|
tabTextActive: {
|
|
91
89
|
color: "#1F2937",
|
|
90
|
+
fontWeight: "600",
|
|
92
91
|
},
|
|
93
92
|
tabCount: {
|
|
94
|
-
fontSize:
|
|
93
|
+
fontSize: 12,
|
|
95
94
|
fontWeight: "600",
|
|
96
95
|
color: "#9CA3AF",
|
|
97
96
|
backgroundColor: "#E5E7EB",
|
|
98
97
|
borderRadius: 10,
|
|
99
|
-
paddingHorizontal:
|
|
100
|
-
paddingVertical:
|
|
101
|
-
minWidth:
|
|
98
|
+
paddingHorizontal: 6,
|
|
99
|
+
paddingVertical: 2,
|
|
100
|
+
minWidth: 20,
|
|
102
101
|
textAlign: "center",
|
|
103
|
-
|
|
102
|
+
overflow: "hidden",
|
|
104
103
|
},
|
|
105
104
|
tabCountActive: {
|
|
106
105
|
color: "#1F2937",
|
|
@@ -18,7 +18,7 @@ export function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
|
|
|
18
18
|
return (
|
|
19
19
|
<View style={styles.container}>
|
|
20
20
|
<TouchableOpacity
|
|
21
|
-
style={styles.
|
|
21
|
+
style={styles.checkboxHitArea}
|
|
22
22
|
onPress={() => onToggle(todo._id)}
|
|
23
23
|
activeOpacity={0.7}
|
|
24
24
|
>
|
|
@@ -26,7 +26,7 @@ export function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
|
|
|
26
26
|
style={[styles.checkbox, todo.completed && styles.checkboxCompleted]}
|
|
27
27
|
>
|
|
28
28
|
{todo.completed && (
|
|
29
|
-
<Check size={
|
|
29
|
+
<Check size={14} color="#FFFFFF" strokeWidth={3} />
|
|
30
30
|
)}
|
|
31
31
|
</View>
|
|
32
32
|
</TouchableOpacity>
|
|
@@ -40,7 +40,7 @@ export function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
|
|
|
40
40
|
</View>
|
|
41
41
|
|
|
42
42
|
<TouchableOpacity
|
|
43
|
-
style={styles.
|
|
43
|
+
style={styles.deleteHitArea}
|
|
44
44
|
onPress={() => onDelete(todo._id)}
|
|
45
45
|
activeOpacity={0.7}
|
|
46
46
|
>
|
|
@@ -56,19 +56,24 @@ const styles = StyleSheet.create({
|
|
|
56
56
|
borderRadius: 12,
|
|
57
57
|
borderWidth: 1,
|
|
58
58
|
borderColor: "#E5E7EB",
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
paddingLeft: 4,
|
|
60
|
+
paddingRight: 4,
|
|
61
|
+
paddingVertical: 4,
|
|
61
62
|
marginBottom: 8,
|
|
62
63
|
flexDirection: "row",
|
|
63
64
|
alignItems: "center",
|
|
65
|
+
minHeight: 56,
|
|
64
66
|
},
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
checkboxHitArea: {
|
|
68
|
+
width: 48,
|
|
69
|
+
height: 48,
|
|
70
|
+
alignItems: "center",
|
|
71
|
+
justifyContent: "center",
|
|
67
72
|
},
|
|
68
73
|
checkbox: {
|
|
69
|
-
width:
|
|
70
|
-
height:
|
|
71
|
-
borderRadius:
|
|
74
|
+
width: 24,
|
|
75
|
+
height: 24,
|
|
76
|
+
borderRadius: 6,
|
|
72
77
|
borderWidth: 2,
|
|
73
78
|
borderColor: "#D1D5DB",
|
|
74
79
|
alignItems: "center",
|
|
@@ -80,7 +85,7 @@ const styles = StyleSheet.create({
|
|
|
80
85
|
},
|
|
81
86
|
content: {
|
|
82
87
|
flex: 1,
|
|
83
|
-
|
|
88
|
+
paddingVertical: 12,
|
|
84
89
|
},
|
|
85
90
|
text: {
|
|
86
91
|
fontSize: 16,
|
|
@@ -91,8 +96,11 @@ const styles = StyleSheet.create({
|
|
|
91
96
|
color: "#9CA3AF",
|
|
92
97
|
textDecorationLine: "line-through",
|
|
93
98
|
},
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
deleteHitArea: {
|
|
100
|
+
width: 48,
|
|
101
|
+
height: 48,
|
|
102
|
+
alignItems: "center",
|
|
103
|
+
justifyContent: "center",
|
|
96
104
|
},
|
|
97
105
|
});
|
|
98
106
|
`;
|