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">
|
<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
|
Public Service Directory
|
||||||
</p>
|
</p>
|
||||||
<h1 class="mt-4 font-display text-4xl font-bold tracking-tight text-white sm:text-5xl">
|
<div class="mt-4 flex items-start justify-between gap-4">
|
||||||
home.lucasacchi.net
|
<h1 class="font-display text-4xl font-bold tracking-tight text-white sm:text-5xl">
|
||||||
</h1>
|
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">
|
<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>.
|
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.
|
Apertura rapida, ricerca istantanea e visual pulito per desktop e mobile.
|
||||||
@@ -255,27 +260,128 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
const STORAGE_KEY = "service-order";
|
||||||
const searchInput = document.getElementById("search");
|
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 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) {
|
function filterServices(term) {
|
||||||
const query = term.trim().toLowerCase();
|
const query = term.trim().toLowerCase();
|
||||||
let visible = 0;
|
let visible = 0;
|
||||||
|
for (const card of getCards()) {
|
||||||
for (const card of cards) {
|
|
||||||
const name = card.dataset.name.toLowerCase();
|
const name = card.dataset.name.toLowerCase();
|
||||||
const url = card.dataset.url.toLowerCase();
|
const url = card.dataset.url.toLowerCase();
|
||||||
const show = name.includes(query) || url.includes(query);
|
const show = name.includes(query) || url.includes(query);
|
||||||
card.classList.toggle("hidden", !show);
|
card.classList.toggle("hidden", !show);
|
||||||
if (show) visible += 1;
|
if (show) visible += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
emptyState.classList.toggle("hidden", visible > 0);
|
emptyState.classList.toggle("hidden", visible > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
searchInput.addEventListener("input", (event) => {
|
searchInput.addEventListener("input", (e) => filterServices(e.target.value));
|
||||||
filterServices(event.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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user