Merge version_7 into main #10

Merged
bender merged 1 commits from version_7 into main 2026-03-12 12:26:43 +00:00

View File

@@ -5,7 +5,7 @@ import { ThemeProvider } from "@/providers/themeProvider/ThemeProvider";
import NavbarStyleCentered from "@/components/navbar/NavbarStyleCentered/NavbarStyleCentered";
import ContactText from "@/components/sections/contact/ContactText";
import FooterBaseCard from "@/components/sections/footer/FooterBaseCard";
import { Camera, Upload, Trash2, Play } from "lucide-react";
import { Camera, Upload, Trash2, Play, Maximize2 } from "lucide-react";
import { useState } from "react";
export default function PortfolioPage() {
@@ -56,6 +56,8 @@ export default function PortfolioPage() {
>([]);
const [uploadError, setUploadError] = useState("");
const [uploadSuccess, setUploadSuccess] = useState(false);
const [selectedMedia, setSelectedMedia] = useState<string | null>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const handleMediaUpload = (
e: React.ChangeEvent<HTMLInputElement>,
@@ -101,8 +103,13 @@ export default function PortfolioPage() {
const handleDeleteMedia = (id: string) => {
setUploadedMedia((prev) => prev.filter((media) => media.id !== id));
if (selectedMedia === id) {
setSelectedMedia(null);
}
};
const selectedMediaItem = uploadedMedia.find((m) => m.id === selectedMedia);
return (
<ThemeProvider
defaultButtonVariant="directional-hover"
@@ -126,188 +133,204 @@ export default function PortfolioPage() {
/>
</div>
{/* Media Upload and Management Section */}
<div id="media-manager" data-section="media-manager">
{/* Large Format Media Gallery Section */}
<div id="media-gallery" data-section="media-gallery">
<div className="w-full py-20 px-4 md:px-8">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="text-center mb-16">
<div className="inline-flex items-center gap-2 mb-4 px-4 py-2 rounded-full bg-[var(--accent)] text-[var(--background)]">
<Camera size={16} />
<span className="text-sm font-medium">Portfolio Projets</span>
<span className="text-sm font-medium">Galerie Projets Complétés</span>
</div>
<h2 className="text-3xl md:text-5xl font-bold mb-4 text-[var(--foreground)]">
Galerie Photos & Vidéos de Projets
Photos & Vidéos de Nos Réalisations
</h2>
<p className="text-lg text-[var(--foreground)] opacity-75 max-w-2xl mx-auto">
Téléchargez et gérez les photos et vidéos de vos projets complétés. Présentez votre portefeuille aux clients potentiels avec une galerie professionnelle.
Découvrez nos projets complétés en grand format. Visualisez la qualité de notre travail et les transformations que nous avons accomplies pour nos clients.
</p>
</div>
<div className="grid md:grid-cols-1 gap-8">
{/* Large Media Gallery Frame */}
<div className="bg-[var(--card)] rounded-3xl p-8 md:p-12 border border-[var(--accent)] border-opacity-20 shadow-lg">
<div className="space-y-8">
{/* Upload Controls */}
<div className="grid md:grid-cols-2 gap-6">
{/* Photo Upload */}
<div>
<h3 className="text-lg font-bold mb-4 text-[var(--foreground)] flex items-center gap-2">
<Camera size={20} />
Télécharger des Photos
</h3>
<label className="flex flex-col items-center justify-center w-full h-40 border-2 border-dashed border-[var(--accent)] border-opacity-40 rounded-xl cursor-pointer hover:border-opacity-60 transition-all bg-[var(--background)] bg-opacity-50">
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<Upload size={28} className="text-[var(--primary-cta)] mb-2" />
<p className="text-sm font-medium text-[var(--foreground)] text-center">
Cliquez pour télécharger<br />
<span className="text-xs opacity-75">(JPG, PNG, WebP - Max 50MB)</span>
</p>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Large Format Viewer - Takes 2 columns on desktop */}
<div className="lg:col-span-2">
<div className="bg-[var(--card)] rounded-3xl p-8 border border-[var(--accent)] border-opacity-20 shadow-lg overflow-hidden">
{selectedMediaItem ? (
<div className="relative">
{/* Large Format Display */}
<div className="relative w-full bg-[var(--background)] rounded-2xl overflow-hidden">
<div className="relative w-full" style={{ paddingBottom: "75%" }}>
{selectedMediaItem.type === "photo" ? (
<img
src={selectedMediaItem.src}
alt={selectedMediaItem.name}
className="absolute inset-0 w-full h-full object-cover"
/>
) : (
<video
src={selectedMediaItem.src}
controls
className="absolute inset-0 w-full h-full object-contain bg-black"
aria-label={selectedMediaItem.name}
/>
)}
</div>
<input
type="file"
multiple
accept="image/*"
onChange={(e) => handleMediaUpload(e, "photo")}
className="hidden"
aria-label="Télécharger des photos"
/>
</label>
</div>
</div>
{/* Video Upload */}
<div>
<h3 className="text-lg font-bold mb-4 text-[var(--foreground)] flex items-center gap-2">
<Play size={20} />
Télécharger des Vidéos
</h3>
<label className="flex flex-col items-center justify-center w-full h-40 border-2 border-dashed border-[var(--accent)] border-opacity-40 rounded-xl cursor-pointer hover:border-opacity-60 transition-all bg-[var(--background)] bg-opacity-50">
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<Upload size={28} className="text-[var(--primary-cta)] mb-2" />
<p className="text-sm font-medium text-[var(--foreground)] text-center">
Cliquez pour télécharger<br />
<span className="text-xs opacity-75">(MP4, WebM, OGG - Max 50MB)</span>
</p>
{/* Media Info */}
<div className="mt-6 p-6 bg-[var(--background)] rounded-xl border border-[var(--accent)] border-opacity-20">
<div className="flex items-start justify-between mb-4">
<div>
<div className="flex items-center gap-2 mb-2">
<span className="px-3 py-1 bg-[var(--primary-cta)] text-[var(--primary-cta-text)] text-xs font-semibold rounded-full">
{selectedMediaItem.type === "photo" ? "📷 Photo" : "🎥 Vidéo"}
</span>
</div>
<h3 className="text-xl font-bold text-[var(--foreground)]">
{selectedMediaItem.name}
</h3>
<p className="text-sm text-[var(--foreground)] opacity-60 mt-1">
{selectedMediaItem.type === "photo" ? "Photo de projet" : "Vidéo de projet"}
</p>
</div>
<button
onClick={() => handleDeleteMedia(selectedMediaItem.id)}
className="p-2 bg-red-500 hover:bg-red-600 text-white rounded-lg transition-colors"
aria-label="Supprimer ce média"
>
<Trash2 size={20} />
</button>
</div>
<input
type="file"
multiple
accept="video/*"
onChange={(e) => handleMediaUpload(e, "video")}
className="hidden"
aria-label="Télécharger des vidéos"
/>
</label>
</div>
</div>
</div>
) : (
<div className="flex items-center justify-center" style={{ minHeight: "600px" }}>
<div className="text-center">
<Camera size={64} className="mx-auto text-[var(--accent)] opacity-20 mb-4" />
<p className="text-[var(--foreground)] opacity-60 text-lg">
Sélectionnez une photo ou vidéo à partir de la liste ci-dessous
</p>
</div>
</div>
)}
</div>
</div>
{/* Sidebar - Media List and Upload Controls */}
<div className="lg:col-span-1 space-y-6">
{/* Upload Controls */}
<div className="bg-[var(--card)] rounded-3xl p-6 border border-[var(--accent)] border-opacity-20 shadow-lg space-y-4">
<h3 className="text-lg font-bold text-[var(--foreground)] flex items-center gap-2">
<Upload size={20} />
Ajouter Médias
</h3>
{/* Photo Upload */}
<label className="flex flex-col items-center justify-center w-full h-24 border-2 border-dashed border-[var(--accent)] border-opacity-40 rounded-xl cursor-pointer hover:border-opacity-60 transition-all bg-[var(--background)] bg-opacity-50">
<div className="flex flex-col items-center justify-center">
<Camera size={20} className="text-[var(--primary-cta)] mb-1" />
<p className="text-xs font-medium text-[var(--foreground)] text-center">
Ajouter Photos
</p>
</div>
<input
type="file"
multiple
accept="image/*"
onChange={(e) => handleMediaUpload(e, "photo")}
className="hidden"
aria-label="Télécharger des photos"
/>
</label>
{/* Video Upload */}
<label className="flex flex-col items-center justify-center w-full h-24 border-2 border-dashed border-[var(--accent)] border-opacity-40 rounded-xl cursor-pointer hover:border-opacity-60 transition-all bg-[var(--background)] bg-opacity-50">
<div className="flex flex-col items-center justify-center">
<Play size={20} className="text-[var(--primary-cta)] mb-1" />
<p className="text-xs font-medium text-[var(--foreground)] text-center">
Ajouter Vidéos
</p>
</div>
<input
type="file"
multiple
accept="video/*"
onChange={(e) => handleMediaUpload(e, "video")}
className="hidden"
aria-label="Télécharger des vidéos"
/>
</label>
{/* Feedback Messages */}
{uploadError && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-800 text-sm">{uploadError}</p>
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-red-800 text-xs">{uploadError}</p>
</div>
)}
{uploadSuccess && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<p className="text-green-800 text-sm font-semibold"> Fichier uploadé avec succès!</p>
<div className="bg-green-50 border border-green-200 rounded-lg p-3">
<p className="text-green-800 text-xs font-semibold"> Uploadé avec succès!</p>
</div>
)}
</div>
{/* Media Gallery Display */}
<div>
<div className="flex items-center justify-between mb-6">
<h3 className="text-2xl font-bold text-[var(--foreground)]">
Galerie des Projets ({uploadedMedia.length})
</h3>
{uploadedMedia.length > 0 && (
<span className="text-sm text-[var(--foreground)] opacity-60">
{uploadedMedia.filter(m => m.type === 'photo').length} photos, {uploadedMedia.filter(m => m.type === 'video').length} vidéos
</span>
)}
</div>
{uploadedMedia.length === 0 ? (
<div className="text-center py-24 bg-[var(--background)] bg-opacity-50 rounded-xl border border-[var(--accent)] border-opacity-20">
<Camera size={56} className="mx-auto text-[var(--accent)] opacity-30 mb-4" />
<p className="text-[var(--foreground)] opacity-60 text-lg">
Aucun média téléchargé pour le moment.<br />
<span className="text-sm">Commencez par ajouter des photos ou des vidéos de vos projets réussis.</span>
</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{uploadedMedia.map((media) => (
<div
key={media.id}
className="relative group rounded-xl overflow-hidden border border-[var(--accent)] border-opacity-20 shadow-md hover:shadow-lg transition-all duration-300"
>
{/* Media Preview */}
<div className="relative w-full h-56 bg-[var(--background)]">
{media.type === "photo" ? (
<img
src={media.src}
alt={media.name}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-black bg-opacity-20">
<video
src={media.src}
controls
className="w-full h-full object-contain"
aria-label={media.name}
/>
</div>
)}
{/* Overlay */}
<div className="absolute inset-0 bg-black opacity-0 group-hover:opacity-50 transition-opacity duration-300" />
{/* Type Badge */}
<div className="absolute top-3 left-3 px-3 py-1 bg-[var(--primary-cta)] text-[var(--primary-cta-text)] text-xs font-semibold rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-300">
{media.type === "photo" ? "📷 Photo" : "🎥 Vidéo"}
</div>
{/* Delete Button */}
<button
onClick={() => handleDeleteMedia(media.id)}
className="absolute top-3 right-3 p-2 bg-red-500 hover:bg-red-600 text-white rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300 hover:scale-110"
aria-label={`Supprimer ${media.name}`}
>
<Trash2 size={16} />
</button>
</div>
{/* File Info Footer */}
<div className="bg-[var(--background)] border-t border-[var(--accent)] border-opacity-20 p-4">
<p className="text-sm font-semibold text-[var(--foreground)] truncate mb-1">
{media.name}
</p>
<p className="text-xs text-[var(--foreground)] opacity-60">
{media.type === "photo" ? "Photo de projet" : "Vidéo de projet"}
</p>
</div>
</div>
))}
</div>
{/* Media List */}
<div className="bg-[var(--card)] rounded-3xl p-6 border border-[var(--accent)] border-opacity-20 shadow-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-bold text-[var(--foreground)]">
Galerie ({uploadedMedia.length})
</h3>
{uploadedMedia.length > 0 && (
<span className="text-xs text-[var(--foreground)] opacity-60 bg-[var(--background)] px-2 py-1 rounded">
{uploadedMedia.filter((m) => m.type === "photo").length}📷 {uploadedMedia.filter((m) => m.type === "video").length}🎥
</span>
)}
</div>
{/* Gallery Statistics */}
{uploadedMedia.length > 0 && (
<div className="grid grid-cols-3 gap-4 pt-8 border-t border-[var(--accent)] border-opacity-20">
<div className="text-center p-4 bg-[var(--background)] bg-opacity-50 rounded-lg">
<p className="text-2xl font-bold text-[var(--primary-cta)]">{uploadedMedia.length}</p>
<p className="text-xs text-[var(--foreground)] opacity-60 mt-1">Total Médias</p>
</div>
<div className="text-center p-4 bg-[var(--background)] bg-opacity-50 rounded-lg">
<p className="text-2xl font-bold text-[var(--primary-cta)]">{uploadedMedia.filter(m => m.type === 'photo').length}</p>
<p className="text-xs text-[var(--foreground)] opacity-60 mt-1">Photos</p>
</div>
<div className="text-center p-4 bg-[var(--background)] bg-opacity-50 rounded-lg">
<p className="text-2xl font-bold text-[var(--primary-cta)]">{uploadedMedia.filter(m => m.type === 'video').length}</p>
<p className="text-xs text-[var(--foreground)] opacity-60 mt-1">Vidéos</p>
</div>
{uploadedMedia.length === 0 ? (
<div className="text-center py-12">
<Camera size={32} className="mx-auto text-[var(--accent)] opacity-20 mb-2" />
<p className="text-sm text-[var(--foreground)] opacity-60">
Aucun média ajouté
</p>
</div>
) : (
<div className="space-y-2 max-h-96 overflow-y-auto">
{uploadedMedia.map((media) => (
<button
key={media.id}
onClick={() => setSelectedMedia(media.id)}
className={`w-full flex items-start gap-3 p-3 rounded-lg border transition-all ${
selectedMedia === media.id
? "bg-[var(--primary-cta)] bg-opacity-10 border-[var(--primary-cta)]"
: "border-[var(--accent)] border-opacity-20 hover:border-opacity-40"
}`}
>
{/* Thumbnail */}
<div className="w-12 h-12 rounded overflow-hidden flex-shrink-0 bg-[var(--background)]">
{media.type === "photo" ? (
<img
src={media.src}
alt={media.name}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-black bg-opacity-20">
<Play size={16} className="text-white" />
</div>
)}
</div>
{/* Info */}
<div className="min-w-0 flex-1 text-left">
<p className="text-xs font-semibold text-[var(--foreground)] truncate">
{media.name}
</p>
<p className="text-xs text-[var(--foreground)] opacity-60">
{media.type === "photo" ? "Photo" : "Vidéo"}
</p>
</div>
</button>
))}
</div>
)}
</div>
@@ -343,4 +366,4 @@ export default function PortfolioPage() {
</div>
</ThemeProvider>
);
}
}