src/components/courses/course-detail-page-content.tsx
Rama main · réplica de lectura del repositorio original.
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { useAuthGuard } from "@/hooks/useAuthGuard";
import { useDocumentTitle } from "@/hooks/useDocumentTitle";
import { useResource } from "@/hooks/useResource";
import { useDeleteResource } from "@/hooks/useDeleteResource";
import { useReturnTo, useWithReturnTo } from "@/hooks/useReturnTo";
import { useRecordByUuid } from "@/hooks/useRecordByUuid";
import { contactsAPI } from "@/lib/api/contacts";
import { usePeopleByUuid } from "@/hooks/usePeopleByUuid";
import { splitCourseInstructor } from "@/lib/people";
import { coursesAPI, Course } from "@/lib/api/courses";
import { certificationAgencyLabel } from "@/lib/api/certifications";
import { courseStatusLabel } from "@/lib/course";
import { formatTripDateRange } from "@/lib/date-time";
import { cn } from "@/lib/utils";
import { RecentDivesCard } from "@/components/dives/recent-dives-card";
import { CourseCertificationsCard } from "@/components/courses/course-certifications-card";
import { CourseDialog } from "@/components/courses/course-dialog";
import { PeopleList } from "@/components/people/people-list";
import { Button, ButtonLink } from "@/components/ui/button";
import {
DeleteMenuItem,
ItemActionsMenu,
} from "@/components/ui/item-actions-menu";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import {
HERO_BODY,
HERO_CONTROL,
MapHeroPageSkeleton,
PAGE_COLUMN,
PlainHero,
} from "@/components/ui/map-hero";
import { NotFoundState } from "@/components/ui/not-found-state";
import { BadgeCheck, Edit, Plus, GraduationCap } from "lucide-react";
import Link from "next/link";
import { PageSpinner } from "@/components/ui/page-spinner";
import { InfoRow } from "@/components/ui/info-row";
export function CourseDetailPageContent() {
const router = useRouter();
const { user, isAuthenticated, isLoading: isAuthLoading } = useAuthGuard();
const [isEditOpen, setIsEditOpen] = useState(false);
// Held here rather than in the certifications card, because the sidebar's
// button opens the same dialog the card's empty state does.
const [isAddingCertification, setIsAddingCertification] = useState(false);
const back = useReturnTo({ href: "/courses", label: "Back to courses" });
const withReturnTo = useWithReturnTo();
const {
resource: course,
setResource: setCourse,
isLoading: isLoadingCourse,
} = useResource<Course>(coursesAPI.getCourse, {
enabled: !!user,
errorMessage: "Failed to load course details. Please try again.",
redirectTo: "/courses",
});
useDocumentTitle(course?.name, "Courses");
const del = useDeleteResource(coursesAPI.deleteCourse, {
// A plain confirm, no reassign offer: deleting a course unlinks it from the
// dives and cards that named it, and there is nothing to hand them to.
confirmMessage:
"Are you sure you want to delete this course? The dives and certifications on it are kept, but they will no longer name it.",
successMessage: "Course deleted successfully.",
errorMessage: "Failed to delete course. Please try again.",
onDeleted: () => router.push(back.href),
});
const isDeleting = del.deletingId !== null;
const contact = useRecordByUuid(contactsAPI.getContact, course?.contact_uuid);
// The instructor on a row of their own, as the dialog holds them, and everyone
// else on the course under it.
const { instructorUuid, others } = splitCourseInstructor(course?.people);
const people = usePeopleByUuid(
(course?.people ?? []).map((reference) => reference.person_uuid),
);
const instructor = instructorUuid ? people[instructorUuid] : undefined;
if (isAuthLoading) {
return <PageSpinner variant="inset" />;
}
if (!isAuthenticated) {
return null; // Will redirect to signin
}
if (isLoadingCourse) {
return (
<MapHeroPageSkeleton
plain
figureless
backHref={back.href}
backLabel={back.label}
icon={GraduationCap}
/>
);
}
if (!course) {
return (
<div className={PAGE_COLUMN}>
<NotFoundState
message="Course not found."
backHref={back.href}
backLabel={back.label}
/>
</div>
);
}
// In its card's formats, so a course reads the same on its page as in the
// list.
const subtitle = [
certificationAgencyLabel(course.agency, course.agency_other),
formatTripDateRange(
course.start_date ?? undefined,
course.end_date ?? undefined,
),
courseStatusLabel(course.status),
];
return (
<div>
<PlainHero
backHref={back.href}
backLabel={back.label}
icon={GraduationCap}
title={course.name}
subtitle={subtitle}
figures={[]}
actions={
<>
<Button
variant="ghost"
size="sm"
className={HERO_CONTROL}
onClick={() => setIsEditOpen(true)}
>
<Edit className="h-4 w-4 mr-2" />
Edit
</Button>
<ItemActionsMenu variant="ghost" size="sm" className={HERO_CONTROL}>
<DeleteMenuItem
onSelect={() => del.requestDelete(course.uuid)}
disabled={isDeleting}
/>
</ItemActionsMenu>
</>
}
/>
<CourseDialog
open={isEditOpen}
onOpenChange={setIsEditOpen}
course={course}
onSaved={setCourse}
/>
<ConfirmDialog
open={del.pendingId !== null}
onOpenChange={(open) => !open && del.cancelDelete()}
title="Delete course"
description={del.confirmMessage}
confirmText="Delete"
isLoading={isDeleting}
onConfirm={del.confirmDelete}
/>
<div
className={cn(
HERO_BODY,
"grid grid-cols-1 lg:grid-cols-3 gap-6 max-sm:gap-2.5",
)}
>
<div className="lg:col-span-2 space-y-6 max-sm:space-y-2.5">
<CourseCertificationsCard
course={course}
isAdding={isAddingCertification}
onAddingChange={setIsAddingCertification}
/>
<RecentDivesCard
complete
enabled={!!user}
courseId={course.uuid}
title="Dives on This Course"
description="All dives logged as part of this course"
viewAllHref={null}
emptyTitle="No dives logged for this course yet"
emptyDescription="Log a dive and assign it to this course to see it here."
newDiveHref={`/dives/new?course_uuid=${course.uuid}`}
newDiveLabel="Log a dive for this course"
/>
</div>
<div className="space-y-6 max-sm:space-y-2.5">
<Card>
<CardHeader>
<CardTitle as="h2" className="flex items-center gap-2">
<GraduationCap className="h-5 w-5" />
Course Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{contact && <InfoRow label="Dive center">{contact.name}</InfoRow>}
{instructor && (
<InfoRow label="Instructor">
<Link
href={withReturnTo(`/people/${instructor.uuid}`)}
className="relative hover:underline touch:tap-target"
>
{instructor.name}
</Link>
</InfoRow>
)}
{course.instructor_number && (
<InfoRow label="Instructor number">
{course.instructor_number}
</InfoRow>
)}
{others.some((reference) => people[reference.person_uuid]) && (
<div>
<div className="text-sm font-medium text-muted-foreground mb-1">
People
</div>
<PeopleList people={others} resolved={people} />
</div>
)}
{course.notes && (
<InfoRow label="Notes">
<span className="whitespace-pre-wrap">{course.notes}</span>
</InfoRow>
)}
{/* Two columns exactly where the sidebar is wide: it is the full
content width until `lg`, where it becomes a third of it and
the pair no longer fits across. */}
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-1">
<ButtonLink
href={withReturnTo(`/dives/new?course_uuid=${course.uuid}`)}
>
<Plus className="h-4 w-4 mr-2" />
Log a dive
</ButtonLink>
<Button
variant="outline"
onClick={() => setIsAddingCertification(true)}
>
<BadgeCheck className="h-4 w-4 mr-2" />
Add a certification
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
);
}