Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions public/admin.html
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,20 @@ <h1>Links</h1>
</button>
</div>
</div>
<!-- Search Bar -->
<div class="links-search-bar">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="search-icon">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
<input type="text" id="linksSearchInput" class="links-search-input" placeholder="Search links by title, URL, or category...">
<button class="search-clear-btn" id="searchClearBtn" style="display: none;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="admin-links-list" id="adminLinksList"></div>
</section>

Expand Down
179 changes: 179 additions & 0 deletions public/css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}

41 changes: 41 additions & 0 deletions public/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
9 changes: 9 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,16 @@ <h1 class="profile-name" id="profileName">Loading...</h1>
<span>Conn<span class="conn-badge-dot">.</span></span>
</a>
</main>
<!-- Theme Preview Controls -->
<div class="theme-preview-controls" id="themePreviewControls">
<button id="applyThemeBtn">
Apply Theme
</button>

<button id="cancelThemeBtn">
Cancel
</button>
</div>
<script src="/js/app.js"></script>
<script src="js/theme-toggle.js"></script>
</body>
Expand Down
96 changes: 96 additions & 0 deletions public/js/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,92 @@
</div>
`).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(`
<div class="admin-link-item skeleton-card">
<div class="skeleton skeleton-checkbox"></div>
<div style="width: 16px; height: 16px; border-radius: 3px; flex-shrink: 0;"></div>
<div class="admin-link-info">
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-url"></div>
</div>
<div class="skeleton skeleton-clicks"></div>
<div class="skeleton" style="width: 40px; height: 20px; border-radius: 12px;"></div>
</div>
`).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 = `
<div class="empty-state search-empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
<h3>No links found</h3>
<p>No links match your search. Try a different keyword.</p>
</div>
`;
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) {
Expand All @@ -531,6 +617,9 @@
}
badge.textContent = `${links?.length || 0} links`;

// Store links for search functionality
allLinks = links || [];

if (links.length === 0) {
list.innerHTML = `
<div class="empty-state">
Expand Down Expand Up @@ -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 ───
Expand Down
Loading