Files
grademaxxing/app/components/GradeTable.tsx
2026-01-17 13:37:57 +01:00

150 lines
6.1 KiB
TypeScript

import type { Grade, Subject } from "~/types/api";
import { formatGrade } from "~/utils/gradeCalculations";
interface GradeTableProps {
grades: Grade[];
subject?: Subject;
onEdit?: (grade: Grade) => void;
onDelete?: (gradeId: string) => void;
deletingGradeId?: string | null;
}
export function GradeTable({ grades, subject, onEdit, onDelete, deletingGradeId }: GradeTableProps) {
if (grades.length === 0) {
return (
<div className="text-center py-8 text-gray-500">
No grades recorded yet. Add your first grade!
</div>
);
}
return (
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-50 border-b">
<th className="text-left py-3 px-4 font-semibold text-gray-700">Date</th>
<th className="text-left py-3 px-4 font-semibold text-gray-700">Name</th>
<th className="text-left py-3 px-4 font-semibold text-gray-700">Category</th>
<th className="text-right py-3 px-4 font-semibold text-gray-700">Grade</th>
<th className="text-right py-3 px-4 font-semibold text-gray-700">Percentage</th>
<th className="text-right py-3 px-4 font-semibold text-gray-700">Weight</th>
<th className="text-right py-3 px-4 font-semibold text-gray-700">Actions</th>
</tr>
</thead>
<tbody>
{grades.map((grade) => {
const percentage = (grade.grade / grade.max_grade) * 100;
const category = subject?.grading_categories.find(
(c) => c.name === grade.category_name
);
return (
<tr key={grade._id} className="border-b hover:bg-gray-50">
<td className="py-3 px-4 text-sm text-gray-600">
{new Date(grade.date).toLocaleDateString()}
</td>
<td className="py-3 px-4">
<div className="font-medium">{grade.name || "Unnamed"}</div>
{grade.notes && (
<div className="text-sm text-gray-500">{grade.notes}</div>
)}
</td>
<td className="py-3 px-4">
<span
className="px-2 py-1 text-xs font-medium rounded-full"
style={{
backgroundColor: category?.color || "#e5e7eb",
color: "#1f2937",
}}
>
{grade.category_name}
</span>
</td>
<td className="py-3 px-4 text-right font-medium">
{grade.grade} / {grade.max_grade}
</td>
<td className="py-3 px-4 text-right font-semibold">
{formatGrade(percentage)}%
</td>
<td className="py-3 px-4 text-right text-sm text-gray-600">
{grade.weight_in_category}x
</td>
<td className="py-3 px-4 text-right">
<div className="flex justify-end gap-2">
{onEdit && (
<button
onClick={() => onEdit(grade)}
className="text-blue-500 hover:text-blue-700"
aria-label="Edit grade"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
/>
</svg>
</button>
)}
{onDelete && (
<button
onClick={() => onDelete(grade._id)}
className="text-red-500 hover:text-red-700 disabled:opacity-50"
aria-label="Delete grade"
disabled={deletingGradeId === grade._id}
>
{deletingGradeId === grade._id ? (
<svg
className="w-4 h-4 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
) : (
<svg
className="w-4 h-4"
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>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}