axum-admin 0.1.0

A modern admin dashboard framework for Axum
Documentation
<div class="bg-white rounded-md border border-zinc-200 shadow-[0_1px_4px_rgba(0,0,0,0.04)] overflow-hidden"
     data-page-ids="[{% for row in rows %}&quot;{{ row.id }}&quot;{% if not loop.last %},{% endif %}{% endfor %}]">
  <div class="overflow-x-auto">
    <table class="w-full text-left border-collapse">
      <thead>
        <tr class="border-b border-zinc-200 bg-zinc-50/50">
          {% if can_delete or can_edit %}
          <th class="py-3 px-4 w-10">
            <input type="checkbox"
                   class="w-4 h-4 rounded border-zinc-300 text-black focus:ring-black"
                   :checked="allIds.length > 0 && allIds.every(id => selected.includes(id))"
                   :indeterminate="allIds.some(id => selected.includes(id)) && !allIds.every(id => selected.includes(id))"
                   @change="$event.target.checked
                     ? selected = [...new Set([...selected, ...allIds])]
                     : selected = selected.filter(id => !allIds.includes(id))" />
          </th>
          {% endif %}
          {% for col in columns %}
          <th class="py-3 px-4 text-[11px] font-bold uppercase tracking-wider text-zinc-500">
            <a class="hover:text-zinc-900 transition-colors flex items-center gap-1 cursor-pointer"
               hx-get="/admin/{{ entity_name }}/?order_by={{ col }}&order_dir={% if order_by == col and order_dir == 'asc' %}desc{% else %}asc{% endif %}"
               hx-target="#list-table"
               hx-include="[data-filter-input], [name='search']">
              {{ col }}
              {% if order_by == col %}
                {% if order_dir == "asc" %}<i class="fa-solid fa-arrow-up text-[10px]"></i>
                {% else %}<i class="fa-solid fa-arrow-down text-[10px]"></i>{% endif %}
              {% endif %}
            </a>
          </th>
          {% endfor %}
          {% if can_edit or can_delete %}<th class="py-3 px-4 text-[11px] font-bold uppercase tracking-wider text-zinc-500 text-right">Actions</th>{% endif %}
        </tr>
      </thead>
      <tbody class="divide-y divide-zinc-100">
        {% for row in rows %}
        <tr class="hover:bg-zinc-50/60 transition-colors group">
          {% if can_delete or can_edit %}
          <td class="py-3 px-4">
            <input type="checkbox" name="selected_ids" value="{{ row.id }}"
                   class="w-4 h-4 rounded border-zinc-300 text-black focus:ring-black"
                   x-model="selected" />
          </td>
          {% endif %}
          {% for col in columns %}
          <td class="py-3 px-4 text-sm text-zinc-700">
            {% if loop.index == 1 and can_edit %}
              <a href="/admin/{{ entity_name }}/{{ row.id }}/" class="font-medium text-zinc-900 hover:underline">
                {% if column_types[col] == "Image" %}
                  {% if row.data[col] %}<img src="{{ row.data[col] }}" class="h-10 w-auto rounded object-cover" alt="{{ row.data[col] | basename }}">{% endif %}
                {% elif column_types[col] == "File" %}
                  {{ row.data[col] | basename }}
                {% else %}
                  {{ row.data[col] }}
                {% endif %}
              </a>
            {% elif column_types[col] == "Image" %}
              {% if row.data[col] %}<img src="{{ row.data[col] }}" class="h-10 w-auto rounded object-cover" alt="{{ row.data[col] | basename }}">{% endif %}
            {% elif column_types[col] == "File" %}
              {{ row.data[col] | basename }}
            {% else %}
              {{ row.data[col] }}
            {% endif %}
          </td>
          {% endfor %}
          {% if can_edit or can_delete %}
          <td class="py-3 px-4 text-right">
            <div class="flex items-center justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
              {% if can_edit %}
              <a href="/admin/{{ entity_name }}/{{ row.id }}/"
                 class="p-1.5 text-zinc-400 hover:text-zinc-900 transition-colors rounded">
                <i class="fa-solid fa-pen text-xs"></i>
              </a>
              {% endif %}
              {% if can_delete %}
              <button
                hx-delete="/admin/{{ entity_name }}/{{ row.id }}/delete"
                hx-confirm="Delete this record?"
                hx-target="closest tr"
                hx-swap="outerHTML"
                class="p-1.5 text-zinc-400 hover:text-red-600 transition-colors rounded">
                <i class="fa-solid fa-trash text-xs"></i>
              </button>
              {% endif %}
            </div>
          </td>
          {% endif %}
        </tr>
        {% endfor %}
        {% if rows | length == 0 %}
        <tr>
          <td colspan="99" class="py-12 text-center text-sm text-zinc-400">
            <i class="fa-solid fa-inbox text-zinc-300 text-2xl mb-2 block"></i>
            No records found.
          </td>
        </tr>
        {% endif %}
      </tbody>
    </table>
  </div>

  <!-- Pagination -->
  <div class="px-6 py-3 flex items-center justify-between border-t border-zinc-100 bg-zinc-50/30">
    <p class="text-xs text-zinc-500 font-medium">Page {{ page }} of {{ total_pages }}</p>
    <div class="flex items-center gap-1">
      {% if page > 1 %}
      <button hx-get="/admin/{{ entity_name }}/?page={{ page - 1 }}"
              hx-target="#list-table"
              hx-include="[data-filter-input], [name='search']"
              class="flex items-center justify-center w-7 h-7 rounded text-xs font-semibold text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100 transition-colors">
        <i class="fa-solid fa-chevron-left text-[10px]"></i>
      </button>
      {% endif %}
      {% for p in range(1, total_pages + 1) %}
        {% if p == page %}
        <span class="flex items-center justify-center w-7 h-7 rounded bg-black text-white text-xs font-semibold">{{ p }}</span>
        {% elif p == 1 or p == total_pages or (p >= page - 2 and p <= page + 2) %}
        <button hx-get="/admin/{{ entity_name }}/?page={{ p }}"
                hx-target="#list-table"
                hx-include="[data-filter-input], [name='search']"
                class="flex items-center justify-center w-7 h-7 rounded text-xs font-semibold text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100 transition-colors">
          {{ p }}
        </button>
        {% elif p == page - 3 or p == page + 3 %}
        <span class="flex items-center justify-center w-7 h-7 text-xs text-zinc-400"></span>
        {% endif %}
      {% endfor %}
      {% if page < total_pages %}
      <button hx-get="/admin/{{ entity_name }}/?page={{ page + 1 }}"
              hx-target="#list-table"
              hx-include="[data-filter-input], [name='search']"
              class="flex items-center justify-center w-7 h-7 rounded text-xs font-semibold text-zinc-500 hover:text-zinc-900 hover:bg-zinc-100 transition-colors">
        <i class="fa-solid fa-chevron-right text-[10px]"></i>
      </button>
      {% endif %}
    </div>
  </div>
</div>