diff --git a/public/app.js b/public/app.js
index f579e25..918cf40 100644
--- a/public/app.js
+++ b/public/app.js
@@ -6,11 +6,13 @@ const linkInput = document.getElementById('linkInput');
const addButton = document.getElementById('addButton');
const searchInput = document.getElementById('searchInput');
const linksContainer = document.getElementById('linksContainer');
-const messageDiv = document.getElementById('message');
+const toastContainer = document.getElementById('toastContainer');
+const archiveToggle = document.getElementById('archiveToggle');
// State
let allLinks = [];
let searchTimeout = null;
+let showArchived = false;
// Initialize app
document.addEventListener('DOMContentLoaded', () => {
@@ -22,6 +24,7 @@ document.addEventListener('DOMContentLoaded', () => {
function setupEventListeners() {
linkForm.addEventListener('submit', handleAddLink);
searchInput.addEventListener('input', handleSearch);
+ archiveToggle.addEventListener('click', handleToggleArchive);
}
// Load all links
@@ -31,13 +34,26 @@ async function loadLinks() {
if (!response.ok) throw new Error('Failed to load links');
allLinks = await response.json();
- displayLinks(allLinks);
+ // Ensure all links have archived property
+ allLinks = allLinks.map(link => ({
+ ...link,
+ archived: link.archived || false
+ }));
+ displayLinks(getFilteredLinks());
} catch (error) {
showMessage('Failed to load links', 'error');
console.error('Error loading links:', error);
}
}
+// Get filtered links based on archived status
+function getFilteredLinks() {
+ return allLinks.filter(link => {
+ const isArchived = link.archived === true;
+ return showArchived ? isArchived : !isArchived;
+ });
+}
+
// Handle add link form submission
async function handleAddLink(e) {
e.preventDefault();
@@ -67,9 +83,11 @@ async function handleAddLink(e) {
throw new Error(data.error || 'Failed to add link');
}
+ // Ensure archived property exists
+ data.archived = data.archived || false;
// Add to beginning of array
allLinks.unshift(data);
- displayLinks(allLinks);
+ displayLinks(getFilteredLinks());
// Clear input
linkInput.value = '';
@@ -93,7 +111,7 @@ function handleSearch(e) {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(async () => {
if (!query) {
- displayLinks(allLinks);
+ displayLinks(getFilteredLinks());
return;
}
@@ -102,7 +120,12 @@ function handleSearch(e) {
if (!response.ok) throw new Error('Search failed');
const filteredLinks = await response.json();
- displayLinks(filteredLinks);
+ // Filter by archived status
+ const archivedFiltered = filteredLinks.filter(link => {
+ const isArchived = link.archived === true;
+ return showArchived ? isArchived : !isArchived;
+ });
+ displayLinks(archivedFiltered);
} catch (error) {
showMessage('Search failed', 'error');
console.error('Error searching:', error);
@@ -110,12 +133,34 @@ function handleSearch(e) {
}, 300);
}
+// Handle archive toggle
+function handleToggleArchive() {
+ showArchived = !showArchived;
+ const toggleWrapper = archiveToggle.closest('.archive-toggle-wrapper');
+ archiveToggle.classList.toggle('active', showArchived);
+ toggleWrapper.classList.toggle('active', showArchived);
+
+ // Re-filter and display links
+ const query = searchInput.value.trim();
+ if (query) {
+ // Re-run search with new filter
+ handleSearch({ target: searchInput });
+ } else {
+ displayLinks(getFilteredLinks());
+ }
+}
+
// Display links
function displayLinks(links) {
if (links.length === 0) {
+ const filteredCount = getFilteredLinks().length;
+ const totalCount = allLinks.length;
+ const message = showArchived
+ ? (totalCount === 0 ? 'Add your first link to get started!' : 'No archived links found.')
+ : (totalCount === 0 ? 'Add your first link to get started!' : (filteredCount === 0 ? 'No active links found. Try a different search term or toggle to view archived links.' : 'Try a different search term.'));
linksContainer.innerHTML = `
-
No links found. ${allLinks.length === 0 ? 'Add your first link to get started!' : 'Try a different search term.'}
+
No links found. ${message}
`;
return;
@@ -130,6 +175,14 @@ function displayLinks(links) {
handleDeleteLink(linkId);
});
});
+
+ // Add archive event listeners
+ document.querySelectorAll('.archive-btn').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ const linkId = e.target.closest('.link-card').dataset.id;
+ handleArchiveLink(linkId);
+ });
+ });
}
// Create link card HTML
@@ -158,13 +211,117 @@ function createLinkCard(link) {
`;
}
+// Update a single link card in place
+function updateLinkCard(link) {
+ const linkCard = document.querySelector(`.link-card[data-id="${link.id}"]`);
+ if (!linkCard) return;
+
+ // Replace the card content
+ linkCard.outerHTML = createLinkCard(link);
+
+ // Re-attach event listeners to the new card
+ const newCard = document.querySelector(`.link-card[data-id="${link.id}"]`);
+ if (newCard) {
+ const deleteBtn = newCard.querySelector('.delete-btn');
+ const archiveBtn = newCard.querySelector('.archive-btn');
+
+ if (deleteBtn) {
+ deleteBtn.addEventListener('click', () => handleDeleteLink(link.id));
+ }
+ if (archiveBtn) {
+ archiveBtn.addEventListener('click', () => handleArchiveLink(link.id));
+ }
+ }
+}
+
+// Remove a single link card
+function removeLinkCard(id) {
+ const linkCard = document.querySelector(`.link-card[data-id="${id}"]`);
+ if (linkCard) {
+ linkCard.remove();
+
+ // Check if we need to show empty state
+ const remainingCards = document.querySelectorAll('.link-card');
+ if (remainingCards.length === 0) {
+ const filteredCount = getFilteredLinks().length;
+ const totalCount = allLinks.length;
+ const message = showArchived
+ ? (totalCount === 0 ? 'Add your first link to get started!' : 'No archived links found.')
+ : (totalCount === 0 ? 'Add your first link to get started!' : (filteredCount === 0 ? 'No active links found. Try a different search term or toggle to view archived links.' : 'Try a different search term.'));
+ linksContainer.innerHTML = `
+
+
No links found. ${message}
+
+ `;
+ }
+ }
+}
+
+// Check if a link should be visible based on current filters
+function shouldLinkBeVisible(link) {
+ const isArchived = link.archived === true;
+ const matchesArchiveFilter = showArchived ? isArchived : !isArchived;
+
+ const query = searchInput.value.trim();
+ if (query) {
+ const queryLower = query.toLowerCase();
+ const titleMatch = link.title?.toLowerCase().includes(queryLower);
+ const descMatch = link.description?.toLowerCase().includes(queryLower);
+ const urlMatch = link.url?.toLowerCase().includes(queryLower);
+ const matchesSearch = titleMatch || descMatch || urlMatch;
+ return matchesArchiveFilter && matchesSearch;
+ }
+
+ return matchesArchiveFilter;
+}
+
+// Handle archive link
+async function handleArchiveLink(id) {
+ try {
+ const link = allLinks.find(l => l.id === id);
+ if (!link) return;
+
+ const newArchivedStatus = !link.archived;
+
+ const response = await fetch(`${API_BASE}/${id}/archive`, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ archived: newArchivedStatus })
+ });
+
+ if (!response.ok) throw new Error('Failed to archive link');
+
+ // Update local array
+ link.archived = newArchivedStatus;
+
+ // Update or remove the link card without refreshing the whole list
+ if (shouldLinkBeVisible(link)) {
+ // Link should still be visible, update it in place
+ updateLinkCard(link);
+ } else {
+ // Link should no longer be visible, remove it
+ removeLinkCard(id);
+ }
+
+ showMessage(newArchivedStatus ? 'Link archived successfully' : 'Link unarchived successfully', 'success');
+ } catch (error) {
+ showMessage('Failed to archive link', 'error');
+ console.error('Error archiving link:', error);
+ }
+}
+
// Handle delete link
async function handleDeleteLink(id) {
if (!confirm('Are you sure you want to delete this link?')) {
@@ -180,7 +337,7 @@ async function handleDeleteLink(id) {
// Remove from local array
allLinks = allLinks.filter(link => link.id !== id);
- displayLinks(allLinks);
+ displayLinks(getFilteredLinks());
showMessage('Link deleted successfully', 'success');
} catch (error) {
showMessage('Failed to delete link', 'error');
@@ -188,14 +345,28 @@ async function handleDeleteLink(id) {
}
}
-// Show message
+// Show toast message
function showMessage(text, type = 'success') {
- messageDiv.textContent = text;
- messageDiv.className = `message ${type}`;
- messageDiv.style.display = 'block';
+ const toast = document.createElement('div');
+ toast.className = `toast toast-${type}`;
+ toast.textContent = text;
+ // Add to container
+ toastContainer.appendChild(toast);
+
+ // Trigger animation
+ requestAnimationFrame(() => {
+ toast.classList.add('show');
+ });
+
+ // Remove after delay
setTimeout(() => {
- messageDiv.style.display = 'none';
+ toast.classList.remove('show');
+ setTimeout(() => {
+ if (toast.parentNode) {
+ toast.parentNode.removeChild(toast);
+ }
+ }, 300); // Wait for fade-out animation
}, 3000);
}
diff --git a/public/index.html b/public/index.html
index 5e67557..33a8dfc 100644
--- a/public/index.html
+++ b/public/index.html
@@ -27,6 +27,12 @@
autocomplete="off"
>
+
+
+ Archive
+
@@ -60,8 +66,6 @@
-
-
No links yet. Add your first link to get started!
@@ -69,6 +73,8 @@
+
+