Compare commits

..

No commits in common. "af61e4f32c924882cf458e99109e1471eebed2ef" and "17ceea25d97e34a43cb04044eb5a9a822c9ad7d0" have entirely different histories.

12 changed files with 1407 additions and 889 deletions

1945
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{ {
"name": "@ldez/manga", "name": "@ldez/manga",
"type": "module", "type": "module",
"version": "1.0.0", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
@ -12,14 +12,14 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/rss": "^4.0.11", "@astrojs/rss": "^4.0.11",
"@astrojs/tailwind": "^6.0.0", "@astrojs/tailwind": "^5.1.0",
"@astrojs/vercel": "^8.0.5", "@astrojs/vercel": "^8.0.2",
"@floating-ui/dom": "^1.6.13", "@floating-ui/dom": "^1.6.3",
"@vercel/analytics": "^1.4.1", "@vercel/analytics": "^1.4.1",
"@vercel/kv": "^3.0.0", "@vercel/kv": "^1.0.1",
"astro": "^5.2.3", "astro": "^5.1.6",
"feed": "^4.2.2", "feed": "^4.2.2",
"ioredis": "^5.4.2", "ioredis": "^5.4.2",
"tailwindcss": "^3.4.1" "tailwindcss": "^3.4.1"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

View file

@ -1,111 +0,0 @@
---
import type { Announcement } from '../lib/types';
interface Props {
announcement: Announcement;
}
const { announcement } = Astro.props;
const bgColorClass = {
info: 'bg-[rgb(var(--accent))] text-[rgb(var(--accent-text))]',
warning: 'bg-yellow-500 text-white',
success: 'bg-emerald-500 text-white'
}[announcement.type];
// Create a simple hash of the announcement content
const announcementHash = btoa(JSON.stringify({
message: announcement.message,
type: announcement.type,
link: announcement.link
}));
---
<div class={`announcement-banner hidden w-full ${bgColorClass} px-6 py-4 shadow-lg`}>
<div class="container mx-auto flex items-center justify-between">
<div class="flex items-center gap-3 text-lg">
{announcement.type === 'info' && (
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)}
{announcement.type === 'warning' && (
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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" />
</svg>
)}
{announcement.type === 'success' && (
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
)}
<span class="font-medium">{announcement.message}</span>
{announcement.link && (
<a
href={announcement.link.url}
class="underline hover:opacity-80 transition-opacity font-semibold ml-2"
>
{announcement.link.text} →
</a>
)}
</div>
{announcement.dismissible && (
<button
class="dismiss-announcement p-2 hover:bg-black/10 rounded-full transition-colors"
aria-label="Dismiss announcement"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
</div>
<script define:vars={{ dismissible: announcement.dismissible, announcementHash }}>
const banner = document.querySelector('.announcement-banner');
const dismissBtn = document.querySelector('.dismiss-announcement');
const storageKey = `announcement-dismissed-${announcementHash}`;
// Check if announcement should be shown
function checkVisibility() {
// Only check localStorage if the banner is dismissible
const isDismissed = dismissible ?
localStorage.getItem(storageKey) === 'true' :
false;
if (!isDismissed && banner) {
banner.classList.remove('hidden');
}
}
// Run after DOM is loaded
document.addEventListener('DOMContentLoaded', checkVisibility);
dismissBtn?.addEventListener('click', () => {
if (dismissible) {
banner?.classList.add('hidden');
localStorage.setItem(storageKey, 'true');
}
});
</script>
<style>
.announcement-banner {
animation: slideDown 0.5s ease-out;
}
@keyframes slideDown {
from {
transform: translateY(-100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
</style>

View file

@ -31,8 +31,6 @@ const isNSFW = comic.md_comics.content_rating === 'erotica';
alt={comic.md_comics.title} alt={comic.md_comics.title}
inferSize={true} inferSize={true}
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
loading="lazy"
decoding="async"
/> />
) : ( ) : (
<img <img
@ -54,10 +52,7 @@ const isNSFW = comic.md_comics.content_rating === 'erotica';
<div class="p-3 sm:p-4 md:p-5 flex-grow flex flex-col bg-secondary relative rounded-b-xl"> <div class="p-3 sm:p-4 md:p-5 flex-grow flex flex-col bg-secondary relative rounded-b-xl">
<div class="absolute -top-6 sm:-top-8 left-2 right-2 sm:left-3 sm:right-3 bg-secondary/95 backdrop-blur-[12px] p-2 sm:p-3 rounded-lg shadow-lg border border-hover/30"> <div class="absolute -top-6 sm:-top-8 left-2 right-2 sm:left-3 sm:right-3 bg-secondary/95 backdrop-blur-[12px] p-2 sm:p-3 rounded-lg shadow-lg border border-hover/30">
<h3 id={`comic-title-${comic.id}`} <h3 id={`comic-title-${comic.id}`} class="text-base sm:text-lg font-bold line-clamp-1 text-center text-primary drop-shadow-[0_1px_1px_rgba(0,0,0,0.5)]">
class="text-base sm:text-lg font-bold line-clamp-1 text-center text-primary drop-shadow-[0_1px_1px_rgba(0,0,0,0.5)]"
aria-label={comic.md_comics.title}
>
{comic.md_comics.title} {comic.md_comics.title}
</h3> </h3>
</div> </div>
@ -71,14 +66,14 @@ const isNSFW = comic.md_comics.content_rating === 'erotica';
<div class="relative mt-3" aria-label="Comic genres"> <div class="relative mt-3" aria-label="Comic genres">
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{genreNames.slice(0, 3).map(genre => ( {genreNames.slice(0, 3).map(genre => (
<span class="bg-hover text-primary/80 px-3 py-1 rounded-full text-xs font-medium truncate"> <span class="bg-hover text-primary/80 px-3 py-1 rounded-full text-xs font-medium truncate transition-all hover:bg-hover/80" title={genre}>
{genre} {genre}
</span> </span>
))} ))}
{genreNames.length > 3 && ( {genreNames.length > 3 && (
<button <button
type="button" type="button"
class="expand-tags-btn bg-hover text-primary/80 px-3 py-1 rounded-full text-xs font-medium hover:bg-hover/80 hover:scale-105 cursor-pointer transition-all duration-200 ease-in-out" class="expand-tags-btn bg-hover text-primary/80 px-3 py-1 rounded-full text-xs font-medium hover:bg-hover/80 cursor-pointer transition-colors"
aria-expanded="false" aria-expanded="false"
data-comic-id={comic.id} data-comic-id={comic.id}
> >
@ -95,7 +90,7 @@ const isNSFW = comic.md_comics.content_rating === 'erotica';
> >
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{genreNames.slice(3).map(genre => ( {genreNames.slice(3).map(genre => (
<span class="bg-hover text-primary/80 px-3 py-1 rounded-full text-xs font-medium truncate"> <span class="bg-hover text-primary/80 px-3 py-1 rounded-full text-xs font-medium truncate transition-all hover:bg-hover/80" title={genre}>
{genre} {genre}
</span> </span>
))} ))}
@ -224,36 +219,21 @@ const isNSFW = comic.md_comics.content_rating === 'erotica';
timerSpan.textContent = timeString; timerSpan.textContent = timeString;
} }
// Add intersection observer for lazy loading
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const timer = entry.target.querySelector('.countdown-timer');
if (timer) updateTimer(timer);
}
});
});
// Update countdown timer initialization
countdownTimers.forEach(timer => { countdownTimers.forEach(timer => {
observer.observe(timer);
updateTimer(timer); updateTimer(timer);
setInterval(() => { // Update every second
if (timer.isConnected) updateTimer(timer); setInterval(() => updateTimer(timer), 1000);
}, 1000);
}); });
}); });
function filterComics() { function filterComics() {
const excludedGenres = new Set(JSON.parse(localStorage.getItem('excluded-genres') || '[]')); const excludedGenres = new Set(JSON.parse(localStorage.getItem('excluded-genres') || '[]'));
// Use requestAnimationFrame for smoother updates document.querySelectorAll('[data-genres]').forEach(card => {
requestAnimationFrame(() => { const cardGenres = JSON.parse((card as HTMLElement).dataset.genres || '[]');
document.querySelectorAll('[data-genres]').forEach(card => { const shouldHide = cardGenres.some((genreId: number) => excludedGenres.has(genreId));
const cardGenres = JSON.parse((card as HTMLElement).dataset.genres || '[]');
const shouldHide = cardGenres.some((genreId: number) => excludedGenres.has(genreId)); card.classList.toggle('hidden', shouldHide);
card.classList.toggle('hidden', shouldHide);
});
}); });
} }

View file

@ -52,7 +52,7 @@ const genres = await fetchGenres();
<input <input
type="checkbox" type="checkbox"
value={genre.id} value={genre.id}
class="genre-checkbox w-4 h-4 rounded border border-hover/30 bg-primary checked:bg-accent transition-all duration-200 ease-in-out" class="genre-checkbox"
/> />
<span class="text-sm">{genre.name}</span> <span class="text-sm">{genre.name}</span>
</label> </label>

View file

@ -50,9 +50,9 @@ const languages = [
<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}>
<span class="text-primary truncate pr-2">{slug}</span> <span class="text-primary">{slug}</span>
<button <button
class="text-red-500 hover:text-red-600 transition-colors flex-shrink-0 pl-2" class="text-red-500 hover:text-red-600 transition-colors"
data-remove={slug} data-remove={slug}
> >
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -125,28 +125,21 @@ const languages = [
const promises = [...slugs].map(async (slug) => { const promises = [...slugs].map(async (slug) => {
try { try {
// Add timeout for comic info fetch // Use our API endpoint instead of direct ComicK API call
const controller = new AbortController(); const response = await fetch(`/api/comic/${encodeURIComponent(slug)}`);
const timeoutId = setTimeout(() => controller.abort(), 5000);
const response = await fetch(`/api/comic/${encodeURIComponent(slug)}`, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) throw new Error('Comic not found'); if (!response.ok) throw new Error('Comic not found');
const data = await response.json(); const data = await response.json();
const div = document.createElement('div'); 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 overflow-hidden'; 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 // Add a link to the comic
const link = document.createElement('a'); const link = document.createElement('a');
link.href = `https://comick.io/comic/${slug}`; link.href = `https://comick.io/comic/${slug}`;
link.target = '_blank'; link.target = '_blank';
link.rel = 'noopener noreferrer'; link.rel = 'noopener noreferrer';
link.className = 'flex items-center gap-4 flex-1 min-w-0'; link.className = 'flex items-center gap-4 flex-1';
// Handle cover image // Handle cover image
if (data.comic?.md_covers?.[0]?.b2key) { if (data.comic?.md_covers?.[0]?.b2key) {
@ -168,7 +161,7 @@ const languages = [
} }
const info = document.createElement('div'); const info = document.createElement('div');
info.className = 'flex-1 min-w-0 truncate'; info.className = 'flex-1 min-w-0';
info.innerHTML = ` info.innerHTML = `
<div class="font-medium text-lg truncate group-hover:text-accent transition-colors"> <div class="font-medium text-lg truncate group-hover:text-accent transition-colors">
${data.comic?.title || slug} ${data.comic?.title || slug}
@ -185,7 +178,7 @@ const languages = [
div.appendChild(link); div.appendChild(link);
const removeBtn = document.createElement('button'); 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'; 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 = ` removeBtn.innerHTML = `
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <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> <path d="M6 18L18 6M6 6l12 12"></path>
@ -340,17 +333,6 @@ const languages = [
if (slugString) { if (slugString) {
try { try {
// Add loading state
generateRSSLink!.innerHTML = `
<span class="flex items-center gap-2">
<svg class="w-5 h-5 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>
Generating...
</span>
`;
generateRSSLink!.onclick = null;
const response = await fetch('/api/create-feed', { const response = await fetch('/api/create-feed', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -366,43 +348,12 @@ const languages = [
const { feedId } = await response.json(); const { feedId } = await response.json();
generateRSSLink!.href = `/rss/${feedId}.xml`; generateRSSLink!.href = `/rss/${feedId}.xml`;
generateRSSLink!.onclick = null;
// Clear loading state after successful generation
generateRSSLink!.innerHTML = `
<span class="flex items-center gap-2">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 11a9 9 0 0 1 9 9"></path>
<path d="M4 4a16 16 0 0 1 16 16"></path>
<circle cx="5" cy="19" r="1"></circle>
</svg>
RSS Feed Ready
</span>
`;
generateRSSLink!.onclick = null; // Remove any existing click handlers
} catch (error) { } catch (error) {
console.error('Failed to generate feed:', error); console.error('Failed to generate feed:', error);
generateRSSLink!.innerHTML = `
<span class="flex items-center gap-2">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Failed to generate
</span>
`;
} }
} else { } else {
// Reset to default state when no slugs
generateRSSLink!.href = '#'; generateRSSLink!.href = '#';
generateRSSLink!.innerHTML = `
<span class="flex items-center gap-2">
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 11a9 9 0 0 1 9 9"></path>
<path d="M4 4a16 16 0 0 1 16 16"></path>
<circle cx="5" cy="19" r="1"></circle>
</svg>
Generate RSS Feed
</span>
`;
generateRSSLink!.onclick = (e) => { generateRSSLink!.onclick = (e) => {
e.preventDefault(); e.preventDefault();
return false; return false;

View file

@ -11,7 +11,7 @@ interface BackgroundImage {
export const backgrounds: BackgroundImage[] = [ export const backgrounds: BackgroundImage[] = [
{ {
url: "/citali.webp", url: '/citali.webp',
title: "Citlali (Genshin Impact)", title: "Citlali (Genshin Impact)",
artist: "ryrmcher", artist: "ryrmcher",
source: "https://danbooru.donmai.us/posts/6148600", source: "https://danbooru.donmai.us/posts/6148600",
@ -42,7 +42,7 @@ export const backgrounds: BackgroundImage[] = [
url: "/original.webp", url: "/original.webp",
title: "読書", title: "読書",
artist: "kamepan44231 - かめぱすた", artist: "kamepan44231 - かめぱすた",
source: "https://danbooru.donmai.us/posts/8743208", source: "https://danbooru.donmai.us/posts/8743208?",
socials: [ socials: [
{ {
name: "Twitter", name: "Twitter",
@ -50,30 +50,6 @@ export const backgrounds: BackgroundImage[] = [
}, },
], ],
}, },
{
url: "/numi.webp",
title: "Numi",
artist: "TenIllustrator",
source: "https://danbooru.donmai.us/posts/8074444",
socials: [
{
name: "Twitter",
link: "https://x.com/Ten_0123_/",
},
],
},
{
url: "/shylily.webp",
title: "Shylily",
artist: "greatodoggo",
source: "https://www.pixiv.net/en/artworks/124222053",
socials: [
{
name: "Twitter",
link: "https://twitter.com/greatodoggo",
},
],
},
// Add more backgrounds here! // Add more backgrounds here!
// { // {
// url: 'your-image-url', // url: 'your-image-url',
@ -90,7 +66,7 @@ export function getRandomBackground(): BackgroundImage {
if (backgrounds.length === 0) { if (backgrounds.length === 0) {
// Fallback background if array is empty // Fallback background if array is empty
return { return {
url: "/citali.webp", url: "https://cdn.donmai.us/original/97/c8/__citlali_genshin_impact_drawn_by_ryrmcher__97c810ff6e992f1789f93e2edbb683a3.jpg",
title: "Citlali (Genshin Impact)", title: "Citlali (Genshin Impact)",
artist: "ryrmcher", artist: "ryrmcher",
source: "https://danbooru.donmai.us/posts/6148600", source: "https://danbooru.donmai.us/posts/6148600",
@ -101,8 +77,8 @@ export function getRandomBackground(): BackgroundImage {
} }
export function preloadBackgrounds() { export function preloadBackgrounds() {
backgrounds.forEach((bg) => { backgrounds.forEach(bg => {
const img = new Image(); const img = new Image();
img.src = bg.url; img.src = bg.url;
}); });
} }

View file

@ -2,7 +2,6 @@
import ThemeToggle from '../components/ThemeToggle.astro'; import ThemeToggle from '../components/ThemeToggle.astro';
import Analytics from '@vercel/analytics/astro'; import Analytics from '@vercel/analytics/astro';
import { getRandomBackground } from '../config/backgrounds'; import { getRandomBackground } from '../config/backgrounds';
import AnnouncementBanner from '../components/AnnouncementBanner.astro';
interface Props { interface Props {
title: string; title: string;
@ -17,17 +16,6 @@ const {
const background = getRandomBackground(); const background = getRandomBackground();
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'; const isCreditsPage = Astro.url.pathname === '/credits';
// Add the announcement configuration
const announcement = {
message: "You can add your own custom feeds!",
type: "info" as const,
dismissible: true,
link: {
text: "Try it now",
url: "/custom"
}
};
--- ---
<!doctype html> <!doctype html>
@ -62,15 +50,12 @@ const announcement = {
id="body-element" id="body-element"
style={`--bg-url: url('${background.url}')`} style={`--bg-url: url('${background.url}')`}
> >
<!-- Add the banner right after body opening tag -->
<AnnouncementBanner announcement={announcement} />
{!isCreditsPage && ( {!isCreditsPage && (
<div class="fixed bottom-4 sm:bottom-6 right-4 sm:right-6 z-50"> <div class="fixed bottom-4 sm:bottom-6 right-4 sm:right-6 z-50">
<div class="relative" id="settings-dropdown"> <div class="relative" id="settings-dropdown">
<button <button
id="settings-button" id="settings-button"
class="p-2.5 sm:p-3 bg-secondary rounded-lg hover:bg-hover transition-all duration-200 shadow-lg border border-hover/30 flex items-center gap-2 hover:-translate-y-0.5" class="p-2.5 sm:p-3 bg-secondary rounded-lg hover:bg-hover transition-colors shadow-lg border border-hover/30 flex items-center gap-2"
aria-label="Settings" aria-label="Settings"
> >
<svg class="w-4 h-4 sm:w-5 sm:h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="w-4 h-4 sm:w-5 sm:h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -271,20 +256,10 @@ const announcement = {
.animate-pulse { .animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
} }
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-3px); }
}
.animate-bounce {
animation: bounce 1s infinite;
}
</style> </style>
<style define:vars={{ bgUrl: background.url }}> <style define:vars={{ bgUrl: background.url }}>
body::before { body::before {
background-image: var(--bg-url); background-image: var(--bg-url);
transition: background-image 0.5s ease-in-out;
} }
</style> </style>

View file

@ -28,14 +28,4 @@ export interface ComicCover {
b2key: string; b2key: string;
width?: number; width?: number;
height?: number; height?: number;
}
export interface Announcement {
message: string;
type: 'info' | 'warning' | 'success';
dismissible?: boolean;
link?: {
text: string;
url: string;
};
} }

View file

@ -7,38 +7,16 @@ const description = "Create a custom RSS feed for your favorite comics";
--- ---
<Layout title={title} description={description}> <Layout title={title} description={description}>
<div class="container mx-auto px-4 py-8"> <main class="mx-auto px-3 sm:px-4 lg:px-8 max-w-[800px] py-4 sm:py-8">
<!-- Add back button --> <h1 class="text-2xl sm:text-3xl lg:text-4xl font-bold text-center mb-4 sm:mb-8">Custom RSS Feed</h1>
<a
href="/en/sfw/all" <div class="bg-secondary/50 p-3 sm:p-6 rounded-lg shadow-lg">
class="inline-flex items-center gap-2 mb-6 px-4 py-2 bg-secondary hover:bg-hover rounded-md transition-colors font-medium" <SlugInput />
> </div>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
Back to Comics
</a>
<main class="mx-auto px-3 sm:px-4 lg:px-8 max-w-[800px] py-4 sm:py-8"> <div class="mt-4 sm:mt-8 text-center text-primary/60 text-sm sm:text-base">
<h1 class="text-2xl sm:text-3xl lg:text-4xl font-bold text-center mb-4 sm:mb-8">Custom RSS Feed</h1> <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>
<div class="bg-secondary/50 p-3 sm:p-6 rounded-lg shadow-lg"> </div>
<SlugInput /> </main>
</div>
<div class="mt-4 sm:mt-8 text-center text-primary/60 text-sm sm:text-base">
<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>
</div>
</main>
</div>
</Layout> </Layout>