axum-admin 0.1.1

A modern admin dashboard framework for Axum
Documentation
{% extends "layout.html" %}
{% block title %}{% if is_create %}New {{ entity_label }}{% else %}Edit {{ entity_label }}{% endif %} — {{ admin_title }}{% endblock %}
{% block content %}

<div class="mb-6">
  <a href="/admin/{{ entity_name }}/"
     class="text-sm text-zinc-500 hover:text-zinc-900 transition-colors flex items-center gap-1 w-fit">
    <i class="fa-solid fa-chevron-left text-xs"></i>
    Back to {{ entity_label }}
  </a>
</div>

<h1 class="text-2xl font-bold tracking-tight text-zinc-900 mb-6">
  {% if is_create %}New {{ entity_label }}{% else %}Edit #{{ record_id }}{% endif %}
</h1>

<div class="bg-white rounded-md border border-zinc-200 shadow-sm p-8 max-w-2xl">
  {% if errors["__all__"] %}
  <div class="mb-5 rounded-md bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700 flex items-center gap-2">
    <i class="fa-solid fa-circle-exclamation shrink-0"></i>
    {{ errors["__all__"] }}
  </div>
  {% endif %}
  <form method="post"
        action="{% if is_create %}/admin/{{ entity_name }}/new{% else %}/admin/{{ entity_name }}/{{ record_id }}/{% endif %}"
        enctype="multipart/form-data"
        class="space-y-5">
    <input type="hidden" name="csrf_token" value="{{ csrf_token }}" />

    {% for field in fields %}
    {% if not field.hidden and not field.list_only %}
    <div>
      <label class="block text-sm font-medium text-zinc-700 mb-1">
        {{ field.label }}{% if field.required %} <span class="text-red-500">*</span>{% endif %}
      </label>

      {% if field.field_type == "Text" %}
      <input type="text" name="{{ field.name }}"
             value="{{ values[field.name] | default('') }}"
             {% if field.readonly %}readonly{% endif %}
             {% if field.required %}required{% endif %}
             class="w-full border {% if errors[field.name] %}border-red-400{% else %}border-zinc-200{% endif %} rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-black focus:border-black{% if field.readonly %} bg-zinc-50 text-zinc-400{% endif %}" />

      {% elif field.field_type == "TextArea" %}
      <textarea name="{{ field.name }}" rows="4"
                {% if field.readonly %}readonly{% endif %}
                {% if field.required %}required{% endif %}
                class="w-full border {% if errors[field.name] %}border-red-400{% else %}border-zinc-200{% endif %} rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-black focus:border-black{% if field.readonly %} bg-zinc-50 text-zinc-400{% endif %}">{{ values[field.name] | default('') }}</textarea>

      {% elif field.field_type == "Email" %}
      <input type="email" name="{{ field.name }}"
             value="{{ values[field.name] | default('') }}"
             {% if field.readonly %}readonly{% endif %}
             {% if field.required %}required{% endif %}
             class="w-full border {% if errors[field.name] %}border-red-400{% else %}border-zinc-200{% endif %} rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-black focus:border-black{% if field.readonly %} bg-zinc-50 text-zinc-400{% endif %}" />

      {% elif field.field_type == "Password" %}
      <input type="password" name="{{ field.name }}"
             {% if field.readonly %}readonly{% endif %}
             class="w-full border {% if errors[field.name] %}border-red-400{% else %}border-zinc-200{% endif %} rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-black focus:border-black" />

      {% elif field.field_type == "Number" %}
      <input type="number" name="{{ field.name }}"
             value="{{ values[field.name] | default('') }}"
             {% if field.readonly %}readonly{% endif %}
             {% if field.required %}required{% endif %}
             class="w-full border {% if errors[field.name] %}border-red-400{% else %}border-zinc-200{% endif %} rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-black focus:border-black{% if field.readonly %} bg-zinc-50 text-zinc-400{% endif %}" />

      {% elif field.field_type == "Boolean" %}
      <input type="checkbox" name="{{ field.name }}"
             {% if values[field.name] %}checked{% endif %}
             {% if field.readonly %}disabled{% endif %}
             class="w-4 h-4 rounded border-zinc-300 text-black focus:ring-black" />

      {% elif field.field_type == "Select" %}
      <select name="{{ field.name }}"
              {% if field.readonly %}disabled{% endif %}
              class="w-full border {% if errors[field.name] %}border-red-400{% else %}border-zinc-200{% endif %} rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-black focus:border-black bg-white">
        {% for opt in field.options %}
        <option value="{{ opt.0 }}" {% if values[field.name] == opt.0 %}selected{% endif %}>{{ opt.1 }}</option>
        {% endfor %}
      </select>

      {% elif field.field_type == "Date" %}
      <input type="date" name="{{ field.name }}"
             value="{{ values[field.name] | default('') }}"
             {% if field.readonly %}readonly{% endif %}
             class="w-full border {% if errors[field.name] %}border-red-400{% else %}border-zinc-200{% endif %} rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-black focus:border-black" />

      {% elif field.field_type == "DateTime" %}
      <input type="datetime-local" name="{{ field.name }}"
             value="{{ values[field.name] | default('') }}"
             {% if field.readonly %}readonly{% endif %}
             class="w-full border {% if errors[field.name] %}border-red-400{% else %}border-zinc-200{% endif %} rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-black focus:border-black" />

      {% elif field.field_type == "ManyToMany" %}
      {# Alpine.js multi-select — stores selection as a JSON array in a hidden input #}
      <div x-data="multiSelect"
           data-options='{{ field.options | tojson }}'
           data-selected='{{ field.selected_ids | tojson }}'
           @click.outside="open = false"
           class="relative">
        <input type="hidden" :name="'{{ field.name }}'" :value="JSON.stringify(selected)" />
        <button type="button" @click="open = !open"
                class="w-full border {% if errors[field.name] %}border-red-400{% else %}border-zinc-200{% endif %} rounded-md px-3 py-2 text-sm text-left bg-white focus:outline-none focus:ring-1 focus:ring-black focus:border-black flex items-center justify-between">
          <span x-text="selected.length === 0 ? 'Select…' : selected.map(label).join(', ')" class="truncate text-zinc-700"></span>
          <i class="fa-solid fa-chevron-down text-zinc-400 text-xs ml-2 shrink-0" :class="open ? 'rotate-180' : ''" style="transition: transform .15s"></i>
        </button>
        <div x-show="open"
             class="absolute z-20 mt-1 w-full bg-white border border-zinc-200 rounded-md shadow-lg max-h-60 overflow-y-auto">
          <template x-for="opt in options" :key="opt[0]">
            <label class="flex items-center gap-3 px-3 py-2 hover:bg-zinc-50 cursor-pointer text-sm">
              <input type="checkbox" :value="opt[0]" :checked="selected.includes(opt[0])"
                     @change="toggle(opt[0])"
                     class="w-4 h-4 rounded border-zinc-300 text-black focus:ring-black" />
              <span x-text="opt[1]" class="text-zinc-700"></span>
            </label>
          </template>
          <p x-show="options.length === 0" class="px-3 py-2 text-sm text-zinc-400">No options available.</p>
        </div>
      </div>

      {% elif field.field_type == "Json" %}
      <textarea name="{{ field.name }}" rows="6"
                {% if field.readonly %}readonly{% endif %}
                class="w-full border {% if errors[field.name] %}border-red-400{% else %}border-zinc-200{% endif %} rounded-md px-3 py-2 text-sm font-mono focus:outline-none focus:ring-1 focus:ring-black focus:border-black{% if field.readonly %} bg-zinc-50 text-zinc-400{% endif %}">{{ values[field.name] | default('') }}</textarea>

      {% elif field.field_type == "File" %}
      {% if values[field.name] %}
      <div class="mb-1 flex items-center gap-3 text-sm text-zinc-600">
        <a href="{{ values[field.name] }}" target="_blank" class="underline hover:text-zinc-900">
          {{ values[field.name] | basename }}
        </a>
        <label class="flex items-center gap-1 text-zinc-500 cursor-pointer">
          <input type="checkbox" name="{{ field.name }}__clear" value="on"> Clear
        </label>
      </div>
      {% endif %}
      <input type="file" name="{{ field.name }}"{% if field.accept %} accept="{{ field.accept | join(',') }}"{% endif %}
             class="block w-full text-sm text-zinc-500 file:mr-3 file:py-1 file:px-3 file:rounded file:border-0 file:text-sm file:bg-zinc-100 file:text-zinc-700 hover:file:bg-zinc-200 border {% if errors[field.name] %}border-red-400{% else %}border-zinc-200{% endif %} rounded-md p-1">

      {% elif field.field_type == "Image" %}
      {% if values[field.name] %}
      <div class="mb-1 flex items-center gap-3">
        <img src="{{ values[field.name] }}" class="h-24 w-auto rounded object-cover border border-zinc-200" alt="{{ field.label }} preview">
        <label class="flex items-center gap-1 text-sm text-zinc-500 cursor-pointer">
          <input type="checkbox" name="{{ field.name }}__clear" value="on"> Clear
        </label>
      </div>
      {% endif %}
      <input type="file" name="{{ field.name }}" accept="image/*"
             class="block w-full text-sm text-zinc-500 file:mr-3 file:py-1 file:px-3 file:rounded file:border-0 file:text-sm file:bg-zinc-100 file:text-zinc-700 hover:file:bg-zinc-200 border {% if errors[field.name] %}border-red-400{% else %}border-zinc-200{% endif %} rounded-md p-1">
      {% endif %}

      {% if field.help_text %}
      <p class="text-xs text-zinc-500 mt-1">{{ field.help_text }}</p>
      {% endif %}
      {% if errors[field.name] %}
      <p class="text-xs text-red-600 mt-1 flex items-center gap-1">
        <i class="fa-solid fa-circle-exclamation text-[10px]"></i>
        {{ errors[field.name] }}
      </p>
      {% endif %}
    </div>
    {% endif %}
    {% endfor %}

    <div class="flex items-center gap-3 pt-4 border-t border-zinc-100">
      <button type="submit"
              class="px-6 py-2 bg-black text-white rounded-md text-sm font-semibold hover:bg-zinc-800 transition-colors">
        Save
      </button>
      <a href="/admin/{{ entity_name }}/"
         class="px-6 py-2 bg-zinc-100 text-zinc-700 rounded-md text-sm font-medium hover:bg-zinc-200 transition-colors">
        Cancel
      </a>
    </div>
  </form>
</div>
{% endblock %}