NeahStable/app/vision/page.tsx
2026-01-14 11:15:51 +01:00

306 lines
11 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { redirect } from "next/navigation";
import { ResponsiveIframe } from "@/app/components/responsive-iframe";
import { Users, FolderKanban, Video, ArrowLeft, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { useToast } from "@/components/ui/use-toast";
interface Group {
id: string;
name: string;
path: string;
}
interface Mission {
id: string;
name: string;
logoUrl?: string | null;
}
type ConferenceType = "group" | "mission" | null;
export default function VisionPage() {
const { data: session, status } = useSession();
const { toast } = useToast();
const [groups, setGroups] = useState<Group[]>([]);
const [missions, setMissions] = useState<Mission[]>([]);
const [loading, setLoading] = useState(true);
const [selectedConference, setSelectedConference] = useState<{
type: ConferenceType;
id: string;
name: string;
} | null>(null);
const [jitsiUrl, setJitsiUrl] = useState<string | null>(null);
// Redirect if not authenticated
useEffect(() => {
if (status === "unauthenticated") {
redirect("/signin");
}
}, [status]);
// Fetch user groups and missions
useEffect(() => {
const fetchData = async () => {
if (status !== "authenticated" || !session?.user?.id) {
return;
}
try {
setLoading(true);
const userId = session.user.id;
// Fetch user groups
const groupsRes = await fetch(`/api/users/${userId}/groups`);
if (groupsRes.ok) {
const groupsData = await groupsRes.json();
setGroups(Array.isArray(groupsData) ? groupsData : []);
} else {
console.error("Failed to fetch groups");
}
// Fetch all missions and filter for user's missions
const missionsRes = await fetch("/api/missions?limit=1000");
if (missionsRes.ok) {
const missionsData = await missionsRes.json();
const allMissions = missionsData.missions || [];
// Filter missions where user is creator or member
const userMissions = allMissions.filter((mission: any) => {
const isCreator = mission.creator?.id === userId;
const isMember = mission.missionUsers?.some(
(mu: any) => mu.user?.id === userId
);
return isCreator || isMember;
});
setMissions(userMissions);
} else {
console.error("Failed to fetch missions");
}
} catch (error) {
console.error("Error fetching data:", error);
toast({
title: "Erreur",
description: "Impossible de charger les données",
variant: "destructive",
});
} finally {
setLoading(false);
}
};
fetchData();
}, [session, status, toast]);
// Handle conference selection
const handleConferenceClick = (type: ConferenceType, id: string, name: string) => {
const baseUrl = process.env.NEXT_PUBLIC_IFRAME_CONFERENCE_URL || 'https://vision.slm-lab.net';
let url: string;
if (type === "group") {
// URL format: https://vision.slm-lab.net/Groupe-{groupId}
url = `${baseUrl}/Groupe-${id}`;
} else {
// URL format: https://vision.slm-lab.net/{missionId}
url = `${baseUrl}/${id}`;
}
setSelectedConference({ type, id, name });
setJitsiUrl(url);
};
// Handle back to list
const handleBack = () => {
setSelectedConference(null);
setJitsiUrl(null);
};
// Show loading state
if (status === "loading" || loading) {
return (
<main className="w-full h-screen bg-gray-50 flex items-center justify-center">
<div className="text-center">
<Loader2 className="h-12 w-12 animate-spin text-blue-600 mx-auto mb-4" />
<p className="text-gray-600">Chargement...</p>
</div>
</main>
);
}
// Show Jitsi iframe if conference is selected
if (selectedConference && jitsiUrl) {
return (
<main className="w-full h-screen bg-black flex flex-col">
{/* Header with back button */}
<div className="bg-white border-b border-gray-200 px-4 py-3 flex items-center gap-4">
<Button
variant="ghost"
size="sm"
onClick={handleBack}
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Retour
</Button>
<div className="flex-1">
<h1 className="text-sm font-medium text-gray-900">
{selectedConference.type === "group" ? "Groupe" : "Mission"}: {selectedConference.name}
</h1>
</div>
</div>
{/* Jitsi iframe */}
<div className="flex-1 overflow-hidden">
<ResponsiveIframe
src={jitsiUrl}
allow="camera; microphone; fullscreen; display-capture; autoplay; clipboard-write; encrypted-media"
/>
</div>
</main>
);
}
// Show list of groups and missions
return (
<main className="w-full h-screen bg-gray-50">
<div className="w-full h-full px-4 pt-12 pb-4">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 mb-2">
Espaces de réunion
</h1>
<p className="text-gray-600">
Sélectionnez un groupe ou une mission pour accéder à son espace de réunion Jitsi
</p>
</div>
{/* Groups Section */}
<div className="mb-8">
<div className="flex items-center gap-2 mb-4">
<Users className="h-5 w-5 text-blue-600" />
<h2 className="text-xl font-semibold text-gray-900">
Groupes ({groups.length})
</h2>
</div>
{groups.length === 0 ? (
<div className="bg-white rounded-lg border border-gray-200 p-8 text-center">
<Users className="h-12 w-12 text-gray-400 mx-auto mb-3" />
<p className="text-gray-500">Aucun groupe disponible</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{groups.map((group) => (
<div
key={group.id}
className="bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition-shadow cursor-pointer"
onClick={() => handleConferenceClick("group", group.id, group.name)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1">
<div className="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center">
<Users className="h-5 w-5 text-blue-600" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-medium text-gray-900 truncate">
{group.name}
</h3>
<p className="text-sm text-gray-500 truncate">
{group.path}
</p>
</div>
</div>
<Button
variant="outline"
size="sm"
className="ml-2 flex items-center gap-2"
onClick={(e) => {
e.stopPropagation();
handleConferenceClick("group", group.id, group.name);
}}
>
<Video className="h-4 w-4" />
Réunion
</Button>
</div>
</div>
))}
</div>
)}
</div>
{/* Missions Section */}
<div>
<div className="flex items-center gap-2 mb-4">
<FolderKanban className="h-5 w-5 text-purple-600" />
<h2 className="text-xl font-semibold text-gray-900">
Missions ({missions.length})
</h2>
</div>
{missions.length === 0 ? (
<div className="bg-white rounded-lg border border-gray-200 p-8 text-center">
<FolderKanban className="h-12 w-12 text-gray-400 mx-auto mb-3" />
<p className="text-gray-500">Aucune mission disponible</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{missions.map((mission) => (
<div
key={mission.id}
className="bg-white rounded-lg border border-gray-200 p-4 hover:shadow-md transition-shadow cursor-pointer"
onClick={() => handleConferenceClick("mission", mission.id, mission.name)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3 flex-1">
{mission.logoUrl ? (
<div className="h-10 w-10 rounded-md overflow-hidden flex-shrink-0">
<img
src={mission.logoUrl}
alt={mission.name}
className="h-full w-full object-cover"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
</div>
) : (
<div className="h-10 w-10 rounded-md bg-purple-100 flex items-center justify-center flex-shrink-0">
<FolderKanban className="h-5 w-5 text-purple-600" />
</div>
)}
<div className="flex-1 min-w-0">
<h3 className="font-medium text-gray-900 truncate">
{mission.name}
</h3>
</div>
</div>
<Button
variant="outline"
size="sm"
className="ml-2 flex items-center gap-2"
onClick={(e) => {
e.stopPropagation();
handleConferenceClick("mission", mission.id, mission.name);
}}
>
<Video className="h-4 w-4" />
Réunion
</Button>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</main>
);
}