diff --git a/.env.development b/.env.development index d3a564534..6467ab5f9 100644 --- a/.env.development +++ b/.env.development @@ -25,6 +25,8 @@ PUBLIC_GLITCHTIP_DSN= MANA_CORE_AUTH_URL=http://localhost:3001 # Mana Credits Service MANA_CREDITS_URL=http://localhost:3061 +# Mana Media Service (CAS, thumbnails, Photos gallery) +MANA_MEDIA_URL=http://localhost:3015 # Service key for service-to-service communication MANA_CORE_SERVICE_KEY=dev-service-key-for-bot-sso-2024 diff --git a/apps/api/package.json b/apps/api/package.json index 4aef6cfe5..3580ee624 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -10,6 +10,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { + "@manacore/media-client": "workspace:*", "@manacore/shared-hono": "workspace:*", "@manacore/shared-storage": "workspace:*", "@mozilla/readability": "^0.5.0", diff --git a/apps/api/src/lib/media.ts b/apps/api/src/lib/media.ts new file mode 100644 index 000000000..e9d1ed969 --- /dev/null +++ b/apps/api/src/lib/media.ts @@ -0,0 +1,40 @@ +/** + * Shared media helper — routes image uploads through mana-media + * for CAS deduplication, thumbnail generation, and Photos gallery visibility. + */ + +import { MediaClient, type MediaResult } from '@manacore/media-client'; + +const MEDIA_URL = process.env.MANA_MEDIA_URL || 'http://localhost:3015'; +let client: MediaClient | null = null; + +function getMediaClient(): MediaClient { + if (!client) client = new MediaClient(MEDIA_URL); + return client; +} + +export async function uploadImageToMedia( + buffer: ArrayBuffer, + filename: string, + options: { app: string; userId: string } +): Promise { + return getMediaClient().upload(buffer, { + app: options.app, + userId: options.userId, + filename, + }); +} + +export function getMediaUrls(mediaId: string) { + const c = getMediaClient(); + return { + original: c.getOriginalUrl(mediaId), + thumbnail: c.getThumbnailUrl(mediaId), + medium: c.getMediumUrl(mediaId), + large: c.getLargeUrl(mediaId), + }; +} + +export function isImageMimeType(mimeType: string): boolean { + return mimeType.startsWith('image/') && mimeType !== 'image/svg+xml'; +} diff --git a/apps/api/src/modules/contacts/routes.ts b/apps/api/src/modules/contacts/routes.ts index 41733cf1d..1ea988d8d 100644 --- a/apps/api/src/modules/contacts/routes.ts +++ b/apps/api/src/modules/contacts/routes.ts @@ -29,22 +29,37 @@ routes.post('/:id/avatar', async (c) => { } try { - const { createContactsStorage, generateUserFileKey, getContentType } = await import( - '@manacore/shared-storage' - ); - const storage = createContactsStorage(); - const key = generateUserFileKey( - userId, - `avatar-${c.req.param('id')}.${file.name.split('.').pop()}` - ); - const buffer = Buffer.from(await file.arrayBuffer()); + const buffer = await file.arrayBuffer(); - const result = await storage.upload(key, buffer, { - contentType: getContentType(file.name), - public: true, - }); + if (file.type === 'image/svg+xml') { + // SVGs stay on shared-storage (Sharp can't process SVG) + const { createContactsStorage, generateUserFileKey } = await import( + '@manacore/shared-storage' + ); + const storage = createContactsStorage(); + const key = generateUserFileKey(userId, `avatar-${c.req.param('id')}.svg`); + const result = await storage.upload(key, Buffer.from(buffer), { + contentType: 'image/svg+xml', + public: true, + }); + return c.json({ avatarUrl: result.url }, 201); + } - return c.json({ avatarUrl: result.url }, 201); + // Raster images -> mana-media for dedup, thumbnails & Photos gallery + const { uploadImageToMedia } = await import('../../lib/media'); + const result = await uploadImageToMedia( + buffer, + `avatar-${c.req.param('id')}.${file.name.split('.').pop()}`, + { app: 'contacts', userId } + ); + + return c.json( + { + avatarUrl: result.urls.thumbnail || result.urls.original, + mediaId: result.id, + }, + 201 + ); } catch { return c.json({ error: 'Upload failed' }, 500); } diff --git a/apps/api/src/modules/nutriphi/routes.ts b/apps/api/src/modules/nutriphi/routes.ts index a52fccb40..2ae67b675 100644 --- a/apps/api/src/modules/nutriphi/routes.ts +++ b/apps/api/src/modules/nutriphi/routes.ts @@ -25,11 +25,15 @@ const routes = new Hono(); // ─── Photo Analysis (server-only: Gemini Vision) ──────────── routes.post('/analysis/photo', async (c) => { + const userId = c.get('userId'); const { imageBase64, mimeType } = await c.req.json(); if (!imageBase64) return c.json({ error: 'imageBase64 required' }, 400); + const mime = mimeType || 'image/jpeg'; + try { - const res = await fetch(`${LLM_URL}/api/v1/chat/completions`, { + // Run AI analysis and mana-media upload in parallel + const analysisPromise = fetch(`${LLM_URL}/api/v1/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -41,7 +45,7 @@ routes.post('/analysis/photo', async (c) => { { type: 'text', text: 'Analysiere diese Mahlzeit.' }, { type: 'image_url', - image_url: { url: `data:${mimeType || 'image/jpeg'};base64,${imageBase64}` }, + image_url: { url: `data:${mime};base64,${imageBase64}` }, }, ], }, @@ -52,12 +56,29 @@ routes.post('/analysis/photo', async (c) => { }), }); + // Store meal photo in mana-media for Photos gallery & persistence + const ext = mime.split('/')[1] || 'jpg'; + const { uploadImageToMedia } = await import('../../lib/media'); + const buffer = Uint8Array.from(atob(imageBase64), (ch) => ch.charCodeAt(0)); + const mediaPromise = uploadImageToMedia(buffer.buffer, `meal-${Date.now()}.${ext}`, { + app: 'nutriphi', + userId, + }).catch(() => null); // Don't fail analysis if media upload fails + + const [res, mediaResult] = await Promise.all([analysisPromise, mediaPromise]); + if (!res.ok) return c.json({ error: 'AI analysis failed' }, 502); const data = await res.json(); const content = data.choices?.[0]?.message?.content; const analysis = typeof content === 'string' ? JSON.parse(content) : content; + // Attach media info so the frontend can store photoMediaId on the meal + if (mediaResult) { + analysis.mediaId = mediaResult.id; + analysis.photoUrl = mediaResult.urls.thumbnail || mediaResult.urls.original; + } + return c.json(analysis); } catch (err) { console.error('Photo analysis failed:', err); diff --git a/apps/api/src/modules/picture/routes.ts b/apps/api/src/modules/picture/routes.ts index 7fe515988..c43c8d692 100644 --- a/apps/api/src/modules/picture/routes.ts +++ b/apps/api/src/modules/picture/routes.ts @@ -98,7 +98,27 @@ routes.post('/generate', async (c) => { await consumeCredits(userId, 'AI_IMAGE_GENERATION', cost, `Image: ${prompt.slice(0, 50)}`); - return c.json({ imageUrl, prompt, model: model || 'flux-schnell' }); + // Store generated image in mana-media for dedup, thumbnails & Photos gallery + try { + const { uploadImageToMedia } = await import('../../lib/media'); + const imgRes = await fetch(imageUrl); + const imgBuffer = await imgRes.arrayBuffer(); + const media = await uploadImageToMedia(imgBuffer, `generated-${Date.now()}.png`, { + app: 'picture', + userId, + }); + + return c.json({ + imageUrl: media.urls.original, + mediaId: media.id, + thumbnailUrl: media.urls.thumbnail, + prompt, + model: model || 'flux-schnell', + }); + } catch { + // Fallback: return raw imageUrl if mana-media is unavailable + return c.json({ imageUrl, prompt, model: model || 'flux-schnell' }); + } } catch (_err) { return c.json({ error: 'Generation failed' }, 500); } @@ -115,19 +135,19 @@ routes.post('/upload', async (c) => { if (file.size > 10 * 1024 * 1024) return c.json({ error: 'Max 10MB' }, 400); try { - const { createPictureStorage, generateUserFileKey, getContentType } = await import( - '@manacore/shared-storage' + const { uploadImageToMedia } = await import('../../lib/media'); + const buffer = await file.arrayBuffer(); + const result = await uploadImageToMedia(buffer, file.name, { app: 'picture', userId }); + + return c.json( + { + storagePath: result.id, + publicUrl: result.urls.original, + mediaId: result.id, + thumbnailUrl: result.urls.thumbnail, + }, + 201 ); - const storage = createPictureStorage(); - const key = generateUserFileKey(userId, file.name); - const buffer = Buffer.from(await file.arrayBuffer()); - - const result = await storage.upload(key, buffer, { - contentType: getContentType(file.name), - public: true, - }); - - return c.json({ storagePath: key, publicUrl: result.url }, 201); } catch (_err) { return c.json({ error: 'Upload failed' }, 500); } diff --git a/apps/api/src/modules/planta/routes.ts b/apps/api/src/modules/planta/routes.ts index 14852dea8..5ce574403 100644 --- a/apps/api/src/modules/planta/routes.ts +++ b/apps/api/src/modules/planta/routes.ts @@ -24,19 +24,19 @@ routes.post('/photos/upload', async (c) => { if (file.size > 10 * 1024 * 1024) return c.json({ error: 'File too large (max 10MB)' }, 400); try { - const { createPlantaStorage, generateUserFileKey, getContentType } = await import( - '@manacore/shared-storage' + const { uploadImageToMedia } = await import('../../lib/media'); + const buffer = await file.arrayBuffer(); + const result = await uploadImageToMedia(buffer, file.name, { app: 'planta', userId }); + + return c.json( + { + storagePath: result.id, + publicUrl: result.urls.original, + mediaId: result.id, + plantId, + }, + 201 ); - const storage = createPlantaStorage(); - const key = generateUserFileKey(userId, file.name); - const buffer = Buffer.from(await file.arrayBuffer()); - - const result = await storage.upload(key, buffer, { - contentType: getContentType(file.name), - public: true, - }); - - return c.json({ storagePath: key, publicUrl: result.url, plantId }, 201); } catch (err) { console.error('Upload failed:', err); return c.json({ error: 'Upload failed' }, 500); diff --git a/apps/api/src/modules/storage/routes.ts b/apps/api/src/modules/storage/routes.ts index 0b6969df9..20750f2ff 100644 --- a/apps/api/src/modules/storage/routes.ts +++ b/apps/api/src/modules/storage/routes.ts @@ -22,14 +22,37 @@ routes.post('/files/upload', async (c) => { if (file.size > 100 * 1024 * 1024) return c.json({ error: 'Max 100MB' }, 400); try { + const buffer = await file.arrayBuffer(); + const { isImageMimeType } = await import('../../lib/media'); + + // Images -> mana-media for dedup, thumbnails & Photos gallery + if (isImageMimeType(file.type)) { + const { uploadImageToMedia } = await import('../../lib/media'); + const result = await uploadImageToMedia(buffer, file.name, { app: 'storage', userId }); + + return c.json( + { + id: crypto.randomUUID(), + name: file.name, + storagePath: result.id, + storageKey: result.id, + mimeType: file.type, + size: file.size, + parentFolderId: folderId, + mediaId: result.id, + }, + 201 + ); + } + + // Non-images -> shared-storage as before const { createStorageStorage, generateUserFileKey, getContentType } = await import( '@manacore/shared-storage' ); const storage = createStorageStorage(); const key = generateUserFileKey(userId, file.name); - const buffer = Buffer.from(await file.arrayBuffer()); - await storage.upload(key, buffer, { + await storage.upload(key, Buffer.from(buffer), { contentType: getContentType(file.name), public: false, }); diff --git a/apps/manacore/apps/web/src/lib/modules/nutriphi/types.ts b/apps/manacore/apps/web/src/lib/modules/nutriphi/types.ts index 9f6aa4032..b192e56bd 100644 --- a/apps/manacore/apps/web/src/lib/modules/nutriphi/types.ts +++ b/apps/manacore/apps/web/src/lib/modules/nutriphi/types.ts @@ -24,6 +24,8 @@ export interface LocalMeal extends BaseRecord { portionSize?: string | null; confidence: number; nutrition?: NutritionData | null; + photoMediaId?: string | null; + photoUrl?: string | null; } export interface LocalGoal extends BaseRecord { @@ -65,5 +67,7 @@ export interface MealWithNutrition { portionSize?: string | null; confidence: number; nutrition: NutritionData | null; + photoMediaId?: string | null; + photoUrl?: string | null; createdAt: string; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2fdb1163..30ad83d9f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,6 +64,9 @@ importers: apps/api: dependencies: + '@manacore/media-client': + specifier: workspace:* + version: link:../../services/mana-media/packages/client '@manacore/shared-hono': specifier: workspace:* version: link:../../packages/shared-hono @@ -26455,7 +26458,7 @@ snapshots: ws: 8.18.3 zod: 3.25.76 optionalDependencies: - expo-router: 55.0.5(7734cwvz7fkv6rui37yzuzjxt4) + expo-router: 55.0.5(qfbmyq6c2bmncbs6qduwm3ekeu) react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4) transitivePeerDependencies: - '@expo/dom-webview' @@ -27204,7 +27207,7 @@ snapshots: react: 19.2.4 optionalDependencies: '@expo/metro-runtime': 55.0.7(@expo/dom-webview@55.0.3)(expo@55.0.5)(react-dom@19.2.4(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) - expo-router: 55.0.5(7734cwvz7fkv6rui37yzuzjxt4) + expo-router: 55.0.5(qfbmyq6c2bmncbs6qduwm3ekeu) react-dom: 19.2.4(react@19.2.4) transitivePeerDependencies: - supports-color @@ -27661,42 +27664,6 @@ snapshots: - supports-color - ts-node - '@jest/core@30.3.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': - dependencies: - '@jest/console': 30.3.0 - '@jest/pattern': 30.0.1 - '@jest/reporters': 30.3.0 - '@jest/test-result': 30.3.0 - '@jest/transform': 30.3.0 - '@jest/types': 30.3.0 - '@types/node': 22.19.1 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 4.3.1 - exit-x: 0.2.2 - graceful-fs: 4.2.11 - jest-changed-files: 30.3.0 - jest-config: 30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - jest-haste-map: 30.3.0 - jest-message-util: 30.3.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.3.0 - jest-resolve-dependencies: 30.3.0 - jest-runner: 30.3.0 - jest-runtime: 30.3.0 - jest-snapshot: 30.3.0 - jest-util: 30.3.0 - jest-validate: 30.3.0 - jest-watcher: 30.3.0 - pretty-format: 30.3.0 - slash: 3.0.0 - transitivePeerDependencies: - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - optional: true - '@jest/core@30.3.0(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': dependencies: '@jest/console': 30.3.0 @@ -32225,19 +32192,6 @@ snapshots: jest: 30.3.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) optional: true - '@testing-library/react-native@13.3.3(jest@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react-test-renderer@19.1.0(react@19.2.4))(react@19.2.4)': - dependencies: - jest-matcher-utils: 30.3.0 - picocolors: 1.1.1 - pretty-format: 30.3.0 - react: 19.2.4 - react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4) - react-test-renderer: 19.1.0(react@19.2.4) - redent: 3.0.0 - optionalDependencies: - jest: 30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - optional: true - '@testing-library/react-native@13.3.3(jest@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.3.3)))(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react-test-renderer@19.1.0(react@19.2.0))(react@19.2.0)': dependencies: jest-matcher-utils: 30.3.0 @@ -32303,6 +32257,19 @@ snapshots: jest: 30.3.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) optional: true + '@testing-library/react-native@13.3.3(jest@30.3.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react-test-renderer@19.1.0(react@19.2.4))(react@19.2.4)': + dependencies: + jest-matcher-utils: 30.3.0 + picocolors: 1.1.1 + pretty-format: 30.3.0 + react: 19.2.4 + react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4) + react-test-renderer: 19.1.0(react@19.2.4) + redent: 3.0.0 + optionalDependencies: + jest: 30.3.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + optional: true + '@testing-library/svelte-core@1.0.0(svelte@5.44.0)': dependencies: svelte: 5.44.0 @@ -38632,57 +38599,6 @@ snapshots: - expo-font - supports-color - expo-router@55.0.5(7734cwvz7fkv6rui37yzuzjxt4): - dependencies: - '@expo/log-box': 55.0.8(@expo/dom-webview@55.0.3)(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) - '@expo/metro-runtime': 55.0.7(@expo/dom-webview@55.0.3)(expo@55.0.5)(react-dom@19.2.4(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) - '@expo/schema-utils': 55.0.2 - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-navigation/bottom-tabs': 7.15.5(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) - '@react-navigation/native': 7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) - '@react-navigation/native-stack': 7.14.4(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) - client-only: 0.0.1 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.7)(expo-router@55.0.5)(react-dom@19.2.4(react@19.2.4))(react-native-webview@13.12.2(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(typescript@5.9.3) - expo-constants: 55.0.7(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(typescript@5.9.3) - expo-glass-effect: 55.0.8(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) - expo-image: 55.0.6(expo@55.0.5)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) - expo-linking: 55.0.7(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(typescript@5.9.3) - expo-server: 55.0.6 - expo-symbols: 55.0.5(expo-font@55.0.4)(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) - fast-deep-equal: 3.1.3 - invariant: 2.2.4 - nanoid: 3.3.11 - query-string: 7.1.3 - react: 19.2.4 - react-fast-compare: 3.2.2 - react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4) - react-native-is-edge-to-edge: 1.2.1(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) - react-native-safe-area-context: 5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) - react-native-screens: 4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) - semver: 7.6.3 - server-only: 0.0.1 - sf-symbols-typescript: 2.2.0 - shallowequal: 1.1.0 - use-latest-callback: 0.2.6(react@19.2.4) - vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - optionalDependencies: - '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-gesture-handler@2.30.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-reanimated@4.2.2(react-native-worklets@0.7.4(@babel/core@7.28.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) - '@testing-library/react-native': 13.3.3(jest@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react-test-renderer@19.1.0(react@19.2.4))(react@19.2.4) - react-dom: 19.2.4(react@19.2.4) - react-native-gesture-handler: 2.30.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) - react-native-reanimated: 4.2.2(react-native-worklets@0.7.4(@babel/core@7.28.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) - react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - transitivePeerDependencies: - - '@react-native-masked-view/masked-view' - - '@types/react' - - '@types/react-dom' - - expo-font - - supports-color - optional: true - expo-router@55.0.5(alplmueeiabdlyo62r6crpdhli): dependencies: '@expo/log-box': 55.0.8(@expo/dom-webview@55.0.3)(expo@55.0.5)(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0) @@ -38884,6 +38800,57 @@ snapshots: - supports-color optional: true + expo-router@55.0.5(qfbmyq6c2bmncbs6qduwm3ekeu): + dependencies: + '@expo/log-box': 55.0.8(@expo/dom-webview@55.0.3)(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) + '@expo/metro-runtime': 55.0.7(@expo/dom-webview@55.0.3)(expo@55.0.5)(react-dom@19.2.4(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) + '@expo/schema-utils': 55.0.2 + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@react-navigation/bottom-tabs': 7.15.5(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) + '@react-navigation/native': 7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) + '@react-navigation/native-stack': 7.14.4(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) + client-only: 0.0.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@55.0.7)(expo-router@55.0.5)(react-dom@19.2.4(react@19.2.4))(react-native-webview@13.12.2(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + expo-constants: 55.0.7(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(typescript@5.9.3) + expo-glass-effect: 55.0.8(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) + expo-image: 55.0.6(expo@55.0.5)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) + expo-linking: 55.0.7(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(typescript@5.9.3) + expo-server: 55.0.6 + expo-symbols: 55.0.5(expo-font@55.0.4)(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.11 + query-string: 7.1.3 + react: 19.2.4 + react-fast-compare: 3.2.2 + react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4) + react-native-is-edge-to-edge: 1.2.1(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) + react-native-safe-area-context: 5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) + react-native-screens: 4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) + semver: 7.6.3 + server-only: 0.0.1 + sf-symbols-typescript: 2.2.0 + shallowequal: 1.1.0 + use-latest-callback: 0.2.6(react@19.2.4) + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + optionalDependencies: + '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.33(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-gesture-handler@2.30.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-reanimated@4.2.2(react-native-worklets@0.7.4(@babel/core@7.28.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-safe-area-context@5.7.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native-screens@4.24.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) + '@testing-library/react-native': 13.3.3(jest@30.3.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react-test-renderer@19.1.0(react@19.2.4))(react@19.2.4) + react-dom: 19.2.4(react@19.2.4) + react-native-gesture-handler: 2.30.0(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) + react-native-reanimated: 4.2.2(react-native-worklets@0.7.4(@babel/core@7.28.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4) + react-native-web: 0.21.2(encoding@0.1.13)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - '@types/react' + - '@types/react-dom' + - expo-font + - supports-color + optional: true + expo-router@55.0.5(x6ppsusqzuln52ezuk2uhtcwta): dependencies: '@expo/log-box': 55.0.8(@expo/dom-webview@55.0.3)(expo@55.0.5)(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) @@ -40916,26 +40883,6 @@ snapshots: - ts-node optional: true - jest-cli@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): - dependencies: - '@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - '@jest/test-result': 30.3.0 - '@jest/types': 30.3.0 - chalk: 4.1.2 - exit-x: 0.2.2 - import-local: 3.2.0 - jest-config: 30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - jest-util: 30.3.0 - jest-validate: 30.3.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - optional: true - jest-cli@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.3.3)): dependencies: '@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.3.3)) @@ -41112,40 +41059,6 @@ snapshots: - supports-color optional: true - jest-config@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): - dependencies: - '@babel/core': 7.28.5 - '@jest/get-type': 30.1.0 - '@jest/pattern': 30.0.1 - '@jest/test-sequencer': 30.3.0 - '@jest/types': 30.3.0 - babel-jest: 30.3.0(@babel/core@7.28.5) - chalk: 4.1.2 - ci-info: 4.3.1 - deepmerge: 4.3.1 - glob: 10.5.0 - graceful-fs: 4.2.11 - jest-circus: 30.3.0 - jest-docblock: 30.2.0 - jest-environment-node: 30.3.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.3.0 - jest-runner: 30.3.0 - jest-util: 30.3.0 - jest-validate: 30.3.0 - parse-json: 5.2.0 - pretty-format: 30.3.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.19.1 - esbuild-register: 3.6.0(esbuild@0.19.12) - ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - optional: true - jest-config@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 @@ -41900,20 +41813,6 @@ snapshots: - ts-node optional: true - jest@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): - dependencies: - '@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - '@jest/types': 30.3.0 - import-local: 3.2.0 - jest-cli: 30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - optional: true - jest@30.3.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.3.3)): dependencies: '@jest/core': 30.3.0(esbuild-register@3.6.0(esbuild@0.27.5))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.3.3))