Merge version_5 into main #8

Merged
bender merged 1 commits from version_5 into main 2026-03-12 12:06:18 +00:00

View File

@@ -6,7 +6,8 @@ import NavbarStyleCentered from "@/components/navbar/NavbarStyleCentered/NavbarS
import TestimonialCardOne from "@/components/sections/testimonial/TestimonialCardOne";
import ContactText from "@/components/sections/contact/ContactText";
import FooterBaseCard from "@/components/sections/footer/FooterBaseCard";
import { Camera } from "lucide-react";
import { Camera, Upload, Trash2 } from "lucide-react";
import { useState } from "react";
export default function PortfolioPage() {
const navItems = [
@@ -51,6 +52,58 @@ export default function PortfolioPage() {
},
];
const [uploadedMedia, setUploadedMedia] = useState<
Array<{ id: string; type: "photo" | "video"; src: string; name: string }>
>([]);
const [uploadError, setUploadError] = useState("");
const [uploadSuccess, setUploadSuccess] = useState(false);
const handleMediaUpload = (
e: React.ChangeEvent<HTMLInputElement>,
mediaType: "photo" | "video"
) => {
setUploadError("");
setUploadSuccess(false);
const files = e.target.files;
if (!files) return;
Array.from(files).forEach((file) => {
const validPhotoTypes = ["image/jpeg", "image/png", "image/webp"];
const validVideoTypes = ["video/mp4", "video/webm", "video/ogg"];
const isValidPhoto = mediaType === "photo" && validPhotoTypes.includes(file.type);
const isValidVideo = mediaType === "video" && validVideoTypes.includes(file.type);
if (!isValidPhoto && !isValidVideo) {
setUploadError(`Type de fichier non supporté: ${file.type}`);
return;
}
if (file.size > 50 * 1024 * 1024) {
setUploadError("Le fichier est trop volumineux (max 50MB)");
return;
}
const reader = new FileReader();
reader.onload = () => {
const newMedia = {
id: Date.now().toString(),
type: mediaType,
src: reader.result as string,
name: file.name,
};
setUploadedMedia((prev) => [...prev, newMedia]);
setUploadSuccess(true);
setTimeout(() => setUploadSuccess(false), 3000);
};
reader.readAsDataURL(file);
});
};
const handleDeleteMedia = (id: string) => {
setUploadedMedia((prev) => prev.filter((media) => media.id !== id));
};
return (
<ThemeProvider
defaultButtonVariant="directional-hover"
@@ -109,6 +162,154 @@ export default function PortfolioPage() {
/>
</div>
{/* Media Upload and Management Section */}
<div id="media-manager" data-section="media-manager">
<div className="w-full py-20 px-4 md:px-8">
<div className="max-w-6xl 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)]">
<Upload size={16} />
<span className="text-sm font-medium">Gestion des Médias</span>
</div>
<h2 className="text-3xl md:text-5xl font-bold mb-4 text-[var(--foreground)]">
Galerie des Projets Réalisés
</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.
</p>
</div>
<div className="grid md:grid-cols-3 gap-8">
{/* Upload Section */}
<div className="md:col-span-1 space-y-6">
{/* Photo Upload */}
<div className="bg-[var(--card)] rounded-2xl p-8 border border-[var(--accent)] border-opacity-20">
<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-32 border-2 border-dashed border-[var(--accent)] border-opacity-40 rounded-lg cursor-pointer hover:border-opacity-60 transition-all">
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<Upload size={24} className="text-[var(--primary-cta)] mb-2" />
<p className="text-sm text-[var(--foreground)] opacity-75 text-center">
Cliquez pour télécharger<br />
<span className="text-xs">(JPG, PNG, WebP)</span>
</p>
</div>
<input
type="file"
multiple
accept="image/*"
onChange={(e) => handleMediaUpload(e, "photo")}
className="hidden"
aria-label="Télécharger des photos"
/>
</label>
</div>
{/* Video Upload */}
<div className="bg-[var(--card)] rounded-2xl p-8 border border-[var(--accent)] border-opacity-20">
<h3 className="text-lg font-bold mb-4 text-[var(--foreground)] flex items-center gap-2">
<Camera size={20} />
Télécharger des Vidéos
</h3>
<label className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed border-[var(--accent)] border-opacity-40 rounded-lg cursor-pointer hover:border-opacity-60 transition-all">
<div className="flex flex-col items-center justify-center pt-5 pb-6">
<Upload size={24} className="text-[var(--primary-cta)] mb-2" />
<p className="text-sm text-[var(--foreground)] opacity-75 text-center">
Cliquez pour télécharger<br />
<span className="text-xs">(MP4, WebM, OGG)</span>
</p>
</div>
<input
type="file"
multiple
accept="video/*"
onChange={(e) => handleMediaUpload(e, "video")}
className="hidden"
aria-label="Télécharger des vidéos"
/>
</label>
</div>
{/* 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>
)}
{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>
)}
</div>
{/* Media Gallery */}
<div className="md:col-span-2">
<div className="bg-[var(--card)] rounded-2xl p-8 border border-[var(--accent)] border-opacity-20">
<h3 className="text-2xl font-bold mb-6 text-[var(--foreground)]">
Galerie ({uploadedMedia.length})
</h3>
{uploadedMedia.length === 0 ? (
<div className="text-center py-12">
<Camera size={48} className="mx-auto text-[var(--accent)] opacity-30 mb-4" />
<p className="text-[var(--foreground)] opacity-60">
Aucun média téléchargé. Commencez par ajouter des photos ou des vidéos de vos projets.
</p>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{uploadedMedia.map((media) => (
<div
key={media.id}
className="relative group rounded-lg overflow-hidden border border-[var(--accent)] border-opacity-20"
>
{media.type === "photo" ? (
<img
src={media.src}
alt={media.name}
className="w-full h-48 object-cover group-hover:scale-105 transition-transform duration-300"
/>
) : (
<video
src={media.src}
controls
className="w-full h-48 object-cover bg-black"
aria-label={media.name}
/>
)}
<div className="absolute inset-0 bg-black opacity-0 group-hover:opacity-40 transition-opacity duration-300" />
{/* Delete Button */}
<button
onClick={() => handleDeleteMedia(media.id)}
className="absolute top-2 right-2 p-2 bg-red-500 hover:bg-red-600 text-white rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-300"
aria-label={`Supprimer ${media.name}`}
>
<Trash2 size={16} />
</button>
{/* File Info */}
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black to-transparent p-4 text-white opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<p className="text-sm font-semibold truncate">{media.name}</p>
<p className="text-xs opacity-75">
{media.type === "photo" ? "Photo" : "Vidéo"}
</p>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
{/* CTA Section */}
<div id="cta" data-section="cta">
<ContactText