All files / src/components/projects ProjectCard.tsx

100% Statements 9/9
92.85% Branches 13/14
100% Functions 7/7
100% Lines 9/9

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180                                                        25x         25x                 2x               25x                                                                                                                               1x                   1x                                             1x                     1x                   1x                      
import { memo } from "react";
import { Link } from "@tanstack/react-router";
import {
	Trash2,
	Image as ImageIcon,
	Eye,
	Archive,
	ArchiveRestore,
	ExternalLink,
	Clock,
	Clapperboard,
} from "lucide-react";
import { Card, CardContent } from "../ui/card";
import { Button } from "../ui/button";
import { ResponsiveImage } from "../ui/ResponsiveImage";
import type { Project } from "../../lib/api";
import { formatRelativeDate } from "../../lib/format-date";
 
interface ProjectCardProps {
	project: Project;
	onDelete: (projectId: string) => void;
	onManagePhotos: (project: Project) => void;
	onPublish: (projectId: string) => void;
	onArchive: (projectId: string) => void;
	onUnarchive: (projectId: string) => void;
}
 
function getStatusBadge(status: string) {
	const styles: Record<string, string> = {
		draft: "bg-warning-light text-warning",
		published: "bg-success-light text-success",
		archived: "bg-background-muted text-foreground-muted",
	};
	return (
		<span
			className={`px-2 py-1 text-xs font-medium rounded-full ${styles[status] || styles.draft}`}
		>
			{status.charAt(0).toUpperCase() + status.slice(1)}
		</span>
	);
}
 
export const ProjectCard = memo(function ProjectCard({
	project,
	onDelete,
	onManagePhotos,
	onPublish,
	onArchive,
	onUnarchive,
}: ProjectCardProps) {
	return (
		<Card className="overflow-hidden transition-shadow duration-200 hover:shadow-card-hover flex flex-col">
			{/* Card Image - Clickable to view details */}
			<Link to="/projects/$projectId" params={{ projectId: project.id }}>
				<div className="h-48 bg-background-muted flex items-center justify-center cursor-pointer hover:opacity-90 transition-opacity overflow-hidden relative">
					{project.coverImage ? (
						<ResponsiveImage
							storageKey={project.coverImage}
							alt={project.title}
							width={600}
							height={600}
							sizes="(max-width: 640px) 50vw, 33vw"
							className="w-full h-full object-cover"
						/>
					) : (
						<div className="w-full h-full bg-gradient-to-br from-gray-50 to-slate-50 flex flex-col items-center justify-center gap-2">
							<ImageIcon className="h-10 w-10 text-foreground-subtle/60" />
							<span className="text-xs text-foreground-subtle font-medium">Add Photos</span>
						</div>
					)}
				</div>
			</Link>
 
			<CardContent className="p-4 flex flex-col flex-1">
				<div className="flex items-start justify-between mb-2">
					<Link
						to="/projects/$projectId"
						params={{ projectId: project.id }}
						className="font-semibold text-foreground-default truncate hover:text-primary-600 transition-colors"
					>
						{project.title}
					</Link>
					{getStatusBadge(project.status)}
				</div>
 
				{project.description && (
					<p className="text-sm text-foreground-muted line-clamp-2 mb-4">
						{project.description}
					</p>
				)}
 
				{/* Metadata row */}
				<div className="flex items-center gap-3 text-xs text-foreground-subtle mb-3 mt-auto">
					<span className="flex items-center gap-1">
						<Clock className="h-3 w-3" />
						{formatRelativeDate(project.dateUpdated)}
					</span>
				</div>
 
				{/* Action buttons */}
				<div className="flex items-center gap-1 sm:gap-2 pt-2 border-t border-border-default flex-wrap">
					<Link to="/projects/$projectId" params={{ projectId: project.id }}>
						<Button
							variant="ghost"
							size="sm"
							title="View Details"
							className="p-2 sm:px-3"
						>
							<ExternalLink className="h-4 w-4" />
						</Button>
					</Link>
					<Button
						variant="ghost"
						size="sm"
						onClick={() => onManagePhotos(project)}
						title="Manage Photos"
						className="p-2 sm:px-3"
					>
						<ImageIcon className="h-4 w-4" />
					</Button>
					{project.status === "draft" && (
						<Button
							variant="ghost"
							size="sm"
							onClick={() => onPublish(project.id)}
							title="Publish"
							className="p-2 sm:px-3"
						>
							<Eye className="h-4 w-4" />
						</Button>
					)}
					{project.status === "published" && (
						<Link to="/social-studio/create" search={{ projectId: project.id }}>
							<Button
								variant="ghost"
								size="sm"
								title="Create Reel"
								className="p-2 sm:px-3"
							>
								<Clapperboard className="h-4 w-4" />
							</Button>
						</Link>
					)}
					{project.status === "published" && (
						<Button
							variant="ghost"
							size="sm"
							onClick={() => onArchive(project.id)}
							title="Archive"
							className="p-2 sm:px-3"
						>
							<Archive className="h-4 w-4" />
						</Button>
					)}
					{project.status === "archived" && (
						<Button
							variant="ghost"
							size="sm"
							onClick={() => onUnarchive(project.id)}
							title="Move to Draft"
							className="p-2 sm:px-3"
						>
							<ArchiveRestore className="h-4 w-4" />
						</Button>
					)}
					<Button
						variant="ghost"
						size="sm"
						onClick={() => onDelete(project.id)}
						title="Delete"
						className="p-2 sm:px-3 text-error hover:text-error hover:bg-error-light"
					>
						<Trash2 className="h-4 w-4" />
					</Button>
				</div>
			</CardContent>
		</Card>
	);
});