feat: add shared utilities and table macros

- Add shared table macros for consistent table styling
- Add shared JavaScript utilities (date formatting, etc.)
- Add admin settings API enhancements
- Add admin schema updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-21 14:12:43 +01:00
parent a118edced5
commit 9cf0a568c0
4 changed files with 346 additions and 0 deletions

View File

@@ -283,3 +283,168 @@
</div>
</div>
{% endmacro %}
{#
Numbered Pagination
===================
A numbered pagination component with first/prev/pages/next/last buttons.
Parameters:
- page_var: Alpine.js variable for current page (default: 'page')
- total_var: Alpine.js variable for total items (default: 'total')
- limit_var: Alpine.js variable for items per page (default: 'limit')
- on_change: Alpine.js function to call on page change (default: 'loadItems()')
- show_rows_per_page: Whether to show rows per page selector (default: false)
- rows_options: List of rows per page options (default: [10, 20, 50, 100])
Required Alpine.js:
page: 1,
total: 0,
limit: 20,
get totalPages() { return Math.ceil(this.total / this.limit); },
getPageNumbers() {
// Returns array of page numbers to display
const total = this.totalPages;
const current = this.page;
const maxVisible = 5;
if (total <= maxVisible) {
return Array.from({length: total}, (_, i) => i + 1);
}
const half = Math.floor(maxVisible / 2);
let start = Math.max(1, current - half);
let end = Math.min(total, start + maxVisible - 1);
if (end - start < maxVisible - 1) {
start = Math.max(1, end - maxVisible + 1);
}
return Array.from({length: end - start + 1}, (_, i) => start + i);
},
goToPage(p) {
this.page = p;
this.loadItems();
}
#}
{% macro numbered_pagination(page_var='page', total_var='total', limit_var='limit', on_change='loadItems()', show_rows_per_page=false, rows_options=[10, 20, 50, 100]) %}
<div x-show="{{ total_var }} > {{ limit_var }}" class="flex flex-col sm:flex-row items-center justify-between gap-4 mt-4 pt-4 border-t dark:border-gray-700">
{# Info text #}
<span class="text-sm text-gray-600 dark:text-gray-400">
Showing
<span x-text="(({{ page_var }} - 1) * {{ limit_var }}) + 1"></span>-<span x-text="Math.min({{ page_var }} * {{ limit_var }}, {{ total_var }})"></span>
of <span x-text="{{ total_var }}"></span> items
</span>
{# Pagination controls #}
<div class="flex items-center gap-1">
{# First page #}
<button
@click="{{ page_var }} = 1; {{ on_change }}"
:disabled="{{ page_var }} <= 1"
class="px-2 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
title="First page"
>
<span x-html="$icon('chevron-double-left', 'w-4 h-4')"></span>
</button>
{# Previous page #}
<button
@click="{{ page_var }}--; {{ on_change }}"
:disabled="{{ page_var }} <= 1"
class="px-2 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
title="Previous page"
>
<span x-html="$icon('chevron-left', 'w-4 h-4')"></span>
</button>
{# Page numbers #}
<template x-for="p in getPageNumbers()" :key="p">
<button
@click="goToPage(p)"
class="px-3 py-1 text-sm font-medium rounded-md border transition-colors"
:class="p === {{ page_var }}
? 'bg-purple-600 text-white border-purple-600 dark:bg-purple-500 dark:border-purple-500'
: 'text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600'"
x-text="p"
></button>
</template>
{# Next page #}
<button
@click="{{ page_var }}++; {{ on_change }}"
:disabled="{{ page_var }} >= Math.ceil({{ total_var }} / {{ limit_var }})"
class="px-2 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
title="Next page"
>
<span x-html="$icon('chevron-right', 'w-4 h-4')"></span>
</button>
{# Last page #}
<button
@click="{{ page_var }} = Math.ceil({{ total_var }} / {{ limit_var }}); {{ on_change }}"
:disabled="{{ page_var }} >= Math.ceil({{ total_var }} / {{ limit_var }})"
class="px-2 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed"
title="Last page"
>
<span x-html="$icon('chevron-double-right', 'w-4 h-4')"></span>
</button>
</div>
{% if show_rows_per_page %}
{# Rows per page selector #}
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600 dark:text-gray-400">Rows:</label>
<select
x-model.number="{{ limit_var }}"
@change="{{ page_var }} = 1; {{ on_change }}"
class="px-2 py-1 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md focus:border-purple-400 focus:outline-none"
>
{% for opt in rows_options %}
<option value="{{ opt }}">{{ opt }}</option>
{% endfor %}
</select>
</div>
{% endif %}
</div>
{% endmacro %}
{#
Simple Pagination
=================
A simpler prev/next pagination without page numbers.
Use this for quick implementation or when numbered pagination isn't needed.
Parameters:
- page_var: Alpine.js variable for current page (default: 'page')
- total_var: Alpine.js variable for total items (default: 'total')
- limit_var: Alpine.js variable for items per page (default: 'limit')
- on_change: Alpine.js function to call on page change (default: 'loadItems()')
#}
{% macro simple_pagination(page_var='page', total_var='total', limit_var='limit', on_change='loadItems()') %}
<div x-show="{{ total_var }} > {{ limit_var }}" class="flex items-center justify-between mt-4 pt-4 border-t dark:border-gray-700">
<span class="text-sm text-gray-600 dark:text-gray-400">
Showing
<span x-text="(({{ page_var }} - 1) * {{ limit_var }}) + 1"></span>-<span x-text="Math.min({{ page_var }} * {{ limit_var }}, {{ total_var }})"></span>
of <span x-text="{{ total_var }}"></span>
</span>
<div class="flex items-center gap-2">
<button
@click="{{ page_var }}--; {{ on_change }}"
:disabled="{{ page_var }} <= 1"
class="px-3 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50"
>
<span x-html="$icon('chevron-left', 'w-4 h-4')"></span>
</button>
<button
@click="{{ page_var }}++; {{ on_change }}"
:disabled="{{ page_var }} * {{ limit_var }} >= {{ total_var }}"
class="px-3 py-1 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 disabled:opacity-50"
>
<span x-html="$icon('chevron-right', 'w-4 h-4')"></span>
</button>
</div>
</div>
{% endmacro %}