Compare commits
4 Commits
ych
...
responsive
| Author | SHA1 | Date | |
|---|---|---|---|
|
26118d2897
|
|||
|
d70f00314b
|
|||
|
874aa5f343
|
|||
|
b559b8250f
|
@ -8,7 +8,12 @@ import { PlayCircle } from "lucide-react";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
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 { id } = await params;
|
||||||
const artwork = await prisma.artwork.findUnique({
|
const artwork = await prisma.artwork.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@ -24,34 +29,44 @@ export default async function SingleArtworkPage({ params }: { params: { id: stri
|
|||||||
tags: true,
|
tags: true,
|
||||||
variants: true,
|
variants: true,
|
||||||
timelapse: { where: { enabled: 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 =
|
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
|
const gradientColors = colors.length
|
||||||
? colors.join(", ")
|
? 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 (
|
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="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 ? (
|
{artwork.name ? (
|
||||||
<div className="pointer-events-none absolute left-1/2 -translate-x-1/2 text-center">
|
<div className="w-full text-center sm:pointer-events-none sm:absolute sm:left-1/2 sm:-translate-x-1/2">
|
||||||
<div className="pointer-events-auto"><h1 className="text-2xl font-bold mb-4 py-4">{artwork.name}</h1></div>
|
<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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center gap-4">
|
<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="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" }}
|
style={{ aspectRatio: "4 / 3" }}
|
||||||
>
|
>
|
||||||
<Link href={`/raw/${artwork.id}`}>
|
<Link href={`/raw/${artwork.id}`}>
|
||||||
@ -94,7 +109,10 @@ export default async function SingleArtworkPage({ params }: { params: { id: stri
|
|||||||
tags={artwork.tags}
|
tags={artwork.tags}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="w-full flex justify-center sm:hidden">
|
||||||
|
<ContextBackButton className="mx-auto flex justify-center" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div >
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2,9 +2,12 @@ import { Badge } from "@/components/ui/badge";
|
|||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
const statusStyles: Record<string, string> = {
|
const statusStyles: Record<string, string> = {
|
||||||
ACCEPTED: "bg-sky-500/15 text-sky-300 border-sky-500/30",
|
ACCEPTED:
|
||||||
INPROGRESS: "bg-amber-500/15 text-amber-300 border-amber-500/30",
|
"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",
|
||||||
COMPLETED: "bg-emerald-500/15 text-emerald-300 border-emerald-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> = {
|
const statusLabels: Record<string, string> = {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const FROM_TO_PATH: Record<string, string> = {
|
|||||||
"animal-index": "/artworks/animalstudies/index"
|
"animal-index": "/artworks/animalstudies/index"
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ContextBackButton() {
|
export function ContextBackButton({ className }: { className?: string }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const sp = useSearchParams();
|
const sp = useSearchParams();
|
||||||
const from = sp.get("from") ?? "";
|
const from = sp.get("from") ?? "";
|
||||||
@ -19,7 +19,7 @@ export function ContextBackButton() {
|
|||||||
if (!target) return null;
|
if (!target) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-xl">
|
<div className={["w-full max-w-xl", className].filter(Boolean).join(" ")}>
|
||||||
<Link
|
<Link
|
||||||
href={target}
|
href={target}
|
||||||
className={[
|
className={[
|
||||||
|
|||||||
@ -44,6 +44,7 @@ type Props = {
|
|||||||
maxRowItems?: number; // desktop
|
maxRowItems?: number; // desktop
|
||||||
maxRowItemsMobile?: number; // <640px
|
maxRowItemsMobile?: number; // <640px
|
||||||
gap?: number; // px
|
gap?: number; // px
|
||||||
|
debug?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -84,6 +85,7 @@ export default function JustifiedGallery({
|
|||||||
maxRowItems = 5,
|
maxRowItems = 5,
|
||||||
maxRowItemsMobile = 3,
|
maxRowItemsMobile = 3,
|
||||||
gap = 12,
|
gap = 12,
|
||||||
|
debug = false,
|
||||||
className,
|
className,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
@ -125,7 +127,12 @@ export default function JustifiedGallery({
|
|||||||
|
|
||||||
const isMobile = containerWidth < 640;
|
const isMobile = containerWidth < 640;
|
||||||
const targetH = isMobile ? targetRowHeightMobile : targetRowHeight;
|
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[][] = [];
|
const rowTiles: RowTile[][] = [];
|
||||||
let current: Array<{ item: JustifiedGalleryItem; aspect: number }> = [];
|
let current: Array<{ item: JustifiedGalleryItem; aspect: number }> = [];
|
||||||
@ -155,7 +162,10 @@ export default function JustifiedGallery({
|
|||||||
aspectSum = 0;
|
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);
|
const a = aspectOf(it);
|
||||||
|
|
||||||
current.push({ item: it, aspect: a });
|
current.push({ item: it, aspect: a });
|
||||||
@ -169,7 +179,46 @@ export default function JustifiedGallery({
|
|||||||
(estimatedWidth >= available || current.length >= maxItems) &&
|
(estimatedWidth >= available || current.length >= maxItems) &&
|
||||||
current.length > 1
|
current.length > 1
|
||||||
) {
|
) {
|
||||||
flush();
|
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,27 +241,50 @@ export default function JustifiedGallery({
|
|||||||
return `${first}-${last}-${row.length}`;
|
return `${first}-${last}-${row.length}`;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const isSmallScreen = containerWidth < 640;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={cn("mx-auto w-full max-w-6xl", className)}
|
className={cn("mx-auto w-full max-w-6xl", className)}
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{rows.map((row) => (
|
{rows.map((row, idx) => (
|
||||||
<div
|
<div key={getRowKey(row)}>
|
||||||
key={getRowKey(row)}
|
<div
|
||||||
className="flex justify-center"
|
className={cn(
|
||||||
style={{ gap }}
|
"flex",
|
||||||
>
|
row.length === 1 && (isSmallScreen || idx !== rows.length - 1)
|
||||||
{row.map((t) => (
|
? "justify-center"
|
||||||
<GalleryTile
|
: idx === rows.length - 1
|
||||||
key={t.item.id}
|
? "justify-start"
|
||||||
tile={t}
|
: "justify-between",
|
||||||
hrefBase={hrefBase}
|
)}
|
||||||
hrefFrom={hrefFrom}
|
style={{ columnGap: gap }}
|
||||||
showCaption={showCaption}
|
>
|
||||||
/>
|
{row.map((t) => (
|
||||||
))}
|
<GalleryTile
|
||||||
|
key={t.item.id}
|
||||||
|
tile={t}
|
||||||
|
hrefBase={hrefBase}
|
||||||
|
hrefFrom={hrefFrom}
|
||||||
|
showCaption={showCaption}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -90,17 +90,17 @@ export default function PortfolioGallery({
|
|||||||
dominantHex: it.dominantHex,
|
dominantHex: it.dominantHex,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (items.length === 0) return;
|
// if (items.length === 0) return;
|
||||||
// Debug: inspect dominantHex values coming from the server.
|
// // Debug: inspect dominantHex values coming from the server.
|
||||||
console.log(
|
// console.log(
|
||||||
"[PortfolioGallery] dominantHex sample",
|
// "[PortfolioGallery] dominantHex sample",
|
||||||
items.slice(0, 5).map((it) => ({
|
// items.slice(0, 5).map((it) => ({
|
||||||
id: it.id,
|
// id: it.id,
|
||||||
dominantHex: it.dominantHex,
|
// dominantHex: it.dominantHex,
|
||||||
}))
|
// }))
|
||||||
);
|
// );
|
||||||
}, [items]);
|
// }, [items]);
|
||||||
|
|
||||||
if (!loading && done && galleryItems.length === 0) {
|
if (!loading && done && galleryItems.length === 0) {
|
||||||
return (
|
return (
|
||||||
@ -116,6 +116,7 @@ export default function PortfolioGallery({
|
|||||||
items={galleryItems}
|
items={galleryItems}
|
||||||
hrefFrom="portfolio"
|
hrefFrom="portfolio"
|
||||||
showCaption={false}
|
showCaption={false}
|
||||||
|
debug={false}
|
||||||
targetRowHeight={160}
|
targetRowHeight={160}
|
||||||
targetRowHeightMobile={160}
|
targetRowHeightMobile={160}
|
||||||
maxRowHeight={300}
|
maxRowHeight={300}
|
||||||
|
|||||||
Reference in New Issue
Block a user