@usequark/quark-create-app 1.23.11
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/README.md +210 -0
- package/package.json +56 -0
- package/src/dependabot-config.test.js +128 -0
- package/src/deploy/adapters/iac.js +203 -0
- package/src/deploy/adapters/index.js +27 -0
- package/src/deploy/adapters/railway.js +508 -0
- package/src/deploy/adapters/railway.test.js +318 -0
- package/src/deploy/contract.js +59 -0
- package/src/deploy/deploy.integration.test.js +183 -0
- package/src/deploy/deploy.js +430 -0
- package/src/deploy/deploy.test.js +105 -0
- package/src/deploy/discovery.js +115 -0
- package/src/deploy/discovery.test.js +141 -0
- package/src/deploy/index.js +38 -0
- package/src/deploy/inspect.js +130 -0
- package/src/index.js +2920 -0
- package/src/mobile-template.test.js +232 -0
- package/src/prepare-hooks.test.js +535 -0
- package/src/scaffold-guards.test.js +140 -0
- package/src/scaffold-output.test.js +228 -0
- package/src/template-scripts.test.js +160 -0
- package/src/utils.js +77 -0
- package/src/utils.test.js +95 -0
- package/src/worker-dockerfile.test.js +85 -0
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.dockerignore +14 -0
- package/templates/base-project/.env.railway.example +87 -0
- package/templates/base-project/.github/copilot-instructions.md +65 -0
- package/templates/base-project/.github/dependabot.yml +31 -0
- package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
- package/templates/base-project/.github/workflows/ci.yml +116 -0
- package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
- package/templates/base-project/.github/workflows/install-windows.yml +27 -0
- package/templates/base-project/.github/workflows/release.yml +67 -0
- package/templates/base-project/.railway/railway.ts +40 -0
- package/templates/base-project/CLAUDE.md +163 -0
- package/templates/base-project/MAIN.md +21 -0
- package/templates/base-project/README.md +164 -0
- package/templates/base-project/apps/web/.env.example +99 -0
- package/templates/base-project/apps/web/Dockerfile +38 -0
- package/templates/base-project/apps/web/biome.json +8 -0
- package/templates/base-project/apps/web/jsconfig.json +9 -0
- package/templates/base-project/apps/web/next.config.js +154 -0
- package/templates/base-project/apps/web/package.json +42 -0
- package/templates/base-project/apps/web/postcss.config.mjs +7 -0
- package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
- package/templates/base-project/apps/web/public/quark.svg +47 -0
- package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
- package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
- package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
- package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
- package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
- package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
- package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
- package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
- package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
- package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
- package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
- package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
- package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
- package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
- package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
- package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
- package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
- package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
- package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
- package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
- package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
- package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
- package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/global-error.js +58 -0
- package/templates/base-project/apps/web/src/app/globals.css +1166 -0
- package/templates/base-project/apps/web/src/app/icon.svg +47 -0
- package/templates/base-project/apps/web/src/app/layout.js +57 -0
- package/templates/base-project/apps/web/src/app/manifest.js +12 -0
- package/templates/base-project/apps/web/src/app/not-found.js +39 -0
- package/templates/base-project/apps/web/src/app/page.js +262 -0
- package/templates/base-project/apps/web/src/app/robots.js +23 -0
- package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
- package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
- package/templates/base-project/apps/web/src/lib/auth.js +191 -0
- package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
- package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
- package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
- package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
- package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
- package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
- package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
- package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
- package/templates/base-project/apps/web/src/preflight.js +55 -0
- package/templates/base-project/apps/web/src/proxy.js +328 -0
- package/templates/base-project/biome.json +52 -0
- package/templates/base-project/docker-compose.override.yml +17 -0
- package/templates/base-project/docker-compose.yml +37 -0
- package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
- package/templates/base-project/docs/START_HERE.md +70 -0
- package/templates/base-project/docs/adr/README.md +42 -0
- package/templates/base-project/package.json +58 -0
- package/templates/base-project/packages/db/package.json +38 -0
- package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
- package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
- package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
- package/templates/base-project/packages/db/prisma/seed.js +152 -0
- package/templates/base-project/packages/db/prisma.config.js +27 -0
- package/templates/base-project/packages/db/src/client.js +75 -0
- package/templates/base-project/packages/db/src/connection.js +44 -0
- package/templates/base-project/packages/db/src/context.js +275 -0
- package/templates/base-project/packages/db/src/index.js +5 -0
- package/templates/base-project/packages/db/src/ping.js +66 -0
- package/templates/base-project/packages/db/src/queries.js +292 -0
- package/templates/base-project/packages/db/src/schemas.js +30 -0
- package/templates/base-project/packages/db/src/umami.js +299 -0
- package/templates/base-project/pnpm-workspace.yaml +20 -0
- package/templates/base-project/scripts/check-loading.mjs +49 -0
- package/templates/base-project/scripts/check-standards.mjs +186 -0
- package/templates/base-project/scripts/clean-workspace.mjs +479 -0
- package/templates/base-project/scripts/dev-preflight.mjs +48 -0
- package/templates/base-project/scripts/doctor.js +408 -0
- package/templates/base-project/scripts/prepare.js +490 -0
- package/templates/base-project/scripts/run-tests.mjs +71 -0
- package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
- package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
- package/templates/base-project/skills/add-model/SKILL.md +28 -0
- package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
- package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
- package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
- package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
- package/templates/base-project/skills/i18n/SKILL.md +211 -0
- package/templates/base-project/skills/payment/SKILL.md +280 -0
- package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
- package/templates/base-project/turbo.json +143 -0
- package/templates/config/package.json +20 -0
- package/templates/config/src/app-url.js +134 -0
- package/templates/config/src/environment.js +284 -0
- package/templates/config/src/index.js +16 -0
- package/templates/config/src/load-config.js +157 -0
- package/templates/config/src/rate-limit.js +136 -0
- package/templates/config/src/redis.js +43 -0
- package/templates/config/src/validate-env.js +532 -0
- package/templates/jobs/README.md +52 -0
- package/templates/jobs/package.json +9 -0
- package/templates/jobs/src/definitions.js +13 -0
- package/templates/jobs/src/index.js +1 -0
- package/templates/mobile/.env.example +16 -0
- package/templates/mobile/CHECKLIST.md +40 -0
- package/templates/mobile/README.md +130 -0
- package/templates/mobile/app/(app)/_layout.tsx +83 -0
- package/templates/mobile/app/(app)/index.tsx +147 -0
- package/templates/mobile/app/(app)/profile.tsx +204 -0
- package/templates/mobile/app/(app)/settings.tsx +80 -0
- package/templates/mobile/app/(auth)/_layout.tsx +10 -0
- package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
- package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
- package/templates/mobile/app/+not-found.tsx +32 -0
- package/templates/mobile/app/_layout.tsx +18 -0
- package/templates/mobile/app.json +47 -0
- package/templates/mobile/assets/adaptive-icon.png +0 -0
- package/templates/mobile/assets/icon.png +0 -0
- package/templates/mobile/assets/notification-icon.png +0 -0
- package/templates/mobile/assets/splash.png +0 -0
- package/templates/mobile/biome.json +27 -0
- package/templates/mobile/components/AuthGate.tsx +19 -0
- package/templates/mobile/components/ThemeProvider.tsx +23 -0
- package/templates/mobile/eas.json +29 -0
- package/templates/mobile/hooks/use-auth.ts +127 -0
- package/templates/mobile/lib/api-client.ts +138 -0
- package/templates/mobile/lib/api.ts +83 -0
- package/templates/mobile/lib/auth-providers-google.ts +74 -0
- package/templates/mobile/lib/auth-providers.ts +74 -0
- package/templates/mobile/lib/auth.ts +100 -0
- package/templates/mobile/lib/config.ts +14 -0
- package/templates/mobile/lib/errors.ts +46 -0
- package/templates/mobile/lib/notifications.ts +77 -0
- package/templates/mobile/lib/storage.ts +25 -0
- package/templates/mobile/lib/theme-tokens.ts +16 -0
- package/templates/mobile/package.json +37 -0
- package/templates/mobile/tsconfig.json +10 -0
- package/templates/pwa/app/_components/PWARegister.js +15 -0
- package/templates/pwa/app/manifest.js +32 -0
- package/templates/pwa/public/sw.js +72 -0
- package/templates/ui/README.md +44 -0
- package/templates/ui/package.json +21 -0
- package/templates/ui/src/badge.js +23 -0
- package/templates/ui/src/button.js +111 -0
- package/templates/ui/src/card.js +128 -0
- package/templates/ui/src/checkbox.js +20 -0
- package/templates/ui/src/container.js +13 -0
- package/templates/ui/src/dialog.js +137 -0
- package/templates/ui/src/error-banner.js +15 -0
- package/templates/ui/src/footer.js +239 -0
- package/templates/ui/src/form-field.integration-test.js +142 -0
- package/templates/ui/src/form-field.js +50 -0
- package/templates/ui/src/index.js +24 -0
- package/templates/ui/src/input.js +11 -0
- package/templates/ui/src/label.js +14 -0
- package/templates/ui/src/lightbox.integration-test.js +167 -0
- package/templates/ui/src/lightbox.js +243 -0
- package/templates/ui/src/logo.js +52 -0
- package/templates/ui/src/navbar.js +602 -0
- package/templates/ui/src/password-input.js +31 -0
- package/templates/ui/src/rich-text.js +514 -0
- package/templates/ui/src/select.js +317 -0
- package/templates/ui/src/skeleton.js +10 -0
- package/templates/ui/src/spinner.js +31 -0
- package/templates/ui/src/table.js +169 -0
- package/templates/ui/src/textarea.js +11 -0
- package/templates/ui/src/theme-constants.js +24 -0
- package/templates/ui/src/theme.js +196 -0
- package/templates/ui/src/toast.js +206 -0
- package/templates/worker/Dockerfile +64 -0
- package/templates/worker/README.md +56 -0
- package/templates/worker/package.json +36 -0
- package/templates/worker/src/config/pricing.js +46 -0
- package/templates/worker/src/handlers/email-job-validation.js +36 -0
- package/templates/worker/src/handlers/email.js +98 -0
- package/templates/worker/src/handlers/files.js +59 -0
- package/templates/worker/src/handlers/index.js +20 -0
- package/templates/worker/src/handlers/push-notification.js +234 -0
- package/templates/worker/src/index.js +505 -0
- package/templates/worker/src/lib/railway.js +438 -0
- package/templates/worker/src/lib/tokens.js +81 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Quark Mobile App
|
|
2
|
+
|
|
3
|
+
Expo-based mobile client for Quark projects.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 1. Start the backend (web + worker) from project root
|
|
9
|
+
pnpm dev
|
|
10
|
+
|
|
11
|
+
# 2. In a second terminal — start the mobile app
|
|
12
|
+
cd apps/mobile
|
|
13
|
+
echo 'EXPO_PUBLIC_API_URL=http://localhost:3000' > .env
|
|
14
|
+
npx expo start
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Review on MacBook (Android Emulator)
|
|
18
|
+
|
|
19
|
+
Requires [Android Studio](https://developer.android.com/studio) with an AVD created.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# First time only: install dependencies + generate native dirs
|
|
23
|
+
npx expo prebuild --platform android
|
|
24
|
+
|
|
25
|
+
# Run on Android emulator
|
|
26
|
+
npx expo run:android
|
|
27
|
+
|
|
28
|
+
# Subsequent runs (fast — reuses build cache)
|
|
29
|
+
npx expo run:android
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Hot reload works out of the box. Shake menu (`Cmd+M`) to open dev tools.
|
|
33
|
+
|
|
34
|
+
## Review on Physical Android Phone
|
|
35
|
+
|
|
36
|
+
### Option A: Expo Go (quickest, no build)
|
|
37
|
+
|
|
38
|
+
1. Install **Expo Go** from the Play Store
|
|
39
|
+
2. Run `npx expo start` and scan the QR code
|
|
40
|
+
3. Limitation: `expo-secure-store` and `expo-notifications` behave differently in Expo Go — use for UI/layout review only
|
|
41
|
+
|
|
42
|
+
### Option B: Development Build (full fidelity)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# First time: generate native dirs + install on device via USB
|
|
46
|
+
npx expo prebuild --platform android
|
|
47
|
+
npx expo run:android --device
|
|
48
|
+
|
|
49
|
+
# Or use EAS Build for a wireless install
|
|
50
|
+
npx eas build --profile development --platform android
|
|
51
|
+
# Download the .apk from the EAS dashboard, install on phone
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Development builds support all native modules (`expo-secure-store`, `expo-notifications`) identically to production.
|
|
55
|
+
|
|
56
|
+
## Commands Reference
|
|
57
|
+
|
|
58
|
+
### Development
|
|
59
|
+
|
|
60
|
+
| Command | What it does |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `npx expo start` | Start Metro bundler (JS only, fastest) |
|
|
63
|
+
| `npx expo start --clear` | Start with cleared cache |
|
|
64
|
+
| `npx expo run:android` | Build + run on Android emulator/device |
|
|
65
|
+
| `npx expo run:android --device` | Build + run on connected physical device |
|
|
66
|
+
| `npx expo start --tunnel` | Expose local server over the internet (for testing on phone without same WiFi) |
|
|
67
|
+
|
|
68
|
+
### Production Builds
|
|
69
|
+
|
|
70
|
+
| Command | What it does |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `npx eas build --profile production --platform android` | Build release APK/AAB for Play Store |
|
|
73
|
+
| `npx eas build --profile production --platform ios` | Build release IPA for App Store |
|
|
74
|
+
| `npx eas build --platform all` | Build both platforms |
|
|
75
|
+
|
|
76
|
+
### OTA Updates
|
|
77
|
+
|
|
78
|
+
| Command | What it does |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `npx eas update --channel staging` | Push JS changes to staging channel |
|
|
81
|
+
| `npx eas update --channel production` | Push JS changes to production (live users) |
|
|
82
|
+
|
|
83
|
+
OTA is instant — no app store review needed for JS-only changes. See `CHECKLIST.md` for what qualifies.
|
|
84
|
+
|
|
85
|
+
### Scripts (from project root)
|
|
86
|
+
|
|
87
|
+
| Command | What it does |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `pnpm dev` | Start web + worker (API on :3000) |
|
|
90
|
+
| `pnpm build:mobile` | Export mobile app for preview |
|
|
91
|
+
|
|
92
|
+
## Environment
|
|
93
|
+
|
|
94
|
+
| Variable | Required | Description |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `EXPO_PUBLIC_API_URL` | Yes | Backend API URL (e.g. `http://localhost:3000` or `https://your-app.quark.dev`) |
|
|
97
|
+
| `EXPO_PUBLIC_GOOGLE_CLIENT_ID` | Optional | Google Sign-In client ID |
|
|
98
|
+
|
|
99
|
+
Set in `apps/mobile/.env`. The `EXPO_PUBLIC_` prefix is required by Expo for client-side access.
|
|
100
|
+
|
|
101
|
+
## Architecture
|
|
102
|
+
|
|
103
|
+
- **Expo Router v55** — File-based routing
|
|
104
|
+
- **expo-secure-store** — Secure token storage (never AsyncStorage)
|
|
105
|
+
- **Zod** — All validation at system boundaries
|
|
106
|
+
- **fetch** — HTTP client with auth injection + automatic 401 retry
|
|
107
|
+
|
|
108
|
+
## Auth Flow
|
|
109
|
+
|
|
110
|
+
1. Sign in via `/api/auth/token` → receives JWT + refresh token
|
|
111
|
+
2. Tokens stored in `expo-secure-store`
|
|
112
|
+
3. API calls include `Authorization: Bearer <token>` header
|
|
113
|
+
4. On 401, token refresh attempted via `/api/auth/refresh`
|
|
114
|
+
5. On refresh failure, auth cleared and user redirected to sign-in
|
|
115
|
+
|
|
116
|
+
## Push Notifications
|
|
117
|
+
|
|
118
|
+
- Token registered on app launch after authentication
|
|
119
|
+
- APNs (iOS) and FCM (Android) handled server-side via worker jobs
|
|
120
|
+
- No Firebase SDK on client or server
|
|
121
|
+
|
|
122
|
+
## Deep Linking
|
|
123
|
+
|
|
124
|
+
Configure your URL scheme in `app.json` under `expo.scheme`. Expo Router handles file-based deep linking automatically.
|
|
125
|
+
|
|
126
|
+
## OTA Updates
|
|
127
|
+
|
|
128
|
+
Configured via `expo-updates`. Channels: `development` → `staging` → `production`.
|
|
129
|
+
|
|
130
|
+
See `CHECKLIST.md` for OTA vs store build decisions.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
2
|
+
import { Redirect, Tabs } from "expo-router";
|
|
3
|
+
import { Platform } from "react-native";
|
|
4
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
5
|
+
import { AuthGate } from "../../components/AuthGate";
|
|
6
|
+
import { useTheme } from "../../components/ThemeProvider";
|
|
7
|
+
import { useAuth } from "../../hooks/use-auth";
|
|
8
|
+
|
|
9
|
+
export default function AppLayout() {
|
|
10
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
11
|
+
const insets = useSafeAreaInsets();
|
|
12
|
+
const theme = useTheme();
|
|
13
|
+
const isDark = theme === "dark";
|
|
14
|
+
|
|
15
|
+
if (isLoading) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!isAuthenticated) {
|
|
20
|
+
return <Redirect href="/(auth)/sign-in" />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<AuthGate>
|
|
25
|
+
<Tabs
|
|
26
|
+
screenOptions={{
|
|
27
|
+
headerShown: false,
|
|
28
|
+
tabBarStyle: {
|
|
29
|
+
backgroundColor: isDark ? "#1c1c1e" : "#ffffff",
|
|
30
|
+
borderTopColor: isDark ? "#38383a" : "#e5e5ea",
|
|
31
|
+
paddingBottom: Platform.OS === "ios" ? insets.bottom : 8,
|
|
32
|
+
height: Platform.OS === "ios" ? 88 : 64,
|
|
33
|
+
},
|
|
34
|
+
tabBarActiveTintColor: isDark ? "#fff" : "#000",
|
|
35
|
+
tabBarInactiveTintColor: isDark ? "#888" : "#666",
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
<Tabs.Screen
|
|
39
|
+
name="index"
|
|
40
|
+
options={{
|
|
41
|
+
title: "Home",
|
|
42
|
+
tabBarLabel: "Home",
|
|
43
|
+
tabBarIcon: ({ color, size }) => (
|
|
44
|
+
<Ionicons
|
|
45
|
+
name="home-outline"
|
|
46
|
+
size={size}
|
|
47
|
+
color={color as string}
|
|
48
|
+
/>
|
|
49
|
+
),
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
<Tabs.Screen
|
|
53
|
+
name="profile"
|
|
54
|
+
options={{
|
|
55
|
+
title: "Profile",
|
|
56
|
+
tabBarLabel: "Profile",
|
|
57
|
+
tabBarIcon: ({ color, size }) => (
|
|
58
|
+
<Ionicons
|
|
59
|
+
name="person-outline"
|
|
60
|
+
size={size}
|
|
61
|
+
color={color as string}
|
|
62
|
+
/>
|
|
63
|
+
),
|
|
64
|
+
}}
|
|
65
|
+
/>
|
|
66
|
+
<Tabs.Screen
|
|
67
|
+
name="settings"
|
|
68
|
+
options={{
|
|
69
|
+
title: "Settings",
|
|
70
|
+
tabBarLabel: "Settings",
|
|
71
|
+
tabBarIcon: ({ color, size }) => (
|
|
72
|
+
<Ionicons
|
|
73
|
+
name="settings-outline"
|
|
74
|
+
size={size}
|
|
75
|
+
color={color as string}
|
|
76
|
+
/>
|
|
77
|
+
),
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
|
+
</Tabs>
|
|
81
|
+
</AuthGate>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
ActivityIndicator,
|
|
4
|
+
RefreshControl,
|
|
5
|
+
ScrollView,
|
|
6
|
+
Text,
|
|
7
|
+
View,
|
|
8
|
+
} from "react-native";
|
|
9
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
10
|
+
import { useAuth } from "../../hooks/use-auth";
|
|
11
|
+
import { getProfile } from "../../lib/auth";
|
|
12
|
+
import { useThemeTokens } from "../../lib/theme-tokens";
|
|
13
|
+
|
|
14
|
+
interface UserProfile {
|
|
15
|
+
id: string;
|
|
16
|
+
email: string;
|
|
17
|
+
name: string | null;
|
|
18
|
+
role: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function HomeScreen() {
|
|
22
|
+
const { isAuthenticated } = useAuth();
|
|
23
|
+
const { bgColor, textColor, mutedColor, cardBg, borderColor, isDark } =
|
|
24
|
+
useThemeTokens();
|
|
25
|
+
const insets = useSafeAreaInsets();
|
|
26
|
+
const [profile, setProfile] = useState<UserProfile | null>(null);
|
|
27
|
+
const [loading, setLoading] = useState(true);
|
|
28
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
29
|
+
|
|
30
|
+
const fetchProfile = useCallback(async () => {
|
|
31
|
+
try {
|
|
32
|
+
const data = await getProfile();
|
|
33
|
+
setProfile(data);
|
|
34
|
+
} catch {
|
|
35
|
+
// Profile fetch failed - non-critical
|
|
36
|
+
} finally {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
setRefreshing(false);
|
|
39
|
+
}
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (isAuthenticated) {
|
|
44
|
+
fetchProfile();
|
|
45
|
+
}
|
|
46
|
+
}, [isAuthenticated, fetchProfile]);
|
|
47
|
+
|
|
48
|
+
async function onRefresh() {
|
|
49
|
+
setRefreshing(true);
|
|
50
|
+
await fetchProfile();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (loading) {
|
|
54
|
+
return (
|
|
55
|
+
<View
|
|
56
|
+
style={{
|
|
57
|
+
flex: 1,
|
|
58
|
+
justifyContent: "center",
|
|
59
|
+
alignItems: "center",
|
|
60
|
+
backgroundColor: bgColor,
|
|
61
|
+
}}
|
|
62
|
+
accessibilityLabel="Loading home screen"
|
|
63
|
+
accessibilityRole="progressbar"
|
|
64
|
+
>
|
|
65
|
+
<ActivityIndicator size="large" color={isDark ? "#fff" : "#000"} />
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<ScrollView
|
|
72
|
+
style={{ flex: 1, backgroundColor: bgColor }}
|
|
73
|
+
contentContainerStyle={{
|
|
74
|
+
padding: 20,
|
|
75
|
+
paddingTop: insets.top + 20,
|
|
76
|
+
}}
|
|
77
|
+
refreshControl={
|
|
78
|
+
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
|
79
|
+
}
|
|
80
|
+
accessibilityLabel="Home screen content"
|
|
81
|
+
>
|
|
82
|
+
<Text
|
|
83
|
+
style={{
|
|
84
|
+
fontSize: 28,
|
|
85
|
+
fontWeight: "bold",
|
|
86
|
+
color: textColor,
|
|
87
|
+
marginBottom: 8,
|
|
88
|
+
}}
|
|
89
|
+
accessibilityRole="header"
|
|
90
|
+
>
|
|
91
|
+
Welcome{profile?.name ? `, ${profile.name}` : ""}
|
|
92
|
+
</Text>
|
|
93
|
+
<Text style={{ fontSize: 16, color: mutedColor, marginBottom: 24 }}>
|
|
94
|
+
{profile?.email || "Signed in to your account"}
|
|
95
|
+
</Text>
|
|
96
|
+
|
|
97
|
+
<View
|
|
98
|
+
style={{
|
|
99
|
+
backgroundColor: cardBg,
|
|
100
|
+
borderRadius: 12,
|
|
101
|
+
padding: 16,
|
|
102
|
+
marginBottom: 16,
|
|
103
|
+
borderWidth: 1,
|
|
104
|
+
borderColor,
|
|
105
|
+
}}
|
|
106
|
+
accessibilityLabel="Account info card"
|
|
107
|
+
accessibilityRole="summary"
|
|
108
|
+
>
|
|
109
|
+
<Text style={{ fontSize: 14, color: mutedColor, marginBottom: 4 }}>
|
|
110
|
+
Account
|
|
111
|
+
</Text>
|
|
112
|
+
<Text style={{ fontSize: 16, color: textColor, fontWeight: "500" }}>
|
|
113
|
+
{profile?.name || "No name set"}
|
|
114
|
+
</Text>
|
|
115
|
+
<Text style={{ fontSize: 14, color: mutedColor, marginTop: 4 }}>
|
|
116
|
+
{profile?.email}
|
|
117
|
+
</Text>
|
|
118
|
+
</View>
|
|
119
|
+
|
|
120
|
+
<View
|
|
121
|
+
style={{
|
|
122
|
+
backgroundColor: cardBg,
|
|
123
|
+
borderRadius: 12,
|
|
124
|
+
padding: 16,
|
|
125
|
+
borderWidth: 1,
|
|
126
|
+
borderColor,
|
|
127
|
+
}}
|
|
128
|
+
accessibilityLabel="Quick actions card"
|
|
129
|
+
accessibilityRole="summary"
|
|
130
|
+
>
|
|
131
|
+
<Text style={{ fontSize: 14, color: mutedColor, marginBottom: 4 }}>
|
|
132
|
+
Role
|
|
133
|
+
</Text>
|
|
134
|
+
<Text
|
|
135
|
+
style={{
|
|
136
|
+
fontSize: 16,
|
|
137
|
+
color: textColor,
|
|
138
|
+
fontWeight: "500",
|
|
139
|
+
textTransform: "capitalize",
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
{profile?.role || "viewer"}
|
|
143
|
+
</Text>
|
|
144
|
+
</View>
|
|
145
|
+
</ScrollView>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
ActivityIndicator,
|
|
4
|
+
Alert,
|
|
5
|
+
Pressable,
|
|
6
|
+
ScrollView,
|
|
7
|
+
Text,
|
|
8
|
+
TextInput,
|
|
9
|
+
View,
|
|
10
|
+
} from "react-native";
|
|
11
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
12
|
+
import { useAuth } from "../../hooks/use-auth";
|
|
13
|
+
import { getProfile, updateProfile } from "../../lib/auth";
|
|
14
|
+
import { useThemeTokens } from "../../lib/theme-tokens";
|
|
15
|
+
|
|
16
|
+
interface UserProfile {
|
|
17
|
+
id: string;
|
|
18
|
+
email: string;
|
|
19
|
+
name: string | null;
|
|
20
|
+
role: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function ProfileScreen() {
|
|
24
|
+
const { isAuthenticated } = useAuth();
|
|
25
|
+
const {
|
|
26
|
+
bgColor,
|
|
27
|
+
textColor,
|
|
28
|
+
mutedColor,
|
|
29
|
+
cardBg,
|
|
30
|
+
borderColor,
|
|
31
|
+
inputBg,
|
|
32
|
+
isDark,
|
|
33
|
+
} = useThemeTokens();
|
|
34
|
+
const insets = useSafeAreaInsets();
|
|
35
|
+
const [profile, setProfile] = useState<UserProfile | null>(null);
|
|
36
|
+
const [name, setName] = useState("");
|
|
37
|
+
const [loading, setLoading] = useState(true);
|
|
38
|
+
const [saving, setSaving] = useState(false);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (isAuthenticated) {
|
|
42
|
+
(async () => {
|
|
43
|
+
try {
|
|
44
|
+
const data = await getProfile();
|
|
45
|
+
setProfile(data);
|
|
46
|
+
setName(data.name || "");
|
|
47
|
+
} catch {
|
|
48
|
+
// Non-critical
|
|
49
|
+
} finally {
|
|
50
|
+
setLoading(false);
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
53
|
+
}
|
|
54
|
+
}, [isAuthenticated]);
|
|
55
|
+
|
|
56
|
+
async function handleSave() {
|
|
57
|
+
if (!name.trim()) {
|
|
58
|
+
Alert.alert("Error", "Name cannot be empty");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
setSaving(true);
|
|
62
|
+
try {
|
|
63
|
+
const updated = await updateProfile({ name: name.trim() });
|
|
64
|
+
setProfile(updated);
|
|
65
|
+
Alert.alert("Saved", "Profile updated successfully");
|
|
66
|
+
} catch {
|
|
67
|
+
Alert.alert("Error", "Failed to update profile. Please try again.");
|
|
68
|
+
} finally {
|
|
69
|
+
setSaving(false);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (loading) {
|
|
74
|
+
return (
|
|
75
|
+
<View
|
|
76
|
+
style={{
|
|
77
|
+
flex: 1,
|
|
78
|
+
justifyContent: "center",
|
|
79
|
+
alignItems: "center",
|
|
80
|
+
backgroundColor: bgColor,
|
|
81
|
+
}}
|
|
82
|
+
accessibilityLabel="Loading profile"
|
|
83
|
+
accessibilityRole="progressbar"
|
|
84
|
+
>
|
|
85
|
+
<ActivityIndicator size="large" color={isDark ? "#fff" : "#000"} />
|
|
86
|
+
</View>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<ScrollView
|
|
92
|
+
style={{ flex: 1, backgroundColor: bgColor }}
|
|
93
|
+
contentContainerStyle={{
|
|
94
|
+
padding: 20,
|
|
95
|
+
paddingTop: insets.top + 20,
|
|
96
|
+
}}
|
|
97
|
+
accessibilityLabel="Profile screen"
|
|
98
|
+
>
|
|
99
|
+
<Text
|
|
100
|
+
style={{
|
|
101
|
+
fontSize: 28,
|
|
102
|
+
fontWeight: "bold",
|
|
103
|
+
color: textColor,
|
|
104
|
+
marginBottom: 24,
|
|
105
|
+
}}
|
|
106
|
+
accessibilityRole="header"
|
|
107
|
+
>
|
|
108
|
+
Profile
|
|
109
|
+
</Text>
|
|
110
|
+
|
|
111
|
+
<Text style={{ fontSize: 14, color: mutedColor, marginBottom: 8 }}>
|
|
112
|
+
Name
|
|
113
|
+
</Text>
|
|
114
|
+
<TextInput
|
|
115
|
+
value={name}
|
|
116
|
+
onChangeText={setName}
|
|
117
|
+
placeholder="Your name"
|
|
118
|
+
placeholderTextColor={mutedColor}
|
|
119
|
+
autoComplete="name"
|
|
120
|
+
style={{
|
|
121
|
+
backgroundColor: inputBg,
|
|
122
|
+
borderWidth: 1,
|
|
123
|
+
borderColor,
|
|
124
|
+
borderRadius: 8,
|
|
125
|
+
padding: 12,
|
|
126
|
+
marginBottom: 16,
|
|
127
|
+
fontSize: 16,
|
|
128
|
+
color: textColor,
|
|
129
|
+
}}
|
|
130
|
+
accessibilityLabel="Name input"
|
|
131
|
+
accessibilityHint="Enter your display name"
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
<Text style={{ fontSize: 14, color: mutedColor, marginBottom: 8 }}>
|
|
135
|
+
Email
|
|
136
|
+
</Text>
|
|
137
|
+
<View
|
|
138
|
+
style={{
|
|
139
|
+
backgroundColor: cardBg,
|
|
140
|
+
borderWidth: 1,
|
|
141
|
+
borderColor,
|
|
142
|
+
borderRadius: 8,
|
|
143
|
+
padding: 12,
|
|
144
|
+
marginBottom: 16,
|
|
145
|
+
}}
|
|
146
|
+
accessibilityLabel="Email display"
|
|
147
|
+
>
|
|
148
|
+
<Text style={{ fontSize: 16, color: mutedColor }}>
|
|
149
|
+
{profile?.email}
|
|
150
|
+
</Text>
|
|
151
|
+
</View>
|
|
152
|
+
|
|
153
|
+
<Text style={{ fontSize: 14, color: mutedColor, marginBottom: 8 }}>
|
|
154
|
+
Role
|
|
155
|
+
</Text>
|
|
156
|
+
<View
|
|
157
|
+
style={{
|
|
158
|
+
backgroundColor: cardBg,
|
|
159
|
+
borderWidth: 1,
|
|
160
|
+
borderColor,
|
|
161
|
+
borderRadius: 8,
|
|
162
|
+
padding: 12,
|
|
163
|
+
marginBottom: 24,
|
|
164
|
+
}}
|
|
165
|
+
accessibilityLabel="Role display"
|
|
166
|
+
>
|
|
167
|
+
<Text
|
|
168
|
+
style={{
|
|
169
|
+
fontSize: 16,
|
|
170
|
+
color: textColor,
|
|
171
|
+
textTransform: "capitalize",
|
|
172
|
+
}}
|
|
173
|
+
>
|
|
174
|
+
{profile?.role || "viewer"}
|
|
175
|
+
</Text>
|
|
176
|
+
</View>
|
|
177
|
+
|
|
178
|
+
<Pressable
|
|
179
|
+
onPress={handleSave}
|
|
180
|
+
disabled={saving}
|
|
181
|
+
style={{
|
|
182
|
+
backgroundColor: isDark ? "#fff" : "#000",
|
|
183
|
+
padding: 14,
|
|
184
|
+
borderRadius: 8,
|
|
185
|
+
alignItems: "center",
|
|
186
|
+
opacity: saving ? 0.6 : 1,
|
|
187
|
+
}}
|
|
188
|
+
accessibilityLabel="Save profile changes"
|
|
189
|
+
accessibilityRole="button"
|
|
190
|
+
accessibilityState={{ disabled: saving }}
|
|
191
|
+
>
|
|
192
|
+
<Text
|
|
193
|
+
style={{
|
|
194
|
+
color: isDark ? "#000" : "#fff",
|
|
195
|
+
fontSize: 16,
|
|
196
|
+
fontWeight: "600",
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
{saving ? "Saving..." : "Save Changes"}
|
|
200
|
+
</Text>
|
|
201
|
+
</Pressable>
|
|
202
|
+
</ScrollView>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useRouter } from "expo-router";
|
|
2
|
+
import { Pressable, Text, View } from "react-native";
|
|
3
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
|
+
import { useAuth } from "../../hooks/use-auth";
|
|
5
|
+
import { useThemeTokens } from "../../lib/theme-tokens";
|
|
6
|
+
|
|
7
|
+
export default function SettingsScreen() {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const { signOut } = useAuth();
|
|
10
|
+
const { bgColor, textColor, mutedColor, cardBg, borderColor } =
|
|
11
|
+
useThemeTokens();
|
|
12
|
+
const insets = useSafeAreaInsets();
|
|
13
|
+
|
|
14
|
+
async function handleSignOut() {
|
|
15
|
+
await signOut();
|
|
16
|
+
router.replace("/(auth)/sign-in");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<View
|
|
21
|
+
style={{
|
|
22
|
+
flex: 1,
|
|
23
|
+
backgroundColor: bgColor,
|
|
24
|
+
padding: 20,
|
|
25
|
+
paddingTop: insets.top + 20,
|
|
26
|
+
}}
|
|
27
|
+
accessibilityLabel="Settings screen"
|
|
28
|
+
>
|
|
29
|
+
<Text
|
|
30
|
+
style={{
|
|
31
|
+
fontSize: 28,
|
|
32
|
+
fontWeight: "bold",
|
|
33
|
+
color: textColor,
|
|
34
|
+
marginBottom: 24,
|
|
35
|
+
}}
|
|
36
|
+
accessibilityRole="header"
|
|
37
|
+
>
|
|
38
|
+
Settings
|
|
39
|
+
</Text>
|
|
40
|
+
|
|
41
|
+
<View
|
|
42
|
+
style={{
|
|
43
|
+
backgroundColor: cardBg,
|
|
44
|
+
borderRadius: 12,
|
|
45
|
+
padding: 16,
|
|
46
|
+
marginBottom: 16,
|
|
47
|
+
borderWidth: 1,
|
|
48
|
+
borderColor,
|
|
49
|
+
}}
|
|
50
|
+
accessibilityLabel="App info section"
|
|
51
|
+
>
|
|
52
|
+
<Text style={{ fontSize: 14, color: mutedColor, marginBottom: 4 }}>
|
|
53
|
+
App
|
|
54
|
+
</Text>
|
|
55
|
+
<Text style={{ fontSize: 16, color: textColor }}>Quark Mobile</Text>
|
|
56
|
+
<Text style={{ fontSize: 14, color: mutedColor, marginTop: 4 }}>
|
|
57
|
+
Version 0.1.0
|
|
58
|
+
</Text>
|
|
59
|
+
</View>
|
|
60
|
+
|
|
61
|
+
<Pressable
|
|
62
|
+
onPress={handleSignOut}
|
|
63
|
+
style={{
|
|
64
|
+
backgroundColor: "#ff3b30",
|
|
65
|
+
paddingHorizontal: 24,
|
|
66
|
+
paddingVertical: 14,
|
|
67
|
+
borderRadius: 8,
|
|
68
|
+
alignItems: "center",
|
|
69
|
+
}}
|
|
70
|
+
accessibilityLabel="Sign out of your account"
|
|
71
|
+
accessibilityRole="button"
|
|
72
|
+
accessibilityHint="Signs you out and returns to the sign in screen"
|
|
73
|
+
>
|
|
74
|
+
<Text style={{ color: "#fff", fontSize: 16, fontWeight: "600" }}>
|
|
75
|
+
Sign Out
|
|
76
|
+
</Text>
|
|
77
|
+
</Pressable>
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
}
|