ok im going to sleep

This commit is contained in:
ld3zian 2025-01-25 21:45:07 -07:00
parent 9f58423953
commit 23c62495eb
9 changed files with 269 additions and 63 deletions

View file

@ -1,50 +1,34 @@
---
import { availableLanguages, availableContentTypes, getLanguageName } from '../lib/api';
import { availableLanguages, languageMap } from '../lib/api';
interface Props {
currentLang: string;
currentType: string;
contentType: string;
}
const { currentLang, currentType } = Astro.props;
const { currentLang, currentType, contentType } = Astro.props;
---
<div class="flex flex-wrap gap-4">
<div class="relative inline-block">
<select
name="language"
id="language"
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"
class="appearance-none bg-secondary text-primary px-4 py-2 pr-8 rounded-lg cursor-pointer hover:bg-hover transition-colors"
onchange="window.location.href=this.value"
>
{availableLanguages.map(lang => (
<option value={lang} selected={lang === currentLang}>
{getLanguageName(lang)}
</option>
))}
</select>
<select
name="contentType"
id="contentType"
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}/${contentType}/${currentType}`}
selected={lang === currentLang}
>
{availableContentTypes.map(type => (
<option value={type} selected={type === currentType}>
{type.toUpperCase()}
{languageMap[lang as keyof typeof languageMap]}
</option>
))}
</select>
</div>
<script>
const langSelect = document.getElementById('language') as HTMLSelectElement;
const typeSelect = document.getElementById('contentType') as HTMLSelectElement;
function updateURL() {
const lang = langSelect.value;
const type = typeSelect.value;
window.location.href = `/${lang}/${type}`;
))
}
langSelect.addEventListener('change', updateURL);
typeSelect.addEventListener('change', updateURL);
</script>
</select>
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-primary">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"/>
</svg>
</div>
</div>

View file

@ -1,21 +1,21 @@
---
interface Props {
lang: string;
contentType: string;
type: string;
}
const { lang, type } = Astro.props;
const { lang, contentType, type } = Astro.props;
---
<a
href={`/rss/${lang}/${type}.xml`}
class="inline-flex items-center gap-2 px-4 py-2 bg-secondary hover:bg-accent text-primary hover:text-white rounded-md transition-colors duration-200"
href={`/rss/${lang}/${contentType}/${type}.xml`}
class="inline-flex items-center gap-2 px-4 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@ -23,9 +23,9 @@ const { lang, type } = Astro.props;
stroke-linecap="round"
stroke-linejoin="round"
>
<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>
<path d="M4 11a9 9 0 0 1 9 9" />
<path d="M4 4a16 16 0 0 1 16 16" />
<circle cx="5" cy="19" r="1" />
</svg>
RSS Feed
</a>

View file

@ -1 +1,31 @@
---
interface Props {
currentType: string;
lang: string;
contentType: string;
}
const { currentType, lang, contentType } = Astro.props;
const types = [
{ id: 'all', label: 'All' },
{ id: 'manga', label: 'Manga' },
{ id: 'manhwa', label: 'Manhwa' },
{ id: 'manhua', label: 'Manhua' },
];
---
<div class="flex gap-2 mb-6">
{types.map(type => (
<a
href={`/${lang}/${contentType}/${type.id}`}
class={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
currentType === type.id
? 'bg-accent text-white'
: 'bg-secondary text-primary/80 hover:bg-hover'
}`}
>
{type.label}
</a>
))}
</div>

View file

@ -1,4 +1,4 @@
import type { Comic, Genre } from './types';
import type { Comic, Genre, ComicType } from './types';
const languageMap = {
"en": "English",
@ -16,6 +16,7 @@ const languageMap = {
const languages = new Set(Object.keys(languageMap));
const contentTypes = new Set(["sfw", "nsfw"]);
const comicTypes = new Set(['manga', 'manhwa', 'manhua', 'all']);
const urlBase = "https://api.comick.fun/chapter";
let genreCache: Genre[] = [];
@ -67,9 +68,18 @@ export function getGenreNames(genreIds: number[], genres: Genre[]): string[] {
.filter(name => name !== '');
}
export async function fetchComics(language: string = 'en', contentType: string = 'sfw', page: number = 1): Promise<Comic[]> {
export async function fetchComics(
language: string = 'en',
contentType: string = 'sfw',
comicType: ComicType = 'all',
page: number = 1
): Promise<Comic[]> {
try {
const apiUrl = `${urlBase}?lang=${language}&page=${page}&order=new&accept_erotic_content=${contentType === 'nsfw'}`;
const typeParam = comicType === 'all' ? '' : `&type=${comicType}`;
const apiUrl = `${urlBase}?lang=${language}&page=${page}&order=new&accept_erotic_content=${contentType === 'nsfw'}${typeParam}`;
console.log('Fetching comics from:', apiUrl);
const response = await fetchWithRetry(apiUrl);
const data = await response.json();
@ -93,12 +103,17 @@ export function isValidContentType(type: string): boolean {
return contentTypes.has(type);
}
export function isValidComicType(type: string): boolean {
return comicTypes.has(type);
}
export function getLanguageName(code: string): string {
return languageMap[code as keyof typeof languageMap] || code.toUpperCase();
}
export const availableLanguages = Array.from(languages);
export const availableContentTypes = Array.from(contentTypes);
export const availableComicTypes = Array.from(comicTypes);
export { languageMap };
interface ComicDetail {

View file

@ -10,6 +10,7 @@ export interface Comic {
b2key: string;
}[];
content_rating: string;
comic_type: string;
};
}
@ -22,3 +23,5 @@ export interface FeedMapping {
slugs: string[];
lang: string;
}
export type ComicType = 'manga' | 'manhwa' | 'manhua' | 'all';

View file

@ -1 +1,117 @@
---
import Layout from '../../../layouts/Layout.astro';
import ComicCard from '../../../components/ComicCard.astro';
import LoadingCard from '../../../components/LoadingCard.astro';
import RSSLink from '../../../components/RSSLink.astro';
import TypeFilter from '../../../components/TypeFilter.astro';
import LanguageSelector from '../../../components/LanguageSelector.astro';
import { fetchComics, fetchGenres, isValidLanguage, isValidContentType, isValidComicType, getLanguageName } from '../../../lib/api';
import type { Comic, Genre } from '../../../lib/types';
export const prerender = false;
const { lang, contentType, type } = Astro.params;
if (!isValidLanguage(lang!) || !isValidContentType(contentType!) || !isValidComicType(type!)) {
return Astro.redirect('/en/sfw/all');
}
let comics: Comic[] = [];
let genres: Genre[] = [];
let error: string | null = null;
try {
[comics, genres] = await Promise.all([
fetchComics(lang, contentType, type as any),
fetchGenres(),
]);
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to fetch comics';
}
const title = `ComicK - ${getLanguageName(lang!)} ${contentType?.toUpperCase()} ${type?.toUpperCase()}`;
---
<Layout title={title}>
<main class="container mx-auto px-4 py-8">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8">
<h1 class="text-3xl font-bold text-primary">Latest Updates</h1>
<div class="flex flex-col sm:flex-row gap-4">
<div class="flex items-center gap-4">
<LanguageSelector currentLang={lang!} currentType={type!} contentType={contentType!} />
<div class="flex gap-2">
<a
href={`/${lang}/sfw/${type}`}
class={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
contentType === 'sfw'
? 'bg-accent text-white'
: 'bg-secondary text-primary/80 hover:bg-hover'
}`}
>
SFW
</a>
<a
href={`/${lang}/nsfw/${type}`}
class={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
contentType === 'nsfw'
? 'bg-accent text-white'
: 'bg-secondary text-primary/80 hover:bg-hover'
}`}
>
NSFW
</a>
</div>
</div>
<a
href="/custom"
class="inline-flex items-center gap-2 px-4 py-2 bg-secondary rounded-lg hover:bg-hover transition-colors"
>
<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"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 5v14M5 12h14"/>
</svg>
Custom Feed/Filter
</a>
<RSSLink lang={lang!} contentType={contentType!} type={type!} />
</div>
</div>
<TypeFilter currentType={type!} lang={lang!} contentType={contentType!} />
{error && (
<div class="bg-red-500 text-white p-4 rounded-lg mb-8">
<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 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 => (
<ComicCard comic={comic} genres={genres} />
))
) : !error ? (
Array(10).fill(null).map(() => (
<LoadingCard />
))
) : (
<div class="col-span-full text-center bg-secondary p-6 rounded-lg">
<p class="text-primary/60">No comics found</p>
</div>
)}
</div>
</main>
</Layout>

View file

@ -7,6 +7,11 @@ import RSSLink from '../../components/RSSLink.astro';
import { fetchComics, isValidLanguage, isValidContentType, fetchGenres, getLanguageName } from '../../lib/api';
import type { Comic } from '../../lib/types';
export const prerender = false;
// Redirect old URLs to new structure
return Astro.redirect(`/${Astro.params.lang}/${Astro.params.type}/all`);
export async function getStaticPaths() {
const languages = ['en', 'fr', 'es', 'it', 'pl', 'tr', 'ja', 'zh', 'sv'];
const types = ['sfw', 'nsfw'];

View file

@ -11,5 +11,5 @@ export const prerender = false;
</h1>
</main>
<meta http-equiv="refresh" content="0;url=/en/sfw">
<meta http-equiv="refresh" content="0;url=/en/sfw/all">
</Layout>

View file

@ -0,0 +1,53 @@
import rss from "@astrojs/rss";
import {
fetchComics,
isValidLanguage,
isValidContentType,
isValidComicType,
fetchGenres,
getGenreNames,
} from "../../../../lib/api";
import type { APIRoute } from "astro";
import type { ComicType } from "../../../../lib/types";
export const GET: APIRoute = async ({ params, request }) => {
const { lang = "en", contentType = "sfw", type = "all" } = params;
if (!isValidLanguage(lang) || !isValidContentType(contentType) || !isValidComicType(type)) {
return new Response("Invalid parameters", { status: 400 });
}
const [comics, genres] = await Promise.all([
fetchComics(lang, contentType, type as ComicType),
fetchGenres(),
]);
const siteURL = new URL(request.url).origin;
return rss({
title: `ComicK - ${lang.toUpperCase()} ${contentType.toUpperCase()} ${type.toUpperCase()}`,
description: `A simple RSS feed for ComicK!`,
site: "https://github.com/ld3z/manga-rss",
items: comics.map((comic) => {
const genreNames = getGenreNames(comic.md_comics.genres, genres);
const genresHtml = genreNames.length > 0
? `<p>Genres: ${genreNames.join(', ')}</p>`
: '';
return {
title: `${comic.md_comics.title} - Chapter ${comic.chap}`,
link: `https://comick.io/comic/${comic.md_comics.slug}`,
pubDate: new Date(comic.updated_at),
description: `Chapter ${comic.chap} of ${comic.md_comics.title} is now available on ComicK!
${genresHtml}
${
comic.md_comics.md_covers[0]
? `<img src="https://meo.comick.pictures/${comic.md_comics.md_covers[0].b2key}"
alt="Cover" style="max-width: 300px;" />`
: ""
}
`,
};
}),
});
};