chore: remove presi mobile app, update outdated mobile docs

- Delete apps/presi/apps/mobile/ (outdated Firebase-based, web app is primary)
- Update presi CLAUDE.md: remove mobile references, fix landing page status
- Update manadeck mobile CLAUDE.md: replace Supabase refs with Mana Core Auth
- Fix picture mobile: pin nativewind version, add shared-logger dependency
- Remove dev:presi:mobile script and env generation config

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-23 13:16:29 +01:00
parent efcb18a98f
commit da6dd4ecb8
62 changed files with 21 additions and 6528 deletions

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -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"
}
}
}
}

View file

@ -1,33 +0,0 @@
import { Stack } from 'expo-router';
export default function AuthLayout() {
return (
<Stack
screenOptions={{
headerShown: false,
contentStyle: {
backgroundColor: '#f5f5f5',
},
}}
>
<Stack.Screen
name="login"
options={{
title: 'Login',
}}
/>
<Stack.Screen
name="register"
options={{
title: 'Register',
}}
/>
<Stack.Screen
name="forgot-password"
options={{
title: 'Reset Password',
}}
/>
</Stack>
);
}

View file

@ -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 (
<View style={styles.container}>
<View style={styles.formContainer}>
<Text style={styles.title}>Check Your Email</Text>
<Text style={styles.message}>We've sent password reset instructions to {email}</Text>
<TouchableOpacity style={styles.button} onPress={() => router.replace('/login')}>
<Text style={styles.buttonText}>Return to Login</Text>
</TouchableOpacity>
</View>
</View>
);
}
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container}
>
<View style={styles.formContainer}>
<Text style={styles.title}>Reset Password</Text>
<Text style={styles.subtitle}>Enter your email to receive reset instructions</Text>
<TextInput
style={styles.input}
placeholder="Email"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
autoComplete="email"
/>
<TouchableOpacity
style={[styles.button, isLoading && styles.buttonDisabled]}
onPress={handleResetPassword}
disabled={isLoading}
>
<Text style={styles.buttonText}>
{isLoading ? 'Sending...' : 'Send Reset Instructions'}
</Text>
</TouchableOpacity>
<View style={styles.links}>
<Link href="/login" style={styles.link}>
Back to Login
</Link>
</View>
</View>
</KeyboardAvoidingView>
);
}
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,
},
});

View file

@ -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 (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container}
>
<View style={styles.formContainer}>
<Text style={styles.title}>Welcome Back</Text>
<Text style={styles.subtitle}>Sign in to continue</Text>
<TextInput
style={styles.input}
placeholder="Email"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
autoComplete="email"
/>
<TextInput
style={styles.input}
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
autoComplete="password"
/>
<TouchableOpacity
style={[styles.button, isLoading && styles.buttonDisabled]}
onPress={handleLogin}
disabled={isLoading}
>
<Text style={styles.buttonText}>{isLoading ? 'Signing in...' : 'Sign In'}</Text>
</TouchableOpacity>
<View style={styles.links}>
<Link href="/forgot-password" style={styles.link}>
Forgot Password?
</Link>
<Link href="/register" style={styles.link}>
Create Account
</Link>
</View>
</View>
</KeyboardAvoidingView>
);
}
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,
},
});

View file

@ -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 (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container}
>
<View style={styles.formContainer}>
<Text style={styles.title}>Create Account</Text>
<Text style={styles.subtitle}>Sign up to get started</Text>
<TextInput
style={styles.input}
placeholder="Email"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
autoComplete="email"
/>
<TextInput
style={styles.input}
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
autoComplete="password-new"
/>
<TextInput
style={styles.input}
placeholder="Confirm Password"
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry
autoComplete="password-new"
/>
<TouchableOpacity
style={[styles.button, isLoading && styles.buttonDisabled]}
onPress={handleRegister}
disabled={isLoading}
>
<Text style={styles.buttonText}>
{isLoading ? 'Creating Account...' : 'Create Account'}
</Text>
</TouchableOpacity>
<View style={styles.links}>
<Link href="/login" style={styles.link}>
Already have an account? Sign In
</Link>
</View>
</View>
</KeyboardAvoidingView>
);
}
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,
},
});

View file

@ -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 (
<View style={{ flex: 1, backgroundColor: theme.colors.backgroundPage }}>
<Stack
screenOptions={{
header: ({ route, options }) => {
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 <Header title={title} showAddDeck={showAddDeck} rightContent={rightContent} />;
},
}}
>
<Stack.Screen
name="index"
options={{
title: 'My Decks',
}}
/>
<Stack.Screen
name="(auth)"
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="settings"
options={{
title: 'Settings',
}}
/>
<Stack.Screen
name="profile"
options={{
title: 'Profile',
}}
/>
<Stack.Screen
name="decks/[id]"
options={{
title: 'Deck Details',
}}
/>
<Stack.Screen
name="deck/[id]"
options={{
headerShown: false,
}}
/>
<Stack.Screen
name="create-deck"
options={{
title: 'Create New Deck',
presentation: 'modal',
}}
/>
<Stack.Screen
name="(auth)/login"
options={{
title: 'Login',
headerShown: false,
}}
/>
<Stack.Screen
name="(auth)/register"
options={{
title: 'Register',
headerShown: false,
}}
/>
</Stack>
</View>
);
}
function RootLayoutContent() {
const { theme } = useTheme();
return (
<View style={{ flex: 1, backgroundColor: theme.colors.backgroundPage }}>
<StackNavigator />
</View>
);
}
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 (
<ThemeProvider>
<RootLayoutContent />
</ThemeProvider>
);
}

View file

@ -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<Slide[]>([]);
const [deck, setDeck] = useState<Deck | null>(null);
const [loading, setLoading] = useState(true);
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);
const [editingSlide, setEditingSlide] = useState<Slide | null>(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 (
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'transparent',
}}
>
<Text style={{ fontSize: 18, fontWeight: '500', color: theme.colors.textPrimary }}>
Loading slides...
</Text>
</View>
);
}
return (
<View style={{ flex: 1, backgroundColor: 'transparent' }}>
<SafeAreaView style={{ flex: 1, backgroundColor: theme.colors.backgroundPage }}>
<View style={{ flex: 1 }}>
<SlideList
slides={slides}
onEditSlide={handleEditSlide}
onDeleteSlide={handleDeleteSlide}
onMoveSlide={handleMoveSlide}
/>
<Header
title={deck?.name || 'Loading...'}
showPresent
onPresentPress={handleStartPresentation}
disabled={!slides.length}
slideCount={slides.length}
position="bottom"
/>
</View>
<Modal
visible={isCreateModalVisible}
animationType="fade"
transparent
onRequestClose={() => {
setIsCreateModalVisible(false);
setEditingSlide(null);
}}
>
<SafeAreaView style={{ flex: 1, backgroundColor: `${theme.colors.backgroundPrimary}CC` }}>
<View
style={{
flex: 1,
margin: 16,
backgroundColor: theme.colors.backgroundPrimary,
borderRadius: 12,
overflow: 'hidden',
maxWidth: 800,
alignSelf: 'center',
width: '100%',
}}
>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: theme.colors.borderPrimary,
}}
>
<Text style={{ fontSize: 20, fontWeight: '600', color: theme.colors.textPrimary }}>
{editingSlide ? 'Edit Slide' : 'Create New Slide'}
</Text>
<TouchableOpacity
style={{
padding: 8,
borderRadius: 8,
backgroundColor: theme.colors.backgroundSecondary,
}}
onPress={() => {
setIsCreateModalVisible(false);
setEditingSlide(null);
}}
>
<MaterialIcons name="close" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
</View>
<ScrollView style={{ flex: 1 }}>
<View style={{ padding: 16 }}>
<SlideEditor
deckId={id as string}
slide={editingSlide}
onSuccess={() => {
setIsCreateModalVisible(false);
setEditingSlide(null);
loadDeckAndSlides();
}}
onCancel={() => {
setIsCreateModalVisible(false);
setEditingSlide(null);
}}
/>
</View>
</ScrollView>
</View>
</SafeAreaView>
</Modal>
<Modal
visible={isDeleteModalVisible}
animationType="fade"
transparent
onRequestClose={() => setIsDeleteModalVisible(false)}
>
<View
style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: `${theme.colors.backgroundPrimary}CC`,
}}
>
<View
style={{
width: '90%',
maxWidth: 600,
borderRadius: 12,
padding: 20,
backgroundColor: theme.colors.backgroundPrimary,
}}
>
<Text style={{ fontSize: 20, fontWeight: '600', color: theme.colors.textPrimary }}>
Delete Slide
</Text>
<Text style={{ fontSize: 16, marginBottom: 20, color: theme.colors.textSecondary }}>
Are you sure you want to delete this slide? This action cannot be undone.
</Text>
<View
style={{ flexDirection: 'row', justifyContent: 'flex-end', gap: 12, marginTop: 20 }}
>
<TouchableOpacity
style={{
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 8,
backgroundColor: theme.colors.backgroundSecondary,
}}
onPress={() => {
setIsDeleteModalVisible(false);
setEditingSlide(null);
}}
>
<Text
style={{ fontSize: 16, fontWeight: '500', color: theme.colors.textPrimary }}
>
Cancel
</Text>
</TouchableOpacity>
<TouchableOpacity
style={{
paddingVertical: 8,
paddingHorizontal: 16,
borderRadius: 8,
backgroundColor: theme.colors.error,
}}
onPress={handleDeleteSlide}
>
<Text
style={{ fontSize: 16, fontWeight: '500', color: theme.colors.textOnPrimary }}
>
Delete
</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
<Modal
visible={isPresentationMode}
animationType="fade"
transparent={false}
onRequestClose={() => setIsPresentationMode(false)}
statusBarTranslucent
>
<View style={{ flex: 1, backgroundColor: theme.colors.backgroundPage }}>
<PresentationMode slides={slides} onClose={() => setIsPresentationMode(false)} />
</View>
</Modal>
</SafeAreaView>
</View>
);
}

View file

@ -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<Deck[]>([]);
const [loading, setLoading] = useState(true);
const [isCreateModalVisible, setIsCreateModalVisible] = useState(false);
const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false);
const [isShareModalVisible, setIsShareModalVisible] = useState(false);
const [deckToDelete, setDeckToDelete] = useState<Deck | null>(null);
const [deckToShare, setDeckToShare] = useState<Deck | null>(null);
const [user, setUser] = useState<User | null>(null);
const [error, setError] = useState<string | null>(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 (
<View style={[styles.container, { backgroundColor: theme.colors.backgroundPage }]}>
<View style={styles.content}>
<DeckList
decks={decks}
onDeckPress={(deck) => {
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={() => (
<View style={{ flexDirection: 'row', gap: 15, marginRight: 15 }}>
<CreateItemButton
onPress={() => setIsCreateModalVisible(true)}
variant="button"
title="Create New Deck"
buttonText="Create New Deck"
/>
<View style={styles.headerActions}>
<TouchableOpacity
onPress={() => router.push('/profile')}
style={[styles.iconButton, { backgroundColor: theme.colors.backgroundSecondary }]}
>
<MaterialIcons name="account-circle" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
<TouchableOpacity
onPress={() => router.push('/settings')}
style={[styles.iconButton, { backgroundColor: theme.colors.backgroundSecondary }]}
>
<MaterialIcons name="settings" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
</View>
</View>
)}
/>
{/* Create Deck Modal */}
<Modal
visible={isCreateModalVisible}
onRequestClose={() => setIsCreateModalVisible(false)}
animationType="fade"
transparent
>
<View style={[styles.modalOverlay, { backgroundColor: 'rgba(0, 0, 0, 0.5)' }]}>
<View
style={[styles.modalContainer, { backgroundColor: theme.colors.backgroundPrimary }]}
>
<CreateDeckForm
userId={user?.uid || ''}
onSuccess={(newDeck) => {
setIsCreateModalVisible(false);
setDecks([newDeck, ...decks]);
router.push(`/deck/${newDeck.id}`);
}}
onCancel={() => setIsCreateModalVisible(false)}
/>
</View>
</View>
</Modal>
{/* Delete Deck Modal */}
<Modal
visible={isDeleteModalVisible}
onRequestClose={() => {
setIsDeleteModalVisible(false);
setDeckToDelete(null);
}}
transparent
animationType="fade"
>
<View style={styles.modalOverlay}>
<View
style={[styles.modalContent, { backgroundColor: theme.colors.backgroundPrimary }]}
>
<Text style={[styles.modalTitle, { color: theme.colors.textPrimary }]}>
Delete Deck
</Text>
<Text style={[styles.modalText, { color: theme.colors.textSecondary }]}>
Are you sure you want to delete "{deckToDelete?.name}"? This action cannot be
undone.
</Text>
<View style={styles.modalButtons}>
<TouchableOpacity
style={[
styles.modalButton,
{ backgroundColor: theme.colors.backgroundSecondary },
]}
onPress={() => {
setIsDeleteModalVisible(false);
setDeckToDelete(null);
}}
>
<Text style={[styles.buttonText, { color: theme.colors.textPrimary }]}>
Cancel
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.modalButton, { backgroundColor: theme.colors.error }]}
onPress={confirmDelete}
>
<Text style={[styles.buttonText, styles.deleteButtonText]}>Delete</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
{/* Share Deck Modal */}
<Modal
visible={isShareModalVisible}
onRequestClose={() => setIsShareModalVisible(false)}
transparent
animationType="fade"
>
<View style={styles.modalOverlay}>
<View
style={[styles.modalContent, { backgroundColor: theme.colors.backgroundPrimary }]}
>
<DeckShareSettings
deck={deckToShare!}
onUpdateSharing={async (sharing) => {
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)}
/>
</View>
</View>
</Modal>
{error && (
<View style={styles.errorContainer}>
<Text style={[styles.errorText, { color: theme.colors.error }]}>{error}</Text>
</View>
)}
</View>
</View>
);
}
export default function IndexPage() {
return (
<ThemeProvider>
<App />
</ThemeProvider>
);
}
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,
},
});

View file

@ -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<UserStats>({ 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 (
<View style={[styles.container, { backgroundColor: theme.colors.backgroundPage }]}>
<Text style={[styles.loadingText, { color: theme.colors.textPrimary }]}>
Loading stats...
</Text>
</View>
);
}
return (
<View style={[styles.container, { backgroundColor: theme.colors.backgroundPage }]}>
<View style={[styles.header, { backgroundColor: theme.colors.backgroundPrimary }]}>
<View style={styles.avatarContainer}>
<MaterialIcons name="account-circle" size={80} color={theme.colors.textPrimary} />
</View>
<Text style={[styles.userName, { color: theme.colors.textPrimary }]}>
{auth.currentUser?.email || 'User'}
</Text>
</View>
<View style={[styles.statsContainer, { backgroundColor: theme.colors.backgroundPrimary }]}>
<View style={styles.statItem}>
<MaterialIcons name="folder" size={32} color={theme.colors.textPrimary} />
<Text style={[styles.statValue, { color: theme.colors.textPrimary }]}>
{stats.totalDecks}
</Text>
<Text style={[styles.statLabel, { color: theme.colors.textSecondary }]}>Total Decks</Text>
</View>
<View style={[styles.divider, { backgroundColor: theme.colors.borderPrimary }]} />
<View style={styles.statItem}>
<MaterialIcons name="slideshow" size={32} color={theme.colors.textPrimary} />
<Text style={[styles.statValue, { color: theme.colors.textPrimary }]}>
{stats.totalSlides}
</Text>
<Text style={[styles.statLabel, { color: theme.colors.textSecondary }]}>
Total Slides
</Text>
</View>
</View>
</View>
);
}
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',
},
});

View file

@ -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<ContrastLevel, string> = {
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 (
<ScrollView style={[styles.container, { backgroundColor: theme.colors.backgroundPage }]}>
<View style={styles.content}>
<ThemeSettings />
<View style={[styles.section, { backgroundColor: theme.colors.backgroundPrimary }]}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>Kontrast:</Text>
<View style={styles.contrastContainer}>
<View style={styles.contrastSlider}>
{([1, 2, 3, 4, 5] as ContrastLevel[]).map((level) => (
<Pressable
key={level}
style={[
styles.contrastOption,
{
backgroundColor:
level === 3 ? theme.colors.primary : theme.colors.backgroundSecondary,
},
]}
onPress={() => {}}
/>
))}
</View>
<Text style={[styles.contrastLabel, { color: theme.colors.textPrimary }]}>
{CONTRAST_LABELS[3]}
</Text>
</View>
</View>
<View style={styles.bottomSection}>
<View style={[styles.emailSection, { backgroundColor: theme.colors.backgroundPrimary }]}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Email</Text>
<Text style={[styles.value, { color: theme.colors.textPrimary }]}>
{currentUser?.email}
</Text>
</View>
<TouchableOpacity
style={[styles.logoutButton, { backgroundColor: theme.colors.backgroundPrimary }]}
onPress={handleLogout}
>
<Text style={[styles.logoutButtonText, { color: theme.colors.textPrimary }]}>
Sign Out
</Text>
</TouchableOpacity>
</View>
</View>
</ScrollView>
);
}
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',
},
});

View file

@ -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<Deck | null>(null);
const [error, setError] = useState<string | null>(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 (
<View style={[styles.container, { backgroundColor: theme.colors.background }]}>
<Text style={[styles.text, { color: theme.colors.textPrimary }]}>Loading...</Text>
</View>
);
}
if (error) {
return (
<View style={[styles.container, { backgroundColor: theme.colors.background }]}>
<Text style={[styles.text, { color: theme.colors.error }]}>{error}</Text>
</View>
);
}
if (!deck) {
return null;
}
return (
<View style={[styles.container, { backgroundColor: theme.colors.background }]}>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>{deck.name}</Text>
{deck.description && (
<Text style={[styles.description, { color: theme.colors.textSecondary }]}>
{deck.description}
</Text>
)}
<SlideList deckId={id as string} isReadOnly />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
text: {
fontSize: 16,
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 8,
},
description: {
fontSize: 16,
marginBottom: 24,
},
});

View file

@ -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<ThemeVariant, any> = {
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<ThemeVariant, string> = {
lume: 'Lume',
nature: 'Nature',
stone: 'Stone',
};
export default function ThemesScreen() {
const router = useRouter();
const { theme, themeVariant, setThemeVariant } = useTheme();
return (
<View style={[styles.container, { backgroundColor: theme.colors.backgroundPage }]}>
<View style={[styles.header, { backgroundColor: theme.colors.backgroundPrimary }]}>
<TouchableOpacity style={styles.backButton} onPress={() => router.back()}>
<MaterialIcons name="arrow-back" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>Designs</Text>
</View>
<ScrollView style={styles.content}>
<View style={styles.themeGrid}>
{(Object.keys(THEME_NAMES) as ThemeVariant[]).map((variant) => {
const isSelected = variant === themeVariant;
return (
<TouchableOpacity
key={variant}
style={[
styles.themeCard,
{
backgroundColor: theme.colors.backgroundPrimary,
borderColor: isSelected ? theme.colors.primary : 'transparent',
borderWidth: isSelected ? 2 : 0,
},
]}
onPress={() => setThemeVariant(variant)}
>
{THEME_PATTERNS[variant] && (
<View style={StyleSheet.absoluteFill}>
<View style={styles.patternContainer}>
{[...Array(2)].map((_, i) => (
<Image
key={i}
source={THEME_PATTERNS[variant]}
style={[styles.patternTile, { opacity: 0.15 }]}
/>
))}
</View>
</View>
)}
<Text style={[styles.themeName, { color: theme.colors.textPrimary }]}>
{THEME_NAMES[variant]}
</Text>
</TouchableOpacity>
);
})}
</View>
</ScrollView>
</View>
);
}
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',
},
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View file

@ -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<HeaderProps> = ({
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 = (
<View style={styles.rightContent}>
{!showPresent && (
<View style={[styles.iconWrapper, { borderColor: theme.colors.borderPrimary }]}>
<TouchableOpacity onPress={() => router.push('/profile')} style={styles.iconButton}>
<MaterialIcons name="account-circle" size={24} color={theme.colors.primary} />
</TouchableOpacity>
</View>
)}
{showPresent && (
<View
style={[
styles.iconWrapper,
{ borderColor: disabled ? theme.colors.borderPrimary : theme.colors.primary },
]}
>
<TouchableOpacity onPress={onPresentPress} style={styles.iconButton} disabled={disabled}>
<MaterialIcons
name="slideshow"
size={24}
color={disabled ? theme.colors.textTertiary : theme.colors.primary}
/>
</TouchableOpacity>
</View>
)}
<View style={[styles.iconWrapper, { borderColor: theme.colors.borderPrimary }]}>
<TouchableOpacity onPress={() => router.push('/settings')} style={styles.iconButton}>
<MaterialIcons name="settings" size={24} color={theme.colors.primary} />
</TouchableOpacity>
</View>
{showAddDeck && (
<View style={[styles.iconWrapper, { borderColor: theme.colors.primary }]}>
<TouchableOpacity
onPress={() => {
const event = new CustomEvent('openCreateDeckModal');
window.dispatchEvent(event);
}}
style={styles.iconButton}
>
<MaterialIcons name="add" size={24} color={theme.colors.primary} />
</TouchableOpacity>
</View>
)}
</View>
);
return (
<View
style={[
styles.header,
{
backgroundColor: theme.colors.backgroundPrimary,
borderBottomColor: shouldBeBottom ? 'transparent' : theme.colors.borderPrimary,
borderTopColor: shouldBeBottom ? theme.colors.borderPrimary : 'transparent',
borderTopWidth: shouldBeBottom ? 1 : 0,
borderBottomWidth: shouldBeBottom ? 0 : 1,
},
]}
>
<View style={styles.titleContainer}>
<View style={styles.titleContent}>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>{title}</Text>
{typeof slideCount === 'number' && (
<Text style={[styles.subtitle, { color: theme.colors.textSecondary }]}>
{slideCount} Slides
</Text>
)}
</View>
</View>
<View style={styles.rightContainer}>{rightContent || defaultRightContent}</View>
</View>
);
};
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%',
},
});

View file

@ -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<string, string>
);
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<ThemeContextType>({
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<ColorMode>('system');
const [themeVariant, setThemeVariant] = useState<ThemeVariant>('default');
const [contrastLevel, setContrastLevel] = useState<ContrastLevel>(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 <ThemeContext.Provider value={contextValue}>{children}</ThemeContext.Provider>;
};

View file

@ -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 (
<TouchableOpacity
onPress={onPress}
disabled={loading || disabled}
style={[styles.button, styles[variant], (loading || disabled) && styles.disabled]}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={[styles.text, styles[`${variant}Text`]]}>{children}</Text>
)}
</TouchableOpacity>
);
};
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',
},
});

View file

@ -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 (
<View style={styles.container}>
{label && <Text style={styles.label}>{label}</Text>}
<TextInput
value={value}
onChangeText={onChangeText}
placeholder={placeholder}
secureTextEntry={secureTextEntry}
style={[styles.input, error && styles.inputError]}
/>
{error && <Text style={styles.error}>{error}</Text>}
</View>
);
};
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,
},
});

View file

@ -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<ContextMenuProps> = ({ visible, onClose, items, position }) => {
const { theme } = useTheme();
if (!visible) return null;
return (
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose}>
<TouchableOpacity style={styles.overlay} onPress={onClose} activeOpacity={1}>
<View
style={[
styles.menuContainer,
{
backgroundColor: theme.colors.backgroundSecondary,
top: position?.y || 0,
left: position?.x || 0,
},
]}
>
{items.map((item, index) => (
<TouchableOpacity
key={index}
style={[
styles.menuItem,
index < items.length - 1 && styles.menuItemBorder,
{ borderBottomColor: theme.colors.border },
]}
onPress={() => {
item.onPress();
onClose();
}}
>
<Text
style={[
styles.menuItemText,
{
color: item.destructive ? theme.colors.error : theme.colors.textPrimary,
},
]}
>
{item.label}
</Text>
</TouchableOpacity>
))}
</View>
</TouchableOpacity>
</Modal>
);
};
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,
},
});

View file

@ -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<CreateItemButtonProps> = ({
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 (
<TouchableOpacity
style={[styles.createButton, { backgroundColor: theme.colors.primary }]}
onPress={onPress}
>
<MaterialIcons name={buttonIcon} size={24} color="#FFFFFF" />
<Text style={[styles.createButtonText, { color: '#FFFFFF' }]}>{buttonText}</Text>
</TouchableOpacity>
);
}
return (
<TouchableOpacity
style={[styles.itemContainer, { width }, { backgroundColor: 'transparent' }]}
onPress={onPress}
>
<View style={styles.itemContent}>
<View
style={[styles.imageContainer, { backgroundColor: theme.colors.backgroundSecondary }]}
>
<View style={styles.placeholderContainer}>
<MaterialIcons name={icon} size={48} color={theme.colors.textPrimary} />
</View>
</View>
<View style={styles.textContainer}>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>{title}</Text>
</View>
</View>
</TouchableOpacity>
);
};
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',
},
});

View file

@ -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<ContrastLevel, string> = {
1: 'Sehr niedrig',
2: 'Niedrig',
3: 'Standard',
4: 'Hoch',
5: 'Sehr hoch',
};
export const ThemeSettings = () => {
const { theme, colorMode, setColorMode, contrastLevel, setContrastLevel } = useTheme();
return (
<View style={styles.container}>
{/* Helligkeits-Einstellungen */}
<View style={[styles.section, { backgroundColor: theme.colors.backgroundPrimary }]}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>Helligkeit:</Text>
<View style={styles.colorModeList}>
{COLOR_MODES.map((mode) => (
<TouchableOpacity
key={mode.value}
style={[
styles.colorModeOption,
{
backgroundColor:
mode.value === colorMode
? `${theme.colors.primary}1A`
: theme.colors.backgroundSecondary,
borderColor: mode.value === colorMode ? theme.colors.primary : 'transparent',
borderWidth: mode.value === colorMode ? 2 : 0,
},
]}
onPress={() => setColorMode(mode.value)}
>
<Text style={[styles.colorModeText, { color: theme.colors.textPrimary }]}>
{mode.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* Kontrast-Einstellungen */}
<View style={[styles.section, { backgroundColor: theme.colors.backgroundPrimary }]}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>Kontrast:</Text>
<View style={styles.contrastContainer}>
<View style={styles.contrastSlider}>
{([1, 2, 3, 4, 5] as ContrastLevel[]).map((level) => (
<Pressable
key={level}
style={[
styles.contrastOption,
{
backgroundColor:
level === contrastLevel
? theme.colors.primary
: theme.colors.backgroundSecondary,
},
]}
onPress={() => setContrastLevel(level)}
/>
))}
</View>
<Text style={[styles.contrastLabel, { color: theme.colors.textPrimary }]}>
{CONTRAST_LABELS[contrastLevel]}
</Text>
</View>
</View>
</View>
);
};
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',
},
});

View file

@ -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;

View file

@ -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<CreateDeckButtonProps> = ({
onPress,
variant = 'card',
width = 'auto',
}) => {
const { theme } = useTheme();
if (variant === 'button') {
return (
<TouchableOpacity
style={[styles.createButton, { backgroundColor: theme.colors.primary }]}
onPress={onPress}
>
<MaterialIcons name="add-circle-outline" size={24} color="#FFFFFF" />
<Text style={[styles.createButtonText, { color: '#FFFFFF' }]}>Create your first deck</Text>
</TouchableOpacity>
);
}
return (
<TouchableOpacity
style={[styles.deckContainer, { width }, { backgroundColor: 'transparent' }]}
onPress={onPress}
>
<View style={styles.deckContent}>
<View
style={[styles.imageContainer, { backgroundColor: theme.colors.backgroundSecondary }]}
>
<View style={styles.placeholderContainer}>
<MaterialIcons name="add" size={48} color={theme.colors.textPrimary} />
</View>
</View>
<View style={styles.textContainer}>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>Create New Deck</Text>
</View>
</View>
</TouchableOpacity>
);
};
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',
},
});

View file

@ -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<DeckCardProps> = ({
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 (
<TouchableOpacity
style={[styles.container, { backgroundColor: theme.colors.backgroundSecondary }]}
onPress={() => onPress(deck)}
>
<View style={styles.content}>
<View style={styles.imageContainer}>
{firstSlideImage ? (
<Image source={{ uri: firstSlideImage }} style={styles.thumbnail} resizeMode="cover" />
) : (
<View style={styles.placeholderContainer}>
<MaterialIcons name="image" size={48} color={theme.colors.textTertiary} />
</View>
)}
</View>
<View style={styles.header}>
<View style={styles.titleRow}>
<Text style={[styles.title, { color: theme.colors.textPrimary }]} numberOfLines={1}>
{deck.name}
</Text>
{isPublic && (
<View style={[styles.badge, { backgroundColor: theme.colors.primary }]}>
<Text style={styles.badgeText}>Public</Text>
</View>
)}
</View>
<Text style={[styles.slideCount, { color: theme.colors.textSecondary }]}>
{slideCount} {slideCount === 1 ? 'Slide' : 'Slides'}
</Text>
<View style={styles.actions}>
<TouchableOpacity onPress={handleShare} style={styles.actionButton}>
<MaterialIcons name="share" size={24} color={theme.colors.textTertiary} />
</TouchableOpacity>
<TouchableOpacity onPress={handleDelete} style={styles.actionButton}>
<MaterialIcons name="delete" size={24} color={theme.colors.textTertiary} />
</TouchableOpacity>
</View>
</View>
</View>
</TouchableOpacity>
);
};
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,
},
});

View file

@ -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<DeckListProps> = ({
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 (
<View style={styles.container}>
<ActivityIndicator size="large" color={theme.colors.primary} />
</View>
);
}
if (decks.length === 0) {
return (
<View style={[styles.container, styles.emptyState]}>
<MaterialIcons name="dashboard" size={48} color={theme.colors.textTertiary} />
<Text style={[styles.emptyStateText, { color: theme.colors.textSecondary }]}>
No decks yet
</Text>
<Text style={[styles.emptyStateSubtext, { color: theme.colors.textTertiary }]}>
Create your first deck to get started
</Text>
<CreateItemButton
onPress={onCreateDeck}
variant="button"
title="Neues Deck erstellen"
buttonText="Erstelle dein erstes Deck"
icon="library-add"
buttonIcon="library-add"
/>
</View>
);
}
const renderDeck = ({ item }: { item: Deck | 'create' }) => {
if (item === 'create') {
return (
<CreateItemButton
onPress={onCreateDeck}
variant="card"
width={deckWidth}
title="Neues Deck erstellen"
buttonText="Neues Deck erstellen"
icon="library-add"
buttonIcon="library-add"
/>
);
}
return (
<TouchableOpacity
style={[
styles.deckContainer,
!isSmallScreen ? { width: deckWidth } : {},
{ backgroundColor: 'transparent' },
]}
onPress={() => onDeckPress(item)}
>
<View style={styles.deckContent}>
<MenuRoot>
<MenuTrigger>
<View style={styles.deckInfoContainer}>
<View style={styles.metaInfo}>
<Text style={[styles.metaText, { color: theme.colors.textSecondary }]}>
{new Date(item.updatedAt).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})}
</Text>
<View
style={[styles.separator, { backgroundColor: theme.colors.borderPrimary }]}
/>
<Text style={[styles.metaText, { color: theme.colors.textSecondary }]}>
{slideCounts[item.id] || 0} {slideCounts[item.id] === 1 ? 'Slide' : 'Slides'}
</Text>
</View>
<View
style={[
styles.imageContainer,
{ backgroundColor: theme.colors.backgroundSecondary },
]}
>
{firstSlideImages[item.id] ? (
<Image
source={{ uri: firstSlideImages[item.id] }}
style={styles.thumbnail}
resizeMode="cover"
/>
) : (
<View style={styles.placeholderContainer}>
<MaterialIcons name="image" size={48} color={theme.colors.textTertiary} />
</View>
)}
</View>
<View style={[styles.deckHeader, !isSmallScreen && styles.horizontalDeckHeader]}>
<View
style={[
styles.titleContainer,
!isSmallScreen && styles.horizontalTitleContainer,
]}
>
<Text
style={[
styles.deckTitle,
{ color: theme.colors.textPrimary },
!isSmallScreen && { textAlign: 'center' },
]}
numberOfLines={1}
>
{item.title || item.name}
</Text>
</View>
</View>
</View>
</MenuTrigger>
<MenuContent>
<View
style={[
styles.menuContent,
{
backgroundColor: theme.colors.backgroundPrimary,
borderColor: theme.colors.borderPrimary,
},
]}
>
<MenuItem onSelect={() => onShareDeck(item)} textValue="Share">
<Pressable
style={({ hovered }) => [
styles.menuItem,
{
backgroundColor: hovered
? theme.colors.backgroundTertiary
: theme.colors.backgroundPrimary,
},
]}
>
<MenuItemTitle>
<View style={styles.menuItemContent}>
<MaterialIcons
name="share"
size={18}
color={theme.colors.textPrimary}
style={styles.menuItemIcon}
/>
<Text style={[styles.menuItemTitle, { color: theme.colors.textPrimary }]}>
Share
</Text>
</View>
</MenuItemTitle>
</Pressable>
</MenuItem>
<MenuItem onSelect={() => onDeleteDeck(item)} textValue="Delete">
<Pressable
style={({ hovered }) => [
styles.menuItem,
{
backgroundColor: hovered
? theme.colors.backgroundError
: theme.colors.backgroundPrimary,
},
]}
>
<MenuItemTitle>
<View style={styles.menuItemContent}>
<MaterialIcons
name="delete"
size={18}
color={theme.colors.error}
style={styles.menuItemIcon}
/>
<Text style={[styles.menuItemTitle, { color: theme.colors.error }]}>
Delete
</Text>
</View>
</MenuItemTitle>
</Pressable>
</MenuItem>
</View>
</MenuContent>
</MenuRoot>
</View>
</TouchableOpacity>
);
};
return (
<View style={styles.container}>
<FlatList
style={[styles.list, { backgroundColor: 'transparent' }]}
data={[...decks, 'create']}
renderItem={renderDeck}
keyExtractor={(item) => (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={() => (
<View
style={{
height: deckSpacing.vertical,
width: deckSpacing.horizontal,
}}
/>
)}
/>
</View>
);
};
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',
},
});

View file

@ -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<DeckShareSettingsProps> = ({
deck,
onUpdateSharing,
onClose,
}) => {
const { theme } = useTheme();
const [isPublic, setIsPublic] = useState(deck.sharing.isPublic);
const [newCollaborator, setNewCollaborator] = useState<CollaboratorInput>({
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 (
<View style={[styles.container, { backgroundColor: theme.colors.background }]}>
<View style={styles.header}>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>Share Settings</Text>
<TouchableOpacity onPress={onClose}>
<MaterialIcons name="close" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
</View>
<View style={styles.content}>
<View style={styles.section}>
<View style={styles.settingRow}>
<View style={styles.settingInfo}>
<Text style={[styles.settingTitle, { color: theme.colors.textPrimary }]}>
Public Access
</Text>
<Text style={[styles.settingDescription, { color: theme.colors.textSecondary }]}>
Anyone with the link can view this deck
</Text>
</View>
<Switch
value={isPublic}
onValueChange={handleTogglePublic}
trackColor={{ false: theme.colors.backgroundSecondary, true: theme.colors.primary }}
/>
</View>
</View>
<View style={[styles.section, styles.linkSection]}>
<Text style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}>
Share Link
</Text>
<View
style={[styles.linkContainer, { backgroundColor: theme.colors.backgroundSecondary }]}
>
<Text style={[styles.link, { color: theme.colors.textPrimary }]} numberOfLines={1}>
{shareUrl}
</Text>
<TouchableOpacity
style={[styles.copyButton, { backgroundColor: theme.colors.primary }]}
onPress={handleCopyLink}
>
<MaterialIcons name={copied ? 'check' : 'content-copy'} size={20} color="#FFFFFF" />
<Text style={styles.copyButtonText}>{copied ? 'Copied!' : 'Copy'}</Text>
</TouchableOpacity>
</View>
</View>
<View style={styles.section}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>
Collaborators
</Text>
<View style={styles.collaboratorInput}>
<TextInput
style={[styles.input, { color: theme.colors.textPrimary }]}
placeholder="Email address"
placeholderTextColor={theme.colors.textSecondary}
value={newCollaborator.email}
onChangeText={(email) => setNewCollaborator({ ...newCollaborator, email })}
/>
<TouchableOpacity
style={[styles.addButton, { backgroundColor: theme.colors.primary }]}
onPress={handleAddCollaborator}
>
<Text style={styles.addButtonText}>Add</Text>
</TouchableOpacity>
</View>
{Object.entries(deck.sharing.collaborators).map(([userId, role]) => (
<View key={userId} style={styles.collaboratorRow}>
<Text style={[styles.collaboratorEmail, { color: theme.colors.textPrimary }]}>
{userId} ({role})
</Text>
<TouchableOpacity onPress={() => handleRemoveCollaborator(userId)}>
<MaterialIcons name="remove-circle" size={24} color={theme.colors.error} />
</TouchableOpacity>
</View>
))}
</View>
</View>
</View>
);
};
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,
},
});

View file

@ -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<CreateDeckFormProps> = ({ onSuccess, onCancel }) => {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [error, setError] = useState<string | null>(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 (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={[styles.container, { backgroundColor: theme.colors.backgroundPrimary }]}
>
<View style={styles.header}>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>Create New Deck</Text>
<TouchableOpacity onPress={onCancel} style={styles.closeButton}>
<MaterialIcons name="close" size={24} color={theme.colors.textSecondary} />
</TouchableOpacity>
</View>
<View style={styles.form}>
<View style={styles.inputContainer}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Name</Text>
<TextInput
style={[
styles.input,
{
backgroundColor: theme.colors.backgroundSecondary,
color: theme.colors.textPrimary,
borderColor: theme.colors.border,
},
]}
value={name}
onChangeText={setName}
placeholder="Enter deck name"
placeholderTextColor={theme.colors.textTertiary}
/>
</View>
<View style={styles.inputContainer}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>
Description (optional)
</Text>
<TextInput
style={[
styles.input,
styles.textArea,
{
backgroundColor: theme.colors.backgroundSecondary,
color: theme.colors.textPrimary,
borderColor: theme.colors.border,
},
]}
value={description}
onChangeText={setDescription}
placeholder="Enter deck description"
placeholderTextColor={theme.colors.textTertiary}
multiline
numberOfLines={4}
/>
</View>
{error && <Text style={[styles.error, { color: theme.colors.error }]}>{error}</Text>}
<View style={styles.buttons}>
<TouchableOpacity
style={[
styles.button,
styles.cancelButton,
{ backgroundColor: theme.colors.backgroundSecondary },
]}
onPress={onCancel}
>
<Text style={[styles.buttonText, { color: theme.colors.textPrimary }]}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.createButton, { backgroundColor: theme.colors.primary }]}
onPress={handleSubmit}
>
<Text style={[styles.buttonText, { color: '#FFFFFF' }]}>Create Deck</Text>
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
);
};
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: {},
});

View file

@ -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<PresentationModeProps> = ({
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<NodeJS.Timeout | null>(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 (
<View style={[styles.container, { backgroundColor: theme.colors.backgroundPage }]}>
{/* Top bar with title and close button */}
<Animated.View
style={[
styles.topBar,
{
opacity: controlsOpacity,
backgroundColor: `${theme.colors.backgroundPrimary}CC`,
},
]}
>
<Text style={[styles.slideTitle, { color: theme.colors.textPrimary }]}>
{currentSlide.title}
</Text>
<TouchableOpacity
style={[styles.closeButton, { backgroundColor: theme.colors.backgroundSecondary }]}
onPress={onClose}
>
<Ionicons name="close" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
</Animated.View>
{/* Current slide */}
<View style={styles.slideContainer}>
<SlideView
slide={currentSlide}
showNotes={showSpeakerNotes}
isFullscreen={isFullscreen}
onNavigate={handleNavigation}
isFirstSlide={currentSlideIndex === 0}
isLastSlide={currentSlideIndex === slides.length - 1}
/>
</View>
{/* Controls overlay with animation */}
<Animated.View
style={[
styles.controlsOverlay,
{
opacity: controlsOpacity,
backgroundColor: `${theme.colors.backgroundPrimary}CC`,
},
]}
>
<View style={styles.controls}>
<TouchableOpacity
style={[styles.controlButton, { backgroundColor: theme.colors.backgroundSecondary }]}
onPress={() => handleNavigation('prev')}
>
<Ionicons name="chevron-back" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
<View style={styles.centerControls}>
<TouchableOpacity
style={[styles.controlButton, { backgroundColor: theme.colors.backgroundSecondary }]}
onPress={() => setShowSpeakerNotes(!showSpeakerNotes)}
>
<Ionicons
name={showSpeakerNotes ? 'eye-off' : 'eye'}
size={24}
color={theme.colors.textPrimary}
/>
</TouchableOpacity>
<TouchableOpacity
style={[styles.controlButton, { backgroundColor: theme.colors.backgroundSecondary }]}
onPress={() => setIsTimerRunning(!isTimerRunning)}
>
<Ionicons
name={isTimerRunning ? 'pause' : 'play'}
size={24}
color={theme.colors.textPrimary}
/>
</TouchableOpacity>
<Text style={[styles.timer, { color: theme.colors.textPrimary }]}>
{Math.floor(elapsedTime / 60)}:{(elapsedTime % 60).toString().padStart(2, '0')}
</Text>
<TouchableOpacity
style={[styles.controlButton, { backgroundColor: theme.colors.backgroundSecondary }]}
onPress={() => setIsFullscreen(!isFullscreen)}
>
<Ionicons
name={isFullscreen ? 'contract' : 'expand'}
size={24}
color={theme.colors.textPrimary}
/>
</TouchableOpacity>
</View>
<TouchableOpacity
style={[styles.controlButton, { backgroundColor: theme.colors.backgroundSecondary }]}
onPress={() => handleNavigation('next')}
>
<Ionicons name="chevron-forward" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
</View>
</Animated.View>
</View>
);
};
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,
},
});

View file

@ -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<SlideEditorProps> = ({ deckId, slide, onSuccess, onCancel }) => {
const { theme } = useTheme();
const [title, setTitle] = useState(slide?.title ?? '');
const [fullText, setFullText] = useState(slide?.fullText ?? '');
const [bulletPoints, setBulletPoints] = useState<string[]>(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 (
<View style={[styles.editorContainer, { backgroundColor: theme.colors.backgroundPrimary }]}>
<ScrollView style={{ flex: 1, padding: 16 }}>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textPrimary }]}>Title</Text>
<TextInput
style={[
styles.input,
{
backgroundColor: theme.colors.backgroundSecondary,
color: theme.colors.textPrimary,
borderColor: theme.colors.borderPrimary,
},
]}
value={title}
onChangeText={setTitle}
placeholder="Enter slide title"
placeholderTextColor={theme.colors.textSecondary}
/>
</View>
<View style={styles.inputContainer}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Image</Text>
<TouchableOpacity
style={[
styles.imagePreview,
{
backgroundColor: theme.colors.backgroundSecondary,
borderColor: theme.colors.border,
},
]}
onPress={handlePickImage}
>
{imageUrl ? (
<Image source={{ uri: imageUrl }} style={styles.imagePreview} resizeMode="cover" />
) : (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<MaterialIcons name="add-photo-alternate" size={32} color={theme.colors.primary} />
<Text style={[styles.addButtonText, { color: theme.colors.primary }]}>
Add Image
</Text>
</View>
)}
</TouchableOpacity>
</View>
<View style={styles.inputContainer}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Full Text</Text>
<TextInput
style={[
styles.input,
styles.textArea,
{
backgroundColor: theme.colors.backgroundSecondary,
color: theme.colors.textPrimary,
borderColor: theme.colors.border,
},
]}
value={fullText}
onChangeText={setFullText}
placeholder="Enter full text content..."
placeholderTextColor={theme.colors.textTertiary}
multiline
/>
</View>
<View style={styles.inputContainer}>
<Text style={[styles.label, { color: theme.colors.textSecondary }]}>Bullet Points</Text>
{bulletPoints.map((point, index) => (
<View key={index} style={styles.bulletPointContainer}>
<MaterialIcons
name="circle"
size={8}
color={theme.colors.textSecondary}
style={{ marginRight: 8 }}
/>
<TextInput
style={[
styles.input,
styles.bulletPointInput,
{
backgroundColor: theme.colors.backgroundSecondary,
color: theme.colors.textPrimary,
borderColor: theme.colors.border,
},
]}
value={point}
onChangeText={(text) => handleUpdateBulletPoint(index, text)}
placeholder="Add bullet point..."
placeholderTextColor={theme.colors.textTertiary}
multiline
/>
<TouchableOpacity
style={{ padding: 4 }}
onPress={() => handleRemoveBulletPoint(index)}
>
<MaterialIcons name="remove-circle-outline" size={20} color={theme.colors.error} />
</TouchableOpacity>
</View>
))}
<TouchableOpacity style={styles.addButton} onPress={handleAddBulletPoint}>
<MaterialIcons name="add-circle-outline" size={20} color={theme.colors.primary} />
<Text style={[styles.addButtonText, { color: theme.colors.primary }]}>
Add Bullet Point
</Text>
</TouchableOpacity>
</View>
<View style={styles.formGroup}>
<Text style={[styles.label, { color: theme.colors.textPrimary }]}>Notes</Text>
<TextInput
style={[
styles.input,
styles.textArea,
{
backgroundColor: theme.colors.backgroundSecondary,
color: theme.colors.textPrimary,
borderColor: theme.colors.borderPrimary,
},
]}
value={notes}
onChangeText={setNotes}
placeholder="Add presenter notes"
placeholderTextColor={theme.colors.textSecondary}
multiline
/>
</View>
</ScrollView>
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[styles.button, { backgroundColor: theme.colors.backgroundSecondary }]}
onPress={onCancel}
>
<Text style={[styles.buttonText, { color: theme.colors.textPrimary }]}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, { backgroundColor: theme.colors.primary }]}
onPress={handleSubmit}
disabled={isSubmitting}
>
<Text style={[styles.buttonText, { color: theme.colors.textOnPrimary }]}>
{isSubmitting ? 'Saving...' : 'Save'}
</Text>
</TouchableOpacity>
</View>
</View>
);
};
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,
},
});

View file

@ -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<SlideListProps> = ({
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 (
<View style={[styles.emptyState, { backgroundColor: theme.colors.backgroundPrimary }]}>
<MaterialIcons name="hourglass-empty" size={48} color={theme.colors.textSecondary} />
<Text style={[styles.emptyStateText, { color: theme.colors.textSecondary }]}>
Loading...
</Text>
</View>
);
}
const renderItem = ({ item, index }: { item: Slide | 'create'; index: number }) => {
if (item === 'create') {
return (
<TouchableOpacity
style={[
styles.slideContainer,
{
width: slideWidth,
},
]}
onPress={onCreateSlide}
>
<View style={styles.slideContent}>
<View
style={[styles.imageContainer, { backgroundColor: theme.colors.backgroundSecondary }]}
>
<View style={styles.placeholderContainer}>
<MaterialIcons
name="add-photo-alternate"
size={48}
color={theme.colors.textTertiary}
/>
</View>
</View>
<View style={styles.slideFooter}>
<Text style={[styles.slideNumber, { color: theme.colors.textPrimary }]}>
Neuen Slide erstellen
</Text>
</View>
</View>
</TouchableOpacity>
);
}
return (
<View
style={[
styles.slideContainer,
{
width: slideWidth,
},
]}
>
<MenuRoot>
<MenuTrigger>
<TouchableOpacity onPress={() => onEditSlide?.(item)} style={{ flex: 1 }}>
<View style={styles.slideContent}>
<View
style={[
styles.imageContainer,
{ backgroundColor: theme.colors.backgroundSecondary },
]}
>
{item.imageUrl ? (
<Image
source={{ uri: item.imageUrl }}
style={styles.thumbnail}
resizeMode="cover"
/>
) : (
<View style={styles.placeholderContainer}>
<MaterialIcons name="image" size={48} color={theme.colors.textTertiary} />
</View>
)}
</View>
<View style={styles.slideFooter}>
<Text style={[styles.slideNumber, { color: theme.colors.textPrimary }]}>
{item.title || `Slide ${index + 1}`}
</Text>
</View>
{showNotes && item.notes && (
<Text
style={[styles.notes, { color: theme.colors.textSecondary }]}
numberOfLines={2}
>
{item.notes}
</Text>
)}
</View>
</TouchableOpacity>
</MenuTrigger>
<MenuContent>
<View
style={[
styles.menuContent,
{
backgroundColor: theme.colors.backgroundPrimary,
borderColor: theme.colors.borderPrimary,
},
]}
>
{onMoveSlide && index > 0 && (
<MenuItem
onSelect={() => {
console.log('[SlideList] Selected Move Up for slide:', item.id);
onMoveSlide(item, 'up');
}}
textValue="Move Up"
>
<Pressable
onPress={() => {
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,
},
]}
>
<MenuItemTitle>
<View style={styles.menuItemContent}>
<MaterialIcons
name="arrow-upward"
size={18}
color={theme.colors.textPrimary}
style={styles.menuItemIcon}
/>
<Text style={[styles.menuItemTitle, { color: theme.colors.textPrimary }]}>
Nach oben
</Text>
</View>
</MenuItemTitle>
</Pressable>
</MenuItem>
)}
{onMoveSlide && index < slides.length - 1 && (
<MenuItem
onSelect={() => {
console.log('[SlideList] Selected Move Down for slide:', item.id);
onMoveSlide(item, 'down');
}}
textValue="Move Down"
>
<Pressable
onPress={() => {
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,
},
]}
>
<MenuItemTitle>
<View style={styles.menuItemContent}>
<MaterialIcons
name="arrow-downward"
size={18}
color={theme.colors.textPrimary}
style={styles.menuItemIcon}
/>
<Text style={[styles.menuItemTitle, { color: theme.colors.textPrimary }]}>
Nach unten
</Text>
</View>
</MenuItemTitle>
</Pressable>
</MenuItem>
)}
{onDeleteSlide && (
<MenuItem onSelect={() => onDeleteSlide(item)} textValue="Delete">
<Pressable
onPress={() => onDeleteSlide(item)}
style={({ hovered }) => [
styles.menuItem,
{
backgroundColor: hovered
? theme.colors.backgroundError
: theme.colors.backgroundPrimary,
},
]}
>
<MenuItemTitle>
<View style={styles.menuItemContent}>
<MaterialIcons
name="delete"
size={18}
color={theme.colors.error}
style={styles.menuItemIcon}
/>
<Text style={[styles.menuItemTitle, { color: theme.colors.error }]}>
Löschen
</Text>
</View>
</MenuItemTitle>
</Pressable>
</MenuItem>
)}
</View>
</MenuContent>
</MenuRoot>
</View>
);
};
if (slides.length === 0) {
return (
<View style={styles.emptyContainer}>
<View style={styles.emptyState}>
<MaterialIcons name="slideshow" size={48} color={theme.colors.textSecondary} />
<Text style={[styles.emptyStateText, { color: theme.colors.textSecondary }]}>
Keine Slides
</Text>
<Text style={[styles.emptyStateSubtext, { color: theme.colors.textSecondary }]}>
Erstelle deinen ersten Slide
</Text>
</View>
<TouchableOpacity
style={[
styles.slideContainer,
{
width: slideWidth,
},
]}
onPress={onCreateSlide}
>
<View style={styles.slideContent}>
<View
style={[styles.imageContainer, { backgroundColor: theme.colors.backgroundSecondary }]}
>
<View style={styles.placeholderContainer}>
<MaterialIcons
name="add-photo-alternate"
size={48}
color={theme.colors.textTertiary}
/>
</View>
</View>
<View style={styles.slideFooter}>
<Text style={[styles.slideNumber, { color: theme.colors.textPrimary }]}>
Erstelle deinen ersten Slide
</Text>
</View>
</View>
</TouchableOpacity>
</View>
);
}
return (
<View style={styles.container}>
<FlatList
data={[...slides, 'create']}
renderItem={renderItem}
keyExtractor={(item) => (item === 'create' ? 'create' : item.id)}
horizontal={!isSmallScreen}
showsHorizontalScrollIndicator={false}
contentContainerStyle={[styles.listContent, !isSmallScreen && { paddingHorizontal: 16 }]}
extraData={slides.map((s) => s.order).join(',')}
/>
</View>
);
};
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',
},
});

View file

@ -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<SlideViewProps> = ({
slide,
showNotes = false,
isFullscreen = false,
onToggleFullscreen,
onNavigate,
isFirstSlide = false,
isLastSlide = false,
}) => {
const { theme } = useTheme();
return (
<View
style={[
styles.container,
isFullscreen && styles.fullscreenContainer,
{
backgroundColor: isFullscreen
? theme.colors.backgroundPage
: theme.colors.backgroundPrimary,
},
]}
>
{slide.imageUrl && (
<View
style={[
styles.imageContainer,
isFullscreen && styles.fullscreenImageContainer,
{ backgroundColor: theme.colors.backgroundPage },
]}
>
{/* Navigation Areas */}
{!isFirstSlide && (
<TouchableOpacity style={styles.navigationArea} onPress={() => onNavigate?.('prev')} />
)}
{!isLastSlide && (
<TouchableOpacity
style={[styles.navigationArea, styles.navigationAreaRight]}
onPress={() => onNavigate?.('next')}
/>
)}
{/* Image */}
<Image
source={{ uri: slide.imageUrl }}
style={styles.image}
resizeMode={isFullscreen ? 'contain' : 'cover'}
/>
{/* Navigation Indicators */}
{isFullscreen && (
<>
{!isFirstSlide && (
<View style={[styles.navigationIndicator, styles.navigationIndicatorLeft]}>
<MaterialIcons
name="chevron-left"
size={36}
color={`${theme.colors.textPrimary}80`}
/>
</View>
)}
{!isLastSlide && (
<View style={[styles.navigationIndicator, styles.navigationIndicatorRight]}>
<MaterialIcons
name="chevron-right"
size={36}
color={`${theme.colors.textPrimary}80`}
/>
</View>
)}
</>
)}
</View>
)}
{!isFullscreen && (
<View style={styles.content}>
<Text style={[styles.contentTitle, { color: theme.colors.textPrimary }]}>
{slide.title}
</Text>
{slide.bulletPoints && slide.bulletPoints.length > 0 && (
<View style={styles.bulletPoints}>
{slide.bulletPoints.map((point, index) => (
<View key={index} style={styles.bulletPoint}>
<Text style={[styles.bullet, { color: theme.colors.textPrimary }]}></Text>
<Text style={[styles.bulletText, { color: theme.colors.textPrimary }]}>
{point}
</Text>
</View>
))}
</View>
)}
{slide.fullText && (
<Text style={[styles.fullText, { color: theme.colors.textPrimary }]}>
{slide.fullText}
</Text>
)}
{showNotes && slide.notes && (
<View
style={[styles.notesContainer, { backgroundColor: theme.colors.backgroundSecondary }]}
>
<Text style={[styles.notesTitle, { color: theme.colors.textSecondary }]}>Notes:</Text>
<Text style={[styles.notes, { color: theme.colors.textPrimary }]}>{slide.notes}</Text>
</View>
)}
</View>
)}
</View>
);
};
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,
},
});

View file

@ -1,279 +0,0 @@
import { ColorSchemeName } from 'react-native';
// Theme types
export type ThemeVariant = 'lume' | 'nature' | 'stone';
export const THEME_PATTERNS: Record<ThemeVariant, any> = {
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<ThemeVariant, string> = {
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<typeof getTheme>;
// Am Ende der Datei fügen Sie diese Zeile hinzu:
export const defaultTheme = getTheme('light');

View file

@ -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": {}
}
}

View file

@ -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,
];

View file

@ -1,6 +0,0 @@
const { getDefaultConfig } = require('@expo/metro-config');
const defaultConfig = getDefaultConfig(__dirname);
defaultConfig.resolver.sourceExts.push('cjs');
module.exports = defaultConfig;

View file

@ -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"
}
}

View file

@ -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<User> => {
const userCredential = await signInWithEmailAndPassword(auth, email, password);
return userCredential.user;
};
export const registerUser = async (email: string, password: string): Promise<User> => {
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<void> => {
await signOut(auth);
};
export const resetPassword = async (email: string): Promise<void> => {
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;
};

View file

@ -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<Deck[]> => {
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<Deck>): Promise<Deck> => {
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<Deck> => {
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<void> => {
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<Slide[]> => {
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<Slide>): Promise<Slide> => {
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<Slide>): Promise<void> => {
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<void> => {
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<void> => {
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;
}
};

View file

@ -1,31 +0,0 @@
import { storage } from '../firebaseConfig';
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
export const uploadImage = async (file: Blob, path: string): Promise<string> => {
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<string[]> => {
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;
}
};

View file

@ -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 (
<ThemeProvider>
<YourApp />
</ThemeProvider>
);
}
```
2. Nutze den useTheme Hook in deinen Komponenten:
```tsx
import { useTheme } from './theme';
export function MyComponent() {
const { theme, isDark } = useTheme();
return (
<View style={{ backgroundColor: theme.colors.background }}>
<Text style={{ color: theme.colors.text }}>Hello World</Text>
</View>
);
}
```
3. Füge die ThemeSettings Komponente in deine Settings-Seite ein:
```tsx
import { ThemeSettings } from './theme';
export function SettingsScreen() {
return (
<View>
<ThemeSettings />
</View>
);
}
```

View file

@ -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<ThemeContextType>({
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<ColorMode>('system');
const [contrastLevel, setContrastLevel] = useState<ContrastLevel>(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 (
<ThemeContext.Provider
value={{
theme,
isDark,
colorMode,
setColorMode: handleColorModeChange,
contrastLevel,
setContrastLevel: handleContrastLevelChange,
}}
>
{children}
</ThemeContext.Provider>
);
};

View file

@ -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<ContrastLevel, string> = {
1: 'Sehr niedrig',
2: 'Niedrig',
3: 'Standard',
4: 'Hoch',
5: 'Sehr hoch',
};
export const ThemeSettings = () => {
const { theme, colorMode, setColorMode, contrastLevel, setContrastLevel } = useTheme();
return (
<View style={styles.container}>
{/* Helligkeits-Einstellungen */}
<View style={[styles.section, { backgroundColor: theme.colors.backgroundPrimary }]}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>Helligkeit:</Text>
<View style={styles.colorModeList}>
{COLOR_MODES.map((mode) => (
<TouchableOpacity
key={mode.value}
style={[
styles.colorModeOption,
{
backgroundColor:
mode.value === colorMode
? `${theme.colors.primary}1A`
: theme.colors.backgroundSecondary,
borderColor: mode.value === colorMode ? theme.colors.primary : 'transparent',
borderWidth: mode.value === colorMode ? 2 : 0,
},
]}
onPress={() => setColorMode(mode.value)}
>
<Text style={[styles.colorModeText, { color: theme.colors.textPrimary }]}>
{mode.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* Kontrast-Einstellungen */}
<View style={[styles.section, { backgroundColor: theme.colors.backgroundPrimary }]}>
<Text style={[styles.sectionTitle, { color: theme.colors.textPrimary }]}>Kontrast:</Text>
<View style={styles.contrastContainer}>
<View style={styles.contrastSlider}>
{([1, 2, 3, 4, 5] as ContrastLevel[]).map((level) => (
<Pressable
key={level}
style={[
styles.contrastOption,
{
backgroundColor:
level === contrastLevel
? theme.colors.primary
: theme.colors.backgroundSecondary,
},
]}
onPress={() => setContrastLevel(level)}
/>
))}
</View>
<Text style={[styles.contrastLabel, { color: theme.colors.textPrimary }]}>
{CONTRAST_LABELS[contrastLevel]}
</Text>
</View>
</View>
</View>
);
};
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',
},
});

View file

@ -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<ThemeVariant, string> = {
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;
}

View file

@ -1,3 +0,0 @@
export * from './constants';
export * from './ThemeProvider';
export * from './ThemeSettings';

View file

@ -1,10 +0,0 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
}

View file

@ -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;
}

View file

@ -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",

View file

@ -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',