Add tags to commssion types and custom types. Add button for example images to cards
This commit is contained in:
@ -1,52 +1,43 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import type { CommissionExtra, CommissionOption, CommissionType, CommissionTypeExtra, CommissionTypeOption } from "@/generated/prisma/client"
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import type {
|
||||
CommissionExtra,
|
||||
CommissionOption,
|
||||
CommissionType,
|
||||
CommissionTypeExtra,
|
||||
CommissionTypeOption,
|
||||
Tag,
|
||||
} from "@/generated/prisma/client";
|
||||
import Link from "next/link";
|
||||
|
||||
type CommissionTypeWithItems = CommissionType & {
|
||||
options: (CommissionTypeOption & {
|
||||
option: CommissionOption | null
|
||||
})[]
|
||||
option: CommissionOption | null;
|
||||
})[];
|
||||
extras: (CommissionTypeExtra & {
|
||||
extra: CommissionExtra | null
|
||||
})[]
|
||||
}
|
||||
|
||||
export function CommissionCard({ commission }: { commission: CommissionTypeWithItems }) {
|
||||
// const [open, setOpen] = useState(false)
|
||||
extra: CommissionExtra | null;
|
||||
})[];
|
||||
tags: Tag[];
|
||||
};
|
||||
|
||||
export function CommissionCard({
|
||||
commission,
|
||||
}: {
|
||||
commission: CommissionTypeWithItems;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<Card className="flex flex-col flex-1">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl font-bold">{commission.name}</CardTitle>
|
||||
<p className="text-muted-foreground text-sm">{commission.description}</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{commission.description}
|
||||
</p>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex flex-col justify-start gap-4">
|
||||
{/* {examples && examples.length > 0 && (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<CollapsibleTrigger className="text-sm underline text-muted-foreground">
|
||||
{open ? "Hide Examples" : "See Examples"}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent asChild>
|
||||
<div className="overflow-hidden transition-all data-[state=closed]:max-h-0 data-[state=open]:max-h-[300px]">
|
||||
<div className="flex gap-2 mt-2 overflow-x-auto">
|
||||
{examples.map((src, idx) => (
|
||||
<Image
|
||||
key={src + idx}
|
||||
src={src}
|
||||
width={100}
|
||||
height={100}
|
||||
alt={`${type.name} example ${idx + 1}`}
|
||||
className="h-24 w-auto rounded border"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)} */}
|
||||
<CardContent className="flex flex-col flex-1 justify-start gap-4">
|
||||
<div>
|
||||
<h4 className="font-semibold">Options</h4>
|
||||
<ul className="pl-4 list-disc">
|
||||
@ -66,7 +57,9 @@ export function CommissionCard({ commission }: { commission: CommissionTypeWithI
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{commission.extras.length > 0 && <h4 className="font-semibold">Extras</h4>}
|
||||
{commission.extras.length > 0 && (
|
||||
<h4 className="font-semibold">Extras</h4>
|
||||
)}
|
||||
<ul className="pl-4 list-disc">
|
||||
{commission.extras.map((extra) => (
|
||||
<li key={extra.id}>
|
||||
@ -82,16 +75,21 @@ export function CommissionCard({ commission }: { commission: CommissionTypeWithI
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* <div className="flex flex-wrap gap-2">
|
||||
{commission.extras.map((extra) => (
|
||||
<Badge variant="outline" key={extra.id}>
|
||||
{extra.extra?.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div> */}
|
||||
</CardContent>
|
||||
{commission.tags.length > 0 ? (
|
||||
<div className="mt-auto px-6 pb-6">
|
||||
<Link
|
||||
href={`/portfolio/tagged?tags=${encodeURIComponent(
|
||||
commission.tags.map((t) => t.slug).join(","),
|
||||
)}`}
|
||||
>
|
||||
<Button variant="secondary" className="w-full">
|
||||
View example artworks
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
@ -8,8 +9,10 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import type { Tag } from "@/generated/prisma/client";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
type CustomCardOption = {
|
||||
id: string;
|
||||
@ -33,6 +36,7 @@ export type CommissionCustomCardWithItems = {
|
||||
description: string | null;
|
||||
referenceImageUrl: string | null;
|
||||
isSpecialOffer: boolean;
|
||||
tags: Tag[];
|
||||
options: CustomCardOption[];
|
||||
extras: CustomCardExtra[];
|
||||
};
|
||||
@ -50,7 +54,7 @@ export function CommissionCustomCard({
|
||||
"flex flex-col h-full relative shadow-sm",
|
||||
card.isSpecialOffer
|
||||
? "border-2 border-primary/50"
|
||||
: "border-border"
|
||||
: "border-border",
|
||||
)}
|
||||
>
|
||||
{card.isSpecialOffer ? (
|
||||
@ -143,6 +147,19 @@ export function CommissionCustomCard({
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
{card.tags.length > 0 ? (
|
||||
<div className="mt-auto px-6 pb-6">
|
||||
<Link
|
||||
href={`/portfolio/tagged?tags=${encodeURIComponent(
|
||||
card.tags.map((t) => t.slug).join(","),
|
||||
)}`}
|
||||
>
|
||||
<Button variant="secondary" className="w-full">
|
||||
View example artworks
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
) : null}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
116
src/components/portfolio/TaggedGallery.tsx
Normal file
116
src/components/portfolio/TaggedGallery.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import type {
|
||||
Cursor,
|
||||
TaggedArtworkItem,
|
||||
} from "@/actions/portfolio/getTaggedArtworksPage";
|
||||
import { getTaggedArtworksPage } from "@/actions/portfolio/getTaggedArtworksPage";
|
||||
import JustifiedGallery, {
|
||||
type JustifiedGalleryItem,
|
||||
} from "@/components/gallery/JustifiedGallery";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
export default function TaggedGallery({ tagSlugs }: { tagSlugs: string[] }) {
|
||||
const normalizedSlugs = useMemo(
|
||||
() => tagSlugs.map((s) => s.trim()).filter(Boolean),
|
||||
[tagSlugs],
|
||||
);
|
||||
const resetKey = useMemo(
|
||||
() => normalizedSlugs.slice().sort().join(","),
|
||||
[normalizedSlugs],
|
||||
);
|
||||
|
||||
const [items, setItems] = useState<TaggedArtworkItem[]>([]);
|
||||
const [done, setDone] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const inFlight = useRef(false);
|
||||
const doneRef = useRef(false);
|
||||
doneRef.current = done;
|
||||
const cursorRef = useRef<Cursor>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setItems([]);
|
||||
setDone(false);
|
||||
doneRef.current = false;
|
||||
inFlight.current = false;
|
||||
cursorRef.current = null;
|
||||
}, [resetKey]);
|
||||
|
||||
const loadMore = useCallback(async () => {
|
||||
if (inFlight.current || doneRef.current || normalizedSlugs.length === 0)
|
||||
return 0;
|
||||
inFlight.current = true;
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const data = await getTaggedArtworksPage({
|
||||
take: 60,
|
||||
cursor: cursorRef.current,
|
||||
tagSlugs: normalizedSlugs,
|
||||
onlyPublished: true,
|
||||
});
|
||||
|
||||
setItems((prev) => {
|
||||
const seen = new Set(prev.map((x) => x.id));
|
||||
const next = data.items.filter((x) => !seen.has(x.id));
|
||||
return prev.concat(next);
|
||||
});
|
||||
|
||||
cursorRef.current = data.nextCursor;
|
||||
if (!data.nextCursor) setDone(true);
|
||||
|
||||
return data.items.length;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
inFlight.current = false;
|
||||
}
|
||||
}, [normalizedSlugs]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadMore();
|
||||
}, [loadMore]);
|
||||
|
||||
const galleryItems: JustifiedGalleryItem[] = items.map((it) => ({
|
||||
id: it.id,
|
||||
name: it.name,
|
||||
altText: it.altText,
|
||||
fileKey: it.fileKey,
|
||||
width: it.thumbW,
|
||||
height: it.thumbH,
|
||||
dominantHex: it.dominantHex,
|
||||
}));
|
||||
|
||||
if (!loading && done && galleryItems.length === 0) {
|
||||
return (
|
||||
<p className="text-muted-foreground text-center py-20">
|
||||
No artworks to display
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<JustifiedGallery
|
||||
items={galleryItems}
|
||||
hrefFrom="portfolio"
|
||||
showCaption={false}
|
||||
debug={false}
|
||||
targetRowHeight={160}
|
||||
targetRowHeightMobile={160}
|
||||
maxRowHeight={300}
|
||||
maxRowItems={5}
|
||||
maxRowItemsMobile={1}
|
||||
gap={12}
|
||||
gapBreakpoints={[
|
||||
{ maxWidth: 685, gap: 6 },
|
||||
{ maxWidth: 910, gap: 8 },
|
||||
{ maxWidth: 1130, gap: 10 },
|
||||
]}
|
||||
onLoadMore={done ? undefined : () => void loadMore()}
|
||||
hasMore={!done}
|
||||
isLoadingMore={loading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user