Skip to content

Ekran — Ustawienia

Opis

Ekran ustawien podzielony jest na sekcje (karty). Kazda sekcja ma wlasny przycisk zapisu i uzywa HTMX do czesciowego wysylania formularza — zmiana jednej sekcji nie wymaga przeladowania calej strony.


ASCII Wireframe

+--------+-----------------------------------------------------------+
|        |  Top Bar                                                   |
| LOGO   |  Ustawienia                                    [Ania v]   |
|        +-----------------------------------------------------------+
| SIDE   |                                                           |
| BAR    |  +-----------------------------------------------------+ |
|        |  | Limity czasu                                         | |
| Dash.  |  |                                                     | |
| Content|  |  Ania:   [====o========] 2h / dzien                 | |
| Worlds |  |  Tomek:  [======o======] 3h / dzien                 | |
| Todo   |  |                                                     | |
| ----   |  |                              [ Zapisz limity ]      | |
| *Settn*|  +-----------------------------------------------------+ |
|        |                                                           |
|        |  +-----------------------------------------------------+ |
|        |  | Grupy wiekowe                                        | |
|        |  |                                                     | |
|        |  |  [x] 6-7 lat    [x] 8-9 lat    [ ] 10-12 lat       | |
|        |  |                                                     | |
|        |  |                        [ Zapisz grupy wiekowe ]     | |
|        |  +-----------------------------------------------------+ |
|        |                                                           |
|        |  +-----------------------------------------------------+ |
|        |  | Powiadomienia                                        | |
|        |  |                                                     | |
|        |  |  Powiadomienia email:    [=o=]  ON                  | |
|        |  |  Raporty tygodniowe:     [=o=]  ON                  | |
|        |  |  Alerty o osiagnieciach: [o==]  OFF                 | |
|        |  |                                                     | |
|        |  |                     [ Zapisz powiadomienia ]        | |
|        |  +-----------------------------------------------------+ |
|        |                                                           |
|        |  +-----------------------------------------------------+ |
|        |  | Konto                                                | |
|        |  |                                                     | |
|        |  |  Email: [ rodzic@example.com              ]         | |
|        |  |                                                     | |
|        |  |  Powiazane dzieci:                                  | |
|        |  |  - Ania (kod: 482917)    [Odlacz]                   | |
|        |  |  - Tomek (kod: 193847)   [Odlacz]                   | |
|        |  |  [ + Dodaj dziecko ]                                | |
|        |  |                                                     | |
|        |  |                             [ Zapisz konto ]        | |
|        |  +-----------------------------------------------------+ |
|        |                                                           |
|        |  +-----------------------------------------------------+ |
|        |  | Strefa zagrozen                   (red border)       | |
|        |  |                                                     | |
|        |  |  [ Odlacz wszystkie dzieci ]                        | |
|        |  |  [ Usun konto ]                                     | |
|        |  +-----------------------------------------------------+ |
|        |                                                           |
+--------+-----------------------------------------------------------+

Struktura strony

html
<main class="max-w-3xl mx-auto px-6 py-8 space-y-8">

  <div>
    <h1 class="text-2xl font-display font-bold text-gray-900">Ustawienia</h1>
    <p class="mt-1 text-sm text-gray-500">Zarzadzaj swoim kontem i preferencjami</p>
  </div>

  <!-- Sekcja: Limity czasu -->
  <!-- Sekcja: Grupy wiekowe -->
  <!-- Sekcja: Powiadomienia -->
  <!-- Sekcja: Konto -->
  <!-- Sekcja: Strefa zagrozen -->

</main>

Sekcja: Limity czasu

html
<div class="bg-white rounded-xl border border-gray-100 shadow-sm" id="settings-time-limits">
  <div class="px-6 py-4 border-b border-gray-100">
    <h2 class="text-lg font-display font-semibold text-gray-800">
      Limity czasu
    </h2>
    <p class="mt-1 text-sm text-gray-500">
      Ustaw dzienny limit czasu ekranowego dla kazdego dziecka
    </p>
  </div>

  <form hx-post="/settings/time-limits"
        hx-target="#settings-time-limits"
        hx-swap="outerHTML"
        class="p-6 space-y-6">

    {{ range .Children }}
    <div class="space-y-2" x-data="{ hours: {{ .DailyLimitHours }} }">
      <div class="flex items-center justify-between">
        <label class="text-sm font-medium text-gray-700">{{ .Name }}</label>
        <span class="text-sm text-gray-500">
          <span x-text="hours"></span>h / dzien
        </span>
      </div>

      <div class="flex items-center gap-4">
        <input type="range" name="limit_{{ .ID }}"
               min="0.5" max="8" step="0.5"
               x-model="hours"
               class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none
                      cursor-pointer accent-indigo-600">
        <input type="number" name="limit_{{ .ID }}_exact"
               min="0.5" max="8" step="0.5"
               x-model="hours"
               class="w-20 px-3 py-2 text-sm text-gray-900 text-center
                      border border-gray-300 rounded-lg
                      focus:outline-none focus:ring-2 focus:ring-indigo-500
                      transition-colors duration-150">
      </div>
    </div>
    {{ end }}

    <div class="pt-2 flex justify-end">
      <button type="submit"
              class="inline-flex items-center gap-2 px-4 py-2.5
                     bg-indigo-600 text-white text-sm font-medium
                     rounded-lg shadow-sm hover:bg-indigo-700
                     transition-colors duration-150">
        Zapisz limity
      </button>
    </div>

  </form>
</div>

Sekcja: Grupy wiekowe

html
<div class="bg-white rounded-xl border border-gray-100 shadow-sm" id="settings-age-groups">
  <div class="px-6 py-4 border-b border-gray-100">
    <h2 class="text-lg font-display font-semibold text-gray-800">
      Grupy wiekowe
    </h2>
    <p class="mt-1 text-sm text-gray-500">
      Wybierz, ktore grupy wiekowe sa aktywne w Twoich swiatach
    </p>
  </div>

  <form hx-post="/settings/age-groups"
        hx-target="#settings-age-groups"
        hx-swap="outerHTML"
        class="p-6">

    <div class="flex flex-wrap gap-4">
      {{ range .AgeGroups }}
      <label class="inline-flex items-center gap-2.5 px-4 py-3
                    border border-gray-200 rounded-lg cursor-pointer
                    has-[:checked]:border-indigo-300 has-[:checked]:bg-indigo-50
                    hover:bg-gray-50 transition-all duration-150">
        <input type="checkbox" name="age_groups" value="{{ .ID }}"
               {{ if .IsActive }}checked{{ end }}
               class="w-4 h-4 text-indigo-600 border-gray-300 rounded
                      focus:ring-indigo-500 focus:ring-2">
        <span class="text-sm font-medium text-gray-700">{{ .Label }}</span>
      </label>
      {{ end }}
    </div>

    <div class="mt-6 flex justify-end">
      <button type="submit"
              class="inline-flex items-center gap-2 px-4 py-2.5
                     bg-indigo-600 text-white text-sm font-medium
                     rounded-lg shadow-sm hover:bg-indigo-700
                     transition-colors duration-150">
        Zapisz grupy wiekowe
      </button>
    </div>

  </form>
</div>

Sekcja: Powiadomienia

html
<div class="bg-white rounded-xl border border-gray-100 shadow-sm" id="settings-notifications">
  <div class="px-6 py-4 border-b border-gray-100">
    <h2 class="text-lg font-display font-semibold text-gray-800">
      Powiadomienia
    </h2>
    <p class="mt-1 text-sm text-gray-500">
      Zarzadzaj powiadomieniami email
    </p>
  </div>

  <form hx-post="/settings/notifications"
        hx-target="#settings-notifications"
        hx-swap="outerHTML"
        class="p-6 space-y-5">

    <!-- Toggle: Powiadomienia email -->
    <div class="flex items-center justify-between">
      <div>
        <p class="text-sm font-medium text-gray-700">Powiadomienia email</p>
        <p class="text-xs text-gray-500">Otrzymuj powiadomienia o aktywnosci dzieci</p>
      </div>
      <label class="relative inline-flex items-center cursor-pointer"
             x-data="{ enabled: {{ .Notifications.Email }} }">
        <input type="checkbox" name="email_notifications"
               x-model="enabled"
               class="sr-only peer">
        <div class="w-11 h-6 bg-gray-200 rounded-full
                    peer-checked:bg-indigo-600
                    peer-focus:ring-2 peer-focus:ring-indigo-500 peer-focus:ring-offset-2
                    after:content-[''] after:absolute after:top-0.5 after:left-[2px]
                    after:bg-white after:rounded-full after:h-5 after:w-5
                    after:shadow-sm after:transition-all
                    peer-checked:after:translate-x-full
                    transition-colors duration-200"></div>
      </label>
    </div>

    <!-- Toggle: Raporty tygodniowe -->
    <div class="flex items-center justify-between">
      <div>
        <p class="text-sm font-medium text-gray-700">Raporty tygodniowe</p>
        <p class="text-xs text-gray-500">Cotygodniowe podsumowanie postepu dzieci</p>
      </div>
      <label class="relative inline-flex items-center cursor-pointer"
             x-data="{ enabled: {{ .Notifications.WeeklyReport }} }">
        <input type="checkbox" name="weekly_report"
               x-model="enabled"
               class="sr-only peer">
        <div class="w-11 h-6 bg-gray-200 rounded-full
                    peer-checked:bg-indigo-600
                    peer-focus:ring-2 peer-focus:ring-indigo-500 peer-focus:ring-offset-2
                    after:content-[''] after:absolute after:top-0.5 after:left-[2px]
                    after:bg-white after:rounded-full after:h-5 after:w-5
                    after:shadow-sm after:transition-all
                    peer-checked:after:translate-x-full
                    transition-colors duration-200"></div>
      </label>
    </div>

    <!-- Toggle: Alerty o osiagnieciach -->
    <div class="flex items-center justify-between">
      <div>
        <p class="text-sm font-medium text-gray-700">Alerty o osiagnieciach</p>
        <p class="text-xs text-gray-500">Powiadomienia o nowych osiagnieciach dzieci</p>
      </div>
      <label class="relative inline-flex items-center cursor-pointer"
             x-data="{ enabled: {{ .Notifications.Achievements }} }">
        <input type="checkbox" name="achievement_alerts"
               x-model="enabled"
               class="sr-only peer">
        <div class="w-11 h-6 bg-gray-200 rounded-full
                    peer-checked:bg-indigo-600
                    peer-focus:ring-2 peer-focus:ring-indigo-500 peer-focus:ring-offset-2
                    after:content-[''] after:absolute after:top-0.5 after:left-[2px]
                    after:bg-white after:rounded-full after:h-5 after:w-5
                    after:shadow-sm after:transition-all
                    peer-checked:after:translate-x-full
                    transition-colors duration-200"></div>
      </label>
    </div>

    <div class="pt-2 flex justify-end">
      <button type="submit"
              class="inline-flex items-center gap-2 px-4 py-2.5
                     bg-indigo-600 text-white text-sm font-medium
                     rounded-lg shadow-sm hover:bg-indigo-700
                     transition-colors duration-150">
        Zapisz powiadomienia
      </button>
    </div>

  </form>
</div>

Sekcja: Konto

html
<div class="bg-white rounded-xl border border-gray-100 shadow-sm" id="settings-account">
  <div class="px-6 py-4 border-b border-gray-100">
    <h2 class="text-lg font-display font-semibold text-gray-800">
      Konto
    </h2>
  </div>

  <div class="p-6 space-y-6">

    <!-- Email -->
    <form hx-post="/settings/email"
          hx-target="#settings-account"
          hx-swap="outerHTML"
          class="space-y-4">
      <div>
        <label class="block text-sm font-medium text-gray-700 mb-1.5">
          Adres email
        </label>
        <input type="email" name="email" value="{{ .User.Email }}"
               class="w-full max-w-md px-3.5 py-2.5 text-sm text-gray-900
                      border border-gray-300 rounded-lg
                      focus:outline-none focus:ring-2 focus:ring-indigo-500
                      transition-colors duration-150">
      </div>
    </form>

    <!-- Powiazane dzieci -->
    <div>
      <h3 class="text-sm font-semibold text-gray-700 mb-3">Powiazane dzieci</h3>

      <div class="space-y-3">
        {{ range .LinkedChildren }}
        <div class="flex items-center justify-between p-3.5
                    bg-gray-50 rounded-lg">
          <div class="flex items-center gap-3">
            <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>
              <p class="text-sm font-medium text-gray-900">{{ .Name }}</p>
              <p class="text-xs text-gray-500">Kod: {{ .LinkCode }}</p>
            </div>
          </div>

          <button hx-post="/settings/unlink-child/{{ .ID }}"
                  hx-target="#settings-account"
                  hx-swap="outerHTML"
                  hx-confirm="Odlaczyc {{ .Name }}? Stracisz dostep do danych tego dziecka."
                  class="text-sm text-red-500 hover:text-red-700 font-medium
                         transition-colors duration-150">
            Odlacz
          </button>
        </div>
        {{ end }}
      </div>

      <!-- Dodaj dziecko -->
      <button @click="showLinkModal = true"
              class="mt-3 inline-flex items-center gap-2 text-sm text-indigo-600
                     hover:text-indigo-800 font-medium
                     transition-colors duration-150">
        <i data-lucide="plus" class="w-4 h-4"></i>
        Dodaj dziecko
      </button>
    </div>

    <div class="pt-2 flex justify-end">
      <button type="submit" form="email-form"
              class="inline-flex items-center gap-2 px-4 py-2.5
                     bg-indigo-600 text-white text-sm font-medium
                     rounded-lg shadow-sm hover:bg-indigo-700
                     transition-colors duration-150">
        Zapisz konto
      </button>
    </div>

  </div>
</div>

Sekcja: Strefa zagrozen (Danger Zone)

html
<div class="bg-white rounded-xl border border-red-200 shadow-sm" id="settings-danger">
  <div class="px-6 py-4 border-b border-red-100">
    <h2 class="text-lg font-display font-semibold text-red-700">
      Strefa zagrozen
    </h2>
    <p class="mt-1 text-sm text-red-500">
      Te akcje sa nieodwracalne. Prosimy o ostroznosc.
    </p>
  </div>

  <div class="p-6 space-y-4">

    <!-- Odlacz wszystkie dzieci -->
    <div class="flex items-center justify-between p-4 bg-red-50/50 rounded-lg">
      <div>
        <p class="text-sm font-medium text-gray-900">Odlacz wszystkie dzieci</p>
        <p class="text-xs text-gray-500">
          Utracisz dostep do danych wszystkich powiazanych dzieci.
        </p>
      </div>
      <button hx-post="/settings/unlink-all-children"
              hx-confirm="Czy na pewno chcesz odlaczyc wszystkie dzieci? Ta akcja jest nieodwracalna."
              class="inline-flex items-center gap-2 px-4 py-2.5
                     bg-white text-red-600 text-sm font-medium
                     rounded-lg border border-red-200 shadow-sm
                     hover:bg-red-50 transition-colors duration-150">
        Odlacz wszystkie
      </button>
    </div>

    <!-- Usun konto -->
    <div class="flex items-center justify-between p-4 bg-red-50/50 rounded-lg">
      <div>
        <p class="text-sm font-medium text-gray-900">Usun konto</p>
        <p class="text-xs text-gray-500">
          Trwale usun swoje konto i wszystkie powiazane dane.
        </p>
      </div>
      <button hx-delete="/settings/account"
              hx-confirm="Czy na pewno chcesz usunac swoje konto? Wszystkie dane zostana trwale usuniete."
              class="inline-flex items-center gap-2 px-4 py-2.5
                     bg-red-600 text-white text-sm font-medium
                     rounded-lg shadow-sm
                     hover:bg-red-700 transition-colors duration-150">
        <i data-lucide="trash-2" class="w-4 h-4"></i>
        Usun konto
      </button>
    </div>

  </div>
</div>

Potwierdzenie zapisu (toast/flash)

Po zapisaniu sekcji serwer zwraca zaktualizowana karte z komunikatem sukcesu:

html
<!-- Dodany na gorze karty po sukcesie -->
<div class="mx-6 mt-4 px-4 py-3 bg-green-50 border border-green-100 rounded-lg
            flex items-center gap-2 text-sm text-green-700"
     x-data="{ show: true }"
     x-init="setTimeout(() => show = false, 3000)"
     x-show="show"
     x-transition:leave="transition ease-in duration-200"
     x-transition:leave-start="opacity-100"
     x-transition:leave-end="opacity-0">
  <i data-lucide="check-circle" class="w-4 h-4"></i>
  Zmiany zostaly zapisane.
</div>

Endpointy

MetodaURLOpis
GET/settingsStrona ustawien (pelna SSR)
POST/settings/time-limitsZapisz limity czasu
POST/settings/age-groupsZapisz grupy wiekowe
POST/settings/notificationsZapisz powiadomienia
POST/settings/emailZmien email
POST/settings/unlink-child/:idOdlacz dziecko
POST/settings/unlink-all-childrenOdlacz wszystkie dzieci
POST/settings/link-childPolacz z dzieckiem (kod)
DELETE/settings/accountUsun konto

Go Handler

go
func SettingsPageHandler(w http.ResponseWriter, r *http.Request) {
    user := middleware.GetUser(r.Context())

    children := childService.GetLinkedChildren(r.Context(), user.ID)
    ageGroups := ageGroupService.GetAllWithStatus(r.Context(), user.ID)
    notifications := notificationService.GetPreferences(r.Context(), user.ID)

    data := SettingsData{
        User:           user,
        LinkedChildren: children,
        AgeGroups:      ageGroups,
        Notifications:  notifications,
    }

    renderTemplate(w, "settings.html", data)
}

func SaveTimeLimitsHandler(w http.ResponseWriter, r *http.Request) {
    user := middleware.GetUser(r.Context())
    r.ParseForm()

    children := childService.GetLinkedChildren(r.Context(), user.ID)
    for _, child := range children {
        limitStr := r.FormValue("limit_" + child.ID)
        limit, _ := strconv.ParseFloat(limitStr, 64)
        childService.SetDailyLimit(r.Context(), child.ID, limit)
    }

    // Zwroc zaktualizowana sekcje (HTMX partial swap)
    renderPartial(w, "settings/time-limits.html", updatedData)
}

Lumos Islands v2 - Dokumentacja Projektowa