diff --git a/public/styles.css b/public/styles.css
index 8bcb58f..584aa37 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -1181,3 +1181,653 @@ body {
}
}
+/* Lists Toggle Button */
+.lists-toggle-btn {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--text-secondary);
+ padding: 0.5rem;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ width: 36px;
+ height: 36px;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+}
+
+.lists-toggle-btn:hover {
+ background: var(--surface-light);
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+.lists-toggle-btn.active {
+ background: rgba(99, 102, 241, 0.1);
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+.lists-toggle-btn svg {
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+ display: block;
+ margin: 0;
+ padding: 0;
+}
+
+/* List Filter Section */
+.list-filter-wrapper {
+ width: 100%;
+ padding: 0.75rem 0;
+ margin-top: 8px;
+ border-top: 1px solid var(--border);
+ display: none;
+ opacity: 0;
+ max-height: 0;
+ overflow: hidden;
+ transition: opacity 0.3s ease, max-height 0.3s ease, padding 0.3s ease;
+}
+
+.list-filter-wrapper.show {
+ display: block;
+ opacity: 1;
+ max-height: 500px;
+}
+
+.list-filter-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 0.5rem;
+}
+
+.list-filter-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.edit-lists-btn {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--text-secondary);
+ cursor: pointer;
+ font-size: 0.875rem;
+ padding: 0.375rem 0.75rem;
+ border-radius: 6px;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 0.375rem;
+}
+
+.edit-lists-btn:hover {
+ background: var(--surface-light);
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+.edit-lists-btn svg {
+ width: 16px;
+ height: 16px;
+ flex-shrink: 0;
+}
+
+.list-filter-label {
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.clear-list-filters-btn {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--text-secondary);
+ cursor: pointer;
+ font-size: 0.875rem;
+ padding: 0.375rem 0.75rem;
+ border-radius: 6px;
+ transition: all 0.3s ease;
+}
+
+.clear-list-filters-btn:hover {
+ background: var(--surface-light);
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+.list-filter-chips {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+}
+
+.list-filter-chip {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--text-secondary);
+ padding: 0.375rem 0.75rem;
+ border-radius: 6px;
+ font-size: 0.875rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.list-filter-chip:hover {
+ background: var(--surface-light);
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+.list-filter-chip.active {
+ background: rgba(99, 102, 241, 0.1);
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+/* Lists Management Modal */
+.lists-modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.7);
+ backdrop-filter: blur(4px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+}
+
+.lists-modal.show {
+ opacity: 1;
+ visibility: visible;
+}
+
+.lists-modal-content {
+ background: var(--surface);
+ border-radius: 1rem;
+ width: 90%;
+ max-width: 600px;
+ max-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ box-shadow: 0 20px 60px var(--shadow-lg);
+ transform: scale(0.9);
+ transition: transform 0.3s ease;
+}
+
+.lists-modal.show .lists-modal-content {
+ transform: scale(1);
+}
+
+.lists-modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1.5rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.lists-modal-header h2 {
+ margin: 0;
+ font-size: 1.5rem;
+ color: var(--text-primary);
+}
+
+.close-modal-btn {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--text-secondary);
+ cursor: pointer;
+ padding: 0.5rem;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s ease;
+ width: 36px;
+ height: 36px;
+}
+
+.close-modal-btn:hover {
+ background: var(--surface-light);
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+.lists-modal-body {
+ padding: 1.5rem;
+ overflow-y: auto;
+ flex: 1;
+}
+
+.create-list-form {
+ display: flex;
+ gap: 0.75rem;
+ margin-bottom: 1.5rem;
+}
+
+.create-list-form input {
+ flex: 1;
+ background: var(--background);
+ border: 1px solid var(--border);
+ color: var(--text-primary);
+ padding: 0.75rem;
+ border-radius: 0.5rem;
+ font-size: 1rem;
+}
+
+.create-list-form input:focus {
+ outline: none;
+ border-color: var(--primary-color);
+}
+
+.btn-create-list {
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
+ color: white;
+ border: none;
+ padding: 0.75rem 1.5rem;
+ border-radius: 8px;
+ cursor: pointer;
+ font-weight: 600;
+ transition: all 0.3s ease;
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
+}
+
+.btn-create-list:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
+}
+
+.btn-create-list:active {
+ transform: translateY(0);
+}
+
+.lists-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.list-item {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.75rem;
+ background: var(--background);
+ border-radius: 0.5rem;
+ border: 1px solid var(--border);
+ min-width: 0;
+ overflow: hidden;
+}
+
+.list-name-input {
+ flex: 1;
+ min-width: 0;
+ background: transparent;
+ border: none;
+ color: var(--text-primary);
+ font-size: 1rem;
+ padding: 0.25rem 0.5rem;
+ border-radius: 0.25rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.list-name-input:focus {
+ outline: 2px solid var(--primary-color);
+ outline-offset: 2px;
+}
+
+.list-item-actions {
+ display: flex;
+ gap: 0.5rem;
+ flex-shrink: 0;
+}
+
+.toggle-public-btn,
+.public-url-btn,
+.save-list-btn,
+.delete-list-btn {
+ background: transparent;
+ color: var(--text-muted);
+ border: 1px solid var(--border);
+ padding: 0.375rem;
+ border-radius: 6px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s ease;
+ position: relative;
+}
+
+.toggle-public-btn svg,
+.public-url-btn svg,
+.save-list-btn svg,
+.delete-list-btn svg {
+ width: 16px;
+ height: 16px;
+ stroke-width: 2;
+}
+
+.toggle-public-btn {
+ color: var(--text-secondary);
+ border-color: rgba(156, 163, 175, 0.3);
+}
+
+.toggle-public-btn:hover {
+ background: var(--surface-light);
+ border-color: var(--text-secondary);
+ color: var(--text-primary);
+}
+
+.toggle-public-btn.active {
+ color: var(--secondary-color);
+ border-color: rgba(139, 92, 246, 0.3);
+}
+
+.toggle-public-btn.active:hover {
+ background: rgba(139, 92, 246, 0.2);
+ border-color: var(--secondary-color);
+ color: var(--secondary-color);
+}
+
+.public-url-btn {
+ color: var(--primary-color);
+ border-color: rgba(99, 102, 241, 0.3);
+}
+
+.public-url-btn:hover {
+ background: rgba(99, 102, 241, 0.2);
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+.save-list-btn {
+ color: var(--success);
+ border-color: rgba(16, 185, 129, 0.3);
+}
+
+.save-list-btn:hover {
+ background: rgba(16, 185, 129, 0.2);
+ border-color: var(--success);
+ color: var(--success);
+}
+
+.delete-list-btn {
+ color: var(--error);
+ border-color: rgba(239, 68, 68, 0.3);
+}
+
+.delete-list-btn:hover {
+ background: rgba(239, 68, 68, 0.2);
+ border-color: var(--error);
+ color: var(--error);
+}
+
+.empty-lists-message {
+ text-align: center;
+ color: var(--text-muted);
+ padding: 2rem;
+}
+
+/* List Selection Overlay */
+.list-selection-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.7);
+ backdrop-filter: blur(4px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1001;
+ opacity: 0;
+ visibility: hidden;
+ transition: all 0.3s ease;
+}
+
+.list-selection-overlay.show {
+ opacity: 1;
+ visibility: visible;
+}
+
+.list-selection-content {
+ background: var(--surface);
+ border-radius: 1rem;
+ width: 90%;
+ max-width: 500px;
+ max-height: 80vh;
+ display: flex;
+ flex-direction: column;
+ box-shadow: 0 20px 60px var(--shadow-lg);
+ transform: scale(0.9);
+ transition: transform 0.3s ease;
+}
+
+.list-selection-overlay.show .list-selection-content {
+ transform: scale(1);
+}
+
+.list-selection-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1.5rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.list-selection-header h3 {
+ margin: 0;
+ font-size: 1.25rem;
+ color: var(--text-primary);
+}
+
+.close-overlay-btn {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--text-secondary);
+ cursor: pointer;
+ padding: 0.5rem;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s ease;
+ width: 36px;
+ height: 36px;
+}
+
+.close-overlay-btn:hover {
+ background: var(--surface-light);
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+.list-selection-body {
+ padding: 1.5rem;
+ overflow-y: auto;
+ max-height: 400px;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.list-checkbox-item {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.75rem;
+ background: var(--background);
+ border-radius: 0.5rem;
+ border: 1px solid var(--border);
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.list-checkbox-item:hover {
+ background: var(--surface-light);
+}
+
+.list-checkbox-item input[type="checkbox"] {
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+ accent-color: var(--primary-color);
+}
+
+.list-checkbox-item span {
+ color: var(--text-primary);
+ flex: 1;
+}
+
+.list-selection-footer {
+ display: flex;
+ gap: 0.75rem;
+ padding: 1.5rem;
+ border-top: 1px solid var(--border);
+ justify-content: flex-end;
+}
+
+.btn-save-lists,
+.btn-cancel-lists {
+ padding: 0.75rem 1.5rem;
+ border-radius: 8px;
+ cursor: pointer;
+ font-weight: 600;
+ transition: all 0.3s ease;
+ border: none;
+}
+
+.btn-save-lists {
+ background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
+ color: white;
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
+}
+
+.btn-save-lists:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 6px 16px rgba(99, 102, 241, 0.4);
+}
+
+.btn-save-lists:active {
+ transform: translateY(0);
+}
+
+.btn-cancel-lists {
+ background: transparent;
+ color: var(--text-secondary);
+ border: 1px solid var(--border);
+}
+
+.btn-cancel-lists:hover {
+ background: var(--surface-light);
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+/* Add to List Button in Link Cards */
+.add-to-list-btn {
+ background: transparent;
+ color: var(--text-muted);
+ border: 1px solid var(--border);
+ padding: 0.375rem;
+ border-radius: 6px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s ease;
+ position: relative;
+ width: 32px;
+ height: 32px;
+ box-sizing: border-box;
+}
+
+.add-to-list-btn svg {
+ width: 16px;
+ height: 16px;
+ stroke-width: 2;
+ flex-shrink: 0;
+ display: block;
+ margin: 0;
+ padding: 0;
+}
+
+.add-to-list-btn {
+ color: var(--primary-color);
+ border-color: rgba(99, 102, 241, 0.3);
+}
+
+.add-to-list-btn:hover {
+ background: rgba(99, 102, 241, 0.2);
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+@media (max-width: 768px) {
+ .lists-modal-content,
+ .list-selection-content {
+ width: 95%;
+ max-height: 90vh;
+ }
+
+ .create-list-form {
+ flex-direction: column;
+ }
+
+ .list-selection-footer {
+ flex-direction: column;
+ }
+
+ .btn-save-lists,
+ .btn-cancel-lists {
+ width: 100%;
+ }
+
+ .list-item {
+ gap: 0.5rem;
+ padding: 0.5rem;
+ }
+
+ .list-name-input {
+ font-size: 0.9rem;
+ padding: 0.25rem;
+ }
+
+ .list-item-actions {
+ gap: 0.25rem;
+ }
+
+ .toggle-public-btn,
+ .public-url-btn,
+ .save-list-btn,
+ .delete-list-btn {
+ padding: 0.25rem;
+ min-width: 28px;
+ width: 28px;
+ height: 28px;
+ }
+
+ .toggle-public-btn svg,
+ .public-url-btn svg,
+ .save-list-btn svg,
+ .delete-list-btn svg {
+ width: 14px;
+ height: 14px;
+ }
+}
+
diff --git a/public/sw.js b/public/sw.js
index 262b65e..7c9dcdf 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -1,4 +1,4 @@
-const CACHE_NAME = 'linkding-v1';
+const CACHE_NAME = 'linkding-v2';
const urlsToCache = [
'/',
'/index.html',
@@ -36,12 +36,14 @@ self.addEventListener('activate', (event) => {
}
})
);
+ }).then(() => {
+ // Claim all clients immediately to ensure new service worker takes control
+ return self.clients.claim();
})
);
- return self.clients.claim();
});
-// Fetch event - serve from cache, fallback to network
+// Fetch event - network first for HTML/CSS/JS, cache first for images
self.addEventListener('fetch', (event) => {
// API requests - always fetch from network, don't cache
if (event.request.url.includes('/api/')) {
@@ -49,12 +51,17 @@ self.addEventListener('fetch', (event) => {
return;
}
- // Static assets - cache first, then network
- event.respondWith(
- caches.match(event.request)
- .then((response) => {
- // Return cached version or fetch from network
- return response || fetch(event.request).then((response) => {
+ const url = new URL(event.request.url);
+ const isHTML = event.request.destination === 'document' || url.pathname === '/' || url.pathname.endsWith('.html');
+ const isCSS = url.pathname.endsWith('.css');
+ const isJS = url.pathname.endsWith('.js');
+ const isImage = event.request.destination === 'image' || url.pathname.match(/\.(png|jpg|jpeg|gif|svg|webp|ico)$/i);
+
+ // For HTML, CSS, and JS: Network first, then cache (for offline support)
+ if (isHTML || isCSS || isJS) {
+ event.respondWith(
+ fetch(event.request)
+ .then((response) => {
// Don't cache non-successful responses
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
@@ -69,14 +76,55 @@ self.addEventListener('fetch', (event) => {
});
return response;
- });
- })
- .catch(() => {
- // If both cache and network fail, return offline page if available
- if (event.request.destination === 'document') {
- return caches.match('/index.html');
- }
- })
- );
+ })
+ .catch(() => {
+ // Network failed, try cache
+ return caches.match(event.request).then((response) => {
+ // If it's a document request and cache fails, return index.html
+ if (!response && isHTML) {
+ return caches.match('/index.html');
+ }
+ return response;
+ });
+ })
+ );
+ return;
+ }
+
+ // For images and other static assets: Cache first, then network
+ if (isImage) {
+ event.respondWith(
+ caches.match(event.request)
+ .then((response) => {
+ // Return cached version or fetch from network
+ return response || fetch(event.request).then((response) => {
+ // Don't cache non-successful responses
+ if (!response || response.status !== 200 || response.type !== 'basic') {
+ return response;
+ }
+
+ // Clone the response
+ const responseToCache = response.clone();
+
+ caches.open(CACHE_NAME)
+ .then((cache) => {
+ cache.put(event.request, responseToCache);
+ });
+
+ return response;
+ });
+ })
+ .catch(() => {
+ // If both cache and network fail, return offline page if available
+ if (event.request.destination === 'document') {
+ return caches.match('/index.html');
+ }
+ })
+ );
+ return;
+ }
+
+ // For other requests: Network first
+ event.respondWith(fetch(event.request));
});
diff --git a/server.js b/server.js
index 8298976..b53d8bf 100644
--- a/server.js
+++ b/server.js
@@ -64,6 +64,7 @@ function findChromeExecutable() {
const app = express();
const PORT = process.env.PORT || 3000;
const DATA_FILE = path.join(__dirname, 'data', 'links.json');
+const LISTS_FILE = path.join(__dirname, 'data', 'lists.json');
// Middleware
app.use(cors());
@@ -83,6 +84,11 @@ async function ensureDataDir() {
} catch {
await fs.writeFile(DATA_FILE, JSON.stringify([]));
}
+ try {
+ await fs.access(LISTS_FILE);
+ } catch {
+ await fs.writeFile(LISTS_FILE, JSON.stringify([]));
+ }
}
// Read links from file
@@ -100,6 +106,21 @@ async function writeLinks(links) {
await fs.writeFile(DATA_FILE, JSON.stringify(links, null, 2));
}
+// Read lists from file
+async function readLists() {
+ try {
+ const data = await fs.readFile(LISTS_FILE, 'utf8');
+ return JSON.parse(data);
+ } catch (error) {
+ return [];
+ }
+}
+
+// Write lists to file
+async function writeLists(lists) {
+ await fs.writeFile(LISTS_FILE, JSON.stringify(lists, null, 2));
+}
+
// Extract metadata using Puppeteer (for JavaScript-heavy sites)
async function extractMetadataWithPuppeteer(url) {
const pptr = await getPuppeteer();
@@ -686,6 +707,162 @@ app.delete('/api/links/:id', async (req, res) => {
}
});
+// Update link's lists
+app.patch('/api/links/:id/lists', async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { listIds } = req.body;
+
+ if (!Array.isArray(listIds)) {
+ return res.status(400).json({ error: 'listIds must be an array' });
+ }
+
+ const links = await readLinks();
+ const linkIndex = links.findIndex(link => link.id === id);
+
+ if (linkIndex === -1) {
+ return res.status(404).json({ error: 'Link not found' });
+ }
+
+ links[linkIndex].listIds = listIds;
+ await writeLinks(links);
+
+ res.json(links[linkIndex]);
+ } catch (error) {
+ res.status(500).json({ error: 'Failed to update link lists' });
+ }
+});
+
+// Lists API Routes
+
+// Get all lists
+app.get('/api/lists', async (req, res) => {
+ try {
+ const lists = await readLists();
+ res.json(lists);
+ } catch (error) {
+ res.status(500).json({ error: 'Failed to read lists' });
+ }
+});
+
+// Create a new list
+app.post('/api/lists', async (req, res) => {
+ try {
+ const { name } = req.body;
+
+ if (!name || typeof name !== 'string' || name.trim().length === 0) {
+ return res.status(400).json({ error: 'List name is required' });
+ }
+
+ const lists = await readLists();
+
+ // Check if list with same name already exists
+ const existingList = lists.find(list => list.name.toLowerCase() === name.trim().toLowerCase());
+ if (existingList) {
+ return res.status(409).json({ error: 'List with this name already exists' });
+ }
+
+ const newList = {
+ id: Date.now().toString(),
+ name: name.trim(),
+ createdAt: new Date().toISOString(),
+ public: false
+ };
+
+ lists.push(newList);
+ await writeLists(lists);
+
+ res.status(201).json(newList);
+ } catch (error) {
+ res.status(500).json({ error: 'Failed to create list' });
+ }
+});
+
+// Update a list
+app.put('/api/lists/:id', async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { name } = req.body;
+
+ if (!name || typeof name !== 'string' || name.trim().length === 0) {
+ return res.status(400).json({ error: 'List name is required' });
+ }
+
+ const lists = await readLists();
+ const listIndex = lists.findIndex(list => list.id === id);
+
+ if (listIndex === -1) {
+ return res.status(404).json({ error: 'List not found' });
+ }
+
+ // Check if another list with same name exists
+ const existingList = lists.find(list => list.id !== id && list.name.toLowerCase() === name.trim().toLowerCase());
+ if (existingList) {
+ return res.status(409).json({ error: 'List with this name already exists' });
+ }
+
+ lists[listIndex].name = name.trim();
+ await writeLists(lists);
+
+ res.json(lists[listIndex]);
+ } catch (error) {
+ res.status(500).json({ error: 'Failed to update list' });
+ }
+});
+
+// Toggle list public status
+app.patch('/api/lists/:id/public', async (req, res) => {
+ try {
+ const { id } = req.params;
+ const { public: isPublic } = req.body;
+
+ if (typeof isPublic !== 'boolean') {
+ return res.status(400).json({ error: 'public must be a boolean' });
+ }
+
+ const lists = await readLists();
+ const listIndex = lists.findIndex(list => list.id === id);
+
+ if (listIndex === -1) {
+ return res.status(404).json({ error: 'List not found' });
+ }
+
+ lists[listIndex].public = isPublic;
+ await writeLists(lists);
+
+ res.json(lists[listIndex]);
+ } catch (error) {
+ res.status(500).json({ error: 'Failed to update list public status' });
+ }
+});
+
+// Delete a list
+app.delete('/api/lists/:id', async (req, res) => {
+ try {
+ const { id } = req.params;
+ const lists = await readLists();
+ const filtered = lists.filter(list => list.id !== id);
+
+ if (filtered.length === lists.length) {
+ return res.status(404).json({ error: 'List not found' });
+ }
+
+ // Remove this list from all links
+ const links = await readLinks();
+ links.forEach(link => {
+ if (link.listIds && Array.isArray(link.listIds)) {
+ link.listIds = link.listIds.filter(listId => listId !== id);
+ }
+ });
+ await writeLinks(links);
+
+ await writeLists(filtered);
+ res.json({ message: 'List deleted successfully' });
+ } catch (error) {
+ res.status(500).json({ error: 'Failed to delete list' });
+ }
+});
+
// Helper function to validate URL
function isValidUrl(string) {
try {