Skip to content

Ekran — Todo Lista

Opis

Todo Lista pozwala rodzicowi/nauczycielowi przypisywac zadania dzieciom i sledzic ich realizacje. Zawiera taby statusow, tabele z paginacja HTMX, oraz modal dodawania nowego todo (Alpine.js slide-in).


ASCII Wireframe

+--------+-----------------------------------------------------------+
|        |  Top Bar                                                   |
| LOGO   |  Todo                                          [Ania v]   |
|        +-----------------------------------------------------------+
| SIDE   |                                                           |
| BAR    |  [ Wszystkie ] [ Oczekujace ] [ Ukonczone ] [ Przetermino]|
|        |  ===================================================      |
| Dash.  |                                        [ + Dodaj todo ]   |
| Content|                                                           |
| Worlds |  +-----------------------------------------------------+ |
| *Todo* |  | Zadanie          | Przyp. do | Termin   | Status  |A| |
| ----   |  |------------------|-----------|----------|---------|--| |
| Settng |  | Przeczytaj roz.3 | Ania      | Jutro    | Oczek.  |v| |
|        |  | Test z przyrody  | Ania      | Za 3 dni | Oczek.  |v| |
|        |  | Projekt plast.   | Tomek     | Za 5 dni | Oczek.  |v| |
|        |  | Slowka angielski | Ania      | Wczoraj  | Wykon.  | | |
|        |  | Cwiczenia mat.   | Tomek     | 2 dni t. | Przeter.|!| |
|        |  |------------------|-----------|----------|---------|--| |
|        |  | Wyniki 1-20 z 34          | [<] [1] [2] [>]         | |
|        |  +-----------------------------------------------------+ |
|        |                                                           |
+--------+-----------------------------------------------------------+

                                    +---+ Slide-in modal (Alpine.js):
                                    |   |
                                    | + | +---------------------------+
                                    |   | | Nowe todo            [x] |
                                    |   | |                          |
                                    |   | | Tytul:                   |
                                    |   | | [ _____________________ ]|
                                    |   | |                          |
                                    |   | | Opis:                    |
                                    |   | | [ _____________________ ]|
                                    |   | | [ _____________________ ]|
                                    |   | |                          |
                                    |   | | Termin:                  |
                                    |   | | [ 2026-03-15          ] |
                                    |   | |                          |
                                    |   | | Przypisz do:             |
                                    |   | | [ Ania          v    ]  |
                                    |   | |                          |
                                    |   | | [Anuluj]  [Dodaj todo]  |
                                    |   | +---------------------------+
                                    +---+

Status tabs (HTMX)

html
<div class="flex items-center gap-1 mb-6 border-b border-gray-200"
     x-data="{ activeTab: '{{ .ActiveTab }}' }">

  <button hx-get="/partials/todo/list?status=all"
          hx-target="#todo-table-body"
          hx-swap="innerHTML transition:true"
          @click="activeTab = 'all'"
          :class="activeTab === 'all'
            ? 'border-indigo-600 text-indigo-600'
            : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
          class="px-4 py-3 text-sm font-medium border-b-2
                 transition-colors duration-150 -mb-px">
    Wszystkie
    <span class="ml-1.5 px-2 py-0.5 rounded-full text-xs bg-gray-100 text-gray-600">
      {{ .Counts.All }}
    </span>
  </button>

  <button hx-get="/partials/todo/list?status=pending"
          hx-target="#todo-table-body"
          hx-swap="innerHTML transition:true"
          @click="activeTab = 'pending'"
          :class="activeTab === 'pending'
            ? 'border-indigo-600 text-indigo-600'
            : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
          class="px-4 py-3 text-sm font-medium border-b-2
                 transition-colors duration-150 -mb-px">
    Oczekujace
    <span class="ml-1.5 px-2 py-0.5 rounded-full text-xs bg-amber-50 text-amber-700">
      {{ .Counts.Pending }}
    </span>
  </button>

  <button hx-get="/partials/todo/list?status=completed"
          hx-target="#todo-table-body"
          hx-swap="innerHTML transition:true"
          @click="activeTab = 'completed'"
          :class="activeTab === 'completed'
            ? 'border-indigo-600 text-indigo-600'
            : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
          class="px-4 py-3 text-sm font-medium border-b-2
                 transition-colors duration-150 -mb-px">
    Ukonczone
    <span class="ml-1.5 px-2 py-0.5 rounded-full text-xs bg-green-50 text-green-700">
      {{ .Counts.Completed }}
    </span>
  </button>

  <button hx-get="/partials/todo/list?status=overdue"
          hx-target="#todo-table-body"
          hx-swap="innerHTML transition:true"
          @click="activeTab = 'overdue'"
          :class="activeTab === 'overdue'
            ? 'border-indigo-600 text-indigo-600'
            : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
          class="px-4 py-3 text-sm font-medium border-b-2
                 transition-colors duration-150 -mb-px">
    Przeterminowane
    <span class="ml-1.5 px-2 py-0.5 rounded-full text-xs bg-red-50 text-red-700">
      {{ .Counts.Overdue }}
    </span>
  </button>

</div>

Tabela Todo

html
<div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden">
  <table class="w-full">
    <thead>
      <tr class="border-b border-gray-100">
        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
          Zadanie
        </th>
        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
          Przypisane do
        </th>
        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
          Termin
        </th>
        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
          Status
        </th>
        <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
          Akcje
        </th>
      </tr>
    </thead>

    <tbody id="todo-table-body"
           hx-get="/partials/todo/list?status=all&page=1&per_page=20"
           hx-trigger="load"
           hx-swap="innerHTML transition:true"
           class="divide-y divide-gray-50">
    </tbody>
  </table>

  <div id="todo-pagination"></div>
</div>

Partial: wiersze tabeli

html
{{/* partials/todo/list.html */}}

{{ range .Todos }}
<tr id="todo-row-{{ .ID }}"
    class="hover:bg-gray-50 transition-colors duration-150">

  <!-- Zadanie -->
  <td class="px-6 py-4">
    <div class="text-sm font-medium text-gray-900">{{ .Title }}</div>
    {{ if .Description }}
    <div class="text-xs text-gray-500 mt-0.5">{{ .Description | truncate 50 }}</div>
    {{ end }}
  </td>

  <!-- Przypisane do -->
  <td class="px-6 py-4">
    <div class="flex items-center gap-2">
      <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">{{ .Child.Initials }}</span>
      </div>
      <span class="text-sm text-gray-700">{{ .Child.Name }}</span>
    </div>
  </td>

  <!-- Termin -->
  <td class="px-6 py-4">
    <span class="text-sm {{ if .IsOverdue }}text-red-600 font-medium{{ else }}text-gray-700{{ end }}">
      {{ .DueDateLabel }}
    </span>
  </td>

  <!-- Status -->
  <td class="px-6 py-4">
    <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
                 {{ if eq .Status "pending" }}bg-amber-50 text-amber-700{{ end }}
                 {{ if eq .Status "completed" }}bg-green-50 text-green-700{{ end }}
                 {{ if eq .Status "overdue" }}bg-red-50 text-red-700{{ end }}">
      {{ .StatusLabel }}
    </span>
  </td>

  <!-- Akcje -->
  <td class="px-6 py-4 text-right">
    {{ if eq .Status "pending" }}
    <button hx-post="/todo/{{ .ID }}/approve"
            hx-target="#todo-row-{{ .ID }}"
            hx-swap="outerHTML transition:true"
            class="inline-flex items-center gap-1.5 px-3 py-1.5
                   text-sm font-medium text-green-700
                   bg-green-50 rounded-lg
                   hover:bg-green-100
                   transition-colors duration-150">
      <i data-lucide="check" class="w-3.5 h-3.5"></i>
      Zatwierdz
    </button>
    {{ end }}

    {{ if eq .Status "overdue" }}
    <span class="inline-flex items-center gap-1 text-xs text-red-500">
      <i data-lucide="alert-circle" class="w-3.5 h-3.5"></i>
      Przeterminowane
    </span>
    {{ end }}
  </td>

</tr>
{{ end }}

<!-- Pagination OOB -->
<div id="todo-pagination"
     hx-swap-oob="innerHTML"
     class="px-6 py-4 border-t border-gray-100 flex items-center justify-between">
  <div class="text-sm text-gray-500">
    Wyniki {{ .From }}-{{ .To }} z {{ .Total }}
  </div>
  <nav class="flex items-center gap-1">
    {{ range .Pages }}
    <button hx-get="/partials/todo/list?status={{ $.Status }}&page={{ .Number }}&per_page={{ $.PerPage }}"
            hx-target="#todo-table-body"
            hx-swap="innerHTML transition:true"
            class="px-3 py-2 text-sm rounded-lg transition-colors duration-150
                   {{ if .IsCurrent }}bg-indigo-50 text-indigo-700 font-medium
                   {{ else }}text-gray-600 hover:bg-gray-100{{ end }}">
      {{ .Number }}
    </button>
    {{ end }}
  </nav>
</div>

Approve inline (HTMX)

Po kliknieciu "Zatwierdz" serwer zwraca zaktualizowany wiersz:

go
func TodoApproveHandler(w http.ResponseWriter, r *http.Request) {
    todoID := r.PathValue("id")

    todo, err := todoService.Approve(r.Context(), todoID)
    if err != nil {
        http.Error(w, "Error", http.StatusInternalServerError)
        return
    }

    // Zwroc zaktualizowany wiersz HTML
    tmpl := template.Must(template.ParseFiles("templates/partials/todo/row.html"))
    w.Header().Set("Content-Type", "text/html")
    tmpl.Execute(w, todo)
}

Zwrocony HTML zastepuje wiersz (hx-swap="outerHTML"):

html
<!-- Wiersz po zatwierdzeniu -->
<tr id="todo-row-{{ .ID }}"
    class="hover:bg-gray-50 transition-colors duration-150 bg-green-50/30">
  <td class="px-6 py-4">
    <div class="text-sm font-medium text-gray-900 line-through">{{ .Title }}</div>
  </td>
  <td class="px-6 py-4">
    <div class="flex items-center gap-2">
      <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">{{ .Child.Initials }}</span>
      </div>
      <span class="text-sm text-gray-700">{{ .Child.Name }}</span>
    </div>
  </td>
  <td class="px-6 py-4">
    <span class="text-sm text-gray-500">{{ .DueDateLabel }}</span>
  </td>
  <td class="px-6 py-4">
    <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
                 bg-green-50 text-green-700">
      Ukonczone
    </span>
  </td>
  <td class="px-6 py-4 text-right">
    <span class="text-xs text-gray-400">Zatwierdzone</span>
  </td>
</tr>

html
<div x-data="{ showModal: false }">

  <!-- Trigger button -->
  <button @click="showModal = true"
          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">
    <i data-lucide="plus" class="w-4 h-4"></i>
    Dodaj todo
  </button>

  <!-- Backdrop -->
  <div x-show="showModal"
       x-transition:enter="transition ease-out duration-200"
       x-transition:enter-start="opacity-0"
       x-transition:enter-end="opacity-100"
       x-transition:leave="transition ease-in duration-150"
       x-transition:leave-start="opacity-100"
       x-transition:leave-end="opacity-0"
       @click="showModal = false"
       class="fixed inset-0 bg-black/30 backdrop-blur-sm z-40"
       x-cloak>
  </div>

  <!-- Slide-in panel -->
  <div x-show="showModal"
       x-transition:enter="transition ease-out duration-300"
       x-transition:enter-start="translate-x-full"
       x-transition:enter-end="translate-x-0"
       x-transition:leave="transition ease-in duration-200"
       x-transition:leave-start="translate-x-0"
       x-transition:leave-end="translate-x-full"
       class="fixed right-0 top-0 bottom-0 w-full max-w-md
              bg-white shadow-2xl z-50 flex flex-col"
       x-cloak>

    <!-- Header -->
    <div class="flex items-center justify-between px-6 py-4 border-b border-gray-100">
      <h2 class="text-lg font-display font-semibold text-gray-800">
        Nowe todo
      </h2>
      <button @click="showModal = false"
              class="text-gray-400 hover:text-gray-600 transition-colors duration-150">
        <i data-lucide="x" class="w-5 h-5"></i>
      </button>
    </div>

    <!-- Form -->
    <form hx-post="/todo"
          hx-target="#todo-table-body"
          hx-swap="innerHTML transition:true"
          @htmx:after-request="showModal = false"
          class="flex-1 flex flex-col">

      <div class="flex-1 overflow-y-auto p-6 space-y-5">

        <!-- Tytul -->
        <div>
          <label class="block text-sm font-medium text-gray-700 mb-1.5">
            Tytul
          </label>
          <input type="text" name="title" required
                 placeholder="Co ma zrobic dziecko?"
                 class="w-full px-3.5 py-2.5 text-sm text-gray-900
                        border border-gray-300 rounded-lg
                        placeholder:text-gray-400
                        focus:outline-none focus:ring-2 focus:ring-indigo-500
                        transition-colors duration-150">
        </div>

        <!-- Opis -->
        <div>
          <label class="block text-sm font-medium text-gray-700 mb-1.5">
            Opis (opcjonalny)
          </label>
          <textarea name="description" rows="3"
                    placeholder="Dodatkowe informacje..."
                    class="w-full px-3.5 py-2.5 text-sm text-gray-900
                           border border-gray-300 rounded-lg resize-none
                           placeholder:text-gray-400
                           focus:outline-none focus:ring-2 focus:ring-indigo-500
                           transition-colors duration-150"></textarea>
        </div>

        <!-- Termin -->
        <div>
          <label class="block text-sm font-medium text-gray-700 mb-1.5">
            Termin
          </label>
          <input type="date" name="due_date" required
                 class="w-full 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>

        <!-- Przypisz do dziecka -->
        <div>
          <label class="block text-sm font-medium text-gray-700 mb-1.5">
            Przypisz do
          </label>
          <select name="child_id" required
                  class="w-full 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">
            <option value="">Wybierz dziecko...</option>
            {{ range .Children }}
            <option value="{{ .ID }}">{{ .Name }}</option>
            {{ end }}
          </select>
        </div>

      </div>

      <!-- Footer -->
      <div class="px-6 py-4 border-t border-gray-100 flex items-center justify-end gap-3">
        <button type="button" @click="showModal = false"
                class="px-4 py-2.5 text-sm font-medium text-gray-700
                       rounded-lg hover:bg-gray-100
                       transition-colors duration-150">
          Anuluj
        </button>
        <button type="submit"
                class="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">
          Dodaj todo
        </button>
      </div>

    </form>

  </div>

</div>

Endpointy

MetodaURLOpis
GET/todoStrona todo (pelna strona SSR)
GET/partials/todo/listPartial: wiersze tabeli + paginacja
POST/todoUtworzenie nowego todo
POST/todo/:id/approveZatwierdzenie todo (inline HTMX)
DELETE/todo/:idUsuniecie todo

Query params dla /partials/todo/list

ParamTypDomyslnaOpcje
statusstringallall, pending, completed, overdue
pageint1
per_pageint20

Go Handler

go
func TodoListPartialHandler(w http.ResponseWriter, r *http.Request) {
    user := middleware.GetUser(r.Context())
    status := r.URL.Query().Get("status")
    page, _ := strconv.Atoi(r.URL.Query().Get("page"))
    perPage, _ := strconv.Atoi(r.URL.Query().Get("per_page"))

    if page < 1 { page = 1 }
    if perPage < 1 { perPage = 20 }

    offset := (page - 1) * perPage
    todos, total := todoService.FindFiltered(r.Context(), user.ID, status, offset, perPage)

    data := TodoListData{
        Todos:   todos,
        Status:  status,
        Page:    page,
        PerPage: perPage,
        Total:   total,
        // ...pagination fields...
    }

    tmpl := template.Must(template.ParseFiles("templates/partials/todo/list.html"))
    w.Header().Set("Content-Type", "text/html")
    tmpl.Execute(w, data)
}

Lumos Islands v2 - Dokumentacja Projektowa