Download apps/web/src/components/bank/QuestionCard.tsx from Atmint/cpns: direct link, hf CLI and curl.
- Browser
- Download file 7.55 kB
-
https://huggingface.co/spaces/Atmint/cpns/resolve/main/apps/web/src/components/bank/QuestionCard.tsx
- Command line
-
hf download hf://spaces/Atmint/cpns/apps/web/src/components/bank/QuestionCard.tsx
-
curl -L -o QuestionCard.tsx https://huggingface.co/spaces/Atmint/cpns/resolve/main/apps/web/src/components/bank/QuestionCard.tsx
7.55 kB
| import { Button } from "@pram/ui/components/button"; | |
| import { MaterialIcon } from "@/components/ui/MaterialIcon"; | |
| import { formatLabel } from "@/lib/format"; | |
| import type { Question } from "@/lib/types"; | |
| interface QuestionCardProps { | |
| q: Question; | |
| isFeatured?: boolean; | |
| isInBundle: boolean; | |
| disabled?: boolean; | |
| selected?: boolean; | |
| bulkSelect?: boolean; | |
| onToggle: () => void; | |
| onOpenDetail: () => void; | |
| isOwner: boolean; | |
| onTogglePublic: () => void; | |
| onDelete: () => void; | |
| } | |
| export function QuestionCard({ | |
| q, | |
| isFeatured = false, | |
| isInBundle, | |
| disabled = false, | |
| selected = false, | |
| bulkSelect = false, | |
| onToggle, | |
| onOpenDetail, | |
| isOwner, | |
| onTogglePublic, | |
| onDelete, | |
| }: QuestionCardProps) { | |
| const isPrivate = isOwner && !q.isPublic; | |
| return ( | |
| <div | |
| onClick={() => { if (!disabled) onOpenDetail(); }} | |
| className={`border-2 rounded-[var(--radius-xl)] h-full flex flex-col transition-all ${ | |
| disabled | |
| ? "bg-[var(--oat-light)] border-[var(--oat-border)] opacity-40 cursor-not-allowed" | |
| : selected | |
| ? "bg-[var(--matcha-100)] border-[var(--matcha-600)] clay-hover cursor-pointer ring-2 ring-[var(--matcha-400)]" | |
| : isInBundle | |
| ? "bg-[var(--matcha-100)] border-[var(--clay-black)] clay-shadow clay-hover cursor-pointer" | |
| : isPrivate | |
| ? "bg-[var(--pure-white)] border-[var(--oat-border)] border-l-[var(--warm-charcoal)] border-l-4 clay-shadow clay-hover cursor-pointer" | |
| : "bg-[var(--pure-white)] border-[var(--oat-border)] clay-shadow clay-hover cursor-pointer" | |
| }`} | |
| > | |
| <div className="p-5 flex flex-col h-full"> | |
| <div className="flex items-start justify-between mb-3"> | |
| <div className="flex gap-2 flex-wrap"> | |
| {bulkSelect && ( | |
| <span className={`px-2 py-1 rounded-full text-[10px] font-semibold flex items-center gap-1 ${ | |
| selected | |
| ? "bg-[var(--matcha-600)] text-[var(--pure-white)]" | |
| : "bg-[var(--oat-light)] text-[var(--warm-charcoal)]" | |
| }`}> | |
| <MaterialIcon name={selected ? "check_circle" : "radio_button_unchecked"} className="text-xs" /> | |
| {selected ? "Terpilih" : "Pilih"} | |
| </span> | |
| )} | |
| <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"> | |
| {q.examTypeName} | |
| </span> | |
| <span className="inline-flex items-center px-2.5 py-1 rounded-full bg-[var(--slushie-500)]/20 text-[var(--slushie-800)] text-xs font-semibold leading-none whitespace-nowrap"> | |
| {q.sectionTypeName} | |
| </span> | |
| {disabled && ( | |
| <span className="px-2 py-1 rounded-full bg-[var(--warm-silver)]/30 text-[var(--warm-charcoal)] text-[10px] font-semibold flex items-center gap-1"> | |
| <MaterialIcon name="lock" className="text-[10px]" /> | |
| Terkunci | |
| </span> | |
| )} | |
| </div> | |
| {q.avgRating && ( | |
| <div className="flex items-center gap-1 text-[var(--lemon-700)]"> | |
| <MaterialIcon name="star" className="text-sm" /> | |
| <span className="text-xs font-bold">{q.avgRating}</span> | |
| </div> | |
| )} | |
| </div> | |
| <h3 | |
| className={`font-headline font-bold text-[var(--clay-black)] mb-2 ${ | |
| isFeatured ? "text-xl line-clamp-3" : "text-base line-clamp-2" | |
| }`} | |
| > | |
| {q.questionText} | |
| </h3> | |
| <p | |
| className={`text-sm text-[var(--warm-charcoal)] flex-1 ${ | |
| isFeatured ? "line-clamp-4 mb-4" : "line-clamp-2 mb-4" | |
| }`} | |
| > | |
| {q.passageText} | |
| </p> | |
| <div className="flex items-center justify-between mt-auto pt-3 border-t border-[var(--oat-border)]"> | |
| <div className="flex gap-2"> | |
| <span className="px-2 py-0.5 rounded bg-[var(--oat-light)] text-[var(--warm-charcoal)] text-xs font-medium"> | |
| {formatLabel(q.format ?? "")} | |
| </span> | |
| <span className="px-2 py-0.5 rounded bg-[var(--oat-light)] text-[var(--warm-charcoal)] text-xs font-medium"> | |
| Lv.{q.difficulty} | |
| </span> | |
| <span className="px-2 py-0.5 rounded bg-[var(--oat-light)] text-[var(--warm-charcoal)] text-xs font-medium"> | |
| {q.usageCount}x digunakan | |
| </span> | |
| </div> | |
| </div> | |
| {/* Actions row */} | |
| <div className="flex items-center justify-between mt-3 pt-3 border-t border-[var(--oat-border)]"> | |
| {isOwner && !bulkSelect && ( | |
| <div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}> | |
| <button | |
| onClick={onTogglePublic} | |
| aria-label={q.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 ${ | |
| q.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 state icon */} | |
| <MaterialIcon | |
| name={q.isPublic ? "public" : "lock"} | |
| className="text-xs group-hover:hidden" | |
| /> | |
| {/* Hover state icon (shows intent) */} | |
| <MaterialIcon | |
| name={q.isPublic ? "lock" : "public"} | |
| className="text-xs hidden group-hover:inline" | |
| /> | |
| {/* Default label */} | |
| <span className="group-hover:hidden"> | |
| {q.isPublic ? "Publik" : "Privat"} | |
| </span> | |
| {/* Hover label (shows what will happen) */} | |
| <span className="hidden group-hover:inline"> | |
| {q.isPublic ? "Jadikan Privat" : "Jadikan Publik"} | |
| </span> | |
| </button> | |
| <button | |
| onClick={onDelete} | |
| aria-label="Hapus soal" | |
| className="text-[var(--pomegranate-400)] hover:bg-[var(--pomegranate-400)]/10 px-2 py-1 rounded-full transition-colors cursor-pointer" | |
| > | |
| <MaterialIcon name="delete" className="text-sm" /> | |
| </button> | |
| </div> | |
| )} | |
| {!bulkSelect && ( | |
| <Button | |
| size="sm" | |
| disabled={disabled} | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| if (!disabled) onToggle(); | |
| }} | |
| className={`ml-auto rounded-[var(--radius-lg)] text-xs ${ | |
| disabled | |
| ? "bg-[var(--oat-light)] text-[var(--warm-silver)] cursor-not-allowed" | |
| : isInBundle | |
| ? "bg-[var(--clay-black)] text-[var(--pure-white)] hover:bg-[var(--warm-charcoal)] cursor-pointer" | |
| : "bg-[var(--matcha-300)] text-[var(--matcha-800)] hover:bg-[var(--matcha-400)] cursor-pointer" | |
| }`} | |
| > | |
| {disabled ? "Tidak Tersedia" : isInBundle ? "Hapus dari Paket" : "Tambah ke Paket"} | |
| </Button> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |