Ekran — Lokacja (Netflix-style)
Przegląd
Ekran lokacji wyświetla zadania w stylu Netflix — header z obrazem i nazwą lokacji, a pod nim horyzontalnie przewijane rzędy kafelków zadań pogrupowane wg typu (video, quiz, podcast, gra, kreatywne). Obsługuje filtrowanie po grupie wiekowej i paginację kursorową (infinite scroll).
Routing (GoRouter)
dart
GoRoute(
path: '/location/:locationId',
builder: (context, state) => LocationScreen(
locationId: state.pathParameters['locationId']!,
),
),Riverpod Providers
dart
// Dane lokacji (header + zadania pogrupowane wg typu)
final locationDetailProvider = FutureProvider.family<LocationDetail, String>((ref, locationId) {
return ref.watch(locationRepositoryProvider).getLocation(locationId);
});
// Filtr grupy wiekowej
final ageGroupFilterProvider = StateProvider<AgeGroup?>((ref) => null);
// Paginacja kursorem per sekcja (typ zadania)
final taskSectionProvider = StateNotifierProvider.family<TaskSectionNotifier, TaskSectionState, TaskSectionParams>(
(ref, params) => TaskSectionNotifier(
ref.watch(locationRepositoryProvider),
params,
),
);
// Aktualnie wybrany task (do nawigacji)
final selectedTaskProvider = StateProvider<Task?>((ref) => null);ASCII Wireframe
┌──────────────────────────────────┐
│ ← Camp Odkrywców │
├──────────────────────────────────┤
│ ┌────────────────────────────────┤
│ │ │
│ │ (hero image lokacji) │
│ │ │
│ │ Camp Odkrywców │
│ │ 🇵🇱 Polska · 11 zadań │
│ │ Postęp: ████░░░░ 40% │
│ │ │
│ └────────────────────────────────┘
│ │
│ [Wszystkie] [6-8] [9-11] [12+] │
│ │
│ 🎬 Video │
│ ┌──────┐ ┌──────┐ ┌──────┐ ──▶ │
│ │thumb │ │thumb │ │thumb │ │
│ │ │ │ │ │ │ │
│ │Tytuł │ │Tytuł │ │Tytuł │ │
│ │🎬 50XP│ │🎬 30XP│ │🎬 40XP│ │
│ │ ✅ │ │ ○ │ │ ○ │ │
│ └──────┘ └──────┘ └──────┘ │
│ │
│ 📝 Quizy │
│ ┌──────┐ ┌──────┐ ┌──────┐ ──▶ │
│ │thumb │ │thumb │ │thumb │ │
│ │ │ │ │ │ │ │
│ │Tytuł │ │Tytuł │ │Tytuł │ │
│ │📝 40XP│ │📝 35XP│ │📝 45XP│ │
│ │ ✅ │ │ ✅ │ │ ○ │ │
│ └──────┘ └──────┘ └──────┘ │
│ │
│ 🎧 Podcasty │
│ ┌──────┐ ┌──────┐ ──▶ │
│ │thumb │ │thumb │ │
│ │ │ │ │ │
│ │Tytuł │ │Tytuł │ │
│ │🎧 30XP│ │🎧 25XP│ │
│ │ ○ │ │ ○ │ │
│ └──────┘ └──────┘ │
│ │
│ 🎮 Mini-gry │
│ ┌──────┐ ──▶ │
│ │thumb │ │
│ │ │ │
│ │Tytuł │ │
│ │🎮 60XP│ │
│ │ ○ │ │
│ └──────┘ │
│ │
└──────────────────────────────────┘Kafelek zadania — szczegóły
┌──────────────────┐
│ ┌──────────────┐│
│ │ ││
│ │ thumbnail ││
│ │ ││
│ │ 🎬 ││ ← ikona typu (prawy dolny róg)
│ └──────────────┘│
│ │
│ Dinozaury i │
│ ich świat │
│ │
│ 🎬 50 XP ✅ │ ← typ, nagroda, status
│ │
└──────────────────┘Komponenty
| Komponent | Opis |
|---|---|
LocationHeader | Hero image z gradientem, nazwa lokacji, kraj, statystyki, postęp |
AgeGroupChips | Horizontal chip bar: "Wszystkie", "6-8", "9-11", "12+" |
TaskSection | Sekcja z nagłówkiem typu + horizontal ListView zadań |
TaskCard | Kafelek zadania: thumbnail, tytuł, ikona typu, XP, status |
TaskTypeIcon | Ikona typu zadania (video, quiz, podcast, game, creative) |
CompletionBadge | Znacznik ukończenia: checkmark (done), kółko (not done), w trakcie |
XpBadge | Badge z liczbą XP do zdobycia |
SectionHeader | Nagłówek sekcji z ikoną i nazwą typu zadań |
LocationAppBar | App bar z przyciskiem back i nazwą lokacji |
Logika
- Przy wejściu — pobranie danych lokacji (
GET /app/locations/:id) - Zadania pogrupowane wg typu i wyświetlone w horyzontalnych sekcjach
- Filtr grupy wiekowej — filtruje zadania w ramach każdej sekcji (wyświetla zadania, których tablica
age_groupszawiera wybraną grupę) - Infinite scroll per sekcja — po dotarciu do końca listy horyzontalnej, ładowanie kolejnych (cursor pagination)
- Tap na kafelek → nawigacja do
/task/:taskId - Pull-to-refresh odświeża całą lokację
Stany kafelka zadania
| Status | Wygląd |
|---|---|
new | Puste kółko, pełna opacity |
in_progress | Kółko z fragmentem wypełnienia (np. 50%), żółta ramka |
completed | Zielony checkmark, lekka redukcja opacity (0.8) |
locked | Kłódka, szary overlay, disabled tap |
Ikony typów zadań
| Typ | Ikona | Kolor |
|---|---|---|
video | Film/kamera | #EF4444 (red) |
quiz | Znaki zapytania | #6366F1 (indigo) |
podcast | Słuchawki | #8B5CF6 (violet) |
game | Gamepad | #10B981 (emerald) |
creative | Pędzel | #F59E0B (amber) |
Endpoint
| Metoda | Endpoint | Opis |
|---|---|---|
| GET | /app/locations/:id | Dane lokacji + zadania (z paginacją) |
Query params:
| Param | Typ | Domyślnie | Opis |
|---|---|---|---|
age_group | string | – | Filtr grupy wiekowej (np. 4-8, 8-13, 14+). Zwraca zadania, których tablica age_groups zawiera podaną wartość |
cursor | string | – | Kursor paginacji |
limit | int | 20 | Maks. elementów per typ |
Response:
json
{
"data": {
"id": "loc-camp",
"name": "Camp Odkrywców",
"world_name": "Polska",
"world_flag": "🇵🇱",
"header_image_url": "https://...",
"description": "Twoja pierwsza przygoda!",
"progress_percent": 40,
"task_count": 11,
"completed_count": 4,
"sections": [
{
"type": "video",
"label": "Video",
"tasks": [
{
"id": "task-v1",
"title": "Dinozaury i ich świat",
"type": "video",
"thumbnail_url": "https://...",
"xp_reward": 50,
"status": "completed",
"age_groups": ["6-8", "8-13"],
"duration_minutes": 8
},
{
"id": "task-v2",
"title": "Podróż do kosmosu",
"type": "video",
"thumbnail_url": "https://...",
"xp_reward": 30,
"status": "new",
"age_groups": ["8-13"],
"duration_minutes": 12
}
],
"next_cursor": "eyJ0IjoidmlkZW8iLCJpZCI6MTB9",
"has_more": true
},
{
"type": "quiz",
"label": "Quizy",
"tasks": [
{
"id": "task-q1",
"title": "Zwierzęta Polski",
"type": "quiz",
"thumbnail_url": "https://...",
"xp_reward": 40,
"status": "completed",
"age_groups": ["6-8"],
"question_count": 10
}
],
"next_cursor": null,
"has_more": false
},
{
"type": "podcast",
"label": "Podcasty",
"tasks": [],
"next_cursor": null,
"has_more": false
},
{
"type": "game",
"label": "Mini-gry",
"tasks": [
{
"id": "task-g1",
"title": "Labirynt wiedzy",
"type": "game",
"thumbnail_url": "https://...",
"xp_reward": 60,
"status": "new",
"age_groups": ["6-8"]
}
],
"next_cursor": null,
"has_more": false
}
]
}
}Stany Ekranu
| Stan | Widok |
|---|---|
| Ładowanie | Skeleton header + skeleton kafelki (shimmer) |
| Dane | Pełny layout z sekcjami |
| Pusty (po filtrze) | Komunikat "Brak zadań dla tej grupy wiekowej" |
| Błąd | ErrorView z przyciskiem retry |
Uwagi Techniczne
- Każda sekcja to
SizedBoxzListView.builder(horizontal, lazy loading) - Infinite scroll per sekcja:
ScrollController+addListener→ fetch next cursor - Filtrowanie po grupie wiekowej — lokalne (dane w pamięci) lub z API (query param)
- Kafelki mają stały rozmiar 140x200 (thumbnail 140x100 + tekst)
- Hero animation na thumbnail przy przejściu do ekranu zadania
- Sekcje z 0 zadań są ukrywane