From c491fd65470b5b952865966019e4fc28cf5014bb Mon Sep 17 00:00:00 2001 From: IwanIDev Date: Fri, 20 Mar 2026 15:20:27 +0000 Subject: Using shadcn/ui for this site. --- src/App.tsx | 86 +++++++++++++++++++++++++-------- src/components/ui/alert.tsx | 76 +++++++++++++++++++++++++++++ src/components/ui/badge.tsx | 52 ++++++++++++++++++++ src/components/ui/card.tsx | 103 ++++++++++++++++++++++++++++++++++++++++ src/components/ui/separator.tsx | 23 +++++++++ 5 files changed, 320 insertions(+), 20 deletions(-) create mode 100644 src/components/ui/alert.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/separator.tsx diff --git a/src/App.tsx b/src/App.tsx index 3472fd0..396af37 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,10 @@ import { useEffect, useState } from 'react' import { fetchDrupalResource } from './lib/drupalClient' -import './App.css' +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' +import { Separator } from '@/components/ui/separator' type DrupalArticle = { id: string @@ -56,34 +60,76 @@ function App() { void loadArticles() }, []) - return ( -
-

Articles

- {!isLoading && !error && resourcePath &&

Using resource: {resourcePath}

} + const articleCount = articles.length + + const formatDate = (value?: string) => { + if (!value) { + return 'Unknown date' + } - {isLoading &&

Loading articles…

} + return new Date(value).toLocaleString() + } + + return ( +
+ + + Portfolio CMS Homepage + This homepage now uses shadcn/ui components and reads content from Drupal JSON:API. + + + + {isLoading ? 'Loading' : error ? 'Error' : 'Connected'} + + Articles: {articleCount} + {resourcePath && Resource: {resourcePath}} + + + Headless Drupal + Vite + shadcn/ui + + + {error && ( -

- Could not fetch articles: {error} -

+ + Could not fetch articles + {error} + )} - {!isLoading && !error && articles.length === 0 && ( -

Connected to Drupal, but no articles were returned.

+ {!isLoading && !error && articleCount === 0 && ( + + No articles found + Connected to Drupal successfully, but this resource currently has no content. + )} - {!isLoading && !error && articles.length > 0 && ( -
    + {!isLoading && !error && articleCount > 0 && ( +
    {articles.map((article) => ( -
  • - {article.attributes?.title ?? '(Untitled)'} -
    ID: {article.id}
    -
    Status: {article.attributes?.status ? 'Published' : 'Unpublished'}
    - {article.attributes?.created &&
    Created: {new Date(article.attributes.created).toLocaleString()}
    } -
  • + + + {article.attributes?.title ?? '(Untitled)'} + {article.id} + + + +
    + Status + + {article.attributes?.status ? 'Published' : 'Unpublished'} + +
    +
    + Created + {formatDate(article.attributes?.created)} +
    +
    +
    ))} -
+ )}
) diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..2a176c7 --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,76 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground", + className + )} + {...props} + /> + ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription, AlertAction } diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..b20959d --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,52 @@ +import { mergeProps } from "@base-ui/react/merge-props" +import { useRender } from "@base-ui/react/use-render" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + secondary: + "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", + destructive: + "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20", + outline: + "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + ghost: + "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant = "default", + render, + ...props +}: useRender.ComponentProps<"span"> & VariantProps) { + return useRender({ + defaultTagName: "span", + props: mergeProps<"span">( + { + className: cn(badgeVariants({ variant }), className), + }, + props + ), + render, + state: { + slot: "badge", + variant, + }, + }) +} + +export { Badge, badgeVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 0000000..40cac5f --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,103 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ + className, + size = "default", + ...props +}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { + return ( +
img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl", + className + )} + {...props} + /> + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx new file mode 100644 index 0000000..4f65961 --- /dev/null +++ b/src/components/ui/separator.tsx @@ -0,0 +1,23 @@ +import { Separator as SeparatorPrimitive } from "@base-ui/react/separator" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + ...props +}: SeparatorPrimitive.Props) { + return ( + + ) +} + +export { Separator } -- cgit