Ekran — Dashboard
Opis
Dashboard to glowny ekran po zalogowaniu. Wyswietla podsumowanie aktywnosci dziecka: statystyki, wykres aktywnosci, ostatnie zadania i nadchodzace todo. Dane ladowane SSR (server-side rendering) — pelna strona renderowana na serwerze.
ASCII Wireframe
+--------+-----------------------------------------------------------+
| | Top Bar |
| LOGO | Dashboard [Ania v] [avatar] [logout] |
| +-----------------------------------------------------------+
| SIDE | |
| BAR | +------------+ +------------+ +------------+ +---------+ |
| | | * XP | | ** Level | | v Tasks | | ~ Streak| |
| Dash. | | 2,450 | | 12 | | 48 | | 7 dni | |
| Content| | +12% ^^ | | Odkrywca | | +5 ten tydz| | rekord! | |
| Worlds | +------------+ +------------+ +------------+ +---------+ |
| Todo | |
| ---- | +--------------------------------------------+ |
| Settng | | Aktywnosc (ostatnie 7 dni) | |
| | | | |
| | | ## | |
| | | ## ## ## | |
| | | ## ## ## ## ## ## | |
| | | ## ## ## ## ## ## ## ## | |
| | | Pn Wt Sr Cz Pt So Nd | |
| | +--------------------------------------------+ |
| | |
| | +---------------------------+ +------------------------+ |
| | | Ostatnie zadania | | Nadchodzace todo | |
| | | | | | |
| | | v Tabliczka mnozenia | | [ ] Przeczytaj roz. 3 | |
| | | Quiz - 2h temu +85XP | | Jutro | |
| | | | | | |
| | | v Stolice Europy | | [ ] Test z przyrody | |
| | | Quiz - wczoraj +120XP | | Za 3 dni | |
| | | | | | |
| | | v Czytanie ze zrozum. | | [ ] Projekt plastyczny | |
| | | Task - wczoraj +60XP | | Za 5 dni | |
| | | | | | |
| | | > Zobacz wszystkie | | > Zobacz wszystkie | |
| | +---------------------------+ +------------------------+ |
| | |
+--------+-----------------------------------------------------------+Struktura strony
Top Bar z child selectorem
html
<header class="sticky top-0 z-30 bg-white/80 backdrop-blur-sm border-b border-gray-100">
<div class="flex items-center justify-between px-6 py-4">
<!-- Tytul strony -->
<h1 class="text-2xl font-display font-bold text-gray-900">
Dashboard
</h1>
<!-- Prawa strona: child selector + user -->
<div class="flex items-center gap-4">
<!-- Child selector (jesli rodzic ma wiele dzieci) -->
{{ if gt (len .Children) 1 }}
<div x-data="{ open: false }" class="relative">
<button @click="open = !open"
class="inline-flex items-center gap-2 px-3.5 py-2
bg-gray-50 rounded-lg text-sm font-medium text-gray-700
hover:bg-gray-100 transition-colors duration-150">
<div class="w-6 h-6 rounded-full bg-indigo-100 flex items-center justify-center">
<span class="text-xs font-semibold text-indigo-600">
{{ .ActiveChild.Initials }}
</span>
</div>
{{ .ActiveChild.Name }}
<i data-lucide="chevron-down" class="w-4 h-4 text-gray-400"></i>
</button>
<!-- Dropdown -->
<div x-show="open" @click.away="open = false"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute right-0 mt-2 w-56 bg-white rounded-xl border border-gray-100
shadow-lg py-1 z-50">
{{ range .Children }}
<a href="/dashboard?child={{ .ID }}"
class="flex items-center gap-3 px-4 py-2.5 text-sm
{{ if .IsActive }}bg-indigo-50 text-indigo-700{{ else }}text-gray-700 hover:bg-gray-50{{ end }}
transition-colors duration-150">
<div class="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center">
<span class="text-xs font-semibold text-indigo-600">{{ .Initials }}</span>
</div>
<div>
<div class="font-medium">{{ .Name }}</div>
<div class="text-xs text-gray-500">Poziom {{ .Level }}</div>
</div>
</a>
{{ end }}
</div>
</div>
{{ end }}
<!-- User avatar + logout -->
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center">
<i data-lucide="user" class="w-4 h-4 text-gray-500"></i>
</div>
<a href="/logout" class="text-sm text-gray-500 hover:text-gray-700">
Wyloguj
</a>
</div>
</div>
</div>
</header>Stat Cards
html
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
<!-- XP Card -->
<div class="bg-white rounded-xl border border-gray-100 shadow-sm p-6">
<div class="flex items-center gap-4">
<div class="flex-shrink-0 w-12 h-12 bg-amber-50 rounded-xl
flex items-center justify-center">
<i data-lucide="zap" class="w-6 h-6 text-amber-600"></i>
</div>
<div>
<p class="text-sm font-medium text-gray-500">Zebrane XP</p>
<p class="text-2xl font-display font-bold text-gray-900">
{{ .Stats.TotalXP | formatNumber }}
</p>
</div>
</div>
<div class="mt-4 flex items-center gap-1 text-xs">
<i data-lucide="trending-up" class="w-3.5 h-3.5 text-green-600"></i>
<span class="font-medium text-green-600">+{{ .Stats.XPChange }}%</span>
<span class="text-gray-500">vs poprzedni tydzien</span>
</div>
</div>
<!-- Level Card -->
<div class="bg-white rounded-xl border border-gray-100 shadow-sm p-6">
<div class="flex items-center gap-4">
<div class="flex-shrink-0 w-12 h-12 bg-indigo-50 rounded-xl
flex items-center justify-center">
<i data-lucide="trophy" class="w-6 h-6 text-indigo-600"></i>
</div>
<div>
<p class="text-sm font-medium text-gray-500">Poziom</p>
<p class="text-2xl font-display font-bold text-gray-900">
{{ .Stats.Level }}
</p>
</div>
</div>
<div class="mt-4">
<p class="text-xs text-gray-500">{{ .Stats.LevelName }}</p>
<!-- Progress bar do nastepnego poziomu -->
<div class="mt-1.5 w-full bg-gray-100 rounded-full h-1.5">
<div class="bg-indigo-600 h-1.5 rounded-full"
style="width: {{ .Stats.LevelProgress }}%"></div>
</div>
</div>
</div>
<!-- Tasks Completed Card -->
<div class="bg-white rounded-xl border border-gray-100 shadow-sm p-6">
<div class="flex items-center gap-4">
<div class="flex-shrink-0 w-12 h-12 bg-green-50 rounded-xl
flex items-center justify-center">
<i data-lucide="check-circle" class="w-6 h-6 text-green-600"></i>
</div>
<div>
<p class="text-sm font-medium text-gray-500">Ukonczone zadania</p>
<p class="text-2xl font-display font-bold text-gray-900">
{{ .Stats.TasksCompleted }}
</p>
</div>
</div>
<div class="mt-4 flex items-center gap-1 text-xs">
<span class="font-medium text-gray-600">+{{ .Stats.TasksThisWeek }}</span>
<span class="text-gray-500">ten tydzien</span>
</div>
</div>
<!-- Streak Card -->
<div class="bg-white rounded-xl border border-gray-100 shadow-sm p-6">
<div class="flex items-center gap-4">
<div class="flex-shrink-0 w-12 h-12 bg-orange-50 rounded-xl
flex items-center justify-center">
<i data-lucide="flame" class="w-6 h-6 text-orange-600"></i>
</div>
<div>
<p class="text-sm font-medium text-gray-500">Seria</p>
<p class="text-2xl font-display font-bold text-gray-900">
{{ .Stats.Streak }} dni
</p>
</div>
</div>
<div class="mt-4 flex items-center gap-1 text-xs">
<i data-lucide="award" class="w-3.5 h-3.5 text-amber-500"></i>
<span class="text-gray-500">Rekord: {{ .Stats.BestStreak }} dni</span>
</div>
</div>
</div>Wykres aktywnosci
Prosty wykres slupkowy renderowany w HTML/CSS (bez zewnetrznej biblioteki):
html
<div class="bg-white rounded-xl border border-gray-100 shadow-sm p-6">
<h3 class="text-lg font-display font-semibold text-gray-800">
Aktywnosc
</h3>
<p class="text-sm text-gray-500 mt-1">Ostatnie 7 dni</p>
<div class="mt-6 flex items-end justify-between gap-3 h-40">
{{ range .ActivityChart }}
<div class="flex-1 flex flex-col items-center gap-2">
<!-- Slupek -->
<div class="w-full bg-indigo-100 rounded-t-lg relative"
style="height: {{ .HeightPercent }}%">
<div class="absolute -top-6 left-1/2 -translate-x-1/2 text-xs font-medium text-gray-600">
{{ .XP }}
</div>
</div>
<!-- Label dnia -->
<span class="text-xs text-gray-500 font-medium">{{ .DayShort }}</span>
</div>
{{ end }}
</div>
</div>Ostatnie zadania
html
<div class="bg-white rounded-xl border border-gray-100 shadow-sm">
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
<h3 class="text-lg font-display font-semibold text-gray-800">
Ostatnie zadania
</h3>
<a href="/content" class="text-sm text-indigo-600 hover:text-indigo-800 font-medium">
Zobacz wszystkie
</a>
</div>
<ul class="divide-y divide-gray-50">
{{ range .RecentTasks }}
<li class="px-6 py-4 flex items-center justify-between hover:bg-gray-50
transition-colors duration-150">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg bg-green-50 flex items-center justify-center">
<i data-lucide="check" class="w-4 h-4 text-green-600"></i>
</div>
<div>
<p class="text-sm font-medium text-gray-900">{{ .Title }}</p>
<p class="text-xs text-gray-500">{{ .TypeLabel }} - {{ .TimeAgo }}</p>
</div>
</div>
<span class="text-sm font-semibold text-amber-600">+{{ .XP }} XP</span>
</li>
{{ end }}
</ul>
</div>Todo Preview
html
<div class="bg-white rounded-xl border border-gray-100 shadow-sm">
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
<h3 class="text-lg font-display font-semibold text-gray-800">
Nadchodzace todo
</h3>
<a href="/todo" class="text-sm text-indigo-600 hover:text-indigo-800 font-medium">
Zobacz wszystkie
</a>
</div>
<ul class="divide-y divide-gray-50">
{{ range .UpcomingTodos }}
<li class="px-6 py-4 flex items-center justify-between hover:bg-gray-50
transition-colors duration-150">
<div class="flex items-center gap-3">
<div class="w-5 h-5 border-2 border-gray-300 rounded
{{ if .IsCompleted }}bg-green-500 border-green-500{{ end }}">
{{ if .IsCompleted }}
<i data-lucide="check" class="w-full h-full text-white"></i>
{{ end }}
</div>
<div>
<p class="text-sm font-medium text-gray-900">{{ .Title }}</p>
<p class="text-xs text-gray-500">{{ .DueDateLabel }}</p>
</div>
</div>
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
{{ if .IsOverdue }}bg-red-50 text-red-700{{ else }}bg-gray-100 text-gray-600{{ end }}">
{{ .StatusLabel }}
</span>
</li>
{{ end }}
</ul>
</div>Layout sekcji na dashboardzie
html
<main class="max-w-7xl mx-auto px-6 py-8 space-y-8">
<!-- Stat cards -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
<!-- ... 4 stat cards ... -->
</div>
<!-- Activity chart (pelna szerokosc) -->
<div>
<!-- ... wykres aktywnosci ... -->
</div>
<!-- Bottom row: dwie kolumny -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Ostatnie zadania -->
<div><!-- ... --></div>
<!-- Todo preview -->
<div><!-- ... --></div>
</div>
</main>Endpointy (web routes)
| Metoda | URL | Opis |
|---|---|---|
| GET | /dashboard | Pelna strona dashboard (SSR) |
| GET | /dashboard?child={id} | Dashboard dla wybranego dziecka |
Go Handler
go
func DashboardHandler(w http.ResponseWriter, r *http.Request) {
user := middleware.GetUser(r.Context())
// Wybor dziecka
childID := r.URL.Query().Get("child")
child, children := childService.GetActiveChild(r.Context(), user.ID, childID)
// Pobranie danych
stats := statsService.GetChildStats(r.Context(), child.ID)
activity := statsService.GetWeeklyActivity(r.Context(), child.ID)
recentTasks := taskService.GetRecentCompleted(r.Context(), child.ID, 5)
upcomingTodos := todoService.GetUpcoming(r.Context(), child.ID, 3)
data := DashboardData{
ActiveChild: child,
Children: children,
Stats: stats,
ActivityChart: activity,
RecentTasks: recentTasks,
UpcomingTodos: upcomingTodos,
}
renderTemplate(w, "dashboard.html", data)
}