feat: lists

This commit is contained in:
2025-11-15 19:11:07 +01:00
parent 3ab8cdcc37
commit 3378a13fe4
5 changed files with 1550 additions and 27 deletions

177
server.js
View File

@@ -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 {