122 lines
4.1 KiB
TypeScript
122 lines
4.1 KiB
TypeScript
import ArtworkMetaCard from "@/components/artworks/ArtworkMetaCard";
|
|
import ArtworkTimelapseViewer from "@/components/artworks/ArtworkTimelapseViewer";
|
|
import { ContextBackButton } from "@/components/artworks/ContextBackButton";
|
|
import NsfwConsentDialog from "@/components/nsfw/NsfwConsentDialog";
|
|
import NsfwImage from "@/components/nsfw/NsfwImage";
|
|
import NsfwLink from "@/components/nsfw/NsfwLink";
|
|
import { Button } from "@/components/ui/button";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { cn } from "@/lib/utils";
|
|
import { PlayCircle } from "lucide-react";
|
|
|
|
export default async function SingleArtworkPage({
|
|
params,
|
|
}: {
|
|
params: { id: string };
|
|
searchParams: Record<string, string | string[] | undefined>;
|
|
}) {
|
|
const { id } = await params;
|
|
const artwork = await prisma.artwork.findUnique({
|
|
where: {
|
|
id,
|
|
},
|
|
include: {
|
|
file: true,
|
|
gallery: true,
|
|
metadata: true,
|
|
albums: true,
|
|
categories: true,
|
|
colors: { include: { color: true } },
|
|
tags: true,
|
|
variants: true,
|
|
timelapse: { where: { enabled: true } },
|
|
},
|
|
});
|
|
|
|
if (!artwork) return <div>Artwork with this ID could not be found</div>;
|
|
|
|
const { width, height } = artwork.variants.find(
|
|
(v) => v.type === "resized",
|
|
) ?? { width: 0, height: 0 };
|
|
|
|
const colors =
|
|
artwork.colors
|
|
?.map((c) => c.color?.hex)
|
|
.filter((hex): hex is string => Boolean(hex)) ?? [];
|
|
|
|
const gradientColors = colors.length
|
|
? colors.join(", ")
|
|
: "rgba(0,0,0,0.1), rgba(0,0,0,0.03)";
|
|
|
|
return (
|
|
<div className="px-4 sm:px-8 py-4">
|
|
<NsfwConsentDialog hasNsfw={Boolean(artwork.nsfw)} />
|
|
<div className="relative w-full min-h-10 flex items-center mb-4">
|
|
<div className="z-10 hidden sm:block">
|
|
<ContextBackButton />
|
|
</div>
|
|
{artwork.name ? (
|
|
<div className="w-full text-center sm:pointer-events-none sm:absolute sm:left-1/2 sm:-translate-x-1/2">
|
|
<div className="sm:pointer-events-auto">
|
|
<h1 className="text-xl sm:text-2xl font-bold mb-2 sm:mb-4 py-2 sm:py-4 px-2 sm:px-0 wrap-break-word">
|
|
{artwork.name}
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
<div className="flex flex-col items-center gap-4">
|
|
<div className="group rounded-lg border overflow-hidden hover:shadow-lg transition-shadow bg-background relative">
|
|
<div
|
|
className="relative w-full bg-muted items-center justify-center"
|
|
style={{ aspectRatio: "4 / 3" }}
|
|
>
|
|
<NsfwLink href={`/raw/${artwork.id}`} nsfw={Boolean(artwork.nsfw)}>
|
|
<NsfwImage
|
|
src={`/api/image/resized/${artwork.file.fileKey}.webp`}
|
|
alt={artwork.altText || "Artwork"}
|
|
fill={!width || !height}
|
|
width={width}
|
|
height={height}
|
|
nsfw={Boolean(artwork.nsfw)}
|
|
className={cn("object-cover transition duration-300")}
|
|
/>
|
|
</NsfwLink>
|
|
</div>
|
|
</div>
|
|
{artwork.timelapse?.enabled ? (
|
|
<div className="flex justify-center">
|
|
<ArtworkTimelapseViewer
|
|
timelapse={artwork.timelapse}
|
|
artworkName={artwork.name}
|
|
trigger={
|
|
<Button size="lg" className="gap-2">
|
|
<PlayCircle className="h-5 w-5" />
|
|
Watch timelapse
|
|
</Button>
|
|
}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
<div
|
|
className="rounded-lg"
|
|
style={{
|
|
background: `linear-gradient(135deg, ${gradientColors})`,
|
|
}}
|
|
>
|
|
<ArtworkMetaCard
|
|
gradientColors={gradientColors}
|
|
altText={artwork.altText}
|
|
description={artwork.description}
|
|
categories={artwork.categories}
|
|
tags={artwork.tags}
|
|
/>
|
|
</div>
|
|
<div className="w-full flex justify-center sm:hidden">
|
|
<ContextBackButton className="mx-auto flex justify-center" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|