mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
hot fix #1
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, ActivityIndicator } from 'react-native';
|
||||
import { CameraView, Camera } from 'expo-camera';
|
||||
import { loginAdmin, logoutAdmin, isAdminLogged } from '../../services/auth';
|
||||
import { getApi } from '../../services/api';
|
||||
import { validateQrPayload } from '../../services/qrHelper';
|
||||
|
||||
export default function AdminQRValidatorScreen() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showScanner, setShowScanner] = useState(false);
|
||||
const [lastResult, setLastResult] = useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
const ok = await isAdminLogged();
|
||||
setIsLoggedIn(ok);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!email || !password) {
|
||||
Alert.alert('Chyba', 'Vyplňte email a heslo');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
await loginAdmin(email, password);
|
||||
setIsLoggedIn(true);
|
||||
Alert.alert('Přihlášení', 'Přihlášen jako admin');
|
||||
} catch (e: any) {
|
||||
Alert.alert('Chyba', e.response?.data?.error || 'Přihlášení selhalo');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logoutAdmin();
|
||||
setIsLoggedIn(false);
|
||||
setShowScanner(false);
|
||||
setLastResult(null);
|
||||
};
|
||||
|
||||
const handleBarcodeScanned = async ({ data }: { data: string }) => {
|
||||
setShowScanner(false);
|
||||
try {
|
||||
const payload = JSON.parse(data);
|
||||
if (!validateQrPayload(payload)) {
|
||||
Alert.alert('Neplatný QR', 'QR kód není platný');
|
||||
return;
|
||||
}
|
||||
const api = await getApi();
|
||||
await api.post(`/tickets/${payload.id}/validate`, { barcode: payload.barcode, used_by: 'admin-qr-validator' });
|
||||
setLastResult(`Vstupenka ${payload.id} ověřena (${payload.event})`);
|
||||
Alert.alert('Ověřeno', `Vstupenka ${payload.event} byla úspěšně ověřena.`);
|
||||
} catch (e: any) {
|
||||
Alert.alert('Chyba ověření', e.response?.data?.error || 'Nepodařilo se ověřit vstupenku');
|
||||
}
|
||||
};
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Admin přihlášení</Text>
|
||||
<TextInput style={styles.input} placeholder="Email" value={email} onChangeText={setEmail} autoCapitalize="none" keyboardType="email-address" />
|
||||
<TextInput style={styles.input} placeholder="Heslo" value={password} onChangeText={setPassword} secureTextEntry />
|
||||
<TouchableOpacity style={styles.button} onPress={handleLogin} disabled={loading}>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.buttonText}>Přihlásit</Text>}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (showScanner) {
|
||||
return (
|
||||
<View style={styles.scannerContainer}>
|
||||
<CameraView
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
barcodeScannerSettings={{
|
||||
barcodeTypes: ['qr'],
|
||||
}}
|
||||
onBarcodeScanned={handleBarcodeScanned}
|
||||
/>
|
||||
<TouchableOpacity style={styles.cancel} onPress={() => setShowScanner(false)}>
|
||||
<Text style={styles.cancelText}>Zrušit</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>QR validátor (Admin)</Text>
|
||||
<TouchableOpacity style={styles.button} onPress={() => setShowScanner(true)}>
|
||||
<Text style={styles.buttonText}>Skenovat QR kód</Text>
|
||||
</TouchableOpacity>
|
||||
{lastResult && <Text style={styles.result}>{lastResult}</Text>}
|
||||
<TouchableOpacity style={[styles.button, styles.logout]} onPress={handleLogout}>
|
||||
<Text style={styles.buttonText}>Odhlásit</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, padding: 24, gap: 16, backgroundColor: '#fff' },
|
||||
title: { fontSize: 22, fontWeight: '700', textAlign: 'center' },
|
||||
input: { borderWidth: 1, borderColor: '#E5E7EB', borderRadius: 8, padding: 12, fontSize: 16 },
|
||||
button: { backgroundColor: '#0B5ED7', paddingVertical: 12, borderRadius: 8, alignItems: 'center' },
|
||||
buttonText: { color: '#fff', fontWeight: '700' },
|
||||
logout: { backgroundColor: '#DC2626', marginTop: 8 },
|
||||
scannerContainer: { flex: 1 },
|
||||
cancel: { position: 'absolute', top: 60, left: 24, backgroundColor: '#0006', padding: 8, borderRadius: 6 },
|
||||
cancelText: { color: '#fff', fontWeight: '600' },
|
||||
result: { marginTop: 16, fontSize: 14, color: '#059669' },
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, ActivityIndicator } from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { loginFan, logoutFan, isFanLogged } from '../../services/auth';
|
||||
import { getApi } from '../../services/api';
|
||||
import { buildQrPayload } from '../../services/qrHelper';
|
||||
import { Ticket } from '../../types/tickets';
|
||||
|
||||
const FAN_QR_CACHE_KEY = 'myclub_fan_cached_qr';
|
||||
|
||||
export default function FanLoginScreen() {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [cachedQr, setCachedQr] = useState<string | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
const ok = await isFanLogged();
|
||||
setIsLoggedIn(ok);
|
||||
if (ok) {
|
||||
const cached = await AsyncStorage.getItem(FAN_QR_CACHE_KEY);
|
||||
setCachedQr(cached);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const handleLogin = async () => {
|
||||
if (!email || !password) {
|
||||
Alert.alert('Chyba', 'Vyplňte email a heslo');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
await loginFan(email, password);
|
||||
setIsLoggedIn(true);
|
||||
Alert.alert('Přihlášení', 'Přihlášen jako fanoušek');
|
||||
// Načíst a uložit QR průkaz
|
||||
const api = await getApi();
|
||||
const res = await api.get('/tickets/my-tickets');
|
||||
const tickets = Array.isArray(res.data) ? res.data : [];
|
||||
const paid = tickets.find((t: Ticket) => t.status === 'paid');
|
||||
if (paid) {
|
||||
const payload = buildQrPayload(paid, paid.campaign);
|
||||
await AsyncStorage.setItem(FAN_QR_CACHE_KEY, JSON.stringify(payload));
|
||||
setCachedQr(JSON.stringify(payload));
|
||||
}
|
||||
} catch (e: any) {
|
||||
Alert.alert('Chyba', e.response?.data?.error || 'Přihlášení selhalo');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logoutFan();
|
||||
await AsyncStorage.removeItem(FAN_QR_CACHE_KEY);
|
||||
setIsLoggedIn(false);
|
||||
setCachedQr(null);
|
||||
};
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Přihlášení fanouška</Text>
|
||||
<TextInput style={styles.input} placeholder="Email" value={email} onChangeText={setEmail} autoCapitalize="none" keyboardType="email-address" />
|
||||
<TextInput style={styles.input} placeholder="Heslo" value={password} onChangeText={setPassword} secureTextEntry />
|
||||
<TouchableOpacity style={styles.button} onPress={handleLogin} disabled={loading}>
|
||||
{loading ? <ActivityIndicator color="#fff" /> : <Text style={styles.buttonText}>Přihlásit</Text>}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Můj QR průkaz</Text>
|
||||
{cachedQr ? (
|
||||
<View style={styles.qrBox}>
|
||||
<Text style={styles.qrNote}>Uložený QR průkaz (offline)</Text>
|
||||
<Text style={styles.qrCode}>{cachedQr}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<Text style={styles.noQr}>Nemáte žádnou zaplacenou vstupenku</Text>
|
||||
)}
|
||||
<TouchableOpacity style={[styles.button, styles.logout]} onPress={handleLogout}>
|
||||
<Text style={styles.buttonText}>Odhlásit</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, padding: 24, gap: 16, backgroundColor: '#fff' },
|
||||
title: { fontSize: 22, fontWeight: '700', textAlign: 'center' },
|
||||
input: { borderWidth: 1, borderColor: '#E5E7EB', borderRadius: 8, padding: 12, fontSize: 16 },
|
||||
button: { backgroundColor: '#0B5ED7', paddingVertical: 12, borderRadius: 8, alignItems: 'center' },
|
||||
buttonText: { color: '#fff', fontWeight: '700' },
|
||||
logout: { backgroundColor: '#DC2626', marginTop: 8 },
|
||||
qrBox: { padding: 16, backgroundColor: '#F9FAFB', borderRadius: 8 },
|
||||
qrNote: { fontSize: 14, color: '#4B5563', marginBottom: 8 },
|
||||
qrCode: { fontFamily: 'monospace', fontSize: 10, color: '#111827' },
|
||||
noQr: { fontSize: 16, color: '#6B7280', textAlign: 'center' },
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, TextInput, FlatList, TouchableOpacity, ActivityIndicator, StyleSheet } from 'react-native';
|
||||
import { searchClubs, ClubDirectoryEntry } from '../../services/directory';
|
||||
import { useClub } from '../../contexts/ClubContext';
|
||||
|
||||
const ClubSelectorScreen: React.FC = () => {
|
||||
const [query, setQuery] = useState('');
|
||||
const [results, setResults] = useState<ClubDirectoryEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { switchClub } = useClub();
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
const res = await searchClubs(query);
|
||||
if (mounted) setResults(res);
|
||||
setLoading(false);
|
||||
})();
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, [query]);
|
||||
|
||||
const handleSelect = async (club: ClubDirectoryEntry) => {
|
||||
try {
|
||||
await switchClub({
|
||||
id: club.id,
|
||||
name: club.name,
|
||||
apiBaseUrl: club.api_base_url,
|
||||
logoUrl: club.logo_url,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to select club:', error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Vyberte klub</Text>
|
||||
<Text style={styles.subtitle}>Najděte svůj klub a připněte si ho k rychlému přístupu.</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Hledat klub..."
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
/>
|
||||
{loading ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<FlatList
|
||||
data={results}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity style={styles.item} onPress={() => handleSelect(item)}>
|
||||
<View>
|
||||
<Text style={styles.itemTitle}>{item.name}</Text>
|
||||
<Text style={styles.itemSub}>{item.api_base_url}</Text>
|
||||
</View>
|
||||
<Text style={styles.pin}>Připnout</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
ListEmptyComponent={<Text style={styles.empty}>Žádný klub nenalezen.</Text>}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, padding: 24, gap: 12, backgroundColor: '#fff' },
|
||||
title: { fontSize: 24, fontWeight: '700' },
|
||||
subtitle: { fontSize: 14, color: '#4B5563' },
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#E5E7EB',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
fontSize: 16,
|
||||
},
|
||||
item: {
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#E5E7EB',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
itemTitle: { fontSize: 16, fontWeight: '600' },
|
||||
itemSub: { fontSize: 12, color: '#6B7280' },
|
||||
pin: { color: '#0B5ED7', fontWeight: '600' },
|
||||
empty: { textAlign: 'center', color: '#6B7280', marginTop: 24 },
|
||||
});
|
||||
|
||||
export default ClubSelectorScreen;
|
||||
@@ -0,0 +1,541 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, ActivityIndicator, TouchableOpacity, FlatList, Dimensions } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { getDashboardData, DashboardData, DashboardMatch, DashboardNews } from '../../services/dashboard';
|
||||
import { useClubTheme } from '../../theme';
|
||||
import { ModernCard, ModernButton, ModernBadge } from '../../components/ui/ModernComponents';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
const DashboardScreen: React.FC = () => {
|
||||
const { theme } = useClubTheme();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [data, setData] = useState<DashboardData | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const dashboardData = await getDashboardData();
|
||||
setData(dashboardData);
|
||||
} catch (err) {
|
||||
setError('Nepodařilo se načíst data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('cs-CZ', { day: 'numeric', month: 'numeric' });
|
||||
};
|
||||
|
||||
const formatTime = (timeStr: string) => {
|
||||
return timeStr.substring(0, 5);
|
||||
};
|
||||
|
||||
const MatchCard: React.FC<{ match: DashboardMatch; isUpcoming?: boolean }> = ({ match, isUpcoming = false }) => (
|
||||
<ModernCard shadow={true} margin={8} padding={16} borderRadius={16}>
|
||||
<View style={styles.matchHeader}>
|
||||
<View style={styles.matchDateRow}>
|
||||
<Ionicons name="calendar" size={16} color="#6B7280" />
|
||||
<Text style={styles.matchDate}>{formatDate(match.date)} {formatTime(match.time)}</Text>
|
||||
{match.competition && (
|
||||
<ModernBadge text={match.competition} variant="secondary" size="small" />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.matchTeamsContainer}>
|
||||
<View style={styles.teamSection}>
|
||||
<Text style={[styles.teamName, match.is_home && styles.homeTeam]}>
|
||||
{match.home_team}
|
||||
</Text>
|
||||
{match.is_home && (
|
||||
<ModernBadge text="DOMÁCÍ" variant="primary" size="small" />
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.vsSection}>
|
||||
<View style={styles.vsCircle}>
|
||||
<Text style={styles.vsText}>VS</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.teamSection}>
|
||||
<Text style={[styles.teamName, !match.is_home && styles.awayTeam]}>
|
||||
{match.away_team}
|
||||
</Text>
|
||||
{!match.is_home && (
|
||||
<ModernBadge text="HOSTÉ" variant="secondary" size="small" />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{match.venue && (
|
||||
<View style={styles.venueRow}>
|
||||
<Ionicons name="location" size={16} color="#6B7280" />
|
||||
<Text style={styles.venueText}>{match.venue}</Text>
|
||||
</View>
|
||||
)}
|
||||
</ModernCard>
|
||||
);
|
||||
|
||||
const NewsCard: React.FC<{ news: DashboardNews }> = ({ news }) => (
|
||||
<TouchableOpacity activeOpacity={0.7}>
|
||||
<ModernCard shadow={true} margin={8} padding={16} borderRadius={16}>
|
||||
<View style={styles.newsHeader}>
|
||||
<Text style={styles.newsTitle} numberOfLines={2}>{news.title}</Text>
|
||||
<View style={styles.newsMeta}>
|
||||
<Ionicons name="time" size={14} color="#6B7280" />
|
||||
<Text style={styles.newsDate}>{formatDate(news.published_at)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{news.summary && (
|
||||
<Text style={styles.newsSummary} numberOfLines={3}>
|
||||
{news.summary}
|
||||
</Text>
|
||||
)}
|
||||
<View style={styles.newsFooter}>
|
||||
<ModernBadge text="Číst více" variant="outline" size="small" />
|
||||
<Ionicons name="chevron-forward" size={16} color={theme.primary} />
|
||||
</View>
|
||||
</ModernCard>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const AnnouncementCard: React.FC<{ announcement: any }> = ({ announcement }) => {
|
||||
const getVariant = () => {
|
||||
switch (announcement.type) {
|
||||
case 'warning': return 'warning';
|
||||
case 'success': return 'success';
|
||||
default: return 'primary';
|
||||
}
|
||||
};
|
||||
|
||||
const getIcon = () => {
|
||||
switch (announcement.type) {
|
||||
case 'warning': return 'warning';
|
||||
case 'success': return 'checkmark-circle';
|
||||
default: return 'information-circle';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModernCard
|
||||
shadow={false}
|
||||
margin={8}
|
||||
padding={16}
|
||||
borderRadius={12}
|
||||
style={{
|
||||
backgroundColor: announcement.type === 'warning' ? '#FEF3C7' :
|
||||
announcement.type === 'success' ? '#D1FAE5' : '#DBEAFE'
|
||||
}}
|
||||
>
|
||||
<View style={styles.announcementHeader}>
|
||||
<Ionicons name={getIcon() as any} size={20} color={
|
||||
announcement.type === 'warning' ? '#D97706' :
|
||||
announcement.type === 'success' ? '#059669' : '#2563EB'
|
||||
} />
|
||||
<Text style={styles.announcementTitle}>{announcement.title}</Text>
|
||||
</View>
|
||||
<Text style={styles.announcementContent}>{announcement.content}</Text>
|
||||
</ModernCard>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={styles.center}>
|
||||
<ActivityIndicator size="large" color={theme.primary} />
|
||||
<Text style={styles.loadingText}>Načítám data...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View style={styles.center}>
|
||||
<ModernCard shadow={false} padding={32} borderRadius={16} style={styles.errorCard}>
|
||||
<Ionicons name="alert-circle" size={48} color="#EF4444" />
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
<ModernButton
|
||||
title="Zkusit znovu"
|
||||
onPress={loadData}
|
||||
variant="primary"
|
||||
size="medium"
|
||||
style={styles.retryButton}
|
||||
/>
|
||||
</ModernCard>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.contentContainer}>
|
||||
{/* Welcome Section */}
|
||||
<ModernCard shadow={false} margin={16} padding={20} borderRadius={16} style={styles.welcomeCard}>
|
||||
<View style={styles.welcomeHeader}>
|
||||
<Text style={styles.welcomeTitle}>Vítejte v klubu</Text>
|
||||
<Ionicons name="football" size={24} color={theme.primary} />
|
||||
</View>
|
||||
<Text style={styles.welcomeSubtitle}>Sledujte nejnovější informace o vašem týmu</Text>
|
||||
</ModernCard>
|
||||
|
||||
{/* Upcoming Matches */}
|
||||
{data?.upcoming_match && (
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Ionicons name="calendar" size={20} color={theme.primary} />
|
||||
<Text style={styles.sectionTitle}>Nadcházející zápas</Text>
|
||||
<ModernBadge text="1" variant="primary" size="small" />
|
||||
</View>
|
||||
<MatchCard match={data.upcoming_match} isUpcoming={true} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Recent Matches */}
|
||||
{data?.recent_matches && data.recent_matches.length > 0 && (
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Ionicons name="time" size={20} color={theme.primary} />
|
||||
<Text style={styles.sectionTitle}>Poslední zápas</Text>
|
||||
<ModernBadge text={data.recent_matches.length.toString()} variant="secondary" size="small" />
|
||||
</View>
|
||||
<MatchCard match={data.recent_matches[0]} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* News */}
|
||||
{data?.news && data.news.length > 0 && (
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Ionicons name="newspaper" size={20} color={theme.primary} />
|
||||
<Text style={styles.sectionTitle}>Novinky</Text>
|
||||
<ModernBadge text={data.news.length.toString()} variant="primary" size="small" />
|
||||
</View>
|
||||
<FlatList
|
||||
data={data.news}
|
||||
renderItem={({ item }) => <NewsCard news={item} />}
|
||||
keyExtractor={(item, index) => `news-${index}`}
|
||||
scrollEnabled={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Announcements */}
|
||||
{data?.announcements && data.announcements.length > 0 && (
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Ionicons name="megaphone" size={20} color={theme.primary} />
|
||||
<Text style={styles.sectionTitle}>Oznámení</Text>
|
||||
<ModernBadge text={data.announcements.length.toString()} variant="warning" size="small" />
|
||||
</View>
|
||||
<FlatList
|
||||
data={data.announcements}
|
||||
renderItem={({ item }) => <AnnouncementCard announcement={item} />}
|
||||
keyExtractor={(item, index) => `announcement-${index}`}
|
||||
scrollEnabled={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{(!data?.upcoming_match && (!data?.recent_matches || data.recent_matches.length === 0) && (!data?.news || data.news.length === 0) && (!data?.announcements || data.announcements.length === 0)) && (
|
||||
<View style={styles.emptyState}>
|
||||
<ModernCard shadow={false} padding={32} borderRadius={16} style={styles.emptyCard}>
|
||||
<Ionicons name="football-outline" size={64} color="#9CA3AF" />
|
||||
<Text style={styles.emptyTitle}>Žádné obsah</Text>
|
||||
<Text style={styles.emptySubtitle}>Zatím zde nejsou žádné informace</Text>
|
||||
</ModernCard>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F9FAFB',
|
||||
},
|
||||
contentContainer: {
|
||||
padding: 16,
|
||||
gap: 20,
|
||||
},
|
||||
center: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 24,
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 16,
|
||||
fontSize: 16,
|
||||
color: '#6B7280',
|
||||
},
|
||||
errorText: {
|
||||
marginTop: 16,
|
||||
fontSize: 16,
|
||||
color: '#EF4444',
|
||||
textAlign: 'center',
|
||||
},
|
||||
retryButton: {
|
||||
marginTop: 16,
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
},
|
||||
retryButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: '600',
|
||||
},
|
||||
section: {
|
||||
gap: 12,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#1F2937',
|
||||
marginBottom: 4,
|
||||
},
|
||||
matchCard: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
borderLeftWidth: 4,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
},
|
||||
matchHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 12,
|
||||
},
|
||||
matchDate: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
},
|
||||
competition: {
|
||||
fontSize: 12,
|
||||
color: '#6B7280',
|
||||
backgroundColor: '#F3F4F6',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 4,
|
||||
},
|
||||
matchTeams: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
team: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#1F2937',
|
||||
textAlign: 'center',
|
||||
},
|
||||
homeTeam: {
|
||||
color: '#059669',
|
||||
},
|
||||
awayTeam: {
|
||||
color: '#DC2626',
|
||||
},
|
||||
vs: {
|
||||
fontSize: 14,
|
||||
color: '#6B7280',
|
||||
marginVertical: 4,
|
||||
},
|
||||
venue: {
|
||||
fontSize: 12,
|
||||
color: '#6B7280',
|
||||
textAlign: 'center',
|
||||
},
|
||||
newsCard: {
|
||||
backgroundColor: '#FFFFFF',
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
borderLeftWidth: 4,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
},
|
||||
newsTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#1F2937',
|
||||
marginBottom: 8,
|
||||
},
|
||||
newsSummary: {
|
||||
fontSize: 14,
|
||||
color: '#4B5563',
|
||||
lineHeight: 20,
|
||||
marginBottom: 8,
|
||||
},
|
||||
newsDate: {
|
||||
fontSize: 12,
|
||||
color: '#6B7280',
|
||||
},
|
||||
announcementCard: {
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
borderLeftWidth: 4,
|
||||
borderLeftColor: '#3B82F6',
|
||||
},
|
||||
announcementHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 8,
|
||||
},
|
||||
announcementTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#1F2937',
|
||||
},
|
||||
announcementContent: {
|
||||
fontSize: 14,
|
||||
color: '#4B5563',
|
||||
lineHeight: 20,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#6B7280',
|
||||
marginTop: 16,
|
||||
},
|
||||
emptySubtext: {
|
||||
fontSize: 14,
|
||||
color: '#9CA3AF',
|
||||
marginTop: 4,
|
||||
},
|
||||
// Missing styles that were referenced in the component
|
||||
matchDateRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
matchTeamsContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginVertical: 8,
|
||||
},
|
||||
teamSection: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
},
|
||||
teamName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#1F2937',
|
||||
},
|
||||
vsSection: {
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
vsCircle: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
backgroundColor: '#F3F4F6',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
vsText: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#6B7280',
|
||||
},
|
||||
venueRow: {
|
||||
marginTop: 8,
|
||||
},
|
||||
venueText: {
|
||||
fontSize: 14,
|
||||
color: '#6B7280',
|
||||
textAlign: 'center',
|
||||
},
|
||||
newsHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
newsMeta: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
newsFooter: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginTop: 8,
|
||||
},
|
||||
errorCard: {
|
||||
backgroundColor: '#FEE2E2',
|
||||
borderLeftColor: '#EF4444',
|
||||
},
|
||||
welcomeCard: {
|
||||
backgroundColor: '#EBF8FF',
|
||||
borderLeftColor: '#3B82F6',
|
||||
},
|
||||
welcomeHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
},
|
||||
welcomeTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: '700',
|
||||
color: '#1F2937',
|
||||
},
|
||||
welcomeSubtitle: {
|
||||
fontSize: 16,
|
||||
color: '#6B7280',
|
||||
marginBottom: 16,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
emptyState: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 32,
|
||||
},
|
||||
emptyCard: {
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
},
|
||||
emptyTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#6B7280',
|
||||
marginTop: 16,
|
||||
},
|
||||
emptySubtitle: {
|
||||
fontSize: 14,
|
||||
color: '#9CA3AF',
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export default DashboardScreen;
|
||||
@@ -0,0 +1,442 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Text, ScrollView, TouchableOpacity, StyleSheet, ActivityIndicator, Alert, TextInput, FlatList, Dimensions } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useClub } from '../../contexts/ClubContext';
|
||||
import { useClubTheme } from '../../theme';
|
||||
import { searchClubs, ClubDirectoryEntry } from '../../services/directory';
|
||||
import { getAllClubs } from '../../services/directory';
|
||||
import { loadPinnedClubs, savePinnedClubs, pinClub as pinClubService, unpinClub as unpinClubService, PinnedClub } from '../../services/pinnedClubs';
|
||||
import { ModernCard, ModernButton, ModernInput, ModernBadge } from '../../components/ui/ModernComponents';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
const ClubHubScreen: React.FC = () => {
|
||||
const { switchClub } = useClub();
|
||||
const { theme } = useClubTheme({ id: 'hub' }); // Use default theme for hub
|
||||
const [pinnedClubs, setPinnedClubs] = useState<PinnedClub[]>([]);
|
||||
const [availableClubs, setAvailableClubs] = useState<ClubDirectoryEntry[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Load pinned clubs from storage
|
||||
const pinned = await loadPinnedClubs();
|
||||
setPinnedClubs(pinned);
|
||||
|
||||
// Load all available clubs
|
||||
const clubs = await getAllClubs();
|
||||
setAvailableClubs(clubs);
|
||||
} catch (error) {
|
||||
console.error('Failed to load club data:', error);
|
||||
Alert.alert('Chyba', 'Nepodařilo se načíst data klubů');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const pinClub = async (club: ClubDirectoryEntry) => {
|
||||
try {
|
||||
const updated = await pinClubService(club);
|
||||
setPinnedClubs(updated);
|
||||
|
||||
Alert.alert('Úspěch', `${club.name} bylo připnuto na hlavní stránku`);
|
||||
setShowSearch(false);
|
||||
setSearchQuery('');
|
||||
} catch (error) {
|
||||
Alert.alert('Chyba', 'Nepodařilo se připnout klub');
|
||||
}
|
||||
};
|
||||
|
||||
const unpinClub = async (clubId: string) => {
|
||||
try {
|
||||
const updated = await unpinClubService(clubId);
|
||||
setPinnedClubs(updated);
|
||||
} catch (error) {
|
||||
Alert.alert('Chyba', 'Nepodařilo se odebrat klub');
|
||||
}
|
||||
};
|
||||
|
||||
const selectClub = async (club: PinnedClub) => {
|
||||
try {
|
||||
await switchClub({
|
||||
id: club.id,
|
||||
name: club.name,
|
||||
apiBaseUrl: club.api_base_url,
|
||||
logoUrl: club.logo_url,
|
||||
});
|
||||
// Navigation will be handled by the ClubContext
|
||||
} catch (error) {
|
||||
Alert.alert('Chyba', 'Nepodařilo se přepnout klub');
|
||||
}
|
||||
};
|
||||
|
||||
const searchResults = searchQuery
|
||||
? availableClubs.filter(club =>
|
||||
club.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
club.city?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: availableClubs;
|
||||
|
||||
const ClubCard: React.FC<{ club: ClubDirectoryEntry | PinnedClub; isPinned?: boolean }> = ({ club, isPinned = false }) => (
|
||||
<ModernCard
|
||||
shadow={true}
|
||||
margin={8}
|
||||
padding={16}
|
||||
borderRadius={16}
|
||||
style={styles.clubCard}
|
||||
>
|
||||
<View style={styles.clubContent}>
|
||||
<View style={styles.clubHeader}>
|
||||
<Text style={styles.clubName}>{club.name}</Text>
|
||||
{club.city && (
|
||||
<ModernBadge
|
||||
text={club.city}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
style={styles.cityBadge}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.clubDetails}>
|
||||
<View style={styles.detailRow}>
|
||||
<Ionicons name="globe" size={16} color="#6B7280" />
|
||||
<Text style={styles.detailText}>{club.api_base_url}</Text>
|
||||
</View>
|
||||
{club.country && (
|
||||
<View style={styles.detailRow}>
|
||||
<Ionicons name="location" size={16} color="#6B7280" />
|
||||
<Text style={styles.detailText}>{club.country}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.clubActions}>
|
||||
{isPinned ? (
|
||||
<View style={styles.pinnedActions}>
|
||||
<ModernButton
|
||||
title="Otevřít klub"
|
||||
onPress={() => selectClub(club as PinnedClub)}
|
||||
variant="primary"
|
||||
size="small"
|
||||
style={styles.actionButton}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.unpinButton}
|
||||
onPress={() => unpinClub(club.id)}
|
||||
>
|
||||
<Ionicons name="heart-dislike" size={20} color="#EF4444" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : (
|
||||
<ModernButton
|
||||
title="Připnout"
|
||||
onPress={() => pinClub(club)}
|
||||
variant="primary"
|
||||
size="small"
|
||||
icon="heart"
|
||||
style={styles.actionButton}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ModernCard>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={styles.center}>
|
||||
<ActivityIndicator size="large" color={theme.primary} />
|
||||
<Text style={styles.loadingText}>Načítám kluby...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerContent}>
|
||||
<Text style={styles.title}>MyClub Hub</Text>
|
||||
<Text style={styles.subtitle}>Objevte a připojte své oblíbené kluby</Text>
|
||||
</View>
|
||||
|
||||
<ModernButton
|
||||
title="Najít klub"
|
||||
onPress={() => setShowSearch(!showSearch)}
|
||||
variant="primary"
|
||||
size="medium"
|
||||
icon="search"
|
||||
style={styles.searchButton}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.content} contentContainerStyle={styles.contentContainer}>
|
||||
{/* Pinned Clubs Section */}
|
||||
{pinnedClubs.length > 0 && (
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Ionicons name="heart" size={20} color={theme.primary} />
|
||||
<Text style={styles.sectionTitle}>Připnuté kluby</Text>
|
||||
<ModernBadge text={pinnedClubs.length.toString()} variant="primary" size="small" />
|
||||
</View>
|
||||
<FlatList
|
||||
data={pinnedClubs}
|
||||
renderItem={({ item }) => <ClubCard club={item} isPinned={true} />}
|
||||
keyExtractor={(item) => item.id}
|
||||
scrollEnabled={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Search Section */}
|
||||
{showSearch && (
|
||||
<View style={styles.section}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<Ionicons name="search" size={20} color={theme.primary} />
|
||||
<Text style={styles.sectionTitle}>
|
||||
{searchQuery ? 'Výsledky hledání' : 'Dostupné kluby'}
|
||||
</Text>
|
||||
{searchQuery && (
|
||||
<ModernBadge
|
||||
text={searchResults.length.toString()}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<ModernInput
|
||||
value={searchQuery}
|
||||
onChangeText={setSearchQuery}
|
||||
placeholder="Hledat klub nebo město..."
|
||||
icon="🔍"
|
||||
style={styles.searchInput}
|
||||
/>
|
||||
|
||||
{/* Search Results */}
|
||||
{searchResults.length === 0 ? (
|
||||
<View style={styles.noResults}>
|
||||
<Ionicons name="search" size={48} color="#9CA3AF" />
|
||||
<Text style={styles.noResultsText}>Žádné kluby nenalezeny</Text>
|
||||
<Text style={styles.noResultsSubtext}>Zkuste jiný dotaz</Text>
|
||||
</View>
|
||||
) : (
|
||||
<FlatList
|
||||
data={searchResults.filter(club => !pinnedClubs.some(pinned => pinned.id === club.id))}
|
||||
renderItem={({ item }) => <ClubCard club={item} />}
|
||||
keyExtractor={(item) => item.id}
|
||||
scrollEnabled={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{pinnedClubs.length === 0 && !showSearch && (
|
||||
<View style={styles.emptyState}>
|
||||
<View style={styles.emptyStateIcon}>
|
||||
<Ionicons name="football" size={64} color={theme.primary} />
|
||||
</View>
|
||||
<Text style={styles.emptyTitle}>Vítejte v MyClub Hub</Text>
|
||||
<Text style={styles.emptySubtitle}>
|
||||
Objevte fotbalové kluby z celé České republiky
|
||||
</Text>
|
||||
<Text style={styles.emptyDescription}>
|
||||
Klepněte na "Najít klub" a prozkoumejte dostupné týmy
|
||||
</Text>
|
||||
<ModernButton
|
||||
title="Začít objevovat"
|
||||
onPress={() => setShowSearch(true)}
|
||||
variant="primary"
|
||||
size="large"
|
||||
style={styles.emptyButton}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#F9FAFB',
|
||||
},
|
||||
header: {
|
||||
padding: 24,
|
||||
paddingBottom: 16,
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#E5E7EB',
|
||||
},
|
||||
headerContent: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontWeight: '700',
|
||||
color: '#1F2937',
|
||||
marginBottom: 8,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 16,
|
||||
color: '#6B7280',
|
||||
marginBottom: 4,
|
||||
},
|
||||
searchButton: {
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
padding: 16,
|
||||
gap: 24,
|
||||
},
|
||||
section: {
|
||||
gap: 16,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 4,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
color: '#1F2937',
|
||||
flex: 1,
|
||||
},
|
||||
clubCard: {
|
||||
marginHorizontal: 0,
|
||||
},
|
||||
clubContent: {
|
||||
gap: 12,
|
||||
},
|
||||
clubHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
clubName: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#1F2937',
|
||||
flex: 1,
|
||||
},
|
||||
cityBadge: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
clubDetails: {
|
||||
gap: 8,
|
||||
},
|
||||
detailRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
},
|
||||
detailText: {
|
||||
fontSize: 14,
|
||||
color: '#6B7280',
|
||||
flex: 1,
|
||||
},
|
||||
clubActions: {
|
||||
marginTop: 8,
|
||||
},
|
||||
pinnedActions: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
actionButton: {
|
||||
flex: 1,
|
||||
marginRight: 12,
|
||||
},
|
||||
unpinButton: {
|
||||
padding: 8,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#FEE2E2',
|
||||
},
|
||||
searchInput: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
noResults: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 40,
|
||||
gap: 12,
|
||||
},
|
||||
noResultsText: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#6B7280',
|
||||
textAlign: 'center',
|
||||
},
|
||||
noResultsSubtext: {
|
||||
fontSize: 14,
|
||||
color: '#9CA3AF',
|
||||
textAlign: 'center',
|
||||
},
|
||||
center: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 16,
|
||||
fontSize: 16,
|
||||
color: '#6B7280',
|
||||
},
|
||||
emptyState: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 60,
|
||||
paddingHorizontal: 32,
|
||||
gap: 16,
|
||||
},
|
||||
emptyStateIcon: {
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 60,
|
||||
backgroundColor: '#F3F4F6',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptyTitle: {
|
||||
fontSize: 24,
|
||||
fontWeight: '700',
|
||||
color: '#1F2937',
|
||||
textAlign: 'center',
|
||||
},
|
||||
emptySubtitle: {
|
||||
fontSize: 16,
|
||||
color: '#6B7280',
|
||||
textAlign: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
emptyDescription: {
|
||||
fontSize: 14,
|
||||
color: '#9CA3AF',
|
||||
textAlign: 'center',
|
||||
lineHeight: 20,
|
||||
marginBottom: 24,
|
||||
},
|
||||
emptyButton: {
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
});
|
||||
|
||||
export default ClubHubScreen;
|
||||
@@ -0,0 +1,91 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet, Alert } from 'react-native';
|
||||
import { isAdminLogged, isFanLogged } from '../../services/auth';
|
||||
import { useClubTheme } from '../../theme';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useClub } from '../../contexts/ClubContext';
|
||||
|
||||
export default function MoreScreen() {
|
||||
const { theme, settings } = useClubTheme();
|
||||
const navigation = useNavigation<any>();
|
||||
const { resetClub } = useClub();
|
||||
const [isAdmin, setIsAdmin] = useState<boolean | null>(null);
|
||||
const [isFan, setIsFan] = useState<boolean | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
const admin = await isAdminLogged();
|
||||
const fan = await isFanLogged();
|
||||
setIsAdmin(admin);
|
||||
setIsFan(fan);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const handleResetClub = async () => {
|
||||
Alert.alert('Změnit klub', 'Opravdu chcete změnit klub? Budete odhlášeni.', [
|
||||
{ text: 'Zrušit', style: 'cancel' },
|
||||
{
|
||||
text: 'Změnit',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
try {
|
||||
await resetClub();
|
||||
Alert.alert('Hotovo', 'Klub byl změněn.');
|
||||
} catch (error) {
|
||||
Alert.alert('Chyba', 'Nepodařilo se změnit klub.');
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const goAdminValidator = () => navigation.navigate('AdminQRValidator');
|
||||
const goFanLogin = () => navigation.navigate('FanLogin');
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Více</Text>
|
||||
<Text style={styles.clubInfo}>Klub: {settings?.club_name || 'neznámý'}</Text>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Přihlášení</Text>
|
||||
{isAdmin === false && (
|
||||
<TouchableOpacity style={[styles.item, { borderLeftColor: theme.primary }]} onPress={goAdminValidator}>
|
||||
<Text style={styles.itemText}>Admin přihlášení / QR validátor</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{isFan === false && (
|
||||
<TouchableOpacity style={[styles.item, { borderLeftColor: theme.primary }]} onPress={goFanLogin}>
|
||||
<Text style={styles.itemText}>Přihlášení fanouška / QR průkaz</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{isAdmin && <Text style={styles.status}>✅ Přihlášen jako admin</Text>}
|
||||
{isFan && <Text style={styles.status}>✅ Přihlášen jako fanoušek</Text>}
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Nastavení</Text>
|
||||
<TouchableOpacity style={[styles.item, { borderLeftColor: theme.secondary }]} onPress={handleResetClub}>
|
||||
<Text style={styles.itemText}>Změnit klub</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>MyClub Mobile v0.1</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, padding: 24, gap: 20, backgroundColor: '#fff' },
|
||||
title: { fontSize: 24, fontWeight: '700', textAlign: 'center' },
|
||||
clubInfo: { fontSize: 16, color: '#4B5563', textAlign: 'center' },
|
||||
section: { gap: 8 },
|
||||
sectionTitle: { fontSize: 18, fontWeight: '600', marginBottom: 4 },
|
||||
item: { paddingVertical: 12, paddingHorizontal: 16, borderLeftWidth: 4, backgroundColor: '#F9FAFB', borderRadius: 6 },
|
||||
itemText: { fontSize: 16 },
|
||||
status: { fontSize: 14, color: '#059669' },
|
||||
footer: { marginTop: 'auto', alignItems: 'center' },
|
||||
footerText: { fontSize: 12, color: '#9CA3AF' },
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity, ScrollView } from 'react-native';
|
||||
import QRCode from 'react-native-qrcode-svg';
|
||||
import { getApi } from '../../services/api';
|
||||
import { TicketQRData } from '../../types/tickets';
|
||||
import { buildQrPayload } from '../../services/qrHelper';
|
||||
|
||||
const TicketScreen: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [ticket, setTicket] = useState<TicketQRData | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const api = await getApi();
|
||||
const res = await api.get('/tickets/my-tickets');
|
||||
const first = Array.isArray(res.data) ? res.data.find((t: any) => t.status === 'paid') : null;
|
||||
if (!first) {
|
||||
setError('Žádná zaplacená vstupenka nebyla nalezena.');
|
||||
} else {
|
||||
setTicket(buildQrPayload(first, first.campaign));
|
||||
}
|
||||
} catch (e: any) {
|
||||
setError('Nepodařilo se načíst vstupenky');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
if (loading) return <View style={styles.center}><ActivityIndicator /></View>;
|
||||
if (error) return <View style={styles.center}><Text>{error}</Text></View>;
|
||||
if (!ticket) return <View style={styles.center}><Text>Žádné vstupenky</Text></View>;
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={styles.container}>
|
||||
<Text style={styles.title}>{ticket.campaignTitle}</Text>
|
||||
<Text style={styles.sub}>QR průkaz pro vstup</Text>
|
||||
<View style={styles.qrBox}>
|
||||
<QRCode value={JSON.stringify(ticket)} size={240} backgroundColor="#fff" color="#000" />
|
||||
</View>
|
||||
<Text style={styles.code}>Kód: {ticket.barcode}</Text>
|
||||
<Text style={styles.info}>Držitel: {ticket.holderName}</Text>
|
||||
{ticket.matchDateTime && <Text style={styles.info}>Datum: {ticket.matchDateTime}</Text>}
|
||||
{ticket.venue && <Text style={styles.info}>Místo: {ticket.venue}</Text>}
|
||||
<TouchableOpacity style={styles.button} onPress={() => { /* TODO: add Wallet pass later */ }}>
|
||||
<Text style={styles.buttonText}>Uložit / sdílet</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { padding: 24, alignItems: 'center', gap: 12 },
|
||||
center: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 24 },
|
||||
title: { fontSize: 20, fontWeight: '700', textAlign: 'center' },
|
||||
sub: { fontSize: 14, color: '#4B5563', marginBottom: 8 },
|
||||
qrBox: { padding: 16, backgroundColor: '#fff', borderRadius: 12, elevation: 2 },
|
||||
code: { fontFamily: 'monospace', marginTop: 8 },
|
||||
info: { fontSize: 14 },
|
||||
button: { marginTop: 16, backgroundColor: '#0B5ED7', paddingVertical: 12, paddingHorizontal: 24, borderRadius: 8 },
|
||||
buttonText: { color: '#fff', fontWeight: '700' },
|
||||
});
|
||||
|
||||
export default TicketScreen;
|
||||
Reference in New Issue
Block a user