new-manga/src/components/ComicCard.astro
2025-01-25 21:33:30 -07:00

180 lines
No EOL
6.4 KiB
Text

---
import type { Comic, Genre } from '../lib/types';
import { Image } from 'astro:assets';
import { getGenreNames } from '../lib/api';
interface Props {
comic: Comic;
genres: Genre[];
}
const { comic, genres } = Astro.props;
const genreNames = getGenreNames(comic.md_comics.genres, genres);
const isAvif = comic.md_comics.md_covers[0]?.b2key.endsWith('.avif');
const coverUrl = comic.md_comics.md_covers[0]
? `https://meo.comick.pictures/${comic.md_comics.md_covers[0].b2key}`
: '/placeholder.png';
const isNSFW = comic.md_comics.content_rating === 'erotica';
---
<div class="group relative bg-secondary rounded-xl overflow-visible shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-xl h-full flex flex-col max-w-[280px] mx-auto border border-hover/30">
<a href={`https://comick.io/comic/${comic.md_comics.slug}`} target="_blank" rel="noopener noreferrer" class="flex flex-col h-full">
<div class="relative overflow-hidden aspect-[2/3] rounded-t-xl">
{!isAvif ? (
<Image
src={coverUrl}
alt={comic.md_comics.title}
inferSize={true}
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
) : (
<img
src={coverUrl}
alt={comic.md_comics.title}
width="280"
height="420"
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
/>
)}
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
{isNSFW && (
<div class="absolute top-2 right-2 bg-red-500/90 text-white px-2 py-1 rounded text-xs font-bold shadow-lg backdrop-blur-[2px]">
NSFW
</div>
)}
</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-secondary/95 backdrop-blur-[12px] p-3 rounded-lg shadow-lg border border-hover/30">
<h3 class="text-lg font-bold line-clamp-1 text-center text-primary drop-shadow-[0_1px_1px_rgba(0,0,0,0.5)]">
{comic.md_comics.title}
</h3>
</div>
<div class="mt-6">
<p class="inline-block bg-accent/20 text-accent px-4 py-1 rounded-full text-sm font-semibold">
Chapter {comic.chap}
</p>
{genreNames.length > 0 && (
<div class="relative mt-3" aria-label="Comic genres">
<div class="flex flex-wrap gap-2">
{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}>
{genre}
</span>
))}
{genreNames.length > 3 && (
<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"
aria-expanded="false"
data-comic-id={comic.id}
>
+{genreNames.length - 3} more
</button>
)}
</div>
{genreNames.length > 3 && (
<div
id={`tags-${comic.id}`}
class="expanded-tags bg-secondary/95 backdrop-blur-sm p-3 rounded-lg shadow-lg border border-hover/30 hidden"
role="tooltip"
>
<div class="flex flex-wrap gap-2">
{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}>
{genre}
</span>
))}
</div>
</div>
)}
</div>
)}
</div>
<div class="mt-auto pt-4 flex items-center justify-between text-sm text-primary/60 border-t border-hover/30">
<time datetime={comic.updated_at}>
{new Date(comic.updated_at).toLocaleString(undefined, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</time>
<span class="group-hover:text-accent transition-colors duration-300">Read Now →</span>
</div>
</div>
</a>
</div>
<script>
import { computePosition, flip, shift, offset } from '@floating-ui/dom';
document.addEventListener('DOMContentLoaded', () => {
const buttons = document.querySelectorAll('.expand-tags-btn');
buttons.forEach(button => {
const comicId = button.getAttribute('data-comic-id');
const tooltip = document.getElementById(`tags-${comicId}`);
if (!tooltip) return;
function updatePosition() {
computePosition(button, tooltip, {
placement: 'bottom',
middleware: [
offset(8),
flip(),
shift({ padding: 8 })
],
}).then(({x, y}) => {
Object.assign(tooltip.style, {
left: `${x}px`,
top: `${y}px`,
});
});
}
function showTooltip() {
tooltip.classList.remove('hidden');
updatePosition();
}
function hideTooltip() {
tooltip.classList.add('hidden');
}
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const isHidden = tooltip.classList.contains('hidden');
if (isHidden) {
showTooltip();
button.textContent = 'Show less';
// Close when clicking outside
const closeHandler = (event: MouseEvent) => {
if (!tooltip.contains(event.target as Node) && !button.contains(event.target as Node)) {
hideTooltip();
button.textContent = `+${tooltip.querySelectorAll('span').length} more`;
document.removeEventListener('click', closeHandler);
}
};
setTimeout(() => {
document.addEventListener('click', closeHandler);
}, 0);
} else {
hideTooltip();
button.textContent = `+${tooltip.querySelectorAll('span').length} more`;
}
});
});
});
</script>