Add drag-and-drop card reorder with localStorage persistence
This commit is contained in:
+115
-9
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user