Compare commits
5 Commits
ych
...
animal-ind
| Author | SHA1 | Date | |
|---|---|---|---|
|
cee86edf44
|
|||
|
26118d2897
|
|||
|
d70f00314b
|
|||
|
874aa5f343
|
|||
|
b559b8250f
|
@ -1,4 +1,4 @@
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import { ArrowLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
import {
|
||||
@ -82,16 +82,17 @@ export default async function AnimalListPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="space-y-1.5">
|
||||
<ul className="space-y-1">
|
||||
{list.map((a) => (
|
||||
<li key={a.id}>
|
||||
<Link
|
||||
href={`/artworks/single/${a.id}?from=animal-index`}
|
||||
className="
|
||||
inline-flex items-center gap-2
|
||||
rounded-md px-2 py-1
|
||||
group flex w-full items-center gap-2
|
||||
rounded-md px-2 py-1.5
|
||||
text-sm font-medium
|
||||
hover:bg-muted
|
||||
hover:bg-muted/60
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
|
||||
"
|
||||
>
|
||||
<span
|
||||
@ -102,7 +103,7 @@ export default async function AnimalListPage() {
|
||||
group-hover:translate-x-0.5
|
||||
"
|
||||
>
|
||||
→
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
</span>
|
||||
|
||||
<span className="leading-snug">{a.name}</span>
|
||||
@ -149,7 +150,7 @@ export default async function AnimalListPage() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 sm:space-y-4">
|
||||
<Card>
|
||||
<CardHeader className="space-y-1">
|
||||
<CardTitle className="text-base">
|
||||
@ -177,15 +178,17 @@ export default async function AnimalListPage() {
|
||||
const isStandalone = children.length === 0;
|
||||
|
||||
return (
|
||||
<AccordionItem key={p.id} value={p.id} className="border-b">
|
||||
<AccordionItem key={p.id} value={p.id} className="py-1 sm:py-1">
|
||||
<AccordionTrigger
|
||||
className="
|
||||
py-4
|
||||
py-4 sm:py-3
|
||||
rounded-md px-2 -mx-2
|
||||
bg-hover text-hover-foreground dark:bg-hover dark:text-hover-foreground
|
||||
transition-colors
|
||||
hover:bg-muted/60
|
||||
hover:bg-hover/80 dark:hover:bg-hover/80
|
||||
font-semibold
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2
|
||||
data-[state=open]:bg-muted/40
|
||||
data-[state=open]:bg-muted/90 dark:data-[state=open]:bg-muted/90
|
||||
"
|
||||
>
|
||||
<div className="flex w-full items-center justify-between pr-2">
|
||||
@ -211,41 +214,44 @@ export default async function AnimalListPage() {
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
|
||||
<AccordionContent className="pb-5">
|
||||
<AccordionContent className="pb-4 sm:pb-4">
|
||||
{isStandalone ? (
|
||||
<div className="rounded-md border p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="text-sm font-medium">Artworks</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm font-medium">
|
||||
<span>Artworks</span>
|
||||
<Badge variant="outline">{p.artworks.length}</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<ArtworkList items={p.artworks} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 sm:space-y-3">
|
||||
{p.artworks.length > 0 ? (
|
||||
<>
|
||||
<div className="rounded-md border p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="text-sm font-medium">{/* Directly tagged */}</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm font-medium">
|
||||
<span>Direct artworks</span>
|
||||
<Badge variant="outline">{p.artworks.length}</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<ArtworkList items={p.artworks} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div className="grid grid-cols-1 gap-4 sm:gap-2 sm:grid-cols-2">
|
||||
{children.map((c) => (
|
||||
<div key={c.id} className="rounded-md border p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div key={c.id} className="space-y-2 pt-3">
|
||||
<div className="flex items-center justify-between text-sm font-medium">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-medium">{c.name}</div>
|
||||
<div className="truncate">{c.name}</div>
|
||||
</div>
|
||||
<Badge variant="outline">{c.artworks.length}</Badge>
|
||||
</div>
|
||||
|
||||
<ArtworkList items={c.artworks} />
|
||||
</div>
|
||||
))}
|
||||
@ -269,12 +275,12 @@ export default async function AnimalListPage() {
|
||||
Tags whose parent is not visible (or not configured for the animal page).
|
||||
</p> */}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<CardContent className="space-y-4 sm:space-y-2">
|
||||
<div className="grid grid-cols-1 gap-4 sm:gap-2 sm:grid-cols-2">
|
||||
{orphans.map((t) => (
|
||||
<div key={t.id} className="rounded-md border p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="truncate text-sm font-medium">{t.name}</div>
|
||||
<div key={t.id} className="space-y-2 pt-3">
|
||||
<div className="flex items-center justify-between text-sm font-medium">
|
||||
<div className="truncate">{t.name}</div>
|
||||
<Badge variant="outline">{t.artworks.length}</Badge>
|
||||
</div>
|
||||
<ArtworkList items={t.artworks} />
|
||||
|
||||
@ -8,7 +8,12 @@ import { PlayCircle } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function SingleArtworkPage({ params }: { params: { id: string }; searchParams: Record<string, string | string[] | undefined>; }) {
|
||||
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: {
|
||||
@ -24,34 +29,44 @@ export default async function SingleArtworkPage({ params }: { params: { id: stri
|
||||
tags: true,
|
||||
variants: true,
|
||||
timelapse: { where: { enabled: true } },
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
if (!artwork) return <div>Artwork with this ID could not be found</div>
|
||||
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 { 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)) ?? []
|
||||
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)"
|
||||
|
||||
: "rgba(0,0,0,0.1), rgba(0,0,0,0.03)";
|
||||
|
||||
return (
|
||||
<div className="px-8 py-4">
|
||||
<div className="px-4 sm:px-8 py-4">
|
||||
<div className="relative w-full min-h-10 flex items-center mb-4">
|
||||
<div className="z-10"><ContextBackButton /></div>
|
||||
<div className="z-10 hidden sm:block">
|
||||
<ContextBackButton />
|
||||
</div>
|
||||
{artwork.name ? (
|
||||
<div className="pointer-events-none absolute left-1/2 -translate-x-1/2 text-center">
|
||||
<div className="pointer-events-auto"><h1 className="text-2xl font-bold mb-4 py-4">{artwork.name}</h1></div>
|
||||
<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"
|
||||
<div
|
||||
className="relative w-full bg-muted items-center justify-center"
|
||||
style={{ aspectRatio: "4 / 3" }}
|
||||
>
|
||||
<Link href={`/raw/${artwork.id}`}>
|
||||
@ -94,7 +109,10 @@ export default async function SingleArtworkPage({ params }: { params: { id: stri
|
||||
tags={artwork.tags}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full flex justify-center sm:hidden">
|
||||
<ContextBackButton className="mx-auto flex justify-center" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
);
|
||||
}
|
||||
@ -2,9 +2,12 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const statusStyles: Record<string, string> = {
|
||||
ACCEPTED: "bg-sky-500/15 text-sky-300 border-sky-500/30",
|
||||
INPROGRESS: "bg-amber-500/15 text-amber-300 border-amber-500/30",
|
||||
COMPLETED: "bg-emerald-500/15 text-emerald-300 border-emerald-500/30",
|
||||
ACCEPTED:
|
||||
"bg-sky-500/20 text-sky-700 border-sky-500/40 dark:bg-sky-500/15 dark:text-sky-300 dark:border-sky-500/30",
|
||||
INPROGRESS:
|
||||
"bg-amber-500/20 text-amber-700 border-amber-500/40 dark:bg-amber-500/15 dark:text-amber-300 dark:border-amber-500/30",
|
||||
COMPLETED:
|
||||
"bg-emerald-500/20 text-emerald-700 border-emerald-500/40 dark:bg-emerald-500/15 dark:text-emerald-300 dark:border-emerald-500/30",
|
||||
};
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
|
||||
@ -105,6 +105,8 @@
|
||||
--border: oklch(0.3289 0.0092 268.3843);
|
||||
--input: oklch(0.3289 0.0092 268.3843);
|
||||
--ring: oklch(0.6132 0.2294 291.7437);
|
||||
--hover: oklch(0.34 0.02 270);
|
||||
--hover-foreground: var(--foreground);
|
||||
--chart-1: oklch(0.8003 0.1821 151.7110);
|
||||
--chart-2: oklch(0.6132 0.2294 291.7437);
|
||||
--chart-3: oklch(0.8077 0.1035 19.5706);
|
||||
|
||||
@ -10,7 +10,7 @@ const FROM_TO_PATH: Record<string, string> = {
|
||||
"animal-index": "/artworks/animalstudies/index"
|
||||
};
|
||||
|
||||
export function ContextBackButton() {
|
||||
export function ContextBackButton({ className }: { className?: string }) {
|
||||
const router = useRouter();
|
||||
const sp = useSearchParams();
|
||||
const from = sp.get("from") ?? "";
|
||||
@ -19,7 +19,7 @@ export function ContextBackButton() {
|
||||
if (!target) return null;
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-xl">
|
||||
<div className={["w-full max-w-xl", className].filter(Boolean).join(" ")}>
|
||||
<Link
|
||||
href={target}
|
||||
className={[
|
||||
|
||||
@ -44,6 +44,7 @@ type Props = {
|
||||
maxRowItems?: number; // desktop
|
||||
maxRowItemsMobile?: number; // <640px
|
||||
gap?: number; // px
|
||||
debug?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
@ -84,6 +85,7 @@ export default function JustifiedGallery({
|
||||
maxRowItems = 5,
|
||||
maxRowItemsMobile = 3,
|
||||
gap = 12,
|
||||
debug = false,
|
||||
className,
|
||||
}: Props) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -125,7 +127,12 @@ export default function JustifiedGallery({
|
||||
|
||||
const isMobile = containerWidth < 640;
|
||||
const targetH = isMobile ? targetRowHeightMobile : targetRowHeight;
|
||||
const maxItems = isMobile ? maxRowItemsMobile : maxRowItems;
|
||||
const maxItems = (() => {
|
||||
if (containerWidth < 480) return Math.min(2, maxRowItemsMobile);
|
||||
if (containerWidth < 720) return Math.min(3, maxRowItems);
|
||||
if (containerWidth < 1024) return Math.min(4, maxRowItems);
|
||||
return maxRowItems;
|
||||
})();
|
||||
|
||||
const rowTiles: RowTile[][] = [];
|
||||
let current: Array<{ item: JustifiedGalleryItem; aspect: number }> = [];
|
||||
@ -155,7 +162,10 @@ export default function JustifiedGallery({
|
||||
aspectSum = 0;
|
||||
};
|
||||
|
||||
for (const it of items) {
|
||||
const workingItems = items.slice();
|
||||
|
||||
for (let i = 0; i < workingItems.length; i += 1) {
|
||||
const it = workingItems[i];
|
||||
const a = aspectOf(it);
|
||||
|
||||
current.push({ item: it, aspect: a });
|
||||
@ -169,7 +179,46 @@ export default function JustifiedGallery({
|
||||
(estimatedWidth >= available || current.length >= maxItems) &&
|
||||
current.length > 1
|
||||
) {
|
||||
const gaps = gap * (current.length - 1);
|
||||
const widthWithoutGaps = Math.max(0, available - gaps);
|
||||
const computedH = widthWithoutGaps / aspectSum;
|
||||
|
||||
// If the row would be shorter than maxRowHeight, reduce items and flush.
|
||||
if (computedH < maxRowHeight && current.length > 1) {
|
||||
const last = current.pop();
|
||||
if (last) {
|
||||
aspectSum -= last.aspect;
|
||||
}
|
||||
|
||||
const limit = widthWithoutGaps / maxRowHeight;
|
||||
let swapped = false;
|
||||
|
||||
for (let look = 1; look <= 2; look += 1) {
|
||||
const idx = i + look;
|
||||
if (idx >= workingItems.length) break;
|
||||
const candidate = workingItems[idx];
|
||||
const candidateAspect = aspectOf(candidate);
|
||||
|
||||
if (aspectSum + candidateAspect <= limit) {
|
||||
workingItems[idx] = last?.item ?? candidate;
|
||||
current.push({ item: candidate, aspect: candidateAspect });
|
||||
aspectSum += candidateAspect;
|
||||
swapped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
flush();
|
||||
if (!swapped && last) {
|
||||
current = [last];
|
||||
aspectSum = last.aspect;
|
||||
} else {
|
||||
current = [];
|
||||
aspectSum = 0;
|
||||
}
|
||||
} else {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,17 +241,26 @@ export default function JustifiedGallery({
|
||||
return `${first}-${last}-${row.length}`;
|
||||
}, []);
|
||||
|
||||
const isSmallScreen = containerWidth < 640;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn("mx-auto w-full max-w-6xl", className)}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{rows.map((row) => (
|
||||
{rows.map((row, idx) => (
|
||||
<div key={getRowKey(row)}>
|
||||
<div
|
||||
key={getRowKey(row)}
|
||||
className="flex justify-center"
|
||||
style={{ gap }}
|
||||
className={cn(
|
||||
"flex",
|
||||
row.length === 1 && (isSmallScreen || idx !== rows.length - 1)
|
||||
? "justify-center"
|
||||
: idx === rows.length - 1
|
||||
? "justify-start"
|
||||
: "justify-between",
|
||||
)}
|
||||
style={{ columnGap: gap }}
|
||||
>
|
||||
{row.map((t) => (
|
||||
<GalleryTile
|
||||
@ -214,6 +272,20 @@ export default function JustifiedGallery({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{debug ? (
|
||||
<div className="text-xs text-muted-foreground font-mono">
|
||||
{`row ${idx + 1} | h=${Math.round(row[0]?.h ?? 0)} | w=${Math.round(
|
||||
row.reduce((sum, t) => sum + t.w, 0) + gap * (row.length - 1),
|
||||
)} | items=${row.length} | `}
|
||||
{row
|
||||
.map(
|
||||
(t) =>
|
||||
`${t.item.id}:${Math.round(t.w)}x${Math.round(t.h)} (src ${t.item.width}x${t.item.height})`,
|
||||
)
|
||||
.join(" | ")}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
@ -90,17 +90,17 @@ export default function PortfolioGallery({
|
||||
dominantHex: it.dominantHex,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (items.length === 0) return;
|
||||
// Debug: inspect dominantHex values coming from the server.
|
||||
console.log(
|
||||
"[PortfolioGallery] dominantHex sample",
|
||||
items.slice(0, 5).map((it) => ({
|
||||
id: it.id,
|
||||
dominantHex: it.dominantHex,
|
||||
}))
|
||||
);
|
||||
}, [items]);
|
||||
// useEffect(() => {
|
||||
// if (items.length === 0) return;
|
||||
// // Debug: inspect dominantHex values coming from the server.
|
||||
// console.log(
|
||||
// "[PortfolioGallery] dominantHex sample",
|
||||
// items.slice(0, 5).map((it) => ({
|
||||
// id: it.id,
|
||||
// dominantHex: it.dominantHex,
|
||||
// }))
|
||||
// );
|
||||
// }, [items]);
|
||||
|
||||
if (!loading && done && galleryItems.length === 0) {
|
||||
return (
|
||||
@ -116,6 +116,7 @@ export default function PortfolioGallery({
|
||||
items={galleryItems}
|
||||
hrefFrom="portfolio"
|
||||
showCaption={false}
|
||||
debug={false}
|
||||
targetRowHeight={160}
|
||||
targetRowHeightMobile={160}
|
||||
maxRowHeight={300}
|
||||
|
||||
Reference in New Issue
Block a user