mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:41:09 +02:00
✨ feat(figgos): face image upload, loading/reveal screens + error handling
- Add optional face photo upload (expo-image-picker + expo-image-manipulator) - Wire face image through backend to Gemini as inline base64 data - Add loading screen with blurred placeholder card + pulsing animation - Add reveal/unboxing screen with aligned banner layout - Handle generation failures (check figure.status, show error on form) - Add Gemini safety settings (BLOCK_NONE) to reduce false rejections - Increase body limit to 5mb for base64 image payloads - Add errorMessage to FigureResponse shared type - Extract FlippableCard to reusable component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0eac48cdc4
commit
a117d5479b
18 changed files with 943 additions and 965 deletions
|
|
@ -18,4 +18,8 @@ export class CreateFigureDto {
|
|||
@IsString()
|
||||
@IsIn(['en', 'de'])
|
||||
language?: FigureLanguage = 'en';
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
faceImage?: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ export class FiguresController {
|
|||
user.userId,
|
||||
dto.name,
|
||||
dto.description,
|
||||
dto.language || 'en'
|
||||
dto.language || 'en',
|
||||
dto.faceImage
|
||||
);
|
||||
return { figure };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ export class FiguresService {
|
|||
userId: string,
|
||||
name: string,
|
||||
description: string,
|
||||
language: FigureLanguage = 'en'
|
||||
language: FigureLanguage = 'en',
|
||||
faceImage?: string
|
||||
): Promise<Figure> {
|
||||
const rarity = this.rollRarity();
|
||||
const cardStyle = getCardStyle(rarity);
|
||||
|
|
@ -58,6 +59,7 @@ export class FiguresService {
|
|||
language,
|
||||
rarity,
|
||||
cardStyle,
|
||||
faceImage,
|
||||
});
|
||||
|
||||
return completed;
|
||||
|
|
|
|||
|
|
@ -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<string>('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<Buffer> {
|
||||
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<string | { inlineData: { mimeType: string; data: string } }> = [];
|
||||
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export class GenerationService {
|
|||
language: FigureLanguage;
|
||||
rarity: FigureRarity;
|
||||
cardStyle: CardStyle;
|
||||
faceImage?: string;
|
||||
}
|
||||
): Promise<Figure> {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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(() => {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@ bootstrapApp(AppModule, {
|
|||
defaultPort: 3025,
|
||||
serviceName: 'Figgos',
|
||||
additionalCorsOrigins: ['http://localhost:5196'],
|
||||
bodyLimit: '5mb',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<FigureRarity, string> = {
|
||||
|
|
@ -27,16 +29,29 @@ const RARITY_COLORS: Record<FigureRarity, string> = {
|
|||
};
|
||||
|
||||
export default function CarouselScreen() {
|
||||
const router = useRouter();
|
||||
const scrollX = useRef(new Animated.Value(0)).current;
|
||||
const [figures, setFigures] = useState<FigureResponse[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selected, setSelected] = useState<FigureResponse | null>(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 (
|
||||
<SafeAreaView className="flex-1 bg-background" edges={['top']}>
|
||||
<View style={{ paddingHorizontal: 20, paddingTop: 24, paddingBottom: 16 }}>
|
||||
<View style={{ paddingHorizontal: 20, paddingTop: 16, paddingBottom: 8 }}>
|
||||
<Text
|
||||
className="text-foreground"
|
||||
style={{ fontSize: 28, fontWeight: '900', letterSpacing: -0.5 }}
|
||||
|
|
@ -69,8 +84,7 @@ export default function CarouselScreen() {
|
|||
Showcase
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={{ flex: 1, justifyContent: 'center' }}>
|
||||
<View style={{ flex: 1, justifyContent: 'center', marginTop: -80 }}>
|
||||
<Animated.FlatList
|
||||
data={figures}
|
||||
keyExtractor={(item) => item.id}
|
||||
|
|
@ -119,10 +133,7 @@ export default function CarouselScreen() {
|
|||
opacity,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => router.push(`/card/v2/${item.id}` as any)}
|
||||
className="active:opacity-90"
|
||||
>
|
||||
<Pressable onPress={() => setSelected(item)} className="active:opacity-90">
|
||||
<View
|
||||
style={{
|
||||
width: CARD_WIDTH,
|
||||
|
|
@ -131,31 +142,11 @@ export default function CarouselScreen() {
|
|||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{item.imageUrl ? (
|
||||
<Image
|
||||
source={{ uri: item.imageUrl }}
|
||||
source={{ uri: item.imageUrl! }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
className="bg-surface items-center justify-center"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderWidth: 2,
|
||||
borderColor: RARITY_COLORS[item.rarity],
|
||||
borderRadius: 14,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className="text-foreground"
|
||||
style={{ fontSize: 14, fontWeight: '800' }}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
|
|
@ -164,41 +155,41 @@ export default function CarouselScreen() {
|
|||
/>
|
||||
</View>
|
||||
|
||||
<View className="flex-row items-center justify-center" style={{ paddingBottom: 24, gap: 8 }}>
|
||||
{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 (
|
||||
<Animated.View
|
||||
key={figure.id}
|
||||
{/* Card Detail Overlay */}
|
||||
<Modal
|
||||
visible={selected !== null}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setSelected(null)}
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => setSelected(null)}
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: RARITY_COLORS[figure.rarity],
|
||||
transform: [{ scale: dotScale }],
|
||||
opacity: dotOpacity,
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(5, 5, 15, 0.92)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
>
|
||||
<Pressable onPress={() => {}}>
|
||||
{selected && (
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text
|
||||
className="text-muted-foreground"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginBottom: 12,
|
||||
}}
|
||||
>
|
||||
Drag to rotate · Double-tap to flip
|
||||
</Text>
|
||||
<FlippableCard figure={selected} maxWidth={SCREEN_WIDTH - 48} />
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</Modal>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Pressable
|
||||
onPress={() => 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 ? (
|
||||
<Image
|
||||
source={{ uri: figure.imageUrl }}
|
||||
source={{ uri: figure.imageUrl! }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
className="bg-surface items-center justify-center"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgb(50, 50, 80)',
|
||||
borderRadius: 12,
|
||||
}}
|
||||
>
|
||||
<Text className="text-muted-foreground" style={{ fontSize: 11 }}>
|
||||
{figure.name}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
|
|
@ -68,14 +57,26 @@ export default function CollectionScreen() {
|
|||
const [figures, setFigures] = useState<FigureResponse[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selected, setSelected] = useState<FigureResponse | null>(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 }) => <CardThumbnail figure={item} />}
|
||||
renderItem={({ item }) => <CardThumbnail figure={item} onPress={setSelected} />}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Card Detail Overlay */}
|
||||
<Modal
|
||||
visible={selected !== null}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setSelected(null)}
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => setSelected(null)}
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(5, 5, 15, 0.92)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Pressable onPress={() => {}}>
|
||||
{selected && (
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Text
|
||||
className="text-muted-foreground"
|
||||
style={{ fontSize: 12, fontWeight: '600', marginBottom: 12 }}
|
||||
>
|
||||
Drag to rotate · Double-tap to flip
|
||||
</Text>
|
||||
<FlippableCard figure={selected} maxWidth={SCREEN_WIDTH - 48} />
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</Modal>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FigureRarity, string> = {
|
||||
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 (
|
||||
<View style={{ position: 'relative', alignSelf: 'center' }}>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 3,
|
||||
left: 2,
|
||||
right: -2,
|
||||
bottom: -3,
|
||||
borderRadius: 999,
|
||||
backgroundColor: shadowColor,
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
className={`${bgClass} rounded-full`}
|
||||
style={{
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 8,
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgba(255,255,255,0.2)',
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`${fgClass}`}
|
||||
style={{ fontSize: 12, fontWeight: '900', letterSpacing: 2, textTransform: 'uppercase' }}
|
||||
>
|
||||
{rarity}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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 (
|
||||
<View className="flex-row items-center mb-1.5" style={{ gap: 6 }}>
|
||||
<Text
|
||||
className="text-muted-foreground"
|
||||
style={{ fontSize: 10, fontWeight: '900', width: 26, letterSpacing: 1 }}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<View
|
||||
className="flex-1 bg-input rounded-full"
|
||||
style={{ height: 8, borderWidth: 1, borderColor: 'rgb(50, 50, 80)' }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: `${value}%`,
|
||||
height: '100%',
|
||||
backgroundColor: color,
|
||||
borderRadius: 999,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
className="text-foreground"
|
||||
style={{ fontSize: 10, fontWeight: '800', width: 22, textAlign: 'right' }}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
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<TextInput>(null);
|
||||
const scrollRef = useRef<ScrollViewType>(null);
|
||||
const storyLayoutY = useRef(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [result, setResult] = useState<FigureResponse | null>(null);
|
||||
const [imageUri, setImageUri] = useState<string | null>(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<string> => {
|
||||
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,29 +155,80 @@ export default function CreateScreen() {
|
|||
setDescription('');
|
||||
setResult(null);
|
||||
setError(null);
|
||||
setImageUri(null);
|
||||
};
|
||||
|
||||
const profile = result?.generatedProfile;
|
||||
|
||||
// ── Result ──
|
||||
if (result) {
|
||||
// ── Loading / Generating ──
|
||||
if (loading) {
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-background" edges={['top']}>
|
||||
<ScrollView className="flex-1" contentContainerStyle={{ paddingBottom: 40 }}>
|
||||
<View className="px-6 pt-8 items-center">
|
||||
{/* Badge */}
|
||||
{/* Banner + hint */}
|
||||
<View className="items-center" style={{ paddingTop: 16, paddingBottom: 8 }}>
|
||||
<View
|
||||
className="bg-secondary rounded mb-5"
|
||||
className="bg-secondary rounded mb-2"
|
||||
style={{
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 6,
|
||||
transform: [{ rotate: '-2deg' }],
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className="text-secondary-foreground"
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontSize: 14,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 3,
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
Generating
|
||||
</Text>
|
||||
</View>
|
||||
<Text className="text-muted-foreground" style={{ fontSize: 13, fontWeight: '600' }}>
|
||||
{LOADING_MESSAGES[loadingMsg]}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Blurred placeholder card — shifted slightly upward */}
|
||||
<View className="flex-1 items-center justify-center" style={{ paddingBottom: 120 }}>
|
||||
<View style={{ width: CARD_WIDTH, height: PLACEHOLDER_HEIGHT }}>
|
||||
<Animated.Image
|
||||
entering={FadeIn.duration(400)}
|
||||
source={placeholderImg}
|
||||
style={{
|
||||
width: CARD_WIDTH,
|
||||
height: PLACEHOLDER_HEIGHT,
|
||||
borderRadius: 16,
|
||||
opacity: 0.25,
|
||||
}}
|
||||
resizeMode="cover"
|
||||
blurRadius={20}
|
||||
/>
|
||||
<PulsingOverlay width={CARD_WIDTH} height={PLACEHOLDER_HEIGHT} />
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Result ──
|
||||
if (result) {
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-background" edges={['top']}>
|
||||
{/* Banner + hint */}
|
||||
<View className="items-center" style={{ paddingTop: 16, paddingBottom: 8 }}>
|
||||
<View
|
||||
className="bg-secondary rounded mb-2"
|
||||
style={{
|
||||
paddingHorizontal: 18,
|
||||
paddingVertical: 6,
|
||||
transform: [{ rotate: '-2deg' }],
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className="text-secondary-foreground"
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 3,
|
||||
textTransform: 'uppercase',
|
||||
|
|
@ -162,169 +237,51 @@ export default function CreateScreen() {
|
|||
Unboxing
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Figure Card */}
|
||||
<View className="w-full" style={{ position: 'relative' }}>
|
||||
<View
|
||||
className="bg-primary-dark rounded-lg"
|
||||
style={{ position: 'absolute', top: 5, left: 5, right: -5, bottom: -5 }}
|
||||
/>
|
||||
<View
|
||||
className="bg-surface rounded-lg"
|
||||
style={{ borderWidth: 3, borderColor: 'rgb(255, 204, 0)', padding: 24 }}
|
||||
>
|
||||
{/* Image */}
|
||||
{result.imageUrl ? (
|
||||
<Image
|
||||
source={{ uri: result.imageUrl }}
|
||||
style={{
|
||||
width: 200,
|
||||
height: 200,
|
||||
alignSelf: 'center',
|
||||
marginBottom: 20,
|
||||
borderRadius: 12,
|
||||
}}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
className="bg-input rounded-lg self-center items-center justify-center mb-5"
|
||||
style={{
|
||||
width: 200,
|
||||
height: 200,
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgb(50, 50, 80)',
|
||||
}}
|
||||
>
|
||||
<Text className="text-muted-foreground" style={{ fontSize: 12 }}>
|
||||
{result.status === 'failed' ? 'Generation failed' : 'No image'}
|
||||
<Text className="text-muted-foreground" style={{ fontSize: 13, fontWeight: '600' }}>
|
||||
Drag to rotate · Double-tap to flip
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Text
|
||||
className="text-foreground text-center"
|
||||
style={{ fontSize: 22, fontWeight: '900', letterSpacing: -0.3 }}
|
||||
>
|
||||
{result.name}
|
||||
</Text>
|
||||
|
||||
{profile?.subtitle && (
|
||||
<Text
|
||||
className="text-muted-foreground text-center mt-1"
|
||||
style={{
|
||||
fontSize: 13,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 1,
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
{profile.subtitle}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{profile?.backstory && (
|
||||
<Text
|
||||
className="text-muted-foreground text-center mt-3"
|
||||
style={{ fontSize: 14, lineHeight: 20 }}
|
||||
>
|
||||
{profile.backstory}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
{profile?.stats && (
|
||||
<View className="mt-4 w-full">
|
||||
<StatBar label="ATK" value={profile.stats.attack} color={STAT_COLORS.attack} />
|
||||
<StatBar
|
||||
label="DEF"
|
||||
value={profile.stats.defense}
|
||||
color={STAT_COLORS.defense}
|
||||
/>
|
||||
<StatBar
|
||||
label="SPL"
|
||||
value={profile.stats.special}
|
||||
color={STAT_COLORS.special}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Special Attack */}
|
||||
{profile?.specialAttack && (
|
||||
<View className="mt-3 bg-input rounded-lg" style={{ padding: 12 }}>
|
||||
<Text
|
||||
className="text-primary"
|
||||
style={{
|
||||
fontSize: 11,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 1,
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
{profile.specialAttack.name}
|
||||
</Text>
|
||||
<Text
|
||||
className="text-muted-foreground mt-1"
|
||||
style={{ fontSize: 12, lineHeight: 16 }}
|
||||
>
|
||||
{profile.specialAttack.description}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View className="mt-4">
|
||||
<RarityBadge rarity={result.rarity} />
|
||||
{/* Flippable Card — shifted slightly upward */}
|
||||
<View className="flex-1 items-center justify-center" style={{ paddingBottom: 120 }}>
|
||||
<FlippableCard figure={result} maxWidth={CARD_WIDTH} />
|
||||
</View>
|
||||
|
||||
{/* Error message */}
|
||||
{result.status === 'failed' && result.errorMessage && (
|
||||
<Text className="text-destructive text-center mt-3" style={{ fontSize: 12 }}>
|
||||
{result.errorMessage}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Create Another */}
|
||||
<View className="w-full mt-8">
|
||||
<Pressable onPress={handleReset} className="active:opacity-90">
|
||||
<View style={{ position: 'relative' }}>
|
||||
{/* Create Another — overlaid at bottom, above tab bar */}
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 5,
|
||||
left: 3,
|
||||
right: -3,
|
||||
bottom: -5,
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'rgb(0, 150, 120)',
|
||||
bottom: 110,
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: 'center',
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Pressable onPress={handleReset} className="active:opacity-80">
|
||||
<View
|
||||
className="bg-accent rounded-lg items-center justify-center"
|
||||
className="bg-accent rounded-full flex-row items-center"
|
||||
style={{
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgba(255,255,255,0.15)',
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontSize: 13,
|
||||
fontWeight: '900',
|
||||
color: 'rgb(15, 15, 30)',
|
||||
letterSpacing: 1,
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
Create Another
|
||||
+ Create Another
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
|
@ -337,6 +294,7 @@ export default function CreateScreen() {
|
|||
className="flex-1"
|
||||
>
|
||||
<ScrollView
|
||||
ref={scrollRef}
|
||||
className="flex-1"
|
||||
contentContainerStyle={{ paddingBottom: 40 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
|
|
@ -404,6 +362,12 @@ export default function CreateScreen() {
|
|||
value={name}
|
||||
onChangeText={setName}
|
||||
maxLength={200}
|
||||
returnKeyType="next"
|
||||
blurOnSubmit={false}
|
||||
onSubmitEditing={() => {
|
||||
storyRef.current?.focus();
|
||||
scrollRef.current?.scrollTo({ y: storyLayoutY.current - 20, animated: true });
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
|
@ -416,6 +380,9 @@ export default function CreateScreen() {
|
|||
letterSpacing: 3,
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
onLayout={(e) => {
|
||||
storyLayoutY.current = e.nativeEvent.layout.y;
|
||||
}}
|
||||
>
|
||||
Story
|
||||
</Text>
|
||||
|
|
@ -425,6 +392,7 @@ export default function CreateScreen() {
|
|||
style={{ position: 'absolute', top: 5, left: 5, right: -5, bottom: -5 }}
|
||||
/>
|
||||
<TextInput
|
||||
ref={storyRef}
|
||||
className="bg-input text-foreground rounded-lg"
|
||||
style={{
|
||||
borderWidth: 3,
|
||||
|
|
@ -442,9 +410,94 @@ export default function CreateScreen() {
|
|||
multiline
|
||||
numberOfLines={4}
|
||||
maxLength={2000}
|
||||
onFocus={() => {
|
||||
scrollRef.current?.scrollToEnd({ animated: true });
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Reference Photo */}
|
||||
<View style={{ position: 'relative', marginBottom: 24 }}>
|
||||
<View
|
||||
className="bg-primary-dark rounded-lg"
|
||||
style={{ position: 'absolute', top: 3, left: 3, right: -3, bottom: -3 }}
|
||||
/>
|
||||
{imageUri ? (
|
||||
<View
|
||||
className="bg-input rounded-lg flex-row items-center"
|
||||
style={{
|
||||
borderWidth: 3,
|
||||
borderColor: 'rgb(255, 204, 0)',
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: imageUri }}
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
borderWidth: 2,
|
||||
borderColor: 'rgb(255, 204, 0)',
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
className="text-foreground"
|
||||
style={{ fontSize: 14, fontWeight: '600', flex: 1 }}
|
||||
>
|
||||
Photo added
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={() => setImageUri(null)}
|
||||
className="active:opacity-70"
|
||||
style={{
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
backgroundColor: 'rgba(255, 80, 80, 0.2)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 14, fontWeight: '800', color: 'rgb(255, 80, 80)' }}>
|
||||
×
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
) : (
|
||||
<Pressable onPress={pickImage} className="active:opacity-80">
|
||||
<View
|
||||
className="bg-input rounded-lg flex-row items-center"
|
||||
style={{
|
||||
borderWidth: 3,
|
||||
borderColor: 'rgb(255, 204, 0)',
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 12,
|
||||
}}
|
||||
>
|
||||
<Text style={{ fontSize: 20 }}>📷</Text>
|
||||
<Text
|
||||
className="text-muted-foreground"
|
||||
style={{ fontSize: 14, fontWeight: '600', flex: 1, marginLeft: 10 }}
|
||||
>
|
||||
Add a face photo
|
||||
</Text>
|
||||
<Text
|
||||
className="text-muted-foreground"
|
||||
style={{ fontSize: 10, fontWeight: '600', marginRight: 4 }}
|
||||
>
|
||||
optional
|
||||
</Text>
|
||||
<Text className="text-muted-foreground" style={{ fontSize: 16 }}>
|
||||
›
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<View
|
||||
|
|
@ -512,3 +565,40 @@ export default function CreateScreen() {
|
|||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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 (
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width,
|
||||
height,
|
||||
borderRadius: 16,
|
||||
backgroundColor: 'rgb(255, 204, 0)',
|
||||
},
|
||||
style,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<FigureRarity, string> = {
|
||||
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 (
|
||||
<SafeAreaView className="flex-1 bg-background items-center justify-center">
|
||||
|
|
@ -114,8 +39,6 @@ export default function CardDetailV2Screen() {
|
|||
);
|
||||
}
|
||||
|
||||
const profile = figure.generatedProfile;
|
||||
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-background" edges={['top']}>
|
||||
{/* Back button */}
|
||||
|
|
@ -134,249 +57,8 @@ export default function CardDetailV2Screen() {
|
|||
</View>
|
||||
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<GestureDetector gesture={composed}>
|
||||
<View style={{ width: imageSize.width, height: imageSize.height }}>
|
||||
{/* ── Front: the image ── */}
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
position: 'absolute',
|
||||
width: imageSize.width,
|
||||
height: imageSize.height,
|
||||
},
|
||||
frontStyle,
|
||||
]}
|
||||
>
|
||||
{figure.imageUrl ? (
|
||||
<Image
|
||||
source={{ uri: figure.imageUrl }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
resizeMode="contain"
|
||||
onLoad={(e) => {
|
||||
const { width: srcW, height: srcH } = e.nativeEvent.source;
|
||||
computeContainSize(srcW, srcH);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
className="bg-surface items-center justify-center rounded-2xl"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderWidth: 3,
|
||||
borderColor: RARITY_COLORS[figure.rarity],
|
||||
}}
|
||||
>
|
||||
<Text className="text-foreground" style={{ fontSize: 18, fontWeight: '800' }}>
|
||||
{figure.name}
|
||||
</Text>
|
||||
<Text className="text-muted-foreground mt-2" style={{ fontSize: 12 }}>
|
||||
No image
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
{/* ── Back ── */}
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
position: 'absolute',
|
||||
width: imageSize.width,
|
||||
height: imageSize.height,
|
||||
},
|
||||
backStyle,
|
||||
]}
|
||||
>
|
||||
{/* Shadow layer */}
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 5,
|
||||
left: 5,
|
||||
right: -5,
|
||||
bottom: -5,
|
||||
borderRadius: 16,
|
||||
backgroundColor: RARITY_COLORS[figure.rarity],
|
||||
opacity: 0.3,
|
||||
}}
|
||||
/>
|
||||
{/* Card back */}
|
||||
<View
|
||||
className="bg-surface rounded-2xl"
|
||||
style={{
|
||||
flex: 1,
|
||||
borderWidth: 3,
|
||||
borderColor: RARITY_COLORS[figure.rarity],
|
||||
padding: 20,
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<View>
|
||||
<View
|
||||
className="bg-secondary rounded self-start mb-2"
|
||||
style={{
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 2,
|
||||
transform: [{ rotate: '-2deg' }],
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className="text-secondary-foreground"
|
||||
style={{
|
||||
fontSize: 9,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 2,
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
Backstory
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
className="text-foreground"
|
||||
style={{ fontSize: 20, fontWeight: '900', letterSpacing: -0.5 }}
|
||||
>
|
||||
{figure.name}
|
||||
</Text>
|
||||
{profile?.subtitle && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 2,
|
||||
textTransform: 'uppercase',
|
||||
marginTop: 2,
|
||||
color: RARITY_COLORS[figure.rarity],
|
||||
}}
|
||||
>
|
||||
{profile.subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Description */}
|
||||
<Text
|
||||
className="text-muted-foreground"
|
||||
style={{ fontSize: 13, lineHeight: 20 }}
|
||||
numberOfLines={5}
|
||||
>
|
||||
{profile?.backstory || figure.userInput.description}
|
||||
</Text>
|
||||
|
||||
{/* Stats */}
|
||||
{profile?.stats && (
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 10,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 3,
|
||||
textTransform: 'uppercase',
|
||||
color: RARITY_COLORS[figure.rarity],
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
Stats
|
||||
</Text>
|
||||
<StatBar label="ATK" value={profile.stats.attack} color={STAT_COLORS.attack} />
|
||||
<StatBar
|
||||
label="DEF"
|
||||
value={profile.stats.defense}
|
||||
color={STAT_COLORS.defense}
|
||||
/>
|
||||
<StatBar
|
||||
label="SPL"
|
||||
value={profile.stats.special}
|
||||
color={STAT_COLORS.special}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Special Attack */}
|
||||
{profile?.specialAttack && (
|
||||
<View className="bg-input rounded" style={{ padding: 8 }}>
|
||||
<Text
|
||||
className="text-primary"
|
||||
style={{
|
||||
fontSize: 9,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 1,
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
⚡ {profile.specialAttack.name}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Bottom: rarity + ID */}
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View
|
||||
className="rounded-full"
|
||||
style={{
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 3,
|
||||
backgroundColor: RARITY_COLORS[figure.rarity],
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 9,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 1.5,
|
||||
textTransform: 'uppercase',
|
||||
color: 'rgb(15, 15, 30)',
|
||||
}}
|
||||
>
|
||||
{figure.rarity}
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
className="text-muted-foreground"
|
||||
style={{ fontSize: 9, fontWeight: '600', letterSpacing: 1 }}
|
||||
>
|
||||
#{figure.id.split('-').pop()?.toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</GestureDetector>
|
||||
<FlippableCard figure={figure} />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
function StatBar({ label, value, color }: { label: string; value: number; color: string }) {
|
||||
return (
|
||||
<View className="flex-row items-center mb-1.5" style={{ gap: 6 }}>
|
||||
<Text
|
||||
className="text-muted-foreground"
|
||||
style={{ fontSize: 10, fontWeight: '900', width: 26, letterSpacing: 1 }}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<View
|
||||
className="flex-1 bg-input rounded-full"
|
||||
style={{ height: 8, borderWidth: 1, borderColor: 'rgb(50, 50, 80)' }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: `${value}%`,
|
||||
height: '100%',
|
||||
backgroundColor: color,
|
||||
borderRadius: 999,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
className="text-foreground"
|
||||
style={{ fontSize: 10, fontWeight: '800', width: 22, textAlign: 'right' }}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
304
apps/figgos/apps/mobile/components/FlippableCard.tsx
Normal file
304
apps/figgos/apps/mobile/components/FlippableCard.tsx
Normal file
|
|
@ -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<FigureRarity, string> = {
|
||||
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 (
|
||||
<GestureDetector gesture={composed}>
|
||||
<View style={{ width: cardWidth, height: cardHeight }}>
|
||||
{/* ── Front ── */}
|
||||
<Animated.View
|
||||
style={[{ position: 'absolute', width: cardWidth, height: cardHeight }, frontStyle]}
|
||||
>
|
||||
{figure.imageUrl ? (
|
||||
<Image
|
||||
source={{ uri: figure.imageUrl }}
|
||||
style={{ width: cardWidth, height: cardHeight }}
|
||||
resizeMode="contain"
|
||||
onLoad={(e) => {
|
||||
const { width: w, height: h } = e.nativeEvent.source;
|
||||
onImageLoad(w, h);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
className="bg-surface items-center justify-center rounded-2xl"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderWidth: 3,
|
||||
borderColor: rarityColor,
|
||||
}}
|
||||
>
|
||||
<Text className="text-foreground" style={{ fontSize: 18, fontWeight: '800' }}>
|
||||
{figure.name}
|
||||
</Text>
|
||||
<Text className="text-muted-foreground mt-2" style={{ fontSize: 12 }}>
|
||||
{figure.status === 'failed' ? 'Generation failed' : 'No image'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Animated.View>
|
||||
|
||||
{/* ── Back ── */}
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
position: 'absolute',
|
||||
width: cardWidth,
|
||||
height: cardHeight,
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
backStyle,
|
||||
]}
|
||||
>
|
||||
<View
|
||||
className="bg-surface rounded-2xl"
|
||||
style={{
|
||||
flex: 1,
|
||||
borderWidth: 3,
|
||||
borderColor: rarityColor,
|
||||
padding: 16,
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{/* Header: name + subtitle */}
|
||||
<View>
|
||||
<Text
|
||||
className="text-foreground"
|
||||
style={{ fontSize: 18, fontWeight: '900', letterSpacing: -0.5 }}
|
||||
>
|
||||
{figure.name}
|
||||
</Text>
|
||||
{profile?.subtitle && (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 9,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 2,
|
||||
textTransform: 'uppercase',
|
||||
marginTop: 1,
|
||||
color: rarityColor,
|
||||
}}
|
||||
>
|
||||
{profile.subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Backstory */}
|
||||
<Text
|
||||
className="text-muted-foreground"
|
||||
style={{ fontSize: 11, lineHeight: 16 }}
|
||||
numberOfLines={4}
|
||||
>
|
||||
{profile?.backstory || figure.userInput.description}
|
||||
</Text>
|
||||
|
||||
{/* Stats */}
|
||||
{profile?.stats && (
|
||||
<View>
|
||||
<StatBar label="ATK" value={profile.stats.attack} color={STAT_COLORS.attack} />
|
||||
<StatBar label="DEF" value={profile.stats.defense} color={STAT_COLORS.defense} />
|
||||
<StatBar label="SPL" value={profile.stats.special} color={STAT_COLORS.special} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Special Attack */}
|
||||
{profile?.specialAttack && (
|
||||
<View className="bg-input rounded" style={{ padding: 8 }}>
|
||||
<Text
|
||||
className="text-primary"
|
||||
style={{
|
||||
fontSize: 9,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 1,
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
⚡ {profile.specialAttack.name}
|
||||
</Text>
|
||||
<Text
|
||||
className="text-muted-foreground"
|
||||
style={{ fontSize: 9, lineHeight: 13, marginTop: 2 }}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{profile.specialAttack.description}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Items */}
|
||||
{profile?.items && profile.items.length > 0 && (
|
||||
<View className="flex-row flex-wrap" style={{ gap: 4 }}>
|
||||
{profile.items.map((item) => (
|
||||
<View
|
||||
key={item.name}
|
||||
className="bg-input rounded-full"
|
||||
style={{ paddingHorizontal: 8, paddingVertical: 3 }}
|
||||
>
|
||||
<Text
|
||||
className="text-foreground"
|
||||
style={{ fontSize: 8, fontWeight: '800', letterSpacing: 0.5 }}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Bottom: rarity + ID */}
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View
|
||||
className="rounded-full"
|
||||
style={{
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 3,
|
||||
backgroundColor: rarityColor,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 9,
|
||||
fontWeight: '900',
|
||||
letterSpacing: 1.5,
|
||||
textTransform: 'uppercase',
|
||||
color: 'rgb(15, 15, 30)',
|
||||
}}
|
||||
>
|
||||
{figure.rarity}
|
||||
</Text>
|
||||
</View>
|
||||
<Text
|
||||
className="text-muted-foreground"
|
||||
style={{ fontSize: 9, fontWeight: '600', letterSpacing: 1 }}
|
||||
>
|
||||
#{figure.id.split('-').pop()?.toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</GestureDetector>
|
||||
);
|
||||
}
|
||||
|
||||
function StatBar({ label, value, color }: { label: string; value: number; color: string }) {
|
||||
return (
|
||||
<View className="flex-row items-center mb-1.5" style={{ gap: 6 }}>
|
||||
<Text
|
||||
className="text-muted-foreground"
|
||||
style={{ fontSize: 10, fontWeight: '900', width: 26, letterSpacing: 1 }}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
<View
|
||||
className="flex-1 bg-input rounded-full"
|
||||
style={{ height: 8, borderWidth: 1, borderColor: 'rgb(50, 50, 80)' }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: `${value}%`,
|
||||
height: '100%',
|
||||
backgroundColor: color,
|
||||
borderRadius: 999,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Text
|
||||
className="text-foreground"
|
||||
style={{ fontSize: 10, fontWeight: '800', width: 22, textAlign: 'right' }}
|
||||
>
|
||||
{value}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ async function fetchApi<T>(path: string, options?: RequestInit): Promise<T> {
|
|||
|
||||
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}`),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
341
pnpm-lock.yaml
generated
341
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue