mirror of
https://github.com/ld3z/new-manga.git
synced 2026-11-09 21:03:45 -07:00
Compare commits
10 commits
23c62495eb
...
3a9bb53ae7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a9bb53ae7 | ||
|
|
71215e4697 | ||
|
|
9f4ec3d3fd | ||
|
|
f9fc2c4cb0 | ||
|
|
daa059e667 | ||
|
|
86fa1d5a64 | ||
|
|
cc173b66bd | ||
|
|
8c634a381a | ||
|
|
007ec1856f | ||
|
|
db585307f5 |
13 changed files with 736 additions and 141 deletions
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
"@astrojs/vercel": "^8.0.2",
|
"@astrojs/vercel": "^8.0.2",
|
||||||
"@floating-ui/dom": "^1.6.3",
|
"@floating-ui/dom": "^1.6.3",
|
||||||
|
"@vercel/analytics": "^1.4.1",
|
||||||
"@vercel/kv": "^1.0.1",
|
"@vercel/kv": "^1.0.1",
|
||||||
"astro": "^5.1.6",
|
"astro": "^5.1.6",
|
||||||
"ioredis": "^5.4.2",
|
"ioredis": "^5.4.2",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
"@astrojs/vercel": "^8.0.2",
|
"@astrojs/vercel": "^8.0.2",
|
||||||
"@floating-ui/dom": "^1.6.3",
|
"@floating-ui/dom": "^1.6.3",
|
||||||
|
"@vercel/analytics": "^1.4.1",
|
||||||
"@vercel/kv": "^1.0.1",
|
"@vercel/kv": "^1.0.1",
|
||||||
"astro": "^5.1.6",
|
"astro": "^5.1.6",
|
||||||
"ioredis": "^5.4.2",
|
"ioredis": "^5.4.2",
|
||||||
|
|
|
||||||
|
|
@ -97,14 +97,21 @@ const isNSFW = comic.md_comics.content_rating === 'erotica';
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-auto pt-4 flex items-center justify-between text-sm text-primary/60 border-t border-hover/30">
|
<div class="mt-auto pt-4 flex items-center justify-between text-sm text-primary/60 border-t border-hover/30">
|
||||||
<time datetime={comic.updated_at}>
|
<div class="flex flex-col gap-1">
|
||||||
{new Date(comic.updated_at).toLocaleString(undefined, {
|
<time datetime={comic.updated_at}>
|
||||||
month: 'short',
|
{new Date(comic.updated_at).toLocaleString(undefined, {
|
||||||
day: 'numeric',
|
month: 'short',
|
||||||
hour: '2-digit',
|
day: 'numeric',
|
||||||
minute: '2-digit'
|
hour: '2-digit',
|
||||||
})}
|
minute: '2-digit'
|
||||||
</time>
|
})}
|
||||||
|
</time>
|
||||||
|
{comic.publish_at && (
|
||||||
|
<div class="countdown-timer text-xs" data-publish-at={comic.publish_at}>
|
||||||
|
Publishing in: <span class="font-medium text-accent">calculating...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<span class="group-hover:text-accent transition-colors duration-300">Read Now →</span>
|
<span class="group-hover:text-accent transition-colors duration-300">Read Now →</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -176,5 +183,41 @@ const isNSFW = comic.md_comics.content_rating === 'erotica';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add countdown timer functionality
|
||||||
|
const countdownTimers = document.querySelectorAll('.countdown-timer');
|
||||||
|
|
||||||
|
function updateTimer(element: Element) {
|
||||||
|
const publishAt = new Date(element.getAttribute('data-publish-at') || '').getTime();
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const distance = publishAt - now;
|
||||||
|
|
||||||
|
const timerSpan = element.querySelector('span');
|
||||||
|
if (!timerSpan) return;
|
||||||
|
|
||||||
|
if (distance < 0) {
|
||||||
|
timerSpan.textContent = 'Published';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const days = Math.floor(distance / (1000 * 60 * 60 * 24));
|
||||||
|
const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||||
|
const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
const seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
||||||
|
|
||||||
|
let timeString = '';
|
||||||
|
if (days > 0) timeString += `${days}d `;
|
||||||
|
if (hours > 0 || days > 0) timeString += `${hours}h `;
|
||||||
|
if (minutes > 0 || hours > 0 || days > 0) timeString += `${minutes}m `;
|
||||||
|
timeString += `${seconds}s`;
|
||||||
|
|
||||||
|
timerSpan.textContent = timeString;
|
||||||
|
}
|
||||||
|
|
||||||
|
countdownTimers.forEach(timer => {
|
||||||
|
updateTimer(timer);
|
||||||
|
// Update every second
|
||||||
|
setInterval(() => updateTimer(timer), 1000);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -6,69 +6,73 @@ interface Props {
|
||||||
const { currentSlugs = [] } = Astro.props;
|
const { currentSlugs = [] } = Astro.props;
|
||||||
|
|
||||||
import { availableLanguages, languageMap } from '../lib/api';
|
import { availableLanguages, languageMap } from '../lib/api';
|
||||||
|
|
||||||
|
const languages = [
|
||||||
|
{ value: 'en', label: 'English' },
|
||||||
|
{ value: 'ja', label: 'Japanese' },
|
||||||
|
{ value: 'ko', label: 'Korean' },
|
||||||
|
{ value: 'zh', label: 'Chinese' },
|
||||||
|
{ value: 'fr', label: 'French' },
|
||||||
|
{ value: 'es', label: 'Spanish' },
|
||||||
|
{ value: 'it', label: 'Italian' },
|
||||||
|
{ value: 'de', label: 'German' },
|
||||||
|
{ value: 'tr', label: 'Turkish' },
|
||||||
|
{ value: 'pl', label: 'Polish' },
|
||||||
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-6">
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="flex flex-col gap-4">
|
||||||
<label for="slug-input" class="text-sm font-medium text-primary">
|
|
||||||
Add Comic Slug
|
|
||||||
<span class="text-primary/60">(e.g., "01-one-piece" from https://comick.io/comic/01-one-piece)</span>
|
|
||||||
</label>
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<input
|
<div class="flex-1">
|
||||||
type="text"
|
<input
|
||||||
id="slug-input"
|
type="text"
|
||||||
placeholder="Enter comic slug..."
|
id="search-input"
|
||||||
class="flex-1 px-4 py-2 rounded-md bg-secondary text-primary border border-hover focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent transition-colors"
|
placeholder="Search for comics..."
|
||||||
/>
|
class="w-full px-4 py-2 bg-primary rounded-lg border border-hover/30 focus:outline-none focus:border-accent/50 transition-colors"
|
||||||
<button
|
/>
|
||||||
id="add-slug"
|
</div>
|
||||||
class="px-4 py-2 bg-accent text-secondary hover:bg-accent-light rounded-md transition-colors font-medium"
|
<select
|
||||||
|
id="lang-select"
|
||||||
|
class="px-4 py-2 bg-primary rounded-lg border border-hover/30 focus:outline-none focus:border-accent/50 transition-colors"
|
||||||
>
|
>
|
||||||
Add
|
{languages.map(lang => (
|
||||||
</button>
|
<option value={lang.value}>{lang.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col space-y-2">
|
<div id="search-results" class="hidden space-y-2">
|
||||||
<label for="lang-select" class="text-sm font-medium text-primary">
|
<!-- Search results will be populated here -->
|
||||||
Select Language
|
</div>
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="lang-select"
|
|
||||||
class="px-4 py-2 rounded-md bg-secondary text-primary border border-hover focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent transition-colors"
|
|
||||||
>
|
|
||||||
{availableLanguages.map(lang => (
|
|
||||||
<option value={lang}>
|
|
||||||
{languageMap[lang as keyof typeof languageMap]}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</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 px-4 py-2 bg-secondary rounded-md group" data-slug={slug}>
|
<div class="flex items-center justify-between p-3 bg-primary rounded-lg border border-hover/30" data-slug={slug}>
|
||||||
<span class="text-primary">{slug}</span>
|
<span class="text-primary">{slug}</span>
|
||||||
<button
|
<button
|
||||||
class="text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
|
class="text-red-500 hover:text-red-600 transition-colors"
|
||||||
data-remove={slug}
|
data-remove={slug}
|
||||||
>
|
>
|
||||||
Remove
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-4">
|
<div class="flex justify-center">
|
||||||
<a
|
<a
|
||||||
id="generate-rss"
|
id="generate-rss"
|
||||||
href="#"
|
href="/rss/custom/"
|
||||||
class="inline-flex items-center gap-2 px-4 py-2 bg-accent text-secondary hover:bg-accent-light rounded-md transition-colors font-medium"
|
class="inline-flex items-center gap-2 px-6 py-3 bg-[rgb(var(--accent-bg))] text-[rgb(var(--accent-text))] rounded-lg hover:bg-[rgb(var(--accent-light))] transition-colors"
|
||||||
|
onclick="return !!this.href.includes(',')"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="w-5 h-5"
|
class="w-5 h-5"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
|
@ -86,8 +90,8 @@ import { availableLanguages, languageMap } from '../lib/api';
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const slugInput = document.getElementById('slug-input') as HTMLInputElement;
|
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||||
const addButton = document.getElementById('add-slug');
|
const searchResults = document.getElementById('search-results');
|
||||||
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;
|
||||||
|
|
@ -98,84 +102,276 @@ import { availableLanguages, languageMap } from '../lib/api';
|
||||||
function updateSlugs() {
|
function updateSlugs() {
|
||||||
localStorage.setItem('comic-slugs', JSON.stringify([...slugs]));
|
localStorage.setItem('comic-slugs', JSON.stringify([...slugs]));
|
||||||
updateRSSLink();
|
updateRSSLink();
|
||||||
|
renderSlugs();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateRSSLink() {
|
async function renderSlugs() {
|
||||||
|
if (!slugList) return;
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
slugList.innerHTML = `
|
||||||
|
<div class="animate-pulse space-y-2">
|
||||||
|
${[...slugs].map(() => `
|
||||||
|
<div class="flex items-center gap-4 p-3 bg-primary rounded-lg border border-hover/30">
|
||||||
|
<div class="w-12 h-16 bg-hover/30 rounded"></div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="h-5 bg-hover/30 rounded w-3/4 mb-2"></div>
|
||||||
|
<div class="h-4 bg-hover/30 rounded w-1/2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const promises = [...slugs].map(async (slug) => {
|
||||||
|
try {
|
||||||
|
// Use our API endpoint instead of direct ComicK API call
|
||||||
|
const response = await fetch(`/api/comic/${encodeURIComponent(slug)}`);
|
||||||
|
if (!response.ok) throw new Error('Comic not found');
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'flex items-center gap-4 p-4 bg-primary rounded-lg border border-hover/30 hover:border-accent/50 transition-all duration-300 group relative';
|
||||||
|
|
||||||
|
// Add a link to the comic
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = `https://comick.io/comic/${slug}`;
|
||||||
|
link.target = '_blank';
|
||||||
|
link.rel = 'noopener noreferrer';
|
||||||
|
link.className = 'flex items-center gap-4 flex-1';
|
||||||
|
|
||||||
|
// Handle cover image
|
||||||
|
if (data.comic?.md_covers?.[0]?.b2key) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = `https://meo.comick.pictures/${data.comic.md_covers[0].b2key}`;
|
||||||
|
img.className = 'w-16 h-24 object-cover rounded shadow-md group-hover:scale-105 transition-transform duration-300';
|
||||||
|
img.alt = data.comic.title || slug;
|
||||||
|
img.onerror = () => {
|
||||||
|
img.src = '/placeholder.png'; // Fallback image
|
||||||
|
img.onerror = null;
|
||||||
|
};
|
||||||
|
link.appendChild(img);
|
||||||
|
} else {
|
||||||
|
// Placeholder if no image
|
||||||
|
const placeholder = document.createElement('div');
|
||||||
|
placeholder.className = 'w-16 h-24 bg-hover/30 rounded flex items-center justify-center text-primary/30';
|
||||||
|
placeholder.innerHTML = '<svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>';
|
||||||
|
link.appendChild(placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = document.createElement('div');
|
||||||
|
info.className = 'flex-1 min-w-0';
|
||||||
|
info.innerHTML = `
|
||||||
|
<div class="font-medium text-lg truncate group-hover:text-accent transition-colors">
|
||||||
|
${data.comic?.title || slug}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-primary/60 truncate">${slug}</div>
|
||||||
|
${data.comic?.last_chapter ? `
|
||||||
|
<div class="text-xs text-accent mt-1">
|
||||||
|
Latest: Chapter ${data.comic.last_chapter}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
link.appendChild(info);
|
||||||
|
div.appendChild(link);
|
||||||
|
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.className = 'text-red-500 hover:text-red-600 transition-colors p-2 hover:bg-red-500/10 rounded-full flex-shrink-0 relative z-10';
|
||||||
|
removeBtn.innerHTML = `
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
removeBtn.onclick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
div.classList.add('scale-95', 'opacity-0');
|
||||||
|
setTimeout(() => {
|
||||||
|
slugs.delete(slug);
|
||||||
|
updateSlugs();
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
div.appendChild(removeBtn);
|
||||||
|
return div;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to fetch comic info for ${slug}:`, error);
|
||||||
|
// Improved error state
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'flex items-center justify-between p-4 bg-primary rounded-lg border border-red-500/30 group';
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="flex items-center gap-4 flex-1">
|
||||||
|
<div class="w-16 h-24 bg-red-500/10 rounded flex items-center justify-center text-red-500/50">
|
||||||
|
<svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
|
<path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-red-500 font-medium">Failed to load details</div>
|
||||||
|
<div class="text-sm text-primary/60">${slug}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="text-red-500 hover:text-red-600 transition-colors p-2 hover:bg-red-500/10 rounded-full"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const removeButton = div.querySelector('button');
|
||||||
|
if (removeButton) {
|
||||||
|
removeButton.onclick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
div.classList.add('scale-95', 'opacity-0');
|
||||||
|
setTimeout(() => {
|
||||||
|
slugs.delete(slug);
|
||||||
|
updateSlugs();
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(promises).then(elements => {
|
||||||
|
slugList.innerHTML = '';
|
||||||
|
elements.forEach(element => slugList.appendChild(element));
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Failed to render slugs:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchTimeout: NodeJS.Timeout;
|
||||||
|
searchInput?.addEventListener('input', () => {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
searchTimeout = setTimeout(async () => {
|
||||||
|
const query = searchInput.value.trim();
|
||||||
|
if (query.length < 2) {
|
||||||
|
searchResults!.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
searchResults!.innerHTML = `
|
||||||
|
<div class="p-3 bg-primary rounded-lg border border-hover/30">
|
||||||
|
Searching...
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
searchResults!.classList.remove('hidden');
|
||||||
|
|
||||||
|
const response = await fetch('/api/search', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ query })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data || !Array.isArray(data) || data.length === 0) {
|
||||||
|
searchResults!.innerHTML = `
|
||||||
|
<div class="p-3 bg-primary rounded-lg border border-hover/30">
|
||||||
|
No comics found
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchResults!.innerHTML = '';
|
||||||
|
|
||||||
|
data.forEach((comic: any) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'flex items-center gap-3 p-3 bg-primary rounded-lg border border-hover/30 cursor-pointer hover:border-accent/50 transition-colors';
|
||||||
|
div.onclick = () => {
|
||||||
|
slugs.add(comic.slug);
|
||||||
|
updateSlugs();
|
||||||
|
searchInput.value = '';
|
||||||
|
searchResults!.classList.add('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (comic.md_covers && comic.md_covers[0]) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = `https://meo.comick.pictures/${comic.md_covers[0].b2key}`;
|
||||||
|
img.className = 'w-12 h-16 object-cover rounded';
|
||||||
|
img.alt = comic.title;
|
||||||
|
div.appendChild(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = document.createElement('div');
|
||||||
|
info.innerHTML = `
|
||||||
|
<div class="font-medium">${comic.title}</div>
|
||||||
|
<div class="text-sm text-primary/60">${comic.slug}</div>
|
||||||
|
`;
|
||||||
|
div.appendChild(info);
|
||||||
|
|
||||||
|
searchResults!.appendChild(div);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to search comics:', error);
|
||||||
|
searchResults!.innerHTML = `
|
||||||
|
<div class="p-3 bg-primary rounded-lg border border-red-500/30 text-red-500">
|
||||||
|
Failed to search comics. Please try again.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function updateRSSLink() {
|
||||||
const slugString = [...slugs].join(',');
|
const slugString = [...slugs].join(',');
|
||||||
const lang = langSelect?.value || 'en';
|
const lang = langSelect?.value || 'en';
|
||||||
|
|
||||||
// Call the API to get a short feed ID
|
if (slugString) {
|
||||||
fetch('/api/create-feed', {
|
try {
|
||||||
method: 'POST',
|
const response = await fetch('/api/create-feed', {
|
||||||
headers: {
|
method: 'POST',
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
},
|
'Content-Type': 'application/json',
|
||||||
body: JSON.stringify({
|
},
|
||||||
slugs: [...slugs],
|
body: JSON.stringify({
|
||||||
lang
|
slugs: [...slugs],
|
||||||
})
|
lang
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
});
|
||||||
.then(data => {
|
|
||||||
generateRSSLink!.setAttribute('href', `/rss/${data.feedId}.xml`);
|
if (!response.ok) throw new Error('Failed to create feed');
|
||||||
})
|
|
||||||
.catch(error => {
|
const { feedId } = await response.json();
|
||||||
console.error('Error generating short URL:', error);
|
generateRSSLink!.href = `/rss/${feedId}.xml`;
|
||||||
});
|
generateRSSLink!.onclick = null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to generate feed:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
generateRSSLink!.href = '#';
|
||||||
|
generateRSSLink!.onclick = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSlugList() {
|
// Initial render
|
||||||
if (!slugList) return;
|
renderSlugs();
|
||||||
|
|
||||||
slugList.innerHTML = '';
|
|
||||||
[...slugs].forEach(slug => {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.className = 'flex items-center justify-between px-4 py-2 bg-secondary rounded-md group';
|
|
||||||
div.dataset.slug = slug;
|
|
||||||
div.innerHTML = `
|
|
||||||
<span class="text-primary">${slug}</span>
|
|
||||||
<button
|
|
||||||
class="text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
||||||
data-remove="${slug}"
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
slugList.appendChild(div);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSlug(slug: string) {
|
|
||||||
const sanitizedSlug = slug.trim().toLowerCase();
|
|
||||||
if (sanitizedSlug && !slugs.has(sanitizedSlug)) {
|
|
||||||
slugs.add(sanitizedSlug);
|
|
||||||
updateSlugs();
|
|
||||||
renderSlugList();
|
|
||||||
}
|
|
||||||
slugInput.value = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
addButton?.addEventListener('click', () => addSlug(slugInput.value));
|
|
||||||
|
|
||||||
slugInput?.addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
addSlug(slugInput.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
slugList?.addEventListener('click', (e) => {
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
const slug = target.dataset.remove;
|
|
||||||
if (slug) {
|
|
||||||
slugs.delete(slug);
|
|
||||||
updateSlugs();
|
|
||||||
renderSlugList();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
langSelect?.addEventListener('change', updateRSSLink);
|
|
||||||
|
|
||||||
// Initialize RSS link and render initial list
|
|
||||||
updateRSSLink();
|
updateRSSLink();
|
||||||
renderSlugList();
|
|
||||||
|
// Update RSS link when language changes
|
||||||
|
langSelect?.addEventListener('change', () => updateRSSLink());
|
||||||
|
|
||||||
|
// Close search results when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!searchResults?.contains(e.target as Node) && !searchInput?.contains(e.target as Node)) {
|
||||||
|
searchResults?.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
---
|
---
|
||||||
<button
|
<button
|
||||||
id="theme-toggle"
|
id="theme-toggle"
|
||||||
class="fixed top-4 right-4 z-50 p-2 w-10 h-10 rounded-full bg-secondary hover:bg-hover flex items-center justify-center transition-colors"
|
class="p-2 w-10 h-10 rounded-full bg-secondary hover:bg-hover flex items-center justify-center transition-colors"
|
||||||
aria-label="Toggle theme"
|
aria-label="Toggle theme"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ const types = [
|
||||||
href={`/${lang}/${contentType}/${type.id}`}
|
href={`/${lang}/${contentType}/${type.id}`}
|
||||||
class={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
class={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
currentType === type.id
|
currentType === type.id
|
||||||
? 'bg-accent text-white'
|
? 'bg-[rgb(var(--accent-bg))] text-[rgb(var(--accent-text))]'
|
||||||
: 'bg-secondary text-primary/80 hover:bg-hover'
|
: 'bg-secondary text-primary hover:bg-hover'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{type.label}
|
{type.label}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
---
|
---
|
||||||
import ThemeToggle from '../components/ThemeToggle.astro';
|
import ThemeToggle from '../components/ThemeToggle.astro';
|
||||||
|
import Analytics from '@vercel/analytics/astro';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
|
|
@ -12,6 +13,7 @@ const {
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
const canonicalURL = new URL(Astro.url.pathname, Astro.site ?? 'https://comick-rss.vercel.app').toString();
|
const canonicalURL = new URL(Astro.url.pathname, Astro.site ?? 'https://comick-rss.vercel.app').toString();
|
||||||
|
const isCreditsPage = Astro.url.pathname === '/credits';
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
|
@ -41,12 +43,134 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site ?? 'https://comick-r
|
||||||
|
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen bg-primary text-primary">
|
<body class="min-h-screen bg-primary text-primary before:fixed before:inset-0 before:bg-[url('https://cdn.donmai.us/original/97/c8/__citlali_genshin_impact_drawn_by_ryrmcher__97c810ff6e992f1789f93e2edbb683a3.jpg')] before:bg-cover before:bg-center before:bg-no-repeat before:opacity-30 before:-z-10 before:transition-all before:duration-300" id="body-element">
|
||||||
<ThemeToggle />
|
{!isCreditsPage && (
|
||||||
<slot />
|
<div class="fixed top-4 right-4 z-50">
|
||||||
|
<div class="relative" id="settings-dropdown">
|
||||||
|
<button
|
||||||
|
id="settings-button"
|
||||||
|
class="p-2 bg-secondary/80 backdrop-blur rounded-lg hover:bg-hover transition-colors shadow-lg border border-hover/30 flex items-center gap-2"
|
||||||
|
aria-label="Settings"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M12 15a3 3 0 100-6 3 3 0 000 6z" />
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z" />
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm">Settings</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="dropdown-menu" class="hidden absolute right-0 mt-2 w-64 bg-secondary/95 backdrop-blur rounded-lg shadow-lg border border-hover/30">
|
||||||
|
<div class="p-4 space-y-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm">Theme</span>
|
||||||
|
<button
|
||||||
|
id="theme-toggle"
|
||||||
|
class="relative w-12 h-6 rounded-full bg-hover transition-colors"
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
>
|
||||||
|
<span class="absolute left-1 top-1 w-4 h-4 rounded-full bg-primary transition-transform duration-200"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm">Content Visibility</span>
|
||||||
|
<button
|
||||||
|
id="toggle-content"
|
||||||
|
class="relative w-12 h-6 rounded-full bg-hover transition-colors"
|
||||||
|
aria-label="Toggle content visibility"
|
||||||
|
>
|
||||||
|
<span class="absolute left-1 top-1 w-4 h-4 rounded-full bg-primary transition-transform duration-200"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-hover/30 pt-4">
|
||||||
|
<a
|
||||||
|
href="/credits"
|
||||||
|
class="flex items-center gap-2 text-sm hover:text-accent transition-colors"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
||||||
|
</svg>
|
||||||
|
Credits
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<main id="main-content" class="transition-opacity duration-300">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<Analytics />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const settingsButton = document.getElementById('settings-button');
|
||||||
|
const dropdownMenu = document.getElementById('dropdown-menu');
|
||||||
|
const themeToggle = document.getElementById('theme-toggle');
|
||||||
|
const toggleContent = document.getElementById('toggle-content');
|
||||||
|
const mainContent = document.getElementById('main-content');
|
||||||
|
const bodyElement = document.getElementById('body-element');
|
||||||
|
|
||||||
|
// Theme toggle functionality
|
||||||
|
const updateThemeToggle = (theme: string) => {
|
||||||
|
const toggleButton = themeToggle?.querySelector('span');
|
||||||
|
if (toggleButton) {
|
||||||
|
toggleButton.style.transform = theme === 'dark' ? 'translateX(24px)' : 'translateX(0)';
|
||||||
|
}
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Content visibility functionality
|
||||||
|
let isVisible = true;
|
||||||
|
const updateContentVisibility = () => {
|
||||||
|
const toggleButton = toggleContent?.querySelector('span');
|
||||||
|
if (toggleButton) {
|
||||||
|
toggleButton.style.transform = isVisible ? 'translateX(0)' : 'translateX(24px)';
|
||||||
|
}
|
||||||
|
if (mainContent) {
|
||||||
|
mainContent.style.opacity = isVisible ? '1' : '0';
|
||||||
|
mainContent.style.pointerEvents = isVisible ? 'auto' : 'none';
|
||||||
|
}
|
||||||
|
if (bodyElement) {
|
||||||
|
bodyElement.style.setProperty('--tw-blur', isVisible ? 'blur(2px)' : 'blur(0)');
|
||||||
|
bodyElement.style.setProperty('--tw-bg-opacity', isVisible ? '0.3' : '1');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize theme
|
||||||
|
const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null;
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
|
||||||
|
updateThemeToggle(initialTheme);
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
settingsButton?.addEventListener('click', () => {
|
||||||
|
dropdownMenu?.classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
themeToggle?.addEventListener('click', () => {
|
||||||
|
const currentTheme = document.documentElement.getAttribute('data-theme');
|
||||||
|
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
|
||||||
|
updateThemeToggle(newTheme);
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleContent?.addEventListener('click', () => {
|
||||||
|
isVisible = !isVisible;
|
||||||
|
updateContentVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!settingsButton?.contains(e.target as Node) && !dropdownMenu?.contains(e.target as Node)) {
|
||||||
|
dropdownMenu?.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<style is:global>
|
<style is:global>
|
||||||
:root {
|
:root {
|
||||||
--accent: 136, 58, 234;
|
--accent: 136, 58, 234;
|
||||||
|
|
@ -66,6 +190,11 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site ?? 'https://comick-r
|
||||||
--bg-secondary: #f0f0f0;
|
--bg-secondary: #f0f0f0;
|
||||||
--bg-hover: #e0e0e0;
|
--bg-hover: #e0e0e0;
|
||||||
--text-color: #13151a;
|
--text-color: #13151a;
|
||||||
|
--accent: 89, 0, 255; /* Brighter, more saturated purple */
|
||||||
|
--accent-light: 124, 58, 237;
|
||||||
|
--accent-dark: 76, 29, 149;
|
||||||
|
--accent-bg: 89, 0, 255; /* Same as accent for bg */
|
||||||
|
--accent-text: 255, 255, 255; /* White text for contrast */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme */
|
/* Dark theme */
|
||||||
|
|
@ -74,6 +203,11 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site ?? 'https://comick-r
|
||||||
--bg-secondary: #23262d;
|
--bg-secondary: #23262d;
|
||||||
--bg-hover: #2a2d35;
|
--bg-hover: #2a2d35;
|
||||||
--text-color: #ffffff;
|
--text-color: #ffffff;
|
||||||
|
--accent: 136, 58, 234;
|
||||||
|
--accent-light: 224, 204, 250;
|
||||||
|
--accent-dark: 49, 10, 101;
|
||||||
|
--accent-bg: 136, 58, 234; /* Same as accent for bg */
|
||||||
|
--accent-text: 255, 255, 255; /* White text for contrast */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Default to dark theme if no data-theme is set */
|
/* Default to dark theme if no data-theme is set */
|
||||||
|
|
@ -82,6 +216,11 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site ?? 'https://comick-r
|
||||||
--bg-secondary: #23262d;
|
--bg-secondary: #23262d;
|
||||||
--bg-hover: #2a2d35;
|
--bg-hover: #2a2d35;
|
||||||
--text-color: #ffffff;
|
--text-color: #ffffff;
|
||||||
|
--accent: 136, 58, 234;
|
||||||
|
--accent-light: 224, 204, 250;
|
||||||
|
--accent-dark: 49, 10, 101;
|
||||||
|
--accent-bg: 136, 58, 234;
|
||||||
|
--accent-text: 255, 255, 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,8 @@ const languageMap = {
|
||||||
"zh": "Chinese",
|
"zh": "Chinese",
|
||||||
"sv": "Swedish",
|
"sv": "Swedish",
|
||||||
"ar": "Arabic",
|
"ar": "Arabic",
|
||||||
"de": "German"
|
"de": "German",
|
||||||
|
"ko": "Korean"
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const languages = new Set(Object.keys(languageMap));
|
const languages = new Set(Object.keys(languageMap));
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,8 @@ const title = `ComicK - ${getLanguageName(lang!)} ${contentType?.toUpperCase()}
|
||||||
href={`/${lang}/sfw/${type}`}
|
href={`/${lang}/sfw/${type}`}
|
||||||
class={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
class={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
contentType === 'sfw'
|
contentType === 'sfw'
|
||||||
? 'bg-accent text-white'
|
? 'bg-[rgb(var(--accent-bg))] text-[rgb(var(--accent-text))]'
|
||||||
: 'bg-secondary text-primary/80 hover:bg-hover'
|
: 'bg-secondary text-primary hover:bg-hover'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
SFW
|
SFW
|
||||||
|
|
@ -54,8 +54,8 @@ const title = `ComicK - ${getLanguageName(lang!)} ${contentType?.toUpperCase()}
|
||||||
href={`/${lang}/nsfw/${type}`}
|
href={`/${lang}/nsfw/${type}`}
|
||||||
class={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
class={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
contentType === 'nsfw'
|
contentType === 'nsfw'
|
||||||
? 'bg-accent text-white'
|
? 'bg-[rgb(var(--accent-bg))] text-[rgb(var(--accent-text))]'
|
||||||
: 'bg-secondary text-primary/80 hover:bg-hover'
|
: 'bg-secondary text-primary hover:bg-hover'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
NSFW
|
NSFW
|
||||||
|
|
|
||||||
31
src/pages/api/comic/[slug].ts
Normal file
31
src/pages/api/comic/[slug].ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import type { APIRoute } from "astro";
|
||||||
|
|
||||||
|
export const GET: APIRoute = async ({ params }) => {
|
||||||
|
const { slug } = params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.comick.fun/comic/${encodeURIComponent(slug!)}/`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Comic fetch error:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Failed to fetch comic details' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
31
src/pages/api/search.ts
Normal file
31
src/pages/api/search.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import type { APIRoute } from "astro";
|
||||||
|
|
||||||
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
|
const { query } = await request.json();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.comick.io/v1.0/search/?page=1&limit=5&q=${encodeURIComponent(query)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return new Response(JSON.stringify(data), {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Search error:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Failed to search comics' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
152
src/pages/credits.astro
Normal file
152
src/pages/credits.astro
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
---
|
||||||
|
import Layout from '../layouts/Layout.astro';
|
||||||
|
|
||||||
|
const title = "ComicK-RSS - Credits";
|
||||||
|
const description = "Credits and attributions for ComicK-RSS";
|
||||||
|
|
||||||
|
const credits = [
|
||||||
|
{
|
||||||
|
title: "Background Art",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: "Citlali (Genshin Impact)",
|
||||||
|
artist: "ryrmcher",
|
||||||
|
link: "https://danbooru.donmai.us/posts/6148600",
|
||||||
|
description: "Beautiful artwork used as the site background",
|
||||||
|
socials: [
|
||||||
|
{
|
||||||
|
name: "Twitter Post",
|
||||||
|
link: "https://x.com/ryrmcher/status/1878812100866560258"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Artist's Twitter",
|
||||||
|
link: "https://x.com/ryrmcher"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Technologies",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: "Astro",
|
||||||
|
description: "The modern web framework powering this site",
|
||||||
|
link: "https://astro.build"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Tailwind CSS",
|
||||||
|
description: "Utility-first CSS framework for styling",
|
||||||
|
link: "https://tailwindcss.com"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Data",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: "ComicK",
|
||||||
|
description: "For providing the comic data and images through their API",
|
||||||
|
link: "https://comick.io"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout title={title} description={description}>
|
||||||
|
<main class="mx-auto px-4 sm:px-6 lg:px-8 max-w-[800px] py-12">
|
||||||
|
<div class="mb-8">
|
||||||
|
<a
|
||||||
|
href="/en/sfw/all"
|
||||||
|
class="inline-flex items-center gap-2 text-primary hover:text-accent transition-colors"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-5 h-5"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||||
|
</svg>
|
||||||
|
<span>Back to Home</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="text-5xl font-bold text-center mb-12 text-primary">
|
||||||
|
Credits
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="space-y-12">
|
||||||
|
{credits.map((section) => (
|
||||||
|
<section class="bg-secondary/50 p-8 rounded-xl shadow-lg backdrop-blur-sm border border-hover/20">
|
||||||
|
<h2 class="text-3xl font-bold mb-6 text-accent">{section.title}</h2>
|
||||||
|
|
||||||
|
<div class="space-y-6">
|
||||||
|
{section.items.map((item) => (
|
||||||
|
<div class="border-l-4 border-accent/30 pl-6 py-2 hover:border-accent transition-colors">
|
||||||
|
<h3 class="font-medium text-xl mb-2">
|
||||||
|
<a
|
||||||
|
href={item.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-primary hover:text-accent transition-colors inline-flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 opacity-50"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||||
|
<polyline points="15 3 21 3 21 9" />
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
{item.artist && (
|
||||||
|
<p class="text-primary/70 mb-1">
|
||||||
|
Artist: <span class="text-accent">{item.artist}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{item.description && (
|
||||||
|
<p class="text-primary/60 leading-relaxed">{item.description}</p>
|
||||||
|
)}
|
||||||
|
{item.socials && (
|
||||||
|
<div class="flex gap-4 mt-2">
|
||||||
|
{item.socials.map((social) => (
|
||||||
|
<a
|
||||||
|
href={social.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-sm text-accent/70 hover:text-accent transition-colors inline-flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||||
|
</svg>
|
||||||
|
{social.name}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-12 text-center text-primary/60">
|
||||||
|
<p class="text-lg">
|
||||||
|
Thank you to all the creators, artists, and contributors to other projects who made this project possible! ✨
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</Layout>
|
||||||
|
|
@ -15,8 +15,8 @@ const description = "Create a custom RSS feed for your favorite comics";
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-8 text-center text-primary/60">
|
<div class="mt-8 text-center text-primary/60">
|
||||||
<p>Add the comic slugs from the URLs of your favorite comics on ComicK.</p>
|
<p>Search for your favorite comics using the search box above, or add their slugs manually.</p>
|
||||||
<p> Example: For <a href="https://comick.io/comic/01-one-piece">https://comick.io/comic/01-one-piece</a>, the slug is "01-one-piece"</p>
|
<p class="mt-2">Manual example: For <a href="https://comick.io/comic/01-one-piece" class="text-accent hover:underline">https://comick.io/comic/01-one-piece</a>, the slug is "01-one-piece"</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue