mirror of
https://github.com/ld3z/new-manga.git
synced 2026-11-09 20:53:45 -07:00
error improvements
This commit is contained in:
parent
3e897aeefa
commit
0277da93e5
4 changed files with 122 additions and 32 deletions
26
src/components/LoadingCard.astro
Normal file
26
src/components/LoadingCard.astro
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
---
|
||||||
|
<div class="bg-secondary rounded-xl overflow-hidden shadow-lg h-full flex flex-col max-w-[280px] mx-auto border border-hover/30">
|
||||||
|
<div class="relative overflow-hidden aspect-[2/3] rounded-t-xl bg-hover animate-pulse"></div>
|
||||||
|
|
||||||
|
<div class="p-4 md:p-5 flex-grow flex flex-col bg-secondary relative rounded-b-xl">
|
||||||
|
<div class="absolute -top-8 left-3 right-3 bg-hover animate-pulse h-12 rounded-lg"></div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<div class="bg-hover animate-pulse h-8 w-24 rounded-full"></div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{[1, 2, 3].map(() => (
|
||||||
|
<div class="bg-hover animate-pulse h-6 w-16 rounded-full"></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-auto pt-4 flex items-center justify-between">
|
||||||
|
<div class="bg-hover animate-pulse h-4 w-32 rounded"></div>
|
||||||
|
<div class="bg-hover animate-pulse h-4 w-20 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -3,20 +3,43 @@ import ThemeToggle from '../components/ThemeToggle.astro';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title } = Astro.props;
|
const {
|
||||||
|
title,
|
||||||
|
description = "Discover and track your favorite comics with ComicK-RSS"
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const canonicalURL = new URL(Astro.url.pathname, Astro.site ?? 'https://comick-rss.vercel.app').toString();
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="description" content="Astro description" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<link rel="canonical" href={canonicalURL} />
|
||||||
|
|
||||||
|
<!-- Primary Meta Tags -->
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
|
<meta name="title" content={title} />
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
|
||||||
|
<!-- Open Graph / Facebook -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content={canonicalURL} />
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
|
<meta property="twitter:url" content={canonicalURL} />
|
||||||
|
<meta property="twitter:title" content={title} />
|
||||||
|
<meta property="twitter:description" content={description} />
|
||||||
|
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen bg-primary text-primary">
|
<body class="min-h-screen bg-primary text-primary">
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
|
|
@ -63,6 +86,7 @@ const { title } = Astro.props;
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: system-ui, sans-serif;
|
font-family: system-ui, sans-serif;
|
||||||
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
|
@ -76,4 +100,14 @@ const { title } = Astro.props;
|
||||||
Courier New,
|
Courier New,
|
||||||
monospace;
|
monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Loading animation */
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: .5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse {
|
||||||
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -20,12 +20,39 @@ const urlBase = "https://api.comick.fun/chapter";
|
||||||
|
|
||||||
let genreCache: Genre[] = [];
|
let genreCache: Genre[] = [];
|
||||||
|
|
||||||
|
const RETRY_COUNT = 3;
|
||||||
|
const RETRY_DELAY = 1000;
|
||||||
|
|
||||||
|
async function fetchWithRetry(url: string, options: RequestInit = {}, retries = RETRY_COUNT): Promise<Response> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'User-Agent': 'Mozilla/5.0 (compatible; ComicReader/1.0)',
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
if (retries > 0) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
||||||
|
return fetchWithRetry(url, options, retries - 1);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchGenres(): Promise<Genre[]> {
|
export async function fetchGenres(): Promise<Genre[]> {
|
||||||
if (genreCache.length > 0) return genreCache;
|
if (genreCache.length > 0) return genreCache;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('https://api.comick.fun/genre');
|
const response = await fetchWithRetry('https://api.comick.fun/genre');
|
||||||
if (!response.ok) throw new Error(`Failed to fetch genres: ${response.status}`);
|
|
||||||
genreCache = await response.json();
|
genreCache = await response.json();
|
||||||
return genreCache;
|
return genreCache;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -40,21 +67,10 @@ export function getGenreNames(genreIds: number[], genres: Genre[]): string[] {
|
||||||
.filter(name => name !== '');
|
.filter(name => name !== '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchComics(language: string = 'en', contentType: string = 'sfw'): Promise<Comic[]> {
|
export async function fetchComics(language: string = 'en', contentType: string = 'sfw', page: number = 1): Promise<Comic[]> {
|
||||||
try {
|
try {
|
||||||
const apiUrl = `${urlBase}?lang=${language}&page=1&order=new&accept_erotic_content=${contentType === 'nsfw'}`;
|
const apiUrl = `${urlBase}?lang=${language}&page=${page}&order=new&accept_erotic_content=${contentType === 'nsfw'}`;
|
||||||
|
const response = await fetchWithRetry(apiUrl);
|
||||||
const response = await fetch(apiUrl, {
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'User-Agent': 'Mozilla/5.0 (compatible; ComicReader/1.0)',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`API responded with status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (!Array.isArray(data)) {
|
if (!Array.isArray(data)) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
---
|
---
|
||||||
import Layout from '../../layouts/Layout.astro';
|
import Layout from '../../layouts/Layout.astro';
|
||||||
import ComicCard from '../../components/ComicCard.astro';
|
import ComicCard from '../../components/ComicCard.astro';
|
||||||
|
import LoadingCard from '../../components/LoadingCard.astro';
|
||||||
import LanguageSelector from '../../components/LanguageSelector.astro';
|
import LanguageSelector from '../../components/LanguageSelector.astro';
|
||||||
import RSSLink from '../../components/RSSLink.astro';
|
import RSSLink from '../../components/RSSLink.astro';
|
||||||
import { fetchComics, isValidLanguage, isValidContentType, fetchGenres } from '../../lib/api';
|
import { fetchComics, isValidLanguage, isValidContentType, fetchGenres, getLanguageName } from '../../lib/api';
|
||||||
import type { Comic } from '../../lib/types';
|
import type { Comic } from '../../lib/types';
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
|
|
@ -34,9 +35,12 @@ try {
|
||||||
console.error('Failed to fetch comics:', e);
|
console.error('Failed to fetch comics:', e);
|
||||||
error = e instanceof Error ? e.message : 'Failed to fetch comics';
|
error = e instanceof Error ? e.message : 'Failed to fetch comics';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const title = `ComicK - ${getLanguageName(lang!)} ${type!.toUpperCase()}`;
|
||||||
|
const description = `Latest ${type!.toUpperCase()} comics in ${getLanguageName(lang!)}. Updated regularly with new chapters.`;
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={`Comick - ${lang?.toUpperCase()} ${type?.toUpperCase()}`}>
|
<Layout title={title} description={description}>
|
||||||
<main class="mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] text-primary">
|
<main class="mx-auto px-4 sm:px-6 lg:px-8 max-w-[1400px] text-primary">
|
||||||
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold text-center mb-8 sm:mb-12 bg-gradient-to-r from-accent via-accent-light to-white bg-clip-text text-transparent bg-[length:400%] bg-[0%]">
|
<h1 class="text-4xl sm:text-5xl lg:text-6xl font-bold text-center mb-8 sm:mb-12 bg-gradient-to-r from-accent via-accent-light to-white bg-clip-text text-transparent bg-[length:400%] bg-[0%]">
|
||||||
ComicK-RSS
|
ComicK-RSS
|
||||||
|
|
@ -50,20 +54,30 @@ try {
|
||||||
{error && (
|
{error && (
|
||||||
<div class="bg-red-500 text-white p-4 rounded-lg mb-8">
|
<div class="bg-red-500 text-white p-4 rounded-lg mb-8">
|
||||||
<p>Error: {error}</p>
|
<p>Error: {error}</p>
|
||||||
|
<button
|
||||||
|
class="mt-2 px-4 py-2 bg-white text-red-500 rounded hover:bg-red-50 transition-colors"
|
||||||
|
onclick="window.location.reload()"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{comics.length > 0 ? (
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
|
{comics.length > 0 ? (
|
||||||
{comics.map(comic => (
|
comics.map(comic => (
|
||||||
<ComicCard comic={comic} genres={genres} />
|
<ComicCard comic={comic} genres={genres} />
|
||||||
))}
|
))
|
||||||
</div>
|
) : !error ? (
|
||||||
) : (
|
Array(10).fill(null).map(() => (
|
||||||
<div class="text-center bg-secondary p-6 rounded-lg">
|
<LoadingCard />
|
||||||
<p class="mb-4">No comics found. Please try a different language or content type.</p>
|
))
|
||||||
<p class="text-sm opacity-70">Language: {lang}, Type: {type}</p>
|
) : (
|
||||||
</div>
|
<div class="col-span-full text-center bg-secondary p-6 rounded-lg">
|
||||||
)}
|
<p class="mb-4">No comics found. Please try a different language or content type.</p>
|
||||||
|
<p class="text-sm opacity-70">Language: {getLanguageName(lang!)}, Type: {type!.toUpperCase()}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue