05/09 Update: favorite item

This commit is contained in:
Nguyen Ngo
2026-05-09 15:10:57 -04:00
parent b4de3e78d7
commit 09193fd81c
3 changed files with 122 additions and 1 deletions
+32
View File
@@ -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);
}
+82 -1
View File
@@ -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) => `<span class="item-tag">${escHtml(t)}</span>`)
.join("");
@@ -528,6 +530,7 @@ const Vault = (() => {
${showCopyUser ? `<button class="btn-icon" title="Copy username" data-action="copy-user">👤</button>` : ""}
${showCopyPass ? `<button class="btn-icon" title="Copy secret" data-action="copy-pass">📋</button>` : ""}
${showTotp ? `<button class="btn-icon" title="Copy 2FA code" data-action="copy-totp">🔐</button>` : ""}
<button class="btn-icon btn-favorite${isFavorite ? " is-favorite" : ""}" title="${isFavorite ? "Remove from favorites" : "Add to favorites"}" data-action="favorite">${isFavorite ? "★" : "☆"}</button>
<button class="btn-icon" title="Edit" data-action="edit"></button>
<button class="btn-icon" title="Delete" data-action="delete">🗑</button>
</div>`;
@@ -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"));
+8
View File
@@ -45,6 +45,14 @@
<span class="sidebar-icon">🏠</span>
<span class="sidebar-label">All Items</span>
</li>
<li
class="sidebar-item"
id="sidebar-favorites"
data-tooltip="Favorites"
>
<span class="sidebar-icon"></span>
<span class="sidebar-label">Favorites</span>
</li>
<li class="sidebar-section-header">Item Types</li>
<li
class="sidebar-item"