"use client" import { Button } from "@/components/ui/button" import { Card } from "@/components/ui/card" import { toggleLike } from "@/lib/actions" import { projectTemplates } from "@/lib/data" import { Sandbox } from "@/lib/types" import { cn } from "@/lib/utils" import { useUser } from "@clerk/nextjs" import { AnimatePresence, motion } from "framer-motion" import { Clock, Eye, Globe, Heart, Lock } from "lucide-react" import Image from "next/image" import Link from "next/link" import { useRouter } from "next/navigation" import { memo, MouseEventHandler, useEffect, useMemo, useOptimistic, useState, useTransition, } from "react" import ProjectCardDropdown from "./dropdown" import { CanvasRevealEffect } from "./revealEffect" type BaseProjectCardProps = { id: string name: string type: string visibility: "public" | "private" createdAt: Date likeCount: number liked?: boolean viewCount: number } type AuthenticatedProjectCardProps = BaseProjectCardProps & { isAuthenticated: true onVisibilityChange: ( sandbox: Pick ) => void onDelete: (sandbox: Pick) => void deletingId: string } type UnauthenticatedProjectCardProps = BaseProjectCardProps & { isAuthenticated: false } type ProjectCardProps = | AuthenticatedProjectCardProps | UnauthenticatedProjectCardProps const StatItem = memo(({ icon: Icon, value }: { icon: any; value: number }) => (
{value}
)) 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( ({ id, visibility, createdAt, likeCount, liked, viewCount, }: Pick< BaseProjectCardProps, "visibility" | "createdAt" | "likeCount" | "liked" | "viewCount" | "id" >) => { const { user } = useUser() const [date, setDate] = useState() const Icon = visibility === "private" ? Lock : Globe useEffect(() => { setDate(formatDate(new Date(createdAt))) }, [createdAt]) return (
{visibility === "private" ? "Private" : "Public"}
{date}
) } ) ProjectMetadata.displayName = "ProjectMetadata" interface LikeButtonProps { sandboxId: string userId: string | null initialLikeCount: number initialIsLiked: boolean } export function LikeButton({ sandboxId, userId, initialLikeCount, initialIsLiked, }: LikeButtonProps) { // Optimistic state for like status and count const [{ isLiked, likeCount }, optimisticUpdateLike] = useOptimistic( { isLiked: initialIsLiked, likeCount: initialLikeCount }, (state, optimisticValue: boolean) => { return { isLiked: optimisticValue, likeCount: state.likeCount + (optimisticValue ? 1 : -1), } } ) const [isPending, startTransition] = useTransition() const handleLike: MouseEventHandler = async (e) => { e.stopPropagation() // Prevent click event from bubbling up which leads to navigation to /code/:id if (!userId) return startTransition(async () => { const newLikeState = !isLiked try { optimisticUpdateLike(newLikeState) await toggleLike(sandboxId, userId) } catch (error) { console.log("error", error) optimisticUpdateLike(!newLikeState) } }) } return ( ) } function ProjectCardComponent({ id, name, type, visibility, createdAt, likeCount, viewCount, ...props }: ProjectCardProps) { const [hovered, setHovered] = useState(false) const router = useRouter() 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, }) } } const handleDelete = () => { if (props.isAuthenticated) { props.onDelete({ id, name, }) } } return ( router.push(`/code/${id}`)} onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)} 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 pointer-events-none cursor-events-none" : "cursor-pointer" } `} > {hovered && (
)}
{`${type} {name} {props.isAuthenticated && ( )}
) } 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], ], }