feat(web): sync translated manga galleries, clean titles, synopsis integration, original source links, anthology badges, and mobile-first reader UI
This commit is contained in:
@@ -1,26 +1,39 @@
|
||||
import Link from "next/link"
|
||||
import { Image } from "lucide-react"
|
||||
import { Image, Search, Shield } from "lucide-react"
|
||||
|
||||
export default function PublicLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<header className="sticky top-0 z-40 border-b border-[var(--border)] bg-[var(--background)]/80 backdrop-blur-lg">
|
||||
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-4">
|
||||
<header className="sticky top-0 z-40 border-b border-[var(--border)] bg-[var(--background)]/85 backdrop-blur-md">
|
||||
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-3 sm:px-4">
|
||||
<Link href="/" className="flex items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-[var(--accent-subtle)]">
|
||||
<Image className="h-4 w-4 text-[var(--accent)]" />
|
||||
</div>
|
||||
<span className="text-sm font-bold tracking-tight">worst-scan</span>
|
||||
</Link>
|
||||
<nav className="flex items-center gap-4">
|
||||
<Link href="/feed" className="text-xs text-[var(--muted)] hover:text-[var(--foreground)] transition-colors">
|
||||
Admin
|
||||
<nav className="flex items-center gap-2 sm:gap-4">
|
||||
<Link
|
||||
href="/search"
|
||||
className="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs text-[var(--muted)] hover:bg-[var(--surface)] hover:text-[var(--foreground)] transition-colors"
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Buscar</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/feed"
|
||||
className="flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-xs text-[var(--muted)] hover:bg-[var(--surface)] hover:text-[var(--foreground)] transition-colors"
|
||||
>
|
||||
<Shield className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Admin</span>
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main className="mx-auto max-w-5xl px-4 py-8">{children}</main>
|
||||
<footer className="border-t border-[var(--border)] py-6 text-center text-xs text-[var(--muted)]">
|
||||
|
||||
<main className="mx-auto max-w-5xl px-3 py-4 sm:px-4 sm:py-8">{children}</main>
|
||||
|
||||
<footer className="border-t border-[var(--border)] py-6 text-center text-xs text-[var(--muted)] mb-12 sm:mb-0">
|
||||
worst-scan · traducción automática de manga
|
||||
</footer>
|
||||
</>
|
||||
|
||||
@@ -21,11 +21,24 @@ export default async function PostDetailPage({
|
||||
.filter((p) => p.id !== post.id)
|
||||
.slice(0, 4)
|
||||
|
||||
const isAnthology =
|
||||
post.tags?.some((t) =>
|
||||
["anthology", "compilation", "tankoubon"].some((k) => t.toLowerCase().includes(k))
|
||||
) || post.num_pages >= 100
|
||||
|
||||
const originalUrl =
|
||||
post.url ||
|
||||
(post.source === "nhentai"
|
||||
? `https://nhentai.net/g/${post.gid}/`
|
||||
: post.source === "ehentai"
|
||||
? `https://e-hentai.org/g/${post.gid}/`
|
||||
: null)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="pb-16 md:pb-0">
|
||||
<Link
|
||||
href="/"
|
||||
className="mb-6 inline-flex items-center gap-1.5 text-xs text-[var(--muted)] hover:text-[var(--foreground)] transition-colors"
|
||||
className="mb-4 sm:mb-6 inline-flex items-center gap-1.5 text-xs text-[var(--muted)] hover:text-[var(--foreground)] transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
Volver
|
||||
@@ -33,23 +46,33 @@ export default async function PostDetailPage({
|
||||
|
||||
<div className="card overflow-hidden p-0">
|
||||
<div className="flex flex-col gap-0 md:flex-row">
|
||||
<div className="md:w-80 flex-shrink-0">
|
||||
<div className="w-full md:w-80 flex-shrink-0 bg-[var(--surface)]">
|
||||
<img
|
||||
src={`/api/cover/${post.gid}`}
|
||||
alt={post.title}
|
||||
className="h-auto w-full object-cover md:h-full"
|
||||
className="h-72 sm:h-96 w-full object-cover md:h-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 p-6">
|
||||
<div className="flex flex-col gap-4 p-4 sm:p-6">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold leading-tight">{post.title}</h1>
|
||||
<div className="flex flex-wrap items-center gap-2 mb-1.5">
|
||||
{isAnthology && (
|
||||
<span className="rounded bg-[var(--accent)]/10 px-2 py-0.5 text-[11px] font-semibold text-[var(--accent)] border border-[var(--accent)]/30">
|
||||
Antología / Tomo Completo
|
||||
</span>
|
||||
)}
|
||||
{post.num_pages > 0 && (
|
||||
<span className="text-xs text-[var(--muted)]">{post.num_pages} páginas</span>
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-lg sm:text-xl font-bold leading-tight">{post.title}</h1>
|
||||
{post.title_jpn && (
|
||||
<p className="mt-1 text-sm text-[var(--muted)]">{post.title_jpn}</p>
|
||||
<p className="mt-1 text-xs sm:text-sm text-[var(--muted)]">{post.title_jpn}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm">
|
||||
<div className="flex flex-wrap items-center gap-2 text-xs sm:text-sm">
|
||||
{post.artist && (
|
||||
<TagBadge tag={`artist:${post.artist}`} href={`/tag/${encodeURIComponent(`artist:${post.artist}`)}`} />
|
||||
)}
|
||||
@@ -64,7 +87,8 @@ export default async function PostDetailPage({
|
||||
</div>
|
||||
|
||||
{post.summary && (
|
||||
<div className="text-sm text-[var(--muted)] leading-relaxed whitespace-pre-line">
|
||||
<div className="text-xs sm:text-sm text-[var(--muted)] leading-relaxed whitespace-pre-line bg-[var(--surface)]/50 p-3 sm:p-4 rounded-lg border border-[var(--border)]">
|
||||
<h3 className="text-[10px] sm:text-xs font-semibold text-[var(--foreground)] uppercase tracking-wider mb-1.5">Sinopsis</h3>
|
||||
{post.summary}
|
||||
</div>
|
||||
)}
|
||||
@@ -77,21 +101,31 @@ export default async function PostDetailPage({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-auto flex items-center gap-2">
|
||||
<div className="hidden md:flex flex-wrap items-center gap-2 pt-2 mt-auto">
|
||||
<Link
|
||||
href={`/gallery/${post.gid}/read`}
|
||||
className="btn-primary"
|
||||
>
|
||||
<BookOpen className="h-4 w-4" />
|
||||
Leer
|
||||
Leer Manga
|
||||
</Link>
|
||||
{originalUrl && (
|
||||
<a
|
||||
href={originalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-ghost border border-[var(--border)] hover:bg-[var(--surface)]"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
Ver en {post.source === "ehentai" ? "e-hentai" : "nhentai"}
|
||||
</a>
|
||||
)}
|
||||
<a
|
||||
href={`/api/proxy/galleries/${post.gid}/artifacts`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-ghost"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
Archivos
|
||||
</a>
|
||||
</div>
|
||||
@@ -99,12 +133,34 @@ export default async function PostDetailPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sticky Bottom Action Bar for Mobile */}
|
||||
<div className="md:hidden fixed bottom-0 left-0 right-0 z-40 flex items-center gap-2 border-t border-[var(--border)] bg-[var(--background)]/95 backdrop-blur-lg p-3 shadow-2xl">
|
||||
<Link
|
||||
href={`/gallery/${post.gid}/read`}
|
||||
className="btn-primary flex-1 py-2.5 text-sm"
|
||||
>
|
||||
<BookOpen className="h-4 w-4" />
|
||||
Leer Manga
|
||||
</Link>
|
||||
{originalUrl && (
|
||||
<a
|
||||
href={originalUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-ghost border border-[var(--border)] p-2.5"
|
||||
title={`Ver en ${post.source === "ehentai" ? "e-hentai" : "nhentai"}`}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{related.length > 0 && (
|
||||
<section className="mt-12">
|
||||
<h2 className="mb-4 text-sm font-semibold text-[var(--muted)] uppercase tracking-wider">
|
||||
<section className="mt-8 sm:mt-12">
|
||||
<h2 className="mb-4 text-xs sm:text-sm font-semibold text-[var(--muted)] uppercase tracking-wider">
|
||||
Más publicaciones
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
<div className="grid grid-cols-2 gap-3 sm:gap-4 sm:grid-cols-4">
|
||||
{related.map((p) => (
|
||||
<PostCard key={p.id} post={p} compact />
|
||||
))}
|
||||
|
||||
@@ -21,14 +21,14 @@ export default function PublicHomePage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold tracking-tight">Publicaciones</h1>
|
||||
<p className="mt-1 text-sm text-[var(--muted)]">
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<h1 className="text-xl sm:text-2xl font-bold tracking-tight">Publicaciones</h1>
|
||||
<p className="mt-1 text-xs sm:text-sm text-[var(--muted)]">
|
||||
Traducciones automáticas generadas por worst-scan
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5">
|
||||
<div className="grid grid-cols-2 gap-3 sm:gap-4 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5">
|
||||
{posts.map((post) => (
|
||||
<PostCard key={post.id} post={post} />
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user