From 34f28dd07749f7a22cb20e9cff1e5b413a98c882 Mon Sep 17 00:00:00 2001 From: kavin553 Date: Tue, 16 Jun 2026 14:20:39 +0530 Subject: [PATCH 1/2] feat(theme) : add live theme preview system --- public/css/style.css | 41 ++++++++++++++++ public/index.html | 9 ++++ public/js/app.js | 110 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 150 insertions(+), 10 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index 9feb664..a4f6192 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -841,4 +841,45 @@ input, textarea, select { .toggle-password svg{ transition:.2s; } +.theme-preview-controls { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + z-index: 9999; + + display: flex; + gap: 12px; + + padding: 12px 16px; + + background: rgba(0,0,0,0.6); + backdrop-filter: blur(12px); + + border: 1px solid rgba(255,255,255,0.1); + border-radius: 999px; +} + +.theme-preview-controls button { + border: none; + padding: 10px 18px; + border-radius: 999px; + cursor: pointer; + + font-weight: 600; + transition: all 0.25s ease; +} +#applyThemeBtn { + background: var(--accent); + color: white; +} + +#cancelThemeBtn { + background: rgba(255,255,255,0.08); + color: var(--text-primary); +} + +.theme-preview-controls button:hover { + transform: translateY(-2px); +} diff --git a/public/index.html b/public/index.html index 9478a5f..fd8480d 100644 --- a/public/index.html +++ b/public/index.html @@ -101,7 +101,16 @@

Loading...

Conn. + +
+ + +
diff --git a/public/js/app.js b/public/js/app.js index 4e56d47..47a3f2c 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -12,9 +12,14 @@ const profileUsername = isPublicProfile ? pathParts[1] : null; const urlParams = new URLSearchParams(window.location.search); const previewTheme = urlParams.get('previewTheme'); - if (previewTheme) { - document.body.className = `theme-${previewTheme}`; - } + +let activeTheme = null; +let originalTheme = null; + +if (previewTheme) { + activeTheme = previewTheme; + document.body.className = `theme-${previewTheme}`; +} function apiUrl(endpoint) { if (isPublicProfile) { @@ -64,6 +69,34 @@ let currentThemeColor = '168, 85, 247'; +// Theme State Management +let activeTheme = 'midnight'; +let previewThemeState = null; +function applyTheme(themeName, isPreview = false) { + if (!themeName) return; + + // Remove existing theme classes + document.body.className = document.body.className + .split(' ') + .filter(cls => !cls.startsWith('theme-')) + .join(' '); + + // Apply new theme + document.body.classList.add(`theme-${themeName}`); + + // Update particle colors + currentThemeColor = + THEME_COLORS[themeName] || '168, 85, 247'; + + // Store preview state + if (isPreview) { + previewThemeState = themeName; + } else { + activeTheme = themeName; + previewThemeState = null; + } +} + // ─── Load Settings & Apply Theme ─── async function loadSettings() { try { @@ -78,8 +111,8 @@ // Apply theme const theme = settings.selectedTheme || 'midnight'; if (!previewTheme) { - document.body.className = `theme-${theme}`; - } + applyTheme(theme); +} currentThemeColor = THEME_COLORS[theme] || '168, 85, 247'; // Update page title @@ -400,12 +433,69 @@ } }); // --- End Scroll Progress Indicator --- + function setupThemePreviewControls() { + const controls = document.getElementById('themePreviewControls'); + const applyBtn = document.getElementById('applyThemeBtn'); + const cancelBtn = document.getElementById('cancelThemeBtn'); + if (!controls) return; + + // Hide controls if no preview theme exists + if (!previewTheme) { + controls.style.display = 'none'; + return; + } + + controls.style.display = 'flex'; + + // APPLY THEME + applyBtn?.addEventListener('click', async () => { + try { + const res = await fetch('/api/settings/theme', { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + selectedTheme: previewTheme + }) + }); + + if (!res.ok) { + throw new Error('Failed to save theme'); + } + + originalTheme = `theme-${previewTheme}`; + + alert('Theme applied successfully!'); + } catch (err) { + console.error(err); + alert('Failed to apply theme'); + } + }); + + // CANCEL PREVIEW + cancelBtn?.addEventListener('click', () => { + document.body.className = originalTheme; + + const cleanUrl = + window.location.pathname; + + window.history.replaceState({}, '', cleanUrl); + + controls.style.display = 'none'; + }); +} // ─── Init ─── document.addEventListener('DOMContentLoaded', async () => { - await loadSettings(); - initParticles(); - renderProfile(); - renderLinks(); - }); + await loadSettings(); + + originalTheme = document.body.className; + + initParticles(); + renderProfile(); + renderLinks(); + + setupThemePreviewControls(); +}); })(); From 84e222fff9bb5f96e3d14914176da5ad603c8d56 Mon Sep 17 00:00:00 2001 From: kavin553 Date: Tue, 16 Jun 2026 23:01:59 +0530 Subject: [PATCH 2/2] feat(search) : add skeleton loading animation --- public/admin.html | 14 ++++ public/css/admin.css | 179 +++++++++++++++++++++++++++++++++++++++++++ public/js/admin.js | 96 +++++++++++++++++++++++ 3 files changed, 289 insertions(+) diff --git a/public/admin.html b/public/admin.html index 37f6b44..c4dc2f9 100644 --- a/public/admin.html +++ b/public/admin.html @@ -114,6 +114,20 @@

Links

+ + diff --git a/public/css/admin.css b/public/css/admin.css index 882ff04..35ac2b1 100644 --- a/public/css/admin.css +++ b/public/css/admin.css @@ -1712,5 +1712,184 @@ select.form-input::-webkit-scrollbar-thumb { /* ─── Empty State for Categories ─── */ .categories-list .empty-state { padding: 60px 20px; +/* ═══════════════════════════════════════════════════════════ + SEARCH & SKELETON LOADERS + ═══════════════════════════════════════════════════════════ */ + +/* ─── Search Bar ─── */ +.links-search-bar { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-md); + margin-bottom: 20px; + transition: all var(--duration-fast) var(--ease-out); +} + +.links-search-bar:focus-within { + border-color: var(--accent); + background: var(--bg-secondary); + box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.1); +} + +.search-icon { + width: 18px; + height: 18px; + color: var(--text-secondary); + flex-shrink: 0; +} + +.links-search-input { + flex: 1; + background: transparent; + border: none; + color: var(--text-primary); + font-size: 0.95rem; + outline: none; +} + +.links-search-input::placeholder { + color: var(--text-muted); +} + +.search-clear-btn { + background: none; + border: none; + cursor: pointer; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + transition: color var(--duration-fast) var(--ease-out); + flex-shrink: 0; +} + +.search-clear-btn:hover { + color: var(--accent); +} + +.search-clear-btn svg { + width: 18px; + height: 18px; +} + +/* ─── Skeleton Loaders ─── */ +.skeleton { + background: linear-gradient( + 90deg, + var(--bg-secondary) 0%, + var(--bg-card) 50%, + var(--bg-secondary) 100% + ); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s infinite; + border-radius: var(--radius-sm); +} + +@keyframes skeleton-shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +/* Skeleton sizes */ +.skeleton-checkbox { + width: 20px; + height: 20px; + min-width: 20px; +} + +.skeleton-title { + width: 100%; + height: 14px; + margin-bottom: 8px; +} + +.skeleton-url { + width: 85%; + height: 12px; +} + +.skeleton-clicks { + width: 60px; + height: 14px; +} + +.skeleton-avatar { + width: 56px; + height: 56px; + border-radius: 50%; +} + +.skeleton-name { + width: 100%; + height: 16px; + margin-bottom: 8px; +} + +.skeleton-username { + width: 70%; + height: 12px; +} + +.skeleton-card { + display: flex; + align-items: center; + gap: 12px; + padding: 16px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius-md); + animation: pulse 1s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.7; + } +} + +/* ─── Search Results Empty State ─── */ +.search-empty-state { + padding: 60px 20px; + text-align: center; +} + +.search-empty-state svg { + width: 48px; + height: 48px; + color: var(--text-secondary); + margin-bottom: 16px; + opacity: 0.5; +} + +.search-empty-state h3 { + color: var(--text-primary); + margin: 0 0 8px; + font-size: 1.1rem; +} + +.search-empty-state p { + color: var(--text-secondary); + margin: 0; + font-size: 0.9rem; +} + +/* ─── Mobile Responsiveness ─── */ +@media (max-width: 768px) { + .links-search-bar { + margin-bottom: 16px; + } +} } diff --git a/public/js/admin.js b/public/js/admin.js index f2ac341..59ffb2b 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -516,6 +516,92 @@ `).join('')} `; + + // ═══════════ SEARCH FUNCTIONALITY ═══════════ + let allLinks = []; // Store all loaded links for search + let searchTimeout = null; + + function renderSearchSkeleton() { + const list = document.getElementById('adminLinksList'); + list.innerHTML = ` + ${Array(5).fill(` + + `).join('')} + `; + } + + function performSearch(query) { + const list = document.getElementById('adminLinksList'); + const normalizedQuery = query.toLowerCase().trim(); + + if (!normalizedQuery) { + renderAdminLinks(allLinks); + return; + } + + const filtered = allLinks.filter(link => { + const titleMatch = link.title.toLowerCase().includes(normalizedQuery); + const urlMatch = link.url.toLowerCase().includes(normalizedQuery); + const categoryMatch = link.category_id && categories.some(cat => + cat.id === link.category_id && (cat.name.toLowerCase().includes(normalizedQuery) || cat.icon?.includes(normalizedQuery)) + ); + return titleMatch || urlMatch || categoryMatch; + }); + + if (filtered.length === 0) { + list.innerHTML = ` +
+ + + + +

No links found

+

No links match your search. Try a different keyword.

+
+ `; + bulkSelection.clearSelection(); + return; + } + + renderAdminLinks(filtered); + } + + function setupSearchBar() { + const searchInput = document.getElementById('linksSearchInput'); + const clearBtn = document.getElementById('searchClearBtn'); + + if (!searchInput) return; + + searchInput.addEventListener('input', (e) => { + const query = e.target.value; + clearBtn.style.display = query ? 'flex' : 'none'; + clearTimeout(searchTimeout); + if (query) { + renderSearchSkeleton(); + searchTimeout = setTimeout(() => { + performSearch(query); + }, 300); + } else { + performSearch(''); + } + }); + + clearBtn.addEventListener('click', () => { + searchInput.value = ''; + searchInput.focus(); + clearBtn.style.display = 'none'; + performSearch(''); + }); + } } function renderAdminLinks(links) { @@ -531,6 +617,9 @@ } badge.textContent = `${links?.length || 0} links`; + // Store links for search functionality + allLinks = links || []; + if (links.length === 0) { list.innerHTML = `
@@ -1326,6 +1415,13 @@ loadCategories(); initPublicUrl(); setupKeyboardShortcuts(); + + // Call setupSearchBar after DOM is ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', setupSearchBar); + } else { + setupSearchBar(); + } }); // ─── Public URL Bar ───