feat: implement event submission flow with image upload and modal interface

This commit is contained in:
2026-05-10 21:47:28 +02:00
parent 7db35361d9
commit 197594f84f
31 changed files with 1481 additions and 75 deletions

View File

@@ -28,7 +28,7 @@ export async function createEvent(data: {
thumbnailUrl: data.thumbnailUrl,
link: data.link || null,
tags: data.tags || [],
status: ContentStatus.PENDING, // Always pending for user submission
status: 'PENDING' as ContentStatus, // Always pending for user submission
slug: `${slug}-${Math.random().toString(36).substring(2, 7)}`
}
});

View File

@@ -11,6 +11,7 @@ export async function createNews(data: {
author: string;
imageUrl: string;
tags?: string[];
sources?: any;
}) {
try {
const slug = data.title
@@ -26,12 +27,13 @@ export async function createNews(data: {
author: data.author,
imageUrl: data.imageUrl,
tags: data.tags || [],
status: ContentStatus.PENDING, // Always pending for user submission
sources: data.sources || [],
status: 'PENDING' as ContentStatus, // Always pending for user submission
slug: `${slug}-${Math.random().toString(36).substring(2, 7)}`
}
});
revalidatePath("/blog");
revalidatePath("/actualites");
return { success: true, post };
} catch (error) {
console.error("Failed to create news:", error);

View File

@@ -13,7 +13,13 @@ export async function uploadImage(formData: FormData) {
// Convert to Base64
const base64 = buffer.toString('base64');
const mimeType = file.type || 'image/jpeg';
let mimeType = file.type || 'image/jpeg';
// Normalize JPEG mime types
if (mimeType === 'image/jpg' || mimeType === 'image/pjpeg') {
mimeType = 'image/jpeg';
}
const dataUrl = `data:${mimeType};base64,${base64}`;
// Return the data URL directly (it will be saved in the database)

View File

@@ -5,6 +5,7 @@ import { ArrowLeft, ArrowRight, User, Calendar, Share2 } from 'lucide-react';
import { sanitizeHTML } from '../../../lib/sanitize';
import { prisma } from '../../../lib/prisma';
import BlogShareButtons from '../../../components/BlogShareButtons';
import LightboxImage from '@/components/LightboxImage';
import { Metadata } from 'next';
interface Props {
@@ -117,11 +118,12 @@ export default async function BlogPostPage({ params }: Props) {
<div className="bg-white rounded-xl shadow-sm p-6 md:p-10 border border-gray-100 relative z-10">
{/* Image d'en-tête */}
<div className="w-full h-64 md:h-80 relative mb-8 rounded-lg overflow-hidden">
<img
src={post.imageUrl}
alt={post.title}
className="w-full h-full object-cover"
<div className="w-full h-64 md:h-80 relative mb-8 rounded-lg overflow-hidden shadow-md">
<LightboxImage
src={post.imageUrl}
alt={post.title}
position={post.coverPosition || "50% 50%"}
zoom={post.coverZoom || 1}
/>
</div>
@@ -160,7 +162,7 @@ export default async function BlogPostPage({ params }: Props) {
{post.excerpt}
</p>
{/* Corps du texte avec ID pour le contrôle CSS */}
{/* Corps du texte avec ID pour le contrôle CSS */}
<div
id="blog-content"
className="mt-6"
@@ -168,6 +170,32 @@ export default async function BlogPostPage({ params }: Props) {
/>
</div>
{/* Sources Section */}
{Array.isArray(post.sources) && post.sources.length > 0 && (
<div className="mt-12 p-6 bg-gray-50 rounded-xl border border-gray-100">
<h3 className="text-sm font-bold uppercase tracking-wider text-gray-400 mb-4 flex items-center gap-2">
<Share2 className="w-4 h-4 text-brand-500" />
Sources & Références
</h3>
<ul className="space-y-3">
{(post.sources as any[]).map((source, index) => (
<li key={index} className="flex items-start gap-3">
<span className="w-1.5 h-1.5 rounded-full bg-brand-500 mt-2 shrink-0" />
<a
href={source.url}
target="_blank"
rel="noopener noreferrer"
className="text-gray-700 hover:text-brand-600 transition-colors break-all"
>
<span className="font-semibold">{source.title || 'Source'}</span>
<span className="text-gray-400 ml-2 text-sm">{source.url}</span>
</a>
</li>
))}
</ul>
</div>
)}
{/* Partage / Footer */}
<div className="mt-12 pt-8 border-t border-gray-100 flex flex-col sm:flex-row justify-between items-center gap-4">
<p className="text-gray-500 font-medium">Vous avez aimé cet article ?</p>

View File

@@ -42,7 +42,16 @@ export default async function BlogPage() {
{posts.map(post => (
<Link key={post.id} href={`/actualites/${post.slug || post.id}`} className="group flex flex-col rounded-lg shadow-lg overflow-hidden bg-white hover:shadow-xl transition-shadow duration-300">
<div className="flex-shrink-0 relative overflow-hidden h-48">
<img className="h-full w-full object-cover group-hover:scale-105 transition-transform duration-500" src={post.imageUrl} alt={post.title} />
<img
className="h-full w-full object-cover group-hover:scale-[1.05] transition-transform duration-500"
src={post.imageUrl}
alt={post.title}
style={{
objectPosition: post.coverPosition || '50% 50%',
transform: `scale(${post.coverZoom || 1})`,
transformOrigin: post.coverPosition || '50% 50%'
}}
/>
<div className="absolute inset-0 bg-black/10 group-hover:bg-transparent transition-colors"></div>
</div>
<div className="flex-1 bg-white p-6 flex flex-col justify-between">

View File

@@ -5,6 +5,7 @@ import { ArrowLeft, Share2, Play, Calendar, User, Building2, MapPin, ArrowRight
import { sanitizeHTML } from '../../../lib/sanitize';
import { prisma } from '../../../lib/prisma';
import { InterviewType } from '@prisma/client';
import LightboxImage from '@/components/LightboxImage';
import { Metadata } from 'next';
@@ -161,8 +162,12 @@ export default async function AfroLifeDetailPage({ params }: Props) {
</div>
) : (
<div className="relative h-64 md:h-96 rounded-xl overflow-hidden shadow-lg mb-10">
<img src={item!.thumbnailUrl} alt={item!.title} className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
<LightboxImage
src={item!.thumbnailUrl}
alt={item!.title}
position={(item as any).coverPosition || "50% 50%"}
zoom={(item as any).coverZoom || 1}
/>
</div>
)}

View File

@@ -149,7 +149,12 @@ export default async function AfroLifePage({ searchParams }: Props) {
<img
src={item.thumbnailUrl}
alt={item.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
className="w-full h-full object-cover group-hover:scale-[1.05] transition-transform duration-700"
style={{
objectPosition: item.coverPosition || '50% 50%',
transform: `scale(${item.coverZoom || 1})`,
transformOrigin: item.coverPosition || '50% 50%'
}}
/>
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/10 transition-colors"></div>

View File

@@ -57,4 +57,34 @@ h4 {
.prose p {
margin-bottom: 1.25em;
text-align: left;
}
/* Styles pour les tableaux dans le contenu */
#blog-content table,
.prose table {
width: 100%;
border-collapse: collapse;
margin: 1.5em 0;
font-size: 0.9em;
border: 1px solid #e2e8f0;
}
#blog-content th,
#blog-content td,
.prose th,
.prose td {
border: 1px solid #e2e8f0;
padding: 0.75rem;
text-align: left;
}
#blog-content th,
.prose th {
background-color: #f8fafc;
font-weight: 600;
}
#blog-content tr:nth-child(even),
.prose tr:nth-child(even) {
background-color: #fcfcfc;
}