All checks were successful
Deploy to Netlify / Deploy to Netlify (push) Successful in 1m55s
444 lines
14 KiB
TypeScript
444 lines
14 KiB
TypeScript
import "katex/dist/katex.min.css";
|
|
import { DomUtils, parseDocument } from "htmlparser2";
|
|
import Latex from "react-latex-next";
|
|
import { useState } from "react";
|
|
import { Link, Navigate, useNavigate, useParams } from "react-router";
|
|
import { Button } from "~/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
|
import {
|
|
ArrowLeft,
|
|
Clock,
|
|
User,
|
|
UserCheck,
|
|
MessageSquare,
|
|
Paperclip,
|
|
Star,
|
|
ChevronUp,
|
|
ChevronDown,
|
|
MapPinHouse,
|
|
RefreshCw,
|
|
Share2,
|
|
ExternalLink,
|
|
Users,
|
|
} from "lucide-react";
|
|
import { Separator } from "~/components/ui/separator";
|
|
import ColleDetailsSkeleton from "~/components/details/skeleton-details";
|
|
import AttachmentItem from "~/components/details/attachment";
|
|
import Error from "~/components/error";
|
|
import { Badge } from "~/components/ui/badge";
|
|
import { AUTH_ERROR, refreshColle, useColle, useUser } from "~/lib/api";
|
|
import { toast } from "sonner";
|
|
import {
|
|
formatDate,
|
|
formatGrade,
|
|
formatTime,
|
|
getColorClass,
|
|
getSubjectColor,
|
|
getSubjectEmoji,
|
|
goBack,
|
|
} from "~/lib/utils";
|
|
import { useQueryClient } from "@tanstack/react-query";
|
|
|
|
// TODO: Move all code to components
|
|
export default function ColleDetailPage() {
|
|
const { user, isLoading: isUserLoading, error: userError } = useUser();
|
|
const queryClient = useQueryClient();
|
|
|
|
const navigate = useNavigate();
|
|
const params = useParams<{ colleId: string }>();
|
|
|
|
const colleId = parseInt(params.colleId!);
|
|
if (isNaN(colleId)) {
|
|
return <Navigate to="/" />;
|
|
}
|
|
const { colle, error, isLoading } = useColle(colleId);
|
|
|
|
const [isReloading, setIsReloading] = useState(false);
|
|
|
|
if (isUserLoading) {
|
|
return <ColleDetailsSkeleton />;
|
|
}
|
|
if (userError?.message === AUTH_ERROR) {
|
|
return <Navigate to="/login" replace />;
|
|
}
|
|
if (userError) {
|
|
return <Error message={userError.message} />;
|
|
}
|
|
|
|
// TODO: Favorite toggle function
|
|
const toggleStar = () => {};
|
|
|
|
if (error)
|
|
return (
|
|
<Error
|
|
title="Impossible de charger cette colle"
|
|
message={error?.toString()}
|
|
code={500}
|
|
description="Une erreur s'est produite lors du chargement de la colle."
|
|
/>
|
|
);
|
|
if (isLoading || !colle) return <ColleDetailsSkeleton />;
|
|
|
|
const handleToggleFavorite = () => {};
|
|
|
|
const handleReload = () => {
|
|
setIsReloading(true);
|
|
refreshColle(colle.id)
|
|
.then(() => {
|
|
toast("Les données de cette colle vont être mises à jour", {
|
|
icon: "🔄",
|
|
description: "Rafraîchissement en cours...",
|
|
});
|
|
})
|
|
.catch((error) => {
|
|
console.error("Error refreshing colle:", error);
|
|
toast.error("Échec du rafraîchissement de la colle", {
|
|
icon: "❌",
|
|
description: "Veuillez réessayer plus tard.",
|
|
});
|
|
})
|
|
.finally(() => {
|
|
setIsReloading(false);
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["colle", colle.id],
|
|
});
|
|
});
|
|
};
|
|
|
|
const handleShare = async () => {
|
|
const shareUrl = window.location.href;
|
|
const shareTitle = `Colle de ${colle.subject.name} - ${colle.student.firstName}`;
|
|
const shareText = `Colle de ${colle.subject.name} du ${formatDate(
|
|
colle.date
|
|
)} à ${formatTime(colle.date)} - ${colle.student} avec ${
|
|
colle.examiner
|
|
}.\n\nConsultez-le résumé ici :`;
|
|
try {
|
|
if (navigator.share) {
|
|
await navigator.share({
|
|
title: shareTitle,
|
|
text: shareText,
|
|
url: shareUrl,
|
|
});
|
|
} else {
|
|
// Fallback to copying the URL
|
|
await navigator.clipboard.writeText(shareUrl);
|
|
toast("Lien copié dans le presse-papiers", {
|
|
icon: "📋",
|
|
description: "Vous pouvez le partager avec vos amis !",
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("Error sharing:", error);
|
|
}
|
|
};
|
|
|
|
const handleOpenBJColle = () => {
|
|
const url = `https://bjcolle.fr/students_oral_disp.php?colle=${colle.bjid}&hgfebrgl8ri3h=${colle.bjsecret}`;
|
|
window.open(url, "_blank");
|
|
toast("Ouverture de BJColle", {
|
|
icon: "🔗",
|
|
description: "Redirection vers BJColle...",
|
|
});
|
|
};
|
|
|
|
const subjectColor = getColorClass(
|
|
getSubjectColor(colle.subject.name, user.preferences)
|
|
);
|
|
const subjectEmoji = getSubjectEmoji(colle.subject.name, user.preferences);
|
|
|
|
return (
|
|
<div className="container mx-auto py-6 px-4 pb-20 md:pb-6 md:py-8">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<Button variant="outline" onClick={() => goBack(navigate)}>
|
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
|
Retour
|
|
</Button>
|
|
<div className="flex md:hidden items-center gap-2">
|
|
<div className="space-x-2">
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
className="h-10 w-10"
|
|
onClick={handleReload}
|
|
disabled={isReloading}
|
|
>
|
|
<RefreshCw
|
|
className={`h-5 w-5 ${isReloading ? "animate-spin" : ""}`}
|
|
/>
|
|
<span className="sr-only">Recharger</span>
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
className="h-10 w-10"
|
|
onClick={handleShare}
|
|
>
|
|
<Share2 className="h-5 w-5" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Card className="max-w-3xl mx-auto">
|
|
<CardHeader className="pb-2">
|
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-2">
|
|
<div className="flex flex-col">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-xl">
|
|
Colle de {colle.subject.name}
|
|
</CardTitle>
|
|
{colle.grade && (
|
|
<div
|
|
className={`md:hidden px-3 py-1 rounded-md text-sm font-medium ${subjectColor}`}
|
|
>
|
|
{formatGrade(colle.grade)}/20
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2 mt-2">
|
|
<Badge className={subjectColor}>
|
|
{colle.subject.name} {subjectEmoji}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Desktop Action Buttons */}
|
|
<div className="hidden md:flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-9 w-9"
|
|
onClick={handleShare}
|
|
title="Partager"
|
|
>
|
|
<Share2 className="h-4 w-4" />
|
|
<span className="sr-only">Partager</span>
|
|
</Button>
|
|
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="h-9 w-9"
|
|
onClick={handleReload}
|
|
disabled={isReloading}
|
|
title="Recharger"
|
|
>
|
|
<RefreshCw
|
|
className={`h-4 w-4 ${isReloading ? "animate-spin" : ""}`}
|
|
/>
|
|
<span className="sr-only">Recharger</span>
|
|
</Button>
|
|
|
|
{/* <Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className={`h-9 w-9 ${
|
|
// TODO: Use a proper favorite state
|
|
false ? "text-yellow-500" : "text-muted-foreground"
|
|
}`}
|
|
onClick={handleToggleFavorite}
|
|
>
|
|
<Star
|
|
className={`h-5 w-5 ${
|
|
// TODO: Use a proper favorite state
|
|
false ? "fill-yellow-500" : ""
|
|
}`}
|
|
/>
|
|
<span className="sr-only">Ajouter aux favoris</span>
|
|
</Button> */}
|
|
|
|
{colle.grade && (
|
|
<div
|
|
className={`px-3 py-1 rounded-md text-sm font-medium ${subjectColor}`}
|
|
>
|
|
{formatGrade(colle.grade)}/20
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
<div className="space-y-2">
|
|
<div>
|
|
<h3 className="text-sm font-medium text-muted-foreground mb-1">
|
|
Date
|
|
</h3>
|
|
<div className="flex items-center gap-2">
|
|
<Clock className="h-4 w-4" />
|
|
<span>
|
|
{formatDate(colle.date)}, à {formatTime(colle.date)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="text-sm font-medium text-muted-foreground mb-1">
|
|
Colleur
|
|
</h3>
|
|
<div className="flex items-center gap-2">
|
|
<UserCheck className="h-4 w-4" />
|
|
<span>{colle.examiner.name}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div>
|
|
<h3 className="text-sm font-medium text-muted-foreground mb-1">
|
|
Élève
|
|
</h3>
|
|
<div className="flex items-center gap-2">
|
|
<User className="h-4 w-4" />
|
|
<span>{colle.student.fullName}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{colle.relatives?.length > 0 && (
|
|
<div>
|
|
<h3 className="text-sm font-medium text-muted-foreground mb-1">
|
|
Autres élèves
|
|
</h3>
|
|
{colle.relatives.map((linkedColle) => (
|
|
<div className="flex items-center gap-2">
|
|
<Users className="h-4 w-4" />
|
|
<Link
|
|
to={`/colles/${linkedColle.id}`}
|
|
key={linkedColle.id}
|
|
>
|
|
{linkedColle.student.fullName}
|
|
</Link>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{colle.room && (
|
|
<div className="space-y-2">
|
|
<div>
|
|
<h3 className="text-sm font-medium text-muted-foreground mb-1">
|
|
Salle
|
|
</h3>
|
|
<div className="flex items-center gap-2">
|
|
<MapPinHouse className="h-4 w-4" />
|
|
<span>{colle.room.name}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{colle.content && (
|
|
<>
|
|
<Separator />
|
|
|
|
<div>
|
|
<h3 className="text-lg font-medium mb-2 flex items-center gap-2">
|
|
<MessageSquare className="h-5 w-5" />
|
|
Sujet
|
|
</h3>
|
|
|
|
<div className="p-4 bg-muted rounded-lg">
|
|
<ExpandableComment comment={colle.content} />
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{colle.comment && (
|
|
<>
|
|
<Separator />
|
|
|
|
<div>
|
|
<h3 className="text-lg font-medium mb-2 flex items-center gap-2">
|
|
<MessageSquare className="h-5 w-5" />
|
|
Commentaires
|
|
</h3>
|
|
<div className="p-4 bg-muted rounded-lg">
|
|
<ExpandableComment comment={colle.comment} />
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{colle.attachments && colle.attachments?.length > 0 && (
|
|
<div>
|
|
<h3 className="text-lg font-medium mb-3 flex items-center gap-2">
|
|
<Paperclip className="h-5 w-5" />
|
|
Pièces jointes
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
|
{colle.attachments.map((attachment, index) => (
|
|
<AttachmentItem key={index} attachment={attachment} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* BJColle External Link */}
|
|
<div className="mt-8 pt-4 border-t border-dashed">
|
|
<div className="flex justify-end items-center space-x-1 mt-1 text-xs text-muted-foreground">
|
|
<ExternalLink className="h-4 w-4" />
|
|
<span onClick={handleOpenBJColle}>
|
|
Accéder à la colle depuis BJColle
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// TODO: Custom render component for LaTeX rendering
|
|
// Component for expandable comments
|
|
function ExpandableComment({ comment }: { comment: string }) {
|
|
// Crop comments longer than 750 characters
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
const commentLimit = 750; // Character limit before truncating
|
|
const isLongComment = removeHtmlElements(comment).length > commentLimit;
|
|
|
|
const toggleExpand = () => {
|
|
setIsExpanded(!isExpanded);
|
|
};
|
|
|
|
const displayedComment =
|
|
isExpanded || !isLongComment
|
|
? comment
|
|
: `${comment.substring(0, commentLimit)}...`;
|
|
|
|
return (
|
|
<div>
|
|
<Latex delimiters={[{ left: "$$", right: "$$", display: false }]}>
|
|
{displayedComment}
|
|
</Latex>
|
|
{isLongComment && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="mt-1 h-7 px-2 text-primary"
|
|
onClick={toggleExpand}
|
|
>
|
|
{isExpanded ? (
|
|
<span className="flex items-center gap-1">
|
|
Afficher moins <ChevronUp className="h-3 w-3" />
|
|
</span>
|
|
) : (
|
|
<span className="flex items-center gap-1">
|
|
Afficher plus <ChevronDown className="h-3 w-3" />
|
|
</span>
|
|
)}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Remove HTML elements and return plain text (for text length calculation and cropping)
|
|
function removeHtmlElements(htmlString: string) {
|
|
const document = parseDocument(htmlString);
|
|
return DomUtils.textContent(document).trim();
|
|
}
|