From b41a4d2908943db6641262c6e29e242fc8655715 Mon Sep 17 00:00:00 2001 From: Citali Date: Sun, 21 Dec 2025 20:20:07 +0100 Subject: [PATCH] Add filtering --- bun.lock | 13 + package.json | 3 + prisma/schema.prisma | 5 +- .../(normal)/artworks/animalstudies/page.tsx | 103 ++++++- src/app/(normal)/page.tsx | 53 +++- src/components/artworks/TagFilterDialog.tsx | 287 ++++++++++++++++++ src/components/global/Banner.tsx | 6 - src/components/ui/checkbox.tsx | 32 ++ src/components/ui/dialog.tsx | 143 +++++++++ src/components/ui/scroll-area.tsx | 58 ++++ src/components/ui/separator.tsx | 28 ++ 11 files changed, 707 insertions(+), 24 deletions(-) create mode 100644 src/components/artworks/TagFilterDialog.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/scroll-area.tsx create mode 100644 src/components/ui/separator.tsx diff --git a/bun.lock b/bun.lock index 4e5d9ae..ad81221 100644 --- a/bun.lock +++ b/bun.lock @@ -8,9 +8,12 @@ "@aws-sdk/s3-request-presigner": "^3.954.0", "@prisma/adapter-pg": "^7.2.0", "@prisma/client": "^7.2.0", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", "class-variance-authority": "^0.7.1", @@ -276,10 +279,14 @@ "@prisma/studio-core": ["@prisma/studio-core@0.9.0", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-xA2zoR/ADu/NCSQuriBKTh6Ps4XjU0bErkEcgMfnSGh346K1VI7iWKnoq1l2DoxUqiddPHIEWwtxJ6xCHG6W7g=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], @@ -314,6 +321,10 @@ "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], + + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], @@ -760,6 +771,8 @@ "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "c12/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], diff --git a/package.json b/package.json index bc64d05..a113bc9 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,12 @@ "@aws-sdk/s3-request-presigner": "^3.954.0", "@prisma/adapter-pg": "^7.2.0", "@prisma/client": "^7.2.0", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-switch": "^1.2.6", "class-variance-authority": "^0.7.1", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e60b4df..59def6a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -101,8 +101,9 @@ model ArtTag { updatedAt DateTime @updatedAt sortIndex Int @default(0) - name String @unique - slug String @unique + name String @unique + slug String @unique + showOnAnimalPage Boolean @default(false) description String? diff --git a/src/app/(normal)/artworks/animalstudies/page.tsx b/src/app/(normal)/artworks/animalstudies/page.tsx index bbee9f6..4714cb5 100644 --- a/src/app/(normal)/artworks/animalstudies/page.tsx +++ b/src/app/(normal)/artworks/animalstudies/page.tsx @@ -1,32 +1,109 @@ import ArtworkThumbGallery from "@/components/artworks/ArtworkThumbGallery"; +import TagFilterDialog from "@/components/artworks/TagFilterDialog"; +import { Button } from "@/components/ui/button"; import { prisma } from "@/lib/prisma"; +import { ListIcon } from "lucide-react"; +import Link from "next/link"; + +function parseTagsParam(tags: string | string[] | undefined): string[] { + if (!tags) return []; + const raw = Array.isArray(tags) ? tags.join(",") : tags; + return raw + .split(",") + .map((s) => s.trim()) + .filter(Boolean); +} + +function expandSelectedWithChildren( + selectedSlugs: string[], + tagsForFilter: Array<{ + slug: string; + children: Array<{ slug: string }>; + }> +) { + const bySlug = new Map(tagsForFilter.map((t) => [t.slug, t])); + const out = new Set(selectedSlugs); + + for (const slug of selectedSlugs) { + const t = bySlug.get(slug); + if (!t) continue; + for (const c of t.children ?? []) out.add(c.slug); + } + + return Array.from(out); +} + +export default async function AnimalStudiesPage({ searchParams }: { searchParams: { tags?: string | string[] } }) { + const { tags } = await searchParams; + + const selectedTagSlugs = parseTagsParam(tags); + const tagsForFilter = await prisma.artTag.findMany({ + where: { showOnAnimalPage: true }, + select: { + id: true, + name: true, + slug: true, + sortIndex: true, + parentId: true, + parent: { select: { id: true, name: true, slug: true, sortIndex: true } }, + children: { + where: { showOnAnimalPage: true }, + select: { id: true, name: true, slug: true, sortIndex: true, parentId: true }, + orderBy: [{ sortIndex: "asc" }, { name: "asc" }], + }, + }, + orderBy: [{ sortIndex: "asc" }, { name: "asc" }], + }); + + const expandedTagSlugs = expandSelectedWithChildren(selectedTagSlugs, tagsForFilter); -export default async function AnimalStudiesPage() { const artworks = await prisma.artwork.findMany({ where: { - categories: { - some: { - name: "Animal Studies" - } - }, - published: true + categories: { some: { name: "Animal Studies" } }, + published: true, + ...(expandedTagSlugs.length + ? { tags: { some: { slug: { in: expandedTagSlugs } } } } + : {}), }, include: { file: true, metadata: true, tags: true, - variants: true + variants: true, }, - orderBy: [{ sortKey: "asc" }, { id: "asc" }] - }) + orderBy: [{ sortKey: "asc" }, { id: "asc" }], + }); // console.log(JSON.stringify(artworks, null, 4)) return (
-
-

Animal studies

-
+
+
+

+ Animal studies +

+

+ {selectedTagSlugs.length > 0 + ? `Filtered by ${selectedTagSlugs.length} tag${selectedTagSlugs.length === 1 ? "" : "s"}` + : "Browse all published artworks in this category."} +

+
+ +
+ + + +
+
diff --git a/src/app/(normal)/page.tsx b/src/app/(normal)/page.tsx index 0b31dfb..da82503 100644 --- a/src/app/(normal)/page.tsx +++ b/src/app/(normal)/page.tsx @@ -1,8 +1,55 @@ +import { ArrowBigRight } from "lucide-react"; export default function Home() { return ( -
- Home -
+
+ {/* Hero Section */} +
+

+ Welcome to my place! +

+

+ I'm an illustrator, character designer, miniature painter, 3d modeller, makeup artist and much more and happy to show you things i've created. +

+

+ If you want to commission me
you can find all the information you need here:
Linktree +

+ + {/* Search */} + {/*
+ + + + +
*/} +
+ + + {/* Section Cards */} +
+ {/*

+ If you want to commission me you can find all the information you need under following link: Linktree +

*/} + {/* {sections.map((section) => ( + + + + + + {section.title} + + + + {section.description} + + + + ))} */} +
+
); } diff --git a/src/components/artworks/TagFilterDialog.tsx b/src/components/artworks/TagFilterDialog.tsx new file mode 100644 index 0000000..eb77944 --- /dev/null +++ b/src/components/artworks/TagFilterDialog.tsx @@ -0,0 +1,287 @@ +"use client"; + +import { FilterIcon, XIcon } from "lucide-react"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import * as React from "react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; + +type Tag = { + id: string; + name: string; + slug: string; + sortIndex: number; + parentId: string | null; + // these may exist, but we do NOT rely on them: + parent?: { id: string; name: string; slug: string; sortIndex: number } | null; + children?: { id: string; name: string; slug: string; sortIndex: number; parentId: string | null }[]; +}; + +function toTagsParam(slugs: string[]) { + return slugs.join(","); +} + +function uniq(arr: string[]) { + return Array.from(new Set(arr)); +} + +function sortTags(a: Tag, b: Tag) { + return (a.sortIndex - b.sortIndex) || a.name.localeCompare(b.name); +} + +export default function TagFilterDialog({ + tags, + selectedTagSlugs, +}: { + tags: Tag[]; + selectedTagSlugs: string[]; +}) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const [open, setOpen] = React.useState(false); + const [draft, setDraft] = React.useState(() => selectedTagSlugs); + + React.useEffect(() => { + setDraft(selectedTagSlugs); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedTagSlugs.join(",")]); + + const hasDraft = draft.length > 0; + const selectedSet = React.useMemo(() => new Set(draft), [draft]); + + const byId = React.useMemo(() => new Map(tags.map((t) => [t.id, t])), [tags]); + + // Build children mapping from the flat list: parentId -> Tag[] + const childrenByParentId = React.useMemo(() => { + const map = new Map(); + for (const t of tags) { + if (!t.parentId) continue; + const arr = map.get(t.parentId) ?? []; + arr.push(t); + map.set(t.parentId, arr); + } + // sort each child list + for (const [k, arr] of map) { + map.set(k, arr.slice().sort(sortTags)); + } + return map; + }, [tags]); + + const rootGroups = React.useMemo(() => { + return tags + .filter((t) => t.parentId === null) + .slice() + .sort(sortTags); + }, [tags]); + + const orphanChildren = React.useMemo(() => { + return tags + .filter((t) => t.parentId !== null && !byId.has(t.parentId)) + .slice() + .sort(sortTags); + }, [tags, byId]); + + const onToggleParent = (parent: Tag, next: boolean) => { + const children = childrenByParentId.get(parent.id) ?? []; + setDraft((prev) => { + const s = new Set(prev); + if (next) { + s.add(parent.slug); + // when selecting parent, remove child selections (redundant) + for (const c of children) s.delete(c.slug); + } else { + s.delete(parent.slug); + } + return Array.from(s); + }); + }; + + const onToggleChild = (childSlug: string, next: boolean) => { + setDraft((prev) => { + const s = new Set(prev); + if (next) s.add(childSlug); + else s.delete(childSlug); + return Array.from(s); + }); + }; + + const clearAll = () => setDraft([]); + + const apply = () => { + const sp = new URLSearchParams(searchParams?.toString()); + const normalized = uniq(draft).sort(); + + if (normalized.length) sp.set("tags", toTagsParam(normalized)); + else sp.delete("tags"); + + router.replace(`${pathname}?${sp.toString()}`, { scroll: false }); + setOpen(false); + }; + + return ( + + + + + + + + + Filter artworks by tag + + {hasDraft ? ( + + ) : null} + + +

+ Select one or more tags. Selecting a parent also adds its children. +

+
+ + + + +
+ {rootGroups.map((p) => { + const children = childrenByParentId.get(p.id) ?? []; + const parentSelected = selectedSet.has(p.slug); + + return ( +
+
+ + + + {children.length} sub + +
+ + {children.length ? ( +
+ {children.map((c) => { + const childSelected = selectedSet.has(c.slug); + const disabled = parentSelected; + + return ( + + ); + })} +
+ ) : null} +
+ ); + })} + + {orphanChildren.length ? ( +
+ {/*
Other tags
*/} + {/*
+ These tags are not currently assigned to a visible parent. +
*/} + +
+ {orphanChildren.map((t) => { + const checked = selectedSet.has(t.slug); + return ( + + ); + })} +
+
+ ) : null} + + {rootGroups.length === 0 && orphanChildren.length === 0 ? ( +
+ No tags available for filtering. +
+ ) : null} +
+
+ + + +
+
+ Selected: {draft.length} +
+ +
+ + + +
+
+
+
+ ); +} diff --git a/src/components/global/Banner.tsx b/src/components/global/Banner.tsx index 920c705..243df12 100644 --- a/src/components/global/Banner.tsx +++ b/src/components/global/Banner.tsx @@ -11,12 +11,6 @@ export default async function Banner() { include: { file: true } - // include: { - // variants: { - // where: { type: "modified" }, - // take: 1, - // }, - // }, }) const alt = headerImage?.altText ?? "Header Image" diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..cb0b07b --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..a6f1cfb --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,143 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx new file mode 100644 index 0000000..8e4fa13 --- /dev/null +++ b/src/components/ui/scroll-area.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar } diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..275381c --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator }