2024-05-26 12:18:09 -07:00
|
|
|
"use client"
|
2024-05-16 10:47:34 -07:00
|
|
|
|
2024-10-21 13:57:45 -06:00
|
|
|
import { Card } from "@/components/ui/card"
|
|
|
|
import { projectTemplates } from "@/lib/data"
|
|
|
|
import { Sandbox } from "@/lib/types"
|
2024-05-26 12:18:09 -07:00
|
|
|
import { AnimatePresence, motion } from "framer-motion"
|
2024-11-11 22:02:34 +01:00
|
|
|
import { Clock, Eye, Globe, Heart, Lock } from "lucide-react"
|
2024-05-26 12:18:09 -07:00
|
|
|
import Image from "next/image"
|
2024-10-21 13:57:45 -06:00
|
|
|
import { useRouter } from "next/navigation"
|
2024-11-11 22:02:34 +01:00
|
|
|
import { memo, useEffect, useMemo, useState } from "react"
|
2024-05-26 12:18:09 -07:00
|
|
|
import ProjectCardDropdown from "./dropdown"
|
2024-11-11 22:02:34 +01:00
|
|
|
import { CanvasRevealEffect } from "./revealEffect"
|
2024-04-16 16:57:15 -04:00
|
|
|
|
2024-11-11 22:02:34 +01:00
|
|
|
type BaseProjectCardProps = {
|
|
|
|
id: string
|
|
|
|
name: string
|
|
|
|
type: string
|
|
|
|
visibility: "public" | "private"
|
|
|
|
createdAt: Date
|
|
|
|
likeCount: number
|
|
|
|
viewCount: number
|
|
|
|
}
|
|
|
|
|
|
|
|
type AuthenticatedProjectCardProps = BaseProjectCardProps & {
|
|
|
|
isAuthenticated: true
|
|
|
|
onVisibilityChange: (
|
|
|
|
sandbox: Pick<Sandbox, "id" | "name" | "visibility">
|
|
|
|
) => void
|
|
|
|
onDelete: (sandbox: Pick<Sandbox, "id" | "name">) => void
|
2024-05-26 12:18:09 -07:00
|
|
|
deletingId: string
|
2024-11-11 22:02:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type UnauthenticatedProjectCardProps = BaseProjectCardProps & {
|
|
|
|
isAuthenticated: false
|
|
|
|
}
|
|
|
|
|
|
|
|
type ProjectCardProps =
|
|
|
|
| AuthenticatedProjectCardProps
|
|
|
|
| UnauthenticatedProjectCardProps
|
|
|
|
|
|
|
|
const StatItem = memo(({ icon: Icon, value }: { icon: any; value: number }) => (
|
|
|
|
<div className="flex items-center space-x-1">
|
|
|
|
<Icon className="size-4" />
|
|
|
|
<span className="text-xs">{value}</span>
|
|
|
|
</div>
|
|
|
|
))
|
|
|
|
|
|
|
|
StatItem.displayName = "StatItem"
|
|
|
|
|
|
|
|
const formatDate = (date: Date): string => {
|
|
|
|
const now = new Date()
|
|
|
|
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / 60000)
|
|
|
|
|
|
|
|
if (diffInMinutes < 1) return "Now"
|
|
|
|
if (diffInMinutes < 60) return `${diffInMinutes}m ago`
|
|
|
|
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}h ago`
|
|
|
|
return `${Math.floor(diffInMinutes / 1440)}d ago`
|
|
|
|
}
|
|
|
|
|
|
|
|
const ProjectMetadata = memo(
|
|
|
|
({
|
|
|
|
visibility,
|
|
|
|
createdAt,
|
|
|
|
likeCount,
|
|
|
|
viewCount,
|
|
|
|
}: Pick<
|
|
|
|
BaseProjectCardProps,
|
|
|
|
"visibility" | "createdAt" | "likeCount" | "viewCount"
|
|
|
|
>) => {
|
|
|
|
const [date, setDate] = useState<string>()
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
setDate(formatDate(new Date(createdAt)))
|
|
|
|
}, [createdAt])
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="flex flex-col text-muted-foreground space-y-2 text-sm z-10">
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
<div className="flex items-center">
|
|
|
|
{visibility === "private" ? (
|
|
|
|
<>
|
|
|
|
<Lock className="size-4 mr-2" /> Private
|
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
<Globe className="size-4 mr-2" /> Public
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="flex gap-4">
|
|
|
|
<div className="flex items-center">
|
|
|
|
<Clock className="size-4 mr-2" /> {date}
|
|
|
|
</div>
|
|
|
|
<StatItem icon={Heart} value={likeCount} />
|
|
|
|
<StatItem icon={Eye} value={viewCount} />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
ProjectMetadata.displayName = "ProjectMetadata"
|
|
|
|
|
|
|
|
function ProjectCardComponent({
|
|
|
|
id,
|
|
|
|
name,
|
|
|
|
type,
|
|
|
|
visibility,
|
|
|
|
createdAt,
|
|
|
|
likeCount,
|
|
|
|
viewCount,
|
|
|
|
...props
|
|
|
|
}: ProjectCardProps) {
|
2024-05-26 12:18:09 -07:00
|
|
|
const [hovered, setHovered] = useState(false)
|
|
|
|
const router = useRouter()
|
2024-05-16 10:47:34 -07:00
|
|
|
|
2024-11-11 22:02:34 +01:00
|
|
|
const projectIcon = useMemo(
|
|
|
|
() =>
|
|
|
|
projectTemplates.find((p) => p.id === type)?.icon ??
|
|
|
|
"/project-icons/node.svg",
|
|
|
|
[type]
|
|
|
|
)
|
|
|
|
|
|
|
|
const handleVisibilityChange = () => {
|
|
|
|
if (props.isAuthenticated) {
|
|
|
|
props.onVisibilityChange({
|
|
|
|
id,
|
|
|
|
name,
|
|
|
|
visibility,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2024-05-25 01:24:13 -07:00
|
|
|
|
2024-11-11 22:02:34 +01:00
|
|
|
const handleDelete = () => {
|
|
|
|
if (props.isAuthenticated) {
|
|
|
|
props.onDelete({
|
|
|
|
id,
|
|
|
|
name,
|
|
|
|
})
|
2024-05-25 01:24:13 -07:00
|
|
|
}
|
2024-11-11 22:02:34 +01:00
|
|
|
}
|
|
|
|
|
2024-04-16 16:57:15 -04:00
|
|
|
return (
|
2024-05-16 10:47:34 -07:00
|
|
|
<Card
|
2024-05-16 21:45:19 -07:00
|
|
|
tabIndex={0}
|
2024-11-11 22:02:34 +01:00
|
|
|
onClick={() => router.push(`/code/${id}`)}
|
2024-05-16 10:47:34 -07:00
|
|
|
onMouseEnter={() => setHovered(true)}
|
|
|
|
onMouseLeave={() => setHovered(false)}
|
2024-11-11 22:02:34 +01:00
|
|
|
className={`
|
|
|
|
group/canvas-card p-4 h-48 flex flex-col justify-between items-start
|
|
|
|
hover:border-muted-foreground/50 relative overflow-hidden transition-all
|
|
|
|
${props.isAuthenticated && props.deletingId === id ? "opacity-50" : ""}
|
|
|
|
`}
|
2024-04-16 16:57:15 -04:00
|
|
|
>
|
2024-05-26 12:18:09 -07:00
|
|
|
<AnimatePresence>
|
2024-05-16 10:47:34 -07:00
|
|
|
{hovered && (
|
|
|
|
<motion.div
|
|
|
|
initial={{ opacity: 0 }}
|
|
|
|
animate={{ opacity: 1 }}
|
|
|
|
className="h-full w-full absolute inset-0"
|
|
|
|
>
|
2024-11-11 22:02:34 +01:00
|
|
|
<CanvasRevealEffect
|
|
|
|
animationSpeed={3}
|
|
|
|
containerClassName="bg-black"
|
|
|
|
colors={colors[type]}
|
|
|
|
dotSize={2}
|
|
|
|
/>
|
|
|
|
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
|
2024-05-16 10:47:34 -07:00
|
|
|
</motion.div>
|
|
|
|
)}
|
2024-05-26 12:18:09 -07:00
|
|
|
</AnimatePresence>
|
2024-05-16 10:47:34 -07:00
|
|
|
|
|
|
|
<div className="space-x-2 flex items-center justify-start w-full z-10">
|
2024-11-11 22:02:34 +01:00
|
|
|
<Image
|
|
|
|
alt={`${type} project icon`}
|
|
|
|
src={projectIcon}
|
|
|
|
width={20}
|
|
|
|
height={20}
|
2024-05-16 10:47:34 -07:00
|
|
|
/>
|
2024-11-11 22:02:34 +01:00
|
|
|
<div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden">
|
|
|
|
{name}
|
2024-05-16 10:47:34 -07:00
|
|
|
</div>
|
2024-11-11 22:02:34 +01:00
|
|
|
{props.isAuthenticated && (
|
|
|
|
<ProjectCardDropdown
|
|
|
|
onVisibilityChange={handleVisibilityChange}
|
|
|
|
onDelete={handleDelete}
|
|
|
|
visibility={visibility}
|
|
|
|
/>
|
|
|
|
)}
|
2024-04-16 16:57:15 -04:00
|
|
|
</div>
|
2024-11-11 22:02:34 +01:00
|
|
|
|
|
|
|
<ProjectMetadata
|
|
|
|
visibility={visibility}
|
|
|
|
createdAt={createdAt}
|
|
|
|
likeCount={likeCount}
|
|
|
|
viewCount={viewCount}
|
|
|
|
/>
|
2024-05-16 10:47:34 -07:00
|
|
|
</Card>
|
2024-05-26 12:18:09 -07:00
|
|
|
)
|
2024-04-16 16:57:15 -04:00
|
|
|
}
|
2024-11-11 22:02:34 +01:00
|
|
|
|
|
|
|
ProjectCardComponent.displayName = "ProjectCard"
|
|
|
|
|
|
|
|
const ProjectCard = memo(ProjectCardComponent)
|
|
|
|
|
|
|
|
export default ProjectCard
|
|
|
|
|
|
|
|
const colors: { [key: string]: number[][] } = {
|
|
|
|
react: [
|
|
|
|
[71, 207, 237],
|
|
|
|
[30, 126, 148],
|
|
|
|
],
|
|
|
|
node: [
|
|
|
|
[86, 184, 72],
|
|
|
|
[59, 112, 52],
|
|
|
|
],
|
|
|
|
}
|