missions mission pages
This commit is contained in:
parent
53f3606035
commit
87bffe35db
@ -1,32 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function EditMissionPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const { missionId } = params;
|
||||
|
||||
// Redirect to the original mission edit page
|
||||
useEffect(() => {
|
||||
if (missionId) {
|
||||
router.push(`/missions/${missionId}/edit`);
|
||||
}
|
||||
}, [router, missionId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-blue-600 mb-4"></div>
|
||||
<p className="text-gray-500 mb-4">Redirection vers le formulaire d'édition de la mission...</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => router.push(`/missions/${missionId}/edit`)}
|
||||
>
|
||||
Cliquez ici si vous n'êtes pas redirigé automatiquement
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function MissionDetailsPage() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const { missionId } = params;
|
||||
|
||||
// Redirect to the original mission details page
|
||||
useEffect(() => {
|
||||
if (missionId) {
|
||||
router.push(`/missions/${missionId}`);
|
||||
}
|
||||
}, [router, missionId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-blue-600 mb-4"></div>
|
||||
<p className="text-gray-500 mb-4">Redirection vers les détails de la mission...</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => router.push(`/missions/${missionId}`)}
|
||||
>
|
||||
Cliquez ici si vous n'êtes pas redirigé automatiquement
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export default function CentralLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<main className="w-full h-screen bg-white">
|
||||
<div className="w-full h-full px-4 pt-12 pb-4 flex">
|
||||
{/* Sidebar with light pink background */}
|
||||
<div className="w-[234px] min-w-[234px] bg-pink-50 border-r border-gray-100 overflow-y-auto">
|
||||
{/* Title section */}
|
||||
<div className="bg-pink-50 py-4 px-6 border-b border-pink-100">
|
||||
<h2 className="text-lg font-medium text-gray-800">Central</h2>
|
||||
<p className="text-xs text-gray-600">Centre d'Administration et de Pilotage</p>
|
||||
</div>
|
||||
|
||||
{/* Navigation links */}
|
||||
<nav className="mt-4">
|
||||
<Link href="/central" passHref>
|
||||
<div className={`px-6 py-[10px] ${pathname === "/central" ? "bg-white" : ""} hover:bg-white`}>
|
||||
<span className="text-sm font-normal text-gray-700">Mes Missions</span>
|
||||
</div>
|
||||
</Link>
|
||||
<Link href="/central/new" passHref>
|
||||
<div className={`px-6 py-[10px] ${pathname === "/central/new" ? "bg-white" : ""} hover:bg-white`}>
|
||||
<span className="text-sm font-normal text-gray-700">Nouvelle Mission</span>
|
||||
</div>
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Main content - white background */}
|
||||
<div className="flex-1 overflow-auto bg-white">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function NewMissionPage() {
|
||||
const router = useRouter();
|
||||
|
||||
// Redirect to new mission form
|
||||
useEffect(() => {
|
||||
router.push("/missions/new");
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-blue-600 mb-4"></div>
|
||||
<p className="text-gray-500 mb-4">Redirection vers le formulaire de création de mission...</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => router.push("/missions/new")}
|
||||
>
|
||||
Cliquez ici si vous n'êtes pas redirigé automatiquement
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,304 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { getPublicUrl } from "@/lib/s3";
|
||||
|
||||
// Define Mission interface
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface MissionUser {
|
||||
id: string;
|
||||
role: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
interface Mission {
|
||||
id: string;
|
||||
name: string;
|
||||
logo?: string;
|
||||
oddScope: string[];
|
||||
niveau: string;
|
||||
missionType: string;
|
||||
projection: string;
|
||||
participation?: string;
|
||||
services?: string[];
|
||||
createdAt: string;
|
||||
creator: User;
|
||||
missionUsers: MissionUser[];
|
||||
intention?: string;
|
||||
}
|
||||
|
||||
export default function CentralPage() {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [missions, setMissions] = useState<Mission[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { toast } = useToast();
|
||||
|
||||
// Fetch missions from API
|
||||
useEffect(() => {
|
||||
const fetchMissions = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/api/missions');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch missions');
|
||||
}
|
||||
const data = await response.json();
|
||||
// Debug log to check mission data structure including intention
|
||||
console.log("Mission data with intention:", data.missions);
|
||||
setMissions(data.missions || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching missions:', error);
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Impossible de charger les missions",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMissions();
|
||||
}, []);
|
||||
|
||||
// Filter missions based on search term
|
||||
const filteredMissions = missions.filter(mission =>
|
||||
mission.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
mission.niveau.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
mission.missionType.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
mission.oddScope.some(scope => scope.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
);
|
||||
|
||||
// Function to format date
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
// Function to get mission category and icon
|
||||
const getODDInfo = (mission: Mission) => {
|
||||
const oddCode = mission.oddScope && mission.oddScope.length > 0
|
||||
? mission.oddScope[0]
|
||||
: null;
|
||||
|
||||
// Extract number from odd code (e.g., "odd-3" -> "3")
|
||||
const oddNumber = oddCode ? oddCode.replace('odd-', '') : null;
|
||||
|
||||
return {
|
||||
number: oddNumber,
|
||||
label: oddNumber ? `ODD ${oddNumber}` : "Non catégorisé",
|
||||
iconPath: oddNumber ? `/F SDG Icons 2019 WEB/F-WEB-Goal-${oddNumber.padStart(2, '0')}.png` : ""
|
||||
};
|
||||
};
|
||||
|
||||
// Function to get appropriate badge color based on niveau
|
||||
const getNiveauBadgeColor = (niveau: string) => {
|
||||
switch(niveau) {
|
||||
case 'a': return 'bg-green-100 text-green-800';
|
||||
case 'b': return 'bg-blue-100 text-blue-800';
|
||||
case 'c': return 'bg-purple-100 text-purple-800';
|
||||
case 's': return 'bg-amber-100 text-amber-800';
|
||||
default: return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
};
|
||||
|
||||
// Function to get full niveau label
|
||||
const getNiveauLabel = (niveau: string) => {
|
||||
switch(niveau) {
|
||||
case 'a': return 'A';
|
||||
case 'b': return 'B';
|
||||
case 'c': return 'C';
|
||||
case 's': return 'S';
|
||||
default: return niveau.toUpperCase();
|
||||
}
|
||||
};
|
||||
|
||||
// Function to get mission type label
|
||||
const getMissionTypeLabel = (type: string) => {
|
||||
switch(type) {
|
||||
case 'remote': return 'À distance';
|
||||
case 'onsite': return 'Sur site';
|
||||
case 'hybrid': return 'Hybride';
|
||||
default: return type;
|
||||
}
|
||||
};
|
||||
|
||||
// Function to get participation label
|
||||
const getParticipationLabel = (participation: string | null | undefined) => {
|
||||
console.log("Participation value:", participation); // Debug log
|
||||
if (!participation) return 'Non spécifié';
|
||||
switch(participation) {
|
||||
case 'volontaire': return 'Volontaire';
|
||||
case 'cooptation': return 'Cooptation';
|
||||
default: return participation;
|
||||
}
|
||||
};
|
||||
|
||||
// Function to get mission duration
|
||||
const getDuration = (projection: string) => {
|
||||
switch(projection) {
|
||||
case 'short': return '< 1 mois';
|
||||
case 'medium': return '1-3 mois';
|
||||
case 'long': return '> 3 mois';
|
||||
default: return projection;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-full bg-white">
|
||||
<div className="bg-white border-b border-gray-100 py-3 px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-gray-800 text-base font-medium">Gérez vos missions et opportunités de bénévolat</h1>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-500" />
|
||||
<Input
|
||||
placeholder="Rechercher une mission..."
|
||||
className="h-9 pl-9 pr-3 py-2 text-sm bg-white text-gray-800 border-gray-200 rounded-md w-60"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto bg-gray-50 p-6">
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center h-40">
|
||||
<div className="animate-spin rounded-full h-10 w-10 border-t-2 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
) : filteredMissions.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredMissions.map((mission) => {
|
||||
const oddInfo = getODDInfo(mission);
|
||||
const niveauColor = getNiveauBadgeColor(mission.niveau);
|
||||
|
||||
return (
|
||||
<div key={mission.id} className="bg-white shadow-sm hover:shadow-md transition-shadow duration-200 border border-gray-200 overflow-hidden h-full rounded-lg flex flex-col">
|
||||
{/* Card Header with Name and Level */}
|
||||
<div className="px-5 pt-4 pb-3 flex justify-between items-center border-b border-gray-100">
|
||||
<h2 className="text-base font-medium text-gray-900 line-clamp-2 flex-1">{mission.name}</h2>
|
||||
<div className="flex items-center gap-2 ml-2">
|
||||
{/* ODD scope icon moved next to level badge */}
|
||||
{oddInfo.number && (
|
||||
<div className="flex items-center bg-gray-100 p-1 rounded-md">
|
||||
<img
|
||||
src={oddInfo.iconPath}
|
||||
alt={oddInfo.label}
|
||||
className="w-8 h-8"
|
||||
onError={(e) => {
|
||||
// Fallback if image fails to load
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<span className={`flex-shrink-0 text-sm font-bold px-2.5 py-1.5 rounded-md ${niveauColor}`}>
|
||||
{getNiveauLabel(mission.niveau)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Centered Logo */}
|
||||
<div className="flex justify-center items-center p-6 flex-grow">
|
||||
<div className="w-48 h-48 relative">
|
||||
{mission.logo ? (
|
||||
<img
|
||||
src={mission.logo || ''}
|
||||
alt={mission.name}
|
||||
className="w-full h-full object-cover rounded-md"
|
||||
onError={(e) => {
|
||||
console.log("Logo failed to load:", mission.logo);
|
||||
// If the image fails to load, show the fallback
|
||||
(e.currentTarget as HTMLImageElement).style.display = 'none';
|
||||
// Show the fallback div
|
||||
const fallbackDiv = e.currentTarget.parentElement?.querySelector('.logo-fallback');
|
||||
if (fallbackDiv) {
|
||||
(fallbackDiv as HTMLElement).style.display = 'flex';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
className={`logo-fallback w-full h-full flex items-center justify-center bg-gray-100 rounded-md text-gray-500 text-4xl font-medium ${mission.logo ? 'hidden' : ''}`}
|
||||
>
|
||||
{mission.name.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mission details */}
|
||||
<div className="px-5 pt-3 pb-4 bg-gray-50 border-t border-gray-100">
|
||||
{/* Type and duration */}
|
||||
<div className="flex flex-wrap gap-x-4 gap-y-2 mb-3">
|
||||
<div className="flex items-center text-sm text-gray-700">
|
||||
<span className="w-3 h-3 rounded-full bg-blue-500 mr-2"></span>
|
||||
{getMissionTypeLabel(mission.missionType)}
|
||||
</div>
|
||||
<div className="flex items-center text-sm text-gray-700">
|
||||
<span className="w-3 h-3 rounded-full bg-purple-500 mr-2"></span>
|
||||
{getDuration(mission.projection)}
|
||||
</div>
|
||||
{mission.participation && (
|
||||
<div className="flex items-center text-sm text-gray-700">
|
||||
<span className="w-3 h-3 rounded-full bg-amber-500 mr-2"></span>
|
||||
{getParticipationLabel(mission.participation)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Optional services */}
|
||||
{mission.services && mission.services.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{mission.services.map(service => (
|
||||
<span key={service} className="inline-flex items-center px-2 py-1 rounded text-xs bg-gray-200 text-gray-800">
|
||||
{service}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Intent preview */}
|
||||
{mission.intention && (
|
||||
<div className="mt-2 text-sm text-gray-700">
|
||||
<p className="line-clamp-2">{mission.intention}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* View details button */}
|
||||
<div className="mt-4">
|
||||
<Link href={`/central/${mission.id}`} passHref>
|
||||
<Button className="w-full text-sm" variant="outline">
|
||||
Voir les détails
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-64">
|
||||
<p className="text-gray-500 mb-4">Aucune mission trouvée</p>
|
||||
<Link href="/central/new" passHref>
|
||||
<Button>Créer une nouvelle mission</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user