ui/topia

Project Card

A versatile and visually appealing project card component designed to showcase your projects on a portfolio website. It features support for multiple technologies, external links, images, and videos to provide a comprehensive overview of your work.

Demo

Stashticly

Stashticly

A simple, open source, and free expense tracking app.

Next.js
Typescript
TailwindCSS
Clerk
React Query
Hono
Drizzle
Neon
Next.js Commerce

Next.js Commerce

Starter kit for high-performance commerce with Shopify.

Next.js
Typescript
TailwindCSS

Installation

Install dependencies

npx shadcn-ui@latest add card badge

Copy the source code

project-card.tsx
import Image from 'next/image'
import Link from 'next/link'
import { Badge } from '../components/ui/badge'
import {
  Card,
  CardContent,
  CardFooter,
  CardHeader,
  CardTitle,
} from '../components/ui/card'
import { cn } from '../lib/utils'
 
interface Props {
title: string
href?: string
description: string
dates: string
tags: readonly string[]
link?: string
image?: string
video?: string
links?: readonly {
icon: React.ReactNode
type: string
href: string
}[]
className?: string
}
 
export function ProjectCard({
  title,
  href,
  description,
  dates,
  tags,
  link,
  image,
  video,
  links,
  className,
}: Props) {
  return (
    <Card
      className={
        'flex h-full flex-col overflow-hidden border transition-all duration-300 ease-out hover:shadow-lg'
      }
    >
      <Link
        href={href || '#'}
        className={cn('block cursor-pointer', className)}
      >
        {video && (
          <video
            src={video}
            autoPlay
            loop
            muted
            playsInline
            className="pointer-events-none mx-auto h-40 w-full object-cover object-top" // needed because random black line at bottom of video
          />
        )}
        {image && (
          <Image
            src={image}
            alt={title}
            className="h-40 w-full overflow-hidden object-cover object-top"
            width={500}
            height={500}
          />
        )}
      </Link>
      <CardHeader className="px-2">
        <div className="space-y-1">
          <CardTitle className="mt-1 text-base">{title}</CardTitle>
          <time className="font-sans text-xs">{dates}</time>
          <div className="hidden font-sans text-xs underline print:visible">
            {link?.replace('https://', '').replace('www.', '').replace('/', '')}
          </div>
          <p className="prose text-muted-foreground dark:prose-invert max-w-full text-pretty font-sans text-xs">
            {description}
          </p>
        </div>
      </CardHeader>
      <CardContent className="mt-auto flex flex-col px-2">
        {tags && tags.length > 0 && (
          <div className="mt-2 flex flex-wrap gap-1">
            {tags?.map((tag) => (
              <Badge
                className="px-1 py-0 text-[10px]"
                variant="secondary"
                key={tag}
              >
                {tag}
              </Badge>
            ))}
          </div>
        )}
      </CardContent>
      <CardFooter className="px-2 pb-2">
        {links && links.length > 0 && (
          <div className="flex flex-row flex-wrap items-start gap-1">
            {links?.map((link, idx) => (
              <Link href={link?.href} key={idx} target="_blank">
                <Badge key={idx} className="flex gap-2 px-2 py-1 text-[10px]">
                  {link.icon}
                  {link.type}
                </Badge>
              </Link>
            ))}
          </div>
        )}
      </CardFooter>
    </Card>
  )
}

Refference

PropTypeDefault
title
string
-
href
string
-
description
string
-
dates
string
-
tags
readonly string[]
-
link
string
-
image
string
-
video
string
-
links
readonly { icon: React.ReactNode, type: string, href: string }[]
-
className
string
-

Last updated on

On this page