diff --git a/games/figgos/.gitignore b/games/figgos/.gitignore new file mode 100644 index 000000000..1861e0868 --- /dev/null +++ b/games/figgos/.gitignore @@ -0,0 +1,25 @@ +node_modules/ +.expo/ +dist/ +npm-debug.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision +*.orig.* +web-build/ +# expo router +expo-env.d.ts + +# firebase/supabase/vexo +.env + +ios +android + +# macOS +.DS_Store + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* \ No newline at end of file diff --git a/games/figgos/Readmes/BackendArchitecture.md b/games/figgos/Readmes/BackendArchitecture.md new file mode 100644 index 000000000..966731b27 --- /dev/null +++ b/games/figgos/Readmes/BackendArchitecture.md @@ -0,0 +1,98 @@ +# WTFigure Backend Architecture + +This document provides an overview of the Supabase backend architecture for the WTFigure application. + +## Database Structure + +The WTFigure database consists of the following tables in the public schema: + +### 1. Prompts Table + +Stores templates for generating action figures. + +| Column Name | Data Type | Nullable | Description | +|-------------|-----------|----------|-------------| +| id | bigint | NO | Primary key, auto-incrementing | +| name | character varying | YES | Name of the prompt template | +| template | text | YES | The actual prompt template content | +| created_at | timestamp with time zone | NO | Creation timestamp | +| updated_at | timestamp with time zone | YES | Last update timestamp | + +### 2. Rarity Table + +Defines rarity levels for action figures. + +| Column Name | Data Type | Nullable | Description | +|-------------|-----------|----------|-------------| +| id | bigint | NO | Primary key, auto-incrementing | +| name | character varying | YES | Name of the rarity level (e.g., Common, Rare, Epic) | +| details | jsonb | YES | Additional details about the rarity level | +| created_at | timestamp with time zone | NO | Creation timestamp | +| updated_at | timestamp with time zone | YES | Last update timestamp | + +### 3. Themes Table + +Defines themes for action figures. + +| Column Name | Data Type | Nullable | Description | +|-------------|-----------|----------|-------------| +| id | bigint | NO | Primary key, auto-incrementing | +| name | character varying | YES | Name of the theme | +| details | jsonb | YES | Additional details about the theme | +| created_at | timestamp with time zone | NO | Creation timestamp | +| updated_at | timestamp with time zone | YES | Last update timestamp | + +## Edge Functions + +### Static Image Generator + +**Endpoint**: `/static-image-generator` + +This edge function generates AI-created action figure images using OpenAI's image generation API. + +**Functionality**: +- Accepts parameters for customizing the action figure (subject, theme, rarity) +- Generates a product-shot style image of an action figure in packaging +- Uploads the generated image to Supabase Storage +- Returns the public URL and metadata for the generated image + +**Key Features**: +- Integrates with OpenAI for image generation +- Supports custom prompts based on themes and rarities +- Stores generated images in Supabase Storage +- Handles error cases gracefully + +**Implementation Details**: +- Uses OpenAI's image generation and editing capabilities +- Dynamically constructs prompts based on the subject, theme, and rarity +- Generates unique filenames for each image +- Uploads images to the 'figures' storage bucket +- Returns structured metadata along with the image URL + +## Authentication + +The WTFigure app uses Supabase Authentication with the following features: +- Email/password authentication +- Session persistence using AsyncStorage +- Protected routes requiring authentication + +## Storage + +The application uses Supabase Storage with the following buckets: +- `figures`: Stores AI-generated action figure images + +## Environment Configuration + +The application connects to Supabase using the following environment variables: +- `EXPO_PUBLIC_SUPABASE_URL`: The URL of your Supabase project +- `EXPO_PUBLIC_SUPABASE_ANON_KEY`: The anonymous API key for client-side access + +## Integration with Frontend + +The WTFigure frontend is a React Native Expo application that: +- Displays action figures in horizontal and vertical layouts +- Supports light/dark theme switching +- Provides user authentication flows +- Allows creation of custom action figures + +The frontend connects to this Supabase backend using the Supabase JavaScript client. diff --git a/games/figgos/Readmes/CreatePageIntegration.md b/games/figgos/Readmes/CreatePageIntegration.md new file mode 100644 index 000000000..d0120a591 --- /dev/null +++ b/games/figgos/Readmes/CreatePageIntegration.md @@ -0,0 +1,467 @@ +# 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 */ +} + + + + + + Theme + + + setSelectedTheme(itemValue)} + style={{ color: theme.colors.text }} + > + {themes.map((themeItem) => ( + + ))} + + +; + +{ + /* Rarity Selection */ +} + + + + + + Rarity + + + setSelectedRarity(itemValue)} + style={{ color: theme.colors.text }} + > + {rarities.map((rarityItem) => ( + + ))} + + +; + +{ + /* Visibility Selection */ +} + + + + + + Visibility + + + Make this figure public + + +; +``` + +### 4. Add Loading State to the Submit Button + +Update the submit button to show a loading indicator: + +```jsx + + {isGenerating ? ( + + ) : ( + + )} + {isGenerating ? 'Creating...' : 'Create Figure'} + +``` + +## 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 diff --git a/games/figgos/Readmes/GPTImageAPI.md b/games/figgos/Readmes/GPTImageAPI.md new file mode 100644 index 000000000..3a3afcd8c --- /dev/null +++ b/games/figgos/Readmes/GPTImageAPI.md @@ -0,0 +1,238 @@ +# OpenAI GPT Image v1 API Dokumentation + +Diese Dokumentation konzentriert sich auf die neue OpenAI GPT Image v1 API (gpt-image-1), die transparente Hintergründe und andere fortschrittliche Funktionen unterstützt. + +# OpenAI GPT Image v1 API Dokumentation + +Diese Dokumentation konzentriert sich auf die neue OpenAI GPT Image v1 API (gpt-image-1), die transparente Hintergründe und andere fortschrittliche Funktionen unterstützt. + +## Der Parameter `background` + +Der Parameter `background` in der OpenAI Image v1 API (gpt-image-1) steuert den Hintergrundtyp des generierten Bildes. Mit diesem Parameter kannst du gezielt festlegen, ob das Bild einen transparenten, undurchsichtigen (opaken) oder automatisch gewählten Hintergrund haben soll. + +### Mögliche Werte für background: + +- **"transparent"**: Das Bild wird mit transparentem Hintergrund erzeugt. Dies funktioniert nur, wenn das Ausgabeformat (`output_format`) auf "png" oder "webp" gesetzt ist, da nur diese Formate Transparenz unterstützen. + +- **"opaque"**: Das Bild erhält einen vollständig undurchsichtigen Hintergrund. + +- **"auto"**: Die API entscheidet automatisch, welcher Hintergrundtyp verwendet wird (Standardwert). + +### Wichtige Hinweise: + +- Für Transparenz muss das Ausgabeformat png oder webp sein. JPEG unterstützt keine Transparenz. + +- Transparenz funktioniert am besten mit mittlerer oder hoher Qualitätsstufe (quality: "medium" oder "high"). + +- Es reicht nicht, nur im Prompt nach „transparent background" zu fragen – der Parameter `background` sollte explizit gesetzt werden. + +### Beispiel für einen API-Request mit transparentem Hintergrund: + +````python +result = client.images.generate( + model="gpt-image-1", + prompt="Vector art icon of a stylized rocket ship, transparent background", + size="1024x1024", + quality="high", + output_format="webp", + background="transparent", + n=1 +) +Damit erhältst du ein Bild mit echtem transparentem Hintergrund, das sich z.B. für Icons oder Overlays eignet. + +OpenAI GPT Image v1 API – Übersicht +1. Bilder generieren +Endpunkt: POST /v1/images/generations + +Wichtige Parameter: + +model: "gpt-image-1" +prompt: Beschreibung des gewünschten Bildes (Pflichtfeld) +n: Anzahl der Bilder (optional, Standard: 1) +size: Bildgröße, z.B. "1024x1024" (optional) +quality: "standard" oder "high" (optional) +style: "vivid" oder "natural" (optional) +background: "transparent", "opaque", "auto" (optional) +output_format: "png", "webp", "jpeg" (optional, für Transparenz: png/webp) +Beispiel: + +json +CopyInsert +{ + "model": "gpt-image-1", + "prompt": "A vector icon of a rocket, transparent background", + "n": 1, + "size": "1024x1024", + "quality": "high", + "output_format": "webp", + "background": "transparent" +} +2. Bilder bearbeiten +Endpunkt: POST /v1/images/edits + +Wichtige Parameter: + +model: "gpt-image-1" +image: Hochzuladende Bilddatei (Pflichtfeld) +prompt: Beschreibung der Änderung (Pflichtfeld) +mask: Optionales Maskenbild (optional) +Weitere Parameter wie bei Bildgenerierung +3. Bildvariationen erstellen +Endpunkt: POST /v1/images/variations + +Wichtige Parameter: + +image: Hochzuladende Bilddatei (Pflichtfeld) +Weitere Parameter wie bei Bildgenerierung +Parameter background im Detail +| Wert | Bedeutung | Hinweis | |------|-----------|--------| | "transparent" | Bild mit transparentem Hintergrund (nur mit PNG/WebP) | Für Icons, Overlays etc. geeignet | | "opaque" | Bild mit undurchsichtigem Hintergrund | Standard für Fotos | | "auto" | Automatische Auswahl durch die API | Standardwert | + +Achtung: + +Für Transparenz muss output_format auf "png" oder "webp" stehen! +Transparenz funktioniert am besten mit quality: "high". +GPT Image v1 - Funktionen und Fähigkeiten +Überblick +Die GPT Image v1 API (gpt-image-1) ist ein nativ multimodales großes Sprachmodell. Es kann sowohl Text als auch Bilder verstehen und sein umfassendes Weltwissen nutzen, um Bilder mit besserer Befolgung von Anweisungen und kontextuellem Bewusstsein zu generieren. + +Bildgenerierung mit Weltwissen +Als nativ multimodales Sprachmodell kann GPT Image sein visuelles Verständnis der Welt nutzen, um lebensechte Bilder einschließlich realer Details ohne Referenz zu generieren. + +Beispiel: Wenn du GPT Image aufforderst, ein Bild einer Glasvitrine mit den beliebtesten Halbedelsteinen zu generieren, weiß das Modell genug, um Edelsteine wie Amethyst, Rosenquarz, Jade usw. auszuwählen und sie auf realistische Weise darzustellen. + +Beispiele für die Verwendung der GPT Image v1 API +Bild mit transparentem Hintergrund generieren +bash +CopyInsert +curl https://api.openai.com/v1/images/generations \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d '{ + "model": "gpt-image-1", + "prompt": "Vector art icon of a stylized fox, transparent background", + "n": 1, + "size": "1024x1024", + "quality": "high", + "background": "transparent", + "output_format": "webp" + }' +Bild mit JavaScript generieren +javascript +CopyInsert +async function generateImage() { + const response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${OPENAI_API_KEY}` + }, + body: JSON.stringify({ + model: "gpt-image-1", + prompt: "Vector art icon of a stylized fox, transparent background", + n: 1, + size: "1024x1024", + quality: "high", + background: "transparent", + output_format: "webp" + }) + }); + + const result = await response.json(); + return result; +} +Authentifizierung +Immer den Header setzen: Authorization: Bearer + +## Response-Format und Base64-Encoding + +### Wichtige Hinweise zum Response-Format: + +- Die GPT Image v1 API kann entweder eine URL oder einen Base64-String zurückgeben. +- Der Parameter `response_format` wird **nicht** unterstützt, im Gegensatz zu DALL-E 3. +- Die API kann zwei verschiedene Antwortformate zurückgeben: + + 1. **URL-Format**: + ```json + { + "created": 1683245297, + "data": [ + { + "url": "https://..." + } + ] + } +```` + +2. **Base64-Format**: + +```json +{ + "created": 1683245297, + "data": [ + { + "b64_json": "iVBORw0KGgoAAAANSUhEUgAA..." + } + ] +} +``` + +- Deine Implementierung sollte beide Formate unterstützen, da die API je nach Anfrage und Konfiguration unterschiedliche Formate zurückgeben kann. + +### Beispiel für die Verarbeitung beider Antwortformate: + +```javascript +const response = await fetch('https://api.openai.com/v1/images/generations', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${OPENAI_API_KEY}`, + }, + body: JSON.stringify({ + model: 'gpt-image-1', + prompt: 'Vector art icon of a stylized fox, transparent background', + n: 1, + size: '1024x1024', + quality: 'high', + background: 'transparent', + output_format: 'png', + }), +}); + +const result = await response.json(); + +// Prüfe, ob die Antwort eine URL oder Base64-Daten enthält +const imageUrl = result.data?.[0]?.url; +const imageBase64 = result.data?.[0]?.b64_json; + +let imageBlob; + +if (imageUrl) { + // Fall 1: URL wurde zurückgegeben + const imageResponse = await fetch(imageUrl); + imageBlob = await imageResponse.blob(); +} else if (imageBase64) { + // Fall 2: Base64-Daten wurden zurückgegeben + const binaryString = atob(imageBase64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + imageBlob = new Blob([bytes], { type: 'image/png' }); +} else { + throw new Error('Keine Bilddaten in der Antwort gefunden'); +} + +// Jetzt kann das Bild verwendet werden +// z.B. zum Anzeigen in einem -Element oder zum Speichern +``` + +## Zusammenfassung + +Mit der OpenAI GPT Image v1 API kannst du: + +- Bilder mit hoher Qualität generieren +- Mit dem Parameter background gezielt den Hintergrundtyp steuern +- Transparente Hintergründe für Icons und Grafiken erstellen +- Bilder über die zurückgegebene URL herunterladen und weiterverarbeiten + +Tipp für transparente Icons, Logos oder Sticker: Verwende background: "transparent" und output_format: "png" oder "webp" diff --git a/games/figgos/Readmes/SupabaseMCPConnect.md b/games/figgos/Readmes/SupabaseMCPConnect.md new file mode 100644 index 000000000..b141caa94 --- /dev/null +++ b/games/figgos/Readmes/SupabaseMCPConnect.md @@ -0,0 +1,261 @@ +Model context protocol (MCP) + +Connect your AI tools to Supabase using MCP + +The Model Context Protocol (MCP) is a standard for connecting Large Language Models (LLMs) to platforms like Supabase. This guide covers how to connect Supabase to the following AI tools using MCP: + +Cursor +Windsurf (Codium) +Visual Studio Code (Copilot) +Cline (VS Code extension) +Claude desktop +Claude code +Once connected, your AI assistants can interact with and query your Supabase projects on your behalf. + +Step 1: Create a personal access token (PAT)# +First, go to your Supabase settings and create a personal access token. Give it a name that describes its purpose, like "Cursor MCP Server". This will be used to authenticate the MCP server with your Supabase account. + +Step 2: Configure in your AI tool# +MCP compatible tools can connect to Supabase using the Supabase MCP server. Below are instructions for connecting to this server using popular AI tools: + +Cursor# +Open Cursor and create a .cursor directory in your project root if it doesn't exist. + +Create a .cursor/mcp.json file if it doesn't exist and open it. + +Add the following configuration: + +macOS + +Windows + +Windows (WSL) + +Linux +{ +"mcpServers": { +"supabase": { +"command": "npx", +"args": [ +"-y", +"@supabase/mcp-server-supabase@latest", +"--access-token", +"" +] +} +} +} +Replace with your personal access token. + +Save the configuration file. + +Open Cursor and navigate to Settings/MCP. You should see a green active status after the server is successfully connected. + +Windsurf# +Open Windsurf and navigate to the Cascade assistant. + +Tap on the hammer (MCP) icon, then Configure to open the configuration file. + +Add the following configuration: + +macOS + +Windows + +Windows (WSL) + +Linux +{ +"mcpServers": { +"supabase": { +"command": "npx", +"args": [ +"-y", +"@supabase/mcp-server-supabase@latest", +"--access-token", +"" +] +} +} +} +Replace with your personal access token. + +Save the configuration file and reload by tapping Refresh in the Cascade assistant. + +You should see a green active status after the server is successfully connected. + +Visual Studio Code (Copilot)# +Open VS Code and create a .vscode directory in your project root if it doesn't exist. + +Create a .vscode/mcp.json file if it doesn't exist and open it. + +Add the following configuration: + +macOS + +Windows + +Windows (WSL) + +Linux +{ +"inputs": [ +{ +"type": "promptString", +"id": "supabase-access-token", +"description": "Supabase personal access token", +"password": true +} +], +"servers": { +"supabase": { +"command": "npx", +"args": ["-y", "@supabase/mcp-server-supabase@latest"], +"env": { +"SUPABASE_ACCESS_TOKEN": "${input:supabase-access-token}" +} +} +} +} +Save the configuration file. + +Open Copilot chat and switch to "Agent" mode. You should see a tool icon that you can tap to confirm the MCP tools are available. Once you begin using the server, you will be prompted to enter your personal access token. Enter the token that you created earlier. + +For more info on using MCP in VS Code, see the Copilot documentation. + +Cline# +Open the Cline extension in VS Code and tap the MCP Servers icon. + +Tap Configure MCP Servers to open the configuration file. + +Add the following configuration: + +macOS + +Windows + +Windows (WSL) + +Linux +{ +"mcpServers": { +"supabase": { +"command": "npx", +"args": [ +"-y", +"@supabase/mcp-server-supabase@latest", +"--access-token", +"" +] +} +} +} +Replace with your personal access token. + +Save the configuration file. Cline should automatically reload the configuration. + +You should see a green active status after the server is successfully connected. + +Claude desktop# +Open Claude desktop and navigate to Settings. + +Under the Developer tab, tap Edit Config to open the configuration file. + +Add the following configuration: + +macOS + +Windows + +Windows (WSL) + +Linux +{ +"mcpServers": { +"supabase": { +"command": "npx", +"args": [ +"-y", +"@supabase/mcp-server-supabase@latest", +"--access-token", +"" +] +} +} +} +Replace with your personal access token. + +Save the configuration file and restart Claude desktop. + +From the new chat screen, you should see a hammer (MCP) icon appear with the new MCP server available. + +Claude code# +Create a .mcp.json file in your project root if it doesn't exist. + +Add the following configuration: + +macOS + +Windows + +Windows (WSL) + +Linux +{ +"mcpServers": { +"supabase": { +"command": "npx", +"args": [ +"-y", +"@supabase/mcp-server-supabase@latest", +"--access-token", +"" +] +} +} +} +Replace with your personal access token. + +Save the configuration file. + +Restart Claude code to apply the new configuration. + +Next steps# +Your AI tool is now connected to Supabase using MCP. Try asking your AI assistant to create a new project, create a table, or fetch project config. + +For a full list of tools available, see the GitHub README. If you experience any issues, submit an bug report. + +MCP for local Supabase instances# +The Supabase MCP server connects directly to the cloud platform to access your database. If you are running a local instance of Supabase, you can instead use the Postgres MCP server to connect to your local database. This MCP server runs all queries as read-only transactions. + +Step 1: Find your database connection string# +To connect to your local Supabase instance, you need to get the connection string for your local database. You can find your connection string by running: + +supabase status +or if you are using npx: + +npx supabase status +This will output a list of details about your local Supabase instance. Copy the DB URL field in the output. + +Step 2: Configure the MCP server# +Configure your client with the following: + +macOS + +Windows + +Windows (WSL) + +Linux +{ +"mcpServers": { +"supabase": { +"command": "npx", +"args": ["-y", "@modelcontextprotocol/server-postgres", ""] +} +} +} +Replace with your connection string. + +Next steps# +Your AI tool is now connected to your local Supabase instance using MCP. Try asking the AI tool to query your database using natural language commands. diff --git a/games/figgos/app-env.d.ts b/games/figgos/app-env.d.ts new file mode 100644 index 000000000..88dc403ea --- /dev/null +++ b/games/figgos/app-env.d.ts @@ -0,0 +1,2 @@ +// @ts-ignore +/// diff --git a/games/figgos/app.json b/games/figgos/app.json new file mode 100644 index 000000000..c14f38b2b --- /dev/null +++ b/games/figgos/app.json @@ -0,0 +1,58 @@ +{ + "expo": { + "name": "Figgos", + "slug": "figgos", + "version": "1.0.0", + "scheme": "figgos", + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./assets/favicon.png" + }, + "plugins": [ + "expo-router", + [ + "expo-dev-launcher", + { + "launchMode": "most-recent" + } + ] + ], + "experiments": { + "typedRoutes": true, + "tsconfigPaths": true + }, + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "splash": { + "image": "./assets/splash.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "assetBundlePatterns": ["**/*"], + "ios": { + "supportsTablet": true, + "bundleIdentifier": "com.memoro.figgos", + "infoPlist": { + "ITSAppUsesNonExemptEncryption": false + } + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "package": "com.memoro.figgos" + }, + "extra": { + "router": { + "origin": false + }, + "eas": { + "projectId": "cd5ac6b1-3491-4205-b0d3-3f8c081bae06" + } + }, + "owner": "memoro" + } +} diff --git a/games/figgos/app/(auth)/_layout.tsx b/games/figgos/app/(auth)/_layout.tsx new file mode 100644 index 000000000..95390c79d --- /dev/null +++ b/games/figgos/app/(auth)/_layout.tsx @@ -0,0 +1,21 @@ +import { Stack } from 'expo-router'; +import { useTheme } from '../../utils/ThemeContext'; + +export default function AuthLayout() { + const { theme } = useTheme(); + + return ( + + ); +} diff --git a/games/figgos/app/(auth)/login.tsx b/games/figgos/app/(auth)/login.tsx new file mode 100644 index 000000000..7db79b5c9 --- /dev/null +++ b/games/figgos/app/(auth)/login.tsx @@ -0,0 +1,596 @@ +import React, { useState } from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, + ActivityIndicator, + Keyboard, + ImageBackground, + Platform, + Pressable, + Alert, +} from 'react-native'; +import { useRouter } from 'expo-router'; +import { useAuth } from '../../utils/AuthContext'; +import { useTheme } from '../../utils/ThemeContext'; +import FontAwesome from '@expo/vector-icons/FontAwesome'; +import { BlurView } from 'expo-blur'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; + +// Validate email format +const validateEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +}; + +// Validate password strength +const validatePassword = (password: string): string | null => { + if (password.length < 6) return 'Das Passwort muss mindestens 6 Zeichen lang sein'; + if (!/[A-Z]/.test(password)) return 'Das Passwort muss mindestens einen Großbuchstaben enthalten'; + if (!/[a-z]/.test(password)) + return 'Das Passwort muss mindestens einen Kleinbuchstaben enthalten'; + if (!/[0-9]/.test(password)) return 'Das Passwort muss mindestens eine Zahl enthalten'; + return null; +}; + +type AuthMode = 'initial' | 'login' | 'register'; + +export default function Login() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [emailError, setEmailError] = useState(null); + const [passwordError, setPasswordError] = useState(null); + const [mode, setMode] = useState('initial'); + const [isResetPassword, setIsResetPassword] = useState(false); + + const { signIn, signUp } = useAuth(); + const router = useRouter(); + const { theme, isDark } = useTheme(); + + // Clear error messages when inputs change + const handleEmailChange = (text: string) => { + setEmail(text); + setEmailError(null); + }; + + const handlePasswordChange = (text: string) => { + setPassword(text); + setPasswordError(null); + }; + + const handleConfirmPasswordChange = (text: string) => { + setConfirmPassword(text); + setPasswordError(null); + }; + + // Handle password reset + const handleResetPassword = async () => { + setEmailError(null); + + if (!email) { + setEmailError('Bitte gib deine E-Mail-Adresse ein'); + return; + } + + if (!validateEmail(email)) { + setEmailError('Bitte gib eine gültige E-Mail-Adresse ein'); + return; + } + + setLoading(true); + try { + // This is a placeholder - Supabase doesn't have a direct method for this + // You would need to implement this in your backend or use Supabase's email templates + Alert.alert( + 'Funktion nicht verfügbar', + 'Die Passwort-Zurücksetzen-Funktion ist noch nicht implementiert.' + ); + setMode('login'); + setIsResetPassword(false); + } catch (error) { + console.error('Password reset error:', error); + Alert.alert('Fehler', 'Ein unbekannter Fehler ist aufgetreten'); + } finally { + setLoading(false); + } + }; + + // Handle login + const handleLogin = async () => { + // Dismiss keyboard + Keyboard.dismiss(); + + setEmailError(null); + setPasswordError(null); + + // Validate inputs + if (!email.trim()) { + setEmailError('Bitte gib deine E-Mail-Adresse ein'); + return; + } + + if (!password) { + setPasswordError('Bitte gib dein Passwort ein'); + return; + } + + // Validate email format + if (!validateEmail(email)) { + setEmailError('Bitte gib eine gültige E-Mail-Adresse ein'); + return; + } + + setLoading(true); + + try { + console.log('Attempting login with email:', email); + const { error } = await signIn(email, password); + + if (error) { + console.error('Login error:', error); + // Handle specific error cases with user-friendly messages + if (error.message.includes('Invalid login credentials')) { + setPasswordError('E-Mail oder Passwort ist falsch'); + } else if (error.message.includes('Email not confirmed')) { + setEmailError('Deine E-Mail wurde noch nicht bestätigt'); + } else if (error.message.includes('rate limit')) { + setEmailError('Zu viele Anmeldeversuche. Bitte versuche es später erneut'); + } else if (error.message.includes('network')) { + setEmailError('Netzwerkfehler. Bitte überprüfe deine Internetverbindung'); + } else { + setEmailError(error.message || 'Ein unbekannter Fehler ist aufgetreten'); + } + } else { + console.log('Login successful'); + router.replace('/(tabs)'); + } + } catch (e) { + console.error('Unexpected login error:', e); + setEmailError('Ein unerwarteter Fehler ist aufgetreten'); + } finally { + setLoading(false); + } + }; + + // Handle signup + const handleSignUp = async () => { + // Dismiss keyboard + Keyboard.dismiss(); + + setEmailError(null); + setPasswordError(null); + + // Validate inputs + if (!email.trim()) { + setEmailError('Bitte gib deine E-Mail-Adresse ein'); + return; + } + + if (!password) { + setPasswordError('Bitte gib ein Passwort ein'); + return; + } + + if (!confirmPassword) { + setPasswordError('Bitte bestätige dein Passwort'); + return; + } + + // Validate email format + if (!validateEmail(email)) { + setEmailError('Bitte gib eine gültige E-Mail-Adresse ein'); + return; + } + + if (password !== confirmPassword) { + setPasswordError('Die Passwörter stimmen nicht überein'); + return; + } + + const passwordValidationError = validatePassword(password); + if (passwordValidationError) { + setPasswordError(passwordValidationError); + return; + } + + setLoading(true); + + try { + console.log('Attempting signup with email:', email); + const { error } = await signUp(email, password); + + if (error) { + console.error('Signup error:', error); + // Handle specific error cases with user-friendly messages + if (error.message.includes('already registered')) { + setEmailError('Diese E-Mail-Adresse wird bereits verwendet'); + } else if (error.message.includes('rate limit')) { + setEmailError('Zu viele Registrierungsversuche. Bitte versuche es später erneut'); + } else if (error.message.includes('network')) { + setEmailError('Netzwerkfehler. Bitte überprüfe deine Internetverbindung'); + } else if (error.message.includes('weak password')) { + setPasswordError('Dein Passwort ist zu schwach. Bitte wähle ein stärkeres Passwort'); + } else { + setEmailError(error.message || 'Ein unbekannter Fehler ist aufgetreten'); + } + } else { + console.log('Signup successful'); + Alert.alert( + 'Registrierung erfolgreich', + 'Bitte überprüfe deine E-Mail, um dein Konto zu bestätigen.', + [ + { + text: 'OK', + onPress: () => { + setMode('login'); + setEmail(''); + setPassword(''); + setConfirmPassword(''); + }, + }, + ] + ); + } + } catch (e) { + console.error('Unexpected signup error:', e); + setEmailError('Ein unerwarteter Fehler ist aufgetreten'); + } finally { + setLoading(false); + } + }; + + // Dynamic styles that depend on the theme + const dynamicStyles = { + loginButton: { + backgroundColor: '#555', // More neutral color for login button + }, + signUpButton: { + backgroundColor: theme.colors.primary, + }, + backButton: { + backgroundColor: theme.colors.text + '80', // With opacity + }, + }; + + return ( + + + + {/* Title removed as requested */} + + + + {mode === 'initial' && 'Erstelle ein Konto oder melde dich an'} + {mode === 'login' && !isResetPassword && 'Melde dich mit deinem Konto an'} + {mode === 'login' && + isResetPassword && + 'Gib deine E-Mail-Adresse ein, um dein Passwort zurückzusetzen'} + {mode === 'register' && 'Erstelle ein neues Konto'} + + + {mode !== 'initial' && ( + + + {emailError && {emailError}} + + {!isResetPassword && ( + <> + + {passwordError && {passwordError}} + + {mode === 'register' && ( + <> + + + )} + + )} + + )} + + {mode === 'login' && !isResetPassword && ( + setIsResetPassword(true)} + > + Passwort vergessen + + + )} + + + {mode === 'initial' ? ( + <> + setMode('register')} + > + + Konto erstellen + + + + + setMode('login')} + > + + Anmelden + + + + + ) : ( + <> + + {loading ? ( + + ) : ( + + + {mode === 'register' + ? 'Konto erstellen' + : isResetPassword + ? 'Passwort zurücksetzen' + : 'Anmelden'} + + + + )} + + + { + setMode('initial'); + setEmail(''); + setPassword(''); + setConfirmPassword(''); + setEmailError(null); + setPasswordError(null); + setIsResetPassword(false); + }} + > + + Zurück + + + + + )} + + + + Mit der Anmeldung stimmst du unseren AGB und der{' '} + Datenschutzerklärung zu. + + + + + + ); +} + +const styles = StyleSheet.create({ + mainContainer: { + flex: 1, + width: '100%', + alignItems: 'center', + justifyContent: 'flex-start', + backgroundColor: '#000', + }, + backgroundImage: { + flex: 1, + width: '100%', + height: '100%', + alignItems: 'center', + justifyContent: 'flex-start', + paddingTop: 0, // Image at the very top + }, + container: { + flex: 1, + backgroundColor: 'transparent', + width: '100%', + maxWidth: 480, + aspectRatio: Platform.OS === 'ios' ? undefined : 0.7, + alignSelf: 'center', + marginHorizontal: 'auto', + }, + scrollContent: { + flexGrow: 1, + width: '100%', + }, + headerContainer: { + marginTop: Platform.OS === 'ios' ? 20 : 30, + paddingHorizontal: Platform.OS === 'ios' ? 20 : 40, + }, + contentContainer: { + width: '100%', + padding: Platform.OS === 'ios' ? 16 : 20, + paddingBottom: Platform.OS === 'ios' ? 16 : 8, + marginTop: 'auto', + overflow: 'hidden', + borderTopLeftRadius: 24, + borderTopRightRadius: 24, + }, + title: { + fontSize: 36, + fontWeight: 'bold', + marginBottom: 12, + textAlign: 'center', + color: '#fff', + }, + subtitle: { + fontSize: 18, + textAlign: 'center', + color: '#fff', + marginBottom: 20, + opacity: 0.9, + }, + inputContainer: { + marginBottom: 20, + }, + input: { + height: 55, + borderColor: '#333', + borderWidth: 1, + marginBottom: 8, + paddingHorizontal: 15, + borderRadius: 10, + backgroundColor: 'rgba(0, 0, 0, 0.2)', + color: '#fff', + fontSize: 18, + }, + inputError: { + borderColor: '#ff4444', + }, + errorText: { + color: '#ff4444', + fontSize: 12, + marginBottom: 10, + marginLeft: 5, + }, + buttonContainer: { + gap: 15, + }, + button: { + height: 50, + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + }, + loginButton: { + backgroundColor: '#4a90e2', // Default color, will be overridden + }, + signUpButton: { + backgroundColor: '#ff69b4', // Default color, will be overridden + }, + backButton: { + backgroundColor: '#555', // Default color, will be overridden + }, + buttonText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, + buttonContent: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + buttonIcon: { + marginLeft: 10, + }, + legalText: { + color: '#fff', + fontSize: 12, + textAlign: 'center', + marginTop: 20, + marginBottom: 10, + opacity: 0.8, + }, + legalLink: { + textDecorationLine: 'underline', + }, + forgotPasswordContainer: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 20, + paddingHorizontal: 4, + }, + forgotPasswordText: { + color: '#fff', + fontSize: 14, + opacity: 0.9, + }, + forgotPasswordArrow: { + color: '#fff', + fontSize: 14, + marginLeft: 8, + opacity: 0.9, + }, +}); diff --git a/games/figgos/app/(auth)/register.tsx b/games/figgos/app/(auth)/register.tsx new file mode 100644 index 000000000..cae8466e1 --- /dev/null +++ b/games/figgos/app/(auth)/register.tsx @@ -0,0 +1,276 @@ +import React, { useState } from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, + Alert, + ActivityIndicator, + Keyboard, +} from 'react-native'; +import { useRouter } from 'expo-router'; +import { useAuth } from '../../utils/AuthContext'; +import { useTheme } from '../../utils/ThemeContext'; +import FontAwesome from '@expo/vector-icons/FontAwesome'; +import ErrorMessage from '../../components/ErrorMessage'; + +export default function Register() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + const { signUp } = useAuth(); + const router = useRouter(); + const { theme, isDark } = useTheme(); + + // Clear error message when inputs change + const handleInputChange = + (setter: React.Dispatch>) => (text: string) => { + setter(text); + if (errorMessage) setErrorMessage(''); + }; + + const handleRegister = async () => { + // Dismiss keyboard + Keyboard.dismiss(); + + // Validate inputs + if (!email.trim()) { + setErrorMessage('Bitte gib deine E-Mail-Adresse ein.'); + return; + } + + if (!password) { + setErrorMessage('Bitte gib ein Passwort ein.'); + return; + } + + if (!confirmPassword) { + setErrorMessage('Bitte bestätige dein Passwort.'); + return; + } + + // Validate email format + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + setErrorMessage('Bitte gib eine gültige E-Mail-Adresse ein.'); + return; + } + + if (password !== confirmPassword) { + setErrorMessage('Die Passwörter stimmen nicht überein.'); + return; + } + + if (password.length < 6) { + setErrorMessage('Das Passwort muss mindestens 6 Zeichen lang sein.'); + return; + } + + // Check password strength + const hasUpperCase = /[A-Z]/.test(password); + const hasLowerCase = /[a-z]/.test(password); + const hasNumbers = /\d/.test(password); + const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password); + + if (!(hasUpperCase && hasLowerCase && (hasNumbers || hasSpecialChar))) { + setErrorMessage( + 'Dein Passwort sollte Groß- und Kleinbuchstaben sowie Zahlen oder Sonderzeichen enthalten.' + ); + return; + } + + setLoading(true); + setErrorMessage(''); + + try { + const { error } = await signUp(email, password); + + if (error) { + // Handle specific error cases with user-friendly messages + if (error.message.includes('already registered')) { + setErrorMessage( + 'Diese E-Mail-Adresse wird bereits verwendet. Bitte wähle eine andere oder melde dich an.' + ); + } else if (error.message.includes('rate limit')) { + setErrorMessage('Zu viele Registrierungsversuche. Bitte versuche es später erneut.'); + } else if (error.message.includes('network')) { + setErrorMessage('Netzwerkfehler. Bitte überprüfe deine Internetverbindung.'); + } else if (error.message.includes('weak password')) { + setErrorMessage('Dein Passwort ist zu schwach. Bitte wähle ein stärkeres Passwort.'); + } else { + setErrorMessage(error.message || 'Ein unbekannter Fehler ist aufgetreten.'); + } + } else { + Alert.alert( + 'Registrierung erfolgreich', + 'Bitte überprüfe deine E-Mail, um dein Konto zu bestätigen.', + [{ text: 'OK', onPress: () => router.replace('/login') }] + ); + } + } catch (e) { + setErrorMessage('Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut.'); + console.error('Registration error:', e); + } finally { + setLoading(false); + } + }; + + return ( + + + + WTFigure + + Erstelle und teile deine Action-Figuren + + + + + + + + + + + + + + Passwort muss mindestens 6 Zeichen lang sein und Groß- und Kleinbuchstaben sowie Zahlen + oder Sonderzeichen enthalten. + + + + {loading ? ( + + ) : ( + Registrieren + )} + + + + Bereits ein Konto? + router.push('/login')}> + Anmelden + + + + + ); +} + +const styles = StyleSheet.create({ + passwordHint: { + fontSize: 12, + marginBottom: 15, + marginTop: -5, + }, + container: { + flex: 1, + padding: 20, + justifyContent: 'center', + }, + logoContainer: { + alignItems: 'center', + marginBottom: 40, + }, + title: { + fontSize: 32, + fontWeight: 'bold', + marginTop: 10, + }, + subtitle: { + fontSize: 16, + marginTop: 5, + textAlign: 'center', + }, + formContainer: { + width: '100%', + maxWidth: 400, + alignSelf: 'center', + }, + input: { + height: 50, + borderWidth: 1, + borderRadius: 10, + marginBottom: 15, + paddingHorizontal: 15, + fontSize: 16, + }, + button: { + height: 50, + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + marginTop: 10, + }, + buttonText: { + color: '#fff', + fontSize: 16, + fontWeight: 'bold', + }, + footerContainer: { + flexDirection: 'row', + justifyContent: 'center', + marginTop: 20, + }, + footerText: { + marginRight: 5, + }, + footerLink: { + fontWeight: 'bold', + }, +}); diff --git a/games/figgos/app/(tabs)/_layout.tsx b/games/figgos/app/(tabs)/_layout.tsx new file mode 100644 index 000000000..af9fc4171 --- /dev/null +++ b/games/figgos/app/(tabs)/_layout.tsx @@ -0,0 +1,87 @@ +import { Link, Tabs } from 'expo-router'; +import { useTheme } from '../../utils/ThemeContext'; +import { Platform, StyleSheet } from 'react-native'; +import { BlurView } from 'expo-blur'; + +import { Header } from '../../components/Header'; +import { TabBarIcon } from '../../components/TabBarIcon'; + +export default function TabLayout() { + const { theme, isDark } = useTheme(); + + // Bestimme die Intensität des Blur-Effekts basierend auf dem Theme + const blurIntensity = isDark ? 80 : 60; + const blurTint = isDark ? 'dark' : 'light'; + + return ( + + Platform.OS === 'ios' ? ( + + ) : null, + headerTransparent: true, + headerStyle: { + backgroundColor: 'transparent', + }, + headerBackground: () => ( + + ), + headerTintColor: theme.colors.text, + headerShadowVisible: false, + headerTitleStyle: { + fontWeight: '600', + }, + // Hide the header completely + headerShown: false, + }} + > + ( + + ), + headerRight: () => ( + +
+ + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + + ); +} diff --git a/games/figgos/app/(tabs)/create.tsx b/games/figgos/app/(tabs)/create.tsx new file mode 100644 index 000000000..c501c8061 --- /dev/null +++ b/games/figgos/app/(tabs)/create.tsx @@ -0,0 +1,156 @@ +import { Stack } from 'expo-router'; +import { + StyleSheet, + View, + Image, + Text, + TouchableOpacity, + ActivityIndicator, + ScrollView, + useWindowDimensions, +} from 'react-native'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; +import React, { useRef, useState } from 'react'; +import FontAwesome from '@expo/vector-icons/FontAwesome'; +import { useTheme } from '~/utils/ThemeContext'; +import SidebarCreateFigureForm from '~/components/CreateFigureForm'; + +export default function Create() { + const { theme } = useTheme(); + const { height } = useWindowDimensions(); + const [isGenerating, setIsGenerating] = useState(false); + const handleSubmitRef = useRef<() => Promise>(); + + // Function to handle the form submission from the sticky button + const handleStickySubmit = async () => { + if (handleSubmitRef.current) { + setIsGenerating(true); + try { + await handleSubmitRef.current(); + } finally { + setIsGenerating(false); + } + } + }; + + // Callback to receive the handleSubmit function from the form component + const onFormSubmit = (handleSubmit: () => Promise) => { + handleSubmitRef.current = handleSubmit; + }; + + return ( + <> + + + + + + + + + + {/* Sticky Create Figure Button */} + + {isGenerating ? ( + + ) : null} + {isGenerating ? 'Creating...' : 'Create Figgo'} + {!isGenerating && ( + + )} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + width: '100%', + padding: 0, + margin: 0, + position: 'relative', + }, + backgroundImage: { + width: '100%', + height: '100%', + maxWidth: 800, + maxHeight: 800, + resizeMode: 'contain', + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + marginLeft: 'auto', + marginRight: 'auto', + zIndex: 1, + opacity: 0.9, + }, + scrollContainer: { + flex: 1, + width: '100%', + zIndex: 2, + }, + scrollContentContainer: { + flexGrow: 1, + paddingBottom: 120, // Add padding at the bottom for the sticky button + }, + formContainer: { + width: '100%', + zIndex: 2, + paddingBottom: 0, // Remove bottom padding to avoid extra space + maxWidth: 450, // Further reduced max width for the form + alignSelf: 'center', // Center the form + }, + stickyButton: { + position: 'absolute', + bottom: 70, // Positioned lower, closer to the tab bar + left: '50%', // Center horizontally + transform: [{ translateX: -100 }], // Half of the width to center + width: 200, // Fixed width for a more compact button + height: 50, // Reduced height + borderRadius: 25, // Half of height for rounded corners + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + zIndex: 999, // Much higher z-index to ensure it's above the tab bar + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 10, // Higher elevation for Android + }, + buttonText: { + color: '#fff', + fontSize: 18, + fontWeight: 'bold', + }, +}); diff --git a/games/figgos/app/(tabs)/index.tsx b/games/figgos/app/(tabs)/index.tsx new file mode 100644 index 000000000..30bd577b0 --- /dev/null +++ b/games/figgos/app/(tabs)/index.tsx @@ -0,0 +1,278 @@ +import { Stack, router } from 'expo-router'; +import { + StyleSheet, + View, + ScrollView, + SafeAreaView, + ActivityIndicator, + useWindowDimensions, + Animated, + TouchableOpacity, +} from 'react-native'; +import React, { useRef, useState as useReactState } from 'react'; +import { useScrollToTop } from '@react-navigation/native'; +import { useTheme } from '~/utils/ThemeContext'; +import { ThemedView, ThemedText } from '~/components/ThemedView'; +import { useState, useEffect, useMemo } from 'react'; +import FontAwesome from '@expo/vector-icons/FontAwesome'; + +import { getPublicFigures } from '~/utils/figureService'; +import { VerticalFigureCard } from '~/components/FigureCard'; +import { Header } from '~/components/Header'; + +// Definiere den Typ für eine Figur +interface Figure { + id: string; + name: string; + subject: string; + image_url: string; + theme?: string; + rarity?: string; + likes?: number; + user_id?: string; + character_info?: { + character?: { + description?: string; + }; + items?: Array<{ + name?: string; + description?: string; + }>; + }; +} + +export default function Home() { + const { theme, isDark } = useTheme(); + const { width } = useWindowDimensions(); + const [figures, setFigures] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Create a ref for the ScrollView + const scrollViewRef = useRef(null); + useScrollToTop(scrollViewRef); + + // Animation für das Settings-Icon + const scrollY = useRef(new Animated.Value(0)).current; + const settingsOpacity = scrollY.interpolate({ + inputRange: [0, 50], + outputRange: [1, 0], + extrapolate: 'clamp', + }); + const settingsTranslateY = scrollY.interpolate({ + inputRange: [0, 50], + outputRange: [0, -50], + extrapolate: 'clamp', + }); + + // Bestimme die Anzahl der Spalten basierend auf der Bildschirmbreite + const columns = useMemo(() => { + if (width >= 1200) return 3; // Drei Spalten für sehr breite Bildschirme (Desktop) + if (width >= 768) return 2; // Zwei Spalten für mittlere Bildschirme (Tablets) + return 1; // Eine Spalte für schmale Bildschirme (Smartphones) + }, [width]); + + // Lade öffentliche Figuren aus der Datenbank + useEffect(() => { + async function loadPublicFigures() { + setLoading(true); + try { + const data = await getPublicFigures(); + console.log('Public figures data:', data); + setFigures(data); + } catch (err: any) { + console.error('Fehler beim Laden der öffentlichen Figuren:', err); + setError(err?.message || 'Unbekannter Fehler'); + } finally { + setLoading(false); + } + } + + loadPublicFigures(); + }, []); + + return ( + <> + + + {/* Header mit App-Icon, Titel und Settings-Icon */} +
+ {error ? ( + + Fehler beim Laden der Figuren: {error} + + ) : figures.length === 0 && !loading ? ( + + Keine öffentlichen Figuren gefunden. + + ) : ( + + + + {loading + ? // Platzhalter-Karten während des Ladens anzeigen + Array.from({ length: 6 }).map((_, index) => ( + 1 ? 8 : 0, + }, + ]} + > + + + )) + : // Tatsächliche Figuren anzeigen, wenn sie geladen sind + figures.map((figure, index) => ( + 1 ? 8 : 0, + }, + ]} + > + + + ))} + + + + )} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + overflow: 'visible', // Erlaubt Elementen, über den Container hinauszuragen + }, + // Styles für Settings-Icon wurden in die Header-Komponente verschoben + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + loadingText: { + marginTop: 16, + fontSize: 16, + }, + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + errorText: { + fontSize: 16, + textAlign: 'center', + }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + emptyText: { + fontSize: 16, + textAlign: 'center', + }, + scrollContent: { + paddingTop: 100, // Abstand oben für den Header + paddingBottom: 20, // Abstand unten + paddingHorizontal: 0, // Horizontalen Abstand entfernt + overflow: 'visible', // Erlaubt Elementen, über den Container hinauszuragen + }, + cardsContainer: { + width: '100%', + alignItems: 'center', + overflow: 'visible', // Erlaubt Elementen, über den Container hinauszuragen + paddingHorizontal: 0, // Kein Padding für die äußeren Ränder + }, + cardsGrid: { + width: '100%', + flexWrap: 'wrap', + justifyContent: 'flex-start', + alignItems: 'flex-start', + }, + cardWrapper: { + marginBottom: 20, + }, + card: { + width: '100%', + borderRadius: 10, + overflow: 'hidden', + backgroundColor: 'rgba(255, 255, 255, 0.1)', + }, + imageContainer: { + width: '100%', + height: 300, + backgroundColor: 'rgba(0, 0, 0, 0.05)', + }, + image: { + width: '100%', + height: '100%', + }, + infoContainer: { + padding: 15, + }, + title: { + fontSize: 18, + fontWeight: 'bold', + marginBottom: 5, + }, + creator: { + fontSize: 14, + marginBottom: 5, + opacity: 0.7, + }, + likes: { + fontSize: 12, + opacity: 0.6, + }, +}); diff --git a/games/figgos/app/(tabs)/shelf.tsx b/games/figgos/app/(tabs)/shelf.tsx new file mode 100644 index 000000000..3935fe7b7 --- /dev/null +++ b/games/figgos/app/(tabs)/shelf.tsx @@ -0,0 +1,207 @@ +import { Stack } from 'expo-router'; +import { StyleSheet, View, ScrollView, ActivityIndicator, useWindowDimensions } from 'react-native'; +import React, { useRef } from 'react'; +import { useScrollToTop } from '@react-navigation/native'; +import { useTheme } from '~/utils/ThemeContext'; +import { ThemedView, ThemedText } from '~/components/ThemedView'; +import { useState, useEffect, useMemo } from 'react'; +import { useAuth } from '~/utils/AuthContext'; + +import { getUserFigures } from '~/utils/figureService'; +import { VerticalFigureCard } from '~/components/FigureCard'; + +// Definiere den Typ für eine Figur +interface Figure { + id: string; + name: string; + subject: string; + image_url: string; + theme?: string; + rarity?: string; + likes?: number; + user_id?: string; + character_info?: { + character?: { + description?: string; + lore?: string; + }; + items?: Array<{ + name?: string; + description?: string; + lore?: string; + }>; + }; +} + +export default function Shelf() { + const { theme, isDark } = useTheme(); + const { user } = useAuth(); + const { width } = useWindowDimensions(); + const [figures, setFigures] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Create a ref for the ScrollView + const scrollViewRef = useRef(null); + useScrollToTop(scrollViewRef); + + // Bestimme die Anzahl der Spalten basierend auf der Bildschirmbreite + const columns = useMemo(() => { + if (width >= 1200) return 3; // Drei Spalten für sehr breite Bildschirme (Desktop) + if (width >= 768) return 2; // Zwei Spalten für mittlere Bildschirme (Tablets) + return 1; // Eine Spalte für schmale Bildschirme (Smartphones) + }, [width]); + + // Lade die Figuren des Nutzers aus der Datenbank + useEffect(() => { + async function loadUserFigures() { + if (!user) { + setError('Du musst angemeldet sein, um deine Figuren zu sehen.'); + setLoading(false); + return; + } + + setLoading(true); + try { + const data = await getUserFigures(user.id); + console.log('User figures data:', data); + setFigures(data); + } catch (err: any) { + console.error('Fehler beim Laden der Figuren des Nutzers:', err); + setError(err?.message || 'Unbekannter Fehler'); + } finally { + setLoading(false); + } + } + + loadUserFigures(); + }, [user]); + + return ( + <> + + + {loading ? ( + + + Lade deine Figuren... + + ) : error ? ( + + + Fehler beim Laden deiner Figuren: {error} + + + ) : figures.length === 0 ? ( + + Du hast noch keine Figuren erstellt. + + ) : ( + + + + {figures.map((figure, index) => ( + 1 ? 8 : 0, + }, + ]} + > + + + ))} + + + + )} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + overflow: 'visible', // Erlaubt Elementen, über den Container hinauszuragen + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + loadingText: { + marginTop: 16, + fontSize: 16, + }, + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + errorText: { + fontSize: 16, + textAlign: 'center', + }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + emptyText: { + fontSize: 18, + textAlign: 'center', + }, + scrollContent: { + paddingTop: 100, // Abstand oben für den Header + paddingBottom: 20, // Abstand unten + paddingHorizontal: 0, // Horizontalen Abstand entfernt + overflow: 'visible', // Erlaubt Elementen, über den Container hinauszuragen + }, + cardsContainer: { + width: '100%', + alignItems: 'center', + overflow: 'visible', // Erlaubt Elementen, über den Container hinauszuragen + paddingHorizontal: 8, // Padding für die äußeren Ränder + }, + cardsGrid: { + width: '100%', + flexWrap: 'wrap', + justifyContent: 'flex-start', + alignItems: 'flex-start', + }, + cardWrapper: { + marginBottom: 20, + }, +}); diff --git a/games/figgos/app/+html.tsx b/games/figgos/app/+html.tsx new file mode 100644 index 000000000..447d6a0a5 --- /dev/null +++ b/games/figgos/app/+html.tsx @@ -0,0 +1,46 @@ +import { ScrollViewStyleReset } from 'expo-router/html'; + +// This file is web-only and used to configure the root HTML for every +// web page during static rendering. +// The contents of this function only run in Node.js environments and +// do not have access to the DOM or browser APIs. +export default function Root({ children }: { children: React.ReactNode }) { + return ( + + + + + + {/* + This viewport disables scaling which makes the mobile website act more like a native app. + However this does reduce built-in accessibility. If you want to enable scaling, use this instead: + + */} + + {/* + Disable body scrolling on web. This makes ScrollView components work closer to how they do on native. + However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line. + */} + + + {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */} +