Add drag-and-drop card reorder with localStorage persistence

This commit is contained in:
Luca Sacchi Ricciardi
2026-04-24 14:13:47 +02:00
parent 50940cb1f2
commit 28fac8ee60
+115 -9
View File
@@ -78,9 +78,14 @@
<p class="inline-flex items-center gap-2 rounded-full border border-cyan-200/20 bg-cyan-300/10 px-3 py-1 text-xs font-semibold tracking-wide text-cyan-100">
Public Service Directory
</p>
<h1 class="mt-4 font-display text-4xl font-bold tracking-tight text-white sm:text-5xl">
home.lucasacchi.net
</h1>
<div class="mt-4 flex items-start justify-between gap-4">
<h1 class="font-display text-4xl font-bold tracking-tight text-white sm:text-5xl">
home.lucasacchi.net
</h1>
<button id="edit-btn" class="mt-1 flex-shrink-0 rounded-xl border border-white/20 bg-slate-800/70 px-4 py-2 text-sm font-semibold text-slate-200 transition hover:border-cyan-300/50 hover:bg-slate-700/70 hover:text-white">
Modifica
</button>
</div>
<p class="mt-4 max-w-2xl text-sm text-slate-200/90 sm:text-base">
Elenco dei servizi pubblici individuati nel dominio <span class="font-semibold text-cyan-200">*.home.lucasacchi.net</span>.
Apertura rapida, ricerca istantanea e visual pulito per desktop e mobile.
@@ -255,27 +260,128 @@
</div>
<script>
const STORAGE_KEY = "service-order";
const searchInput = document.getElementById("search");
const cards = Array.from(document.querySelectorAll("#service-list > li"));
const list = document.getElementById("service-list");
const emptyState = document.getElementById("empty-state");
const editBtn = document.getElementById("edit-btn");
// ── Restore saved order ──────────────────────────────────────────────
function restoreOrder() {
const saved = localStorage.getItem(STORAGE_KEY);
if (!saved) return;
try {
const order = JSON.parse(saved);
order.forEach((url) => {
const item = list.querySelector(`[data-url="${CSS.escape(url)}"]`);
if (item) list.appendChild(item);
});
} catch (_) {}
}
restoreOrder();
// ── Search ───────────────────────────────────────────────────────────
function getCards() {
return Array.from(list.querySelectorAll(":scope > li"));
}
function filterServices(term) {
const query = term.trim().toLowerCase();
let visible = 0;
for (const card of cards) {
for (const card of getCards()) {
const name = card.dataset.name.toLowerCase();
const url = card.dataset.url.toLowerCase();
const show = name.includes(query) || url.includes(query);
card.classList.toggle("hidden", !show);
if (show) visible += 1;
}
emptyState.classList.toggle("hidden", visible > 0);
}
searchInput.addEventListener("input", (event) => {
filterServices(event.target.value);
searchInput.addEventListener("input", (e) => filterServices(e.target.value));
// ── Drag-and-drop reorder ────────────────────────────────────────────
let editMode = false;
let dragging = null;
function enableEdit() {
editMode = true;
editBtn.textContent = "Salva";
editBtn.classList.add("border-cyan-300/60", "text-cyan-200");
searchInput.disabled = true;
searchInput.classList.add("opacity-40", "pointer-events-none");
for (const item of getCards()) {
item.setAttribute("draggable", "true");
item.classList.add("cursor-grab", "select-none", "ring-1", "ring-cyan-300/20");
item.addEventListener("dragstart", onDragStart);
item.addEventListener("dragenter", onDragEnter);
item.addEventListener("dragover", onDragOver);
item.addEventListener("drop", onDrop);
item.addEventListener("dragend", onDragEnd);
}
}
function disableEdit() {
editMode = false;
editBtn.textContent = "Modifica";
editBtn.classList.remove("border-cyan-300/60", "text-cyan-200");
searchInput.disabled = false;
searchInput.classList.remove("opacity-40", "pointer-events-none");
const order = getCards().map((c) => c.dataset.url);
localStorage.setItem(STORAGE_KEY, JSON.stringify(order));
for (const item of getCards()) {
item.removeAttribute("draggable");
item.classList.remove("cursor-grab", "select-none", "ring-1", "ring-cyan-300/20", "opacity-40");
item.removeEventListener("dragstart", onDragStart);
item.removeEventListener("dragenter", onDragEnter);
item.removeEventListener("dragover", onDragOver);
item.removeEventListener("drop", onDrop);
item.removeEventListener("dragend", onDragEnd);
}
}
function onDragStart(e) {
dragging = this;
this.classList.add("opacity-40");
e.dataTransfer.effectAllowed = "move";
}
function onDragEnter(e) {
e.preventDefault();
if (this !== dragging) this.classList.add("ring-cyan-300/70");
}
function onDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
}
function onDrop(e) {
e.preventDefault();
if (this !== dragging) {
const items = getCards();
const fromIdx = items.indexOf(dragging);
const toIdx = items.indexOf(this);
if (fromIdx < toIdx) {
list.insertBefore(dragging, this.nextSibling);
} else {
list.insertBefore(dragging, this);
}
}
this.classList.remove("ring-cyan-300/70");
}
function onDragEnd() {
this.classList.remove("opacity-40");
for (const item of getCards()) item.classList.remove("ring-cyan-300/70");
dragging = null;
}
editBtn.addEventListener("click", () => {
if (editMode) disableEdit(); else enableEdit();
});
</script>
</body>