ueberpruefen
This commit is contained in:
490
src/screens/ProjectsScreen.tsx
Normal file
490
src/screens/ProjectsScreen.tsx
Normal file
@@ -0,0 +1,490 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Image,
|
||||
RefreshControl,
|
||||
TextInput,
|
||||
} from 'react-native';
|
||||
import { useFocusEffect, useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { RootStackParamList } from '../navigation/types';
|
||||
import { Project } from '../types';
|
||||
import { getAllProjects } from '../lib/db/repositories';
|
||||
import { Button, Card } from '../components';
|
||||
import { colors, spacing, typography, borderRadius } from '../lib/theme';
|
||||
import { formatDate } from '../lib/utils/datetime';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||
|
||||
type SortOption = 'newest' | 'oldest' | 'name';
|
||||
|
||||
export const ProjectsScreen: React.FC = () => {
|
||||
const navigation = useNavigation<NavigationProp>();
|
||||
const { user } = useAuth();
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [filteredProjects, setFilteredProjects] = useState<Project[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [sortBy, setSortBy] = useState<SortOption>('newest');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const sortProjects = (projectsToSort: Project[]) => {
|
||||
const sorted = [...projectsToSort];
|
||||
switch (sortBy) {
|
||||
case 'newest':
|
||||
return sorted.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
||||
case 'oldest':
|
||||
return sorted.sort((a, b) => new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime());
|
||||
case 'name':
|
||||
return sorted.sort((a, b) => a.title.localeCompare(b.title));
|
||||
default:
|
||||
return sorted;
|
||||
}
|
||||
};
|
||||
|
||||
const filterProjects = (projectsToFilter: Project[], query: string) => {
|
||||
if (!query.trim()) return projectsToFilter;
|
||||
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return projectsToFilter.filter(project =>
|
||||
project.title.toLowerCase().includes(lowerQuery) ||
|
||||
project.tags.some(tag => tag.toLowerCase().includes(lowerQuery))
|
||||
);
|
||||
};
|
||||
|
||||
const loadProjects = async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
const allProjects = await getAllProjects(user.id);
|
||||
const sorted = sortProjects(allProjects);
|
||||
setProjects(sorted);
|
||||
setFilteredProjects(filterProjects(sorted, searchQuery));
|
||||
} catch (error) {
|
||||
console.error('Failed to load projects:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
loadProjects();
|
||||
}, [])
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (projects.length > 0) {
|
||||
const sorted = sortProjects(projects);
|
||||
setProjects(sorted);
|
||||
setFilteredProjects(filterProjects(sorted, searchQuery));
|
||||
}
|
||||
}, [sortBy]);
|
||||
|
||||
useEffect(() => {
|
||||
setFilteredProjects(filterProjects(projects, searchQuery));
|
||||
}, [searchQuery]);
|
||||
|
||||
const handleRefresh = () => {
|
||||
setRefreshing(true);
|
||||
loadProjects();
|
||||
};
|
||||
|
||||
const handleCreateProject = () => {
|
||||
navigation.navigate('ProjectDetail', { projectId: 'new' });
|
||||
};
|
||||
|
||||
const handleProjectPress = (projectId: string) => {
|
||||
navigation.navigate('ProjectDetail', { projectId });
|
||||
};
|
||||
|
||||
const getStepCount = (projectId: string) => {
|
||||
// This will be calculated from actual steps
|
||||
return 0; // Placeholder
|
||||
};
|
||||
|
||||
const renderProject = ({ item }: { item: Project }) => {
|
||||
const stepCount = getStepCount(item.id);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => handleProjectPress(item.id)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Open project ${item.title}`}
|
||||
>
|
||||
<Card style={styles.projectCard}>
|
||||
{item.coverImageUri && (
|
||||
<View style={styles.imageContainer}>
|
||||
<Image
|
||||
source={{ uri: item.coverImageUri }}
|
||||
style={styles.coverImage}
|
||||
accessibilityLabel={`Cover image for ${item.title}`}
|
||||
/>
|
||||
{stepCount > 0 && (
|
||||
<View style={styles.stepCountBadge}>
|
||||
<Text style={styles.stepCountText}>{stepCount} steps</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.projectInfo}>
|
||||
<View style={styles.titleRow}>
|
||||
<Text style={styles.projectTitle}>{item.title}</Text>
|
||||
</View>
|
||||
<View style={styles.projectMeta}>
|
||||
<View style={[
|
||||
styles.statusBadge,
|
||||
item.status === 'in_progress' && styles.statusBadgeInProgress,
|
||||
item.status === 'done' && styles.statusBadgeDone,
|
||||
]}>
|
||||
<Text style={styles.statusText}>
|
||||
{item.status === 'in_progress' ? 'In Progress' : 'Done'}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.date}>{formatDate(item.updatedAt)}</Text>
|
||||
</View>
|
||||
{item.tags.length > 0 && (
|
||||
<View style={styles.tags}>
|
||||
{item.tags.map((tag, index) => (
|
||||
<View key={index} style={styles.tag}>
|
||||
<Text style={styles.tagText}>{tag}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Card>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={styles.centerContainer}>
|
||||
<Text>Loading projects...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{filteredProjects.length === 0 && searchQuery.length > 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={styles.emptyIcon}>🔍</Text>
|
||||
<Text style={styles.emptyTitle}>No matches found</Text>
|
||||
<Text style={styles.emptyText}>Try a different search term</Text>
|
||||
</View>
|
||||
) : projects.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={styles.emptyIcon}>🏺</Text>
|
||||
<Text style={styles.emptyTitle}>No projects yet</Text>
|
||||
<Text style={styles.emptyText}>Start your first piece!</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={filteredProjects}
|
||||
renderItem={renderProject}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={styles.listContent}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
|
||||
}
|
||||
ListHeaderComponent={
|
||||
<View>
|
||||
{/* MY PROJECTS Title */}
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.headerTitle}>My Projects</Text>
|
||||
</View>
|
||||
|
||||
{/* Create Project Button */}
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
title="New Project"
|
||||
onPress={handleCreateProject}
|
||||
size="sm"
|
||||
accessibilityLabel="Create new project"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Search Bar */}
|
||||
<View style={styles.searchContainer}>
|
||||
<TextInput
|
||||
style={styles.searchInput}
|
||||
placeholder="Search projects or tags..."
|
||||
value={searchQuery}
|
||||
onChangeText={setSearchQuery}
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
/>
|
||||
{searchQuery.length > 0 && (
|
||||
<TouchableOpacity onPress={() => setSearchQuery('')} style={styles.clearButton}>
|
||||
<Text style={styles.clearButtonText}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Sort Buttons */}
|
||||
<View style={styles.sortContainer}>
|
||||
<Text style={styles.sortLabel}>Sort:</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.sortButton, sortBy === 'newest' && styles.sortButtonActive]}
|
||||
onPress={() => setSortBy('newest')}
|
||||
>
|
||||
<Text style={[styles.sortButtonText, sortBy === 'newest' && styles.sortButtonTextActive]}>
|
||||
Newest
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.sortButton, sortBy === 'oldest' && styles.sortButtonActive]}
|
||||
onPress={() => setSortBy('oldest')}
|
||||
>
|
||||
<Text style={[styles.sortButtonText, sortBy === 'oldest' && styles.sortButtonTextActive]}>
|
||||
Oldest
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.sortButton, sortBy === 'name' && styles.sortButtonActive]}
|
||||
onPress={() => setSortBy('name')}
|
||||
>
|
||||
<Text style={[styles.sortButtonText, sortBy === 'name' && styles.sortButtonTextActive]}>
|
||||
Name
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
},
|
||||
centerContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
buttonContainer: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingTop: spacing.lg,
|
||||
paddingBottom: spacing.md,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
header: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingTop: 64,
|
||||
paddingBottom: spacing.lg,
|
||||
backgroundColor: colors.background,
|
||||
borderBottomWidth: 3,
|
||||
borderBottomColor: colors.primary,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: typography.fontSize.xl,
|
||||
fontWeight: typography.fontWeight.bold,
|
||||
color: colors.primary,
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
searchContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingBottom: spacing.md,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
borderRadius: borderRadius.md,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.border,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.md,
|
||||
fontSize: typography.fontSize.md,
|
||||
color: colors.text,
|
||||
fontWeight: typography.fontWeight.medium,
|
||||
},
|
||||
clearButton: {
|
||||
position: 'absolute',
|
||||
right: spacing.lg + spacing.md,
|
||||
paddingHorizontal: spacing.sm,
|
||||
},
|
||||
clearButtonText: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
color: colors.textSecondary,
|
||||
fontWeight: typography.fontWeight.bold,
|
||||
},
|
||||
sortContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingBottom: spacing.md,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
sortLabel: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
fontWeight: typography.fontWeight.bold,
|
||||
color: colors.textSecondary,
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
sortButton: {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.full,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
},
|
||||
sortButtonActive: {
|
||||
borderColor: colors.primary,
|
||||
backgroundColor: colors.primaryLight,
|
||||
},
|
||||
sortButtonText: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
fontWeight: typography.fontWeight.bold,
|
||||
color: colors.textSecondary,
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
sortButtonTextActive: {
|
||||
color: colors.text,
|
||||
},
|
||||
listContent: {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingBottom: 100, // Extra space for floating tab bar
|
||||
},
|
||||
projectCard: {
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
imageContainer: {
|
||||
position: 'relative',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
coverImage: {
|
||||
width: '100%',
|
||||
height: 200,
|
||||
borderRadius: borderRadius.md,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
stepCountBadge: {
|
||||
position: 'absolute',
|
||||
top: spacing.md,
|
||||
right: spacing.md,
|
||||
backgroundColor: colors.primary,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.full,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.primaryDark,
|
||||
},
|
||||
stepCountText: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
fontWeight: typography.fontWeight.bold,
|
||||
color: colors.background,
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
projectInfo: {
|
||||
gap: spacing.sm,
|
||||
},
|
||||
titleRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
projectTitle: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: typography.fontWeight.bold,
|
||||
color: colors.text,
|
||||
letterSpacing: 0.3,
|
||||
flex: 1,
|
||||
},
|
||||
projectMeta: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
statusBadge: {
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.xs,
|
||||
borderRadius: borderRadius.full,
|
||||
borderWidth: 2,
|
||||
},
|
||||
statusBadgeInProgress: {
|
||||
backgroundColor: '#FFF4ED',
|
||||
borderColor: '#E8A87C',
|
||||
},
|
||||
statusBadgeDone: {
|
||||
backgroundColor: '#E8F9F8',
|
||||
borderColor: '#85CDCA',
|
||||
},
|
||||
statusBadgeArchived: {
|
||||
backgroundColor: '#F9EFF3',
|
||||
borderColor: '#C38D9E',
|
||||
},
|
||||
statusText: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
fontWeight: typography.fontWeight.bold,
|
||||
color: colors.text,
|
||||
letterSpacing: 0.5,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
date: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
tags: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: spacing.xs,
|
||||
},
|
||||
tag: {
|
||||
backgroundColor: colors.primaryLight,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.full,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.primaryDark,
|
||||
},
|
||||
tagText: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.background,
|
||||
fontWeight: typography.fontWeight.bold,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: spacing.xl,
|
||||
},
|
||||
emptyIcon: {
|
||||
fontSize: 64,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
emptyTitle: {
|
||||
fontSize: typography.fontSize.xl,
|
||||
fontWeight: typography.fontWeight.semiBold,
|
||||
color: colors.text,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: typography.fontSize.md,
|
||||
color: colors.textSecondary,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
emptyButton: {
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user