Files
grademaxxing/app/components/SubjectCard.tsx

115 lines
4.4 KiB
TypeScript

import type { Subject } from "~/types/api";
import { Link } from "react-router";
import { formatGradeBySystem, type GradeSystem } from "~/utils/gradeSystems";
import { calculateTargetProgress, getTargetStatusBgColor } from "~/utils/gradeCalculations";
interface SubjectCardProps {
subject: Subject;
averageGrade?: number;
calculatedAverage?: number;
gradeSystem?: string;
onDelete?: () => void;
}
export function SubjectCard({ subject, averageGrade, calculatedAverage, gradeSystem, onDelete }: SubjectCardProps) {
// Use provided gradeSystem, fallback to subject's own system
const displaySystem = (gradeSystem || subject.grade_system || "percentage") as GradeSystem;
// Calculate target progress if grade is available
const targetProgress = averageGrade !== undefined ? calculateTargetProgress(averageGrade, subject) : null;
const hasTaget = targetProgress && targetProgress.status !== "no-target";
const isOverridden = calculatedAverage !== undefined && averageGrade !== undefined && Math.abs(calculatedAverage - averageGrade) > 0.01;
return (
<div
className={`p-6 rounded-lg border-2 hover:shadow-lg transition-shadow ${hasTaget ? getTargetStatusBgColor(targetProgress.status) : ""}`}
style={{ borderColor: subject.color || "#3b82f6" }}
>
<div className="flex justify-between items-start mb-3">
<Link to={`/subjects/${subject._id}`} className="flex-1">
<h3 className="text-xl font-semibold text-gray-900 hover:text-blue-600">
{subject.name}
</h3>
{subject.teacher && (
<p className="text-sm text-gray-600 mt-1">{subject.teacher}</p>
)}
</Link>
{onDelete && (
<button
onClick={onDelete}
className="text-red-500 hover:text-red-700 ml-2"
aria-label="Delete subject"
>
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
</button>
)}
</div>
{averageGrade !== undefined && (
<div className="mb-3">
<div className="flex items-center justify-between">
<div>
<div className="text-3xl font-bold" style={{ color: subject.color || "#3b82f6" }}>
{formatGradeBySystem(averageGrade, displaySystem, 1)}
{isOverridden && (
<span className="text-gray-500 text-lg font-normal ml-2">
({formatGradeBySystem(calculatedAverage!, displaySystem, 1)})
</span>
)}
</div>
<p className="text-sm text-gray-600">Current Average</p>
</div>
{hasTaget && (
<div className="text-right">
<div className="text-sm text-gray-600">Target: {formatGradeBySystem(targetProgress.targetPercentage, displaySystem, 1)}</div>
<div className="text-xs font-semibold mt-1">
{targetProgress.status === "above" && (
<span className="text-green-700"> Above Target</span>
)}
{targetProgress.status === "near" && (
<span className="text-yellow-700"> Near Target</span>
)}
{targetProgress.status === "below" && (
<span className="text-red-700"> Below Target</span>
)}
</div>
</div>
)}
</div>
</div>
)}
<div className="space-y-2">
<p className="text-sm font-medium text-gray-700">Grading Categories:</p>
<div className="flex flex-wrap gap-2">
{subject.grading_categories.map((category, index) => (
<span
key={index}
className="px-2 py-1 text-xs font-medium rounded-full"
style={{
backgroundColor: category.color || "#e5e7eb",
color: "#1f2937",
}}
>
{category.name} ({category.weight}%)
</span>
))}
</div>
</div>
</div>
);
}