mirror of
https://github.com/ld3z/new-manga.git
synced 2026-11-09 21:03:45 -07:00
Compare commits
7 commits
17ceea25d9
...
af61e4f32c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af61e4f32c | ||
|
|
997c817b4f | ||
|
|
a4641ea675 | ||
|
|
60f236dea2 | ||
|
|
f20097e45c | ||
|
|
f8ceba0d0d | ||
|
|
71c775f70e |
12 changed files with 888 additions and 1406 deletions
1943
package-lock.json
generated
1943
package-lock.json
generated
File diff suppressed because it is too large
Load diff
12
package.json
12
package.json
|
|
@ -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
BIN
public/numi.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
BIN
public/shylily.webp
Normal file
BIN
public/shylily.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
111
src/components/AnnouncementBanner.astro
Normal file
111
src/components/AnnouncementBanner.astro
Normal 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>
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue