vibefast-cli 0.1.1
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/FINAL-STATUS.md +144 -0
- package/HOW-IT-WORKS.md +559 -0
- package/PLAN.md +453 -0
- package/README.md +129 -0
- package/RECIPES-READY.md +172 -0
- package/STATUS.md +199 -0
- package/SUCCESS.md +259 -0
- package/TESTING-CHECKLIST.md +450 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/64907821e2634080acce34618d2f3d4c/blobs/11f2769953c717e188062bc644da97c1fd1e4d6d0813a226ce7567dba759afab0000019a736fb8d4 +1 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite +0 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-shm +0 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-wal +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-shm +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-wal +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/vibefast-recipes/blobs/620e8cf7c35d9806da25dee237e1d7e8b2432bd98f755b60e2c7f08a48d2c7b90000019a73736484 +0 -0
- package/cloudflare-worker/MIGRATION.md +160 -0
- package/cloudflare-worker/QUICKSTART.md +200 -0
- package/cloudflare-worker/README.md +242 -0
- package/cloudflare-worker/generate-token.js +32 -0
- package/cloudflare-worker/mini-native@latest.zip +0 -0
- package/cloudflare-worker/setup.sh +143 -0
- package/cloudflare-worker/test-recipe/apps/native/src/app/mini/index.tsx +15 -0
- package/cloudflare-worker/test-recipe/recipe.json +16 -0
- package/cloudflare-worker/worker.js +308 -0
- package/cloudflare-worker/wrangler.toml +13 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +149 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/devices.d.ts +3 -0
- package/dist/commands/devices.d.ts.map +1 -0
- package/dist/commands/devices.js +35 -0
- package/dist/commands/devices.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +67 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +40 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +23 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +16 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/remove.d.ts +3 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +67 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/core/__tests__/journal.test.d.ts +2 -0
- package/dist/core/__tests__/journal.test.d.ts.map +1 -0
- package/dist/core/__tests__/journal.test.js +101 -0
- package/dist/core/__tests__/journal.test.js.map +1 -0
- package/dist/core/__tests__/validate.test.d.ts +2 -0
- package/dist/core/__tests__/validate.test.d.ts.map +1 -0
- package/dist/core/__tests__/validate.test.js +53 -0
- package/dist/core/__tests__/validate.test.js.map +1 -0
- package/dist/core/archive.d.ts +2 -0
- package/dist/core/archive.d.ts.map +1 -0
- package/dist/core/archive.js +59 -0
- package/dist/core/archive.js.map +1 -0
- package/dist/core/auth.d.ts +15 -0
- package/dist/core/auth.d.ts.map +1 -0
- package/dist/core/auth.js +76 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/codemod.d.ts +20 -0
- package/dist/core/codemod.d.ts.map +1 -0
- package/dist/core/codemod.js +150 -0
- package/dist/core/codemod.js.map +1 -0
- package/dist/core/fsx.d.ts +12 -0
- package/dist/core/fsx.d.ts.map +1 -0
- package/dist/core/fsx.js +70 -0
- package/dist/core/fsx.js.map +1 -0
- package/dist/core/http.d.ts +30 -0
- package/dist/core/http.d.ts.map +1 -0
- package/dist/core/http.js +95 -0
- package/dist/core/http.js.map +1 -0
- package/dist/core/journal.d.ts +18 -0
- package/dist/core/journal.d.ts.map +1 -0
- package/dist/core/journal.js +34 -0
- package/dist/core/journal.js.map +1 -0
- package/dist/core/log.d.ts +8 -0
- package/dist/core/log.d.ts.map +1 -0
- package/dist/core/log.js +9 -0
- package/dist/core/log.js.map +1 -0
- package/dist/core/pathGuard.d.ts +3 -0
- package/dist/core/pathGuard.d.ts.map +1 -0
- package/dist/core/pathGuard.js +18 -0
- package/dist/core/pathGuard.js.map +1 -0
- package/dist/core/paths.d.ts +11 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +22 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/validate.d.ts +8 -0
- package/dist/core/validate.d.ts.map +1 -0
- package/dist/core/validate.js +27 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/docs/decisions.md +55 -0
- package/package.json +39 -0
- package/recipes/audio-recorder/apps/native/src/app/audio-recorder/index.tsx +5 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-player.tsx +301 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +373 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-waveform.tsx +270 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/index.ts +4 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/recording-list.tsx +89 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-player-demo.tsx +66 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-cloud.tsx +68 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-interview.tsx +102 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/basic.tsx +27 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/index.ts +5 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +82 -0
- package/recipes/audio-recorder/recipe.json +22 -0
- package/recipes/audio-recorder@latest.zip +0 -0
- package/recipes/charts/apps/native/src/app/charts/index.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/README.md +185 -0
- package/recipes/charts/apps/native/src/features/charts/app/preview.tsx +223 -0
- package/recipes/charts/apps/native/src/features/charts/components/area-chart.tsx +40 -0
- package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +143 -0
- package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +196 -0
- package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +65 -0
- package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +143 -0
- package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +246 -0
- package/recipes/charts/apps/native/src/features/charts/components/index.ts +10 -0
- package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +308 -0
- package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +180 -0
- package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +188 -0
- package/recipes/charts/apps/native/src/features/charts/components/stacked-area-chart.tsx +265 -0
- package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +322 -0
- package/recipes/charts/apps/native/src/features/charts/data/mock-data.ts +183 -0
- package/recipes/charts/apps/native/src/features/charts/types/index.ts +66 -0
- package/recipes/charts/recipe.json +22 -0
- package/recipes/charts@latest.zip +0 -0
- package/recipes/chatbot/apps/native/src/app/chatbot/index.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/app/index.tsx +302 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +59 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-input-bar.tsx +469 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +575 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +246 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +161 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/image-preview-list.tsx +115 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +165 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/index.ts +10 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +129 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-error-boundary.tsx +78 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-list.tsx +173 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/model-selector.tsx +283 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/report-content-modal.tsx +188 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/suggested-messages.tsx +67 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/models.ts +20 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/report-reasons.ts +9 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +143 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-config.ts +664 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +359 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +89 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-conversation.ts +79 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-image-picker.ts +122 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +161 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +207 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/index.ts +86 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/models.ts +162 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/providers.ts +62 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/types.ts +40 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/services/file-uploader.ts +238 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/services/message-handler-service.ts +180 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/types/index.ts +60 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/utils/chat-telemetry.ts +91 -0
- package/recipes/chatbot/recipe.json +22 -0
- package/recipes/chatbot@latest.zip +0 -0
- package/recipes/image-generator/apps/native/src/app/image-generator/gallery.tsx +3 -0
- package/recipes/image-generator/apps/native/src/app/image-generator/index.tsx +3 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/app/_layout.tsx +25 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/app/gallery.tsx +217 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/app/index.tsx +237 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/gallery-image.tsx +26 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/image-detail-modal.tsx +215 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/image-model-selector.tsx +210 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/image-placeholder.tsx +26 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-gallery.ts +71 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator-settings.ts +152 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator.ts +93 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/models/models.ts +66 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/services/image-gallery-service.ts +98 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/services/image-save-service.ts +121 -0
- package/recipes/image-generator/recipe.json +22 -0
- package/recipes/image-generator@latest.zip +0 -0
- package/recipes/quiz/apps/native/src/app/quiz/index.tsx +47 -0
- package/recipes/quiz/apps/native/src/features/quiz/components/question.tsx +67 -0
- package/recipes/quiz/apps/native/src/features/quiz/config.ts +11 -0
- package/recipes/quiz/apps/native/src/features/quiz/index.tsx +133 -0
- package/recipes/quiz/recipe.json +22 -0
- package/recipes/quiz@latest.zip +0 -0
- package/recipes/tracker-app/apps/native/src/app/tracker-app/index.tsx +1 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/app/index.tsx +108 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/animated-number.tsx +102 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/calorie-card.tsx +66 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/circular-progress.tsx +97 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/floating-add-button.tsx +27 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/macro-card.tsx +80 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/promo-banner.tsx +98 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/recently-logged.tsx +64 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/week-calendar.tsx +68 -0
- package/recipes/tracker-app/recipe.json +22 -0
- package/recipes/tracker-app@latest.zip +0 -0
- package/recipes/upload-all.sh +32 -0
- package/recipes/voice-bot/apps/native/src/app/voice-bot/index.tsx +27 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/README.md +185 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/conversation-status.tsx +76 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/index.ts +4 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/message-input.tsx +98 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-bot-screen.tsx +173 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-controls.tsx +73 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/index.ts +3 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/services/index.ts +1 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/services/use-voice-bot.ts +161 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/types.ts +29 -0
- package/recipes/voice-bot/recipe.json +22 -0
- package/recipes/voice-bot@latest.zip +0 -0
- package/scripts/create-recipes.mjs +189 -0
- package/src/commands/add.ts +183 -0
- package/src/commands/devices.ts +38 -0
- package/src/commands/doctor.ts +67 -0
- package/src/commands/list.ts +45 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/logout.ts +15 -0
- package/src/commands/remove.ts +78 -0
- package/src/core/__tests__/journal.test.ts +119 -0
- package/src/core/__tests__/validate.test.ts +64 -0
- package/src/core/archive.ts +69 -0
- package/src/core/auth.ts +103 -0
- package/src/core/codemod.ts +211 -0
- package/src/core/fsx.ts +80 -0
- package/src/core/http.ts +136 -0
- package/src/core/journal.ts +64 -0
- package/src/core/log.ts +9 -0
- package/src/core/pathGuard.ts +22 -0
- package/src/core/paths.ts +33 -0
- package/src/core/validate.ts +44 -0
- package/src/index.ts +27 -0
- package/test-critical-cases.mjs +258 -0
- package/tsconfig.json +21 -0
- package/vitest.config.mts +12 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Id } from '@vibefast/backend/_generated/dataModel';
|
|
2
|
+
|
|
3
|
+
import { defaultCacheService } from '@/core/cache';
|
|
4
|
+
|
|
5
|
+
export type SavedImageMetadata = {
|
|
6
|
+
id: string;
|
|
7
|
+
prompt: string;
|
|
8
|
+
provider: string;
|
|
9
|
+
model: string;
|
|
10
|
+
savedAt: number;
|
|
11
|
+
localUri: string;
|
|
12
|
+
remoteImageId?: Id<'generatedImages'>;
|
|
13
|
+
storageId?: Id<'_storage'>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const GALLERY_CACHE_KEY = 'image_gallery_v1';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Service for managing the local image gallery with MMKV persistence
|
|
20
|
+
*/
|
|
21
|
+
export class ImageGalleryService {
|
|
22
|
+
/**
|
|
23
|
+
* Get all saved images from the gallery
|
|
24
|
+
*/
|
|
25
|
+
static async getAllImages(): Promise<SavedImageMetadata[]> {
|
|
26
|
+
try {
|
|
27
|
+
const images =
|
|
28
|
+
await defaultCacheService.getItem<SavedImageMetadata[]>(
|
|
29
|
+
GALLERY_CACHE_KEY,
|
|
30
|
+
);
|
|
31
|
+
return images || [];
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Failed to load gallery images:', error);
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Add a new image to the gallery
|
|
40
|
+
*/
|
|
41
|
+
static async addImage(
|
|
42
|
+
metadata: Omit<SavedImageMetadata, 'id' | 'savedAt'>,
|
|
43
|
+
): Promise<void> {
|
|
44
|
+
try {
|
|
45
|
+
const existingImages = await ImageGalleryService.getAllImages();
|
|
46
|
+
const newImage: SavedImageMetadata = {
|
|
47
|
+
...metadata,
|
|
48
|
+
id: `img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
49
|
+
savedAt: Date.now(),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const updatedImages = [newImage, ...existingImages];
|
|
53
|
+
await defaultCacheService.setItem(GALLERY_CACHE_KEY, updatedImages);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('Failed to add image to gallery:', error);
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Remove an image from the gallery
|
|
62
|
+
*/
|
|
63
|
+
static async removeImage(imageId: string): Promise<void> {
|
|
64
|
+
try {
|
|
65
|
+
const existingImages = await ImageGalleryService.getAllImages();
|
|
66
|
+
const updatedImages = existingImages.filter((img) => img.id !== imageId);
|
|
67
|
+
await defaultCacheService.setItem(GALLERY_CACHE_KEY, updatedImages);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Failed to remove image from gallery:', error);
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Clear all images from the gallery
|
|
76
|
+
*/
|
|
77
|
+
static async clearGallery(): Promise<void> {
|
|
78
|
+
try {
|
|
79
|
+
await defaultCacheService.setItem(GALLERY_CACHE_KEY, []);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
console.error('Failed to clear gallery:', error);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get the total number of saved images
|
|
88
|
+
*/
|
|
89
|
+
static async getImageCount(): Promise<number> {
|
|
90
|
+
try {
|
|
91
|
+
const images = await ImageGalleryService.getAllImages();
|
|
92
|
+
return images.length;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Failed to get image count:', error);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Id } from '@vibefast/backend/_generated/dataModel';
|
|
2
|
+
import { File, Paths } from 'expo-file-system';
|
|
3
|
+
import * as MediaLibrary from 'expo-media-library';
|
|
4
|
+
import { Alert, Platform } from 'react-native';
|
|
5
|
+
|
|
6
|
+
import { ImageGalleryService } from './image-gallery-service';
|
|
7
|
+
|
|
8
|
+
export type ImageSaveMetadata = {
|
|
9
|
+
prompt: string;
|
|
10
|
+
provider: string;
|
|
11
|
+
model: string;
|
|
12
|
+
remoteImageId?: Id<'generatedImages'>;
|
|
13
|
+
storageId?: Id<'_storage'>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Service for handling image saving operations to the device gallery and local storage
|
|
18
|
+
*/
|
|
19
|
+
export class ImageSaveService {
|
|
20
|
+
/**
|
|
21
|
+
* Save a base64 image data URI to the device gallery and local gallery
|
|
22
|
+
* @param imageDataUri - The data URI containing the image (e.g., 'data:image/png;base64,iVBORw0KGgo...')
|
|
23
|
+
* @param metadata - Optional metadata about the image generation
|
|
24
|
+
* @returns Promise that resolves when save is complete
|
|
25
|
+
*/
|
|
26
|
+
static async saveImageToGallery(
|
|
27
|
+
imageDataUri: string,
|
|
28
|
+
metadata?: ImageSaveMetadata,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
try {
|
|
31
|
+
// Check if running on web - FileSystem is not available
|
|
32
|
+
if (Platform.OS === 'web') {
|
|
33
|
+
// On web, just save to local gallery
|
|
34
|
+
if (metadata) {
|
|
35
|
+
await ImageGalleryService.addImage({
|
|
36
|
+
prompt: metadata.prompt,
|
|
37
|
+
provider: metadata.provider,
|
|
38
|
+
model: metadata.model,
|
|
39
|
+
localUri: imageDataUri,
|
|
40
|
+
remoteImageId: metadata.remoteImageId,
|
|
41
|
+
storageId: metadata.storageId,
|
|
42
|
+
});
|
|
43
|
+
Alert.alert('Success!', 'Image saved to your gallery.');
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Request permissions (native only)
|
|
49
|
+
const { status } = await MediaLibrary.requestPermissionsAsync();
|
|
50
|
+
if (status !== 'granted') {
|
|
51
|
+
Alert.alert(
|
|
52
|
+
'Permission Required',
|
|
53
|
+
'We need permission to save photos to your gallery.',
|
|
54
|
+
);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Extract base64 data from data URI (supports multiple formats)
|
|
59
|
+
const match = imageDataUri.match(/^data:image\/(\w+);base64,(.+)$/);
|
|
60
|
+
if (!match) {
|
|
61
|
+
throw new Error('Invalid image data format');
|
|
62
|
+
}
|
|
63
|
+
const [, imageFormat, base64Code] = match;
|
|
64
|
+
const fileExtension = imageFormat === 'jpeg' ? 'jpg' : imageFormat;
|
|
65
|
+
|
|
66
|
+
// Create temporary file using new File API
|
|
67
|
+
const filename = `generated-image-${Date.now()}.${fileExtension}`;
|
|
68
|
+
const file = new File(Paths.cache, filename);
|
|
69
|
+
|
|
70
|
+
// Create and write the file
|
|
71
|
+
file.create();
|
|
72
|
+
await file.write(base64Code, { encoding: 'base64' });
|
|
73
|
+
|
|
74
|
+
// Save to device gallery
|
|
75
|
+
const asset = await MediaLibrary.createAssetAsync(file.uri);
|
|
76
|
+
await MediaLibrary.createAlbumAsync('MyAppImages', asset, false);
|
|
77
|
+
|
|
78
|
+
// Save to local gallery if metadata is provided
|
|
79
|
+
if (metadata) {
|
|
80
|
+
await ImageGalleryService.addImage({
|
|
81
|
+
prompt: metadata.prompt,
|
|
82
|
+
provider: metadata.provider,
|
|
83
|
+
model: metadata.model,
|
|
84
|
+
localUri: imageDataUri,
|
|
85
|
+
remoteImageId: metadata.remoteImageId,
|
|
86
|
+
storageId: metadata.storageId,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Alert.alert('Success!', 'Image saved to your gallery.');
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error('Failed to save image:', error);
|
|
93
|
+
Alert.alert('Error', 'Could not save the image.');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Save image only to local gallery (without device gallery)
|
|
99
|
+
* @param imageDataUri - The data URI containing the image
|
|
100
|
+
* @param metadata - Metadata about the image generation
|
|
101
|
+
* @returns Promise that resolves when save is complete
|
|
102
|
+
*/
|
|
103
|
+
static async saveToLocalGallery(
|
|
104
|
+
imageDataUri: string,
|
|
105
|
+
metadata: ImageSaveMetadata,
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
try {
|
|
108
|
+
await ImageGalleryService.addImage({
|
|
109
|
+
prompt: metadata.prompt,
|
|
110
|
+
provider: metadata.provider,
|
|
111
|
+
model: metadata.model,
|
|
112
|
+
localUri: imageDataUri,
|
|
113
|
+
remoteImageId: metadata.remoteImageId,
|
|
114
|
+
storageId: metadata.storageId,
|
|
115
|
+
});
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Failed to save to local gallery:', error);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "image-generator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-powered image generation",
|
|
5
|
+
"copy": [
|
|
6
|
+
{
|
|
7
|
+
"from": "apps/native/src/app/image-generator",
|
|
8
|
+
"to": "apps/native/src/app/(root)/(protected)/image-generator"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"from": "apps/native/src/features/image-generator",
|
|
12
|
+
"to": "apps/native/src/features/image-generator"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"nav": {
|
|
16
|
+
"href": "/(root)/(protected)/image-generator",
|
|
17
|
+
"label": "Image Generator",
|
|
18
|
+
"icon": "🎨",
|
|
19
|
+
"color": "#A855F7"
|
|
20
|
+
},
|
|
21
|
+
"target": "native"
|
|
22
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import Quiz from '@/features/quiz';
|
|
2
|
+
import { QuestionMode } from '@/features/quiz/config';
|
|
3
|
+
|
|
4
|
+
const questions = [
|
|
5
|
+
{
|
|
6
|
+
text: 'What is your favorite color?',
|
|
7
|
+
mode: QuestionMode.AutoAdvance,
|
|
8
|
+
options: [
|
|
9
|
+
{ id: 'red', text: 'Red', note: 'Red is the color of passion.' },
|
|
10
|
+
{ id: 'blue', text: 'Blue', note: 'Blue is the color of calm.' },
|
|
11
|
+
{ id: 'green', text: 'Green', note: 'Green is the color of nature.' },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
text: 'What is your favorite animal?',
|
|
16
|
+
mode: QuestionMode.ManualAdvance,
|
|
17
|
+
options: [
|
|
18
|
+
{ id: 'dog', text: 'Dog', note: 'Dogs are loyal.' },
|
|
19
|
+
{ id: 'cat', text: 'Cat', note: 'Cats are independent.' },
|
|
20
|
+
{ id: 'bird', text: 'Bird', note: 'Birds can fly.' },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
text: 'What is your favorite animal?',
|
|
25
|
+
mode: QuestionMode.ManualAdvance,
|
|
26
|
+
options: [
|
|
27
|
+
{ id: 'dog', text: 'Dog', note: 'Dogs are loyal.' },
|
|
28
|
+
{ id: 'cat', text: 'Cat', note: 'Cats are independent.' },
|
|
29
|
+
{ id: 'bird', text: 'Bird', note: 'Birds can fly.' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
text: 'What is your favorite animal?',
|
|
34
|
+
mode: QuestionMode.ManualAdvance,
|
|
35
|
+
options: [
|
|
36
|
+
{ id: 'dog', text: 'Dog', note: 'Dogs are loyal.' },
|
|
37
|
+
{ id: 'cat', text: 'Cat', note: 'Cats are independent.' },
|
|
38
|
+
{ id: 'bird', text: 'Bird', note: 'Birds can fly.' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const QuizScreen = () => {
|
|
44
|
+
return <Quiz questions={questions} />;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default QuizScreen;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { MotiView } from 'moti';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { TouchableOpacity } from 'react-native';
|
|
4
|
+
import Animated, { Layout } from 'react-native-reanimated';
|
|
5
|
+
|
|
6
|
+
import { Text } from '@/components/ui';
|
|
7
|
+
|
|
8
|
+
import { QuestionMode, quizConfig } from '../config';
|
|
9
|
+
|
|
10
|
+
const Question = ({ question, onOptionSelect, selectedOption }) => {
|
|
11
|
+
return (
|
|
12
|
+
<MotiView
|
|
13
|
+
from={{ opacity: 0, translateY: 50 }}
|
|
14
|
+
animate={{ opacity: 1, translateY: 0 }}
|
|
15
|
+
exit={{ opacity: 0, translateY: -50 }}
|
|
16
|
+
transition={{
|
|
17
|
+
type: 'timing',
|
|
18
|
+
duration: quizConfig.animation.duration,
|
|
19
|
+
}}
|
|
20
|
+
className="flex-1"
|
|
21
|
+
>
|
|
22
|
+
<Text className="my-8 text-center text-2xl font-bold">
|
|
23
|
+
{question.text}
|
|
24
|
+
</Text>
|
|
25
|
+
<Animated.View
|
|
26
|
+
className="flex-1 justify-center gap-y-4"
|
|
27
|
+
layout={Layout.springify().duration(quizConfig.animation.duration)}
|
|
28
|
+
>
|
|
29
|
+
{question.options.map((option) => (
|
|
30
|
+
<TouchableOpacity
|
|
31
|
+
key={option.id}
|
|
32
|
+
onPress={() => onOptionSelect(option)}
|
|
33
|
+
>
|
|
34
|
+
<MotiView
|
|
35
|
+
from={{ scale: 1 }}
|
|
36
|
+
animate={{ scale: selectedOption === option ? 1.02 : 1 }}
|
|
37
|
+
transition={{
|
|
38
|
+
type: 'timing',
|
|
39
|
+
duration: quizConfig.animation.duration,
|
|
40
|
+
}}
|
|
41
|
+
className={`rounded-lg border p-4 ${selectedOption === option ? 'border-primary-500 bg-primary-500/20 dark:border-primary-500 dark:bg-primary-900/20' : 'border-neutral-300'}`}
|
|
42
|
+
>
|
|
43
|
+
<Text className="text-lg">{option.text}</Text>
|
|
44
|
+
{selectedOption === option &&
|
|
45
|
+
question.mode === QuestionMode.ManualAdvance && (
|
|
46
|
+
<MotiView
|
|
47
|
+
from={{ opacity: 0, height: 0 }}
|
|
48
|
+
animate={{ opacity: 1, height: 'auto' }}
|
|
49
|
+
transition={{
|
|
50
|
+
type: 'timing',
|
|
51
|
+
duration: quizConfig.animation.duration,
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<Text className="mt-2 text-sm text-neutral-600">
|
|
55
|
+
{option.note}
|
|
56
|
+
</Text>
|
|
57
|
+
</MotiView>
|
|
58
|
+
)}
|
|
59
|
+
</MotiView>
|
|
60
|
+
</TouchableOpacity>
|
|
61
|
+
))}
|
|
62
|
+
</Animated.View>
|
|
63
|
+
</MotiView>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default Question;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Ionicons } from '@expo/vector-icons';
|
|
2
|
+
import { useRouter } from 'expo-router';
|
|
3
|
+
import { AnimatePresence, MotiView } from 'moti';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { View } from 'react-native';
|
|
6
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
7
|
+
|
|
8
|
+
import { Button, Text } from '@/components/ui';
|
|
9
|
+
|
|
10
|
+
import Question from './components/question';
|
|
11
|
+
import { QuestionMode, quizConfig } from './config';
|
|
12
|
+
|
|
13
|
+
const ProgressBar = ({ progress, handlePreviousQuestion }) => (
|
|
14
|
+
<View className="mr-3 flex flex-row items-center">
|
|
15
|
+
<Button
|
|
16
|
+
size="icon"
|
|
17
|
+
onPress={handlePreviousQuestion}
|
|
18
|
+
className="mb-5 mr-3 mt-0 shrink-0 !bg-transparent"
|
|
19
|
+
>
|
|
20
|
+
<Ionicons name="arrow-back" size={24} color="#666" />
|
|
21
|
+
</Button>
|
|
22
|
+
<View className="mb-5 h-3 flex-1 overflow-hidden rounded-xl bg-neutral-200 dark:bg-neutral-800">
|
|
23
|
+
<MotiView
|
|
24
|
+
from={{ width: '0%' }}
|
|
25
|
+
animate={{ width: `${progress * 100}%` }}
|
|
26
|
+
transition={{
|
|
27
|
+
type: 'timing',
|
|
28
|
+
duration: quizConfig.animation.duration,
|
|
29
|
+
}}
|
|
30
|
+
className="h-full bg-primary-500"
|
|
31
|
+
/>
|
|
32
|
+
</View>
|
|
33
|
+
</View>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const Quiz = ({ questions }) => {
|
|
37
|
+
const router = useRouter();
|
|
38
|
+
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
|
|
39
|
+
const [selectedOption, setSelectedOption] = useState(null);
|
|
40
|
+
const [showContinue, setShowContinue] = useState(false);
|
|
41
|
+
|
|
42
|
+
const handleOptionSelect = (option) => {
|
|
43
|
+
setSelectedOption(option);
|
|
44
|
+
const currentQuestionMode = questions[currentQuestionIndex].mode;
|
|
45
|
+
if (currentQuestionMode === QuestionMode.AutoAdvance) {
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
handleNextQuestion();
|
|
48
|
+
}, quizConfig.animation.duration + 200);
|
|
49
|
+
} else {
|
|
50
|
+
setShowContinue(true);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleNextQuestion = () => {
|
|
55
|
+
setSelectedOption(null);
|
|
56
|
+
setShowContinue(false);
|
|
57
|
+
setCurrentQuestionIndex(currentQuestionIndex + 1);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handlePreviousQuestion = () => {
|
|
61
|
+
if (currentQuestionIndex === 0) {
|
|
62
|
+
router.back();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
setSelectedOption(null);
|
|
66
|
+
setShowContinue(false);
|
|
67
|
+
setCurrentQuestionIndex(currentQuestionIndex - 1);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const progress = currentQuestionIndex / questions.length;
|
|
71
|
+
|
|
72
|
+
if (currentQuestionIndex >= questions.length) {
|
|
73
|
+
return (
|
|
74
|
+
<SafeAreaView className="flex-1 p-5">
|
|
75
|
+
<Text className="mb-5 text-xl font-bold">Quiz Complete!</Text>
|
|
76
|
+
</SafeAreaView>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const currentQuestion = questions[currentQuestionIndex];
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<SafeAreaView className="flex-1 p-5">
|
|
84
|
+
<ProgressBar
|
|
85
|
+
progress={progress}
|
|
86
|
+
handlePreviousQuestion={handlePreviousQuestion}
|
|
87
|
+
/>
|
|
88
|
+
<AnimatePresence exitBeforeEnter>
|
|
89
|
+
<Question
|
|
90
|
+
key={currentQuestionIndex}
|
|
91
|
+
question={currentQuestion}
|
|
92
|
+
onOptionSelect={handleOptionSelect}
|
|
93
|
+
selectedOption={selectedOption}
|
|
94
|
+
/>
|
|
95
|
+
</AnimatePresence>
|
|
96
|
+
|
|
97
|
+
{quizConfig.showContinueButton &&
|
|
98
|
+
showContinue &&
|
|
99
|
+
currentQuestion.mode === QuestionMode.ManualAdvance && (
|
|
100
|
+
<MotiView
|
|
101
|
+
from={{ opacity: 0, translateY: 50 }}
|
|
102
|
+
animate={{ opacity: 1, translateY: 0 }}
|
|
103
|
+
transition={{
|
|
104
|
+
type: 'timing',
|
|
105
|
+
duration: quizConfig.animation.duration,
|
|
106
|
+
}}
|
|
107
|
+
className="mt-8"
|
|
108
|
+
>
|
|
109
|
+
<View className="flex-row gap-3">
|
|
110
|
+
{/* {currentQuestionIndex > 0 && (
|
|
111
|
+
<Button
|
|
112
|
+
variant="outline"
|
|
113
|
+
onPress={handlePreviousQuestion}
|
|
114
|
+
className="flex-shrink-0"
|
|
115
|
+
>
|
|
116
|
+
<Ionicons name="arrow-back" size={20} color="#666" />
|
|
117
|
+
</Button>
|
|
118
|
+
)} */}
|
|
119
|
+
<Button
|
|
120
|
+
variant="secondary"
|
|
121
|
+
onPress={handleNextQuestion}
|
|
122
|
+
label="Continue"
|
|
123
|
+
className="flex-1"
|
|
124
|
+
textClassName="text-white"
|
|
125
|
+
/>
|
|
126
|
+
</View>
|
|
127
|
+
</MotiView>
|
|
128
|
+
)}
|
|
129
|
+
</SafeAreaView>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export default Quiz;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "quiz",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Interactive quiz feature",
|
|
5
|
+
"copy": [
|
|
6
|
+
{
|
|
7
|
+
"from": "apps/native/src/app/quiz",
|
|
8
|
+
"to": "apps/native/src/app/(root)/(protected)/quiz"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"from": "apps/native/src/features/quiz",
|
|
12
|
+
"to": "apps/native/src/features/quiz"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"nav": {
|
|
16
|
+
"href": "/(root)/(protected)/quiz",
|
|
17
|
+
"label": "Quiz",
|
|
18
|
+
"icon": "📱",
|
|
19
|
+
"color": "#F97316"
|
|
20
|
+
},
|
|
21
|
+
"target": "native"
|
|
22
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '@/features/tracker-app/app';
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Ionicons, MaterialIcons } from '@expo/vector-icons';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { ScrollView, View } from 'react-native';
|
|
4
|
+
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
5
|
+
|
|
6
|
+
import { Text } from '@/components/ui';
|
|
7
|
+
import { useThemeConfig } from '@/lib/use-theme-config';
|
|
8
|
+
|
|
9
|
+
import CalorieCard from '../components/calorie-card';
|
|
10
|
+
import MacroCard from '../components/macro-card';
|
|
11
|
+
import PromoBanner from '../components/promo-banner';
|
|
12
|
+
import RecentlyLogged from '../components/recently-logged';
|
|
13
|
+
import WeekCalendar from '../components/week-calendar';
|
|
14
|
+
|
|
15
|
+
export default function HomeScreen() {
|
|
16
|
+
const theme = useThemeConfig();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<SafeAreaView
|
|
20
|
+
className="flex-1"
|
|
21
|
+
style={{ backgroundColor: theme.colors.background }}
|
|
22
|
+
>
|
|
23
|
+
<ScrollView className="flex-1 px-5" showsVerticalScrollIndicator={false}>
|
|
24
|
+
<View className="flex-row items-center justify-between pb-6 pt-4">
|
|
25
|
+
<View className="flex-row items-center">
|
|
26
|
+
<MaterialIcons
|
|
27
|
+
name="restaurant-menu"
|
|
28
|
+
size={28}
|
|
29
|
+
color={theme.colors.foreground}
|
|
30
|
+
/>
|
|
31
|
+
<Text
|
|
32
|
+
className="ml-2 text-2xl font-extrabold"
|
|
33
|
+
style={{ color: theme.colors.foreground }}
|
|
34
|
+
>
|
|
35
|
+
CalAI
|
|
36
|
+
</Text>
|
|
37
|
+
</View>
|
|
38
|
+
<View
|
|
39
|
+
className="flex-row items-center rounded-2xl px-3 py-1.5"
|
|
40
|
+
style={{ backgroundColor: theme.colors.card }}
|
|
41
|
+
>
|
|
42
|
+
<Ionicons name="flame" size={16} color={theme.colors.warning} />
|
|
43
|
+
<Text
|
|
44
|
+
className="ml-1 text-base font-bold"
|
|
45
|
+
style={{ color: theme.colors.foreground }}
|
|
46
|
+
>
|
|
47
|
+
1
|
|
48
|
+
</Text>
|
|
49
|
+
</View>
|
|
50
|
+
</View>
|
|
51
|
+
|
|
52
|
+
<WeekCalendar />
|
|
53
|
+
|
|
54
|
+
<CalorieCard calories={1852} progress={25} />
|
|
55
|
+
|
|
56
|
+
<View className="mb-2 flex-row gap-3">
|
|
57
|
+
<MacroCard
|
|
58
|
+
value={122}
|
|
59
|
+
unit="g"
|
|
60
|
+
label="Protein left"
|
|
61
|
+
progress={35}
|
|
62
|
+
color={theme.colors.destructive}
|
|
63
|
+
icon={
|
|
64
|
+
<MaterialIcons
|
|
65
|
+
name="restaurant-menu"
|
|
66
|
+
size={20}
|
|
67
|
+
color={theme.colors.destructive}
|
|
68
|
+
/>
|
|
69
|
+
}
|
|
70
|
+
delay={600}
|
|
71
|
+
/>
|
|
72
|
+
<MacroCard
|
|
73
|
+
value={222}
|
|
74
|
+
unit="g"
|
|
75
|
+
label="Carbs left"
|
|
76
|
+
progress={60}
|
|
77
|
+
color={theme.colors.warning}
|
|
78
|
+
icon={
|
|
79
|
+
<Ionicons
|
|
80
|
+
name="nutrition"
|
|
81
|
+
size={20}
|
|
82
|
+
color={theme.colors.warning}
|
|
83
|
+
/>
|
|
84
|
+
}
|
|
85
|
+
delay={800}
|
|
86
|
+
/>
|
|
87
|
+
<MacroCard
|
|
88
|
+
value={49}
|
|
89
|
+
unit="g"
|
|
90
|
+
label="Fat left"
|
|
91
|
+
progress={45}
|
|
92
|
+
color={theme.colors.primary}
|
|
93
|
+
icon={
|
|
94
|
+
<Ionicons name="water" size={20} color={theme.colors.primary} />
|
|
95
|
+
}
|
|
96
|
+
delay={1000}
|
|
97
|
+
/>
|
|
98
|
+
</View>
|
|
99
|
+
|
|
100
|
+
<PromoBanner />
|
|
101
|
+
|
|
102
|
+
<RecentlyLogged />
|
|
103
|
+
|
|
104
|
+
<View className="h-25" />
|
|
105
|
+
</ScrollView>
|
|
106
|
+
</SafeAreaView>
|
|
107
|
+
);
|
|
108
|
+
}
|