140 lines
4.2 KiB
TypeScript
140 lines
4.2 KiB
TypeScript
'use client'
|
|
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { cn } from '@/lib/utils'
|
|
import { BadgeListBlock, Media } from '@/payload-types'
|
|
import { DefaultTypedEditorState } from '@payloadcms/richtext-lexical'
|
|
import Image from 'next/image'
|
|
import Link from 'next/link'
|
|
import { RichText } from './RichText'
|
|
import clsx from 'clsx'
|
|
|
|
type Props = {
|
|
title: string
|
|
href?: string
|
|
description?: DefaultTypedEditorState
|
|
dates: string
|
|
tags?: BadgeListBlock[]
|
|
link?: string
|
|
image?: Media
|
|
video?: Media
|
|
links?: BadgeListBlock[]
|
|
className?: string
|
|
}
|
|
|
|
export function ProjectCard({
|
|
title,
|
|
href,
|
|
description,
|
|
dates,
|
|
link,
|
|
image,
|
|
video,
|
|
className,
|
|
...rest
|
|
}: Props) {
|
|
const tags = rest?.tags?.length ? rest.tags[0] : null
|
|
const links = rest?.links?.length ? rest.links[0] : null
|
|
|
|
return (
|
|
<Card
|
|
className={
|
|
'flex flex-col overflow-hidden border hover:shadow-lg transition-all duration-300 ease-out h-full'
|
|
}
|
|
>
|
|
<button
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
if (!href) return
|
|
|
|
window.open(href)
|
|
}}
|
|
className={cn('block', className, !!href && 'cursor-pointer')}
|
|
>
|
|
{video && (
|
|
<video
|
|
src={video.url || ''}
|
|
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.url || ''}
|
|
alt={image.alt || ''}
|
|
width={500}
|
|
height={300}
|
|
className="h-40 w-full overflow-hidden object-cover object-top"
|
|
/>
|
|
)}
|
|
</button>
|
|
<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>
|
|
{!!description && (
|
|
<RichText
|
|
className="prose max-w-full text-pretty font-sans text-xs text-muted-foreground dark:prose-invert"
|
|
data={description}
|
|
/>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="mt-auto flex flex-col px-2">
|
|
{tags?.badges && tags.badges.length > 0 && (
|
|
<div className="mt-2 flex flex-wrap gap-1">
|
|
{tags.badges.map((b, i) => (
|
|
<Badge
|
|
key={title + dates + b + i.toString()}
|
|
variant="secondary"
|
|
className={clsx(
|
|
'px-1 py-0 text-[10px]',
|
|
b.color || tags.defaultColor ? `bg-[${b.color || tags.defaultColor}]` : '',
|
|
b.textColor || tags.defaultTextColor
|
|
? `text-[${b.textColor || tags.defaultTextColor}]`
|
|
: '',
|
|
)}
|
|
>
|
|
{b.value}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
<CardFooter className="px-2 pb-2">
|
|
{links?.badges && links.badges.length > 0 && (
|
|
<div className="flex flex-row flex-wrap items-start gap-1">
|
|
{links.badges?.map((link, idx) => {
|
|
const icon = link.icon as Media | undefined
|
|
|
|
return (
|
|
<Link href={link?.href || ''} key={idx} target="_blank">
|
|
<Badge key={idx} className="flex gap-2 px-2 py-1 text-[10px]">
|
|
{!!icon && icon.url && (
|
|
<Image
|
|
src={icon.url}
|
|
alt={icon.alt || ''}
|
|
width={16}
|
|
height={16}
|
|
className="not-[dark]:invert"
|
|
/>
|
|
)}
|
|
{link.value}
|
|
</Badge>
|
|
</Link>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</CardFooter>
|
|
</Card>
|
|
)
|
|
}
|