Compare commits

...

7 commits

Author SHA1 Message Date
ld3zian
af61e4f32c oops change around 2025-01-31 22:45:56 -07:00
ld3zian
997c817b4f add more bgs 2025-01-31 22:26:27 -07:00
ld3zian
a4641ea675 fix custom feed length 2025-01-31 21:57:27 -07:00
ld3zian
60f236dea2 fix button 2025-01-31 21:21:33 -07:00
ld3zian
f20097e45c home improvements 2025-01-31 20:59:17 -07:00
ld3zian
f8ceba0d0d guess I can't update tailwind 2025-01-31 17:23:00 -07:00
ld3zian
71c775f70e add annoucement banner and bump version 2025-01-31 17:03:07 -07:00
12 changed files with 888 additions and 1406 deletions

1943
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": "0.0.1", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
@ -12,12 +12,12 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/rss": "^4.0.11", "@astrojs/rss": "^4.0.11",
"@astrojs/tailwind": "^5.1.0", "@astrojs/tailwind": "^6.0.0",
"@astrojs/vercel": "^8.0.2", "@astrojs/vercel": "^8.0.5",
"@floating-ui/dom": "^1.6.3", "@floating-ui/dom": "^1.6.13",
"@vercel/analytics": "^1.4.1", "@vercel/analytics": "^1.4.1",
"@vercel/kv": "^1.0.1", "@vercel/kv": "^3.0.0",
"astro": "^5.1.6", "astro": "^5.2.3",
"feed": "^4.2.2", "feed": "^4.2.2",
"ioredis": "^5.4.2", "ioredis": "^5.4.2",
"tailwindcss": "^3.4.1" "tailwindcss": "^3.4.1"

BIN
public/numi.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
public/shylily.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

View file

@ -0,0 +1,111 @@
---
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,6 +31,8 @@ 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
@ -52,7 +54,10 @@ 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}`} 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)]"> <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)]"
aria-label={comic.md_comics.title}
>
{comic.md_comics.title} {comic.md_comics.title}
</h3> </h3>
</div> </div>
@ -66,14 +71,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 transition-all hover:bg-hover/80" title={genre}> <span class="bg-hover text-primary/80 px-3 py-1 rounded-full text-xs font-medium truncate">
{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 cursor-pointer transition-colors" 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"
aria-expanded="false" aria-expanded="false"
data-comic-id={comic.id} data-comic-id={comic.id}
> >
@ -90,7 +95,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 transition-all hover:bg-hover/80" title={genre}> <span class="bg-hover text-primary/80 px-3 py-1 rounded-full text-xs font-medium truncate">
{genre} {genre}
</span> </span>
))} ))}
@ -219,21 +224,36 @@ 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);
// Update every second setInterval(() => {
setInterval(() => updateTimer(timer), 1000); if (timer.isConnected) updateTimer(timer);
}, 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') || '[]'));
document.querySelectorAll('[data-genres]').forEach(card => { // Use requestAnimationFrame for smoother updates
const cardGenres = JSON.parse((card as HTMLElement).dataset.genres || '[]'); requestAnimationFrame(() => {
const shouldHide = cardGenres.some((genreId: number) => excludedGenres.has(genreId)); document.querySelectorAll('[data-genres]').forEach(card => {
const cardGenres = JSON.parse((card as HTMLElement).dataset.genres || '[]');
card.classList.toggle('hidden', shouldHide); const shouldHide = cardGenres.some((genreId: number) => excludedGenres.has(genreId));
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" class="genre-checkbox w-4 h-4 rounded border border-hover/30 bg-primary checked:bg-accent transition-all duration-200 ease-in-out"
/> />
<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">{slug}</span> <span class="text-primary truncate pr-2">{slug}</span>
<button <button
class="text-red-500 hover:text-red-600 transition-colors" class="text-red-500 hover:text-red-600 transition-colors flex-shrink-0 pl-2"
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,21 +125,28 @@ const languages = [
const promises = [...slugs].map(async (slug) => { const promises = [...slugs].map(async (slug) => {
try { try {
// Use our API endpoint instead of direct ComicK API call // Add timeout for comic info fetch
const response = await fetch(`/api/comic/${encodeURIComponent(slug)}`); const controller = new AbortController();
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'; 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';
// 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'; link.className = 'flex items-center gap-4 flex-1 min-w-0';
// Handle cover image // Handle cover image
if (data.comic?.md_covers?.[0]?.b2key) { if (data.comic?.md_covers?.[0]?.b2key) {
@ -161,7 +168,7 @@ const languages = [
} }
const info = document.createElement('div'); const info = document.createElement('div');
info.className = 'flex-1 min-w-0'; info.className = 'flex-1 min-w-0 truncate';
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}
@ -178,7 +185,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 relative z-10'; removeBtn.className = 'text-red-500 hover:text-red-600 transition-colors p-2 hover:bg-red-500/10 rounded-full flex-shrink-0';
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>
@ -333,6 +340,17 @@ 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: {
@ -348,12 +366,43 @@ 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,6 +50,30 @@ 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',
@ -66,7 +90,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: "https://cdn.donmai.us/original/97/c8/__citlali_genshin_impact_drawn_by_ryrmcher__97c810ff6e992f1789f93e2edbb683a3.jpg", 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",
@ -77,7 +101,7 @@ 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,6 +2,7 @@
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;
@ -16,6 +17,17 @@ 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>
@ -50,12 +62,15 @@ const isCreditsPage = Astro.url.pathname === '/credits';
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-colors shadow-lg border border-hover/30 flex items-center gap-2" 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"
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">
@ -256,10 +271,20 @@ const isCreditsPage = Astro.url.pathname === '/credits';
.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

@ -29,3 +29,13 @@ export interface ComicCover {
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,16 +7,38 @@ const description = "Create a custom RSS feed for your favorite comics";
--- ---
<Layout title={title} description={description}> <Layout title={title} description={description}>
<main class="mx-auto px-3 sm:px-4 lg:px-8 max-w-[800px] py-4 sm:py-8"> <div class="container mx-auto px-4 py-8">
<h1 class="text-2xl sm:text-3xl lg:text-4xl font-bold text-center mb-4 sm:mb-8">Custom RSS Feed</h1> <!-- Add back button -->
<a
href="/en/sfw/all"
class="inline-flex items-center gap-2 mb-6 px-4 py-2 bg-secondary hover:bg-hover rounded-md transition-colors font-medium"
>
<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>
<div class="bg-secondary/50 p-3 sm:p-6 rounded-lg shadow-lg"> <main class="mx-auto px-3 sm:px-4 lg:px-8 max-w-[800px] py-4 sm:py-8">
<SlugInput /> <h1 class="text-2xl sm:text-3xl lg:text-4xl font-bold text-center mb-4 sm:mb-8">Custom RSS Feed</h1>
</div>
<div class="mt-4 sm:mt-8 text-center text-primary/60 text-sm sm:text-base"> <div class="bg-secondary/50 p-3 sm:p-6 rounded-lg shadow-lg">
<p>Search for your favorite comics using the search box above, or add their slugs manually.</p> <SlugInput />
<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> <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>