Compare commits

..

No commits in common. "3a9bb53ae79b81404ef47ed3d59df1bf9b1f3f17" and "23c62495ebbc9bbb9098131cb134dee7e9b3d5e0" have entirely different histories.

13 changed files with 138 additions and 733 deletions

1
package-lock.json generated
View file

@ -12,7 +12,6 @@
"@astrojs/tailwind": "^5.1.0",
"@astrojs/vercel": "^8.0.2",
"@floating-ui/dom": "^1.6.3",
"@vercel/analytics": "^1.4.1",
"@vercel/kv": "^1.0.1",
"astro": "^5.1.6",
"ioredis": "^5.4.2",

View file

@ -15,7 +15,6 @@
"@astrojs/tailwind": "^5.1.0",
"@astrojs/vercel": "^8.0.2",
"@floating-ui/dom": "^1.6.3",
"@vercel/analytics": "^1.4.1",
"@vercel/kv": "^1.0.1",
"astro": "^5.1.6",
"ioredis": "^5.4.2",

View file

@ -97,21 +97,14 @@ const isNSFW = comic.md_comics.content_rating === 'erotica';
</div>
<div class="mt-auto pt-4 flex items-center justify-between text-sm text-primary/60 border-t border-hover/30">
<div class="flex flex-col gap-1">
<time datetime={comic.updated_at}>
{new Date(comic.updated_at).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</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>
<time datetime={comic.updated_at}>
{new Date(comic.updated_at).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</time>
<span class="group-hover:text-accent transition-colors duration-300">Read Now →</span>
</div>
</div>
@ -183,41 +176,5 @@ 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>

View file

@ -6,73 +6,69 @@ interface Props {
const { currentSlugs = [] } = Astro.props;
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-6">
<div class="flex flex-col gap-4">
<div class="space-y-4">
<div class="flex flex-col space-y-2">
<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-1">
<input
type="text"
id="search-input"
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"
/>
</div>
<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"
<input
type="text"
id="slug-input"
placeholder="Enter comic slug..."
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"
/>
<button
id="add-slug"
class="px-4 py-2 bg-accent text-secondary hover:bg-accent-light rounded-md transition-colors font-medium"
>
{languages.map(lang => (
<option value={lang.value}>{lang.label}</option>
))}
</select>
Add
</button>
</div>
</div>
<div id="search-results" class="hidden space-y-2">
<!-- Search results will be populated here -->
</div>
<div class="flex flex-col space-y-2">
<label for="lang-select" class="text-sm font-medium text-primary">
Select Language
</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 id="slug-list" class="space-y-2">
{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 px-4 py-2 bg-secondary rounded-md group" data-slug={slug}>
<span class="text-primary">{slug}</span>
<button
class="text-red-500 hover:text-red-600 transition-colors"
class="text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
data-remove={slug}
>
<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>
Remove
</button>
</div>
))}
</div>
<div class="flex justify-center">
<div class="pt-4">
<a
id="generate-rss"
href="/rss/custom/"
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(',')"
href="#"
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"
>
<svg
class="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@ -90,8 +86,8 @@ const languages = [
</div>
<script>
const searchInput = document.getElementById('search-input') as HTMLInputElement;
const searchResults = document.getElementById('search-results');
const slugInput = document.getElementById('slug-input') as HTMLInputElement;
const addButton = document.getElementById('add-slug');
const slugList = document.getElementById('slug-list');
const generateRSSLink = document.getElementById('generate-rss');
const langSelect = document.getElementById('lang-select') as HTMLSelectElement;
@ -102,276 +98,84 @@ const languages = [
function updateSlugs() {
localStorage.setItem('comic-slugs', JSON.stringify([...slugs]));
updateRSSLink();
renderSlugs();
}
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() {
function updateRSSLink() {
const slugString = [...slugs].join(',');
const lang = langSelect?.value || 'en';
if (slugString) {
try {
const response = await fetch('/api/create-feed', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
slugs: [...slugs],
lang
})
});
if (!response.ok) throw new Error('Failed to create feed');
const { feedId } = await response.json();
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;
};
}
// Call the API to get a short feed ID
fetch('/api/create-feed', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
slugs: [...slugs],
lang
})
})
.then(response => response.json())
.then(data => {
generateRSSLink!.setAttribute('href', `/rss/${data.feedId}.xml`);
})
.catch(error => {
console.error('Error generating short URL:', error);
});
}
// Initial render
renderSlugs();
updateRSSLink();
function renderSlugList() {
if (!slugList) return;
// Update RSS link when language changes
langSelect?.addEventListener('change', () => updateRSSLink());
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);
});
}
// 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');
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();
renderSlugList();
</script>

View file

@ -2,7 +2,7 @@
---
<button
id="theme-toggle"
class="p-2 w-10 h-10 rounded-full bg-secondary hover:bg-hover flex items-center justify-center transition-colors"
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"
aria-label="Toggle theme"
>
<svg

View file

@ -21,8 +21,8 @@ const types = [
href={`/${lang}/${contentType}/${type.id}`}
class={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
currentType === type.id
? 'bg-[rgb(var(--accent-bg))] text-[rgb(var(--accent-text))]'
: 'bg-secondary text-primary hover:bg-hover'
? 'bg-accent text-white'
: 'bg-secondary text-primary/80 hover:bg-hover'
}`}
>
{type.label}

View file

@ -1,6 +1,5 @@
---
import ThemeToggle from '../components/ThemeToggle.astro';
import Analytics from '@vercel/analytics/astro';
interface Props {
title: string;
@ -13,7 +12,6 @@ const {
} = Astro.props;
const canonicalURL = new URL(Astro.url.pathname, Astro.site ?? 'https://comick-rss.vercel.app').toString();
const isCreditsPage = Astro.url.pathname === '/credits';
---
<!doctype html>
@ -43,134 +41,12 @@ const isCreditsPage = Astro.url.pathname === '/credits';
<meta name="generator" content={Astro.generator} />
</head>
<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">
{!isCreditsPage && (
<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 class="min-h-screen bg-primary text-primary">
<ThemeToggle />
<slot />
</body>
</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>
:root {
--accent: 136, 58, 234;
@ -190,11 +66,6 @@ const isCreditsPage = Astro.url.pathname === '/credits';
--bg-secondary: #f0f0f0;
--bg-hover: #e0e0e0;
--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 */
@ -203,11 +74,6 @@ const isCreditsPage = Astro.url.pathname === '/credits';
--bg-secondary: #23262d;
--bg-hover: #2a2d35;
--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 */
@ -216,11 +82,6 @@ const isCreditsPage = Astro.url.pathname === '/credits';
--bg-secondary: #23262d;
--bg-hover: #2a2d35;
--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 {

View file

@ -11,8 +11,7 @@ const languageMap = {
"zh": "Chinese",
"sv": "Swedish",
"ar": "Arabic",
"de": "German",
"ko": "Korean"
"de": "German"
} as const;
const languages = new Set(Object.keys(languageMap));

View file

@ -44,8 +44,8 @@ const title = `ComicK - ${getLanguageName(lang!)} ${contentType?.toUpperCase()}
href={`/${lang}/sfw/${type}`}
class={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
contentType === 'sfw'
? 'bg-[rgb(var(--accent-bg))] text-[rgb(var(--accent-text))]'
: 'bg-secondary text-primary hover:bg-hover'
? 'bg-accent text-white'
: 'bg-secondary text-primary/80 hover:bg-hover'
}`}
>
SFW
@ -54,8 +54,8 @@ const title = `ComicK - ${getLanguageName(lang!)} ${contentType?.toUpperCase()}
href={`/${lang}/nsfw/${type}`}
class={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
contentType === 'nsfw'
? 'bg-[rgb(var(--accent-bg))] text-[rgb(var(--accent-text))]'
: 'bg-secondary text-primary hover:bg-hover'
? 'bg-accent text-white'
: 'bg-secondary text-primary/80 hover:bg-hover'
}`}
>
NSFW

View file

@ -1,31 +0,0 @@
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'
}
});
}
};

View file

@ -1,31 +0,0 @@
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'
}
});
}
};

View file

@ -1,152 +0,0 @@
---
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>

View file

@ -15,8 +15,8 @@ const description = "Create a custom RSS feed for your favorite comics";
</div>
<div class="mt-8 text-center text-primary/60">
<p>Search for your favorite comics using the search box above, or add their slugs manually.</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>
<p>Add the comic slugs from the URLs of your favorite comics on ComicK.</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>
</div>
</main>
</Layout>