Refactor galleries and single page
This commit is contained in:
@ -55,9 +55,9 @@ export default async function PortfolioPage({
|
|||||||
<div className="mx-auto w-full max-w-6xl px-4 py-8">
|
<div className="mx-auto w-full max-w-6xl px-4 py-8">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-semibold">Portfolio</h1>
|
<h1 className="text-2xl font-semibold">Portfolio</h1>
|
||||||
|
<PortfolioFiltersBar years={years} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PortfolioFiltersBar years={years} />
|
|
||||||
<ColorMasonryGallery filters={filters} />
|
<ColorMasonryGallery filters={filters} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
|
import ArtworkMetaCard from "@/components/artworks/ArtworkMetaCard";
|
||||||
|
import { ContextBackButton } from "@/components/artworks/ContextBackButton";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { LayersIcon, QuoteIcon, TagIcon } 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 } }) {
|
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: {
|
||||||
@ -36,10 +37,15 @@ export default async function SingleArtworkPage({ params }: { params: { id: stri
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-8 py-4">
|
<div className="px-8 py-4">
|
||||||
<div className="flex flex-col items-center gap-4">
|
<div className="relative w-full min-h-10 flex items-center mb-4">
|
||||||
<div>
|
<div className="z-10"><ContextBackButton /></div>
|
||||||
<h1 className="text-2xl font-bold mb-4 pb-4">{artwork.name}</h1>
|
{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>
|
</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="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" }}
|
||||||
@ -54,109 +60,23 @@ export default async function SingleArtworkPage({ params }: { params: { id: stri
|
|||||||
className={cn("object-cover transition duration-300")}
|
className={cn("object-cover transition duration-300")}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
{/* {image.nsfw && (
|
|
||||||
<div className="absolute top-1 left-1 z-10 flex gap-1">
|
|
||||||
<TagBadge
|
|
||||||
label="NSFW"
|
|
||||||
variant="destructive"
|
|
||||||
icon={<EyeOffIcon size={12} />}
|
|
||||||
className="text-xs px-2 py-0.5 inline-flex items-center"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
{/* {!isLarge && (
|
|
||||||
<div className="p-4">
|
|
||||||
<h2 className="text-lg font-semibold truncate text-center">{image.imageName}</h2>
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="rounded-lg"
|
className="rounded-lg"
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${gradientColors})`,
|
background: `linear-gradient(135deg, ${gradientColors})`,
|
||||||
}}>
|
}}
|
||||||
<div
|
|
||||||
className="overflow-hidden border rounded-lg m-0.5 p-4 shadow w-full max-w-xl space-y-3 bg-white dark:bg-black"
|
|
||||||
>
|
>
|
||||||
{artwork.altText && (
|
<ArtworkMetaCard
|
||||||
<div className="flex items-center gap-2">
|
gradientColors={gradientColors}
|
||||||
<TagIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-px" />
|
altText={artwork.altText}
|
||||||
<span className="text-sm text-muted-foreground">{artwork.altText}</span>
|
description={artwork.description}
|
||||||
</div>
|
categories={artwork.categories}
|
||||||
)}
|
tags={artwork.tags}
|
||||||
|
|
||||||
{artwork.description && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<QuoteIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-px" />
|
|
||||||
<span className="text-sm text-muted-foreground">{artwork.description}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* {creationLabel && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<CalendarDaysIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-[1px]" />
|
|
||||||
<span className="text-sm text-muted-foreground">{creationLabel}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{album && (
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<ImagesIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-[1px]" />
|
|
||||||
<Link href={`/galleries/${album.gallery?.slug}/${album.slug}`} className="text-sm underline">
|
|
||||||
{album.name}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{artwork.categories.length > 0 && (
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<LayersIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-1px" />
|
|
||||||
{artwork.categories.map((cat) => (
|
|
||||||
<Link
|
|
||||||
key={cat.id}
|
|
||||||
href={`/categories/${cat.id}`}
|
|
||||||
className="text-sm underline"
|
|
||||||
>
|
|
||||||
{cat.name}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{artwork.tags.length > 0 && (
|
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
|
||||||
<TagIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-1px" />
|
|
||||||
{artwork.tags.map((tag) => (
|
|
||||||
<Link
|
|
||||||
key={tag.id}
|
|
||||||
href={`/tags/${tag.id}`}
|
|
||||||
className="text-sm underline"
|
|
||||||
>
|
|
||||||
{tag.name}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* <ImageCard image={image} gallerySlug={image.album?.gallery?.slug} albumSlug={image.album?.slug} size="large" />
|
|
||||||
<section className="py-8 flex flex-col gap-4">
|
|
||||||
{image.artist && <ArtistInfoBox artist={image.artist} />}
|
|
||||||
<ImageMetadataBox
|
|
||||||
album={image.album}
|
|
||||||
categories={image.categories}
|
|
||||||
tags={image.tags}
|
|
||||||
creationDate={image.creationDate}
|
|
||||||
creationYear={image.creationYear}
|
|
||||||
creationMonth={image.creationMonth}
|
|
||||||
altText={image.altText}
|
|
||||||
description={image.description}
|
|
||||||
/>
|
/>
|
||||||
</section> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -16,7 +16,7 @@ export default function NormalLayout({
|
|||||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
|
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60">
|
||||||
<Header />
|
<Header />
|
||||||
</header>
|
</header>
|
||||||
<main className="container mx-auto">
|
<main>
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
<footer className="mt-auto p-4 h-14 border-t bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 ">
|
<footer className="mt-auto p-4 h-14 border-t bg-background/95 backdrop-blur supports-backdrop-filter:bg-background/60 ">
|
||||||
|
|||||||
@ -143,10 +143,6 @@
|
|||||||
@apply list-decimal pl-6 my-4;
|
@apply list-decimal pl-6 my-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown li {
|
|
||||||
/* @apply mb-1; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown blockquote {
|
.markdown blockquote {
|
||||||
@apply border-l-4 pl-4 italic text-muted-foreground my-4;
|
@apply border-l-4 pl-4 italic text-muted-foreground my-4;
|
||||||
}
|
}
|
||||||
@ -175,9 +171,9 @@
|
|||||||
@apply line-through text-muted-foreground;
|
@apply line-through text-muted-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
div:hover {
|
/* div:hover {
|
||||||
border-color: var(--hover-border-color);
|
border-color: var(--hover-border-color);
|
||||||
}
|
} */
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
|
|||||||
131
src/components/artworks/ArtworkImageCard.tsx
Normal file
131
src/components/artworks/ArtworkImageCard.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
type ClampOpts = { maxIntrinsic: number };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clamps intrinsic w/h while preserving aspect ratio.
|
||||||
|
* This helps prevent Next/Image from treating a tiny thumbnail as a large asset.
|
||||||
|
*/
|
||||||
|
function clampDims(w: number, h: number, { maxIntrinsic }: ClampOpts) {
|
||||||
|
const W = Math.max(1, w);
|
||||||
|
const H = Math.max(1, h);
|
||||||
|
const maxSide = Math.max(W, H);
|
||||||
|
if (maxSide <= maxIntrinsic) return { w: W, h: H };
|
||||||
|
|
||||||
|
const scale = maxIntrinsic / maxSide;
|
||||||
|
return {
|
||||||
|
w: Math.max(1, Math.round(W * scale)),
|
||||||
|
h: Math.max(1, Math.round(H * scale)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ArtworkImageCardProps = {
|
||||||
|
href: string;
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide real dimensions if you have them.
|
||||||
|
* For tile mode, we still pass intrinsic sizes, but clamp them.
|
||||||
|
*/
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tile: uses `fill` + container aspect ratio (best for masonry/grid)
|
||||||
|
* hero: uses width/height when available (best for single page)
|
||||||
|
*/
|
||||||
|
mode?: "tile" | "hero";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For tile mode only.
|
||||||
|
* Example: "4 / 3" or `${w} / ${h}`
|
||||||
|
*/
|
||||||
|
aspectRatio?: string;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
imageClassName?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls intrinsic clamp for thumbnails:
|
||||||
|
* you asked: “max the double of 160” => 320 default.
|
||||||
|
*/
|
||||||
|
maxThumbIntrinsic?: number;
|
||||||
|
|
||||||
|
sizes?: string;
|
||||||
|
priority?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
linkClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ArtworkImageCard({
|
||||||
|
href,
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
mode = "tile",
|
||||||
|
aspectRatio = "4 / 3",
|
||||||
|
className,
|
||||||
|
imageClassName,
|
||||||
|
maxThumbIntrinsic = 320,
|
||||||
|
sizes,
|
||||||
|
priority,
|
||||||
|
style,
|
||||||
|
}: ArtworkImageCardProps) {
|
||||||
|
const w = width ?? 0;
|
||||||
|
const h = height ?? 0;
|
||||||
|
|
||||||
|
const isHero = mode === "hero";
|
||||||
|
const hasDims = w > 0 && h > 0;
|
||||||
|
|
||||||
|
// Clamp only for non-hero (thumbnail-like) usage
|
||||||
|
const clamped = !isHero && hasDims ? clampDims(w, h, { maxIntrinsic: maxThumbIntrinsic }) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={style}
|
||||||
|
className={[
|
||||||
|
"group rounded-lg overflow-hidden bg-background relative",
|
||||||
|
// IMPORTANT: put the border here so the --dom hover works again
|
||||||
|
"border border-transparent transition-colors duration-150 hover:border-(--dom)",
|
||||||
|
"hover:shadow-lg transition-shadow",
|
||||||
|
className ?? "",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="relative w-full bg-muted items-center justify-center"
|
||||||
|
style={mode === "tile" ? { aspectRatio } : undefined}
|
||||||
|
>
|
||||||
|
<Link href={href} className="block">
|
||||||
|
{isHero ? (
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
// If we have dimensions, use them; otherwise fallback to fill.
|
||||||
|
width={hasDims ? w : undefined}
|
||||||
|
height={hasDims ? h : undefined}
|
||||||
|
fill={!hasDims}
|
||||||
|
className={["object-cover transition duration-300", imageClassName ?? ""].join(" ")}
|
||||||
|
sizes={sizes}
|
||||||
|
priority={priority}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
// Pass clamped intrinsic sizes when possible
|
||||||
|
width={clamped?.w ?? 320}
|
||||||
|
height={clamped?.h ?? 240}
|
||||||
|
className={["w-full h-full object-cover select-none", imageClassName ?? ""].join(" ")}
|
||||||
|
loading={priority ? "eager" : "lazy"}
|
||||||
|
sizes={sizes}
|
||||||
|
priority={priority}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
src/components/artworks/ArtworkMetaCard.tsx
Normal file
72
src/components/artworks/ArtworkMetaCard.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { LayersIcon, QuoteIcon, TagIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export default function ArtworkMetaCard({
|
||||||
|
gradientColors,
|
||||||
|
altText,
|
||||||
|
description,
|
||||||
|
categories,
|
||||||
|
tags,
|
||||||
|
}: {
|
||||||
|
gradientColors: string;
|
||||||
|
altText?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
categories: { id: string; name: string }[];
|
||||||
|
tags: { id: string; name: string }[];
|
||||||
|
}) {
|
||||||
|
const hasData =
|
||||||
|
Boolean(altText) ||
|
||||||
|
Boolean(description) ||
|
||||||
|
categories.length > 0 ||
|
||||||
|
tags.length > 0;
|
||||||
|
|
||||||
|
if (!hasData) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="w-full max-w-xl rounded-xl p-px"
|
||||||
|
style={{ background: `linear-gradient(135deg, ${gradientColors})` }}
|
||||||
|
>
|
||||||
|
<div className="rounded-[11px] border bg-background p-4 shadow-sm space-y-3">
|
||||||
|
{altText && (
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<TagIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-0.5" />
|
||||||
|
<span className="text-sm text-muted-foreground">{altText}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{description && (
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<QuoteIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-0.5" />
|
||||||
|
<span className="text-sm text-muted-foreground">{description}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{categories.length > 0 && (
|
||||||
|
<div className="flex items-start gap-2 flex-wrap">
|
||||||
|
<LayersIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-0.5" />
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<div key={cat.id} className="text-sm underline">
|
||||||
|
{/* <Link key={cat.id} href={`/categories/${cat.id}`} className="text-sm underline"> */}
|
||||||
|
{cat.name}
|
||||||
|
{/* </Link> */}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tags.length > 0 && (
|
||||||
|
<div className="flex items-start gap-2 flex-wrap">
|
||||||
|
<TagIcon className="shrink-0 w-4 h-4 text-muted-foreground mt-0.5" />
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<div key={tag.id} className="text-sm underline">
|
||||||
|
{/* <Link key={tag.id} href={`/tags/${tag.id}`} className="text-sm underline"> */}
|
||||||
|
{tag.name}
|
||||||
|
{/* </Link> */}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,9 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { ArtworkImageCard } from "./ArtworkImageCard";
|
||||||
|
|
||||||
type ArtworkGalleryItem = {
|
type ArtworkGalleryItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -77,37 +76,36 @@ export default function ArtworkThumbGallery({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={a.id} className="w-full" style={tileStyle}>
|
<div key={a.id} className="w-full" style={tileStyle}>
|
||||||
<div className="relative h-full w-full overflow-hidden rounded-md border">
|
<div className="relative h-full w-full">
|
||||||
<Link href={`${hrefBase}/single/${a.id}`} className="block h-full w-full">
|
<ArtworkImageCard
|
||||||
<Image
|
mode="tile"
|
||||||
|
href={`${hrefBase}/single/${a.id}?from=animal-studies`}
|
||||||
src={`/api/image/thumbnail/${a.file.fileKey}.webp`}
|
src={`/api/image/thumbnail/${a.file.fileKey}.webp`}
|
||||||
alt={a.altText ?? a.name ?? "Artwork"}
|
alt={a.altText ?? a.name ?? "Artwork"}
|
||||||
fill
|
width={a.metadata?.width ?? 0}
|
||||||
className="object-cover"
|
height={a.metadata?.height ?? 0}
|
||||||
loading="lazy"
|
aspectRatio={`${w} / ${h}`}
|
||||||
|
className="h-full w-full rounded-md"
|
||||||
|
imageClassName="object-cover"
|
||||||
sizes="(min-width: 1280px) 20vw, (min-width: 1024px) 25vw, (min-width: 768px) 33vw, (min-width: 640px) 50vw, 100vw"
|
sizes="(min-width: 1280px) 20vw, (min-width: 1024px) 25vw, (min-width: 768px) 33vw, (min-width: 640px) 50vw, 100vw"
|
||||||
/>
|
/>
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Title overlay */}
|
{/* Title overlay (restored) */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute left-0 right-0 top-0 px-3 py-2",
|
"pointer-events-none absolute left-0 right-0 top-0 px-3 py-2",
|
||||||
bgClass,
|
bgClass,
|
||||||
"backdrop-blur-[1px]"
|
"backdrop-blur-[1px]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={cn("truncate text-sm font-medium", textClass)}>
|
<div className={cn("truncate text-sm font-medium", textClass)}>{a.name}</div>
|
||||||
{a.name}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom button bar (reserved space) */}
|
{/* Bottom reserved bar (if you need it later) */}
|
||||||
<div
|
<div
|
||||||
className="absolute left-0 right-0 bottom-0 z-20 flex items-center justify-between px-2"
|
className="absolute left-0 right-0 bottom-0 z-20 flex items-center justify-between px-2"
|
||||||
style={{ height: BUTTON_BAR_HEIGHT }}
|
style={{ height: BUTTON_BAR_HEIGHT }}
|
||||||
>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
43
src/components/artworks/ContextBackButton.tsx
Normal file
43
src/components/artworks/ContextBackButton.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ArrowLeftIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
|
const FROM_TO_PATH: Record<string, string> = {
|
||||||
|
portfolio: "/portfolio",
|
||||||
|
"animal-studies": "/animal-studies",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ContextBackButton() {
|
||||||
|
const router = useRouter();
|
||||||
|
const sp = useSearchParams();
|
||||||
|
const from = sp.get("from") ?? "";
|
||||||
|
|
||||||
|
const target = FROM_TO_PATH[from];
|
||||||
|
if (!target) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-xl">
|
||||||
|
<Link
|
||||||
|
href={target}
|
||||||
|
className={[
|
||||||
|
"inline-flex items-center gap-2 rounded-md border px-3 py-2",
|
||||||
|
"text-sm text-muted-foreground hover:text-foreground",
|
||||||
|
"hover:bg-muted transition-colors",
|
||||||
|
].join(" ")}
|
||||||
|
onClick={(e) => {
|
||||||
|
// If user has real history, prefer back; otherwise use link target.
|
||||||
|
// (Prevents “dead end” if they opened single page directly in new tab.)
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
e.preventDefault();
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon className="h-4 w-4" />
|
||||||
|
Back
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,7 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -10,6 +8,7 @@ import type {
|
|||||||
PortfolioFilters,
|
PortfolioFilters,
|
||||||
} from "@/actions/portfolio/getPortfolioArtworksPage";
|
} from "@/actions/portfolio/getPortfolioArtworksPage";
|
||||||
import { getPortfolioArtworksPage } from "@/actions/portfolio/getPortfolioArtworksPage";
|
import { getPortfolioArtworksPage } from "@/actions/portfolio/getPortfolioArtworksPage";
|
||||||
|
import { ArtworkImageCard } from "../artworks/ArtworkImageCard";
|
||||||
|
|
||||||
type Placement = {
|
type Placement = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -188,8 +187,6 @@ export default function ColorMasonryGallery({
|
|||||||
const it = itemsById.get(p.id);
|
const it = itemsById.get(p.id);
|
||||||
if (!it) return null;
|
if (!it) return null;
|
||||||
|
|
||||||
const href = `/artworks/single/${it.id}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={p.id}
|
key={p.id}
|
||||||
@ -200,27 +197,19 @@ export default function ColorMasonryGallery({
|
|||||||
height: p.h,
|
height: p.h,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Link
|
{/* <div style={{ ["--dom" as any]: p.dominantHex }} className="h-full w-full"> */}
|
||||||
href={href}
|
<ArtworkImageCard
|
||||||
className={[
|
mode="tile"
|
||||||
"block w-full h-full overflow-hidden rounded-md",
|
href={`/artworks/single/${it.id}?from=portfolio`}
|
||||||
"border border-transparent",
|
|
||||||
"transition-colors duration-150",
|
|
||||||
"hover:border-(--dom)",
|
|
||||||
].join(" ")}
|
|
||||||
style={{ ["--dom" as any]: p.dominantHex }}
|
|
||||||
aria-label={`Open ${it.name}`}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={thumbUrl(it.fileKey)}
|
src={thumbUrl(it.fileKey)}
|
||||||
alt={it.altText ?? it.name}
|
alt={it.altText ?? it.name}
|
||||||
width={Math.max(1, it.thumbW)}
|
width={Math.max(1, it.thumbW)}
|
||||||
height={Math.max(1, it.thumbH)}
|
height={Math.max(1, it.thumbH)}
|
||||||
className="w-full h-full object-cover select-none"
|
style={{ ["--dom" as any]: p.dominantHex }}
|
||||||
loading="lazy"
|
className="w-full h-full rounded-md"
|
||||||
/>
|
/>
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
// </div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user