cpns / apps /web /src /components /packages /PackageCard.tsx
pramudya-yb
Rename project from Labas to Pram
2b62b0c
Raw
History Blame Contribute Delete
7.43 kB
import { Link, useNavigate } from "@tanstack/react-router";
import { Button } from "@pram/ui/components/button";
import { Card, CardContent } from "@pram/ui/components/card";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { toast } from "sonner";
export interface PackageItem {
id: string;
title: string;
description?: string | null;
examTypeName?: string | null;
isPublic: boolean;
creatorUserId?: string | null;
totalQuestions: number;
totalSections: number;
estimatedDurationMin?: number | null;
usageCount: number;
avgRating?: number | null;
}
interface PackageCardProps {
pkg: PackageItem;
isOwner: boolean;
bulkMode: boolean;
isSelected: boolean;
isTogglePending?: boolean;
onToggleSelect: () => void;
onTogglePublic: () => void;
}
export function PackageCard({
pkg,
isOwner,
bulkMode,
isSelected,
isTogglePending = false,
onToggleSelect,
onTogglePublic,
}: PackageCardProps) {
const navigate = useNavigate();
const isPrivate = isOwner && !pkg.isPublic;
return (
<Card
className={`clay-shadow clay-hover bg-[var(--pure-white)] border-2 rounded-[var(--radius-xl)] h-full flex flex-col ${
bulkMode && isSelected
? "border-[var(--matcha-600)] ring-2 ring-[var(--matcha-400)]"
: isPrivate && !bulkMode
? "border-[var(--oat-border)] border-l-[var(--warm-charcoal)] border-l-4"
: "border-[var(--oat-border)]"
}`}
>
<CardContent className="p-5 flex flex-col h-full">
{/* Clickable content area */}
<div
className="block flex-1 cursor-pointer"
onClick={bulkMode ? onToggleSelect : undefined}
>
<Link
to="/package/$id"
params={{ id: pkg.id }}
className={bulkMode ? "pointer-events-none" : ""}
>
<div className="flex items-start justify-between mb-3">
<div className="flex gap-2 flex-wrap">
{bulkMode && (
<span className={`px-2 py-1 rounded-full text-[10px] font-semibold flex items-center gap-1 ${
isSelected
? "bg-[var(--matcha-600)] text-[var(--pure-white)]"
: "bg-[var(--oat-light)] text-[var(--warm-charcoal)]"
}`}>
<MaterialIcon name={isSelected ? "check_circle" : "radio_button_unchecked"} className="text-xs" />
{isSelected ? "Terpilih" : "Pilih"}
</span>
)}
{pkg.examTypeName && (
<span className="inline-flex items-center px-2.5 py-1 rounded-full bg-[var(--matcha-300)] text-[var(--matcha-800)] text-xs font-semibold leading-none whitespace-nowrap">
{pkg.examTypeName}
</span>
)}
</div>
{pkg.avgRating && (
<div className="flex items-center gap-1 text-[var(--lemon-700)] shrink-0">
<MaterialIcon name="star" className="text-sm" />
<span className="text-xs font-bold">{pkg.avgRating}</span>
</div>
)}
</div>
<h3 className="font-headline text-lg font-bold text-[var(--clay-black)] mb-2 line-clamp-2">
{pkg.title}
</h3>
{pkg.description && (
<p className="text-sm text-[var(--warm-charcoal)] line-clamp-2 mb-4">
{pkg.description}
</p>
)}
</Link>
<div className="flex items-center justify-between mt-auto pt-3 border-t border-[var(--oat-border)]">
<div className="flex gap-3 text-xs text-[var(--warm-charcoal)]">
<span className="flex items-center gap-1">
<MaterialIcon name="quiz" className="text-xs" />
{pkg.totalQuestions}
</span>
<span className="flex items-center gap-1">
<MaterialIcon name="folder" className="text-xs" />
{pkg.totalSections}
</span>
{pkg.estimatedDurationMin && (
<span className="flex items-center gap-1">
<MaterialIcon name="timer" className="text-xs" />
{pkg.estimatedDurationMin}m
</span>
)}
</div>
<span className="text-xs text-[var(--warm-silver)]">
{pkg.usageCount}x digunakan
</span>
</div>
</div>
{/* Owner actions */}
{isOwner && !bulkMode && (
<div className="mt-3 pt-3 border-t border-[var(--oat-border)] flex items-center justify-between">
<button
onClick={onTogglePublic}
disabled={isTogglePending}
aria-label={pkg.isPublic ? "Jadikan privat" : "Jadikan publik"}
className={`group text-xs font-semibold px-3 py-1.5 rounded-full transition-all cursor-pointer flex items-center gap-1 disabled:opacity-50 disabled:cursor-not-allowed ${
pkg.isPublic
? "bg-[var(--matcha-300)] text-[var(--matcha-800)] hover:bg-[var(--pomegranate-400)]/15 hover:text-[var(--pomegranate-600)]"
: "bg-[var(--slushie-500)]/15 text-[var(--slushie-800)] hover:bg-[var(--matcha-300)] hover:text-[var(--matcha-800)]"
}`}
>
{/* Default icon */}
<MaterialIcon
name={pkg.isPublic ? "public" : "lock"}
className="text-xs group-hover:hidden"
/>
{/* Hover icon (shows intent) */}
<MaterialIcon
name={pkg.isPublic ? "lock" : "public"}
className="text-xs hidden group-hover:inline"
/>
{/* Default label */}
<span className="group-hover:hidden">
{pkg.isPublic ? "Publik" : "Privat"}
</span>
{/* Hover label */}
<span className="hidden group-hover:inline">
{pkg.isPublic ? "Jadikan Privat" : "Jadikan Publik"}
</span>
</button>
{pkg.isPublic && (
<button
onClick={() => {
const url = `${window.location.origin}/package/${pkg.id}`;
navigator.clipboard.writeText(url);
toast.success("Link paket disalin!");
}}
className="text-xs text-[var(--matcha-600)] hover:bg-[var(--matcha-300)]/20 px-3 py-1.5 rounded-full transition-colors flex items-center gap-1 cursor-pointer"
>
<MaterialIcon name="share" className="text-xs" />
Bagikan
</button>
)}
</div>
)}
{/* CTA */}
{!bulkMode && (
<div className="mt-3 pt-3 border-t border-[var(--oat-border)]">
<Button
className="w-full bg-[var(--matcha-600)] text-[var(--pure-white)] hover:bg-[var(--matcha-800)] clay-hover rounded-[var(--radius-lg)]"
onClick={() => navigate({ to: "/package/$id/take", params: { id: pkg.id } })}
size="xl"
>
<MaterialIcon name="play_arrow" className="mr-2" />
Mulai Latihan
</Button>
</div>
)}
</CardContent>
</Card>
);
}