Files
PoleDanceApp/mobile/src/screens/championships/ChampionshipsScreen.tsx
Dianaka123 789d2bf0a6 Full app rebuild: FastAPI backend + React Native mobile with auth, championships, admin
Backend (FastAPI + SQLAlchemy + SQLite):
- JWT auth with access/refresh tokens, bcrypt password hashing
- User model with member/organizer/admin roles, auto-approve members
- Championship, Registration, ParticipantList, Notification models
- Alembic async migrations, seed data with test users
- Registration endpoint returns tokens for members, pending for organizers
- /registrations/my returns championship title/date/location via eager loading
- Admin endpoints: list users, approve/reject organizers

Mobile (React Native + Expo + TypeScript):
- Zustand auth store, Axios client with token refresh interceptor
- Role-based registration (Member vs Organizer) with contextual form labels
- Tab navigation with Ionicons, safe area headers, admin tab for admin role
- Championships list with status badges, detail screen with registration progress
- My Registrations with championship title, progress bar, and tap-to-navigate
- Admin panel with pending/all filter, approve/reject with confirmation
- Profile screen with role badge, Ionicons info rows, sign out
- Password visibility toggle (Ionicons), keyboard flow hints (returnKeyType)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 22:46:50 +03:00

136 lines
4.3 KiB
TypeScript

import { useEffect, useState } from 'react';
import {
View,
Text,
FlatList,
TouchableOpacity,
StyleSheet,
RefreshControl,
ActivityIndicator,
Image,
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { championshipsApi } from '../../api/championships';
import type { Championship } from '../../types';
import type { AppStackParams } from '../../navigation';
const STATUS_COLOR: Record<string, string> = {
open: '#16a34a',
draft: '#9ca3af',
closed: '#dc2626',
completed: '#2563eb',
};
function StatusBadge({ status }: { status: string }) {
return (
<View style={[styles.badge, { backgroundColor: STATUS_COLOR[status] ?? '#9ca3af' }]}>
<Text style={styles.badgeText}>{status.toUpperCase()}</Text>
</View>
);
}
function ChampionshipCard({ item, onPress }: { item: Championship; onPress: () => void }) {
return (
<TouchableOpacity style={styles.card} onPress={onPress} activeOpacity={0.85}>
{item.image_url && (
<Image source={{ uri: item.image_url }} style={styles.cardImage} resizeMode="cover" />
)}
<View style={styles.cardBody}>
<View style={styles.cardHeader}>
<Text style={styles.cardTitle} numberOfLines={2}>{item.title}</Text>
<StatusBadge status={item.status} />
</View>
{item.location && <Text style={styles.cardMeta}>📍 {item.location}</Text>}
{item.event_date && (
<Text style={styles.cardMeta}>
📅 {new Date(item.event_date).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })}
</Text>
)}
{item.entry_fee != null && (
<Text style={styles.cardMeta}>💰 Entry fee: {item.entry_fee} BYN</Text>
)}
</View>
</TouchableOpacity>
);
}
export default function ChampionshipsScreen() {
const navigation = useNavigation<NativeStackNavigationProp<AppStackParams>>();
const [championships, setChampionships] = useState<Championship[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const load = async (silent = false) => {
if (!silent) setLoading(true);
setError(null);
try {
const data = await championshipsApi.list();
setChampionships(data);
} catch {
setError('Failed to load championships');
} finally {
setLoading(false);
setRefreshing(false);
}
};
useEffect(() => { load(); }, []);
if (loading) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" color="#7c3aed" />
</View>
);
}
return (
<FlatList
data={championships}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
ListEmptyComponent={
<View style={styles.center}>
<Text style={styles.empty}>{error ?? 'No championships yet'}</Text>
</View>
}
renderItem={({ item }) => (
<ChampionshipCard
item={item}
onPress={() => navigation.navigate('ChampionshipDetail', { id: item.id })}
/>
)}
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={() => { setRefreshing(true); load(true); }} />
}
/>
);
}
const styles = StyleSheet.create({
list: { padding: 16 },
heading: { fontSize: 24, fontWeight: '700', color: '#1a1a2e', marginBottom: 16 },
center: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingTop: 60 },
empty: { color: '#9ca3af', fontSize: 15 },
card: {
backgroundColor: '#fff',
borderRadius: 14,
marginBottom: 14,
overflow: 'hidden',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 6,
elevation: 3,
},
cardImage: { width: '100%', height: 160 },
cardBody: { padding: 14 },
cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 8 },
cardTitle: { flex: 1, fontSize: 17, fontWeight: '600', color: '#1a1a2e', marginRight: 8 },
badge: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 6 },
badgeText: { color: '#fff', fontSize: 11, fontWeight: '700' },
cardMeta: { fontSize: 13, color: '#555', marginTop: 4 },
});