diff --git a/apps/manadeck/apps/mobile/CLAUDE.md b/apps/manadeck/apps/mobile/CLAUDE.md index 7d6e01fe2..a1c05e0e9 100644 --- a/apps/manadeck/apps/mobile/CLAUDE.md +++ b/apps/manadeck/apps/mobile/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Manadeck is a React Native/Expo application using Expo Router for navigation, TypeScript for type safety, NativeWind (Tailwind CSS) for styling, and Supabase for backend services. The app uses Zustand for state management and is configured for cross-platform development (iOS, Android, and Web). +Manadeck is a React Native/Expo application using Expo Router for navigation, TypeScript for type safety, NativeWind (Tailwind CSS) for styling, and Mana Core Auth for authentication. The app uses Zustand for state management and connects to the NestJS backend (port 3009). ## Essential Commands @@ -43,7 +43,8 @@ npm run format # Auto-fix ESLint and format with Prettier - `modal.tsx`: Modal screens - **components/**: Reusable UI components - **store/**: Zustand state management stores -- **utils/**: Utility functions (e.g., Supabase client) +- **services/**: Auth service, API client +- **utils/**: Utility functions - **assets/**: Static assets (images, icons) ### Navigation Pattern @@ -66,11 +67,17 @@ Uses Expo Router v5 with file-based routing: - Create typed stores with actions and selectors - Access via hooks (e.g., `useStore`) +### Authentication + +- Mana Core Auth via `@manacore/shared-auth` in `services/authService.ts` +- Zustand auth store in `store/authStore.ts` +- Tokens stored in Expo SecureStore (encrypted) +- Supports email/password, Google, and Apple sign-in + ### Backend Integration -- Supabase client configured in `utils/supabase.ts` -- Uses AsyncStorage for session persistence -- Environment variables: `EXPO_PUBLIC_SUPABASE_URL` and `EXPO_PUBLIC_SUPABASE_ANON_KEY` +- NestJS backend at port 3009 (`@mana-core/nestjs-integration`) +- Environment variable: `EXPO_PUBLIC_MANA_CORE_AUTH_URL` ### TypeScript Configuration @@ -98,15 +105,6 @@ For new screens: 3. Configure navigation options if needed 4. Import and use existing UI components -### Supabase Integration - -When working with Supabase: - -1. Import client from `utils/supabase.ts` -2. Handle authentication state with AsyncStorage -3. Use environment variables for configuration -4. Follow RLS (Row Level Security) best practices - ### Code Style - ESLint configured with Expo preset @@ -121,8 +119,8 @@ When working with Supabase: Create a `.env` or `.env.local` file: ``` -EXPO_PUBLIC_SUPABASE_URL=your_supabase_url -EXPO_PUBLIC_SUPABASE_ANON_KEY=your_anon_key +EXPO_PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001 +EXPO_PUBLIC_BACKEND_URL=http://localhost:3009 ``` ### EAS Configuration diff --git a/apps/picture/apps/mobile/package.json b/apps/picture/apps/mobile/package.json index 7af1ce59e..cddd6ad13 100644 --- a/apps/picture/apps/mobile/package.json +++ b/apps/picture/apps/mobile/package.json @@ -20,6 +20,7 @@ "@callstack/liquid-glass": "^0.4.2", "@expo/vector-icons": "^15.0.2", "@manacore/shared-auth": "workspace:*", + "@manacore/shared-logger": "workspace:*", "@picture/design-tokens": "workspace:*", "@picture/shared": "workspace:*", "@react-native-async-storage/async-storage": "2.2.0", @@ -43,7 +44,7 @@ "expo-symbols": "^1.0.7", "expo-system-ui": "~6.0.7", "expo-web-browser": "~15.0.8", - "nativewind": "latest", + "nativewind": "^4.2.1", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.4", diff --git a/apps/presi/CLAUDE.md b/apps/presi/CLAUDE.md index 3218f9250..74bfb0276 100644 --- a/apps/presi/CLAUDE.md +++ b/apps/presi/CLAUDE.md @@ -6,9 +6,8 @@ apps/presi/ ├── apps/ │ ├── backend/ # NestJS API server (@presi/backend) -│ ├── mobile/ # Expo/React Native mobile app (@presi/mobile) │ ├── web/ # SvelteKit web application (@presi/web) -│ └── landing/ # Astro marketing landing page (@presi/landing) - TODO +│ └── landing/ # Astro marketing landing page (@presi/landing) ├── packages/ │ └── shared/ # Shared types and utils (@presi/shared) └── package.json @@ -20,7 +19,6 @@ apps/presi/ ```bash pnpm presi:dev # Run all presi apps -pnpm dev:presi:mobile # Start mobile app pnpm dev:presi:web # Start web app (port 5178) pnpm dev:presi:backend # Start backend server pnpm dev:presi:app # Start web + backend together @@ -29,14 +27,6 @@ pnpm presi:db:studio # Open Drizzle Studio pnpm presi:db:seed # Seed database with sample data ``` -### Mobile App (apps/presi/apps/mobile) - -```bash -pnpm dev # Start Expo dev server -pnpm ios # Run on iOS simulator -pnpm android # Run on Android emulator -``` - ### Web App (apps/presi/apps/web) ```bash @@ -59,7 +49,6 @@ pnpm db:seed # Seed database ## Technology Stack -- **Mobile**: React Native 0.76 + Expo SDK 52, Expo Router, Zustand - **Web**: SvelteKit 2.x, Svelte 5 (runes mode), Tailwind CSS - **Backend**: NestJS 10, Drizzle ORM, PostgreSQL - **Types**: TypeScript 5.x @@ -143,13 +132,6 @@ MANA_CORE_AUTH_URL=http://localhost:3001 CORS_ORIGINS=http://localhost:5173,http://localhost:8081 ``` -#### Mobile (.env) - -``` -EXPO_PUBLIC_BACKEND_URL=http://localhost:3008 -EXPO_PUBLIC_MANA_CORE_AUTH_URL=http://localhost:3001 -``` - #### Web (.env) ``` @@ -178,15 +160,14 @@ Located at `packages/shared/` ## Code Style Guidelines - **TypeScript**: Strict typing with interfaces -- **Mobile**: Functional components with hooks, Zustand for state - **Web**: Svelte 5 runes mode (`$state`, `$derived`, `$effect`) - **Backend**: NestJS modules with controllers and services -- **Styling**: Tailwind CSS (Web), NativeWind (Mobile) +- **Styling**: Tailwind CSS - **Formatting**: Prettier with project config ## Web App Features -The SvelteKit web app provides feature parity with the mobile app: +The SvelteKit web app provides the main user interface: - **Authentication**: Login/Register/Forgot Password with Mana Core Auth - **Deck Management**: Create, edit, delete presentation decks @@ -229,4 +210,4 @@ src/ 1. **Authentication**: Uses Mana Core Auth (JWT in Authorization header) 2. **Database**: PostgreSQL with Drizzle ORM 3. **Ports**: Backend=3008, Web=5178 -4. **Landing**: Not yet implemented (empty folder) +4. **Landing**: Deployed on Cloudflare Pages diff --git a/apps/presi/apps/mobile/app.json b/apps/presi/apps/mobile/app.json deleted file mode 100644 index 6c2ba6cc3..000000000 --- a/apps/presi/apps/mobile/app.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "expo": { - "name": "presi", - "slug": "presi", - "version": "1.0.0", - "orientation": "portrait", - "icon": "./assets/images/icon.png", - "scheme": "myapp", - "userInterfaceStyle": "automatic", - "newArchEnabled": true, - "ios": { - "supportsTablet": true - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "./assets/images/adaptive-icon.png", - "backgroundColor": "#ffffff" - } - }, - "web": { - "bundler": "metro", - "output": "static", - "favicon": "./assets/images/favicon.png" - }, - "plugins": [ - "expo-router", - [ - "expo-splash-screen", - { - "image": "./assets/images/splash-icon.png", - "imageWidth": 200, - "resizeMode": "contain", - "backgroundColor": "#ffffff" - } - ] - ], - "experiments": { - "typedRoutes": true - }, - "extra": { - "router": { - "origin": false - }, - "eas": { - "projectId": "83d42377-9b68-4b82-a049-565584d893ba" - } - } - } -} diff --git a/apps/presi/apps/mobile/app/(auth)/_layout.tsx b/apps/presi/apps/mobile/app/(auth)/_layout.tsx deleted file mode 100644 index 78115c959..000000000 --- a/apps/presi/apps/mobile/app/(auth)/_layout.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Stack } from 'expo-router'; - -export default function AuthLayout() { - return ( - - - - - - ); -} diff --git a/apps/presi/apps/mobile/app/(auth)/forgot-password.tsx b/apps/presi/apps/mobile/app/(auth)/forgot-password.tsx deleted file mode 100644 index 5ec6dc0af..000000000 --- a/apps/presi/apps/mobile/app/(auth)/forgot-password.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React, { useState } from 'react'; -import { - View, - Text, - TextInput, - TouchableOpacity, - StyleSheet, - Alert, - KeyboardAvoidingView, - Platform, -} from 'react-native'; -import { Link, useRouter } from 'expo-router'; -import { resetPassword } from '../../services/auth'; - -export default function ForgotPasswordScreen() { - const [email, setEmail] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [resetSent, setResetSent] = useState(false); - const router = useRouter(); - - const handleResetPassword = async () => { - if (!email) { - Alert.alert('Error', 'Please enter your email address'); - return; - } - - setIsLoading(true); - try { - await resetPassword(email); - setResetSent(true); - } catch (error: any) { - console.error('Password reset error:', error); - Alert.alert('Reset Failed', error.message || 'An error occurred while sending reset email'); - } finally { - setIsLoading(false); - } - }; - - if (resetSent) { - return ( - - - Check Your Email - We've sent password reset instructions to {email} - router.replace('/login')}> - Return to Login - - - - ); - } - - return ( - - - Reset Password - Enter your email to receive reset instructions - - - - - - {isLoading ? 'Sending...' : 'Send Reset Instructions'} - - - - - - Back to Login - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#ffffff', - }, - formContainer: { - flex: 1, - padding: 20, - justifyContent: 'center', - maxWidth: 400, - width: '100%', - alignSelf: 'center', - }, - title: { - fontSize: 32, - fontWeight: 'bold', - marginBottom: 8, - color: '#1a1a1a', - textAlign: 'center', - }, - subtitle: { - fontSize: 16, - color: '#666666', - marginBottom: 32, - textAlign: 'center', - }, - message: { - fontSize: 16, - color: '#666666', - marginBottom: 32, - textAlign: 'center', - lineHeight: 24, - }, - input: { - borderWidth: 1, - borderColor: '#dddddd', - borderRadius: 8, - padding: 12, - marginBottom: 16, - fontSize: 16, - backgroundColor: '#f8f8f8', - }, - button: { - backgroundColor: '#007AFF', - padding: 16, - borderRadius: 8, - marginTop: 16, - }, - buttonDisabled: { - backgroundColor: '#cccccc', - }, - buttonText: { - color: '#ffffff', - textAlign: 'center', - fontSize: 16, - fontWeight: '600', - }, - links: { - marginTop: 24, - alignItems: 'center', - }, - link: { - color: '#007AFF', - fontSize: 16, - }, -}); diff --git a/apps/presi/apps/mobile/app/(auth)/login.tsx b/apps/presi/apps/mobile/app/(auth)/login.tsx deleted file mode 100644 index 0628ac1af..000000000 --- a/apps/presi/apps/mobile/app/(auth)/login.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import React, { useState } from 'react'; -import { - View, - Text, - TextInput, - TouchableOpacity, - StyleSheet, - Alert, - KeyboardAvoidingView, - Platform, -} from 'react-native'; -import { useRouter, Link } from 'expo-router'; -import { loginUser } from '../../services/auth'; - -export default function LoginScreen() { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const router = useRouter(); - - const handleLogin = async () => { - if (!email || !password) { - Alert.alert('Error', 'Please fill in all fields'); - return; - } - - setIsLoading(true); - try { - await loginUser(email, password); - router.replace('/'); - } catch (error: any) { - console.error('Login error:', error); - Alert.alert('Login Failed', error.message || 'Please check your credentials and try again'); - } finally { - setIsLoading(false); - } - }; - - return ( - - - Welcome Back - Sign in to continue - - - - - - - {isLoading ? 'Signing in...' : 'Sign In'} - - - - - Forgot Password? - - - Create Account - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#ffffff', - }, - formContainer: { - flex: 1, - padding: 20, - justifyContent: 'center', - maxWidth: 400, - width: '100%', - alignSelf: 'center', - }, - title: { - fontSize: 32, - fontWeight: 'bold', - marginBottom: 8, - color: '#1a1a1a', - textAlign: 'center', - }, - subtitle: { - fontSize: 16, - color: '#666666', - marginBottom: 32, - textAlign: 'center', - }, - input: { - borderWidth: 1, - borderColor: '#dddddd', - borderRadius: 8, - padding: 12, - marginBottom: 16, - fontSize: 16, - backgroundColor: '#f8f8f8', - }, - button: { - backgroundColor: '#007AFF', - padding: 16, - borderRadius: 8, - marginTop: 16, - }, - buttonDisabled: { - backgroundColor: '#cccccc', - }, - buttonText: { - color: '#ffffff', - textAlign: 'center', - fontSize: 16, - fontWeight: '600', - }, - links: { - flexDirection: 'row', - justifyContent: 'space-between', - marginTop: 24, - }, - link: { - color: '#007AFF', - fontSize: 16, - }, -}); diff --git a/apps/presi/apps/mobile/app/(auth)/register.tsx b/apps/presi/apps/mobile/app/(auth)/register.tsx deleted file mode 100644 index c7fff9ecc..000000000 --- a/apps/presi/apps/mobile/app/(auth)/register.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import React, { useState } from 'react'; -import { - View, - Text, - TextInput, - TouchableOpacity, - StyleSheet, - Alert, - KeyboardAvoidingView, - Platform, -} from 'react-native'; -import { useRouter, Link } from 'expo-router'; -import { registerUser } from '../../services/auth'; - -export default function RegisterScreen() { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const router = useRouter(); - - const handleRegister = async () => { - if (!email || !password || !confirmPassword) { - Alert.alert('Error', 'Please fill in all fields'); - return; - } - - if (password !== confirmPassword) { - Alert.alert('Error', 'Passwords do not match'); - return; - } - - if (password.length < 6) { - Alert.alert('Error', 'Password should be at least 6 characters long'); - return; - } - - setIsLoading(true); - try { - await registerUser(email, password); - router.replace('/'); - } catch (error: any) { - console.error('Registration error:', error); - Alert.alert('Registration Failed', error.message || 'An error occurred during registration'); - } finally { - setIsLoading(false); - } - }; - - return ( - - - Create Account - Sign up to get started - - - - - - - - - - {isLoading ? 'Creating Account...' : 'Create Account'} - - - - - - Already have an account? Sign In - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#ffffff', - }, - formContainer: { - flex: 1, - padding: 20, - justifyContent: 'center', - maxWidth: 400, - width: '100%', - alignSelf: 'center', - }, - title: { - fontSize: 32, - fontWeight: 'bold', - marginBottom: 8, - color: '#1a1a1a', - textAlign: 'center', - }, - subtitle: { - fontSize: 16, - color: '#666666', - marginBottom: 32, - textAlign: 'center', - }, - input: { - borderWidth: 1, - borderColor: '#dddddd', - borderRadius: 8, - padding: 12, - marginBottom: 16, - fontSize: 16, - backgroundColor: '#f8f8f8', - }, - button: { - backgroundColor: '#007AFF', - padding: 16, - borderRadius: 8, - marginTop: 16, - }, - buttonDisabled: { - backgroundColor: '#cccccc', - }, - buttonText: { - color: '#ffffff', - textAlign: 'center', - fontSize: 16, - fontWeight: '600', - }, - links: { - marginTop: 24, - alignItems: 'center', - }, - link: { - color: '#007AFF', - fontSize: 16, - }, -}); diff --git a/apps/presi/apps/mobile/app/_layout.tsx b/apps/presi/apps/mobile/app/_layout.tsx deleted file mode 100644 index 850190508..000000000 --- a/apps/presi/apps/mobile/app/_layout.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { useEffect } from 'react'; -import { Stack } from 'expo-router'; -import { useSegments, useRouter } from 'expo-router'; -import { View, TouchableOpacity, Text } from 'react-native'; -import { MaterialIcons } from '@expo/vector-icons'; -import { onAuthStateChange } from '../services/auth'; -import { ThemeProvider, useTheme } from '../components/ThemeProvider'; -import { Header } from '../components/Menu/Header'; - -function StackNavigator() { - const router = useRouter(); - const { theme } = useTheme(); - - return ( - - { - let title = options.title || ''; - let showAddDeck = false; - const rightContent = options.headerRight?.({}); - - if (route.name === 'index') { - title = `My Decks (${route.params?.deckCount || 0})`; - showAddDeck = true; - } - - return
; - }, - }} - > - - - - - - - - - - - - ); -} - -function RootLayoutContent() { - const { theme } = useTheme(); - - return ( - - - - ); -} - -export default function RootLayout() { - const segments = useSegments(); - const router = useRouter(); - - useEffect(() => { - const unsubscribe = onAuthStateChange((user) => { - if (!user && !segments.includes('(auth)')) { - router.replace('/login'); - } - }); - return () => { - unsubscribe(); - }; - }, [segments]); - - return ( - - - - ); -} diff --git a/apps/presi/apps/mobile/app/deck/[id].tsx b/apps/presi/apps/mobile/app/deck/[id].tsx deleted file mode 100644 index 84962f245..000000000 --- a/apps/presi/apps/mobile/app/deck/[id].tsx +++ /dev/null @@ -1,399 +0,0 @@ -import React, { useEffect, useState, useCallback } from 'react'; -import { - View, - TouchableOpacity, - Text, - Modal, - SafeAreaView, - Alert, - Platform, - ScrollView, -} from 'react-native'; -import { useLocalSearchParams, useRouter } from 'expo-router'; -import { getDeckSlides, getDeck, deleteSlide, reorderSlide } from '../../services/firestore'; -import { Slide } from '../../types/models'; -import type { Deck } from '../../types/models'; -import { SlideList } from '../../components/slides/SlideList'; -import { SlideEditor } from '../../components/slides/SlideEditor'; -import { PresentationMode } from '../../components/presentation/PresentationMode'; -import MaterialIcons from '@expo/vector-icons/MaterialIcons'; -import { useTheme } from '../../components/ThemeProvider'; -import { Header } from '../../components/Menu/Header'; - -export default function DeckScreen() { - const { theme } = useTheme(); - const { id } = useLocalSearchParams(); - const router = useRouter(); - const [slides, setSlides] = useState([]); - const [deck, setDeck] = useState(null); - const [loading, setLoading] = useState(true); - const [isCreateModalVisible, setIsCreateModalVisible] = useState(false); - const [editingSlide, setEditingSlide] = useState(null); - const [isPresentationMode, setIsPresentationMode] = useState(false); - const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); - - const loadDeckAndSlides = async () => { - try { - const [deckData, deckSlides] = await Promise.all([ - getDeck(id as string), - getDeckSlides(id as string), - ]); - console.log( - '[DeckScreen] Loaded slides:', - deckSlides.map((s) => ({ - id: s.id.substring(0, 4), - order: s.order, - updatedAt: s.updatedAt, - })) - ); - setDeck(deckData); - const sortedSlides = deckSlides.sort((a, b) => a.order - b.order); - console.log( - '[DeckScreen] Sorted slides:', - sortedSlides.map((s) => ({ - id: s.id.substring(0, 4), - order: s.order, - updatedAt: s.updatedAt, - })) - ); - setSlides(sortedSlides); - } catch (error) { - console.error('Error loading deck data:', error); - } finally { - setLoading(false); - } - }; - - const handleStartPresentation = useCallback(() => { - if (slides.length > 0) { - setIsPresentationMode(true); - } - }, [slides.length]); - - const handleDeckDelete = useCallback(() => { - setIsDeleteModalVisible(true); - }, []); - - const handleEditSlide = useCallback((slide: Slide) => { - setEditingSlide(slide); - setIsCreateModalVisible(true); - }, []); - - const handleDeleteDeck = async () => { - if (!id) return; - - try { - setLoading(true); - // await deleteDeck(id as string, setId as string); - router.back(); - } catch (error) { - console.error('[DeckScreen] Error deleting deck:', error); - // setError('Failed to delete deck'); - } finally { - setLoading(false); - setIsDeleteModalVisible(false); - } - }; - - const handleDeleteSlide = useCallback( - async (slide: Slide) => { - try { - await deleteSlide(slide.id, id as string); - await loadDeckAndSlides(); - } catch (error) { - console.error('[DeckScreen] Error deleting slide:', error); - Alert.alert('Error', 'Failed to delete slide'); - } - }, - [loadDeckAndSlides, id] - ); - - const handleMoveSlide = useCallback( - async (slide: Slide, direction: 'up' | 'down') => { - console.log('[DeckScreen] Starting handleMoveSlide:', { - slideId: slide.id.substring(0, 4), - direction, - currentOrder: slide.order, - }); - const currentIndex = slides.findIndex((s) => s.id === slide.id); - console.log( - '[DeckScreen] Current slide index:', - currentIndex, - 'Total slides:', - slides.length - ); - console.log( - '[DeckScreen] All slides:', - slides.map((s) => ({ - id: s.id.substring(0, 4), - order: s.order, - })) - ); - if (currentIndex === -1) { - console.log('[DeckScreen] Slide not found in array'); - return; - } - - // Normalize all orders to be integers between 1 and slides.length - const normalizedSlides = [...slides].sort((a, b) => a.order - b.order); - const normalizedOrders = new Map(normalizedSlides.map((s, i) => [s.id, i + 1])); - - let newOrder; - if (direction === 'up' && currentIndex > 0) { - // Moving up: use the previous slide's normalized order - const prevOrder = normalizedOrders.get(slides[currentIndex - 1].id) || 1; - const currOrder = normalizedOrders.get(slide.id) || 2; - newOrder = prevOrder + (currOrder - prevOrder) / 2; - - console.log('[DeckScreen] Moving up - New order:', { - newOrder, - previousSlideId: slides[currentIndex - 1].id.substring(0, 4), - previousOrder: prevOrder, - currentSlideId: slides[currentIndex].id.substring(0, 4), - currentOrder: currOrder, - }); - } else if (direction === 'down' && currentIndex < slides.length - 1) { - // Moving down: use the next slide's normalized order - const currOrder = normalizedOrders.get(slide.id) || 1; - const nextOrder = normalizedOrders.get(slides[currentIndex + 1].id) || 2; - newOrder = currOrder + (nextOrder - currOrder) / 2; - - console.log('[DeckScreen] Moving down - New order:', { - newOrder, - nextSlideId: slides[currentIndex + 1].id.substring(0, 4), - nextOrder: nextOrder, - currentSlideId: slides[currentIndex].id.substring(0, 4), - currentOrder: currOrder, - }); - } else { - console.log('[DeckScreen] Cannot move slide:', { - direction, - currentIndex, - slidesLength: slides.length, - }); - return; - } - - try { - console.log('[DeckScreen] Calling reorderSlide with:', { - slideId: slide.id.substring(0, 4), - newOrder, - deckId: id, - }); - await reorderSlide(slide.id, newOrder, id as string); - console.log('[DeckScreen] Reorder successful, reloading slides'); - await loadDeckAndSlides(); - } catch (error) { - console.error('[DeckScreen] Error moving slide:', error); - Alert.alert('Error', 'Failed to move slide'); - } - }, - [slides, loadDeckAndSlides, id] - ); - - useEffect(() => { - loadDeckAndSlides(); - }, [id]); - - useEffect(() => { - if (deck) { - router.setParams({ - deckName: deck.name, - slideCount: slides.length, - onStartPresentation: handleStartPresentation, - onDeleteDeck: handleDeckDelete, - }); - } - }, [deck, slides, handleStartPresentation, handleDeckDelete]); - - if (loading) { - return ( - - - Loading slides... - - - ); - } - - return ( - - - - -
- - { - setIsCreateModalVisible(false); - setEditingSlide(null); - }} - > - - - - - {editingSlide ? 'Edit Slide' : 'Create New Slide'} - - { - setIsCreateModalVisible(false); - setEditingSlide(null); - }} - > - - - - - - { - setIsCreateModalVisible(false); - setEditingSlide(null); - loadDeckAndSlides(); - }} - onCancel={() => { - setIsCreateModalVisible(false); - setEditingSlide(null); - }} - /> - - - - - - - setIsDeleteModalVisible(false)} - > - - - - Delete Slide - - - Are you sure you want to delete this slide? This action cannot be undone. - - - { - setIsDeleteModalVisible(false); - setEditingSlide(null); - }} - > - - Cancel - - - - - Delete - - - - - - - - setIsPresentationMode(false)} - statusBarTranslucent - > - - setIsPresentationMode(false)} /> - - - - - ); -} diff --git a/apps/presi/apps/mobile/app/index.tsx b/apps/presi/apps/mobile/app/index.tsx deleted file mode 100644 index 461dd6133..000000000 --- a/apps/presi/apps/mobile/app/index.tsx +++ /dev/null @@ -1,418 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { View, Text, StyleSheet, TouchableOpacity, Modal, Platform, Image } from 'react-native'; -import { useRouter } from 'expo-router'; -import { - getUserDecks, - deleteDeck, - getDeckSlides, - migrateDecksToNewSchema, -} from '../services/firestore'; -import { Deck, Slide } from '../types/models'; -import { DeckList } from '../components/decks/DeckList'; -import { CreateDeckForm } from '../components/forms/CreateDeckForm'; -import { DeckShareSettings } from '../components/decks/DeckShareSettings'; -import { auth } from '../firebaseConfig'; -import { onAuthStateChanged } from 'firebase/auth'; -import type { User } from 'firebase/auth'; -import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; -import { ThemeProvider } from '../components/ThemeProvider'; -import { useTheme } from '../components/ThemeProvider'; -import { db } from '../firebaseConfig'; -import { updateDoc, doc } from 'firebase/firestore'; -import { ThemeVariant, THEME_PATTERNS, THEME_NAMES } from '../constants/theme'; -import { CreateItemButton } from '../components/common/CreateItemButton'; - -function App() { - const router = useRouter(); - const { theme, themeVariant, colorMode } = useTheme(); - const [decks, setDecks] = useState([]); - const [loading, setLoading] = useState(true); - const [isCreateModalVisible, setIsCreateModalVisible] = useState(false); - const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); - const [isShareModalVisible, setIsShareModalVisible] = useState(false); - const [deckToDelete, setDeckToDelete] = useState(null); - const [deckToShare, setDeckToShare] = useState(null); - const [user, setUser] = useState(null); - const [error, setError] = useState(null); - const [decksWithFirstSlide, setDecksWithFirstSlide] = useState<{ [key: string]: string }>({}); - const [slideCounts, setSlideCounts] = useState<{ [key: string]: number }>({}); - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - - // Listen for the create deck modal event - const handleOpenCreateModal = () => { - setIsCreateModalVisible(true); - }; - window.addEventListener('openCreateDeckModal', handleOpenCreateModal); - - return () => { - setMounted(false); - window.removeEventListener('openCreateDeckModal', handleOpenCreateModal); - }; - }, []); - - useEffect(() => { - if (mounted && decks) { - router.setParams({ deckCount: decks.length }); - } - }, [decks, mounted]); - - useEffect(() => { - console.log('[App] Setting up auth state listener'); - let mounted = true; - - const loadDecksForUser = async (currentUser: User) => { - if (!currentUser || !mounted) return; - - try { - setLoading(true); - setError(null); - - // Migrate existing decks to new schema - await migrateDecksToNewSchema(currentUser.uid); - - const userDecks = await getUserDecks(currentUser.uid); - if (mounted) { - setDecks(userDecks); - - // Load first slide for each deck - const firstSlides: { [key: string]: string } = {}; - const counts: { [key: string]: number } = {}; - - for (const deck of userDecks) { - try { - const slides = await getDeckSlides(deck.id); - if (slides.length > 0) { - firstSlides[deck.id] = slides[0].imageUrl || ''; - } - counts[deck.id] = slides.length; - } catch (error) { - console.error('[Firestore] Error getting slides:', error); - } - } - - if (mounted) { - setDecksWithFirstSlide(firstSlides); - setSlideCounts(counts); - } - } - } catch (error) { - console.error('[App] Error loading decks:', error); - if (mounted) { - setError('Failed to load decks'); - } - } finally { - if (mounted) { - setLoading(false); - } - } - }; - - const unsubscribe = onAuthStateChanged(auth, async (currentUser) => { - console.log('[App] Auth state changed:', currentUser?.email); - - if (!mounted) return; - - if (currentUser) { - setUser(currentUser); - await loadDecksForUser(currentUser); - } else { - setUser(null); - setDecks([]); - } - }); - - return () => { - mounted = false; - unsubscribe(); - }; - }, []); - - const handleDeckPress = (deck: Deck) => { - router.push(`/deck/${deck.id}`); - }; - - const handleDeckDelete = (deck: Deck) => { - setDeckToDelete(deck); - setIsDeleteModalVisible(true); - }; - - const handleShareDeck = (deck: Deck) => { - setDeckToShare(deck); - setIsShareModalVisible(true); - }; - - const confirmDelete = async () => { - if (!deckToDelete) return; - - try { - setLoading(true); - await deleteDeck(deckToDelete.id); - const updatedDecks = decks.filter((deck) => deck.id !== deckToDelete.id); - setDecks(updatedDecks); - } catch (error) { - console.error('[App] Error deleting deck:', error); - setError('Failed to delete deck'); - } finally { - setLoading(false); - setIsDeleteModalVisible(false); - setDeckToDelete(null); - } - }; - - return ( - - - { - router.push(`/deck/${deck.id}`); - }} - onCreateDeck={() => setIsCreateModalVisible(true)} - onDeleteDeck={(deck) => { - setDeckToDelete(deck); - setIsDeleteModalVisible(true); - }} - onShareDeck={(deck) => { - setDeckToShare(deck); - setIsShareModalVisible(true); - }} - firstSlideImages={decksWithFirstSlide} - loading={loading} - slideCounts={slideCounts} - onProfilePress={() => router.push('/profile')} - onSettingsPress={() => router.push('/settings')} - scrollPadding={{ - vertical: { top: 20, bottom: 80 }, - horizontal: { top: 200, bottom: 200 }, - }} - deckSpacing={{ vertical: 24, horizontal: 40 }} - headerRight={() => ( - - setIsCreateModalVisible(true)} - variant="button" - title="Create New Deck" - buttonText="Create New Deck" - /> - - router.push('/profile')} - style={[styles.iconButton, { backgroundColor: theme.colors.backgroundSecondary }]} - > - - - router.push('/settings')} - style={[styles.iconButton, { backgroundColor: theme.colors.backgroundSecondary }]} - > - - - - - )} - /> - - {/* Create Deck Modal */} - setIsCreateModalVisible(false)} - animationType="fade" - transparent - > - - - { - setIsCreateModalVisible(false); - setDecks([newDeck, ...decks]); - router.push(`/deck/${newDeck.id}`); - }} - onCancel={() => setIsCreateModalVisible(false)} - /> - - - - - {/* Delete Deck Modal */} - { - setIsDeleteModalVisible(false); - setDeckToDelete(null); - }} - transparent - animationType="fade" - > - - - - Delete Deck - - - Are you sure you want to delete "{deckToDelete?.name}"? This action cannot be - undone. - - - { - setIsDeleteModalVisible(false); - setDeckToDelete(null); - }} - > - - Cancel - - - - Delete - - - - - - - {/* Share Deck Modal */} - setIsShareModalVisible(false)} - transparent - animationType="fade" - > - - - { - if (!deckToShare) return; - try { - await updateDoc(doc(db, 'decks', deckToShare.id), { sharing }); - // Refresh decks list - if (user) { - const updatedDecks = await getUserDecks(user.uid); - setDecks(updatedDecks); - } - setIsShareModalVisible(false); - } catch (error) { - console.error('Error updating sharing settings:', error); - } - }} - onClose={() => setIsShareModalVisible(false)} - /> - - - - - {error && ( - - {error} - - )} - - - ); -} - -export default function IndexPage() { - return ( - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - content: { - flex: 1, - }, - modalOverlay: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 16, - }, - modalContainer: { - width: '100%', - maxWidth: 600, - borderRadius: 12, - overflow: 'hidden', - }, - modalContent: { - padding: 20, - borderRadius: 8, - width: '80%', - maxWidth: 500, - }, - modalTitle: { - fontSize: 20, - fontWeight: 'bold', - marginBottom: 10, - }, - modalText: { - fontSize: 16, - marginBottom: 20, - }, - modalButtons: { - flexDirection: 'row', - justifyContent: 'space-between', - width: '100%', - }, - modalButton: { - flex: 1, - padding: 12, - borderRadius: 8, - marginHorizontal: 8, - }, - buttonText: { - textAlign: 'center', - fontSize: 16, - fontWeight: '600', - }, - deleteButtonText: { - color: '#FFFFFF', - }, - errorContainer: { - position: 'absolute', - bottom: 20, - left: 20, - right: 20, - padding: 10, - borderRadius: 8, - backgroundColor: '#FFFFFF', - shadowColor: '#000000', - shadowOpacity: 0.2, - shadowRadius: 5, - elevation: 5, - }, - errorText: { - fontSize: 16, - fontWeight: '600', - }, - headerActions: { - flexDirection: 'row', - gap: 10, - }, - iconButton: { - padding: 10, - borderRadius: 8, - }, -}); diff --git a/apps/presi/apps/mobile/app/profile.tsx b/apps/presi/apps/mobile/app/profile.tsx deleted file mode 100644 index 257cbcd36..000000000 --- a/apps/presi/apps/mobile/app/profile.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { View, Text, StyleSheet } from 'react-native'; -import { useTheme } from '../components/ThemeProvider'; -import { getUserDecks, getDeckSlides } from '../services/firestore'; -import { auth } from '../firebaseConfig'; -import MaterialIcons from '@expo/vector-icons/MaterialIcons'; - -interface UserStats { - totalDecks: number; - totalSlides: number; -} - -export default function ProfileScreen() { - const { theme } = useTheme(); - const [stats, setStats] = useState({ totalDecks: 0, totalSlides: 0 }); - const [loading, setLoading] = useState(true); - - useEffect(() => { - async function loadStats() { - if (!auth.currentUser) return; - - try { - setLoading(true); - const decks = await getUserDecks(auth.currentUser.uid); - let totalSlides = 0; - - // Get slides for each deck - for (const deck of decks) { - const slides = await getDeckSlides(deck.id); - totalSlides += slides.length; - } - - setStats({ - totalDecks: decks.length, - totalSlides: totalSlides, - }); - } catch (error) { - console.error('Error loading user stats:', error); - } finally { - setLoading(false); - } - } - - loadStats(); - }, []); - - if (loading) { - return ( - - - Loading stats... - - - ); - } - - return ( - - - - - - - {auth.currentUser?.email || 'User'} - - - - - - - - {stats.totalDecks} - - Total Decks - - - - - - - - {stats.totalSlides} - - - Total Slides - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - padding: 16, - }, - header: { - padding: 24, - borderRadius: 12, - alignItems: 'center', - marginBottom: 16, - }, - avatarContainer: { - marginBottom: 16, - }, - userName: { - fontSize: 24, - fontWeight: '600', - }, - statsContainer: { - flexDirection: 'row', - padding: 24, - borderRadius: 12, - justifyContent: 'space-around', - alignItems: 'center', - }, - statItem: { - alignItems: 'center', - }, - statValue: { - fontSize: 32, - fontWeight: '700', - marginTop: 8, - }, - statLabel: { - fontSize: 14, - marginTop: 4, - }, - divider: { - width: 1, - height: '80%', - }, - loadingText: { - fontSize: 16, - textAlign: 'center', - }, -}); diff --git a/apps/presi/apps/mobile/app/settings.tsx b/apps/presi/apps/mobile/app/settings.tsx deleted file mode 100644 index c71e1886e..000000000 --- a/apps/presi/apps/mobile/app/settings.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React from 'react'; -import { - View, - Text, - TouchableOpacity, - StyleSheet, - ScrollView, - Pressable, - Image, -} from 'react-native'; -import { useRouter } from 'expo-router'; -import { getCurrentUser, logoutUser } from '../services/auth'; -import { useTheme } from '../components/ThemeProvider'; -import MaterialIcons from '@expo/vector-icons/MaterialIcons'; -import { ThemeVariant, getTheme, THEME_PATTERNS, THEME_NAMES } from '../constants/theme'; -import { ThemeSettings } from '../components/common/ThemeSettings'; - -const COLOR_MODES: { label: string; value: ColorMode }[] = [ - { label: 'System', value: 'system' }, - { label: 'Hell', value: 'light' }, - { label: 'Dunkel', value: 'dark' }, -]; - -const CONTRAST_LABELS: Record = { - 1: 'Sehr niedrig', - 2: 'Niedrig', - 3: 'Standard', - 4: 'Hoch', - 5: 'Sehr hoch', -}; - -export default function SettingsScreen() { - const router = useRouter(); - const currentUser = getCurrentUser(); - const { theme, isDark } = useTheme(); - - const handleLogout = async () => { - try { - await logoutUser(); - router.replace('/login'); - } catch (error) { - console.error('Logout error:', error); - } - }; - - return ( - - - - - Kontrast: - - - {([1, 2, 3, 4, 5] as ContrastLevel[]).map((level) => ( - {}} - /> - ))} - - - {CONTRAST_LABELS[3]} - - - - - - - Email - - {currentUser?.email} - - - - - - Sign Out - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - content: { - flex: 1, - padding: 20, - }, - section: { - marginBottom: 32, - padding: 16, - borderRadius: 8, - }, - sectionTitleContainer: { - alignItems: 'center', - marginBottom: 24, - }, - sectionTitle: { - fontSize: 20, - fontWeight: '600', - }, - label: { - fontSize: 14, - marginBottom: 4, - }, - value: { - fontSize: 16, - fontWeight: '500', - }, - contrastContainer: { - marginTop: 16, - alignItems: 'center', - gap: 12, - }, - contrastSlider: { - flexDirection: 'row', - gap: 4, - height: 48, - alignItems: 'center', - width: '100%', - }, - contrastOption: { - flex: 1, - height: 32, - borderRadius: 16, - }, - contrastLabel: { - fontSize: 14, - fontWeight: '500', - }, - bottomSection: { - gap: 12, - marginTop: 'auto', - }, - emailSection: { - padding: 16, - borderRadius: 8, - }, - logoutButton: { - padding: 16, - borderRadius: 8, - alignItems: 'center', - }, - logoutButtonText: { - fontSize: 16, - fontWeight: '600', - }, -}); diff --git a/apps/presi/apps/mobile/app/shared/[id].tsx b/apps/presi/apps/mobile/app/shared/[id].tsx deleted file mode 100644 index a6dafc5f8..000000000 --- a/apps/presi/apps/mobile/app/shared/[id].tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useLocalSearchParams } from 'expo-router'; -import React, { useEffect, useState } from 'react'; -import { View, Text, StyleSheet } from 'react-native'; -import { useTheme } from '../../components/ThemeProvider'; -import { Deck } from '../../types/models'; -import { doc, getDoc } from 'firebase/firestore'; -import { db } from '../../firebaseConfig'; -import { SlideList } from '../../components/slides/SlideList'; - -export default function SharedDeckView() { - const { id } = useLocalSearchParams(); - const { theme } = useTheme(); - const [deck, setDeck] = useState(null); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const fetchDeck = async () => { - try { - const deckDoc = await getDoc(doc(db, 'decks', id as string)); - if (!deckDoc.exists()) { - setError('Deck not found'); - return; - } - - const deckData = deckDoc.data() as Deck; - if (!deckData.sharing.isPublic) { - setError('This deck is not publicly accessible'); - return; - } - - setDeck(deckData); - } catch (err) { - setError('Failed to load deck'); - console.error(err); - } finally { - setLoading(false); - } - }; - - fetchDeck(); - }, [id]); - - if (loading) { - return ( - - Loading... - - ); - } - - if (error) { - return ( - - {error} - - ); - } - - if (!deck) { - return null; - } - - return ( - - {deck.name} - {deck.description && ( - - {deck.description} - - )} - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - padding: 16, - }, - text: { - fontSize: 16, - }, - title: { - fontSize: 24, - fontWeight: 'bold', - marginBottom: 8, - }, - description: { - fontSize: 16, - marginBottom: 24, - }, -}); diff --git a/apps/presi/apps/mobile/app/themes.tsx b/apps/presi/apps/mobile/app/themes.tsx deleted file mode 100644 index a4099c179..000000000 --- a/apps/presi/apps/mobile/app/themes.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import React from 'react'; -import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Image } from 'react-native'; -import { useRouter } from 'expo-router'; -import { useTheme } from '../components/ThemeProvider'; -import type { ThemeVariant } from '../components/ThemeProvider'; -import MaterialIcons from '@expo/vector-icons/MaterialIcons'; - -const THEME_PATTERNS: Record = { - lume: require('../assets/images/patterns/memo-theme-tile.png'), - nature: require('../assets/images/patterns/nature-theme-tile.png'), - stone: require('../assets/images/patterns/stone-theme-tile.png'), -}; - -const THEME_NAMES: Record = { - lume: 'Lume', - nature: 'Nature', - stone: 'Stone', -}; - -export default function ThemesScreen() { - const router = useRouter(); - const { theme, themeVariant, setThemeVariant } = useTheme(); - - return ( - - - router.back()}> - - - Designs - - - - - {(Object.keys(THEME_NAMES) as ThemeVariant[]).map((variant) => { - const isSelected = variant === themeVariant; - - return ( - setThemeVariant(variant)} - > - {THEME_PATTERNS[variant] && ( - - - {[...Array(2)].map((_, i) => ( - - ))} - - - )} - - {THEME_NAMES[variant]} - - - ); - })} - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - header: { - flexDirection: 'row', - alignItems: 'center', - padding: 16, - paddingTop: 60, - }, - backButton: { - marginRight: 16, - }, - title: { - fontSize: 20, - fontWeight: '600', - }, - content: { - flex: 1, - padding: 16, - }, - themeGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: 16, - }, - themeCard: { - width: '100%', - aspectRatio: 2, - borderRadius: 12, - padding: 16, - justifyContent: 'flex-end', - overflow: 'hidden', - }, - patternContainer: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - flexDirection: 'row', - alignItems: 'center', - }, - patternTile: { - width: '50%', - height: '100%', - }, - themeName: { - fontSize: 18, - fontWeight: '600', - }, -}); diff --git a/apps/presi/apps/mobile/assets/fonts/SpaceMono-Regular.ttf b/apps/presi/apps/mobile/assets/fonts/SpaceMono-Regular.ttf deleted file mode 100755 index 28d7ff717..000000000 Binary files a/apps/presi/apps/mobile/assets/fonts/SpaceMono-Regular.ttf and /dev/null differ diff --git a/apps/presi/apps/mobile/assets/images/adaptive-icon.png b/apps/presi/apps/mobile/assets/images/adaptive-icon.png deleted file mode 100644 index 03d6f6b6c..000000000 Binary files a/apps/presi/apps/mobile/assets/images/adaptive-icon.png and /dev/null differ diff --git a/apps/presi/apps/mobile/assets/images/favicon.png b/apps/presi/apps/mobile/assets/images/favicon.png deleted file mode 100644 index e75f697b1..000000000 Binary files a/apps/presi/apps/mobile/assets/images/favicon.png and /dev/null differ diff --git a/apps/presi/apps/mobile/assets/images/icon.png b/apps/presi/apps/mobile/assets/images/icon.png deleted file mode 100644 index c19868f6e..000000000 Binary files a/apps/presi/apps/mobile/assets/images/icon.png and /dev/null differ diff --git a/apps/presi/apps/mobile/assets/images/partial-react-logo.png b/apps/presi/apps/mobile/assets/images/partial-react-logo.png deleted file mode 100644 index 66fd9570e..000000000 Binary files a/apps/presi/apps/mobile/assets/images/partial-react-logo.png and /dev/null differ diff --git a/apps/presi/apps/mobile/assets/images/patterns/memo-theme-tile.png b/apps/presi/apps/mobile/assets/images/patterns/memo-theme-tile.png deleted file mode 100644 index 3a036cf16..000000000 Binary files a/apps/presi/apps/mobile/assets/images/patterns/memo-theme-tile.png and /dev/null differ diff --git a/apps/presi/apps/mobile/assets/images/patterns/nature-theme-tile.png b/apps/presi/apps/mobile/assets/images/patterns/nature-theme-tile.png deleted file mode 100644 index 8e2aad877..000000000 Binary files a/apps/presi/apps/mobile/assets/images/patterns/nature-theme-tile.png and /dev/null differ diff --git a/apps/presi/apps/mobile/assets/images/patterns/stone-theme-tile.png b/apps/presi/apps/mobile/assets/images/patterns/stone-theme-tile.png deleted file mode 100644 index 628d74586..000000000 Binary files a/apps/presi/apps/mobile/assets/images/patterns/stone-theme-tile.png and /dev/null differ diff --git a/apps/presi/apps/mobile/assets/images/react-logo.png b/apps/presi/apps/mobile/assets/images/react-logo.png deleted file mode 100644 index 9d72a9ffc..000000000 Binary files a/apps/presi/apps/mobile/assets/images/react-logo.png and /dev/null differ diff --git a/apps/presi/apps/mobile/assets/images/react-logo@2x.png b/apps/presi/apps/mobile/assets/images/react-logo@2x.png deleted file mode 100644 index 2229b130a..000000000 Binary files a/apps/presi/apps/mobile/assets/images/react-logo@2x.png and /dev/null differ diff --git a/apps/presi/apps/mobile/assets/images/react-logo@3x.png b/apps/presi/apps/mobile/assets/images/react-logo@3x.png deleted file mode 100644 index a99b20322..000000000 Binary files a/apps/presi/apps/mobile/assets/images/react-logo@3x.png and /dev/null differ diff --git a/apps/presi/apps/mobile/assets/images/splash-icon.png b/apps/presi/apps/mobile/assets/images/splash-icon.png deleted file mode 100644 index 03d6f6b6c..000000000 Binary files a/apps/presi/apps/mobile/assets/images/splash-icon.png and /dev/null differ diff --git a/apps/presi/apps/mobile/assets/images/splash.png b/apps/presi/apps/mobile/assets/images/splash.png deleted file mode 100644 index dd83f5779..000000000 Binary files a/apps/presi/apps/mobile/assets/images/splash.png and /dev/null differ diff --git a/apps/presi/apps/mobile/components/Menu/Header.tsx b/apps/presi/apps/mobile/components/Menu/Header.tsx deleted file mode 100644 index 61bc2e67d..000000000 --- a/apps/presi/apps/mobile/components/Menu/Header.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import React from 'react'; -import { View, TouchableOpacity, Text, StyleSheet, useWindowDimensions } from 'react-native'; -import { MaterialIcons } from '@expo/vector-icons'; -import { useTheme } from '../ThemeProvider'; -import { useRouter } from 'expo-router'; - -interface HeaderProps { - title: string; - showAddDeck?: boolean; - showPresent?: boolean; - onPresentPress?: () => void; - disabled?: boolean; - slideCount?: number; - rightContent?: React.ReactNode; - position?: 'top' | 'bottom'; -} - -export const Header: React.FC = ({ - title, - showAddDeck = false, - showPresent = false, - onPresentPress, - disabled = false, - slideCount, - rightContent, - position = 'top', -}) => { - const router = useRouter(); - const { theme } = useTheme(); - const { width } = useWindowDimensions(); - const isMobile = width < 768; - const shouldBeBottom = isMobile && position === 'bottom'; - - const defaultRightContent = ( - - {!showPresent && ( - - router.push('/profile')} style={styles.iconButton}> - - - - )} - {showPresent && ( - - - - - - )} - - router.push('/settings')} style={styles.iconButton}> - - - - {showAddDeck && ( - - { - const event = new CustomEvent('openCreateDeckModal'); - window.dispatchEvent(event); - }} - style={styles.iconButton} - > - - - - )} - - ); - - return ( - - - - {title} - {typeof slideCount === 'number' && ( - - {slideCount} Slides - - )} - - - {rightContent || defaultRightContent} - - ); -}; - -const styles = StyleSheet.create({ - header: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - height: 56, - gap: 16, - }, - titleContainer: { - flex: 1, - maxWidth: '50%', - }, - titleContent: { - padding: 8, - width: '100%', - }, - rightContainer: { - flex: 1, - maxWidth: '50%', - }, - title: { - fontSize: 20, - fontWeight: 'bold', - }, - subtitle: { - fontSize: 14, - marginTop: 2, - }, - rightContent: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - width: '100%', - gap: 8, - padding: 8, - }, - iconWrapper: { - flex: 1, - borderWidth: 1, - borderRadius: 8, - padding: 4, - }, - iconButton: { - padding: 4, - alignItems: 'center', - justifyContent: 'center', - width: '100%', - }, -}); diff --git a/apps/presi/apps/mobile/components/ThemeProvider.tsx b/apps/presi/apps/mobile/components/ThemeProvider.tsx deleted file mode 100644 index 057ef8250..000000000 --- a/apps/presi/apps/mobile/components/ThemeProvider.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import React, { createContext, useContext, useState, useEffect, useMemo } from 'react'; -import { useColorScheme } from 'react-native'; -import { getTheme } from '../constants/theme'; -import type { Theme, ThemeVariant } from '../constants/theme'; - -export type ColorMode = 'system' | 'light' | 'dark'; -export type ContrastLevel = 1 | 2 | 3 | 4 | 5; - -type ColorType = 'text' | 'primary' | 'background'; - -// Hilfsfunktion zum Konvertieren von Hex zu RGB -const hexToRgb = (hex: string) => { - const h = hex.replace('#', ''); - return { - r: parseInt(h.substr(0, 2), 16), - g: parseInt(h.substr(2, 2), 16), - b: parseInt(h.substr(4, 2), 16), - }; -}; - -// Hilfsfunktion zum Konvertieren von RGB zu Hex mit Alpha -const rgbaToHex = (r: number, g: number, b: number, a = 1) => { - const alpha = Math.round(a * 255); - return ( - '#' + - [r, g, b, alpha] - .map((x) => { - const hex = Math.round(Math.max(0, Math.min(255, x))).toString(16); - return hex.length === 1 ? '0' + hex : hex; - }) - .join('') - ); -}; - -// Funktion zum Anpassen des Kontrasts -const adjustContrast = ( - color: string, - level: ContrastLevel, - type: ColorType, - isDark: boolean -): string => { - if (level === 3) return color; - - const { r, g, b } = hexToRgb(color); - - if (level < 3) { - // Niedrigerer Kontrast: Nur Text-Opacity wird reduziert - if (type === 'text') { - const opacity = 0.5 + (level - 1) * 0.25; // 0.5 für Level 1, 0.75 für Level 2 - return rgbaToHex(r, g, b, opacity); - } - return color; - } else { - // Höherer Kontrast: Nur Hintergründe werden angepasst - if (type === 'background') { - const factor = (level - 3) * 0.45; // 0.45 für Level 4, 0.9 für Level 5 - if (isDark) { - // Im Dark Mode: Hintergründe werden schwärzer - return rgbaToHex( - Math.round(r * (1 - factor)), - Math.round(g * (1 - factor)), - Math.round(b * (1 - factor)) - ); - } else { - // Im Light Mode: Hintergründe werden weißer - return rgbaToHex( - Math.round(r + (255 - r) * factor), - Math.round(g + (255 - g) * factor), - Math.round(b + (255 - b) * factor) - ); - } - } - return color; - } -}; - -const adjustThemeContrast = (theme: Theme, level: ContrastLevel, isDark: boolean): Theme => { - if (level === 3) return theme; - - const adjustedColors = Object.entries(theme.colors).reduce( - (acc, [key, value]) => { - if (typeof value === 'string' && value.startsWith('#')) { - let colorType: ColorType = 'background'; - if (key.toLowerCase().includes('text')) { - colorType = 'text'; - } else if (key.toLowerCase().includes('primary')) { - colorType = 'primary'; - } - - acc[key] = adjustContrast(value, level, colorType, isDark); - } else { - acc[key] = value; - } - return acc; - }, - {} as Record - ); - - return { - ...theme, - colors: adjustedColors, - }; -}; - -type ThemeContextType = { - theme: Theme; - isDark: boolean; - colorMode: ColorMode; - setColorMode: (mode: ColorMode) => void; - themeVariant: ThemeVariant; - setThemeVariant: (variant: ThemeVariant) => void; - contrastLevel: ContrastLevel; - setContrastLevel: (level: ContrastLevel) => void; -}; - -const ThemeContext = createContext({ - theme: getTheme('light'), - isDark: false, - colorMode: 'system', - setColorMode: () => {}, - themeVariant: 'default', - setThemeVariant: () => {}, - contrastLevel: 3, - setContrastLevel: () => {}, -}); - -export const useTheme = () => useContext(ThemeContext); - -export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const systemColorScheme = useColorScheme(); - const [colorMode, setColorMode] = useState('system'); - const [themeVariant, setThemeVariant] = useState('default'); - const [contrastLevel, setContrastLevel] = useState(3); - - const isDark = useMemo(() => { - if (colorMode === 'system') { - return systemColorScheme === 'dark'; - } - return colorMode === 'dark'; - }, [colorMode, systemColorScheme]); - - const theme = useMemo(() => { - const baseTheme = getTheme(isDark ? 'dark' : 'light', themeVariant); - return adjustThemeContrast(baseTheme, contrastLevel, isDark); - }, [isDark, themeVariant, contrastLevel]); - - const contextValue = useMemo( - () => ({ - theme, - isDark, - colorMode, - setColorMode, - themeVariant, - setThemeVariant, - contrastLevel, - setContrastLevel, - }), - [theme, isDark, colorMode, themeVariant, contrastLevel] - ); - - return {children}; -}; diff --git a/apps/presi/apps/mobile/components/atoms/Button.tsx b/apps/presi/apps/mobile/components/atoms/Button.tsx deleted file mode 100644 index 387d12cd9..000000000 --- a/apps/presi/apps/mobile/components/atoms/Button.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { TouchableOpacity, Text, StyleSheet, ActivityIndicator } from 'react-native'; -import { ReactNode } from 'react'; - -interface ButtonProps { - onPress: () => void; - children: ReactNode; - variant?: 'primary' | 'secondary' | 'outline'; - loading?: boolean; - disabled?: boolean; -} - -export const Button = ({ - onPress, - children, - variant = 'primary', - loading = false, - disabled = false, -}: ButtonProps) => { - return ( - - {loading ? ( - - ) : ( - {children} - )} - - ); -}; - -const styles = StyleSheet.create({ - button: { - padding: 16, - borderRadius: 8, - alignItems: 'center', - justifyContent: 'center', - minWidth: 200, - }, - primary: { - backgroundColor: '#f4511e', - }, - secondary: { - backgroundColor: '#6200ee', - }, - outline: { - backgroundColor: 'transparent', - borderWidth: 2, - borderColor: '#f4511e', - }, - disabled: { - opacity: 0.5, - }, - text: { - fontSize: 16, - fontWeight: 'bold', - }, - primaryText: { - color: '#fff', - }, - secondaryText: { - color: '#fff', - }, - outlineText: { - color: '#f4511e', - }, -}); diff --git a/apps/presi/apps/mobile/components/atoms/Input.tsx b/apps/presi/apps/mobile/components/atoms/Input.tsx deleted file mode 100644 index 329f05cae..000000000 --- a/apps/presi/apps/mobile/components/atoms/Input.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { TextInput, View, Text, StyleSheet } from 'react-native'; - -interface InputProps { - value: string; - onChangeText: (text: string) => void; - placeholder?: string; - secureTextEntry?: boolean; - label?: string; - error?: string; -} - -export const Input = ({ - value, - onChangeText, - placeholder, - secureTextEntry = false, - label, - error, -}: InputProps) => { - return ( - - {label && {label}} - - {error && {error}} - - ); -}; - -const styles = StyleSheet.create({ - container: { - marginBottom: 16, - }, - label: { - fontSize: 16, - marginBottom: 8, - color: '#000', - }, - input: { - borderWidth: 1, - borderColor: '#ccc', - borderRadius: 8, - padding: 12, - fontSize: 16, - }, - inputError: { - borderColor: '#ff0000', - }, - error: { - color: '#ff0000', - fontSize: 14, - marginTop: 4, - }, -}); diff --git a/apps/presi/apps/mobile/components/common/ContextMenu.tsx b/apps/presi/apps/mobile/components/common/ContextMenu.tsx deleted file mode 100644 index ce0a35996..000000000 --- a/apps/presi/apps/mobile/components/common/ContextMenu.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React from 'react'; -import { View, Modal, TouchableOpacity, Text, StyleSheet, Platform } from 'react-native'; -import { useTheme } from '../ThemeProvider'; - -export interface ContextMenuItem { - label: string; - icon: string; - onPress: () => void; - destructive?: boolean; -} - -interface ContextMenuProps { - visible: boolean; - onClose: () => void; - items: ContextMenuItem[]; - position?: { x: number; y: number }; -} - -export const ContextMenu: React.FC = ({ visible, onClose, items, position }) => { - const { theme } = useTheme(); - - if (!visible) return null; - - return ( - - - - {items.map((item, index) => ( - { - item.onPress(); - onClose(); - }} - > - - {item.label} - - - ))} - - - - ); -}; - -const styles = StyleSheet.create({ - overlay: { - flex: 1, - backgroundColor: 'rgba(0, 0, 0, 0.5)', - }, - menuContainer: { - position: 'absolute', - minWidth: 150, - borderRadius: 8, - ...Platform.select({ - web: { - boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.15)', - }, - default: { - elevation: 4, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.25, - shadowRadius: 4, - }, - }), - }, - menuItem: { - paddingVertical: 12, - paddingHorizontal: 16, - }, - menuItemBorder: { - borderBottomWidth: 1, - }, - menuItemText: { - fontSize: 16, - }, -}); diff --git a/apps/presi/apps/mobile/components/common/CreateItemButton.tsx b/apps/presi/apps/mobile/components/common/CreateItemButton.tsx deleted file mode 100644 index d1b6ed24e..000000000 --- a/apps/presi/apps/mobile/components/common/CreateItemButton.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; -import { TouchableOpacity, View, Text, StyleSheet, useWindowDimensions } from 'react-native'; -import MaterialIcons from '@expo/vector-icons/MaterialIcons'; -import { useTheme } from '../ThemeProvider'; - -interface CreateItemButtonProps { - onPress: () => void; - variant?: 'card' | 'button'; - width?: number | 'auto'; - title?: string; - buttonText?: string; - icon?: keyof typeof MaterialIcons.glyphMap; - buttonIcon?: keyof typeof MaterialIcons.glyphMap; -} - -export const CreateItemButton: React.FC = ({ - onPress, - variant = 'card', - width = 'auto', - title = 'Create New Item', - buttonText = 'Create New', - icon = 'add', - buttonIcon = 'add-circle-outline', -}) => { - const { theme } = useTheme(); - - if (variant === 'button') { - return ( - - - {buttonText} - - ); - } - - return ( - - - - - - - - - {title} - - - - ); -}; - -const styles = StyleSheet.create({ - itemContainer: { - marginVertical: 0, - marginHorizontal: 0, - borderRadius: 8, - overflow: 'hidden', - borderWidth: 1, - borderColor: 'transparent', - }, - itemContent: { - flex: 1, - gap: 8, - }, - imageContainer: { - aspectRatio: 16 / 9, - borderRadius: 8, - overflow: 'hidden', - }, - placeholderContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - textContainer: { - padding: 0, - marginTop: 12, - }, - title: { - fontSize: 20, - fontWeight: '600', - }, - createButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - padding: 12, - borderRadius: 8, - marginTop: 16, - }, - createButtonText: { - marginLeft: 8, - fontSize: 18, - fontWeight: '600', - }, -}); diff --git a/apps/presi/apps/mobile/components/common/ThemeSettings.tsx b/apps/presi/apps/mobile/components/common/ThemeSettings.tsx deleted file mode 100644 index c708df79c..000000000 --- a/apps/presi/apps/mobile/components/common/ThemeSettings.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React from 'react'; -import { View, Text, TouchableOpacity, StyleSheet, Pressable } from 'react-native'; -import { useTheme } from '../ThemeProvider'; -import type { ColorMode, ContrastLevel } from '../ThemeProvider'; - -const COLOR_MODES: { label: string; value: ColorMode }[] = [ - { label: 'System', value: 'system' }, - { label: 'Hell', value: 'light' }, - { label: 'Dunkel', value: 'dark' }, -]; - -const CONTRAST_LABELS: Record = { - 1: 'Sehr niedrig', - 2: 'Niedrig', - 3: 'Standard', - 4: 'Hoch', - 5: 'Sehr hoch', -}; - -export const ThemeSettings = () => { - const { theme, colorMode, setColorMode, contrastLevel, setContrastLevel } = useTheme(); - - return ( - - {/* Helligkeits-Einstellungen */} - - Helligkeit: - - {COLOR_MODES.map((mode) => ( - setColorMode(mode.value)} - > - - {mode.label} - - - ))} - - - - {/* Kontrast-Einstellungen */} - - Kontrast: - - - {([1, 2, 3, 4, 5] as ContrastLevel[]).map((level) => ( - setContrastLevel(level)} - /> - ))} - - - {CONTRAST_LABELS[contrastLevel]} - - - - - ); -}; - -const styles = StyleSheet.create({ - container: { - width: '100%', - gap: 16, - }, - section: { - padding: 16, - borderRadius: 12, - gap: 12, - }, - sectionTitle: { - fontSize: 16, - fontWeight: '600', - }, - colorModeList: { - flexDirection: 'row', - gap: 8, - }, - colorModeOption: { - flex: 1, - padding: 12, - borderRadius: 8, - alignItems: 'center', - }, - colorModeText: { - fontSize: 14, - fontWeight: '500', - }, - contrastContainer: { - gap: 12, - }, - contrastSlider: { - flexDirection: 'row', - gap: 8, - alignItems: 'center', - }, - contrastOption: { - flex: 1, - height: 4, - borderRadius: 2, - }, - contrastLabel: { - fontSize: 14, - textAlign: 'center', - }, -}); diff --git a/apps/presi/apps/mobile/components/common/menu.tsx b/apps/presi/apps/mobile/components/common/menu.tsx deleted file mode 100644 index 7e092fc05..000000000 --- a/apps/presi/apps/mobile/components/common/menu.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import * as ContextMenu from 'zeego/context-menu'; - -export const MenuRoot = ContextMenu.Root; -export const MenuTrigger = ContextMenu.Trigger; -export const MenuContent = ContextMenu.Content; -export const MenuItem = ContextMenu.Item; -export const MenuItemTitle = ContextMenu.ItemTitle; -export const MenuSeparator = ContextMenu.Separator; diff --git a/apps/presi/apps/mobile/components/decks/CreateDeckButton.tsx b/apps/presi/apps/mobile/components/decks/CreateDeckButton.tsx deleted file mode 100644 index 7334f6f08..000000000 --- a/apps/presi/apps/mobile/components/decks/CreateDeckButton.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react'; -import { TouchableOpacity, View, Text, StyleSheet, useWindowDimensions } from 'react-native'; -import MaterialIcons from '@expo/vector-icons/MaterialIcons'; -import { useTheme } from '../ThemeProvider'; - -interface CreateDeckButtonProps { - onPress: () => void; - variant?: 'card' | 'button'; - width?: number | 'auto'; -} - -export const CreateDeckButton: React.FC = ({ - onPress, - variant = 'card', - width = 'auto', -}) => { - const { theme } = useTheme(); - - if (variant === 'button') { - return ( - - - Create your first deck - - ); - } - - return ( - - - - - - - - - Create New Deck - - - - ); -}; - -const styles = StyleSheet.create({ - deckContainer: { - marginVertical: 8, - marginHorizontal: 8, - borderRadius: 12, - overflow: 'hidden', - }, - deckContent: { - flex: 1, - }, - imageContainer: { - aspectRatio: 16 / 9, - borderRadius: 12, - overflow: 'hidden', - }, - placeholderContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - textContainer: { - padding: 12, - }, - title: { - fontSize: 16, - fontWeight: '600', - }, - createButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - padding: 16, - borderRadius: 12, - marginTop: 16, - }, - createButtonText: { - marginLeft: 8, - fontSize: 16, - fontWeight: '600', - }, -}); diff --git a/apps/presi/apps/mobile/components/decks/DeckCard.tsx b/apps/presi/apps/mobile/components/decks/DeckCard.tsx deleted file mode 100644 index 676fb3052..000000000 --- a/apps/presi/apps/mobile/components/decks/DeckCard.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import React from 'react'; -import { View, Text, TouchableOpacity, StyleSheet, Image } from 'react-native'; -import { Deck } from '../../types/models'; -import { MaterialIcons } from '@expo/vector-icons'; -import { useTheme } from '../../components/ThemeProvider'; - -interface DeckCardProps { - deck: Deck; - onPress: (deck: Deck) => void; - onDelete: (deck: Deck) => void; - onShare: (deck: Deck) => void; - firstSlideImage?: string; - slideCount: number; -} - -export const DeckCard: React.FC = ({ - deck, - onPress, - onDelete, - onShare, - firstSlideImage, - slideCount, -}) => { - const { theme } = useTheme(); - - const isPublic = deck.sharing?.type === 'public'; - - const handleDelete = (event: any) => { - event?.preventDefault?.(); - event?.stopPropagation?.(); - if (typeof onDelete === 'function') { - onDelete(deck); - } else { - console.warn('onDelete is not a function'); - } - }; - - const handleShare = (event: any) => { - event?.preventDefault?.(); - event?.stopPropagation?.(); - if (typeof onShare === 'function') { - onShare(deck); - } - }; - - return ( - onPress(deck)} - > - - - {firstSlideImage ? ( - - ) : ( - - - - )} - - - - - - {deck.name} - - {isPublic && ( - - Public - - )} - - - {slideCount} {slideCount === 1 ? 'Slide' : 'Slides'} - - - - - - - - - - - - - ); -}; - -const styles = StyleSheet.create({ - container: { - borderRadius: 12, - marginBottom: 16, - overflow: 'hidden', - }, - content: { - flexDirection: 'row', - }, - imageContainer: { - width: 120, - height: 120, - justifyContent: 'center', - alignItems: 'center', - }, - thumbnail: { - width: '100%', - height: '100%', - }, - placeholderContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - header: { - flex: 1, - padding: 12, - justifyContent: 'space-between', - }, - titleRow: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - title: { - fontSize: 18, - fontWeight: '600', - flex: 1, - }, - badge: { - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 12, - }, - badgeText: { - color: '#FFFFFF', - fontSize: 12, - fontWeight: '600', - }, - slideCount: { - fontSize: 14, - }, - actions: { - flexDirection: 'row', - gap: 12, - }, - actionButton: { - padding: 4, - }, -}); diff --git a/apps/presi/apps/mobile/components/decks/DeckList.tsx b/apps/presi/apps/mobile/components/decks/DeckList.tsx deleted file mode 100644 index 03c0f0a1a..000000000 --- a/apps/presi/apps/mobile/components/decks/DeckList.tsx +++ /dev/null @@ -1,443 +0,0 @@ -import React, { useState } from 'react'; -import { - View, - FlatList, - TouchableOpacity, - Text, - StyleSheet, - Image, - useWindowDimensions, - ActivityIndicator, - Pressable, -} from 'react-native'; -import MaterialIcons from '@expo/vector-icons/MaterialIcons'; -import { Deck } from '../../types/models'; -import { useTheme } from '../../components/ThemeProvider'; -import { CreateItemButton } from '../common/CreateItemButton'; -import { MenuRoot, MenuTrigger, MenuContent, MenuItem, MenuItemTitle } from '../common/menu'; - -interface DeckListProps { - decks: Deck[]; - onDeckPress: (deck: Deck) => void; - onCreateDeck: () => void; - onDeleteDeck: (deck: Deck) => void; - onShareDeck: (deck: Deck) => void; - firstSlideImages: { [key: string]: string }; - loading?: boolean; - slideCounts: { [key: string]: number }; - scrollPadding?: { - vertical?: { top?: number; bottom?: number }; - horizontal?: { top?: number; bottom?: number }; - }; - deckSpacing?: { - vertical?: number; - horizontal?: number; - }; -} - -export const DeckList: React.FC = ({ - decks, - onDeckPress, - onCreateDeck, - onDeleteDeck, - onShareDeck, - firstSlideImages, - loading = false, - slideCounts, - scrollPadding = { - vertical: { top: 0, bottom: 0 }, - horizontal: { top: 0, bottom: 0 }, - }, - deckSpacing = { vertical: 8, horizontal: 8 }, -}) => { - const { width } = useWindowDimensions(); - const isSmallScreen = width < 768; - const deckWidth = !isSmallScreen - ? Math.floor((width - 40 - deckSpacing.horizontal * 2) / 2.5) - : 'auto'; - const { theme } = useTheme(); - - if (loading) { - return ( - - - - ); - } - - if (decks.length === 0) { - return ( - - - - No decks yet - - - Create your first deck to get started - - - - ); - } - - const renderDeck = ({ item }: { item: Deck | 'create' }) => { - if (item === 'create') { - return ( - - ); - } - - return ( - onDeckPress(item)} - > - - - - - - - {new Date(item.updatedAt).toLocaleDateString('de-DE', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - })} - - - - {slideCounts[item.id] || 0} {slideCounts[item.id] === 1 ? 'Slide' : 'Slides'} - - - - - {firstSlideImages[item.id] ? ( - - ) : ( - - - - )} - - - - - - {item.title || item.name} - - - - - - - - onShareDeck(item)} textValue="Share"> - [ - styles.menuItem, - { - backgroundColor: hovered - ? theme.colors.backgroundTertiary - : theme.colors.backgroundPrimary, - }, - ]} - > - - - - - Share - - - - - - onDeleteDeck(item)} textValue="Delete"> - [ - styles.menuItem, - { - backgroundColor: hovered - ? theme.colors.backgroundError - : theme.colors.backgroundPrimary, - }, - ]} - > - - - - - Delete - - - - - - - - - - - ); - }; - - return ( - - (item === 'create' ? 'create' : item.id)} - showsVerticalScrollIndicator={false} - showsHorizontalScrollIndicator={false} - horizontal={!isSmallScreen} - contentContainerStyle={[ - styles.listContent, - { - backgroundColor: 'transparent', - paddingHorizontal: 20, - paddingTop: !isSmallScreen ? scrollPadding.horizontal.top : scrollPadding.vertical.top, - paddingBottom: !isSmallScreen - ? scrollPadding.horizontal.bottom - : scrollPadding.vertical.bottom, - }, - !isSmallScreen && styles.horizontalListContent, - ]} - scrollIndicatorInsets={{ - top: !isSmallScreen ? scrollPadding.horizontal.top : scrollPadding.vertical.top, - bottom: !isSmallScreen ? scrollPadding.horizontal.bottom : scrollPadding.vertical.bottom, - }} - ItemSeparatorComponent={() => ( - - )} - /> - - ); -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - list: { - flex: 1, - }, - listContent: { - padding: 8, - gap: 16, - }, - horizontalListContent: { - padding: 8, - gap: 16, - flexDirection: 'row', - alignItems: 'flex-start', - }, - deckContainer: {}, - deckContent: { - backgroundColor: 'transparent', - }, - deckInfoContainer: { - width: '100%', - gap: 8, - }, - metaInfo: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'flex-end', - paddingHorizontal: 4, - gap: 8, - }, - metaText: { - fontSize: 12, - fontWeight: '500', - textAlign: 'right', - }, - separator: { - width: 1, - height: 12, - }, - imageContainer: { - width: '100%', - aspectRatio: 16 / 9, - borderRadius: 8, - overflow: 'hidden', - }, - thumbnail: { - width: '100%', - height: '100%', - }, - placeholderContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - deckHeader: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 12, - }, - horizontalDeckHeader: { - width: '100%', - flexDirection: 'column', - alignItems: 'center', - marginTop: 12, - gap: 8, - }, - titleContainer: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - width: '100%', - }, - horizontalTitleContainer: { - flexDirection: 'column', - alignItems: 'center', - gap: 4, - width: '100%', - position: 'relative', - }, - actionsContainer: { - flexDirection: 'row', - justifyContent: 'flex-end', - gap: 16, - }, - horizontalActionsContainer: { - position: 'absolute', - top: 0, - right: 0, - width: 'auto', - }, - deckTitle: { - fontSize: 20, - fontWeight: '600', - flex: 1, - marginRight: 16, - }, - createDeckContent: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: 8, - }, - createDeckText: { - fontSize: 16, - fontWeight: '500', - }, - emptyState: { - justifyContent: 'center', - alignItems: 'center', - gap: 16, - }, - emptyStateText: { - fontSize: 18, - fontWeight: '600', - }, - emptyStateSubtext: { - fontSize: 14, - }, - createButton: { - paddingHorizontal: 16, - paddingVertical: 8, - borderRadius: 8, - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - createButtonText: { - fontSize: 16, - fontWeight: '600', - }, - menuContent: { - minWidth: 180, - borderRadius: 8, - borderWidth: 1, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 4, - overflow: 'hidden', - }, - menuItem: { - height: 44, - paddingHorizontal: 16, - justifyContent: 'center', - }, - menuItemContent: { - flexDirection: 'row', - alignItems: 'center', - }, - menuItemIcon: { - marginRight: 12, - }, - menuItemTitle: { - fontSize: 16, - fontWeight: '500', - }, -}); diff --git a/apps/presi/apps/mobile/components/decks/DeckShareSettings.tsx b/apps/presi/apps/mobile/components/decks/DeckShareSettings.tsx deleted file mode 100644 index 1a8b6dd83..000000000 --- a/apps/presi/apps/mobile/components/decks/DeckShareSettings.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import React, { useState } from 'react'; -import { - View, - Text, - TouchableOpacity, - StyleSheet, - Switch, - TextInput, - Platform, - Clipboard, -} from 'react-native'; -import { useTheme } from '../ThemeProvider'; -import MaterialIcons from '@expo/vector-icons/MaterialIcons'; -import { Deck } from '../../types/models'; -import type { CollaboratorRole } from '../../types/models'; - -interface DeckShareSettingsProps { - deck: Deck; - onUpdateSharing: (sharing: Deck['sharing']) => void; - onClose: () => void; -} - -interface CollaboratorInput { - email: string; - role: CollaboratorRole; -} - -export const DeckShareSettings: React.FC = ({ - deck, - onUpdateSharing, - onClose, -}) => { - const { theme } = useTheme(); - const [isPublic, setIsPublic] = useState(deck.sharing.isPublic); - const [newCollaborator, setNewCollaborator] = useState({ - email: '', - role: 'viewer', - }); - const [copied, setCopied] = useState(false); - - const shareUrl = `${Platform.OS === 'web' ? window.location.origin : 'https://presi.app'}/deck/${deck.id}`; - - const handleCopyLink = () => { - Clipboard.setString(shareUrl); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - - const handleTogglePublic = () => { - setIsPublic(!isPublic); - onUpdateSharing({ - ...deck.sharing, - isPublic: !isPublic, - }); - }; - - const handleAddCollaborator = () => { - // TODO: Implement email to userId lookup - const mockUserId = 'user_' + Date.now(); - onUpdateSharing({ - ...deck.sharing, - collaborators: { - ...deck.sharing.collaborators, - [mockUserId]: newCollaborator.role, - }, - }); - setNewCollaborator({ email: '', role: 'viewer' }); - }; - - const handleRemoveCollaborator = (userId: string) => { - const newCollaborators = { ...deck.sharing.collaborators }; - delete newCollaborators[userId]; - onUpdateSharing({ - ...deck.sharing, - collaborators: newCollaborators, - }); - }; - - return ( - - - Share Settings - - - - - - - - - - - Public Access - - - Anyone with the link can view this deck - - - - - - - - - Share Link - - - - {shareUrl} - - - - {copied ? 'Copied!' : 'Copy'} - - - - - - - Collaborators - - - setNewCollaborator({ ...newCollaborator, email })} - /> - - Add - - - - {Object.entries(deck.sharing.collaborators).map(([userId, role]) => ( - - - {userId} ({role}) - - handleRemoveCollaborator(userId)}> - - - - ))} - - - - ); -}; - -const styles = StyleSheet.create({ - container: { - padding: 16, - borderRadius: 8, - maxWidth: 500, - width: '100%', - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 24, - borderBottomWidth: 1, - borderBottomColor: '#ccc', - }, - title: { - fontSize: 24, - fontWeight: 'bold', - }, - content: { - padding: 16, - gap: 24, - }, - section: { - gap: 16, - }, - settingRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - settingInfo: { - flex: 1, - marginRight: 16, - }, - settingTitle: { - fontSize: 16, - fontWeight: '500', - }, - settingDescription: { - fontSize: 14, - marginTop: 4, - }, - linkSection: { - gap: 8, - }, - sectionTitle: { - fontSize: 14, - fontWeight: '500', - }, - linkContainer: { - flexDirection: 'row', - alignItems: 'center', - borderRadius: 8, - padding: 8, - gap: 8, - }, - link: { - flex: 1, - fontSize: 14, - }, - copyButton: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 12, - paddingVertical: 6, - borderRadius: 6, - gap: 4, - }, - copyButtonText: { - color: '#FFFFFF', - fontSize: 14, - fontWeight: '500', - }, - collaboratorInput: { - flexDirection: 'row', - marginBottom: 16, - }, - input: { - flex: 1, - height: 40, - borderWidth: 1, - borderColor: '#ccc', - borderRadius: 4, - paddingHorizontal: 8, - marginRight: 8, - }, - addButton: { - paddingHorizontal: 16, - height: 40, - justifyContent: 'center', - alignItems: 'center', - borderRadius: 4, - }, - addButtonText: { - color: 'white', - fontWeight: 'bold', - }, - collaboratorRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingVertical: 8, - borderBottomWidth: 1, - borderBottomColor: '#eee', - }, - collaboratorEmail: { - fontSize: 16, - }, -}); diff --git a/apps/presi/apps/mobile/components/forms/CreateDeckForm.tsx b/apps/presi/apps/mobile/components/forms/CreateDeckForm.tsx deleted file mode 100644 index 6fe461950..000000000 --- a/apps/presi/apps/mobile/components/forms/CreateDeckForm.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import React, { useState } from 'react'; -import { - View, - Text, - TextInput, - TouchableOpacity, - StyleSheet, - KeyboardAvoidingView, - Platform, -} from 'react-native'; -import { useTheme } from '../ThemeProvider'; -import { createDeck } from '../../services/firestore'; -import { Deck } from '../../types/models'; -import MaterialIcons from '@expo/vector-icons/MaterialIcons'; - -interface CreateDeckFormProps { - onSuccess: (deck: Deck) => void; - onCancel: () => void; -} - -export const CreateDeckForm: React.FC = ({ onSuccess, onCancel }) => { - const [name, setName] = useState(''); - const [description, setDescription] = useState(''); - const [error, setError] = useState(null); - const { theme } = useTheme(); - - const handleSubmit = async () => { - if (!name.trim()) { - setError('Please enter a name for your deck'); - return; - } - - try { - const newDeck = await createDeck({ - name: name.trim(), - description: description.trim(), - }); - onSuccess(newDeck); - } catch (err) { - setError('Failed to create deck. Please try again.'); - console.error('Error creating deck:', err); - } - }; - - return ( - - - Create New Deck - - - - - - - - Name - - - - - - Description (optional) - - - - - {error && {error}} - - - - Cancel - - - Create Deck - - - - - ); -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: 16, - borderBottomWidth: 1, - }, - title: { - fontSize: 20, - fontWeight: '600', - }, - closeButton: { - padding: 8, - }, - form: { - padding: 16, - gap: 16, - }, - inputContainer: { - gap: 8, - }, - label: { - fontSize: 16, - fontWeight: '500', - }, - input: { - borderWidth: 1, - borderRadius: 8, - padding: 12, - fontSize: 16, - }, - textArea: { - minHeight: 100, - textAlignVertical: 'top', - }, - error: { - fontSize: 14, - marginTop: 8, - }, - buttons: { - flexDirection: 'row', - gap: 12, - marginTop: 16, - }, - button: { - flex: 1, - padding: 12, - borderRadius: 8, - alignItems: 'center', - }, - buttonText: { - fontSize: 16, - fontWeight: '600', - }, - cancelButton: {}, - createButton: {}, -}); diff --git a/apps/presi/apps/mobile/components/presentation/PresentationMode.tsx b/apps/presi/apps/mobile/components/presentation/PresentationMode.tsx deleted file mode 100644 index ddc0c4a88..000000000 --- a/apps/presi/apps/mobile/components/presentation/PresentationMode.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import React, { useState, useEffect, useCallback, useRef } from 'react'; -import { - View, - Text, - StyleSheet, - TouchableOpacity, - Platform, - useWindowDimensions, - Image, - Animated, -} from 'react-native'; -import { Ionicons } from '@expo/vector-icons'; -import { SlideView } from '../slides/SlideView'; -import { Slide } from '../../types/models'; -import * as ScreenOrientation from 'expo-screen-orientation'; -import { useTheme } from '../ThemeProvider'; - -interface PresentationModeProps { - slides: Slide[]; - initialSlideIndex?: number; - onClose?: () => void; -} - -export const PresentationMode: React.FC = ({ - slides, - initialSlideIndex = 0, - onClose, -}) => { - const { theme } = useTheme(); - const [currentSlideIndex, setCurrentSlideIndex] = useState(initialSlideIndex); - const [isFullscreen, setIsFullscreen] = useState(false); - const [isTimerRunning, setIsTimerRunning] = useState(false); - const [elapsedTime, setElapsedTime] = useState(0); - const [showSpeakerNotes, setShowSpeakerNotes] = useState(false); - const { width, height } = useWindowDimensions(); - - // Control visibility state - const controlsOpacity = useRef(new Animated.Value(1)).current; - const hideControlsTimer = useRef(null); - - const showControls = useCallback(() => { - // Clear any existing timer - if (hideControlsTimer.current) { - clearTimeout(hideControlsTimer.current); - } - - // Show controls with animation - Animated.timing(controlsOpacity, { - toValue: 1, - duration: 200, - useNativeDriver: true, - }).start(); - - // Set timer to hide controls after 5 seconds - hideControlsTimer.current = setTimeout(() => { - Animated.timing(controlsOpacity, { - toValue: 0, - duration: 200, - useNativeDriver: true, - }).start(); - }, 5000); - }, [controlsOpacity]); - - const handleNavigation = useCallback( - (direction: 'prev' | 'next') => { - if (direction === 'prev') { - setCurrentSlideIndex((prev) => (prev === 0 ? slides.length - 1 : prev - 1)); - } else { - setCurrentSlideIndex((prev) => (prev === slides.length - 1 ? 0 : prev + 1)); - } - showControls(); - }, - [slides.length, showControls] - ); - - // Handle keyboard navigation - useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - console.log('Key pressed:', event.key); // Debug log - switch (event.key.toLowerCase()) { - case 'arrowleft': - case 'a': - event.preventDefault(); - handleNavigation('prev'); - break; - case 'arrowright': - case 'd': - event.preventDefault(); - handleNavigation('next'); - break; - } - }; - - if (Platform.OS === 'web') { - window.addEventListener('keydown', handleKeyDown); - return () => { - window.removeEventListener('keydown', handleKeyDown); - }; - } - }, [handleNavigation]); - - // Handle mouse movement - useEffect(() => { - if (Platform.OS === 'web') { - window.addEventListener('mousemove', showControls); - return () => { - window.removeEventListener('mousemove', showControls); - }; - } - }, [showControls]); - - // Show controls initially - useEffect(() => { - showControls(); - }, []); - - // Clean up timer on unmount - useEffect(() => { - return () => { - if (hideControlsTimer.current) { - clearTimeout(hideControlsTimer.current); - } - }; - }, []); - - useEffect(() => { - let timer: NodeJS.Timeout; - if (isTimerRunning) { - timer = setInterval(() => { - setElapsedTime((prev) => prev + 1); - }, 1000); - } - return () => clearInterval(timer); - }, [isTimerRunning]); - - useEffect(() => { - const setupOrientation = async () => { - if (isFullscreen) { - await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); - } else { - await ScreenOrientation.unlockAsync(); - } - }; - - if (Platform.OS !== 'web') { - setupOrientation(); - } - - return () => { - if (Platform.OS !== 'web') { - ScreenOrientation.unlockAsync(); - } - }; - }, [isFullscreen]); - - const currentSlide = slides[currentSlideIndex]; - - return ( - - {/* Top bar with title and close button */} - - - {currentSlide.title} - - - - - - - {/* Current slide */} - - - - - {/* Controls overlay with animation */} - - - handleNavigation('prev')} - > - - - - - setShowSpeakerNotes(!showSpeakerNotes)} - > - - - - setIsTimerRunning(!isTimerRunning)} - > - - - - - {Math.floor(elapsedTime / 60)}:{(elapsedTime % 60).toString().padStart(2, '0')} - - - setIsFullscreen(!isFullscreen)} - > - - - - - handleNavigation('next')} - > - - - - - - ); -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - position: 'relative', - }, - topBar: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - padding: 20, - paddingTop: 40, - zIndex: 10, - }, - slideTitle: { - fontSize: 18, - fontWeight: '500', - }, - closeButton: { - padding: 8, - borderRadius: 20, - }, - slideContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - controlsOverlay: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - padding: 16, - }, - controls: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - maxWidth: 800, - marginHorizontal: 'auto', - }, - centerControls: { - flexDirection: 'row', - alignItems: 'center', - gap: 16, - }, - controlButton: { - padding: 8, - borderRadius: 20, - }, - timer: { - fontSize: 16, - marginLeft: 8, - }, -}); diff --git a/apps/presi/apps/mobile/components/slides/SlideEditor.tsx b/apps/presi/apps/mobile/components/slides/SlideEditor.tsx deleted file mode 100644 index f5b68def7..000000000 --- a/apps/presi/apps/mobile/components/slides/SlideEditor.tsx +++ /dev/null @@ -1,336 +0,0 @@ -import React, { useState } from 'react'; -import { - View, - TextInput, - TouchableOpacity, - Text, - StyleSheet, - ScrollView, - Alert, - Image, -} from 'react-native'; -import * as ImagePicker from 'expo-image-picker'; -import { storage } from '../../firebaseConfig'; -import { ref, uploadBytes, getDownloadURL } from 'firebase/storage'; -import { createSlide, updateSlide } from '../../services/firestore'; -import { Slide } from '../../types/models'; -import MaterialIcons from '@expo/vector-icons/MaterialIcons'; -import { useTheme } from '../ThemeProvider'; - -interface SlideEditorProps { - deckId: string; - slide?: Slide; - onSuccess?: () => void; - onCancel?: () => void; -} - -export const SlideEditor: React.FC = ({ deckId, slide, onSuccess, onCancel }) => { - const { theme } = useTheme(); - const [title, setTitle] = useState(slide?.title ?? ''); - const [fullText, setFullText] = useState(slide?.fullText ?? ''); - const [bulletPoints, setBulletPoints] = useState(slide?.bulletPoints ?? ['']); - const [notes, setNotes] = useState(slide?.notes ?? ''); - const [imageUrl, setImageUrl] = useState(slide?.imageUrl); - const [isSubmitting, setIsSubmitting] = useState(false); - - const handleAddBulletPoint = () => { - setBulletPoints([...bulletPoints, '']); - }; - - const handleUpdateBulletPoint = (index: number, text: string) => { - const newBulletPoints = [...bulletPoints]; - newBulletPoints[index] = text; - setBulletPoints(newBulletPoints); - }; - - const handleRemoveBulletPoint = (index: number) => { - const newBulletPoints = bulletPoints.filter((_, i) => i !== index); - setBulletPoints(newBulletPoints); - }; - - const handlePickImage = async () => { - const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync(); - - if (permissionResult.granted === false) { - Alert.alert('Permission Required', 'Please allow access to your photos to upload images.'); - return; - } - - const result = await ImagePicker.launchImageLibraryAsync({ - mediaTypes: ImagePicker.MediaTypeOptions.Images, - allowsEditing: true, - aspect: [16, 9], - quality: 1, - }); - - if (!result.canceled) { - const uri = result.assets[0].uri; - setImageUrl(uri); - } - }; - - const handleSubmit = async () => { - if (isSubmitting) return; - - setIsSubmitting(true); - try { - let uploadedImageUrl = imageUrl; - - // Upload image if selected - if (imageUrl && imageUrl.startsWith('file://')) { - const response = await fetch(imageUrl); - const blob = await response.blob(); - const imagePath = `slides/${deckId}/${Date.now()}.jpg`; - const imageRef = ref(storage, imagePath); - await uploadBytes(imageRef, blob); - uploadedImageUrl = await getDownloadURL(imageRef); - } - - // Create or update slide - const slideData = { - deckId, - title, - fullText, - bulletPoints: bulletPoints.filter((bp) => bp.trim() !== ''), - notes, - imageUrl: uploadedImageUrl, - }; - - if (slide?.id) { - // Update existing slide - await updateSlide(slide.id, slideData); - } else { - // Create new slide - const newSlide = await createSlide(slideData); - console.log('[SlideEditor] Slide created successfully:', newSlide); - } - - onSuccess?.(); - } catch (error) { - console.error('[SlideEditor] Error saving slide:', error); - Alert.alert('Error', 'Failed to save slide. Please try again.'); - } finally { - setIsSubmitting(false); - } - }; - - return ( - - - - Title - - - - - Image - - {imageUrl ? ( - - ) : ( - - - - Add Image - - - )} - - - - - Full Text - - - - - Bullet Points - {bulletPoints.map((point, index) => ( - - - handleUpdateBulletPoint(index, text)} - placeholder="Add bullet point..." - placeholderTextColor={theme.colors.textTertiary} - multiline - /> - handleRemoveBulletPoint(index)} - > - - - - ))} - - - - Add Bullet Point - - - - - - Notes - - - - - - - Cancel - - - - {isSubmitting ? 'Saving...' : 'Save'} - - - - - ); -}; - -const styles = StyleSheet.create({ - editorContainer: { - flex: 1, - }, - formGroup: { - marginBottom: 16, - }, - inputContainer: { - marginBottom: 16, - }, - label: { - fontSize: 14, - marginBottom: 4, - fontWeight: '500', - }, - input: { - borderWidth: 1, - borderRadius: 8, - padding: 12, - fontSize: 14, - }, - textArea: { - height: 100, - textAlignVertical: 'top', - }, - bulletPointContainer: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 8, - }, - bulletPointInput: { - flex: 1, - marginRight: 8, - }, - addButton: { - flexDirection: 'row', - alignItems: 'center', - padding: 8, - borderRadius: 8, - marginTop: 8, - }, - addButtonText: { - marginLeft: 8, - fontSize: 14, - fontWeight: '500', - }, - buttonContainer: { - flexDirection: 'row', - justifyContent: 'flex-end', - gap: 8, - marginTop: 16, - }, - button: { - paddingVertical: 8, - paddingHorizontal: 16, - borderRadius: 8, - minWidth: 80, - alignItems: 'center', - }, - buttonText: { - fontSize: 14, - fontWeight: '500', - }, - imagePreview: { - width: '100%', - height: 200, - marginTop: 8, - borderRadius: 8, - }, -}); diff --git a/apps/presi/apps/mobile/components/slides/SlideList.tsx b/apps/presi/apps/mobile/components/slides/SlideList.tsx deleted file mode 100644 index b53987986..000000000 --- a/apps/presi/apps/mobile/components/slides/SlideList.tsx +++ /dev/null @@ -1,426 +0,0 @@ -import React from 'react'; -import { - View, - FlatList, - TouchableOpacity, - Text, - StyleSheet, - Image, - useWindowDimensions, - Platform, - Pressable, -} from 'react-native'; -import MaterialIcons from '@expo/vector-icons/MaterialIcons'; -import { Slide } from '../../types/models'; -import { useTheme } from '../ThemeProvider'; -import { MenuRoot, MenuTrigger, MenuContent, MenuItem, MenuItemTitle } from '../common/menu'; - -interface SlideListProps { - slides: Slide[]; - showNotes?: boolean; - onEditSlide?: (slide: Slide) => void; - onCreateSlide?: () => void; - onDeleteSlide?: (slide: Slide) => void; - onMoveSlide?: (slide: Slide, direction: 'up' | 'down') => void; - loading?: boolean; -} - -export const SlideList: React.FC = ({ - slides, - showNotes = false, - onEditSlide, - onCreateSlide, - onDeleteSlide, - onMoveSlide, - loading, -}) => { - const { width } = useWindowDimensions(); - const { theme } = useTheme(); - const isSmallScreen = width < 768; // Tablet breakpoint - - // Calculate slide width to show 2.5 slides - const slideWidth = !isSmallScreen ? Math.floor(width / 2.5) : 'auto'; - - if (loading) { - return ( - - - - Loading... - - - ); - } - - const renderItem = ({ item, index }: { item: Slide | 'create'; index: number }) => { - if (item === 'create') { - return ( - - - - - - - - - - Neuen Slide erstellen - - - - - ); - } - - return ( - - - - onEditSlide?.(item)} style={{ flex: 1 }}> - - - {item.imageUrl ? ( - - ) : ( - - - - )} - - - - {item.title || `Slide ${index + 1}`} - - - {showNotes && item.notes && ( - - {item.notes} - - )} - - - - - - {onMoveSlide && index > 0 && ( - { - console.log('[SlideList] Selected Move Up for slide:', item.id); - onMoveSlide(item, 'up'); - }} - textValue="Move Up" - > - { - console.log('[SlideList] Pressed Move Up for slide:', item.id); - onMoveSlide(item, 'up'); - }} - style={({ hovered }) => [ - styles.menuItem, - { - backgroundColor: hovered - ? theme.colors.backgroundTertiary - : theme.colors.backgroundPrimary, - }, - ]} - > - - - - - Nach oben - - - - - - )} - {onMoveSlide && index < slides.length - 1 && ( - { - console.log('[SlideList] Selected Move Down for slide:', item.id); - onMoveSlide(item, 'down'); - }} - textValue="Move Down" - > - { - console.log('[SlideList] Pressed Move Down for slide:', item.id); - onMoveSlide(item, 'down'); - }} - style={({ hovered }) => [ - styles.menuItem, - { - backgroundColor: hovered - ? theme.colors.backgroundTertiary - : theme.colors.backgroundPrimary, - }, - ]} - > - - - - - Nach unten - - - - - - )} - {onDeleteSlide && ( - onDeleteSlide(item)} textValue="Delete"> - onDeleteSlide(item)} - style={({ hovered }) => [ - styles.menuItem, - { - backgroundColor: hovered - ? theme.colors.backgroundError - : theme.colors.backgroundPrimary, - }, - ]} - > - - - - - Löschen - - - - - - )} - - - - - ); - }; - - if (slides.length === 0) { - return ( - - - - - Keine Slides - - - Erstelle deinen ersten Slide - - - - - - - - - - - - Erstelle deinen ersten Slide - - - - - - ); - } - - return ( - - (item === 'create' ? 'create' : item.id)} - horizontal={!isSmallScreen} - showsHorizontalScrollIndicator={false} - contentContainerStyle={[styles.listContent, !isSmallScreen && { paddingHorizontal: 16 }]} - extraData={slides.map((s) => s.order).join(',')} - /> - - ); -}; - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - listContent: { - paddingBottom: 100, - }, - horizontalListContent: { - flexGrow: 1, - paddingHorizontal: 16, - gap: 16, - alignItems: 'center', - minHeight: '100%', - }, - slideContainer: { - marginHorizontal: 16, - marginVertical: 8, - }, - slideContent: { - gap: 8, - }, - imageContainer: { - width: '100%', - height: undefined, - aspectRatio: 16 / 9, - borderRadius: 4, - overflow: 'hidden', - }, - thumbnail: { - width: '100%', - height: '100%', - }, - placeholderContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - slideFooter: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - marginTop: 0, - }, - slideNumber: { - fontSize: 18, - fontWeight: '600', - }, - actionButtons: { - flexDirection: 'row', - gap: 8, - }, - actionButton: { - width: 32, - height: 32, - borderRadius: 16, - justifyContent: 'center', - alignItems: 'center', - }, - notes: { - fontSize: 14, - marginTop: 4, - }, - emptyState: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 20, - }, - emptyStateText: { - fontSize: 20, - fontWeight: '600', - marginTop: 16, - }, - emptyStateSubtext: { - fontSize: 14, - marginTop: 8, - }, - emptyContainer: { - flex: 1, - justifyContent: 'space-between', - paddingBottom: 32, - }, - menuContent: { - minWidth: 180, - borderRadius: 8, - borderWidth: 1, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 4, - overflow: 'hidden', - }, - menuItem: { - height: 44, - paddingHorizontal: 16, - justifyContent: 'center', - }, - menuItemContent: { - flexDirection: 'row', - alignItems: 'center', - }, - menuItemIcon: { - marginRight: 12, - }, - menuItemTitle: { - fontSize: 16, - fontWeight: '500', - }, -}); diff --git a/apps/presi/apps/mobile/components/slides/SlideView.tsx b/apps/presi/apps/mobile/components/slides/SlideView.tsx deleted file mode 100644 index 99db757bc..000000000 --- a/apps/presi/apps/mobile/components/slides/SlideView.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import React from 'react'; -import { View, Text, Image, StyleSheet, Dimensions, TouchableOpacity } from 'react-native'; -import { Slide } from '../../types/models'; -import MaterialIcons from 'react-native-vector-icons/MaterialIcons'; -import { useTheme } from '../ThemeProvider'; - -interface SlideViewProps { - slide: Slide; - showNotes?: boolean; - isFullscreen?: boolean; - onToggleFullscreen?: () => void; - onNavigate?: (direction: 'prev' | 'next') => void; - isFirstSlide?: boolean; - isLastSlide?: boolean; -} - -export const SlideView: React.FC = ({ - slide, - showNotes = false, - isFullscreen = false, - onToggleFullscreen, - onNavigate, - isFirstSlide = false, - isLastSlide = false, -}) => { - const { theme } = useTheme(); - - return ( - - {slide.imageUrl && ( - - {/* Navigation Areas */} - {!isFirstSlide && ( - onNavigate?.('prev')} /> - )} - {!isLastSlide && ( - onNavigate?.('next')} - /> - )} - - {/* Image */} - - - {/* Navigation Indicators */} - {isFullscreen && ( - <> - {!isFirstSlide && ( - - - - )} - {!isLastSlide && ( - - - - )} - - )} - - )} - - {!isFullscreen && ( - - - {slide.title} - - - {slide.bulletPoints && slide.bulletPoints.length > 0 && ( - - {slide.bulletPoints.map((point, index) => ( - - - - {point} - - - ))} - - )} - - {slide.fullText && ( - - {slide.fullText} - - )} - - {showNotes && slide.notes && ( - - Notes: - {slide.notes} - - )} - - )} - - ); -}; - -const { width, height } = Dimensions.get('window'); -const ASPECT_RATIO = 16 / 9; -const SLIDE_WIDTH = width; -const SLIDE_HEIGHT = SLIDE_WIDTH / ASPECT_RATIO; - -const styles = StyleSheet.create({ - container: { - width: SLIDE_WIDTH, - height: SLIDE_HEIGHT, - borderRadius: 8, - overflow: 'hidden', - }, - fullscreenContainer: { - width: '100%', - height: '100%', - borderRadius: 0, - }, - imageContainer: { - width: '100%', - height: undefined, - aspectRatio: 16 / 9, - overflow: 'hidden', - position: 'relative', - }, - fullscreenImageContainer: { - width: '100%', - height: '100%', - aspectRatio: undefined, - }, - image: { - width: '100%', - height: '100%', - position: 'absolute', - }, - navigationArea: { - position: 'absolute', - top: 0, - left: 0, - width: '50%', - height: '100%', - zIndex: 2, - }, - navigationAreaRight: { - left: '50%', - }, - navigationIndicator: { - position: 'absolute', - top: '50%', - transform: [{ translateY: -18 }], - opacity: 0.5, - zIndex: 1, - }, - navigationIndicatorLeft: { - left: 16, - }, - navigationIndicatorRight: { - right: 16, - }, - content: { - padding: 16, - flex: 1, - }, - contentTitle: { - fontSize: 24, - fontWeight: 'bold', - marginBottom: 16, - }, - bulletPoints: { - marginBottom: 16, - }, - bulletPoint: { - flexDirection: 'row', - marginBottom: 8, - alignItems: 'flex-start', - }, - bullet: { - fontSize: 16, - marginRight: 8, - }, - bulletText: { - fontSize: 16, - flex: 1, - }, - fullText: { - fontSize: 16, - marginBottom: 16, - }, - notesContainer: { - marginTop: 16, - padding: 16, - borderRadius: 8, - }, - notesTitle: { - fontSize: 14, - fontWeight: 'bold', - marginBottom: 8, - }, - notes: { - fontSize: 14, - lineHeight: 20, - }, -}); diff --git a/apps/presi/apps/mobile/constants/theme.ts b/apps/presi/apps/mobile/constants/theme.ts deleted file mode 100644 index acc40334a..000000000 --- a/apps/presi/apps/mobile/constants/theme.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { ColorSchemeName } from 'react-native'; - -// Theme types -export type ThemeVariant = 'lume' | 'nature' | 'stone'; - -export const THEME_PATTERNS: Record = { - lume: require('../assets/images/patterns/memo-theme-tile.png'), - nature: require('../assets/images/patterns/nature-theme-tile.png'), - stone: require('../assets/images/patterns/stone-theme-tile.png'), -}; - -export const THEME_NAMES: Record = { - lume: 'Lume', - nature: 'Nature', - stone: 'Stone', -}; - -// Farbpalette -export const lightColors = { - primary: '#f8d62b', - backgroundForPrimary: '#383838', - primaryHover: '#2980b9', - primaryPressed: '#1f6da3', - secondary: '#D4B200', - secondaryHover: '#27ae60', - secondaryPressed: '#229954', - backgroundPage: '#dddddd', - backgroundPrimary: '#ffffff', - backgroundSecondary: '#eeeeee', // Geändert zu einem helleren Grau - backgroundTertiary: '#e8e8e8', // Neue Farbe für Hover-Zustand - backgroundError: '#FFEBEE', // Helles Rot für Fehlerhintergrund - textPrimary: '#000000', - textSecondary: '#666666', - textTertiary: '#999999', - borderPrimary: '#bbbbbb', - borderSecondary: '#282828', - textOnPrimary: '#000000', // Schwarz für Text auf primärer Farbe - error: '#e74c3c', // Rot für Gefahren-Buttons - backgroundFree: '#95a5a6', // Grau - backgroundPlus: '#f39c12', // Orange - backgroundPro: '#f8d62b', // Gelb - backgroundUltra: '#e74c3c', // Rot -}; - -export const darkColors = { - primary: '#f8d62b', - backgroundForPrimary: '#383838', - primaryHover: '#f8d62b', - primaryPressed: '#1f6da3', - secondary: '#D4B200', - secondaryHover: '#27ae60', - secondaryPressed: '#229954', - backgroundPage: '#121212', - backgroundPrimary: '#1e1e1e', - backgroundSecondary: '#2c2c2c', - backgroundTertiary: '#383838', // Neue Farbe für Hover-Zustand - backgroundError: '#260000', // Dunkles Rot für Fehlerhintergrund - textPrimary: '#ffffff', - textSecondary: '#cccccc', - textTertiary: '#999999', - textOnPrimary: '#000000', - borderPrimary: '#424242', - borderSecondary: '#282828', - error: '#e74c3c', // Rot für Gefahren-Buttons - backgroundFree: '#95a5a6', // Grau - backgroundPlus: '#f39c12', // Orange - backgroundPro: '#f8d62b', // Gelb - backgroundUltra: '#e74c3c', // Rot -}; - -// Nature theme colors -export const natureLightColors = { - primary: '#81C784', - backgroundForPrimary: '#2E7D32', - primaryHover: '#66BB6A', - primaryPressed: '#4CAF50', - secondary: '#A5D6A7', - secondaryHover: '#81C784', - secondaryPressed: '#66BB6A', - backgroundPage: '#F1F8E9', - backgroundPrimary: '#FFFFFF', - backgroundSecondary: '#F9FBE7', - backgroundTertiary: '#F0F4C3', - backgroundError: '#FFEBEE', - textPrimary: '#1B5E20', - textSecondary: '#33691E', - textTertiary: '#558B2F', - textOnPrimary: '#000000', - borderPrimary: '#C8E6C9', - borderSecondary: '#A5D6A7', - error: '#E57373', - backgroundFree: '#AED581', - backgroundPlus: '#9CCC65', - backgroundPro: '#8BC34A', - backgroundUltra: '#7CB342', -}; - -export const natureDarkColors = { - primary: '#81C784', - backgroundForPrimary: '#2E7D32', - primaryHover: '#66BB6A', - primaryPressed: '#4CAF50', - secondary: '#A5D6A7', - secondaryHover: '#81C784', - secondaryPressed: '#66BB6A', - backgroundPage: '#1B1B1B', - backgroundPrimary: '#1E1E1E', - backgroundSecondary: '#2C2C2C', - backgroundTertiary: '#333333', - backgroundError: '#CF6679', - textPrimary: '#FFFFFF', - textSecondary: '#C8E6C9', - textTertiary: '#A5D6A7', - textOnPrimary: '#000000', - borderPrimary: '#2E7D32', - borderSecondary: '#1B5E20', - error: '#CF6679', - backgroundFree: '#558B2F', - backgroundPlus: '#7CB342', - backgroundPro: '#8BC34A', - backgroundUltra: '#9CCC65', -}; - -// Stone theme colors -export const stoneLightColors = { - primary: '#90A4AE', - backgroundForPrimary: '#455A64', - primaryHover: '#78909C', - primaryPressed: '#607D8B', - secondary: '#B0BEC5', - secondaryHover: '#90A4AE', - secondaryPressed: '#78909C', - backgroundPage: '#ECEFF1', - backgroundPrimary: '#FFFFFF', - backgroundSecondary: '#F5F5F5', - backgroundTertiary: '#EEEEEE', - backgroundError: '#FFEBEE', - textPrimary: '#263238', - textSecondary: '#37474F', - textTertiary: '#455A64', - textOnPrimary: '#000000', - borderPrimary: '#CFD8DC', - borderSecondary: '#B0BEC5', - error: '#EF5350', - backgroundFree: '#90A4AE', - backgroundPlus: '#78909C', - backgroundPro: '#607D8B', - backgroundUltra: '#546E7A', -}; - -export const stoneDarkColors = { - primary: '#90A4AE', - backgroundForPrimary: '#455A64', - primaryHover: '#78909C', - primaryPressed: '#607D8B', - secondary: '#B0BEC5', - secondaryHover: '#90A4AE', - secondaryPressed: '#78909C', - backgroundPage: '#121212', - backgroundPrimary: '#1A1A1A', - backgroundSecondary: '#242424', - backgroundTertiary: '#2C2C2C', - backgroundError: '#CF6679', - textPrimary: '#FFFFFF', - textSecondary: '#B0BEC5', - textTertiary: '#90A4AE', - textOnPrimary: '#000000', - borderPrimary: '#455A64', - borderSecondary: '#37474F', - error: '#CF6679', - backgroundFree: '#546E7A', - backgroundPlus: '#607D8B', - backgroundPro: '#78909C', - backgroundUltra: '#90A4AE', -}; - -// Schriftgrößen -const fontSizes = { - small: 14, - body: 16, - subtitle: 18, - title: 20, - h1: 28, // Neue Zeile für h1 - h2: 24, // Neue Zeile für h2 -}; - -// Schriftstärken -const fontWeights = { - regular: '400', - medium: '500', - bold: '700', -}; - -// Abstände -const spacing = { - none: 0, - xxs: 2, // Neuer Wert für sehr kleine Abstände - xsmall: 4, - small: 8, - medium: 12, - large: 24, - xl: 32, - xxl: 48, - xxxl: 64, - horizontalPageMargin: 16, - verticalPageMargin: 24, - sectionSpacing: 40, - elementSpacing: 20, - inlineElementSpacing: 12, - cardPadding: 16, - headerHeight: 60, -}; - -// Rundungen -const borderRadius = { - small: 4, - medium: 12, - large: 16, - round: 9999, -}; - -// Schatten aktualisieren -const shadows = { - small: '0px 2px 4px 0px rgba(0, 0, 0, 0.1)', - medium: '0px 4px 8px 0px rgba(0, 0, 0, 0.15)', - large: '0px 6px 12px 0px rgba(0, 0, 0, 0.2)', -}; - -// Z-Index-Werte -const zIndex = { - base: 1, - dropdown: 1000, - modal: 2000, - tooltip: 3000, -}; - -// Neue TagColors mit englischen Namen -export const tagColors = { - blue: '#3498db', - green: '#2ecc71', - red: '#e74c3c', - orange: '#f39c12', - purple: '#9b59b6', - teal: '#1abc9c', - pink: '#e84393', - gray: '#95a5a6', -}; - -// Updated getTheme function to support multiple themes -export const getTheme = (colorScheme: ColorSchemeName, themeVariant: ThemeVariant = 'lume') => { - let colors; - switch (themeVariant) { - case 'nature': - colors = colorScheme === 'dark' ? natureDarkColors : natureLightColors; - break; - case 'stone': - colors = colorScheme === 'dark' ? stoneDarkColors : stoneLightColors; - break; - default: - colors = colorScheme === 'dark' ? darkColors : lightColors; - } - - return { - colors, - tagColors, - fontSizes, - fontWeights, - spacing, - borderRadius, - shadows, - zIndex, - }; -}; - -// Typdefinition für das Theme -export type Theme = ReturnType; - -// Am Ende der Datei fügen Sie diese Zeile hinzu: -export const defaultTheme = getTheme('light'); diff --git a/apps/presi/apps/mobile/eas.json b/apps/presi/apps/mobile/eas.json deleted file mode 100644 index 5e27de9bc..000000000 --- a/apps/presi/apps/mobile/eas.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "cli": { - "version": ">= 13.4.2", - "appVersionSource": "remote" - }, - "build": { - "development": { - "developmentClient": true, - "distribution": "internal" - }, - "preview": { - "distribution": "internal" - }, - "production": { - "autoIncrement": true - } - }, - "submit": { - "production": {} - } -} diff --git a/apps/presi/apps/mobile/eslint.config.mjs b/apps/presi/apps/mobile/eslint.config.mjs deleted file mode 100644 index 0a8233f8d..000000000 --- a/apps/presi/apps/mobile/eslint.config.mjs +++ /dev/null @@ -1,12 +0,0 @@ -// @ts-check -import { baseConfig, typescriptConfig, reactConfig, prettierConfig } from '@manacore/eslint-config'; - -export default [ - { - ignores: ['dist/**', '.expo/**', 'node_modules/**', 'android/**', 'ios/**'], - }, - ...baseConfig, - ...typescriptConfig, - ...reactConfig, - ...prettierConfig, -]; diff --git a/apps/presi/apps/mobile/metro.config.js b/apps/presi/apps/mobile/metro.config.js deleted file mode 100644 index 948c81033..000000000 --- a/apps/presi/apps/mobile/metro.config.js +++ /dev/null @@ -1,6 +0,0 @@ -const { getDefaultConfig } = require('@expo/metro-config'); - -const defaultConfig = getDefaultConfig(__dirname); -defaultConfig.resolver.sourceExts.push('cjs'); - -module.exports = defaultConfig; diff --git a/apps/presi/apps/mobile/package.json b/apps/presi/apps/mobile/package.json deleted file mode 100644 index ef359eef7..000000000 --- a/apps/presi/apps/mobile/package.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "name": "@presi/mobile", - "version": "0.2.0", - "private": true, - "main": "expo-router/entry", - "scripts": { - "dev": "expo start", - "android": "expo start --android", - "ios": "expo start --ios", - "web": "expo start --web", - "lint": "eslint .", - "format": "eslint . --fix", - "type-check": "echo 'Skipping type-check: @presi/mobile needs Firebase migration'" - }, - "dependencies": { - "@expo/vector-icons": "^14.0.2", - "@presi/shared": "workspace:*", - "@react-navigation/bottom-tabs": "^7.0.0", - "@react-navigation/native": "^7.0.0", - "expo": "~52.0.11", - "expo-blur": "~14.0.1", - "expo-constants": "~17.0.3", - "expo-font": "~13.0.1", - "expo-haptics": "~14.0.0", - "expo-image-picker": "~16.0.3", - "expo-linking": "~7.0.3", - "expo-router": "~4.0.9", - "expo-screen-orientation": "~8.0.0", - "expo-secure-store": "~14.0.0", - "expo-splash-screen": "~0.29.13", - "expo-status-bar": "~2.0.0", - "expo-symbols": "~0.2.0", - "expo-system-ui": "~4.0.4", - "expo-web-browser": "~14.0.1", - "react": "18.3.1", - "react-dom": "18.3.1", - "react-native": "0.76.3", - "react-native-gesture-handler": "~2.20.2", - "react-native-reanimated": "~3.16.1", - "react-native-safe-area-context": "4.12.0", - "react-native-screens": "~4.1.0", - "react-native-web": "~0.19.13", - "react-native-webview": "13.12.2", - "zeego": "^2.0.4", - "zustand": "^5.0.0" - }, - "devDependencies": { - "@babel/core": "^7.25.2", - "@types/react": "~18.3.12", - "eslint": "^9.39.1", - "typescript": "^5.7.2" - } -} diff --git a/apps/presi/apps/mobile/services/auth.ts b/apps/presi/apps/mobile/services/auth.ts deleted file mode 100644 index 98cbdbfbf..000000000 --- a/apps/presi/apps/mobile/services/auth.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - signInWithEmailAndPassword, - createUserWithEmailAndPassword, - signOut, - sendPasswordResetEmail, - onAuthStateChanged, - type User, -} from 'firebase/auth'; -import { auth } from '../firebaseConfig'; -import { doc, setDoc } from 'firebase/firestore'; -import { db } from '../firebaseConfig'; - -export const loginUser = async (email: string, password: string): Promise => { - const userCredential = await signInWithEmailAndPassword(auth, email, password); - return userCredential.user; -}; - -export const registerUser = async (email: string, password: string): Promise => { - const userCredential = await createUserWithEmailAndPassword(auth, email, password); - - // Create user document in Firestore - await setDoc(doc(db, 'users', userCredential.user.uid), { - email: userCredential.user.email, - createdAt: new Date(), - updatedAt: new Date(), - }); - - return userCredential.user; -}; - -export const logoutUser = async (): Promise => { - await signOut(auth); -}; - -export const resetPassword = async (email: string): Promise => { - await sendPasswordResetEmail(auth, email); -}; - -export const getCurrentUser = (): User | null => { - return auth.currentUser; -}; - -export const onAuthStateChange = (callback: (user: User | null) => void): (() => void) => { - return onAuthStateChanged(auth, callback); -}; - -export const isAuthenticated = (): boolean => { - return auth.currentUser !== null; -}; diff --git a/apps/presi/apps/mobile/services/firestore.ts b/apps/presi/apps/mobile/services/firestore.ts deleted file mode 100644 index d29ab9c8c..000000000 --- a/apps/presi/apps/mobile/services/firestore.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { - collection, - query, - where, - getDocs, - addDoc, - updateDoc, - deleteDoc, - doc, - getDoc, - orderBy, - limit, - writeBatch, -} from 'firebase/firestore'; -import { db, auth } from '../firebaseConfig'; -import { Deck } from '../types/models'; -import type { Slide } from '../types/models'; - -// Decks -export const getUserDecks = async (userId: string): Promise => { - console.log('[Firestore] Getting all decks for user:', userId); - - try { - const decksRef = collection(db, 'decks'); - const q = query(decksRef, where('userId', '==', userId), orderBy('createdAt', 'desc')); - - const querySnapshot = await getDocs(q); - const decks = querySnapshot.docs.map((doc) => { - const data = doc.data(); - return { - id: doc.id, - ...data, - createdAt: data.createdAt?.toDate() || new Date(), - updatedAt: data.updatedAt?.toDate() || new Date(), - } as Deck; - }); - - console.log('[Firestore] Retrieved decks:', decks.length); - return decks; - } catch (error) { - console.error('[Firestore] Error getting decks:', error); - throw error; - } -}; - -export const createDeck = async (deckData: Partial): Promise => { - console.log('[Firestore] Creating new deck:', deckData); - - try { - if (!auth.currentUser) { - throw new Error('No authenticated user'); - } - - const now = new Date(); - const newDeck = { - ...deckData, - userId: auth.currentUser.uid, - createdAt: now, - updatedAt: now, - sharing: { - isPublic: false, - collaborators: {}, - }, - }; - - const docRef = await addDoc(collection(db, 'decks'), newDeck); - - return { - id: docRef.id, - ...newDeck, - } as Deck; - } catch (error) { - console.error('[Firestore] Error creating deck:', error); - throw error; - } -}; - -export const getDeck = async (deckId: string): Promise => { - console.log('[Firestore] Getting deck:', deckId); - - try { - const deckRef = doc(db, 'decks', deckId); - const deckDoc = await getDoc(deckRef); - - if (!deckDoc.exists()) { - throw new Error('Deck not found'); - } - - const data = deckDoc.data(); - return { - id: deckDoc.id, - ...data, - createdAt: data.createdAt?.toDate() || new Date(), - updatedAt: data.updatedAt?.toDate() || new Date(), - } as Deck; - } catch (error) { - console.error('[Firestore] Error getting deck:', error); - throw error; - } -}; - -export const deleteDeck = async (deckId: string): Promise => { - try { - console.log('[Firestore] Deleting deck:', deckId); - const deckRef = doc(db, 'decks', deckId); - await deleteDoc(deckRef); - console.log('[Firestore] Deck deleted successfully'); - } catch (error) { - console.error('[Firestore] Error deleting deck:', error); - throw error; - } -}; - -// Slides -export const getDeckSlides = async (deckId: string): Promise => { - console.log('[Firestore] Getting slides for deck:', deckId); - - try { - const slidesRef = collection(db, 'decks', deckId, 'slides'); - const q = query(slidesRef, orderBy('order', 'asc')); - const querySnapshot = await getDocs(q); - - const slides = querySnapshot.docs.map((doc) => ({ - id: doc.id, - ...doc.data(), - createdAt: doc.data().createdAt?.toDate() || new Date(), - updatedAt: doc.data().updatedAt?.toDate() || new Date(), - })) as Slide[]; - - console.log('[Firestore] Retrieved slides:', slides.length); - return slides; - } catch (error) { - console.error('[Firestore] Error getting slides:', error); - throw error; - } -}; - -export const createSlide = async (slideData: Partial): Promise => { - console.log('[Firestore] Creating new slide:', slideData); - - if (!slideData.deckId) { - throw new Error('deckId is required to create a slide'); - } - - try { - const slidesRef = collection(db, 'decks', slideData.deckId, 'slides'); - const now = new Date(); - - // Get the current highest order - const q = query(slidesRef, orderBy('order', 'desc'), limit(1)); - const querySnapshot = await getDocs(q); - const highestOrder = querySnapshot.empty ? 0 : querySnapshot.docs[0].data().order; - - const newSlide = { - ...slideData, - order: highestOrder + 1, - createdAt: now, - updatedAt: now, - }; - - const docRef = await addDoc(slidesRef, newSlide); - return { - id: docRef.id, - ...newSlide, - } as Slide; - } catch (error) { - console.error('[Firestore] Error creating slide:', error); - throw error; - } -}; - -export const updateSlide = async (slideId: string, slideData: Partial): Promise => { - console.log('[Firestore] Updating slide:', slideId, slideData); - - if (!slideData.deckId) { - throw new Error('deckId is required to update a slide'); - } - - try { - const slideRef = doc(db, 'decks', slideData.deckId, 'slides', slideId); - await updateDoc(slideRef, { - ...slideData, - updatedAt: new Date(), - }); - } catch (error) { - console.error('[Firestore] Error updating slide:', error); - throw error; - } -}; - -export const deleteSlide = async (slideId: string, deckId: string): Promise => { - console.log('[Firestore] Deleting slide:', slideId); - - try { - const slideRef = doc(db, 'decks', deckId, 'slides', slideId); - await deleteDoc(slideRef); - } catch (error) { - console.error('[Firestore] Error deleting slide:', error); - throw error; - } -}; - -export const reorderSlide = async ( - slideId: string, - newOrder: number, - deckId: string -): Promise => { - console.log('[Firestore] Reordering slide:', slideId, 'to order:', newOrder); - - try { - const slidesRef = collection(db, 'decks', deckId, 'slides'); - const batch = writeBatch(db); - const now = new Date(); - - // Get all slides in the deck - const q = query(slidesRef, orderBy('order', 'asc')); - const querySnapshot = await getDocs(q); - const slides = querySnapshot.docs; - - // Find the current slide and its order - const currentSlide = slides.find((doc) => doc.id === slideId); - if (!currentSlide) { - throw new Error('Slide not found'); - } - const currentOrder = currentSlide.data().order; - - // Update orders - slides.forEach((doc) => { - const slideOrder = doc.data().order; - if (doc.id === slideId) { - // Update the target slide - batch.update(doc.ref, { - order: newOrder, - updatedAt: now, - }); - } else if (newOrder > currentOrder && slideOrder > currentOrder && slideOrder <= newOrder) { - // Move slides up - batch.update(doc.ref, { - order: slideOrder - 1, - updatedAt: now, - }); - } else if (newOrder < currentOrder && slideOrder >= newOrder && slideOrder < currentOrder) { - // Move slides down - batch.update(doc.ref, { - order: slideOrder + 1, - updatedAt: now, - }); - } - }); - - await batch.commit(); - console.log('[Firestore] Reorder operation completed successfully'); - } catch (error) { - console.error('[Firestore] Error reordering slide:', error); - throw error; - } -}; - -export const migrateDecksToNewSchema = async (userId: string) => { - console.log('[Firestore] Migrating decks to new schema for user:', userId); - - try { - const decksRef = collection(db, 'decks'); - const q = query(decksRef, where('userId', '==', userId)); - - const querySnapshot = await getDocs(q); - const batch = writeBatch(db); - let updateCount = 0; - - querySnapshot.docs.forEach((docSnapshot) => { - const deckData = docSnapshot.data(); - if (!deckData.sharing) { - batch.update(docSnapshot.ref, { - sharing: { - isPublic: false, - collaborators: {}, - }, - }); - updateCount++; - } - }); - - if (updateCount > 0) { - await batch.commit(); - console.log(`[Firestore] Successfully migrated ${updateCount} decks`); - } else { - console.log('[Firestore] No decks needed migration'); - } - } catch (error) { - console.error('[Firestore] Error migrating decks:', error); - throw error; - } -}; diff --git a/apps/presi/apps/mobile/services/storage.ts b/apps/presi/apps/mobile/services/storage.ts deleted file mode 100644 index f5d42a366..000000000 --- a/apps/presi/apps/mobile/services/storage.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { storage } from '../firebaseConfig'; -import { ref, uploadBytes, getDownloadURL } from 'firebase/storage'; - -export const uploadImage = async (file: Blob, path: string): Promise => { - try { - const storageRef = ref(storage, path); - const metadata = { - contentType: 'image/jpeg', - cacheControl: 'public,max-age=3600', - }; - const snapshot = await uploadBytes(storageRef, file, metadata); - const downloadURL = await getDownloadURL(snapshot.ref); - return downloadURL; - } catch (error) { - console.error('[Storage] Error uploading image:', error); - throw error; - } -}; - -export const uploadImages = async (files: Blob[], basePath: string): Promise => { - try { - const uploadPromises = files.map((file, index) => { - const path = `${basePath}/${index}_${Date.now()}.jpg`; - return uploadImage(file, path); - }); - return await Promise.all(uploadPromises); - } catch (error) { - console.error('[Storage] Error uploading images:', error); - throw error; - } -}; diff --git a/apps/presi/apps/mobile/theme/README.md b/apps/presi/apps/mobile/theme/README.md deleted file mode 100644 index 14ee76c7a..000000000 --- a/apps/presi/apps/mobile/theme/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# Portable Theme Module - -Dieses Modul enthält ein komplettes Theme-System für React Native Apps mit: - -- Hell/Dunkel Modus (inkl. System-Einstellung) -- Kontrast-Einstellungen (5 Stufen) -- Theme Provider & Hooks -- Fertige UI-Komponenten für Theme-Einstellungen - -## Installation - -1. Kopiere den gesamten `theme` Ordner in dein Projekt -2. Installiere die benötigten Dependencies: - -```bash -npm install @react-native-async-storage/async-storage -``` - -## Verwendung - -1. Wrapp deine App mit dem ThemeProvider: - -```tsx -import { ThemeProvider } from './theme'; - -export default function App() { - return ( - - - - ); -} -``` - -2. Nutze den useTheme Hook in deinen Komponenten: - -```tsx -import { useTheme } from './theme'; - -export function MyComponent() { - const { theme, isDark } = useTheme(); - return ( - - Hello World - - ); -} -``` - -3. Füge die ThemeSettings Komponente in deine Settings-Seite ein: - -```tsx -import { ThemeSettings } from './theme'; - -export function SettingsScreen() { - return ( - - - - ); -} -``` diff --git a/apps/presi/apps/mobile/theme/ThemeProvider.tsx b/apps/presi/apps/mobile/theme/ThemeProvider.tsx deleted file mode 100644 index 64a255d29..000000000 --- a/apps/presi/apps/mobile/theme/ThemeProvider.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import React, { createContext, useContext, useState, useEffect } from 'react'; -import { useColorScheme } from 'react-native'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { getTheme } from './constants'; -import type { Theme } from './constants'; - -export type ColorMode = 'system' | 'light' | 'dark'; -export type ContrastLevel = 1 | 2 | 3 | 4 | 5; - -const STORAGE_KEYS = { - COLOR_MODE: '@theme/colorMode', - CONTRAST_LEVEL: '@theme/contrastLevel', -}; - -type ThemeContextType = { - theme: Theme; - isDark: boolean; - colorMode: ColorMode; - setColorMode: (mode: ColorMode) => void; - contrastLevel: ContrastLevel; - setContrastLevel: (level: ContrastLevel) => void; -}; - -const ThemeContext = createContext({ - theme: getTheme('light'), - isDark: false, - colorMode: 'system', - setColorMode: () => {}, - contrastLevel: 3, - setContrastLevel: () => {}, -}); - -export const useTheme = () => useContext(ThemeContext); - -// Hilfsfunktion zum Konvertieren von Hex zu RGB -const hexToRgb = (hex: string) => { - const h = hex.replace('#', ''); - return { - r: parseInt(h.substr(0, 2), 16), - g: parseInt(h.substr(2, 2), 16), - b: parseInt(h.substr(4, 2), 16), - }; -}; - -// Hilfsfunktion zum Konvertieren von RGB zu Hex mit Alpha -const rgbaToHex = (r: number, g: number, b: number, a = 1) => { - const alpha = Math.round(a * 255); - return ( - '#' + - [r, g, b, alpha] - .map((x) => { - const hex = Math.round(Math.max(0, Math.min(255, x))).toString(16); - return hex.length === 1 ? '0' + hex : hex; - }) - .join('') - ); -}; - -// Funktion zum Anpassen des Kontrasts -const adjustContrast = ( - color: string, - level: ContrastLevel, - type: 'text' | 'primary' | 'background', - isDark: boolean -): string => { - if (level === 3) return color; - - const { r, g, b } = hexToRgb(color); - - if (level < 3) { - // Niedrigerer Kontrast: Nur Text-Opacity wird reduziert - if (type === 'text') { - const opacity = 0.5 + (level - 1) * 0.25; // 0.5 für Level 1, 0.75 für Level 2 - return rgbaToHex(r, g, b, opacity); - } - return color; - } else { - // Höherer Kontrast: Nur Hintergründe werden angepasst - if (type === 'background') { - const factor = (level - 3) * 0.45; // 0.45 für Level 4, 0.9 für Level 5 - if (isDark) { - // Im Dark Mode: Hintergründe werden schwärzer - return rgbaToHex( - Math.round(r * (1 - factor)), - Math.round(g * (1 - factor)), - Math.round(b * (1 - factor)) - ); - } else { - // Im Light Mode: Hintergründe werden weißer - return rgbaToHex( - Math.round(r + (255 - r) * factor), - Math.round(g + (255 - g) * factor), - Math.round(b + (255 - b) * factor) - ); - } - } - return color; - } -}; - -// Funktion zum Anpassen des gesamten Themes basierend auf dem Kontrast-Level -const adjustThemeContrast = (theme: Theme, level: ContrastLevel, isDark: boolean): Theme => { - return { - ...theme, - colors: { - ...theme.colors, - textPrimary: adjustContrast(theme.colors.textPrimary, level, 'text', isDark), - textSecondary: adjustContrast(theme.colors.textSecondary, level, 'text', isDark), - backgroundPage: adjustContrast(theme.colors.backgroundPage, level, 'background', isDark), - backgroundPrimary: adjustContrast( - theme.colors.backgroundPrimary, - level, - 'background', - isDark - ), - backgroundSecondary: adjustContrast( - theme.colors.backgroundSecondary, - level, - 'background', - isDark - ), - }, - }; -}; - -export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const systemColorScheme = useColorScheme(); - const [colorMode, setColorMode] = useState('system'); - const [contrastLevel, setContrastLevel] = useState(3); - - // Lade gespeicherte Einstellungen - useEffect(() => { - const loadSettings = async () => { - try { - const savedColorMode = await AsyncStorage.getItem(STORAGE_KEYS.COLOR_MODE); - if (savedColorMode) { - setColorMode(savedColorMode as ColorMode); - } - - const savedContrastLevel = await AsyncStorage.getItem(STORAGE_KEYS.CONTRAST_LEVEL); - if (savedContrastLevel) { - setContrastLevel(parseInt(savedContrastLevel) as ContrastLevel); - } - } catch (error) { - console.error('Error loading theme settings:', error); - } - }; - loadSettings(); - }, []); - - // Speichere Einstellungen bei Änderungen - const handleColorModeChange = async (mode: ColorMode) => { - try { - await AsyncStorage.setItem(STORAGE_KEYS.COLOR_MODE, mode); - setColorMode(mode); - } catch (error) { - console.error('Error saving color mode:', error); - } - }; - - const handleContrastLevelChange = async (level: ContrastLevel) => { - try { - await AsyncStorage.setItem(STORAGE_KEYS.CONTRAST_LEVEL, level.toString()); - setContrastLevel(level); - } catch (error) { - console.error('Error saving contrast level:', error); - } - }; - - // Bestimme den aktiven Modus - const isDark = colorMode === 'system' ? systemColorScheme === 'dark' : colorMode === 'dark'; - - // Hole das Basis-Theme und passe den Kontrast an - const baseTheme = getTheme(isDark ? 'dark' : 'light'); - const theme = adjustThemeContrast(baseTheme, contrastLevel, isDark); - - return ( - - {children} - - ); -}; diff --git a/apps/presi/apps/mobile/theme/ThemeSettings.tsx b/apps/presi/apps/mobile/theme/ThemeSettings.tsx deleted file mode 100644 index 84f61b486..000000000 --- a/apps/presi/apps/mobile/theme/ThemeSettings.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React from 'react'; -import { View, Text, TouchableOpacity, StyleSheet, Pressable } from 'react-native'; -import { useTheme } from './ThemeProvider'; -import type { ColorMode, ContrastLevel } from './ThemeProvider'; - -const COLOR_MODES: { label: string; value: ColorMode }[] = [ - { label: 'System', value: 'system' }, - { label: 'Hell', value: 'light' }, - { label: 'Dunkel', value: 'dark' }, -]; - -const CONTRAST_LABELS: Record = { - 1: 'Sehr niedrig', - 2: 'Niedrig', - 3: 'Standard', - 4: 'Hoch', - 5: 'Sehr hoch', -}; - -export const ThemeSettings = () => { - const { theme, colorMode, setColorMode, contrastLevel, setContrastLevel } = useTheme(); - - return ( - - {/* Helligkeits-Einstellungen */} - - Helligkeit: - - {COLOR_MODES.map((mode) => ( - setColorMode(mode.value)} - > - - {mode.label} - - - ))} - - - - {/* Kontrast-Einstellungen */} - - Kontrast: - - - {([1, 2, 3, 4, 5] as ContrastLevel[]).map((level) => ( - setContrastLevel(level)} - /> - ))} - - - {CONTRAST_LABELS[contrastLevel]} - - - - - ); -}; - -const styles = StyleSheet.create({ - container: { - width: '100%', - gap: 16, - }, - section: { - padding: 16, - borderRadius: 12, - gap: 12, - }, - sectionTitle: { - fontSize: 16, - fontWeight: '600', - }, - colorModeList: { - flexDirection: 'row', - gap: 8, - }, - colorModeOption: { - flex: 1, - padding: 12, - borderRadius: 8, - alignItems: 'center', - }, - colorModeText: { - fontSize: 14, - fontWeight: '500', - }, - contrastContainer: { - gap: 12, - }, - contrastSlider: { - flexDirection: 'row', - gap: 8, - alignItems: 'center', - }, - contrastOption: { - flex: 1, - height: 4, - borderRadius: 2, - }, - contrastLabel: { - fontSize: 14, - textAlign: 'center', - }, -}); diff --git a/apps/presi/apps/mobile/theme/constants.ts b/apps/presi/apps/mobile/theme/constants.ts deleted file mode 100644 index 605b6699e..000000000 --- a/apps/presi/apps/mobile/theme/constants.ts +++ /dev/null @@ -1,52 +0,0 @@ -export type ThemeVariant = 'default' | 'modern' | 'classic' | 'dark' | 'light'; - -export interface Theme { - colors: { - primary: string; - backgroundPage: string; - backgroundPrimary: string; - backgroundSecondary: string; - textPrimary: string; - textSecondary: string; - error: string; - success: string; - }; -} - -export const THEME_NAMES: Record = { - default: 'Standard', - modern: 'Modern', - classic: 'Klassisch', - dark: 'Dunkel', - light: 'Hell', -}; - -const LIGHT_THEME: Theme = { - colors: { - primary: '#007AFF', - backgroundPage: '#F2F2F7', - backgroundPrimary: '#FFFFFF', - backgroundSecondary: '#F2F2F7', - textPrimary: '#000000', - textSecondary: '#6C6C6C', - error: '#FF3B30', - success: '#34C759', - }, -}; - -const DARK_THEME: Theme = { - colors: { - primary: '#0A84FF', - backgroundPage: '#000000', - backgroundPrimary: '#1C1C1E', - backgroundSecondary: '#2C2C2E', - textPrimary: '#FFFFFF', - textSecondary: '#8E8E93', - error: '#FF453A', - success: '#32D74B', - }, -}; - -export function getTheme(mode: 'light' | 'dark'): Theme { - return mode === 'light' ? LIGHT_THEME : DARK_THEME; -} diff --git a/apps/presi/apps/mobile/theme/index.ts b/apps/presi/apps/mobile/theme/index.ts deleted file mode 100644 index c2fc45fb5..000000000 --- a/apps/presi/apps/mobile/theme/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './constants'; -export * from './ThemeProvider'; -export * from './ThemeSettings'; diff --git a/apps/presi/apps/mobile/tsconfig.json b/apps/presi/apps/mobile/tsconfig.json deleted file mode 100644 index 49511b927..000000000 --- a/apps/presi/apps/mobile/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "expo/tsconfig.base", - "compilerOptions": { - "strict": true, - "paths": { - "@/*": ["./*"] - } - }, - "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] -} diff --git a/apps/presi/apps/mobile/types/models.ts b/apps/presi/apps/mobile/types/models.ts deleted file mode 100644 index 5330d163d..000000000 --- a/apps/presi/apps/mobile/types/models.ts +++ /dev/null @@ -1,35 +0,0 @@ -export interface Deck { - id: string; - name: string; - description?: string; - createdAt: Date; - updatedAt: Date; - userId: string; - sharing: DeckSharing; -} - -export type CollaboratorRole = 'viewer' | 'editor'; - -export interface DeckSharing { - isPublic: boolean; - collaborators: { - [userId: string]: CollaboratorRole; - }; - shareLink?: string; - expiresAt?: Date; -} - -export interface Slide { - id: string; - deckId: string; - order: number; - imageUrl?: string; - title: string; - fullText?: string; - summary?: string; - bulletPoints?: string[]; - notes?: string; - altText?: string; - createdAt: Date; - updatedAt: Date; -} diff --git a/package.json b/package.json index 1d1c8e19b..38a49353f 100644 --- a/package.json +++ b/package.json @@ -196,7 +196,6 @@ "presi:dev": "turbo run dev --filter=presi...", "dev:presi:web": "pnpm --filter @presi/web dev", "dev:presi:landing": "pnpm --filter @presi/landing dev", - "dev:presi:mobile": "pnpm --filter @presi/mobile dev", "dev:presi:backend": "pnpm --filter @presi/backend dev", "dev:presi:app": "turbo run dev --filter=@presi/web --filter=@presi/backend", "dev:presi:full": "./scripts/setup-databases.sh presi && ./scripts/setup-databases.sh auth && concurrently -n auth,backend,web -c blue,green,cyan \"pnpm dev:auth\" \"pnpm dev:presi:backend\" \"pnpm dev:presi:web\"", @@ -225,6 +224,7 @@ "dev:traces:full": "./scripts/setup-databases.sh traces && ./scripts/setup-databases.sh auth && concurrently -n auth,backend,mobile -c blue,green,cyan \"pnpm dev:auth\" \"pnpm dev:traces:backend\" \"pnpm dev:traces:mobile\"", "traces:db:push": "pnpm --filter @traces/backend db:push", "traces:db:studio": "pnpm --filter @traces/backend db:studio", + "dev:landing-builder": "pnpm --filter @mana-landing-builder/service start:dev", "docker:up": "docker compose -f docker-compose.dev.yml --env-file .env.development up -d postgres redis minio minio-init", "docker:up:infra": "docker compose -f docker-compose.dev.yml --env-file .env.development up -d postgres redis minio minio-init", "docker:up:db": "docker compose -f docker-compose.dev.yml --env-file .env.development up -d postgres redis", diff --git a/scripts/generate-env.mjs b/scripts/generate-env.mjs index 702365fa8..4fbb030bb 100644 --- a/scripts/generate-env.mjs +++ b/scripts/generate-env.mjs @@ -299,15 +299,6 @@ const APP_CONFIGS = [ }, }, - // Presi Mobile (Expo) - { - path: 'apps/presi/apps/mobile/.env', - vars: { - EXPO_PUBLIC_BACKEND_URL: (env) => `http://localhost:${env.PRESI_BACKEND_PORT || '3008'}`, - EXPO_PUBLIC_MANA_CORE_AUTH_URL: (env) => env.MANA_CORE_AUTH_URL, - }, - }, - // Presi Web (SvelteKit) { path: 'apps/presi/apps/web/.env',