From 09193fd81c70f2a88247d25808a5c9171a49c1be Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Sat, 9 May 2026 15:10:57 -0400 Subject: [PATCH] 05/09 Update: favorite item --- app/static/css/app.css | 32 +++++++++++++ app/static/js/vault.js | 83 +++++++++++++++++++++++++++++++++- app/templates/vault/index.html | 8 ++++ 3 files changed, 122 insertions(+), 1 deletion(-) diff --git a/app/static/css/app.css b/app/static/css/app.css index a69b235..4212fa6 100644 --- a/app/static/css/app.css +++ b/app/static/css/app.css @@ -2318,3 +2318,35 @@ html.sidebar-open { color: var(--color-text-muted); line-height: 1.45; } + +/* ── Favorite star button ───────────────────────────────────────────────── */ + +/* Outline star — subtle, not distracting */ +.btn-favorite { + color: #d1d5db; + font-size: 16px; + transition: + color var(--transition), + transform 0.12s ease; +} + +.btn-favorite:hover { + color: #f59e0b; + background: #fef9c3; + transform: scale(1.15); +} + +/* Filled gold star — item is favorited */ +.btn-favorite.is-favorite { + color: #f59e0b; +} + +.btn-favorite.is-favorite:hover { + color: #d97706; + background: #fef3c7; +} + +/* Brief pop animation when toggling */ +.btn-favorite:active { + transform: scale(0.88); +} diff --git a/app/static/js/vault.js b/app/static/js/vault.js index 5cfb590..b3b4057 100644 --- a/app/static/js/vault.js +++ b/app/static/js/vault.js @@ -511,7 +511,9 @@ const Vault = (() => { item.item_type === "password" && !!extractTotpSecret(item.plain?.totp_uri); - const tagBadges = itemTags + const isFavorite = itemTags.includes("favorite"); + const visibleTags = itemTags.filter((t) => t !== "favorite"); + const tagBadges = visibleTags .map((t) => `${escHtml(t)}`) .join(""); @@ -528,6 +530,7 @@ const Vault = (() => { ${showCopyUser ? `` : ""} ${showCopyPass ? `` : ""} ${showTotp ? `` : ""} + `; @@ -603,6 +606,13 @@ const Vault = (() => { }, ); } + li.querySelector('[data-action="favorite"]').addEventListener( + "click", + (e) => { + e.stopPropagation(); + toggleFavorite(item); + }, + ); li.querySelector('[data-action="edit"]').addEventListener("click", (e) => { e.stopPropagation(); openModal("edit", item); @@ -3125,6 +3135,67 @@ const Vault = (() => { } } + /** + * Toggle the "favorite" tag on a vault item. + * Re-encrypts and PUTs the item in-place, then patches _items and + * re-renders so the star flips instantly without a full vault reload. + */ + async function toggleFavorite(item) { + const vaultKey = VaultSession.getKey(); + if (!vaultKey) { + showUnlockOverlay(); + return; + } + if (!item.plain) { + showToast("Item could not be decrypted", "error"); + return; + } + + const currentTags = item.plain.tags || []; + const isFavorite = currentTags.includes("favorite"); + const newTags = isFavorite + ? currentTags.filter((t) => t !== "favorite") + : [...currentTags, "favorite"].sort(); + + const updatedPlain = { ...item.plain, tags: newTags }; + + try { + const { enc_data, iv } = await Crypto.encryptItem(vaultKey, updatedPlain); + const { enc_name, iv_name } = await Crypto.encryptName(vaultKey, item.name); + + const payload = { + name: item.item_type, + item_type: item.item_type, + folder_id: item.folder_id || null, + enc_data, + iv, + enc_name, + iv_name, + }; + + const res = await apiFetch(`/api/vault/${item.id}`, { + method: "PUT", + body: JSON.stringify(payload), + }); + if (!res) return; + + // Patch the in-memory item so the re-render is instant. + const idx = _items.findIndex((i) => i.id === item.id); + if (idx !== -1) { + _items[idx] = { ..._items[idx], plain: updatedPlain }; + } + + showToast(isFavorite ? "Removed from favorites" : "Added to favorites ★"); + + // Refresh the sidebar tag list ("favorite" tag may appear/disappear). + renderTagList(); + // Re-render the current view preserving the active filter. + applyCurrentFilter(); + } catch (err) { + showToast("Could not update favorite: " + err.message, "error"); + } + } + // ── Logout ──────────────────────────────────────────────────────────────── async function handleLogout() { @@ -3450,6 +3521,16 @@ const Vault = (() => { updateVaultTitle("All Items"); renderItemList(_items); }); + document.getElementById("sidebar-favorites")?.addEventListener("click", () => { + _activeFilter = { type: "tag", value: "favorite" }; + switchView("vault"); + document + .querySelectorAll(".sidebar-item") + .forEach((el) => el.classList.remove("active")); + document.getElementById("sidebar-favorites").classList.add("active"); + updateVaultTitle("⭐ Favorites"); + renderItemList(_items.filter((i) => (i.plain?.tags || []).includes("favorite"))); + }); document .getElementById("sidebar-security") ?.addEventListener("click", () => switchView("security")); diff --git a/app/templates/vault/index.html b/app/templates/vault/index.html index a2fe1af..486b9e5 100644 --- a/app/templates/vault/index.html +++ b/app/templates/vault/index.html @@ -45,6 +45,14 @@ 🏠 All Items +