feat(pages): add admin page list with drag-and-drop reorder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Leon Mika 2026-03-22 19:06:48 +11:00
parent f386403ced
commit 5eece96700
4 changed files with 104 additions and 1 deletions

View file

@ -0,0 +1,63 @@
import { Controller } from "@hotwired/stimulus"
import { showToast } from "../services/toast";
export default class PagelistController extends Controller {
static values = {
siteId: Number,
};
static targets = ["list"];
dragStart(ev) {
this.draggedRow = ev.currentTarget;
ev.currentTarget.classList.add("opacity-50");
ev.dataTransfer.effectAllowed = "move";
}
dragOver(ev) {
ev.preventDefault();
ev.dataTransfer.dropEffect = "move";
}
drop(ev) {
ev.preventDefault();
const targetRow = ev.currentTarget;
if (this.draggedRow && this.draggedRow !== targetRow) {
const rows = [...this.listTarget.children];
const draggedIdx = rows.indexOf(this.draggedRow);
const targetIdx = rows.indexOf(targetRow);
if (draggedIdx < targetIdx) {
targetRow.after(this.draggedRow);
} else {
targetRow.before(this.draggedRow);
}
this.saveOrder();
}
}
dragEnd(ev) {
ev.currentTarget.classList.remove("opacity-50");
this.draggedRow = null;
}
async saveOrder() {
const rows = [...this.listTarget.children];
const pageIds = rows.map(row => parseInt(row.dataset.pageId, 10));
try {
await fetch(`/sites/${this.siteIdValue}/pages/reorder`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
body: JSON.stringify({ page_ids: pageIds }),
});
} catch (error) {
showToast({
title: "Error",
body: "Failed to reorder pages.",
});
}
}
}

View file

@ -7,6 +7,7 @@ import LogoutController from "./controllers/logout";
import FirstRunController from "./controllers/firstrun"; import FirstRunController from "./controllers/firstrun";
import UploadController from "./controllers/upload"; import UploadController from "./controllers/upload";
import ShowUploadController from "./controllers/show_upload"; import ShowUploadController from "./controllers/show_upload";
import PagelistController from "./controllers/pagelist";
window.Stimulus = Application.start() window.Stimulus = Application.start()
Stimulus.register("toast", ToastController); Stimulus.register("toast", ToastController);
@ -16,3 +17,4 @@ Stimulus.register("logout", LogoutController);
Stimulus.register("first-run", FirstRunController); Stimulus.register("first-run", FirstRunController);
Stimulus.register("upload", UploadController); Stimulus.register("upload", UploadController);
Stimulus.register("show-upload", ShowUploadController); Stimulus.register("show-upload", ShowUploadController);
Stimulus.register("pagelist", PagelistController);

View file

@ -13,6 +13,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" aria-current="page" href="/sites/{{.site.ID}}/categories">Categories</a> <a class="nav-link active" aria-current="page" href="/sites/{{.site.ID}}/categories">Categories</a>
</li> </li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/sites/{{.site.ID}}/pages">Pages</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link active" aria-current="page" href="/sites/{{.site.ID}}/uploads">Uploads</a> <a class="nav-link active" aria-current="page" href="/sites/{{.site.ID}}/uploads">Uploads</a>
</li> </li>

35
views/pages/index.html Normal file
View file

@ -0,0 +1,35 @@
<main class="container">
<div class="my-4 d-flex justify-content-between align-items-baseline">
<div>
<a href="/sites/{{ .site.ID }}/pages/new" class="btn btn-success">New Page</a>
</div>
</div>
{{ if .pages }}
<table class="table" data-controller="pagelist" data-pagelist-site-id-value="{{ .site.ID }}">
<thead>
<tr>
<th style="width: 2rem;"></th>
<th>Title</th>
<th>Slug</th>
<th>Nav</th>
</tr>
</thead>
<tbody data-pagelist-target="list">
{{ range .pages }}
<tr draggable="true" data-page-id="{{ .ID }}"
data-action="dragstart->pagelist#dragStart dragover->pagelist#dragOver drop->pagelist#drop dragend->pagelist#dragEnd">
<td class="text-muted" style="cursor: grab;">&#x2630;</td>
<td><a href="/sites/{{ $.site.ID }}/pages/{{ .ID }}">{{ .Title }}</a></td>
<td><code>{{ .Slug }}</code></td>
<td>{{ if .ShowInNav }}Yes{{ end }}</td>
</tr>
{{ end }}
</tbody>
</table>
{{ else }}
<div class="h4 m-3 text-center">
<div class="position-absolute top-50 start-50 translate-middle">No pages yet.</div>
</div>
{{ end }}
</main>