first commit
This commit is contained in:
149
app/components/GradeTable.tsx
Normal file
149
app/components/GradeTable.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user