Download apps/web/src/components/packages/PackageCard.tsx from Atmint/cpns: direct link, hf CLI and curl.
- Browser
- Download file 7.43 kB
-
https://huggingface.co/spaces/Atmint/cpns/resolve/main/apps/web/src/components/packages/PackageCard.tsx
- Command line
-
hf download hf://spaces/Atmint/cpns/apps/web/src/components/packages/PackageCard.tsx
-
curl -L -o PackageCard.tsx https://huggingface.co/spaces/Atmint/cpns/resolve/main/apps/web/src/components/packages/PackageCard.tsx
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> | |
| ); | |
| } | |