# Project Card
*URL: https://www.grettech.com/components/project-card*

A project card with a cover image, dates, technology tags, and source and live demo links.

- Cover image with a hover zoom
- Project dates and technology tags
- Source and live demo links, or a private-source label
- Works with your framework's Image and Link components

## Install with the shadcn CLI
```bash
npx shadcn@latest add https://www.grettech.com/api/registry/project-card
```
With its demo: `npx shadcn@latest add https://www.grettech.com/api/registry/project-card-demo`

**Dependencies:** npm: clsx, tailwind-merge, lucide-react, class-variance-authority; shadcn/ui: button, card

## Manual installation
**Step 1: Install dependencies**
Install the packages this component uses and the shadcn/ui components it builds on. It also needs the `cn` helper in `lib/utils.ts`, which `npx shadcn@latest init` adds.

```bash
npm install clsx tailwind-merge lucide-react class-variance-authority
npx shadcn@latest add button card
```

**Step 2: Copy the source code**
Add these files to your project components directory.

File: `components/ui/project-card.tsx`
```tsx
"use client"

import * as React from "react"
import { Badge } from "@/components/ui/custom-badge"
import { buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Calendar, ExternalLink, Lock } from "lucide-react"
import { cva, type VariantProps } from "class-variance-authority"

const projectCardVariants = cva(
  "group flex h-full flex-col gap-0 overflow-hidden py-0 transition-all duration-500 hover:-translate-y-1 hover:border-primary/30 hover:shadow-xl",
  {
    variants: {
      variant: {
        default: "",
        outline: "border-2 border-border/50",
      },
      size: {
        default: "",
        sm: "text-sm",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

export interface Project {
  title?: string
  description?: string
  technologies?: string[]
  url?: string
  githubUrl?: string
  startDate?: Date | string
  endDate?: Date | string
  imageUrl?: string
}

export interface ProjectCardProps
  extends
    React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof projectCardVariants> {
  project?: Project
  imageAs?: React.ElementType
  linkAs?: React.ElementType
}

/** GitHub mark (brand icons were removed from lucide-react) */
function GithubIcon(props: React.SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
      <path d="M12 .3a12 12 0 0 0-3.8 23.38c.6.12.83-.26.83-.57L9 21.07c-3.34.72-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.08-.74.09-.73.09-.73 1.2.09 1.83 1.24 1.83 1.24 1.07 1.83 2.81 1.3 3.5 1 .1-.78.42-1.31.76-1.61-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.14-.3-.54-1.52.1-3.18 0 0 1-.32 3.3 1.23a11.5 11.5 0 0 1 6 0c2.28-1.55 3.29-1.23 3.29-1.23.64 1.66.24 2.88.12 3.18a4.65 4.65 0 0 1 1.23 3.22c0 4.61-2.8 5.63-5.48 5.92.42.36.81 1.1.81 2.22l-.01 3.29c0 .31.2.69.82.57A12 12 0 0 0 12 .3" />
    </svg>
  )
}

/** Shown when no `project` is passed, so the card previews on its own */
const SAMPLE_PROJECT: Project = {
  title: "Portfolio Website Redesign",
  description:
    "A modern, responsive portfolio website built with Next.js, Tailwind CSS, and Framer Motion.",
  technologies: ["Next.js", "React", "Tailwind CSS", "Framer Motion"],
  startDate: new Date(2023, 0, 1),
  endDate: new Date(2023, 5, 15),
}

function formatDate(date: Date | string) {
  // Date-only strings ("2024-03-01") are UTC; format them in UTC so the
  // month doesn't shift with the visitor's timezone (or differ from SSR)
  return new Date(date).toLocaleDateString("en-US", {
    month: "short",
    year: "numeric",
    ...(typeof date === "string" && { timeZone: "UTC" }),
  })
}

export const ProjectCard = React.forwardRef<HTMLDivElement, ProjectCardProps>(
  (
    {
      project = SAMPLE_PROJECT,
      imageAs: ImageTag = "img",
      linkAs: LinkTag = "a",
      variant,
      size,
      className,
      ...props
    },
    ref
  ) => {
    const {
      title,
      description,
      technologies = [],
      url,
      githubUrl,
      startDate,
      endDate,
      imageUrl,
    } = project

    return (
      <Card
        ref={ref}
        className={cn(projectCardVariants({ variant, size, className }))}
        {...props}
      >
        {imageUrl && (
          <div className="relative aspect-[16/9] w-full overflow-hidden border-b bg-muted">
            <ImageTag
              src={imageUrl}
              alt={title ? `${title} preview` : "Project preview"}
              loading="lazy"
              sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
              className="absolute inset-0 h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
            />
          </div>
        )}

        <CardHeader className="gap-2 space-y-0 px-6 pt-5 pb-0">
          {startDate && (
            <p className="flex items-center gap-1.5 text-xs text-muted-foreground">
              <Calendar className="size-3.5" />
              <span>
                {formatDate(startDate)} –{" "}
                {endDate ? formatDate(endDate) : "Present"}
              </span>
            </p>
          )}
          {title && (
            <CardTitle className="text-xl leading-tight font-semibold tracking-tight">
              {title}
            </CardTitle>
          )}
          {description && (
            <CardDescription className="line-clamp-2 text-pretty">
              {description}
            </CardDescription>
          )}
        </CardHeader>

        <CardContent className="flex-grow px-6 pt-4 pb-5">
          {technologies.length > 0 && (
            <ul className="flex flex-wrap gap-1.5" aria-label="Technologies">
              {technologies.map((tech, i) => (
                <li key={`${tech}-${i}`}>
                  <Badge
                    variant="secondary"
                    className="rounded-full border-border/60 bg-muted px-2.5 py-0.5 text-xs font-medium text-muted-foreground"
                  >
                    {tech}
                  </Badge>
                </li>
              ))}
            </ul>
          )}
        </CardContent>

        <CardFooter className="flex items-center justify-between gap-3 border-t px-6 py-4">
          {/* Styled links rather than <Button asChild>, so they stay real
              links with both Radix and Base UI versions of shadcn/ui */}
          {githubUrl ? (
            <LinkTag
              href={githubUrl}
              target="_blank"
              rel="noopener noreferrer"
              className={cn(
                buttonVariants({ variant: "outline", size: "sm" }),
                "rounded-full"
              )}
            >
              <GithubIcon /> Source
            </LinkTag>
          ) : (
            <span className="inline-flex items-center gap-1.5 text-xs text-muted-foreground">
              <Lock className="size-3.5" /> Private source
            </span>
          )}
          {url && (
            <LinkTag
              href={url}
              target="_blank"
              rel="noopener noreferrer"
              className={cn(buttonVariants({ size: "sm" }), "rounded-full")}
            >
              <ExternalLink /> Live Demo
            </LinkTag>
          )}
        </CardFooter>
      </Card>
    )
  }
)
ProjectCard.displayName = "ProjectCard"

```

File: `components/ui/custom-badge.tsx`
```tsx
import { cn } from "@/lib/utils"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
const badgeVariants = cva(
  "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
  {
    variants: {
      variant: {
        default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
        secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
        destructive: "border-transparent bg-destructive text-white shadow hover:bg-destructive/80",
        outline: "text-foreground",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
  return <div className={cn(badgeVariants({ variant }), className)} {...props} />
}
export { Badge, badgeVariants }
```

**Step 3: Framework Adjustments (Optional)**
If you are manually copying this code into a Vite or Remix project (without using the CLI), make sure to remove the `"use client";` directive from the very top of the file.

## Props
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
| `project` | `Project` | `Sample project` | No | The project to show. Leave it out to preview the card with sample content; a passed project shows only the fields you set. |
| `project.title` | `string` | - | No | Project name. |
| `project.description` | `string` | - | No | Short description, clamped to two lines. |
| `project.technologies` | `string[]` | `[]` | No | Shown as tags. |
| `project.url` | `string` | - | No | Live URL. Shows a Live Demo button when set. |
| `project.githubUrl` | `string` | - | No | Repository URL. Shows a Source button when set, otherwise a private-source label. |
| `project.startDate` | `Date \| string` | - | No | Start date. The date row only shows when this is set. |
| `project.endDate` | `Date \| string` | - | No | End date. Shows "Present" when left out. |
| `project.imageUrl` | `string` | - | No | Cover image URL. |
| `imageAs` | `React.ElementType` | `"img"` | No | Component used for the cover image, e.g. next/image's Image. It receives src, alt, sizes and className. |
| `linkAs` | `React.ElementType` | `"a"` | No | Component used for the Source and Live Demo links, e.g. next/link's Link. It receives href, target, rel and className. |
| `variant` | `"default" \| "outline"` | `"default"` | No | "outline" draws a thicker border. |
| `size` | `"default" \| "sm"` | `"default"` | No | "sm" uses smaller text. |
| `className` | `string` | - | No | Extra classes for the card. |
| `...props` | `React.HTMLAttributes<HTMLDivElement>` | - | No | Any other attributes (id, style, aria-*, etc.), passed to the card. |

## Usage
```tsx
"use client"

import { ProjectCard } from "@/components/ui/project-card"

export default function ProjectCardDemo() {
  const project = {
    title: "Awesome App",
    description:
      "A fantastic new web application with real-time features and a modern UI.",
    technologies: ["React", "Next.js", "Tailwind CSS", "TypeScript"],
    githubUrl: "https://github.com",
    url: "https://example.com",
    imageUrl:
      "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&q=75&auto=format",
  }
  return (
    <ProjectCard
      project={project}
      // imageAs={Image} // Pass your framework's Image component
      // linkAs={Link} // Pass your framework's Link component
    />
  )
}

```
