Ekran — Profil i Ranking
Przegląd
Ekran profilu dziecka wyświetla awatar, statystyki, kolekcję odznak i dostęp do ustawień. Opcjonalnie zawiera sekcję rankingu (leaderboard) z tabelą wyników i pozycją aktualnego użytkownika.
Routing (GoRouter)
dart
// Profil jest częścią ShellRoute (bottom nav)
GoRoute(path: '/profile', builder: (_, __) => ProfileScreen()),
// Szczegóły odznaki
GoRoute(
path: '/profile/badge/:badgeId',
builder: (context, state) => BadgeDetailScreen(
badgeId: state.pathParameters['badgeId']!,
),
),
// Ustawienia
GoRoute(path: '/profile/settings', builder: (_, __) => SettingsScreen()),
// Ranking (opcjonalnie osobna strona lub sekcja)
GoRoute(path: '/profile/ranking', builder: (_, __) => RankingScreen()),Riverpod Providers
dart
// Profil użytkownika (reuse z spaceship)
final userProfileProvider = FutureProvider<UserProfile>((ref) {
return ref.watch(userRepositoryProvider).getMe();
});
// Statystyki (reuse z spaceship)
final userStatsProvider = FutureProvider<UserStats>((ref) {
return ref.watch(userRepositoryProvider).getStats();
});
// Pełna lista odznak
final allBadgesProvider = FutureProvider<List<Badge>>((ref) {
return ref.watch(userRepositoryProvider).getBadges();
});
// Ranking (paginowany)
final rankingProvider = StateNotifierProvider<RankingNotifier, PaginatedList<RankingEntry>>(
(ref) => RankingNotifier(ref.watch(rankingRepositoryProvider)),
);
// Pozycja aktualnego użytkownika w rankingu
final myRankProvider = FutureProvider<int>((ref) {
return ref.watch(rankingRepositoryProvider).getMyRank();
});Profil
ASCII Wireframe
┌──────────────────────────────────┐
│ Profil ⚙️ │
├──────────────────────────────────┤
│ │
│ ┌──────────────┐ │
│ │ │ │
│ │ 👤 │ │
│ │ (awatar) │ │
│ │ │ │
│ └──────────────┘ │
│ │
│ Kacper │
│ Poziom 5 │
│ ████████████░░░░ 1240/1500 XP │
│ │
│ ┌────────────────────────────┐ │
│ │ │ │
│ │ ┌──────┐ ┌──────┐ │ │
│ │ │ 42 │ │ 3 │ │ │
│ │ │zadań │ │światów│ │ │
│ │ │ukończ│ │odwiedz│ │ │
│ │ └──────┘ └──────┘ │ │
│ │ ┌──────┐ ┌──────┐ │ │
│ │ │ 7 │ │ 12 │ │ │
│ │ │ dni │ │odznak│ │ │
│ │ │streak│ │ │ │ │
│ │ └──────┘ └──────┘ │ │
│ │ │ │
│ └────────────────────────────┘ │
│ │
│ ── Moje odznaki (12) ── │
│ │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ 🏅 │ │ 🎖️ │ │ 🥇 │ │ 🏆 │ │
│ │Dino│ │Geog│ │Quiz│ │7dni│ │
│ └────┘ └────┘ └────┘ └────┘ │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ 🌟 │ │ 📚 │ │ 🎨 │ │ 🎮 │ │
│ │Lvl5│ │10vid│ │Art │ │Game│ │
│ └────┘ └────┘ └────┘ └────┘ │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ 🔬 │ │ 🌍 │ │ 🎧 │ │ 🧩 │ │
│ │Lab │ │Glob│ │Pod │ │Puzz│ │
│ └────┘ └────┘ └────┘ └────┘ │
│ │
│ ── Ranking ── │
│ [ ZOBACZ RANKING → ] │
│ │
├──────────────────────────────────┤
│ 🌍 🪐 🚀 👤 │
│ Globus Kosmos Statek Profil│
└──────────────────────────────────┘Szczegóły odznaki (bottom sheet)
┌──────────────────────────────────┐
│ │
│ ┌──────────┐ │
│ │ 🏅 │ │
│ │ (duża │ │
│ │ ikona) │ │
│ └──────────┘ │
│ │
│ Odkrywca Dinozaurów │
│ │
│ Ukończ wszystkie zadania │
│ o dinozaurach. │
│ │
│ Zdobyta: 5 marca 2026 │
│ │
│ [ ZAMKNIJ ] │
│ │
└──────────────────────────────────┘Komponenty — Profil
| Komponent | Opis |
|---|---|
AvatarDisplay | Awatar dziecka (duży, display mode) |
UserNameLevel | Imię + numer poziomu |
XpProgressBar | Pasek XP z liczbami (current / next level) |
StatsGrid | Siatka 2x2 ze statystykami |
StatCard | Karta statystyki: duża liczba + label |
BadgeCollection | Siatka odznak (GridView, 4 kolumny) |
BadgeTile | Kafelek odznaki: ikona + krótka nazwa |
BadgeDetailSheet | Bottom sheet ze szczegółami odznaki |
RankingLink | Przycisk "ZOBACZ RANKING" |
SettingsButton | Ikona ustawień w app bar |
Logika — Profil
- Przy wejściu → pobranie: profil, stats, badges
- Awatar wyświetlony z aktualnym ubraniem
- Statystyki w siatce 2x2
- Odznaki w grid — tap →
BadgeDetailSheet - "ZOBACZ RANKING" → nawigacja do
/profile/ranking - Ikona ⚙️ → nawigacja do
/profile/settings
Endpointy — Profil
| Metoda | Endpoint | Opis |
|---|---|---|
| GET | /app/users/me | Profil użytkownika |
| GET | /app/users/me/stats | Statystyki |
| GET | /app/users/me/badges | Pełna lista odznak |
Ranking
ASCII Wireframe
┌──────────────────────────────────┐
│ ← Ranking │
├──────────────────────────────────┤
│ │
│ ┌────────────────────────────┐ │
│ │ 🥇 1. Zosia 2450 XP │ │
│ │ 👧 │ │
│ └────────────────────────────┘ │
│ ┌────────────────────────────┐ │
│ │ 🥈 2. Kuba 2380 XP │ │
│ │ 🧒 │ │
│ └────────────────────────────┘ │
│ ┌────────────────────────────┐ │
│ │ 🥉 3. Marysia 2210 XP │ │
│ │ 👧 │ │
│ └────────────────────────────┘ │
│ ┌────────────────────────────┐ │
│ │ 4. Tomek 2100 XP │ │
│ │ 🧒 │ │
│ └────────────────────────────┘ │
│ ┌────────────────────────────┐ │
│ │ 5. Jan 1950 XP │ │
│ │ 🧒 │ │
│ └────────────────────────────┘ │
│ │
│ ... │
│ │
│ ┌────────────────────────────┐ │
│ │ 12. Ola 1400 XP │ │
│ │ 👧 │ │
│ └────────────────────────────┘ │
│ │
│ ════════════════════════════ │
│ ┌────────────────────────────┐ │
│ │ ★ 8. Kacper 1240 XP ★ │ │
│ │ 👤 (Ty) │ │
│ └────────────────────────────┘ │
│ ════════════════════════════ │
│ │
│ (ładowanie kolejnych...) │
│ │
└──────────────────────────────────┘Komponenty — Ranking
| Komponent | Opis |
|---|---|
RankingList | ListView z infinite scroll (cursor pagination) |
RankingEntry | Wiersz: pozycja, awatar, imię, XP |
RankingEntry.podium | Top 3: medal (🥇🥈🥉), większy awatar, wyróżniony kolor |
RankingEntry.current | Aktualny użytkownik: wyróżniony tłem (brand-primary/10%), gwiazdki |
MyRankSticky | Sticky footer z pozycją aktualnego użytkownika (widoczny gdy user nie jest na ekranie) |
Logika — Ranking
- Przy wejściu →
GET /app/rankings(pierwsza strona) - Infinite scroll → kolejne strony (cursor pagination)
- Pozycja aktualnego użytkownika wyróżniona w liście
- Jeśli użytkownik nie jest widoczny na ekranie → sticky footer z jego pozycją
- Top 3 z medalami i wyróżnionym stylem
Endpoint — Ranking
| Metoda | Endpoint | Opis |
|---|---|---|
| GET | /app/rankings | Tablica wyników |
Query params:
| Param | Typ | Domyślnie | Opis |
|---|---|---|---|
cursor | string | – | Kursor paginacji |
limit | int | 20 | Maks. elementów |
Response:
json
{
"data": [
{
"position": 1,
"user_id": "child-zosia",
"display_name": "Zosia",
"avatar_url": "https://...",
"xp": 2450,
"level": 7,
"is_current_user": false
},
{
"position": 2,
"user_id": "child-kuba",
"display_name": "Kuba",
"avatar_url": "https://...",
"xp": 2380,
"level": 7,
"is_current_user": false
},
{
"position": 8,
"user_id": "child-1",
"display_name": "Kacper",
"avatar_url": "https://...",
"xp": 1240,
"level": 5,
"is_current_user": true
}
],
"next_cursor": "eyJwb3MiOjIwfQ==",
"has_more": true,
"my_position": 8
}Ustawienia
ASCII Wireframe
┌──────────────────────────────────┐
│ ← Ustawienia │
├──────────────────────────────────┤
│ │
│ Konto │
│ ┌────────────────────────────┐ │
│ │ Zmień PIN → │ │
│ ├────────────────────────────┤ │
│ │ Zmień język → │ │
│ ├────────────────────────────┤ │
│ │ Powiąż z rodzicem → │ │
│ └────────────────────────────┘ │
│ │
│ Aplikacja │
│ ┌────────────────────────────┐ │
│ │ Motyw [Auto ▼] │ │
│ ├────────────────────────────┤ │
│ │ Dźwięki [● włącz] │ │
│ ├────────────────────────────┤ │
│ │ Powiadomienia [● włącz] │ │
│ └────────────────────────────┘ │
│ │
│ Informacje │
│ ┌────────────────────────────┐ │
│ │ O aplikacji → │ │
│ ├────────────────────────────┤ │
│ │ Polityka prywatności → │ │
│ └────────────────────────────┘ │
│ │
│ [ WYLOGUJ SIĘ ] │
│ │
│ Wersja 1.0.0 (build 42) │
│ │
└──────────────────────────────────┘Komponenty — Ustawienia
| Komponent | Opis |
|---|---|
SettingsSection | Sekcja z nagłówkiem i listą opcji |
SettingsItem.navigation | Opcja z chevronem (→) — nawigacja |
SettingsItem.toggle | Opcja z Switch (toggle) |
SettingsItem.dropdown | Opcja z dropdown (np. motyw) |
LogoutButton | Przycisk wylogowania (destructive style) |
VersionLabel | Label z wersją aplikacji |
Stany Ekranu
Profil
| Stan | Widok |
|---|---|
| Ładowanie | Skeleton awatara + skeleton stats + skeleton badges |
| Dane | Pełny profil z awatarem, statystykami, odznakami |
| Błąd | ErrorView z przyciskiem retry |
Ranking
| Stan | Widok |
|---|---|
| Ładowanie | Skeleton entries (5 wierszy z shimmer) |
| Dane | Lista rankingowa z wyróżnionym userem |
| Pusty | "Brak danych rankingu" |
| Błąd | ErrorView z przyciskiem retry |
| Ładowanie więcej | Spinner na dole listy |
Uwagi Techniczne
- Profil i stats cachowane z TTL 5 min — odświeżanie pull-to-refresh
- Badges ładowane pełną listą (nie paginowane — typowo max 50-100)
- Ranking paginowany kursorem (20 per strona)
- Sticky footer z pozycją użytkownika —
StackzPositionedna dole - Top 3 ranking entries: większy rozmiar (height 80 vs 60), gradient tła
- Wylogowanie czyści token z Secure Storage + redirect na
/welcome - Ustawienia motywu zapisywane lokalnie (SharedPreferences)