Skip to content

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)

MetodaURLOpis
GET/dashboardPelna 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)
}

Lumos Islands v2 - Dokumentacja Projektowa