mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 17:19:40 +02:00
Migrate figgos from single Expo app to multi-app monorepo structure: - Move mobile app to apps/mobile/ - Add apps/web/ (SvelteKit) and apps/backend/ (NestJS) scaffolds - Add packages/shared/ for shared types and constants - Update root package.json with new dev commands - Temporarily skip type-check (run pnpm install first) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
467 lines
12 KiB
Markdown
467 lines
12 KiB
Markdown
# Figgos Create Page Integration
|
|
|
|
This document outlines the integration between the Create page and the Supabase figgos-generator edge function.
|
|
|
|
## Database Structure
|
|
|
|
We've created a new `figures` table with the following structure:
|
|
|
|
```sql
|
|
CREATE TABLE public.figures (
|
|
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
name VARCHAR NOT NULL,
|
|
subject VARCHAR NOT NULL,
|
|
image_url VARCHAR NOT NULL,
|
|
theme VARCHAR NOT NULL,
|
|
rarity VARCHAR NOT NULL,
|
|
character_description TEXT,
|
|
style_description TEXT,
|
|
accessory1_description TEXT NOT NULL,
|
|
accessory2_description TEXT NOT NULL,
|
|
accessory3_description TEXT NOT NULL,
|
|
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
likes INTEGER DEFAULT 0,
|
|
is_public BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ
|
|
);
|
|
```
|
|
|
|
This table includes:
|
|
|
|
- Basic figure information (name, image URL)
|
|
- Direct storage of theme and rarity values as strings
|
|
- Character and accessory descriptions
|
|
- User ownership, likes, and public/private setting
|
|
- Timestamps for creation and updates
|
|
|
|
## Integration Steps
|
|
|
|
### 1. Create a Supabase Client Utility
|
|
|
|
First, we need to enhance our existing Supabase client to handle the figure generation:
|
|
|
|
```typescript
|
|
// utils/supabaseClient.ts
|
|
import { createClient } from '@supabase/supabase-js';
|
|
import { ExtendedFigureData } from '../components/SidebarCreateFigureForm';
|
|
import * as FileSystem from 'expo-file-system';
|
|
|
|
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL!;
|
|
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!;
|
|
|
|
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
|
|
|
// Function to fetch themes from the database
|
|
export async function fetchThemes() {
|
|
const { data, error } = await supabase.from('themes').select('name, details');
|
|
|
|
if (error) {
|
|
console.error('Error fetching themes:', error);
|
|
return [];
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
// Function to fetch rarities from the database
|
|
export async function fetchRarities() {
|
|
const { data, error } = await supabase.from('rarity').select('name, details');
|
|
|
|
if (error) {
|
|
console.error('Error fetching rarities:', error);
|
|
return [];
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
// Function to generate a figure using the edge function
|
|
export async function generateFigure(
|
|
formData: ExtendedFigureData,
|
|
themeName: string,
|
|
rarityName: string
|
|
) {
|
|
try {
|
|
// Convert image to base64 if it exists
|
|
let faceImageBase64 = null;
|
|
if (formData.characterImage) {
|
|
// For web
|
|
if (formData.characterImage.startsWith('data:')) {
|
|
faceImageBase64 = formData.characterImage.split(',')[1];
|
|
}
|
|
// For native
|
|
else {
|
|
const base64 = await FileSystem.readAsStringAsync(formData.characterImage, {
|
|
encoding: FileSystem.EncodingType.Base64,
|
|
});
|
|
faceImageBase64 = base64;
|
|
}
|
|
}
|
|
|
|
// Prepare the request payload
|
|
const payload = {
|
|
subject: formData.name,
|
|
theme: formData.style?.description || 'Dark Professional',
|
|
rarity: 'common', // Default rarity, can be customized
|
|
clothing_description: formData.characterDescription,
|
|
accessory1_description: formData.artifacts[0].description || 'Standard accessory',
|
|
accessory2_description: formData.artifacts[1].description || 'Standard accessory',
|
|
accessory3_description: formData.artifacts[2].description || 'Standard accessory',
|
|
face_image: faceImageBase64,
|
|
};
|
|
|
|
// Call the edge function
|
|
const { data, error } = await supabase.functions.invoke('figgos-generator', {
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (error) {
|
|
console.error('Error generating figure:', error);
|
|
throw new Error(error.message);
|
|
}
|
|
|
|
// Save the figure to the database
|
|
const { data: figureData, error: figureError } = await supabase
|
|
.from('figures')
|
|
.insert({
|
|
name: formData.name,
|
|
subject: formData.name,
|
|
image_url: data.image_url,
|
|
theme: themeName,
|
|
rarity: rarityName,
|
|
character_description: formData.characterDescription,
|
|
style_description: formData.style?.description,
|
|
accessory1_description: formData.artifacts[0].description,
|
|
accessory2_description: formData.artifacts[1].description,
|
|
accessory3_description: formData.artifacts[2].description,
|
|
is_public: isPublic,
|
|
user_id: (await supabase.auth.getUser()).data.user?.id,
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (figureError) {
|
|
console.error('Error saving figure:', figureError);
|
|
throw new Error(figureError.message);
|
|
}
|
|
|
|
return figureData;
|
|
} catch (error) {
|
|
console.error('Error in generateFigure:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 2. Update the SidebarCreateFigureForm Component
|
|
|
|
Modify the form component to use the new figure generation function:
|
|
|
|
```typescript
|
|
// components/SidebarCreateFigureForm.tsx
|
|
|
|
// Add imports
|
|
import { useState, useEffect } from 'react';
|
|
import { fetchThemes, fetchRarities, generateFigure } from '~/utils/supabaseClient';
|
|
import { useAuth } from '~/utils/AuthContext';
|
|
import { router } from 'expo-router';
|
|
|
|
// Add state for themes and rarities
|
|
const [themes, setThemes] = useState([]);
|
|
const [rarities, setRarities] = useState([]);
|
|
const [selectedTheme, setSelectedTheme] = useState('');
|
|
const [selectedRarity, setSelectedRarity] = useState('');
|
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
const { user } = useAuth();
|
|
|
|
// Add useEffect to fetch themes and rarities
|
|
useEffect(() => {
|
|
async function loadData() {
|
|
const themesData = await fetchThemes();
|
|
const raritiesData = await fetchRarities();
|
|
|
|
setThemes(themesData);
|
|
setRarities(raritiesData);
|
|
|
|
// Set defaults
|
|
if (themesData.length > 0) setSelectedTheme(themesData[0].name);
|
|
if (raritiesData.length > 0) setSelectedRarity(raritiesData[0].name);
|
|
}
|
|
|
|
loadData();
|
|
}, []);
|
|
|
|
// Update the handleSubmit function
|
|
const handleSubmit = async () => {
|
|
// Validate form
|
|
if (!formData.name.trim()) {
|
|
Alert.alert('Error', 'Please enter a name.');
|
|
return;
|
|
}
|
|
|
|
if (!formData.characterDescription.trim()) {
|
|
Alert.alert('Error', 'Please describe your character.');
|
|
return;
|
|
}
|
|
|
|
// Check if all three artifacts have descriptions
|
|
const artifactDescriptions = formData.artifacts.map((a) => a.description.trim());
|
|
if (artifactDescriptions.some((desc) => desc === '')) {
|
|
Alert.alert('Error', 'Please describe all three accessories.');
|
|
return;
|
|
}
|
|
|
|
if (!formData.style?.description.trim()) {
|
|
Alert.alert('Error', 'Please describe the style.');
|
|
return;
|
|
}
|
|
|
|
if (!user) {
|
|
Alert.alert('Error', 'You must be logged in to create a figure.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsGenerating(true);
|
|
|
|
// Generate the figure
|
|
const figure = await generateFigure(formData, selectedTheme, selectedRarity);
|
|
|
|
// Show success message
|
|
Alert.alert('Success!', 'Your action figure has been created!', [
|
|
{
|
|
text: 'View My Shelf',
|
|
onPress: () => router.push('/myshelf'),
|
|
},
|
|
{
|
|
text: 'OK',
|
|
style: 'cancel',
|
|
},
|
|
]);
|
|
|
|
// Reset form
|
|
setFormData({
|
|
name: '',
|
|
characterDescription: '',
|
|
characterImage: null,
|
|
artifacts: [
|
|
{ description: '', image: null },
|
|
{ description: '', image: null },
|
|
{ description: '', image: null },
|
|
],
|
|
style: {
|
|
description: '',
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error('Error creating figure:', error);
|
|
Alert.alert('Error', 'There was a problem creating your figure. Please try again.');
|
|
} finally {
|
|
setIsGenerating(false);
|
|
}
|
|
};
|
|
```
|
|
|
|
### 3. Add Theme and Rarity Selection to the Form
|
|
|
|
Add dropdowns to the form to allow users to select themes and rarities:
|
|
|
|
```jsx
|
|
{
|
|
/* Theme Selection */
|
|
}
|
|
<View style={styles.sectionContainer}>
|
|
<View style={styles.sectionHeader}>
|
|
<View style={styles.iconContainer}>
|
|
<FontAwesome name="image" size={20} color={theme.colors.text} />
|
|
</View>
|
|
<Text style={[styles.sectionTitle, { color: theme.colors.text }]}>Theme</Text>
|
|
</View>
|
|
<View
|
|
style={[
|
|
styles.pickerContainer,
|
|
{ borderColor: theme.colors.border, backgroundColor: theme.colors.input },
|
|
]}
|
|
>
|
|
<Picker
|
|
selectedValue={selectedTheme}
|
|
onValueChange={(itemValue) => setSelectedTheme(itemValue)}
|
|
style={{ color: theme.colors.text }}
|
|
>
|
|
{themes.map((themeItem) => (
|
|
<Picker.Item key={themeItem.name} label={themeItem.name} value={themeItem.name} />
|
|
))}
|
|
</Picker>
|
|
</View>
|
|
</View>;
|
|
|
|
{
|
|
/* Rarity Selection */
|
|
}
|
|
<View style={styles.sectionContainer}>
|
|
<View style={styles.sectionHeader}>
|
|
<View style={styles.iconContainer}>
|
|
<FontAwesome name="star" size={20} color={theme.colors.text} />
|
|
</View>
|
|
<Text style={[styles.sectionTitle, { color: theme.colors.text }]}>Rarity</Text>
|
|
</View>
|
|
<View
|
|
style={[
|
|
styles.pickerContainer,
|
|
{ borderColor: theme.colors.border, backgroundColor: theme.colors.input },
|
|
]}
|
|
>
|
|
<Picker
|
|
selectedValue={selectedRarity}
|
|
onValueChange={(itemValue) => setSelectedRarity(itemValue)}
|
|
style={{ color: theme.colors.text }}
|
|
>
|
|
{rarities.map((rarityItem) => (
|
|
<Picker.Item key={rarityItem.name} label={rarityItem.name} value={rarityItem.name} />
|
|
))}
|
|
</Picker>
|
|
</View>
|
|
</View>;
|
|
|
|
{
|
|
/* Visibility Selection */
|
|
}
|
|
<View style={styles.sectionContainer}>
|
|
<View style={styles.sectionHeader}>
|
|
<View style={styles.iconContainer}>
|
|
<FontAwesome name="globe" size={20} color={theme.colors.text} />
|
|
</View>
|
|
<Text style={[styles.sectionTitle, { color: theme.colors.text }]}>Visibility</Text>
|
|
</View>
|
|
<View
|
|
style={[
|
|
styles.switchContainer,
|
|
{ borderColor: theme.colors.border, backgroundColor: theme.colors.input },
|
|
]}
|
|
>
|
|
<Text style={{ color: theme.colors.text }}>Make this figure public</Text>
|
|
<Switch
|
|
value={isPublic}
|
|
onValueChange={setIsPublic}
|
|
trackColor={{ false: theme.colors.border, true: theme.colors.primary }}
|
|
thumbColor={isPublic ? theme.colors.card : '#f4f3f4'}
|
|
/>
|
|
</View>
|
|
</View>;
|
|
```
|
|
|
|
### 4. Add Loading State to the Submit Button
|
|
|
|
Update the submit button to show a loading indicator:
|
|
|
|
```jsx
|
|
<TouchableOpacity
|
|
style={[
|
|
styles.submitButton,
|
|
{
|
|
backgroundColor: isGenerating ? theme.colors.border : theme.colors.primary,
|
|
opacity: isGenerating ? 0.7 : 1,
|
|
},
|
|
]}
|
|
onPress={handleSubmit}
|
|
disabled={isGenerating}
|
|
>
|
|
{isGenerating ? (
|
|
<ActivityIndicator color="#fff" size="small" style={{ marginRight: 10 }} />
|
|
) : (
|
|
<FontAwesome name="check" size={20} color="#fff" style={{ marginRight: 10 }} />
|
|
)}
|
|
<Text style={styles.submitButtonText}>{isGenerating ? 'Creating...' : 'Create Figure'}</Text>
|
|
</TouchableOpacity>
|
|
```
|
|
|
|
## 5. Update the MyShelf Page
|
|
|
|
Enhance the MyShelf page to display the user's created figures:
|
|
|
|
```typescript
|
|
// app/(tabs)/myshelf.tsx
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { View, Text, FlatList, StyleSheet } from 'react-native';
|
|
import { supabase } from '~/utils/supabaseClient';
|
|
import { useAuth } from '~/utils/AuthContext';
|
|
import { HorizontalFigureCard } from '~/components/HorizontalFigureCard';
|
|
|
|
export default function MyShelf() {
|
|
const [figures, setFigures] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const { user } = useAuth();
|
|
|
|
useEffect(() => {
|
|
async function loadFigures() {
|
|
if (!user) return;
|
|
|
|
try {
|
|
setLoading(true);
|
|
|
|
const { data, error } = await supabase
|
|
.from('figures')
|
|
.select(
|
|
`
|
|
id,
|
|
name,
|
|
subject,
|
|
image_url,
|
|
theme,
|
|
rarity,
|
|
likes,
|
|
is_public
|
|
`
|
|
)
|
|
.eq('user_id', user.id)
|
|
.order('created_at', { ascending: false });
|
|
|
|
if (error) {
|
|
console.error('Error loading figures:', error);
|
|
return;
|
|
}
|
|
|
|
setFigures(data || []);
|
|
} catch (error) {
|
|
console.error('Error in loadFigures:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
loadFigures();
|
|
}, [user]);
|
|
|
|
// Rest of the component...
|
|
}
|
|
```
|
|
|
|
## Testing Plan
|
|
|
|
1. **Unit Testing**:
|
|
- Test the `generateFigure` function with various inputs
|
|
- Validate form submission with different combinations of data
|
|
|
|
2. **Integration Testing**:
|
|
- Test the end-to-end flow from form submission to figure creation
|
|
- Verify that the figure appears in the user's shelf after creation
|
|
|
|
3. **Error Handling**:
|
|
- Test with invalid inputs to ensure proper error messages
|
|
- Test with network failures to ensure graceful degradation
|
|
|
|
## Deployment Steps
|
|
|
|
1. Deploy the updated code to your Expo project
|
|
2. Verify that the edge function is accessible and working
|
|
3. Monitor the first few figure creations to ensure everything works as expected
|
|
|
|
## Future Enhancements
|
|
|
|
1. **Progress Tracking**: Add a progress bar during figure generation
|
|
2. **Image Preview**: Show a preview of the generated image before saving
|
|
3. **Social Sharing**: Add ability to share figures directly to social media
|
|
4. **Figure Collections**: Allow users to organize figures into collections
|
|
5. **Figure Editing**: Allow users to edit their existing figures
|