diff --git a/apps/figgos/apps/backend/src/figures/dto/create-figure.dto.ts b/apps/figgos/apps/backend/src/figures/dto/create-figure.dto.ts index 080f2604e..21596ec95 100644 --- a/apps/figgos/apps/backend/src/figures/dto/create-figure.dto.ts +++ b/apps/figgos/apps/backend/src/figures/dto/create-figure.dto.ts @@ -18,4 +18,8 @@ export class CreateFigureDto { @IsString() @IsIn(['en', 'de']) language?: FigureLanguage = 'en'; + + @IsOptional() + @IsString() + faceImage?: string; } diff --git a/apps/figgos/apps/backend/src/figures/figures.controller.ts b/apps/figgos/apps/backend/src/figures/figures.controller.ts index 4faf29b33..0c36aa7d3 100644 --- a/apps/figgos/apps/backend/src/figures/figures.controller.ts +++ b/apps/figgos/apps/backend/src/figures/figures.controller.ts @@ -23,7 +23,8 @@ export class FiguresController { user.userId, dto.name, dto.description, - dto.language || 'en' + dto.language || 'en', + dto.faceImage ); return { figure }; } diff --git a/apps/figgos/apps/backend/src/figures/figures.service.ts b/apps/figgos/apps/backend/src/figures/figures.service.ts index 6da0cac83..5f17830a6 100644 --- a/apps/figgos/apps/backend/src/figures/figures.service.ts +++ b/apps/figgos/apps/backend/src/figures/figures.service.ts @@ -33,7 +33,8 @@ export class FiguresService { userId: string, name: string, description: string, - language: FigureLanguage = 'en' + language: FigureLanguage = 'en', + faceImage?: string ): Promise
{ const rarity = this.rollRarity(); const cardStyle = getCardStyle(rarity); @@ -58,6 +59,7 @@ export class FiguresService { language, rarity, cardStyle, + faceImage, }); return completed; diff --git a/apps/figgos/apps/backend/src/generation/gemini.service.ts b/apps/figgos/apps/backend/src/generation/gemini.service.ts index 5ab7fb909..6761259b3 100644 --- a/apps/figgos/apps/backend/src/generation/gemini.service.ts +++ b/apps/figgos/apps/backend/src/generation/gemini.service.ts @@ -1,6 +1,6 @@ import type { CardStyle, FigureLanguage, FigureRarity, GeneratedProfile } from '@figgos/shared'; import { STAT_RANGES } from '@figgos/shared'; -import { GoogleGenAI } from '@google/genai'; +import { GoogleGenAI, HarmCategory, HarmBlockThreshold } from '@google/genai'; import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { @@ -18,6 +18,19 @@ export class GeminiService { private readonly logger = new Logger(GeminiService.name); private readonly client: GoogleGenAI; + private readonly safetySettings = [ + { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE }, + { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE }, + { + category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_NONE, + }, + ]; + constructor(private config: ConfigService) { const apiKey = this.config.get('GEMINI_API_KEY'); if (!apiKey) { @@ -46,6 +59,7 @@ export class GeminiService { responseSchema: PROFILE_JSON_SCHEMA, temperature: 1.0, thinkingConfig: { thinkingBudget: 0 }, + safetySettings: this.safetySettings, }, }); @@ -87,26 +101,28 @@ export class GeminiService { visualDescription: string, items: string[], cardStyle: CardStyle, - faceImageUrl?: string | null + faceImage?: string | null ): Promise { - const prompt = buildImagePrompt( - name, - subtitle, - visualDescription, - items, - cardStyle, - !!faceImageUrl + const hasFace = !!faceImage; + const prompt = buildImagePrompt(name, subtitle, visualDescription, items, cardStyle, hasFace); + + this.logger.log( + `Generating image for "${name}" (${cardStyle})${hasFace ? ' with face reference' : ''}...` ); - this.logger.log(`Generating image for "${name}" (${cardStyle})...`); - - // Build contents array — if face image provided, include it + // Build contents array — if face image provided, include it as inline data const contents: Array = []; - if (faceImageUrl) { - // TODO: Download face image from S3, convert to base64, add as inline data - // For now, face transfer is not yet supported in the backend - this.logger.warn('Face transfer not yet implemented in backend'); + if (faceImage) { + // Strip data URL prefix if present (e.g. "data:image/jpeg;base64,...") + const base64Data = faceImage.includes(',') ? faceImage.split(',')[1] : faceImage; + contents.push({ + inlineData: { + mimeType: 'image/jpeg', + data: base64Data, + }, + }); + this.logger.log('Face reference image attached to generation request'); } contents.push(prompt); @@ -116,13 +132,16 @@ export class GeminiService { contents, config: { responseModalities: ['IMAGE', 'TEXT'], + safetySettings: this.safetySettings, }, }); // Extract image from response const parts = response.candidates?.[0]?.content?.parts; if (!parts) { - throw new Error('Gemini returned no content parts'); + throw new Error( + 'The AI could not generate this figure — try a different description or photo' + ); } for (const part of parts) { diff --git a/apps/figgos/apps/backend/src/generation/generation.service.ts b/apps/figgos/apps/backend/src/generation/generation.service.ts index f47e93b97..8d48faa31 100644 --- a/apps/figgos/apps/backend/src/generation/generation.service.ts +++ b/apps/figgos/apps/backend/src/generation/generation.service.ts @@ -35,6 +35,7 @@ export class GenerationService { language: FigureLanguage; rarity: FigureRarity; cardStyle: CardStyle; + faceImage?: string; } ): Promise
{ try { @@ -58,7 +59,8 @@ export class GenerationService { profile.subtitle, profile.visualDescription, itemLabels, - input.cardStyle + input.cardStyle, + input.faceImage ); // Phase 3: Process image (bg removal + WebP) diff --git a/apps/figgos/apps/backend/src/generation/image-processing.service.ts b/apps/figgos/apps/backend/src/generation/image-processing.service.ts index d6b31b387..2dca571b7 100644 --- a/apps/figgos/apps/backend/src/generation/image-processing.service.ts +++ b/apps/figgos/apps/backend/src/generation/image-processing.service.ts @@ -53,6 +53,7 @@ export class ImageProcessingService implements OnModuleInit { } return sharp(data, { raw: { width: info.width, height: info.height, channels: 4 } }) + .trim() .webp({ quality: 85 }) .toBuffer(); } @@ -84,7 +85,10 @@ export class ImageProcessingService implements OnModuleInit { const { data, width, height, channels } = img; const buf = Buffer.from(data); - return sharp(buf, { raw: { width, height, channels } }).webp({ quality: 85 }).toBuffer(); + return sharp(buf, { raw: { width, height, channels } }) + .trim() + .webp({ quality: 85 }) + .toBuffer(); } finally { await unlink(tmpPath).catch(() => {}); } diff --git a/apps/figgos/apps/backend/src/main.ts b/apps/figgos/apps/backend/src/main.ts index cee04a4e3..c444be819 100644 --- a/apps/figgos/apps/backend/src/main.ts +++ b/apps/figgos/apps/backend/src/main.ts @@ -5,4 +5,5 @@ bootstrapApp(AppModule, { defaultPort: 3025, serviceName: 'Figgos', additionalCorsOrigins: ['http://localhost:5196'], + bodyLimit: '5mb', }); diff --git a/apps/figgos/apps/mobile/app.json b/apps/figgos/apps/mobile/app.json index 888d972f7..a20518dd9 100644 --- a/apps/figgos/apps/mobile/app.json +++ b/apps/figgos/apps/mobile/app.json @@ -31,7 +31,7 @@ "output": "static", "favicon": "./assets/images/favicon.png" }, - "plugins": ["expo-router", "expo-secure-store"], + "plugins": ["expo-router", "expo-secure-store", "expo-image-picker"], "experiments": { "typedRoutes": true, "tsconfigPaths": true diff --git a/apps/figgos/apps/mobile/app/(tabs)/carousel.tsx b/apps/figgos/apps/mobile/app/(tabs)/carousel.tsx index aedfb0d17..94da64ac1 100644 --- a/apps/figgos/apps/mobile/app/(tabs)/carousel.tsx +++ b/apps/figgos/apps/mobile/app/(tabs)/carousel.tsx @@ -7,16 +7,18 @@ import { Animated, Dimensions, ActivityIndicator, + Modal, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useRouter, useFocusEffect } from 'expo-router'; +import { useFocusEffect } from 'expo-router'; import type { FigureResponse, FigureRarity } from '@figgos/shared'; import { api } from '../../services/api'; +import FlippableCard from '../../components/FlippableCard'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); -const CARD_WIDTH = SCREEN_WIDTH * 0.65; +const CARD_WIDTH = SCREEN_WIDTH * 0.8; const CARD_HEIGHT = CARD_WIDTH * 1.45; -const SPACING = 14; +const SPACING = 12; const SIDE_SPACE = (SCREEN_WIDTH - CARD_WIDTH) / 2; const RARITY_COLORS: Record = { @@ -27,16 +29,29 @@ const RARITY_COLORS: Record = { }; export default function CarouselScreen() { - const router = useRouter(); const scrollX = useRef(new Animated.Value(0)).current; const [figures, setFigures] = useState([]); const [loading, setLoading] = useState(true); + const [selected, setSelected] = useState(null); useFocusEffect( useCallback(() => { api.figures .list() - .then(({ figures }) => setFigures(figures)) + .then(async ({ figures }) => { + const withImages = figures.filter((f) => f.imageUrl); + const checks = await Promise.all( + withImages.map(async (f) => { + try { + await Image.prefetch(f.imageUrl!); + return f; + } catch { + return null; + } + }) + ); + setFigures(checks.filter((f): f is FigureResponse => f !== null)); + }) .finally(() => setLoading(false)); }, []) ); @@ -61,7 +76,7 @@ export default function CarouselScreen() { return ( - + - - + item.id} @@ -119,10 +133,7 @@ export default function CarouselScreen() { opacity, }} > - router.push(`/card/v2/${item.id}` as any)} - className="active:opacity-90" - > + setSelected(item)} className="active:opacity-90"> - {item.imageUrl ? ( - - ) : ( - - - {item.name} - - - )} + @@ -164,41 +155,41 @@ export default function CarouselScreen() { /> - - {figures.map((figure, i) => { - const inputRange = [ - (i - 1) * (CARD_WIDTH + SPACING), - i * (CARD_WIDTH + SPACING), - (i + 1) * (CARD_WIDTH + SPACING), - ]; - - const dotScale = scrollX.interpolate({ - inputRange, - outputRange: [1, 1.4, 1], - extrapolate: 'clamp', - }); - - const dotOpacity = scrollX.interpolate({ - inputRange, - outputRange: [0.3, 1, 0.3], - extrapolate: 'clamp', - }); - - return ( - - ); - })} - + {/* Card Detail Overlay */} + setSelected(null)} + > + setSelected(null)} + style={{ + flex: 1, + backgroundColor: 'rgba(5, 5, 15, 0.92)', + justifyContent: 'center', + alignItems: 'center', + }} + > + {}}> + {selected && ( + + + Drag to rotate · Double-tap to flip + + + + )} + + + ); } diff --git a/apps/figgos/apps/mobile/app/(tabs)/collection.tsx b/apps/figgos/apps/mobile/app/(tabs)/collection.tsx index 13a97d11a..afd0ce8f5 100644 --- a/apps/figgos/apps/mobile/app/(tabs)/collection.tsx +++ b/apps/figgos/apps/mobile/app/(tabs)/collection.tsx @@ -7,11 +7,13 @@ import { FlatList, Dimensions, ActivityIndicator, + Modal, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import { useRouter, useFocusEffect } from 'expo-router'; +import { useFocusEffect } from 'expo-router'; import type { FigureResponse } from '@figgos/shared'; import { api } from '../../services/api'; +import FlippableCard from '../../components/FlippableCard'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); const GAP = 10; @@ -20,12 +22,16 @@ const COLUMNS = 2; const CARD_WIDTH = (SCREEN_WIDTH - PADDING * 2 - GAP * (COLUMNS - 1)) / COLUMNS; const CARD_HEIGHT = CARD_WIDTH * 1.45; -function CardThumbnail({ figure }: { figure: FigureResponse }) { - const router = useRouter(); - +function CardThumbnail({ + figure, + onPress, +}: { + figure: FigureResponse; + onPress: (f: FigureResponse) => void; +}) { return ( router.push(`/card/v2/${figure.id}` as any)} + onPress={() => onPress(figure)} style={{ width: CARD_WIDTH }} className="active:opacity-80" > @@ -37,28 +43,11 @@ function CardThumbnail({ figure }: { figure: FigureResponse }) { overflow: 'hidden', }} > - {figure.imageUrl ? ( - - ) : ( - - - {figure.name} - - - )} + ); @@ -68,14 +57,26 @@ export default function CollectionScreen() { const [figures, setFigures] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [selected, setSelected] = useState(null); useFocusEffect( useCallback(() => { setLoading(true); api.figures .list() - .then(({ figures }) => { - setFigures(figures); + .then(async ({ figures }) => { + const withImages = figures.filter((f) => f.imageUrl); + const checks = await Promise.all( + withImages.map(async (f) => { + try { + await Image.prefetch(f.imageUrl!); + return f; + } catch { + return null; + } + }) + ); + setFigures(checks.filter((f): f is FigureResponse => f !== null)); setError(null); }) .catch((e) => setError(e.message)) @@ -126,9 +127,41 @@ export default function CollectionScreen() { keyExtractor={(item) => item.id} contentContainerStyle={{ paddingHorizontal: PADDING, paddingBottom: 40 }} columnWrapperStyle={{ gap: GAP, marginBottom: GAP }} - renderItem={({ item }) => } + renderItem={({ item }) => } /> )} + + {/* Card Detail Overlay */} + setSelected(null)} + > + setSelected(null)} + style={{ + flex: 1, + backgroundColor: 'rgba(5, 5, 15, 0.92)', + justifyContent: 'center', + alignItems: 'center', + }} + > + {}}> + {selected && ( + + + Drag to rotate · Double-tap to flip + + + + )} + + + ); } diff --git a/apps/figgos/apps/mobile/app/(tabs)/index.tsx b/apps/figgos/apps/mobile/app/(tabs)/index.tsx index bdd9209ca..94851ad2f 100644 --- a/apps/figgos/apps/mobile/app/(tabs)/index.tsx +++ b/apps/figgos/apps/mobile/app/(tabs)/index.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useRef, useEffect } from 'react'; import { View, Text, @@ -9,105 +9,120 @@ import { Platform, ActivityIndicator, Image, + Alert, + Dimensions, } from 'react-native'; +import type { ScrollView as ScrollViewType } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; -import type { FigureResponse, FigureRarity } from '@figgos/shared'; +import Animated, { + useSharedValue, + useAnimatedStyle, + withRepeat, + withTiming, + withSequence, + Easing, + FadeIn, + FadeOut, +} from 'react-native-reanimated'; +import * as ImagePicker from 'expo-image-picker'; +import * as ImageManipulator from 'expo-image-manipulator'; +import type { FigureResponse } from '@figgos/shared'; import { api } from '../../services/api'; +import FlippableCard from '../../components/FlippableCard'; -// ── Rarity ── +const { width: SCREEN_WIDTH } = Dimensions.get('window'); +const CARD_WIDTH = SCREEN_WIDTH - 64; +const PLACEHOLDER_HEIGHT = CARD_WIDTH * 1.5; -const RARITY_SHADOW: Record = { - common: 'rgb(80, 90, 100)', - rare: 'rgb(60, 120, 180)', - epic: 'rgb(120, 80, 180)', - legendary: 'rgb(180, 130, 20)', -}; +// Random placeholder from our cole cards +const PLACEHOLDER_IMAGES = [ + require('../../assets/images/cole-common.png'), + require('../../assets/images/cole-rare.png'), + require('../../assets/images/cole-epic.png'), + require('../../assets/images/cole-kraft.png'), +]; -function RarityBadge({ rarity }: { rarity: FigureRarity }) { - const shadowColor = RARITY_SHADOW[rarity]; - const bgClass = `bg-rarity-${rarity}`; - const fgClass = `text-rarity-${rarity}-foreground`; - return ( - - - - - {rarity} - - - - ); -} - -// ── Stat Bar ── - -const STAT_COLORS = { - attack: 'rgb(255, 51, 102)', - defense: 'rgb(0, 210, 170)', - special: 'rgb(180, 130, 255)', -}; - -function StatBar({ label, value, color }: { label: string; value: number; color: string }) { - return ( - - - {label} - - - - - - {value} - - - ); -} +const LOADING_MESSAGES = [ + 'Rolling rarity...', + 'Crafting backstory...', + 'Forging stats...', + 'Designing figure...', + 'Painting details...', + 'Assembling packaging...', + 'Almost there...', +]; // ── Screen ── export default function CreateScreen() { const [name, setName] = useState(''); const [description, setDescription] = useState(''); + const storyRef = useRef(null); + const scrollRef = useRef(null); + const storyLayoutY = useRef(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [result, setResult] = useState(null); + const [imageUri, setImageUri] = useState(null); + const [loadingMsg, setLoadingMsg] = useState(0); + const [placeholderImg] = useState( + () => PLACEHOLDER_IMAGES[Math.floor(Math.random() * PLACEHOLDER_IMAGES.length)] + ); + + const pickImage = () => { + Alert.alert('Reference Photo', 'How would you like to add a photo?', [ + { + text: 'Take a Selfie', + onPress: async () => { + const { status } = await ImagePicker.requestCameraPermissionsAsync(); + if (status !== 'granted') { + setError('Camera permission is required for selfies'); + return; + } + const result = await ImagePicker.launchCameraAsync({ + allowsEditing: true, + quality: 0.7, + cameraType: ImagePicker.CameraType.front, + }); + if (!result.canceled && result.assets[0]) { + setImageUri(result.assets[0].uri); + } + }, + }, + { + text: 'Choose from Library', + onPress: async () => { + const result = await ImagePicker.launchImageLibraryAsync({ + mediaTypes: ['images'], + allowsEditing: true, + quality: 0.7, + }); + if (!result.canceled && result.assets[0]) { + setImageUri(result.assets[0].uri); + } + }, + }, + { text: 'Cancel', style: 'cancel' }, + ]); + }; + + // Cycle loading messages + useEffect(() => { + if (!loading) return; + const interval = setInterval(() => { + setLoadingMsg((prev) => (prev + 1) % LOADING_MESSAGES.length); + }, 2500); + return () => clearInterval(interval); + }, [loading]); + + const prepareImage = async (uri: string): Promise => { + const result = await ImageManipulator.manipulateAsync(uri, [{ resize: { width: 512 } }], { + compress: 0.8, + format: ImageManipulator.SaveFormat.JPEG, + base64: true, + }); + return result.base64!; + }; const handleGenerate = async () => { if (!name.trim() || !description.trim()) { @@ -116,12 +131,21 @@ export default function CreateScreen() { } setLoading(true); setError(null); + setLoadingMsg(0); try { - const { figure } = await api.figures.create(name.trim(), description.trim()); + let faceImage: string | undefined; + if (imageUri) { + faceImage = await prepareImage(imageUri); + } + const { figure } = await api.figures.create(name.trim(), description.trim(), 'en', faceImage); + setLoading(false); + if (figure.status === 'failed') { + setError(figure.errorMessage || 'Generation failed — try a different description'); + return; + } setResult(figure); } catch (e: any) { setError(e.message || 'Something went wrong'); - } finally { setLoading(false); } }; @@ -131,200 +155,133 @@ export default function CreateScreen() { setDescription(''); setResult(null); setError(null); + setImageUri(null); }; - const profile = result?.generatedProfile; + // ── Loading / Generating ── + if (loading) { + return ( + + {/* Banner + hint */} + + + + Generating + + + + {LOADING_MESSAGES[loadingMsg]} + + + + {/* Blurred placeholder card — shifted slightly upward */} + + + + + + + + ); + } // ── Result ── if (result) { return ( - - - {/* Badge */} - + + + Unboxing + + + + Drag to rotate · Double-tap to flip + + + + {/* Flippable Card — shifted slightly upward */} + + + + + {/* Create Another — overlaid at bottom, above tab bar */} + + + - Unboxing + + Create Another - - {/* Figure Card */} - - - - {/* Image */} - {result.imageUrl ? ( - - ) : ( - - - {result.status === 'failed' ? 'Generation failed' : 'No image'} - - - )} - - - {result.name} - - - {profile?.subtitle && ( - - {profile.subtitle} - - )} - - {profile?.backstory && ( - - {profile.backstory} - - )} - - {/* Stats */} - {profile?.stats && ( - - - - - - )} - - {/* Special Attack */} - {profile?.specialAttack && ( - - - {profile.specialAttack.name} - - - {profile.specialAttack.description} - - - )} - - - - - - {/* Error message */} - {result.status === 'failed' && result.errorMessage && ( - - {result.errorMessage} - - )} - - - - {/* Create Another */} - - - - - - - Create Another - - - - - - - + + ); } @@ -337,6 +294,7 @@ export default function CreateScreen() { className="flex-1" > { + storyRef.current?.focus(); + scrollRef.current?.scrollTo({ y: storyLayoutY.current - 20, animated: true }); + }} /> @@ -416,6 +380,9 @@ export default function CreateScreen() { letterSpacing: 3, textTransform: 'uppercase', }} + onLayout={(e) => { + storyLayoutY.current = e.nativeEvent.layout.y; + }} > Story @@ -425,6 +392,7 @@ export default function CreateScreen() { style={{ position: 'absolute', top: 5, left: 5, right: -5, bottom: -5 }} /> { + scrollRef.current?.scrollToEnd({ animated: true }); + }} /> + {/* Reference Photo */} + + + {imageUri ? ( + + + + Photo added + + setImageUri(null)} + className="active:opacity-70" + style={{ + width: 28, + height: 28, + borderRadius: 14, + backgroundColor: 'rgba(255, 80, 80, 0.2)', + alignItems: 'center', + justifyContent: 'center', + }} + > + + × + + + + ) : ( + + + 📷 + + Add a face photo + + + optional + + + › + + + + )} + + {/* Error */} {error && ( ); } + +// ── Pulsing shimmer overlay for loading card ── +function PulsingOverlay({ width, height }: { width: number; height: number }) { + const opacity = useSharedValue(0.3); + + useEffect(() => { + opacity.value = withRepeat( + withSequence( + withTiming(0.7, { duration: 1200, easing: Easing.inOut(Easing.ease) }), + withTiming(0.3, { duration: 1200, easing: Easing.inOut(Easing.ease) }) + ), + -1, + false + ); + }, []); + + const style = useAnimatedStyle(() => ({ + opacity: opacity.value, + })); + + return ( + + ); +} diff --git a/apps/figgos/apps/mobile/app/card/v2/[id].tsx b/apps/figgos/apps/mobile/app/card/v2/[id].tsx index 9000b4cd7..667fae1d1 100644 --- a/apps/figgos/apps/mobile/app/card/v2/[id].tsx +++ b/apps/figgos/apps/mobile/app/card/v2/[id].tsx @@ -1,30 +1,10 @@ import { useState, useEffect } from 'react'; -import { View, Text, Pressable, Image, Dimensions, ActivityIndicator } from 'react-native'; +import { View, Text, Pressable, ActivityIndicator } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useLocalSearchParams, useRouter } from 'expo-router'; -import { Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'; -import type { FigureResponse, FigureRarity } from '@figgos/shared'; +import type { FigureResponse } from '@figgos/shared'; import { api } from '../../../services/api'; - -const { width: SCREEN_WIDTH } = Dimensions.get('window'); -const CONTAINER_WIDTH = SCREEN_WIDTH - 48; -const CONTAINER_HEIGHT = CONTAINER_WIDTH * 1.5; - -const RARITY_COLORS: Record = { - common: 'rgb(136, 136, 170)', - rare: 'rgb(100, 180, 255)', - epic: 'rgb(180, 130, 255)', - legendary: 'rgb(255, 185, 30)', -}; - -const STAT_COLORS = { - attack: 'rgb(255, 51, 102)', - defense: 'rgb(0, 210, 170)', - special: 'rgb(180, 130, 255)', -}; - -const SPRING_CONFIG = { damping: 20, stiffness: 200, mass: 0.8 }; +import FlippableCard from '../../../components/FlippableCard'; export default function CardDetailV2Screen() { const { id } = useLocalSearchParams<{ id: string }>(); @@ -41,61 +21,6 @@ export default function CardDetailV2Screen() { .finally(() => setLoading(false)); }, [id]); - // Track the actual rendered image size - const [imageSize, setImageSize] = useState({ width: CONTAINER_WIDTH, height: CONTAINER_HEIGHT }); - - const computeContainSize = (srcW: number, srcH: number) => { - const ratio = Math.min(CONTAINER_WIDTH / srcW, CONTAINER_HEIGHT / srcH); - setImageSize({ - width: Math.round(srcW * ratio), - height: Math.round(srcH * ratio), - }); - }; - - // Rotation around vertical axis only - const rotateY = useSharedValue(0); - const savedRotateY = useSharedValue(0); - - const pan = Gesture.Pan() - .onUpdate((e) => { - rotateY.value = savedRotateY.value + e.translationX * 0.5; - }) - .onEnd(() => { - const normalised = ((rotateY.value % 360) + 360) % 360; - let target: number; - if (normalised < 90) { - target = 0; - } else if (normalised < 270) { - target = 180; - } else { - target = 360; - } - const diff = target - normalised; - const snapTo = rotateY.value + diff; - savedRotateY.value = snapTo % 360; - rotateY.value = withSpring(snapTo, SPRING_CONFIG); - }); - - const doubleTap = Gesture.Tap() - .numberOfTaps(2) - .onEnd(() => { - const target = savedRotateY.value + 180; - savedRotateY.value = target % 360; - rotateY.value = withSpring(target, { damping: 18, stiffness: 150, mass: 1 }); - }); - - const composed = Gesture.Race(doubleTap, pan); - - const frontStyle = useAnimatedStyle(() => ({ - transform: [{ perspective: 1200 }, { rotateY: `${rotateY.value}deg` }], - backfaceVisibility: 'hidden' as const, - })); - - const backStyle = useAnimatedStyle(() => ({ - transform: [{ perspective: 1200 }, { rotateY: `${rotateY.value + 180}deg` }], - backfaceVisibility: 'hidden' as const, - })); - if (loading) { return ( @@ -114,8 +39,6 @@ export default function CardDetailV2Screen() { ); } - const profile = figure.generatedProfile; - return ( {/* Back button */} @@ -134,249 +57,8 @@ export default function CardDetailV2Screen() { - - - {/* ── Front: the image ── */} - - {figure.imageUrl ? ( - { - const { width: srcW, height: srcH } = e.nativeEvent.source; - computeContainSize(srcW, srcH); - }} - /> - ) : ( - - - {figure.name} - - - No image - - - )} - - - {/* ── Back ── */} - - {/* Shadow layer */} - - {/* Card back */} - - {/* Header */} - - - - Backstory - - - - {figure.name} - - {profile?.subtitle && ( - - {profile.subtitle} - - )} - - - {/* Description */} - - {profile?.backstory || figure.userInput.description} - - - {/* Stats */} - {profile?.stats && ( - - - Stats - - - - - - )} - - {/* Special Attack */} - {profile?.specialAttack && ( - - - ⚡ {profile.specialAttack.name} - - - )} - - {/* Bottom: rarity + ID */} - - - - {figure.rarity} - - - - #{figure.id.split('-').pop()?.toUpperCase()} - - - - - - + ); } - -function StatBar({ label, value, color }: { label: string; value: number; color: string }) { - return ( - - - {label} - - - - - - {value} - - - ); -} diff --git a/apps/figgos/apps/mobile/components/FlippableCard.tsx b/apps/figgos/apps/mobile/components/FlippableCard.tsx new file mode 100644 index 000000000..bf7c7c22b --- /dev/null +++ b/apps/figgos/apps/mobile/components/FlippableCard.tsx @@ -0,0 +1,304 @@ +import { useState } from 'react'; +import { View, Text, Image, Dimensions } from 'react-native'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; +import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'; +import type { FigureResponse, FigureRarity } from '@figgos/shared'; + +const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window'); + +const RARITY_COLORS: Record = { + common: 'rgb(136, 136, 170)', + rare: 'rgb(100, 180, 255)', + epic: 'rgb(180, 130, 255)', + legendary: 'rgb(255, 185, 30)', +}; + +const STAT_COLORS = { + attack: 'rgb(255, 51, 102)', + defense: 'rgb(0, 210, 170)', + special: 'rgb(180, 130, 255)', +}; + +const SPRING_CONFIG = { damping: 20, stiffness: 200, mass: 0.8 }; + +interface FlippableCardProps { + figure: FigureResponse; + maxWidth?: number; + maxHeight?: number; +} + +export default function FlippableCard({ figure, maxWidth, maxHeight }: FlippableCardProps) { + const mW = maxWidth ?? SCREEN_WIDTH - 48; + const mH = maxHeight ?? SCREEN_HEIGHT * 0.7; + + // Card size starts at max bounds, then snaps to image aspect ratio on load + const [cardSize, setCardSize] = useState({ width: mW, height: mH }); + const [imageLoaded, setImageLoaded] = useState(false); + + const onImageLoad = (srcW: number, srcH: number) => { + // Fit the source image dimensions into max bounds + const scale = Math.min(mW / srcW, mH / srcH); + setCardSize({ + width: Math.round(srcW * scale), + height: Math.round(srcH * scale), + }); + setImageLoaded(true); + }; + + const { width: cardWidth, height: cardHeight } = cardSize; + + // ── Gestures ── + const rotateY = useSharedValue(0); + const savedRotateY = useSharedValue(0); + + const pan = Gesture.Pan() + .onUpdate((e) => { + rotateY.value = savedRotateY.value + e.translationX * 0.5; + }) + .onEnd(() => { + const normalised = ((rotateY.value % 360) + 360) % 360; + let target: number; + if (normalised < 90) target = 0; + else if (normalised < 270) target = 180; + else target = 360; + const snapTo = rotateY.value + (target - normalised); + savedRotateY.value = snapTo % 360; + rotateY.value = withSpring(snapTo, SPRING_CONFIG); + }); + + const doubleTap = Gesture.Tap() + .numberOfTaps(2) + .onEnd(() => { + const target = savedRotateY.value + 180; + savedRotateY.value = target % 360; + rotateY.value = withSpring(target, { damping: 18, stiffness: 150, mass: 1 }); + }); + + const composed = Gesture.Race(doubleTap, pan); + + const frontStyle = useAnimatedStyle(() => ({ + transform: [{ perspective: 1200 }, { rotateY: `${rotateY.value}deg` }], + backfaceVisibility: 'hidden' as const, + })); + + const backStyle = useAnimatedStyle(() => ({ + transform: [{ perspective: 1200 }, { rotateY: `${rotateY.value + 180}deg` }], + backfaceVisibility: 'hidden' as const, + })); + + const profile = figure.generatedProfile; + const rarityColor = RARITY_COLORS[figure.rarity]; + + return ( + + + {/* ── Front ── */} + + {figure.imageUrl ? ( + { + const { width: w, height: h } = e.nativeEvent.source; + onImageLoad(w, h); + }} + /> + ) : ( + + + {figure.name} + + + {figure.status === 'failed' ? 'Generation failed' : 'No image'} + + + )} + + + {/* ── Back ── */} + + + {/* Header: name + subtitle */} + + + {figure.name} + + {profile?.subtitle && ( + + {profile.subtitle} + + )} + + + {/* Backstory */} + + {profile?.backstory || figure.userInput.description} + + + {/* Stats */} + {profile?.stats && ( + + + + + + )} + + {/* Special Attack */} + {profile?.specialAttack && ( + + + ⚡ {profile.specialAttack.name} + + + {profile.specialAttack.description} + + + )} + + {/* Items */} + {profile?.items && profile.items.length > 0 && ( + + {profile.items.map((item) => ( + + + {item.name} + + + ))} + + )} + + {/* Bottom: rarity + ID */} + + + + {figure.rarity} + + + + #{figure.id.split('-').pop()?.toUpperCase()} + + + + + + + ); +} + +function StatBar({ label, value, color }: { label: string; value: number; color: string }) { + return ( + + + {label} + + + + + + {value} + + + ); +} diff --git a/apps/figgos/apps/mobile/package.json b/apps/figgos/apps/mobile/package.json index b09bb0658..ca8391758 100644 --- a/apps/figgos/apps/mobile/package.json +++ b/apps/figgos/apps/mobile/package.json @@ -18,10 +18,13 @@ "@expo/vector-icons": "^15.0.3", "@figgos/shared": "workspace:*", "@manacore/shared-auth": "workspace:*", + "@react-native-async-storage/async-storage": "2.2.0", "@react-navigation/native": "^7.1.8", "expo": "~54.0.33", "expo-constants": "~18.0.13", "expo-font": "~14.0.11", + "expo-image-manipulator": "^14.0.8", + "expo-image-picker": "^17.0.10", "expo-linking": "~8.0.11", "expo-router": "~6.0.23", "expo-secure-store": "~15.0.8", @@ -38,8 +41,7 @@ "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-web": "~0.21.0", - "react-native-worklets": "0.5.1", - "@react-native-async-storage/async-storage": "2.2.0" + "react-native-worklets": "0.5.1" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/apps/figgos/apps/mobile/services/api.ts b/apps/figgos/apps/mobile/services/api.ts index 8b93d345f..40e398182 100644 --- a/apps/figgos/apps/mobile/services/api.ts +++ b/apps/figgos/apps/mobile/services/api.ts @@ -26,10 +26,10 @@ export const api = { health: () => fetchApi('/health'), figures: { - create: (name: string, description: string, language: string = 'en') => + create: (name: string, description: string, language: string = 'en', faceImage?: string) => fetchApi<{ figure: FigureResponse }>('/api/v1/figures', { method: 'POST', - body: JSON.stringify({ name, description, language }), + body: JSON.stringify({ name, description, language, ...(faceImage && { faceImage }) }), }), list: () => fetchApi<{ figures: FigureResponse[] }>('/api/v1/figures'), diff --git a/apps/figgos/apps/web/src/lib/api.ts b/apps/figgos/apps/web/src/lib/api.ts index f5b699e02..e32a2dcf7 100644 --- a/apps/figgos/apps/web/src/lib/api.ts +++ b/apps/figgos/apps/web/src/lib/api.ts @@ -16,10 +16,10 @@ async function fetchApi(path: string, options?: RequestInit): Promise { export const api = { figures: { - create: (name: string, description: string, language = 'en') => + create: (name: string, description: string, language = 'en', faceImage?: string) => fetchApi<{ figure: FigureResponse }>('/api/v1/figures', { method: 'POST', - body: JSON.stringify({ name, description, language }), + body: JSON.stringify({ name, description, language, ...(faceImage && { faceImage }) }), }), list: () => fetchApi<{ figures: FigureResponse[] }>('/api/v1/figures'), get: (id: string) => fetchApi<{ figure: FigureResponse }>(`/api/v1/figures/${id}`), diff --git a/apps/figgos/packages/shared/src/index.ts b/apps/figgos/packages/shared/src/index.ts index 7b7eafb0d..b606d3736 100644 --- a/apps/figgos/packages/shared/src/index.ts +++ b/apps/figgos/packages/shared/src/index.ts @@ -55,7 +55,6 @@ export type FigureStatus = export interface FigureUserInput { description: string; - faceImageUrl?: string | null; language: FigureLanguage; } @@ -100,6 +99,7 @@ export interface FigureResponse { language: FigureLanguage; status: FigureStatus; isPublic: boolean; + errorMessage: string | null; isArchived: boolean; createdAt: string; updatedAt: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0da5d2b6..249a54469 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -626,19 +626,19 @@ importers: version: 18.3.27 '@typescript-eslint/eslint-plugin': specifier: ^7.7.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/parser': specifier: ^7.7.0 - version: 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + version: 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) dotenv: specifier: ^16.4.7 version: 16.6.1 eslint: specifier: ^9.39.1 - version: 9.39.1(jiti@1.21.7) + version: 9.39.1(jiti@2.6.1) eslint-config-universe: specifier: ^12.0.1 - version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2)(typescript@5.3.3) + version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2)(typescript@5.3.3) prettier: specifier: ^3.2.5 version: 3.6.2 @@ -1551,7 +1551,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9(esbuild@0.19.12) + version: 10.4.9(esbuild@0.27.0) '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) @@ -1566,7 +1566,7 @@ importers: version: 0.5.21 ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)) + version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -1606,6 +1606,12 @@ importers: expo-font: specifier: ~14.0.11 version: 14.0.11(expo@54.0.33)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-image-manipulator: + specifier: ^14.0.8 + version: 14.0.8(expo@54.0.33) + expo-image-picker: + specifier: ^17.0.10 + version: 17.0.10(expo@54.0.33) expo-linking: specifier: ~8.0.11 version: 8.0.11(expo@54.0.33)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -16773,11 +16779,21 @@ packages: peerDependencies: expo: '*' + expo-image-manipulator@14.0.8: + resolution: {integrity: sha512-sXsXjm7rIxLWZe0j2A41J/Ph53PpFJRdyzJ3EQ/qetxLUvS2m3K1sP5xy37px43qCf0l79N/i6XgFgenFV36/Q==} + peerDependencies: + expo: '*' + expo-image-picker@16.0.6: resolution: {integrity: sha512-HN4xZirFjsFDIsWFb12AZh19fRzuvZjj2ll17cGr19VNRP06S/VPQU3Tdccn5vwUzQhOBlLu704CnNm278boiQ==} peerDependencies: expo: '*' + expo-image-picker@17.0.10: + resolution: {integrity: sha512-a2xrowp2trmvXyUWgX3O6Q2rZaa2C59AqivKI7+bm+wLvMfTEbZgldLX4rEJJhM8xtmEDTNU+lzjtObwzBRGaw==} + peerDependencies: + expo: '*' + expo-image-picker@17.0.8: resolution: {integrity: sha512-489ByhVs2XPoAu9zodivAKLv7hG4S/FOe8hO/C2U6jVxmRjpAKakKNjMml0IwWjf1+c/RYBqm1XxKaZ+vq/fDQ==} peerDependencies: @@ -27296,11 +27312,6 @@ snapshots: eslint: 8.57.1 eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@1.21.7))': - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))': dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -27988,6 +27999,7 @@ snapshots: - graphql - supports-color - utf-8-validate + optional: true '@expo/code-signing-certificates@0.0.5': dependencies: @@ -28203,6 +28215,7 @@ snapshots: optionalDependencies: react: 19.1.0 react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + optional: true '@expo/env@0.4.2': dependencies: @@ -28422,7 +28435,7 @@ snapshots: postcss: 8.4.49 resolve-from: 5.0.0 optionalDependencies: - expo: 54.0.33(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo: 54.0.33(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - bufferutil - supports-color @@ -28884,7 +28897,7 @@ snapshots: '@expo/json-file': 10.0.8 '@react-native/normalize-colors': 0.81.5 debug: 4.4.3 - expo: 54.0.33(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo: 54.0.33(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) resolve-from: 5.0.0 semver: 7.7.3 xml2js: 0.6.0 @@ -29010,6 +29023,7 @@ snapshots: expo-font: 14.0.11(expo@54.0.33)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react: 19.1.0 react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + optional: true '@expo/vector-icons@15.0.3(expo-font@14.0.9(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)': dependencies: @@ -30320,32 +30334,6 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/cli@10.4.9(esbuild@0.19.12)': - dependencies: - '@angular-devkit/core': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) - '@angular-devkit/schematics-cli': 17.3.11(chokidar@3.6.0) - '@nestjs/schematics': 10.2.3(chokidar@3.6.0)(typescript@5.7.2) - chalk: 4.1.2 - chokidar: 3.6.0 - cli-table3: 0.6.5 - commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)) - glob: 10.4.5 - inquirer: 8.2.6 - node-emoji: 1.11.0 - ora: 5.4.1 - tree-kill: 1.2.2 - tsconfig-paths: 4.2.0 - tsconfig-paths-webpack-plugin: 4.2.0 - typescript: 5.7.2 - webpack: 5.97.1(esbuild@0.19.12) - webpack-node-externals: 3.0.0 - transitivePeerDependencies: - - esbuild - - uglify-js - - webpack-cli - '@nestjs/cli@10.4.9(esbuild@0.27.0)': dependencies: '@angular-devkit/core': 17.3.11(chokidar@3.6.0) @@ -35008,16 +34996,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/type-utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -35066,15 +35054,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/parser': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -35166,14 +35154,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: @@ -35205,14 +35193,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.18.0 debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: @@ -35338,12 +35326,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/type-utils@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) - '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 1.4.3(typescript@5.3.3) optionalDependencies: typescript: 5.3.3 @@ -35374,12 +35362,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/type-utils@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 1.4.3(typescript@5.3.3) optionalDependencies: typescript: 5.3.3 @@ -35561,15 +35549,15 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/utils@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) '@types/json-schema': 7.0.15 '@types/semver': 7.7.1 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) semver: 7.7.3 transitivePeerDependencies: - supports-color @@ -35600,13 +35588,13 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/utils@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) transitivePeerDependencies: - supports-color - typescript @@ -36957,7 +36945,7 @@ snapshots: resolve-from: 5.0.0 optionalDependencies: '@babel/runtime': 7.28.4 - expo: 54.0.33(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo: 54.0.33(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - '@babel/core' - supports-color @@ -39060,10 +39048,6 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -39092,17 +39076,17 @@ snapshots: - supports-color - typescript - eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2)(typescript@5.3.3): + eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2)(typescript@5.3.3): dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) - eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-node: 11.1.0(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2) - eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-react-hooks: 4.6.2(eslint@9.39.1(jiti@1.21.7)) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + eslint: 9.39.1(jiti@2.6.1) + eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-node: 11.1.0(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2) + eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-react-hooks: 4.6.2(eslint@9.39.1(jiti@2.6.1)) optionalDependencies: prettier: 3.6.2 transitivePeerDependencies: @@ -39165,12 +39149,12 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@1.21.7)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color @@ -39227,12 +39211,6 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-es@3.0.1(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-utils: 2.1.0 - regexpp: 3.2.0 - eslint-plugin-es@3.0.1(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -39286,7 +39264,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -39295,9 +39273,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@1.21.7)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -39309,7 +39287,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -39412,16 +39390,6 @@ snapshots: resolve: 1.22.11 semver: 6.3.1 - eslint-plugin-node@11.1.0(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-plugin-es: 3.0.1(eslint@9.39.1(jiti@1.21.7)) - eslint-utils: 2.1.0 - ignore: 5.3.2 - minimatch: 3.1.2 - resolve: 1.22.11 - semver: 6.3.1 - eslint-plugin-node@11.1.0(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -39452,16 +39420,6 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 8.10.2(eslint@8.57.1) - eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - prettier: 3.6.2 - prettier-linter-helpers: 1.0.0 - synckit: 0.11.11 - optionalDependencies: - '@types/eslint': 9.6.1 - eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -39486,10 +39444,6 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-plugin-react-hooks@4.6.2(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-plugin-react-hooks@4.6.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -39520,28 +39474,6 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@1.21.7)): - dependencies: - array-includes: 3.1.9 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.2.1 - eslint: 9.39.1(jiti@1.21.7) - estraverse: 5.3.0 - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.2 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.5 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@2.6.1)): dependencies: array-includes: 3.1.9 @@ -39668,47 +39600,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint@9.39.1(jiti@1.21.7): - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.39.1 - '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - optionalDependencies: - jiti: 1.21.7 - transitivePeerDependencies: - - supports-color - eslint@9.39.1(jiti@2.6.1): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) @@ -40012,6 +39903,7 @@ snapshots: react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - supports-color + optional: true expo-blur@14.0.3(expo@52.0.47)(react-native@0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(encoding@0.1.13)(react@18.3.1))(react@18.3.1): dependencies: @@ -40159,6 +40051,7 @@ snapshots: react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - supports-color + optional: true expo-dev-client@5.0.20(expo@52.0.47): dependencies: @@ -40343,6 +40236,7 @@ snapshots: dependencies: expo: 54.0.33(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + optional: true expo-font@13.0.4(expo@52.0.47)(react@18.3.1): dependencies: @@ -40412,6 +40306,7 @@ snapshots: fontfaceobserver: 2.3.0 react: 19.1.0 react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + optional: true expo-font@14.0.9(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): dependencies: @@ -40436,11 +40331,25 @@ snapshots: dependencies: expo: 54.0.13(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-image-loader@6.0.0(expo@54.0.33): + dependencies: + expo: 54.0.33(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + + expo-image-manipulator@14.0.8(expo@54.0.33): + dependencies: + expo: 54.0.33(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-image-loader: 6.0.0(expo@54.0.33) + expo-image-picker@16.0.6(expo@52.0.47): dependencies: expo: 52.0.47(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@expo/metro-runtime@6.1.2)(encoding@0.1.13)(react-native-webview@13.12.2(react-native@0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(encoding@0.1.13)(react@18.3.1))(react@18.3.1))(react-native@0.76.3(@babel/core@7.28.5)(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@types/react@18.3.27)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) expo-image-loader: 5.0.0(expo@52.0.47) + expo-image-picker@17.0.10(expo@54.0.33): + dependencies: + expo: 54.0.33(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-image-loader: 6.0.0(expo@54.0.33) + expo-image-picker@17.0.8(expo@54.0.13): dependencies: expo: 54.0.13(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -40485,7 +40394,7 @@ snapshots: expo-keep-awake@15.0.8(expo@54.0.33)(react@19.1.0): dependencies: - expo: 54.0.33(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo: 54.0.33(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react: 19.1.0 expo-linear-gradient@15.0.7(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0): @@ -40742,6 +40651,7 @@ snapshots: invariant: 2.2.4 react: 19.1.0 react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + optional: true expo-router@4.0.21(65am5mbkmp63mwl4oejo7rwcjy): dependencies: @@ -41764,6 +41674,7 @@ snapshots: - graphql - supports-color - utf-8-validate + optional: true exponential-backoff@3.1.3: {} @@ -42148,23 +42059,6 @@ snapshots: forever-agent@0.6.1: {} - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - '@babel/code-frame': 7.27.1 - chalk: 4.1.2 - chokidar: 3.6.0 - cosmiconfig: 8.3.6(typescript@5.7.2) - deepmerge: 4.3.1 - fs-extra: 10.1.0 - memfs: 3.5.3 - minimatch: 3.1.2 - node-abort-controller: 3.1.1 - schema-utils: 3.3.0 - semver: 7.7.3 - tapable: 2.3.0 - typescript: 5.7.2 - webpack: 5.97.1(esbuild@0.19.12) - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(esbuild@0.27.0)): dependencies: '@babel/code-frame': 7.27.1 @@ -50890,17 +50784,6 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - terser: 5.44.1 - webpack: 5.97.1(esbuild@0.19.12) - optionalDependencies: - esbuild: 0.19.12 - terser-webpack-plugin@5.3.14(esbuild@0.27.0)(webpack@5.100.2(esbuild@0.27.0)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -51176,16 +51059,6 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2 - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - chalk: 4.1.2 - enhanced-resolve: 5.18.3 - micromatch: 4.0.8 - semver: 7.7.3 - source-map: 0.7.6 - typescript: 5.9.3 - webpack: 5.97.1(esbuild@0.19.12) - ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -52518,36 +52391,6 @@ snapshots: - esbuild - uglify-js - webpack@5.97.1(esbuild@0.19.12): - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - browserslist: 4.28.0 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.3 - es-module-lexer: 1.7.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.1 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 3.3.0 - tapable: 2.3.0 - terser-webpack-plugin: 5.3.14(esbuild@0.19.12)(webpack@5.97.1(esbuild@0.19.12)) - watchpack: 2.4.4 - webpack-sources: 3.3.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - webpack@5.97.1(esbuild@0.27.0): dependencies: '@types/eslint-scope': 3.7.7