adminx-core 3.0.0

Framework-neutral core for the adminx admin-panel framework: the Resource trait, storage abstraction, registry, and neutral request/response types shared by every web-framework and database adapter.
Documentation
{% extends "layout.html" %}
{% block content %}
  <div class="flex flex-col lg:flex-row gap-6">
    <!-- Main content -->
    <div class="flex-1 min-w-0 bg-white dark:bg-gray-800 shadow-sm rounded-xl border border-gray-200 dark:border-gray-700 p-6">
      <div class="flex flex-wrap items-center justify-between gap-3 mb-6">
        <h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ resource_name }}</h1>
        <div class="flex items-center gap-2">
          {% if has_filters %}
          <button type="button" onclick="adminxToggleFilters()" title="Filters"
            class="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium border transition
                   {% if has_active_filters %}border-indigo-400 text-indigo-700 bg-indigo-50 dark:border-indigo-500/50 dark:text-indigo-300 dark:bg-indigo-500/10{% else %}border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700{% endif %}">
            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.414A1 1 0 013 6.707V4z"/></svg>
            Filters
            {% if has_active_filters %}<span class="text-xs px-1.5 py-0.5 rounded-full bg-indigo-600 text-white">On</span>{% endif %}
          </button>
          {% endif %}
          <a href="{{ mount | safe }}/{{ base_path | safe }}/list?download=csv" class="inline-flex items-center gap-1 px-3 py-2 rounded-lg text-sm font-medium border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition">CSV</a>
          <a href="{{ mount | safe }}/{{ base_path | safe }}/list?download=json" class="inline-flex items-center gap-1 px-3 py-2 rounded-lg text-sm font-medium border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition">JSON</a>
          <a href="{{ mount | safe }}/{{ base_path | safe }}/new" class="inline-flex items-center gap-1 px-4 py-2 rounded-lg text-sm font-semibold text-white bg-gradient-to-r from-indigo-600 to-fuchsia-600 shadow hover:opacity-95 transition">
            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
            New
          </a>
        </div>
      </div>

      <div class="overflow-x-auto">
        <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
          <thead class="bg-gray-50 dark:bg-gray-700/50">
            <tr>
              {% for h in headers %}
                <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">{{ h | replace(from="_", to=" ") | title }}</th>
              {% endfor %}
              <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Actions</th>
            </tr>
          </thead>
          <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
            {% for row in rows %}
              <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/40 transition-colors">
                {% for h in headers %}
                  <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{{ row[h] | default(value="") }}</td>
                {% endfor %}
                <td class="px-6 py-4 whitespace-nowrap text-right text-sm">
                  <div class="flex items-center justify-end gap-1">
                    <a href="{{ mount | safe }}/{{ base_path | safe }}/view/{{ row[pk] }}" title="View" class="p-1.5 rounded-lg text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-500/10 transition">
                      <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
                    </a>
                    <a href="{{ mount | safe }}/{{ base_path | safe }}/edit/{{ row[pk] }}" title="Edit" class="p-1.5 rounded-lg text-gray-500 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-500/10 transition">
                      <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
                    </a>
                    <form method="post" action="{{ mount | safe }}/{{ base_path | safe }}/{{ row[pk] }}/delete" class="inline" onsubmit="return confirm('Delete this {{ resource_name }}?');">
                      <input type="hidden" name="_csrf" value="{{ csrf_token }}">
                      <button type="submit" title="Delete" class="p-1.5 rounded-lg text-gray-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 transition">
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
                      </button>
                    </form>
                  </div>
                </td>
              </tr>
            {% else %}
              <tr><td colspan="{{ headers | length + 1 }}" class="px-6 py-10 text-center text-sm text-gray-400 dark:text-gray-500">No records. <a href="{{ mount | safe }}/{{ base_path | safe }}/new" class="text-indigo-600 dark:text-indigo-400 hover:underline">Create the first one</a>.</td></tr>
            {% endfor %}
          </tbody>
        </table>
      </div>

      <div class="flex items-center justify-between mt-6 text-sm text-gray-600 dark:text-gray-400">
        <span>{{ total }} total ยท page {{ page }}</span>
        <span class="flex gap-2">
          {% if page > 1 %}<a href="{{ mount | safe }}/{{ base_path | safe }}/list?page={{ page - 1 }}&per_page={{ per_page }}" class="px-3 py-1.5 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition">Prev</a>{% endif %}
          {% if page * per_page < total %}<a href="{{ mount | safe }}/{{ base_path | safe }}/list?page={{ page + 1 }}&per_page={{ per_page }}" class="px-3 py-1.5 rounded-lg border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 transition">Next</a>{% endif %}
        </span>
      </div>
    </div>

    <!-- Right filter sidebar: hidden by default; opens on the Filters button,
         and starts open when filters are already applied. -->
    {% if has_filters %}
    <aside id="filter-sidebar" class="{% if not has_active_filters %}hidden {% endif %}w-full lg:w-72 shrink-0 bg-white dark:bg-gray-800 shadow-sm rounded-xl border border-gray-200 dark:border-gray-700 p-6 h-fit">
      <div class="flex items-center justify-between mb-4">
        <h3 class="text-base font-semibold text-gray-900 dark:text-white">Filters</h3>
        <button type="button" onclick="adminxToggleFilters()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300" title="Close">
          <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
        </button>
      </div>

      <form method="get" action="{{ mount | safe }}/{{ base_path | safe }}/list" class="space-y-4">
        {% for f in filter_fields %}
          <div>
            <label for="f_{{ f.field }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5">{{ f.label }}</label>
            {% set input_cls = "w-full px-3 py-2 rounded-lg text-sm border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 focus:ring-2 focus:ring-indigo-500" %}
            {% if f.kind == "select" or f.kind == "boolean" %}
              <select id="f_{{ f.field }}" name="{{ f.field }}" class="{{ input_cls }}">
                <option value="">All</option>
                {% for o in f.options %}
                  <option value="{{ o.value }}" {% if current_filters[f.field] and current_filters[f.field] == o.value %}selected{% endif %}>{{ o.label }}</option>
                {% endfor %}
              </select>
            {% elif f.kind == "date_range" %}
              {% set from_key = f.field ~ "_from" %}
              {% set to_key = f.field ~ "_to" %}
              <div class="grid grid-cols-2 gap-2">
                <input type="date" name="{{ from_key }}" value="{{ current_filters[from_key] | default(value='') }}" class="{{ input_cls }}">
                <input type="date" name="{{ to_key }}" value="{{ current_filters[to_key] | default(value='') }}" class="{{ input_cls }}">
              </div>
            {% else %}
              <input type="text" id="f_{{ f.field }}" name="{{ f.field }}"
                     value="{{ current_filters[f.field] | default(value='') }}" placeholder="{{ f.label }}"
                     class="{{ input_cls }}">
            {% endif %}
          </div>
        {% endfor %}
        <div class="flex items-center gap-2 pt-4 border-t border-gray-200 dark:border-gray-700">
          <button type="submit" class="flex-1 px-4 py-2 rounded-lg text-sm font-semibold text-white bg-gradient-to-r from-indigo-600 to-fuchsia-600 shadow hover:opacity-95 transition">Apply</button>
          {% if has_active_filters %}
            <a href="{{ mount | safe }}/{{ base_path | safe }}/list" class="px-4 py-2 rounded-lg text-sm font-medium border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 transition">Clear</a>
          {% endif %}
        </div>
      </form>
    </aside>
    {% endif %}
  </div>

  <script>
    function adminxToggleFilters() {
      var s = document.getElementById('filter-sidebar');
      if (s) s.classList.toggle('hidden');
    }
  </script>
{% endblock content %}