React Native Expo app — full mobile client
- Expo SDK 54, expo-router, NativeWind - 7 screens: login, tasks, calendar, goals, chat, settings, tabs - API client (api.hasdo.info), SecureStore auth, AuthContext - Tab navigation: Ukoly, Kalendar, Cile, Chat, Nastaveni - Pull-to-refresh, FAB, group colors, priority dots - Calendar grid with task dots - AI chat with keyboard avoiding - Web export verified (780 modules, 1.5MB bundle) - Bundle ID: info.hasdo.taskteam Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
162
mobile/app/(tabs)/goals.tsx
Normal file
162
mobile/app/(tabs)/goals.tsx
Normal file
@@ -0,0 +1,162 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
RefreshControl,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useAuthContext } from '../../lib/AuthContext';
|
||||
import * as api from '../../lib/api';
|
||||
|
||||
export default function GoalsScreen() {
|
||||
const { token } = useAuthContext();
|
||||
const [goals, setGoals] = useState<any[]>([]);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
const loadGoals = useCallback(async () => {
|
||||
if (!token) return;
|
||||
try {
|
||||
const res = await api.getGoals(token);
|
||||
setGoals(res.data || []);
|
||||
} catch (err: any) {
|
||||
Alert.alert('Chyba', err.message);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
loadGoals();
|
||||
}, [loadGoals]);
|
||||
|
||||
const onRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
await loadGoals();
|
||||
setRefreshing(false);
|
||||
}, [loadGoals]);
|
||||
|
||||
const renderGoal = ({ item }: { item: any }) => {
|
||||
const progress = item.progress || 0;
|
||||
const total = item.target || 100;
|
||||
const pct = Math.min(Math.round((progress / total) * 100), 100);
|
||||
|
||||
return (
|
||||
<View style={styles.goalCard}>
|
||||
<View style={styles.goalHeader}>
|
||||
<View style={styles.goalIcon}>
|
||||
<Ionicons
|
||||
name={pct >= 100 ? 'trophy' : 'flag-outline'}
|
||||
size={24}
|
||||
color={pct >= 100 ? '#F59E0B' : '#3B82F6'}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.goalInfo}>
|
||||
<Text style={styles.goalTitle}>{item.title}</Text>
|
||||
{item.description && (
|
||||
<Text style={styles.goalDesc} numberOfLines={2}>
|
||||
{item.description}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.goalPct}>{pct}%</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.progressBarBg}>
|
||||
<View
|
||||
style={[
|
||||
styles.progressBarFill,
|
||||
{
|
||||
width: `${pct}%`,
|
||||
backgroundColor: pct >= 100 ? '#22C55E' : pct >= 50 ? '#3B82F6' : '#F59E0B',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.goalFooter}>
|
||||
<Text style={styles.goalStat}>
|
||||
{progress} / {total}
|
||||
</Text>
|
||||
{item.deadline && (
|
||||
<Text style={styles.goalDeadline}>
|
||||
<Ionicons name="time-outline" size={12} color="#94A3B8" />{' '}
|
||||
{new Date(item.deadline).toLocaleDateString('cs-CZ')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<FlatList
|
||||
data={goals}
|
||||
keyExtractor={(item) => item.id?.toString()}
|
||||
renderItem={renderGoal}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||
}
|
||||
contentContainerStyle={styles.listContent}
|
||||
ListEmptyComponent={
|
||||
<View style={styles.empty}>
|
||||
<Ionicons name="trophy-outline" size={48} color="#CBD5E1" />
|
||||
<Text style={styles.emptyText}>Zatim zadne cile</Text>
|
||||
<Text style={styles.emptySubtext}>
|
||||
Vytvorte cile ve webove aplikaci
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#F8FAFC' },
|
||||
listContent: { padding: 12, paddingBottom: 20 },
|
||||
goalCard: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 3,
|
||||
elevation: 2,
|
||||
},
|
||||
goalHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 },
|
||||
goalIcon: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 10,
|
||||
backgroundColor: '#EFF6FF',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
goalInfo: { flex: 1, marginLeft: 12 },
|
||||
goalTitle: { fontSize: 16, fontWeight: '600', color: '#1E293B' },
|
||||
goalDesc: { fontSize: 13, color: '#64748B', marginTop: 2 },
|
||||
goalPct: { fontSize: 18, fontWeight: '700', color: '#3B82F6' },
|
||||
progressBarBg: {
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#E2E8F0',
|
||||
},
|
||||
progressBarFill: {
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
goalFooter: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 8,
|
||||
},
|
||||
goalStat: { fontSize: 12, color: '#64748B' },
|
||||
goalDeadline: { fontSize: 12, color: '#94A3B8' },
|
||||
empty: { alignItems: 'center', marginTop: 60 },
|
||||
emptyText: { color: '#94A3B8', fontSize: 16, marginTop: 12 },
|
||||
emptySubtext: { color: '#CBD5E1', fontSize: 13, marginTop: 4 },
|
||||
});
|
||||
Reference in New Issue
Block a user