Compare commits

...

3 commits

Author SHA1 Message Date
ld3zian
cc3d0e6dc8 add clear all button 2025-02-13 17:17:03 -07:00
ld3zian
b614d9fb3f add importing feed ids 2025-02-13 17:12:57 -07:00
ld3zian
ced8f26537 bump deps 2025-02-13 17:07:51 -07:00
4 changed files with 160 additions and 29 deletions

45
package-lock.json generated
View file

@ -10,12 +10,12 @@
"dependencies": { "dependencies": {
"@astrojs/rss": "^4.0.11", "@astrojs/rss": "^4.0.11",
"@astrojs/tailwind": "^6.0.0", "@astrojs/tailwind": "^6.0.0",
"@astrojs/vercel": "^8.0.6", "@astrojs/vercel": "^8.0.7",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@types/node": "^22.13.1", "@types/node": "^22.13.4",
"@vercel/analytics": "^1.5.0", "@vercel/analytics": "^1.5.0",
"@vercel/kv": "^3.0.0", "@vercel/kv": "^3.0.0",
"astro": "^5.2.5", "astro": "^5.3.0",
"feed": "^4.2.2", "feed": "^4.2.2",
"ioredis": "^5.5.0", "ioredis": "^5.5.0",
"tailwindcss": "^3.4.1" "tailwindcss": "^3.4.1"
@ -38,9 +38,9 @@
"integrity": "sha512-bL/O7YBxsFt55YHU021oL+xz+B/9HvGNId3F9xURN16aeqDK9juHGktdkCSXz+U4nqFACq6ZFvWomOzhV+zfPw==" "integrity": "sha512-bL/O7YBxsFt55YHU021oL+xz+B/9HvGNId3F9xURN16aeqDK9juHGktdkCSXz+U4nqFACq6ZFvWomOzhV+zfPw=="
}, },
"node_modules/@astrojs/internal-helpers": { "node_modules/@astrojs/internal-helpers": {
"version": "0.4.2", "version": "0.5.1",
"resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.4.2.tgz", "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.5.1.tgz",
"integrity": "sha512-EdDWkC3JJVcpGpqJAU/5hSk2LKXyG3mNGkzGoAuyK+xoPHbaVdSuIWoN1QTnmK3N/gGfaaAfM8gO2KDCAW7S3w==" "integrity": "sha512-M7rAge1n2+aOSxNvKUFa0u/KFn0W+sZy7EW91KOSERotm2Ti8qs+1K0xx3zbOxtAVrmJb5/J98eohVvvEqtNkw=="
}, },
"node_modules/@astrojs/markdown-remark": { "node_modules/@astrojs/markdown-remark": {
"version": "6.1.0", "version": "6.1.0",
@ -121,15 +121,15 @@
} }
}, },
"node_modules/@astrojs/vercel": { "node_modules/@astrojs/vercel": {
"version": "8.0.6", "version": "8.0.7",
"resolved": "https://registry.npmjs.org/@astrojs/vercel/-/vercel-8.0.6.tgz", "resolved": "https://registry.npmjs.org/@astrojs/vercel/-/vercel-8.0.7.tgz",
"integrity": "sha512-5lchUcf1HC8O8zQVJSTyGxZ9rxDsOj6NeueSC+qO5fZDlQXxf4avEzuJaHN2q1qNS9fZLOGEzcPgT0hNfHYBXg==", "integrity": "sha512-2DPqPGZ/Et4afpJt8CAoIql2S649rzEmLBNJPHUGBysP0JklMaZ05HhQVKr+qEhZSm3NrVccxpkiIG/eLW3nPg==",
"dependencies": { "dependencies": {
"@astrojs/internal-helpers": "^0.4.2", "@astrojs/internal-helpers": "^0.5.1",
"@vercel/analytics": "^1.4.1", "@vercel/analytics": "^1.4.1",
"@vercel/edge": "^1.2.1", "@vercel/edge": "^1.2.1",
"@vercel/nft": "^0.29.0", "@vercel/nft": "^0.29.0",
"@vercel/routing-utils": "5.0.2", "@vercel/routing-utils": "^5.0.4",
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
"fast-glob": "^3.3.3" "fast-glob": "^3.3.3"
}, },
@ -1464,9 +1464,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.13.1", "version": "22.13.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz",
"integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==", "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==",
"dependencies": { "dependencies": {
"undici-types": "~6.20.0" "undici-types": "~6.20.0"
} }
@ -1568,9 +1568,9 @@
} }
}, },
"node_modules/@vercel/routing-utils": { "node_modules/@vercel/routing-utils": {
"version": "5.0.2", "version": "5.0.4",
"resolved": "https://registry.npmjs.org/@vercel/routing-utils/-/routing-utils-5.0.2.tgz", "resolved": "https://registry.npmjs.org/@vercel/routing-utils/-/routing-utils-5.0.4.tgz",
"integrity": "sha512-uJViB3+HEo+kzHYELs7cQWX5k0kCNvq9G/8nJQX8mP5Ta0fG68CBRmOaaG8A+2xbtTp/QuGORIwV8CsI9ebcNg==", "integrity": "sha512-4ke67zkXVi2fRZdoYckABcsSkRC9CnrdadOGxoS/Bk22+ObHjGQWvUHExRSXh339anwu9YY7ZacNSGH4gUnTQA==",
"dependencies": { "dependencies": {
"path-to-regexp": "6.1.0", "path-to-regexp": "6.1.0",
"path-to-regexp-updated": "npm:path-to-regexp@6.3.0" "path-to-regexp-updated": "npm:path-to-regexp@6.3.0"
@ -1753,9 +1753,9 @@
} }
}, },
"node_modules/astro": { "node_modules/astro": {
"version": "5.2.5", "version": "5.3.0",
"resolved": "https://registry.npmjs.org/astro/-/astro-5.2.5.tgz", "resolved": "https://registry.npmjs.org/astro/-/astro-5.3.0.tgz",
"integrity": "sha512-AYXyYkc+c5xbKTm48FyQA91y81nXyNPAaoyafR0LUugE4lAwuvIUcXDBfMzmbuP1lGRvsE33G2oypv6gbGaPFg==", "integrity": "sha512-e88l/Yk/6enR/ZDddLbqtM+oblBFk5mneNSmNesyVYGL/6Dj4UA67GPAZOk79VxT5dbLlclZSyyw/wlxN1aj3A==",
"dependencies": { "dependencies": {
"@astrojs/compiler": "^2.10.3", "@astrojs/compiler": "^2.10.3",
"@astrojs/internal-helpers": "0.5.1", "@astrojs/internal-helpers": "0.5.1",
@ -1833,11 +1833,6 @@
"sharp": "^0.33.3" "sharp": "^0.33.3"
} }
}, },
"node_modules/astro/node_modules/@astrojs/internal-helpers": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.5.1.tgz",
"integrity": "sha512-M7rAge1n2+aOSxNvKUFa0u/KFn0W+sZy7EW91KOSERotm2Ti8qs+1K0xx3zbOxtAVrmJb5/J98eohVvvEqtNkw=="
},
"node_modules/astro/node_modules/@vercel/kv": { "node_modules/astro/node_modules/@vercel/kv": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@vercel/kv/-/kv-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@vercel/kv/-/kv-1.0.1.tgz",

View file

@ -13,12 +13,12 @@
"dependencies": { "dependencies": {
"@astrojs/rss": "^4.0.11", "@astrojs/rss": "^4.0.11",
"@astrojs/tailwind": "^6.0.0", "@astrojs/tailwind": "^6.0.0",
"@astrojs/vercel": "^8.0.6", "@astrojs/vercel": "^8.0.7",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.13",
"@types/node": "^22.13.1", "@types/node": "^22.13.4",
"@vercel/analytics": "^1.5.0", "@vercel/analytics": "^1.5.0",
"@vercel/kv": "^3.0.0", "@vercel/kv": "^3.0.0",
"astro": "^5.2.5", "astro": "^5.3.0",
"feed": "^4.2.2", "feed": "^4.2.2",
"ioredis": "^5.5.0", "ioredis": "^5.5.0",
"tailwindcss": "^3.4.1" "tailwindcss": "^3.4.1"

View file

@ -1,4 +1,5 @@
--- ---
import { getFeedMapping } from '../lib/db';
interface Props { interface Props {
currentSlugs?: string[]; currentSlugs?: string[];
} }
@ -42,11 +43,43 @@ const languages = [
</select> </select>
</div> </div>
<div class="flex gap-2">
<input
type="text"
id="feed-id-input"
placeholder="Enter feed ID to import..."
class="flex-1 px-3 sm:px-4 py-2 bg-primary rounded-lg border border-hover/30 focus:outline-none focus:border-accent/50 transition-colors text-sm sm:text-base"
/>
<button
id="import-feed"
class="px-4 py-2 bg-secondary hover:bg-hover transition-colors rounded-lg text-sm sm:text-base flex items-center gap-2"
>
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/>
</svg>
Import Feed
</button>
</div>
<div id="search-results" class="hidden space-y-2"> <div id="search-results" class="hidden space-y-2">
<!-- Search results will be populated here --> <!-- Search results will be populated here -->
</div> </div>
</div> </div>
<div class="flex justify-between items-center">
<h2 class="text-lg font-medium">Selected Comics</h2>
<button
id="clear-all"
class="px-3 py-1.5 text-sm text-red-500 hover:text-red-600 hover:bg-red-500/10 rounded-lg transition-colors flex items-center gap-2"
style="display: none;"
>
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
</svg>
Clear All
</button>
</div>
<div id="slug-list" class="space-y-2"> <div id="slug-list" class="space-y-2">
{currentSlugs.map(slug => ( {currentSlugs.map(slug => (
<div class="flex items-center justify-between p-3 bg-primary rounded-lg border border-hover/30" data-slug={slug}> <div class="flex items-center justify-between p-3 bg-primary rounded-lg border border-hover/30" data-slug={slug}>
@ -95,6 +128,9 @@ const languages = [
const slugList = document.getElementById('slug-list'); const slugList = document.getElementById('slug-list');
const generateRSSLink = document.getElementById('generate-rss'); const generateRSSLink = document.getElementById('generate-rss');
const langSelect = document.getElementById('lang-select') as HTMLSelectElement; const langSelect = document.getElementById('lang-select') as HTMLSelectElement;
const feedIdInput = document.getElementById('feed-id-input') as HTMLInputElement;
const importFeedButton = document.getElementById('import-feed');
const clearAllButton = document.getElementById('clear-all');
// Load saved slugs from localStorage // Load saved slugs from localStorage
let slugs = new Set(JSON.parse(localStorage.getItem('comic-slugs') || '[]')); let slugs = new Set(JSON.parse(localStorage.getItem('comic-slugs') || '[]'));
@ -103,8 +139,23 @@ const languages = [
localStorage.setItem('comic-slugs', JSON.stringify([...slugs])); localStorage.setItem('comic-slugs', JSON.stringify([...slugs]));
updateRSSLink(); updateRSSLink();
renderSlugs(); renderSlugs();
updateClearAllButton();
} }
function updateClearAllButton() {
if (clearAllButton) {
clearAllButton.style.display = slugs.size > 0 ? 'flex' : 'none';
}
}
// Clear all functionality
clearAllButton?.addEventListener('click', () => {
if (confirm('Are you sure you want to clear all comics? This cannot be undone.')) {
slugs.clear();
updateSlugs();
}
});
async function renderSlugs() { async function renderSlugs() {
if (!slugList) return; if (!slugList) return;
@ -410,9 +461,66 @@ const languages = [
} }
} }
// Import feed functionality
importFeedButton?.addEventListener('click', async () => {
const feedId = feedIdInput.value.trim();
if (!feedId) return;
try {
importFeedButton.disabled = true;
importFeedButton.innerHTML = `
<svg class="w-4 h-4 animate-spin" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-linecap="round"/>
</svg>
Importing...
`;
const response = await fetch(`/api/feed/${feedId}`);
if (!response.ok) throw new Error('Failed to fetch feed');
const { slugs: feedSlugs, lang } = await response.json();
// Update language if provided
if (lang && langSelect) {
langSelect.value = lang;
}
// Add new slugs to existing collection
feedSlugs.forEach((slug: string) => slugs.add(slug));
// Update UI
updateSlugs();
feedIdInput.value = '';
// Show success message
const successDiv = document.createElement('div');
successDiv.className = 'text-sm text-green-500 mt-1';
successDiv.textContent = 'Feed imported successfully!';
feedIdInput.parentNode?.appendChild(successDiv);
setTimeout(() => successDiv.remove(), 3000);
} catch (error) {
console.error('Failed to import feed:', error);
const errorDiv = document.createElement('div');
errorDiv.className = 'text-sm text-red-500 mt-1';
errorDiv.textContent = 'Failed to import feed. Please check the feed ID and try again.';
feedIdInput.parentNode?.appendChild(errorDiv);
setTimeout(() => errorDiv.remove(), 3000);
} finally {
importFeedButton.disabled = false;
importFeedButton.innerHTML = `
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/>
</svg>
Import Feed
`;
}
});
// Initial render // Initial render
renderSlugs(); renderSlugs();
updateRSSLink(); updateRSSLink();
updateClearAllButton();
// Update RSS link when language changes // Update RSS link when language changes
langSelect?.addEventListener('change', () => updateRSSLink()); langSelect?.addEventListener('change', () => updateRSSLink());

View file

@ -0,0 +1,28 @@
import type { APIRoute } from "astro";
import { getFeedMapping } from '../../../lib/db';
export const GET: APIRoute = async ({ params }) => {
const { feedId } = params;
if (!feedId) {
return new Response("Feed ID not provided", { status: 400 });
}
try {
const mapping = await getFeedMapping(feedId);
if (!mapping) {
return new Response("Feed not found", { status: 404 });
}
return new Response(JSON.stringify(mapping), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
});
} catch (error) {
console.error('Error fetching feed:', error);
return new Response("Internal server error", { status: 500 });
}
};