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
| Metoda | URL | Opis |
|---|---|---|
| GET | /settings | Strona ustawien (pelna SSR) |
| POST | /settings/time-limits | Zapisz limity czasu |
| POST | /settings/age-groups | Zapisz grupy wiekowe |
| POST | /settings/notifications | Zapisz powiadomienia |
| POST | /settings/email | Zmien email |
| POST | /settings/unlink-child/:id | Odlacz dziecko |
| POST | /settings/unlink-all-children | Odlacz wszystkie dzieci |
| POST | /settings/link-child | Polacz z dzieckiem (kod) |
| DELETE | /settings/account | Usun 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)
}