Skip to content

Ekran — Custom Worlds

Opis

Ekran Custom Worlds pozwala tworzyc i zarzadzac wlasnymi swiatami (planetami) edukacyjnymi. Kazdy swiat zawiera lokacje, do ktorych przypisywane sa zadania. Rodzic/nauczyciel moze zapraszac innych uzytkownikow do swoich swiatow.


ASCII Wireframe — Grid planet

+--------+-----------------------------------------------------------+
|        |  Top Bar                                                   |
| LOGO   |  Swiaty                                       [Ania v]    |
|        +-----------------------------------------------------------+
| SIDE   |                                                           |
| BAR    |                                        [ + Nowa planeta ] |
|        |                                                           |
| Dash.  |  +------------------+ +------------------+ +------------+ |
| Content|  |  [  img  ]       | |  [  img  ]       | | [  img  ]  | |
| *World*|  |                  | |                  | |            | |
| Todo   |  |  Matematyka      | |  Geografia       | | Przyroda  | |
| ----   |  |                  | |                  | |            | |
| Settng |  |  5 lokacji       | |  3 lokacje       | | 7 lokacji | |
|        |  |  12 czlonkow     | |  8 czlonkow      | | 4 czlonk. | |
|        |  |                  | |                  | |            | |
|        |  |  [Aktywny]       | |  [Aktywny]       | | [Szkic]   | |
|        |  +------------------+ +------------------+ +------------+ |
|        |                                                           |
|        |  +------------------+                                     |
|        |  |  [  + ]          |                                     |
|        |  |                  |                                     |
|        |  |  Stworz nowy     |                                     |
|        |  |  swiat           |                                     |
|        |  +------------------+                                     |
|        |                                                           |
+--------+-----------------------------------------------------------+

ASCII Wireframe — Szczegoly planety

+--------+-----------------------------------------------------------+
|        |  Top Bar                                                   |
| LOGO   |  Swiaty > Matematyka                          [Ania v]    |
|        +-----------------------------------------------------------+
| SIDE   |                                                           |
| BAR    |  +----------------------------------+                     |
|        |  | Informacje o planecie            |                     |
| Dash.  |  |                                  |                     |
| Content|  | Nazwa:  [ Matematyka         ]   |                     |
| *World*|  |                                  |                     |
| Todo   |  | Opis:   [ Swiat liczb i      ]   |                     |
| ----   |  |         [ geometrii.         ]   |                     |
| Settng |  |                                  |                     |
|        |  | Obraz:  [Zmien obraz]  (prev)    |                     |
|        |  |                                  |                     |
|        |  | Dostep: (o) Otwarty  (o) Zamkniety|                    |
|        |  |                                  |                     |
|        |  |              [ Zapisz zmiany ]   |                     |
|        |  +----------------------------------+                     |
|        |                                                           |
|        |  +----------------------------------+                     |
|        |  | Lokacje                 [+Dodaj] |                     |
|        |  |                                  |                     |
|        |  |  ::  1. Dodawanie       [E] [x]  |                     |
|        |  |  ::  2. Odejmowanie      [E] [x]  |                     |
|        |  |  ::  3. Mnozenie        [E] [x]  |                     |
|        |  |  ::  4. Dzielenie       [E] [x]  |                     |
|        |  |  ::  5. Ulamki          [E] [x]  |                     |
|        |  |                                  |                     |
|        |  |  :: = drag handle                |                     |
|        |  +----------------------------------+                     |
|        |                                                           |
|        |  +----------------------------------+                     |
|        |  | Czlonkowie               [Zapros]|                     |
|        |  |                                  |                     |
|        |  | [ Szukaj czlonkow...        ]    |                     |
|        |  |                                  |                     |
|        |  | Jan Kowalski   rodzic   [Usun]   |                     |
|        |  | Anna Nowak     rodzic   [Usun]   |                     |
|        |  | Szkola #4      szkola   [Usun]   |                     |
|        |  +----------------------------------+                     |
|        |                                                           |
+--------+-----------------------------------------------------------+

Grid planet — implementacja

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

  <!-- Header -->
  <div class="flex items-center justify-between mb-8">
    <div>
      <h1 class="text-2xl font-display font-bold text-gray-900">Swiaty</h1>
      <p class="mt-1 text-sm text-gray-500">Zarzadzaj swoimi planetami edukacyjnymi</p>
    </div>
    <a href="/worlds/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>
      Nowa planeta
    </a>
  </div>

  <!-- Grid -->
  <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">

    {{ range .Worlds }}
    <a href="/worlds/{{ .ID }}"
       class="group bg-white rounded-xl border border-gray-100 shadow-sm
              overflow-hidden hover:shadow-md transition-shadow duration-200">

      <!-- Obraz planety -->
      <div class="aspect-[16/9] bg-gray-100 overflow-hidden">
        {{ if .ImageURL }}
        <img src="{{ .ImageURL }}" alt="{{ .Name }}"
             class="w-full h-full object-cover
                    group-hover:scale-105 transition-transform duration-300">
        {{ else }}
        <div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-indigo-100 to-indigo-200">
          <i data-lucide="globe" class="w-12 h-12 text-indigo-400"></i>
        </div>
        {{ end }}
      </div>

      <!-- Tresc -->
      <div class="p-5">
        <div class="flex items-start justify-between">
          <h3 class="text-lg font-display font-semibold text-gray-800
                     group-hover:text-indigo-600 transition-colors duration-150">
            {{ .Name }}
          </h3>
          <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium
                       {{ if eq .Status "active" }}bg-green-50 text-green-700{{ end }}
                       {{ if eq .Status "draft" }}bg-gray-100 text-gray-600{{ end }}">
            {{ .StatusLabel }}
          </span>
        </div>

        <div class="mt-3 flex items-center gap-4 text-sm text-gray-500">
          <span class="inline-flex items-center gap-1.5">
            <i data-lucide="map-pin" class="w-3.5 h-3.5"></i>
            {{ .LocationsCount }} lokacji
          </span>
          <span class="inline-flex items-center gap-1.5">
            <i data-lucide="users" class="w-3.5 h-3.5"></i>
            {{ .MembersCount }} czlonkow
          </span>
        </div>
      </div>

    </a>
    {{ end }}

    <!-- Karta "Stworz nowy" -->
    <a href="/worlds/new"
       class="group flex flex-col items-center justify-center
              bg-gray-50 rounded-xl border-2 border-dashed border-gray-200
              min-h-[250px]
              hover:border-indigo-300 hover:bg-indigo-50
              transition-all duration-200">
      <div class="w-12 h-12 bg-white rounded-xl shadow-sm
                  flex items-center justify-center mb-3
                  group-hover:shadow-md transition-shadow duration-200">
        <i data-lucide="plus" class="w-6 h-6 text-gray-400
                                     group-hover:text-indigo-600
                                     transition-colors duration-150"></i>
      </div>
      <span class="text-sm font-medium text-gray-500
                   group-hover:text-indigo-600 transition-colors duration-150">
        Stworz nowy swiat
      </span>
    </a>

  </div>

</main>

Szczegoly planety — informacje

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

  <!-- Breadcrumb -->
  <nav class="flex items-center gap-2 text-sm text-gray-500">
    <a href="/worlds" class="hover:text-gray-700">Swiaty</a>
    <i data-lucide="chevron-right" class="w-4 h-4"></i>
    <span class="text-gray-900 font-medium">{{ .World.Name }}</span>
  </nav>

  <!-- Informacje o planecie -->
  <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">
      Informacje o planecie
    </h2>

    <form hx-post="/worlds/{{ .World.ID }}"
          hx-target="this"
          hx-swap="outerHTML"
          class="space-y-5">

      <!-- Nazwa -->
      <div>
        <label class="block text-sm font-medium text-gray-700 mb-1.5">Nazwa</label>
        <input type="text" name="name" value="{{ .World.Name }}"
               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"
               required>
      </div>

      <!-- Opis -->
      <div>
        <label class="block text-sm font-medium text-gray-700 mb-1.5">Opis</label>
        <textarea name="description" rows="3"
                  class="w-full max-w-md 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
                         transition-colors duration-150">{{ .World.Description }}</textarea>
      </div>

      <!-- Obraz -->
      <div>
        <label class="block text-sm font-medium text-gray-700 mb-1.5">Obraz planety</label>
        <div class="flex items-center gap-4">
          {{ if .World.ImageURL }}
          <img src="{{ .World.ImageURL }}" alt="{{ .World.Name }}"
               class="w-20 h-20 rounded-xl object-cover border border-gray-100">
          {{ end }}
          <label class="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 cursor-pointer
                        transition-colors duration-150">
            <i data-lucide="upload" class="w-4 h-4"></i>
            Zmien obraz
            <input type="file" name="image" accept="image/*" class="hidden">
          </label>
        </div>
      </div>

      <!-- Dostep -->
      <div>
        <label class="block text-sm font-medium text-gray-700 mb-2">Dostep</label>
        <div class="flex items-center gap-6">
          <label class="inline-flex items-center gap-2 cursor-pointer">
            <input type="radio" name="access" value="open"
                   {{ if eq .World.Access "open" }}checked{{ end }}
                   class="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-500">
            <span class="text-sm text-gray-700">Otwarty</span>
          </label>
          <label class="inline-flex items-center gap-2 cursor-pointer">
            <input type="radio" name="access" value="closed"
                   {{ if eq .World.Access "closed" }}checked{{ end }}
                   class="w-4 h-4 text-indigo-600 border-gray-300 focus:ring-indigo-500">
            <span class="text-sm text-gray-700">Zamkniety</span>
          </label>
        </div>
        <p class="mt-1 text-xs text-gray-500">
          Otwarty swiat jest widoczny dla wszystkich. Zamkniety wymaga zaproszenia.
        </p>
      </div>

      <!-- Submit -->
      <div class="pt-2">
        <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 zmiany
        </button>
      </div>

    </form>
  </div>

Location Manager (z drag-to-reorder)

html
  <!-- Lokacje -->
  <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">
      <h2 class="text-lg font-display font-semibold text-gray-800">
        Lokacje
      </h2>
      <button @click="showLocationModal = true"
              class="inline-flex items-center gap-2 px-3.5 py-2
                     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
      </button>
    </div>

    <!-- Lista lokacji z drag -->
    <div id="locations-list"
         x-data="locationSortable()"
         x-init="initSortable()"
         class="divide-y divide-gray-50">

      {{ range .Locations }}
      <div class="flex items-center gap-4 px-6 py-4
                  hover:bg-gray-50 transition-colors duration-150"
           data-location-id="{{ .ID }}">

        <!-- Drag handle -->
        <div class="cursor-grab active:cursor-grabbing text-gray-400
                    hover:text-gray-600 transition-colors duration-150">
          <i data-lucide="grip-vertical" class="w-4 h-4"></i>
        </div>

        <!-- Numer porzadkowy -->
        <span class="w-6 h-6 bg-gray-100 rounded-full flex items-center justify-center
                     text-xs font-medium text-gray-500">
          {{ .Order }}
        </span>

        <!-- Nazwa -->
        <span class="flex-1 text-sm font-medium text-gray-900">
          {{ .Name }}
        </span>

        <!-- Licznik zadan -->
        <span class="text-xs text-gray-500">
          {{ .TasksCount }} zadan
        </span>

        <!-- Akcje -->
        <div class="flex items-center gap-2">
          <button hx-get="/partials/worlds/{{ $.World.ID }}/locations/{{ .ID }}/edit"
                  hx-target="closest div"
                  hx-swap="outerHTML"
                  class="text-gray-400 hover:text-indigo-600
                         transition-colors duration-150">
            <i data-lucide="pencil" class="w-4 h-4"></i>
          </button>
          <button hx-delete="/worlds/{{ $.World.ID }}/locations/{{ .ID }}"
                  hx-target="closest div"
                  hx-swap="outerHTML swap:200ms"
                  hx-confirm="Usunac lokacje {{ .Name }}?"
                  class="text-gray-400 hover:text-red-600
                         transition-colors duration-150">
            <i data-lucide="trash-2" class="w-4 h-4"></i>
          </button>
        </div>

      </div>
      {{ end }}

    </div>

    {{ if not .Locations }}
    <div class="flex flex-col items-center justify-center py-12">
      <i data-lucide="map-pin" class="w-8 h-8 text-gray-300 mb-3"></i>
      <p class="text-sm text-gray-500">Brak lokacji. Dodaj pierwsza lokacje.</p>
    </div>
    {{ end }}
  </div>

Alpine.js — Sortable

html
<script>
function locationSortable() {
  return {
    initSortable() {
      // Uzyj Sortable.js do drag-and-drop
      new Sortable(this.$el, {
        handle: '[data-lucide="grip-vertical"]',
        animation: 200,
        ghostClass: 'bg-indigo-50',
        onEnd: (evt) => {
          const ids = Array.from(this.$el.children).map(
            el => el.dataset.locationId
          );
          // Wyslij nowa kolejnosc do serwera
          fetch(`/worlds/{{ .World.ID }}/locations/reorder`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ order: ids }),
          });
        }
      });
    }
  }
}
</script>

Lista czlonkow

html
  <!-- Czlonkowie -->
  <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">
      <h2 class="text-lg font-display font-semibold text-gray-800">
        Czlonkowie
      </h2>
      <button @click="showInviteModal = true"
              class="inline-flex items-center gap-2 px-3.5 py-2
                     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="user-plus" class="w-4 h-4"></i>
        Zapros
      </button>
    </div>

    <!-- Search -->
    <div class="px-6 py-3 border-b border-gray-50">
      <div class="relative">
        <i data-lucide="search"
           class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400"></i>
        <input type="search"
               placeholder="Szukaj czlonkow..."
               hx-get="/partials/worlds/{{ .World.ID }}/members"
               hx-trigger="keyup changed delay:300ms"
               hx-target="#members-list"
               hx-swap="innerHTML"
               class="w-full pl-10 pr-4 py-2 text-sm text-gray-900
                      border border-gray-200 rounded-lg
                      placeholder:text-gray-400
                      focus:outline-none focus:ring-2 focus:ring-indigo-500
                      transition-colors duration-150">
      </div>
    </div>

    <!-- Lista -->
    <div id="members-list" class="divide-y divide-gray-50">
      {{ range .Members }}
      <div class="flex items-center justify-between px-6 py-3.5
                  hover:bg-gray-50 transition-colors duration-150">
        <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">{{ .RoleLabel }}</p>
          </div>
        </div>

        {{ if ne .Role "owner" }}
        <button hx-delete="/worlds/{{ $.World.ID }}/members/{{ .ID }}"
                hx-target="closest div"
                hx-swap="outerHTML swap:200ms"
                hx-confirm="Usunac {{ .Name }} z tego swiata?"
                class="text-sm text-red-500 hover:text-red-700 font-medium
                       transition-colors duration-150">
          Usun
        </button>
        {{ end }}
      </div>
      {{ end }}
    </div>
  </div>

</main>

Endpointy

MetodaURLOpis
GET/worldsGrid planet (pelna strona)
GET/worlds/newFormularz nowej planety
GET/worlds/:idSzczegoly planety
POST/worldsUtworzenie nowej planety
POST/worlds/:idAktualizacja planety
DELETE/worlds/:idUsuniecie planety
POST/worlds/:id/locationsDodanie lokacji
PUT/worlds/:id/locations/:lidEdycja lokacji
DELETE/worlds/:id/locations/:lidUsuniecie lokacji
POST/worlds/:id/locations/reorderZmiana kolejnosci lokacji
GET/partials/worlds/:id/membersPartial: lista czlonkow
GET/partials/worlds/:id/locations/:lid/editPartial: formularz edycji lokacji
POST/worlds/:id/members/inviteZaproszenie czlonka
DELETE/worlds/:id/members/:midUsuniecie czlonka

Lumos Islands v2 - Dokumentacja Projektowa