Skip to content

Ekran — Content Manager

Opis

Content Manager pozwala rodzicowi/nauczycielowi tworzyc i zarzadzac zadaniami edukacyjnymi. Sklada sie z dwoch widokow:

  1. Lista zadan — tabela z wyszukiwaniem, filtrami i paginacja HTMX
  2. Szczegoly zadania — formularz edycji z quiz builderem (Alpine.js)

ASCII Wireframe — Lista zadan

+--------+-----------------------------------------------------------+
|        |  Top Bar                                                   |
| LOGO   |  Content                                       [Ania v]   |
|        +-----------------------------------------------------------+
| SIDE   |                                                           |
| BAR    |  [ Szukaj zadania...        ]  [Typ v] [Wiek v] [Swiat v] |
|        |                                                           |
| Dash.  |  +-----------------------------------------------------+ |
| *Cont* |  | Tytul          | Typ    | Wiek  | Swiat  | Status  |A| |
| Worlds |  |----------------|--------|-------|--------|---------|--| |
| Todo   |  | Tabliczka mn.  | Quiz   | 7-9   | Matema | Aktywne |E| |
| ----   |  | Stolice Europy | Quiz   | 10-12 | Geogra | Szkic   |E| |
| Settng |  | Czytanie ze... | Task   | 7-9   | Polski | Aktywne |E| |
|        |  | Figury geomet. | Quiz   | 10-12 | Matema | Review  |E| |
|        |  | Slowka ang.    | Flash  | 7-9   | Jezyki | Aktywne |E| |
|        |  |----------------|--------|-------|--------|---------|--| |
|        |  | Wyniki 1-20 z 47          | [<] [1] [2] [3] [>]      | |
|        |  +-----------------------------------------------------+ |
|        |                                                           |
|        |                               [ + Nowe zadanie ]          |
|        |                                                           |
+--------+-----------------------------------------------------------+

ASCII Wireframe — Szczegoly zadania

+--------+-----------------------------------------------------------+
|        |  Top Bar                                                   |
| LOGO   |  Content > Tabliczka mnozenia                 [Ania v]   |
|        +-----------------------------------------------------------+
| SIDE   |                                                           |
| BAR    |  +----------------------------------+ +------------------+|
|        |  | Informacje podstawowe            | | Podglad          ||
| Dash.  |  |                                  | |                  ||
| *Cont* |  | Tytul:                           | | +-------------+  ||
| Worlds |  | [ Tabliczka mnozenia         ]   | | |  QUIZ       |  ||
| Todo   |  |                                  | | |             |  ||
| ----   |  | Opis:                            | | | Pytanie 1:  |  ||
| Settng |  | [ Cwicz tabliczke mnozenia   ]   | | | 2 x 3 = ?  |  ||
|        |  | [ do 100. Kazde pytanie ma   ]   | | |             |  ||
|        |  | [ 4 odpowiedzi.              ]   | | | o 4  o 5    |  ||
|        |  |                                  | | | o 6  o 7    |  ||
|        |  | Typ:  [Quiz v]                   | | +-------------+  ||
|        |  |                                  | |                  ||
|        |  | Grupy wiekowe:                   | +------------------+|
|        |  | [x] 6-7  [x] 8-9  [ ] 10-12     |                     |
|        |  |                                  |                     |
|        |  | Swiat:    [Matematyka v]          |                     |
|        |  | Lokacja:  [Mnozenie v   ]        |                     |
|        |  +----------------------------------+                     |
|        |                                                           |
|        |  +----------------------------------+                     |
|        |  | Quiz Builder                     |                     |
|        |  |                                  |                     |
|        |  | Pytanie 1:                       |                     |
|        |  | [ Ile to 2 x 3?             ]    |                     |
|        |  |   [x] 6    [ ] 4    [ ] 8   [-]  |                     |
|        |  |   [ ] 9                     [-]  |                     |
|        |  |   [+ Dodaj odpowiedz]            |                     |
|        |  |                                  |                     |
|        |  | Pytanie 2:                       |                     |
|        |  | [ Ile to 4 x 5?             ]    |                     |
|        |  |   [x] 20   [ ] 15   [ ] 25  [-]  |                     |
|        |  |   [+ Dodaj odpowiedz]            |                     |
|        |  |                                  |                     |
|        |  | [+ Dodaj pytanie]                |                     |
|        |  +----------------------------------+                     |
|        |                                                           |
|        |  +----------------------------------+                     |
|        |  | Media                            |                     |
|        |  | [Przeciagnij pliki lub kliknij]   |                     |
|        |  | img1.png  img2.png               |                     |
|        |  +----------------------------------+                     |
|        |                                                           |
|        |  [ Anuluj ]              [ Zapisz szkic ] [ Publikuj ]    |
|        |                                                           |
+--------+-----------------------------------------------------------+

Lista zadan — implementacja

Toolbar z wyszukiwaniem i filtrami

html
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-4 mb-6">

  <!-- Search -->
  <div class="relative flex-1 w-full">
    <i data-lucide="search"
       class="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"></i>
    <input type="search"
           name="search"
           placeholder="Szukaj zadania..."
           hx-get="/partials/content/task-list"
           hx-trigger="keyup changed delay:300ms"
           hx-target="#table-body"
           hx-swap="innerHTML transition:true"
           hx-include="[name='type_filter'],[name='age_filter'],[name='world_filter']"
           class="w-full pl-10 pr-4 py-2.5 text-sm text-gray-900
                  bg-white border border-gray-300 rounded-lg
                  placeholder:text-gray-400
                  focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500
                  transition-colors duration-150">
  </div>

  <!-- Filtry -->
  <div class="flex items-center gap-3">
    <select name="type_filter"
            hx-get="/partials/content/task-list"
            hx-trigger="change"
            hx-target="#table-body"
            hx-swap="innerHTML transition:true"
            hx-include="[name='search'],[name='age_filter'],[name='world_filter']"
            class="px-3.5 py-2.5 text-sm text-gray-700 bg-white border border-gray-300
                   rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500
                   transition-colors duration-150">
      <option value="">Wszystkie typy</option>
      <option value="quiz">Quiz</option>
      <option value="task">Zadanie</option>
      <option value="flashcard">Fiszki</option>
      <option value="story">Opowiadanie</option>
    </select>

    <select name="age_filter"
            hx-get="/partials/content/task-list"
            hx-trigger="change"
            hx-target="#table-body"
            hx-swap="innerHTML transition:true"
            hx-include="[name='search'],[name='type_filter'],[name='world_filter']"
            class="px-3.5 py-2.5 text-sm text-gray-700 bg-white border border-gray-300
                   rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500
                   transition-colors duration-150">
      <option value="">Wszystkie grupy</option>
      <option value="6-7">6-7 lat</option>
      <option value="8-9">8-9 lat</option>
      <option value="10-12">10-12 lat</option>
    </select>

    <select name="world_filter"
            hx-get="/partials/content/task-list"
            hx-trigger="change"
            hx-target="#table-body"
            hx-swap="innerHTML transition:true"
            hx-include="[name='search'],[name='type_filter'],[name='age_filter']"
            class="px-3.5 py-2.5 text-sm text-gray-700 bg-white border border-gray-300
                   rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500
                   transition-colors duration-150">
      <option value="">Wszystkie swiaty</option>
      {{ range .Worlds }}
      <option value="{{ .ID }}">{{ .Name }}</option>
      {{ end }}
    </select>
  </div>

  <!-- New task button -->
  <a href="/content/new"
     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>
    Nowe zadanie
  </a>

</div>

Tabela

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">
          Tytul
        </th>
        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
          Typ
        </th>
        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
          Grupa wiekowa
        </th>
        <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
          Swiat
        </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="table-body"
           hx-get="/partials/content/task-list?page=1&per_page=20"
           hx-trigger="load"
           hx-swap="innerHTML transition:true"
           class="divide-y divide-gray-50">
      <!-- Ladowane przez HTMX -->
    </tbody>
  </table>

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

Szczegoly zadania — formularz

Informacje podstawowe

html
<form hx-post="/content/{{ .Task.ID }}"
      hx-target="body"
      class="space-y-8">

  <!-- Informacje podstawowe -->
  <div class="bg-white rounded-xl border border-gray-100 shadow-sm p-6 space-y-5">
    <h2 class="text-lg font-display font-semibold text-gray-800">
      Informacje podstawowe
    </h2>

    <!-- Tytul -->
    <div>
      <label class="block text-sm font-medium text-gray-700 mb-1.5">Tytul</label>
      <input type="text" name="title" value="{{ .Task.Title }}"
             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 focus:border-indigo-500
                    transition-colors duration-150"
             required>
    </div>

    <!-- Opis -->
    <div>
      <label class="block text-sm font-medium text-gray-700 mb-1.5">Opis</label>
      <textarea name="description" rows="4"
                class="w-full px-3.5 py-2.5 text-sm text-gray-900
                       border border-gray-300 rounded-lg resize-none
                       focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500
                       transition-colors duration-150">{{ .Task.Description }}</textarea>
    </div>

    <!-- Typ -->
    <div>
      <label class="block text-sm font-medium text-gray-700 mb-1.5">Typ zadania</label>
      <select name="type"
              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 focus:border-indigo-500
                     transition-colors duration-150">
        <option value="quiz" {{ if eq .Task.Type "quiz" }}selected{{ end }}>Quiz</option>
        <option value="task" {{ if eq .Task.Type "task" }}selected{{ end }}>Zadanie</option>
        <option value="flashcard" {{ if eq .Task.Type "flashcard" }}selected{{ end }}>Fiszki</option>
        <option value="story" {{ if eq .Task.Type "story" }}selected{{ end }}>Opowiadanie</option>
      </select>
    </div>

    <!-- Grupy wiekowe (checkboxes) -->
    <div>
      <label class="block text-sm font-medium text-gray-700 mb-2">Grupy wiekowe</label>
      <div class="flex flex-wrap gap-4">
        {{ range .AgeGroups }}
        <label class="inline-flex items-center gap-2 cursor-pointer">
          <input type="checkbox" name="age_groups" value="{{ .ID }}"
                 {{ if .IsSelected }}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 text-gray-700">{{ .Label }}</span>
        </label>
        {{ end }}
      </div>
    </div>

    <!-- Swiat i lokacja -->
    <div class="grid grid-cols-1 sm:grid-cols-2 gap-5">
      <div>
        <label class="block text-sm font-medium text-gray-700 mb-1.5">Swiat</label>
        <select name="world_id"
                hx-get="/partials/content/locations"
                hx-trigger="change"
                hx-target="#location-select"
                hx-swap="innerHTML"
                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 swiat...</option>
          {{ range .Worlds }}
          <option value="{{ .ID }}" {{ if eq .ID $.Task.WorldID }}selected{{ end }}>
            {{ .Name }}
          </option>
          {{ end }}
        </select>
      </div>
      <div>
        <label class="block text-sm font-medium text-gray-700 mb-1.5">Lokacja</label>
        <select name="location_id" id="location-select"
                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 lokacje...</option>
          {{ range .Locations }}
          <option value="{{ .ID }}" {{ if eq .ID $.Task.LocationID }}selected{{ end }}>
            {{ .Name }}
          </option>
          {{ end }}
        </select>
      </div>
    </div>
  </div>

Quiz Builder (Alpine.js)

html
  <!-- Quiz Builder -->
  <div class="bg-white rounded-xl border border-gray-100 shadow-sm p-6"
       x-data="quizBuilder({{ .Task.QuestionsJSON }})">

    <h2 class="text-lg font-display font-semibold text-gray-800 mb-5">
      Quiz Builder
    </h2>

    <div class="space-y-6">
      <template x-for="(question, qi) in questions" :key="qi">
        <div class="bg-gray-50 rounded-lg p-5 space-y-4">

          <!-- Header pytania -->
          <div class="flex items-center justify-between">
            <h4 class="text-sm font-semibold text-gray-700">
              Pytanie <span x-text="qi + 1"></span>
            </h4>
            <button type="button" @click="removeQuestion(qi)"
                    class="text-red-500 hover:text-red-700 text-sm font-medium
                           transition-colors duration-150"
                    x-show="questions.length > 1">
              <i data-lucide="trash-2" class="w-4 h-4"></i>
            </button>
          </div>

          <!-- Tresc pytania -->
          <input type="text"
                 x-model="question.text"
                 :name="'questions[' + qi + '].text'"
                 placeholder="Wpisz tresc pytania..."
                 class="w-full px-3.5 py-2.5 text-sm text-gray-900
                        border border-gray-300 rounded-lg bg-white
                        focus:outline-none focus:ring-2 focus:ring-indigo-500
                        transition-colors duration-150">

          <!-- Odpowiedzi -->
          <div class="space-y-2.5 pl-4">
            <template x-for="(answer, ai) in question.answers" :key="ai">
              <div class="flex items-center gap-3">
                <!-- Zaznacz poprawna -->
                <label class="flex items-center gap-2 cursor-pointer">
                  <input type="radio"
                         :name="'questions[' + qi + '].correct'"
                         :value="ai"
                         :checked="answer.isCorrect"
                         @change="setCorrect(qi, ai)"
                         class="w-4 h-4 text-indigo-600 border-gray-300
                                focus:ring-indigo-500">
                </label>

                <!-- Tresc odpowiedzi -->
                <input type="text"
                       x-model="answer.text"
                       :name="'questions[' + qi + '].answers[' + ai + '].text'"
                       placeholder="Odpowiedz..."
                       class="flex-1 px-3 py-2 text-sm text-gray-900
                              border border-gray-200 rounded-lg bg-white
                              focus:outline-none focus:ring-2 focus:ring-indigo-500
                              transition-colors duration-150">

                <!-- Usun odpowiedz -->
                <button type="button" @click="removeAnswer(qi, ai)"
                        class="text-gray-400 hover:text-red-500
                               transition-colors duration-150"
                        x-show="question.answers.length > 2">
                  <i data-lucide="x" class="w-4 h-4"></i>
                </button>
              </div>
            </template>

            <!-- Dodaj odpowiedz -->
            <button type="button" @click="addAnswer(qi)"
                    class="inline-flex items-center gap-1.5 text-sm text-indigo-600
                           hover:text-indigo-800 font-medium transition-colors duration-150">
              <i data-lucide="plus" class="w-3.5 h-3.5"></i>
              Dodaj odpowiedz
            </button>
          </div>

        </div>
      </template>
    </div>

    <!-- Dodaj pytanie -->
    <button type="button" @click="addQuestion()"
            class="mt-5 inline-flex items-center gap-2 px-4 py-2.5
                   bg-white text-gray-700 text-sm font-medium
                   rounded-lg border border-gray-300 shadow-sm
                   hover:bg-gray-50 transition-colors duration-150">
      <i data-lucide="plus" class="w-4 h-4"></i>
      Dodaj pytanie
    </button>

    <!-- Hidden field z JSON dla serwera -->
    <input type="hidden" name="questions_json" :value="JSON.stringify(questions)">

  </div>

Alpine.js — logika Quiz Buildera

html
<script>
function quizBuilder(initialQuestions) {
  return {
    questions: initialQuestions || [{
      text: '',
      answers: [
        { text: '', isCorrect: true },
        { text: '', isCorrect: false },
      ]
    }],

    addQuestion() {
      this.questions.push({
        text: '',
        answers: [
          { text: '', isCorrect: true },
          { text: '', isCorrect: false },
        ]
      });
    },

    removeQuestion(qi) {
      this.questions.splice(qi, 1);
    },

    addAnswer(qi) {
      this.questions[qi].answers.push({ text: '', isCorrect: false });
    },

    removeAnswer(qi, ai) {
      // Jesli usuwamy poprawna odpowiedz, zaznacz pierwsza jako poprawna
      if (this.questions[qi].answers[ai].isCorrect) {
        const remaining = this.questions[qi].answers.filter((_, i) => i !== ai);
        if (remaining.length > 0) remaining[0].isCorrect = true;
      }
      this.questions[qi].answers.splice(ai, 1);
    },

    setCorrect(qi, ai) {
      this.questions[qi].answers.forEach((a, i) => {
        a.isCorrect = (i === ai);
      });
    }
  }
}
</script>

Media upload

html
  <!-- Media -->
  <div class="bg-white rounded-xl border border-gray-100 shadow-sm p-6">
    <h2 class="text-lg font-display font-semibold text-gray-800 mb-5">
      Media
    </h2>

    <!-- Upload area -->
    <div class="border-2 border-dashed border-gray-200 rounded-lg p-8 text-center
                hover:border-indigo-300 transition-colors duration-150"
         x-data="fileUpload()"
         @dragover.prevent="dragging = true"
         @dragleave.prevent="dragging = false"
         @drop.prevent="handleDrop($event)"
         :class="dragging ? 'border-indigo-400 bg-indigo-50' : ''">
      <i data-lucide="upload-cloud" class="w-10 h-10 text-gray-400 mx-auto"></i>
      <p class="mt-2 text-sm text-gray-600">
        Przeciagnij pliki lub
        <label class="text-indigo-600 hover:text-indigo-800 font-medium cursor-pointer">
          kliknij aby wybrac
          <input type="file" multiple accept="image/*,audio/*" class="hidden"
                 @change="handleFiles($event)">
        </label>
      </p>
      <p class="mt-1 text-xs text-gray-500">PNG, JPG, MP3 do 10MB</p>
    </div>

    <!-- Uploaded files -->
    {{ if .Task.Media }}
    <div class="mt-4 grid grid-cols-2 sm:grid-cols-4 gap-3">
      {{ range .Task.Media }}
      <div class="relative group rounded-lg overflow-hidden border border-gray-100">
        <img src="{{ .ThumbnailURL }}" alt="{{ .Name }}"
             class="w-full h-24 object-cover">
        <button type="button"
                hx-delete="/api/media/{{ .ID }}"
                hx-target="closest div"
                hx-swap="outerHTML"
                class="absolute top-1 right-1 w-6 h-6 bg-black/50 rounded-full
                       flex items-center justify-center
                       opacity-0 group-hover:opacity-100
                       transition-opacity duration-150">
          <i data-lucide="x" class="w-3 h-3 text-white"></i>
        </button>
        <div class="px-2 py-1.5">
          <p class="text-xs text-gray-600 truncate">{{ .Name }}</p>
        </div>
      </div>
      {{ end }}
    </div>
    {{ end }}
  </div>

Preview panel

html
  <!-- Panel podgladu (prawa kolumna) -->
  <div class="bg-white rounded-xl border border-gray-100 shadow-sm p-6 sticky top-24">
    <h3 class="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-4">
      Podglad
    </h3>

    <!-- Symulacja wygladu w aplikacji -->
    <div class="bg-gray-900 rounded-2xl p-4 aspect-[9/16] max-h-[480px] overflow-hidden">
      <div class="bg-gray-800 rounded-xl p-4 h-full flex flex-col">
        <div class="text-center mb-4">
          <span class="text-xs text-gray-400 uppercase tracking-wider"
                x-text="taskType"></span>
          <h4 class="text-white text-sm font-semibold mt-1"
              x-text="title || 'Tytul zadania'"></h4>
        </div>
        <!-- Tresc podgladu w zaleznosci od typu -->
        <div class="flex-1 text-gray-300 text-xs">
          <template x-if="taskType === 'quiz'">
            <!-- Podglad quizu -->
          </template>
        </div>
      </div>
    </div>
  </div>

html
  <!-- Akcje -->
  <div class="flex items-center justify-between pt-4">
    <a href="/content"
       class="px-4 py-2.5 text-sm font-medium text-gray-700
              rounded-lg hover:bg-gray-100
              transition-colors duration-150">
      Anuluj
    </a>
    <div class="flex items-center gap-3">
      <button type="submit" name="action" value="draft"
              class="px-4 py-2.5 bg-white text-gray-700 text-sm font-medium
                     rounded-lg border border-gray-300 shadow-sm
                     hover:bg-gray-50 transition-colors duration-150">
        Zapisz szkic
      </button>
      <button type="submit" name="action" value="publish"
              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">
        Publikuj
      </button>
    </div>
  </div>

</form>

Layout strony szczegulow

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

  <!-- Breadcrumb -->
  <nav class="flex items-center gap-2 text-sm text-gray-500 mb-6">
    <a href="/content" class="hover:text-gray-700">Content</a>
    <i data-lucide="chevron-right" class="w-4 h-4"></i>
    <span class="text-gray-900 font-medium">{{ .Task.Title }}</span>
  </nav>

  <!-- Dwie kolumny: formularz + podglad -->
  <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
    <div class="lg:col-span-2">
      <!-- Formularz (informacje + quiz builder + media) -->
    </div>
    <div class="hidden lg:block">
      <!-- Preview panel -->
    </div>
  </div>

</main>

Endpointy

MetodaURLOpis
GET/contentLista zadan (pelna strona)
GET/content/newFormularz nowego zadania
GET/content/:idFormularz edycji zadania
POST/contentUtworzenie nowego zadania
POST/content/:idAktualizacja zadania
DELETE/content/:idUsuniecie zadania
GET/partials/content/task-listPartial: wiersze tabeli + paginacja
GET/partials/content/locationsPartial: opcje lokacji dla swiata
DELETE/api/media/:idUsuniecie pliku media

Go Handler — lista

go
func ContentListHandler(w http.ResponseWriter, r *http.Request) {
    user := middleware.GetUser(r.Context())
    worlds := worldService.GetUserWorlds(r.Context(), user.ID)

    data := ContentListData{
        Worlds: worlds,
    }
    renderTemplate(w, "content/list.html", data)
}

Go Handler — szczegoly

go
func ContentDetailHandler(w http.ResponseWriter, r *http.Request) {
    user := middleware.GetUser(r.Context())
    taskID := r.PathValue("id")

    task, err := taskService.GetByID(r.Context(), taskID)
    if err != nil {
        http.Error(w, "Not found", http.StatusNotFound)
        return
    }

    worlds := worldService.GetUserWorlds(r.Context(), user.ID)
    locations := locationService.GetByWorldID(r.Context(), task.WorldID)
    ageGroups := ageGroupService.GetAll(r.Context())

    // Oznacz wybrane grupy wiekowe
    for i := range ageGroups {
        ageGroups[i].IsSelected = task.HasAgeGroup(ageGroups[i].ID)
    }

    data := ContentDetailData{
        Task:      task,
        Worlds:    worlds,
        Locations: locations,
        AgeGroups: ageGroups,
    }
    renderTemplate(w, "content/detail.html", data)
}

Lumos Islands v2 - Dokumentacja Projektowa