save avant prochaine modif
This commit is contained in:
113
app/afrolife/[id]/page.tsx
Normal file
113
app/afrolife/[id]/page.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { ArrowLeft, Share2, Play, Calendar, User, Building2 } from 'lucide-react';
|
||||
import { MOCK_INTERVIEWS } from '../../../lib/mockData';
|
||||
import { InterviewType } from '../../../types';
|
||||
|
||||
const InterviewDetailPage = () => {
|
||||
const { id } = useParams();
|
||||
const interview = MOCK_INTERVIEWS.find(i => i.id === id);
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [id]);
|
||||
|
||||
if (!interview) return null;
|
||||
|
||||
// Helper to get YouTube embed URL
|
||||
const getEmbedUrl = (url: string) => {
|
||||
const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
|
||||
const match = url.match(regExp);
|
||||
return (match && match[2].length === 11) ? `https://www.youtube.com/embed/${match[2]}` : null;
|
||||
};
|
||||
|
||||
const isVideo = interview.type === InterviewType.VIDEO;
|
||||
|
||||
return (
|
||||
<div className="bg-white min-h-screen">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<Link href="/afrolife" className="inline-flex items-center text-gray-500 hover:text-brand-600 mb-8 transition-colors">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" /> Retour à Afro Life
|
||||
</Link>
|
||||
|
||||
{/* Header Info */}
|
||||
<div className="text-center mb-10">
|
||||
<div className="inline-flex items-center justify-center px-3 py-1 rounded-full bg-gray-100 text-gray-600 text-xs font-bold uppercase tracking-wider mb-4">
|
||||
{isVideo ? 'Interview Vidéo' : 'Grand Entretien'}
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-5xl font-serif font-bold text-gray-900 mb-6 leading-tight">
|
||||
{interview.title}
|
||||
</h1>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-6 text-sm text-gray-500">
|
||||
<div className="flex items-center">
|
||||
<User className="w-4 h-4 mr-2 text-brand-500" />
|
||||
<span className="font-medium text-gray-900">{interview.guestName}</span>
|
||||
<span className="mx-1 text-gray-400">|</span>
|
||||
<span>{interview.role}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Building2 className="w-4 h-4 mr-2 text-brand-500" />
|
||||
{interview.companyName}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Calendar className="w-4 h-4 mr-2 text-brand-500" />
|
||||
{interview.date}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content Area */}
|
||||
{isVideo ? (
|
||||
<div className="bg-black rounded-xl overflow-hidden shadow-2xl aspect-w-16 aspect-h-9 mb-10">
|
||||
{interview.videoUrl ? (
|
||||
<iframe
|
||||
src={getEmbedUrl(interview.videoUrl) || ''}
|
||||
title={interview.title}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
className="w-full h-full"
|
||||
></iframe>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center bg-gray-900">
|
||||
<p className="text-white">Vidéo non disponible</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative h-64 md:h-96 rounded-xl overflow-hidden shadow-lg mb-10">
|
||||
<img src={interview.thumbnailUrl} alt={interview.title} className="w-full h-full object-cover" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description / Article Content */}
|
||||
<div className="prose prose-lg prose-orange max-w-none mx-auto text-gray-600">
|
||||
{!isVideo && interview.content ? (
|
||||
interview.content.split('\n').map((paragraph, idx) => (
|
||||
<p key={idx}>{paragraph}</p>
|
||||
))
|
||||
) : (
|
||||
<p className="text-xl font-serif italic text-gray-500 border-l-4 border-brand-500 pl-6 py-2">
|
||||
{interview.excerpt}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="mt-12 pt-8 border-t border-gray-100 flex justify-center">
|
||||
<button className="flex items-center px-6 py-3 rounded-full bg-brand-50 text-brand-700 font-medium hover:bg-brand-100 transition-colors">
|
||||
<Share2 className="w-5 h-5 mr-2" />
|
||||
Partager cette interview
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InterviewDetailPage;
|
||||
110
app/afrolife/page.tsx
Normal file
110
app/afrolife/page.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Play, FileText, Clock, Mic } from 'lucide-react';
|
||||
import { MOCK_INTERVIEWS } from '../../lib/mockData';
|
||||
import { InterviewType } from '../../types';
|
||||
|
||||
const AfroLifePage = () => {
|
||||
const [filter, setFilter] = useState<'ALL' | 'VIDEO' | 'ARTICLE'>('ALL');
|
||||
|
||||
const filteredInterviews = MOCK_INTERVIEWS.filter(interview => {
|
||||
if (filter === 'ALL') return true;
|
||||
return interview.type === filter;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-white min-h-screen">
|
||||
{/* Hero Header */}
|
||||
<div className="bg-dark-900 text-white py-20 relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-30 bg-[url('https://images.unsplash.com/photo-1523580494863-6f3031224c94?ixlib=rb-4.0.3&auto=format&fit=crop&w=1740&q=80')] bg-cover bg-center"></div>
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<span className="inline-block py-1 px-3 rounded-full bg-brand-600/20 border border-brand-500 text-brand-400 text-xs font-bold tracking-wider uppercase mb-4">
|
||||
Lifestyle & Inspiration
|
||||
</span>
|
||||
<h1 className="text-4xl md:text-6xl font-serif font-bold mb-6">Afro Life</h1>
|
||||
<p className="text-xl text-gray-300 max-w-2xl mx-auto">
|
||||
Plongez dans l'intimité des bâtisseurs. Entretiens exclusifs, parcours de vie et leçons de leadership.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
{/* Filters */}
|
||||
<div className="flex justify-center mb-12 space-x-4">
|
||||
<button
|
||||
onClick={() => setFilter('ALL')}
|
||||
className={`px-6 py-2 rounded-full text-sm font-medium transition-all ${filter === 'ALL' ? 'bg-brand-600 text-white shadow-md' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
|
||||
>
|
||||
Tout voir
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('VIDEO')}
|
||||
className={`flex items-center px-6 py-2 rounded-full text-sm font-medium transition-all ${filter === 'VIDEO' ? 'bg-brand-600 text-white shadow-md' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
|
||||
>
|
||||
<Play className="w-4 h-4 mr-2" /> Vidéos
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('ARTICLE')}
|
||||
className={`flex items-center px-6 py-2 rounded-full text-sm font-medium transition-all ${filter === 'ARTICLE' ? 'bg-brand-600 text-white shadow-md' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}`}
|
||||
>
|
||||
<FileText className="w-4 h-4 mr-2" /> Articles
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{filteredInterviews.map(interview => (
|
||||
<Link href={`/afrolife/${interview.id}`} key={interview.id} className="group block h-full">
|
||||
<div className="relative h-64 rounded-xl overflow-hidden shadow-sm group-hover:shadow-xl transition-all duration-300">
|
||||
<img
|
||||
src={interview.thumbnailUrl}
|
||||
alt={interview.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/10 transition-colors"></div>
|
||||
|
||||
{/* Type Badge */}
|
||||
<div className="absolute top-4 left-4">
|
||||
<span className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wide text-white backdrop-blur-md ${interview.type === InterviewType.VIDEO ? 'bg-red-600/90' : 'bg-blue-600/90'}`}>
|
||||
{interview.type === InterviewType.VIDEO ? <Play className="w-3 h-3 mr-1 fill-current" /> : <Mic className="w-3 h-3 mr-1" />}
|
||||
{interview.type === InterviewType.VIDEO ? 'Vidéo' : 'Interview'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Play Icon Overlay for Videos */}
|
||||
{interview.type === InterviewType.VIDEO && (
|
||||
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<div className="w-16 h-16 bg-white/30 backdrop-blur-sm rounded-full flex items-center justify-center border border-white/50">
|
||||
<Play className="w-8 h-8 text-white fill-current ml-1" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-6 px-2">
|
||||
<div className="flex items-center text-xs text-gray-500 mb-3 space-x-3">
|
||||
<span className="font-semibold text-brand-600 uppercase">{interview.guestName}</span>
|
||||
<span className="w-1 h-1 bg-gray-300 rounded-full"></span>
|
||||
<span>{interview.companyName}</span>
|
||||
<span className="w-1 h-1 bg-gray-300 rounded-full"></span>
|
||||
<span className="flex items-center"><Clock className="w-3 h-3 mr-1"/> {interview.duration}</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-serif font-bold text-gray-900 mb-2 group-hover:text-brand-600 transition-colors leading-tight">
|
||||
{interview.title}
|
||||
</h3>
|
||||
<p className="text-gray-600 text-sm line-clamp-2">
|
||||
{interview.excerpt}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AfroLifePage;
|
||||
42
app/api/blog/[id]/route.ts
Normal file
42
app/api/blog/[id]/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../lib/prisma'
|
||||
|
||||
type Params = { params: Promise<{ id: string }> }
|
||||
|
||||
// GET /api/blog/[id]
|
||||
export async function GET(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const post = await prisma.blogPost.findUnique({ where: { id } })
|
||||
if (!post) return NextResponse.json({ error: 'Non trouvé' }, { status: 404 })
|
||||
return NextResponse.json(post)
|
||||
} catch (error) {
|
||||
console.error('GET /api/blog/[id] error:', error)
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH /api/blog/[id]
|
||||
export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const post = await prisma.blogPost.update({ where: { id }, data: body })
|
||||
return NextResponse.json(post)
|
||||
} catch (error) {
|
||||
console.error('PATCH /api/blog/[id] error:', error)
|
||||
return NextResponse.json({ error: 'Erreur lors de la mise à jour' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/blog/[id]
|
||||
export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params
|
||||
await prisma.blogPost.delete({ where: { id } })
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('DELETE /api/blog/[id] error:', error)
|
||||
return NextResponse.json({ error: 'Erreur lors de la suppression' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
27
app/api/blog/route.ts
Normal file
27
app/api/blog/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../lib/prisma'
|
||||
|
||||
// GET /api/blog
|
||||
export async function GET() {
|
||||
try {
|
||||
const posts = await prisma.blogPost.findMany({
|
||||
orderBy: { date: 'desc' },
|
||||
})
|
||||
return NextResponse.json(posts)
|
||||
} catch (error) {
|
||||
console.error('GET /api/blog error:', error)
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/blog
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const post = await prisma.blogPost.create({ data: body })
|
||||
return NextResponse.json(post, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('POST /api/blog error:', error)
|
||||
return NextResponse.json({ error: 'Erreur lors de la création' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
49
app/api/businesses/[id]/route.ts
Normal file
49
app/api/businesses/[id]/route.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../lib/prisma'
|
||||
|
||||
type Params = { params: Promise<{ id: string }> }
|
||||
|
||||
// GET /api/businesses/[id]
|
||||
export async function GET(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const business = await prisma.business.findUnique({
|
||||
where: { id },
|
||||
include: { owner: true, offers: true },
|
||||
})
|
||||
if (!business) return NextResponse.json({ error: 'Non trouvé' }, { status: 404 })
|
||||
return NextResponse.json(business)
|
||||
} catch (error) {
|
||||
console.error('GET /api/businesses/[id] error:', error)
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH /api/businesses/[id]
|
||||
export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const business = await prisma.business.update({
|
||||
where: { id },
|
||||
data: body,
|
||||
include: { owner: true, offers: true },
|
||||
})
|
||||
return NextResponse.json(business)
|
||||
} catch (error) {
|
||||
console.error('PATCH /api/businesses/[id] error:', error)
|
||||
return NextResponse.json({ error: 'Erreur lors de la mise à jour' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/businesses/[id]
|
||||
export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params
|
||||
await prisma.business.delete({ where: { id } })
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('DELETE /api/businesses/[id] error:', error)
|
||||
return NextResponse.json({ error: 'Erreur lors de la suppression' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
49
app/api/businesses/route.ts
Normal file
49
app/api/businesses/route.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../lib/prisma'
|
||||
|
||||
// GET /api/businesses — List all businesses
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const category = searchParams.get('category')
|
||||
const q = searchParams.get('q')
|
||||
const featured = searchParams.get('featured')
|
||||
|
||||
const where: Record<string, unknown> = {}
|
||||
if (category && category !== 'All') where.category = category
|
||||
if (featured === 'true') where.isFeatured = true
|
||||
if (q) {
|
||||
where.OR = [
|
||||
{ name: { contains: q, mode: 'insensitive' } },
|
||||
{ description: { contains: q, mode: 'insensitive' } },
|
||||
{ tags: { hasSome: [q] } },
|
||||
]
|
||||
}
|
||||
|
||||
const businesses = await prisma.business.findMany({
|
||||
where,
|
||||
include: { owner: true, offers: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
return NextResponse.json(businesses)
|
||||
} catch (error) {
|
||||
console.error('GET /api/businesses error:', error)
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/businesses — Create a business
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const business = await prisma.business.create({
|
||||
data: body,
|
||||
include: { owner: true },
|
||||
})
|
||||
return NextResponse.json(business, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('POST /api/businesses error:', error)
|
||||
return NextResponse.json({ error: 'Erreur lors de la création' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
57
app/api/gemini/route.ts
Normal file
57
app/api/gemini/route.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { GoogleGenAI } from '@google/genai'
|
||||
|
||||
// POST /api/gemini — AI content generation
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const apiKey = process.env.GEMINI_API_KEY
|
||||
if (!apiKey || apiKey === 'PLACEHOLDER_API_KEY') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Clé API Gemini non configurée' },
|
||||
{ status: 503 }
|
||||
)
|
||||
}
|
||||
|
||||
const ai = new GoogleGenAI({ apiKey })
|
||||
const body = await request.json()
|
||||
const { action, name, category, keywords } = body
|
||||
|
||||
if (action === 'description') {
|
||||
const prompt = `
|
||||
Tu es un expert en copywriting marketing pour Afropreunariat.
|
||||
Rédige une description professionnelle, attrayante et optimisée SEO pour une entreprise.
|
||||
|
||||
Nom de l'entreprise : ${name}
|
||||
Secteur : ${category}
|
||||
Mots-clés/Services : ${keywords}
|
||||
|
||||
La description doit faire environ 80-100 mots, être en français, inspirer confiance et professionnalisme.
|
||||
Ne mets pas de guillemets au début ou à la fin.
|
||||
`
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash',
|
||||
contents: prompt,
|
||||
})
|
||||
return NextResponse.json({ text: response.text || '' })
|
||||
}
|
||||
|
||||
if (action === 'slogans') {
|
||||
const prompt = `Donne-moi 3 idées de slogans courts et percutants pour une entreprise dans le secteur : ${category}. Retourne uniquement une liste JSON de chaînes de caractères.`
|
||||
const response = await ai.models.generateContent({
|
||||
model: 'gemini-2.5-flash',
|
||||
contents: prompt,
|
||||
config: { responseMimeType: 'application/json' },
|
||||
})
|
||||
const json = JSON.parse(response.text || '[]')
|
||||
return NextResponse.json({ slogans: Array.isArray(json) ? json : [] })
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Action inconnue' }, { status: 400 })
|
||||
} catch (error) {
|
||||
console.error('POST /api/gemini error:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Erreur lors de la génération IA' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
42
app/api/interviews/[id]/route.ts
Normal file
42
app/api/interviews/[id]/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../lib/prisma'
|
||||
|
||||
type Params = { params: Promise<{ id: string }> }
|
||||
|
||||
// GET /api/interviews/[id]
|
||||
export async function GET(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const interview = await prisma.interview.findUnique({ where: { id } })
|
||||
if (!interview) return NextResponse.json({ error: 'Non trouvé' }, { status: 404 })
|
||||
return NextResponse.json(interview)
|
||||
} catch (error) {
|
||||
console.error('GET /api/interviews/[id] error:', error)
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH /api/interviews/[id]
|
||||
export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const interview = await prisma.interview.update({ where: { id }, data: body })
|
||||
return NextResponse.json(interview)
|
||||
} catch (error) {
|
||||
console.error('PATCH /api/interviews/[id] error:', error)
|
||||
return NextResponse.json({ error: 'Erreur lors de la mise à jour' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/interviews/[id]
|
||||
export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params
|
||||
await prisma.interview.delete({ where: { id } })
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('DELETE /api/interviews/[id] error:', error)
|
||||
return NextResponse.json({ error: 'Erreur lors de la suppression' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
27
app/api/interviews/route.ts
Normal file
27
app/api/interviews/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../lib/prisma'
|
||||
|
||||
// GET /api/interviews
|
||||
export async function GET() {
|
||||
try {
|
||||
const interviews = await prisma.interview.findMany({
|
||||
orderBy: { date: 'desc' },
|
||||
})
|
||||
return NextResponse.json(interviews)
|
||||
} catch (error) {
|
||||
console.error('GET /api/interviews error:', error)
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/interviews
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const interview = await prisma.interview.create({ data: body })
|
||||
return NextResponse.json(interview, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('POST /api/interviews error:', error)
|
||||
return NextResponse.json({ error: 'Erreur lors de la création' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
49
app/api/offers/[id]/route.ts
Normal file
49
app/api/offers/[id]/route.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../../lib/prisma'
|
||||
|
||||
type Params = { params: Promise<{ id: string }> }
|
||||
|
||||
// GET /api/offers/[id]
|
||||
export async function GET(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const offer = await prisma.offer.findUnique({
|
||||
where: { id },
|
||||
include: { business: true },
|
||||
})
|
||||
if (!offer) return NextResponse.json({ error: 'Non trouvé' }, { status: 404 })
|
||||
return NextResponse.json(offer)
|
||||
} catch (error) {
|
||||
console.error('GET /api/offers/[id] error:', error)
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// PATCH /api/offers/[id]
|
||||
export async function PATCH(request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params
|
||||
const body = await request.json()
|
||||
const offer = await prisma.offer.update({
|
||||
where: { id },
|
||||
data: body,
|
||||
include: { business: true },
|
||||
})
|
||||
return NextResponse.json(offer)
|
||||
} catch (error) {
|
||||
console.error('PATCH /api/offers/[id] error:', error)
|
||||
return NextResponse.json({ error: 'Erreur lors de la mise à jour' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/offers/[id]
|
||||
export async function DELETE(_request: NextRequest, { params }: Params) {
|
||||
try {
|
||||
const { id } = await params
|
||||
await prisma.offer.delete({ where: { id } })
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
console.error('DELETE /api/offers/[id] error:', error)
|
||||
return NextResponse.json({ error: 'Erreur lors de la suppression' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
38
app/api/offers/route.ts
Normal file
38
app/api/offers/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../lib/prisma'
|
||||
|
||||
// GET /api/offers — optionally filter by businessId
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const businessId = searchParams.get('businessId')
|
||||
|
||||
const where: Record<string, unknown> = {}
|
||||
if (businessId) where.businessId = businessId
|
||||
|
||||
const offers = await prisma.offer.findMany({
|
||||
where,
|
||||
include: { business: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
return NextResponse.json(offers)
|
||||
} catch (error) {
|
||||
console.error('GET /api/offers error:', error)
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/offers
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const offer = await prisma.offer.create({
|
||||
data: body,
|
||||
include: { business: true },
|
||||
})
|
||||
return NextResponse.json(offer, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('POST /api/offers error:', error)
|
||||
return NextResponse.json({ error: 'Erreur lors de la création' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
28
app/api/users/route.ts
Normal file
28
app/api/users/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import prisma from '../../../lib/prisma'
|
||||
|
||||
// GET /api/users
|
||||
export async function GET() {
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
include: { businesses: true },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
return NextResponse.json(users)
|
||||
} catch (error) {
|
||||
console.error('GET /api/users error:', error)
|
||||
return NextResponse.json({ error: 'Erreur serveur' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/users
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json()
|
||||
const user = await prisma.user.create({ data: body })
|
||||
return NextResponse.json(user, { status: 201 })
|
||||
} catch (error) {
|
||||
console.error('POST /api/users error:', error)
|
||||
return NextResponse.json({ error: 'Erreur lors de la création' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
100
app/blog/[id]/page.tsx
Normal file
100
app/blog/[id]/page.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { ArrowLeft, User, Calendar, Share2 } from 'lucide-react';
|
||||
import { MOCK_BLOG_POSTS } from '../../../lib/mockData';
|
||||
|
||||
const BlogPostPage = () => {
|
||||
const { id } = useParams();
|
||||
const post = MOCK_BLOG_POSTS.find(p => p.id === id);
|
||||
|
||||
// Scroll to top when loading a new post
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [id]);
|
||||
|
||||
if (!post) {
|
||||
return (
|
||||
<div className="min-h-[60vh] flex flex-col items-center justify-center">
|
||||
<h2 className="text-3xl font-serif font-bold text-gray-900 mb-4">Article introuvable</h2>
|
||||
<p className="text-gray-600 mb-8">L'article que vous recherchez n'existe pas ou a été supprimé.</p>
|
||||
<Link href="/blog" className="text-brand-600 hover:text-brand-700 font-medium flex items-center">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" /> Retour au blog
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="bg-white min-h-screen">
|
||||
{/* Hero Image */}
|
||||
<div className="w-full h-64 md:h-96 relative">
|
||||
<img
|
||||
src={post.imageUrl}
|
||||
alt={post.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
|
||||
<div className="absolute bottom-0 left-0 w-full p-4 sm:p-8 max-w-4xl mx-auto">
|
||||
<Link href="/blog" className="inline-flex items-center text-white/80 hover:text-white mb-4 text-sm font-medium transition-colors">
|
||||
<ArrowLeft className="w-4 h-4 mr-1" /> Retour aux articles
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12 -mt-20 relative z-10">
|
||||
<div className="bg-white rounded-xl shadow-xl p-6 md:p-10 border border-gray-100">
|
||||
{/* Header */}
|
||||
<header className="mb-8 border-b border-gray-100 pb-8">
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-500 mb-4">
|
||||
<span className="flex items-center">
|
||||
<User className="w-4 h-4 mr-1 text-brand-500" />
|
||||
{post.author}
|
||||
</span>
|
||||
<span className="flex items-center">
|
||||
<Calendar className="w-4 h-4 mr-1 text-brand-500" />
|
||||
{post.date}
|
||||
</span>
|
||||
<span className="px-2 py-1 bg-brand-50 text-brand-700 rounded text-xs font-semibold uppercase tracking-wide">
|
||||
Conseils
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-4xl font-serif font-bold text-gray-900 leading-tight">
|
||||
{post.title}
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
{/* Content */}
|
||||
<div className="prose prose-lg prose-orange max-w-none text-gray-600">
|
||||
<p className="lead text-xl text-gray-500 font-serif italic mb-6">
|
||||
{post.excerpt}
|
||||
</p>
|
||||
{/* Rendering paragraphs manually for the mock data */}
|
||||
{post.content.split('\n').map((paragraph, index) => (
|
||||
paragraph.trim() !== '' && (
|
||||
<p key={index} className="mb-4 leading-relaxed">
|
||||
{paragraph}
|
||||
</p>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Footer / Share */}
|
||||
<div className="mt-12 pt-8 border-t border-gray-100 flex justify-between items-center">
|
||||
<p className="text-sm text-gray-500 font-medium">Vous avez aimé cet article ?</p>
|
||||
<div className="flex space-x-2">
|
||||
<button className="p-2 rounded-full bg-gray-100 text-gray-600 hover:bg-brand-100 hover:text-brand-600 transition-colors">
|
||||
<Share2 className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogPostPage;
|
||||
58
app/blog/page.tsx
Normal file
58
app/blog/page.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { MOCK_BLOG_POSTS } from '../../lib/mockData';
|
||||
|
||||
const BlogPage = () => (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-3xl font-bold font-serif text-gray-900 sm:text-4xl">Le Blog de l'Entrepreneur</h1>
|
||||
<p className="mt-3 max-w-2xl mx-auto text-xl text-gray-500 sm:mt-4">
|
||||
Conseils, actualités et success stories de l'écosystème africain.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-8 lg:grid-cols-3">
|
||||
{MOCK_BLOG_POSTS.map(post => (
|
||||
<Link key={post.id} href={`/blog/${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} />
|
||||
<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">
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-brand-600">Conseils</p>
|
||||
<div className="block mt-2">
|
||||
<p className="text-xl font-semibold text-gray-900 group-hover:text-brand-700 transition-colors">{post.title}</p>
|
||||
<p className="mt-3 text-base text-gray-500 line-clamp-3">{post.excerpt}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="sr-only">{post.author}</span>
|
||||
<div className="h-10 w-10 rounded-full bg-gray-200 flex items-center justify-center text-gray-600 font-bold border border-gray-300">
|
||||
{post.author.charAt(0)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium text-gray-900">{post.author}</p>
|
||||
<div className="flex space-x-1 text-sm text-gray-500">
|
||||
<time>{post.date}</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-brand-600 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<ArrowRight className="w-5 h-5" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default BlogPage;
|
||||
124
app/dashboard/page.tsx
Normal file
124
app/dashboard/page.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { LayoutDashboard, Edit3, ShoppingBag, CreditCard, LogOut, Eye } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { User } from '../../types';
|
||||
import { MOCK_BUSINESSES } from '../../lib/mockData';
|
||||
import DashboardOverview from '../../components/dashboard/DashboardOverview';
|
||||
import DashboardProfile from '../../components/dashboard/DashboardProfile';
|
||||
import DashboardOffers from '../../components/dashboard/DashboardOffers';
|
||||
import PricingSection from '../../components/PricingSection';
|
||||
import { useUser } from '../../components/UserProvider';
|
||||
|
||||
const DashboardPage = () => {
|
||||
const { user, logout } = useUser();
|
||||
const router = useRouter();
|
||||
const [currentView, setCurrentView] = useState<'overview' | 'profile' | 'offers' | 'subscription'>('overview');
|
||||
const [business, setBusiness] = useState(MOCK_BUSINESSES[0]); // In real app, fetch by user.id
|
||||
|
||||
if (!user) {
|
||||
if (typeof window !== 'undefined') router.push('/login');
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 flex">
|
||||
{/* 1. Sidebar */}
|
||||
<div className="hidden md:flex md:flex-col md:w-64 md:fixed md:inset-y-0 bg-white border-r border-gray-200">
|
||||
<div className="flex items-center h-16 flex-shrink-0 px-4 border-b border-gray-200">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-brand-600 rounded-lg flex items-center justify-center text-white font-bold font-serif">A</div>
|
||||
<span className="font-serif font-bold text-lg text-gray-900">Afropreunariat</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col overflow-y-auto pt-5 pb-4">
|
||||
<div className="px-4 mb-6">
|
||||
<div className="flex items-center">
|
||||
<div className="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center text-gray-600 font-bold shrink-0">
|
||||
{user.name.charAt(0)}
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium text-gray-700 truncate">{user.name}</p>
|
||||
<div className="flex items-center mt-1">
|
||||
<span className="h-2 w-2 rounded-full bg-green-400 mr-1"></span>
|
||||
<span className="text-xs text-gray-500">Actif</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav className="mt-2 flex-1 px-2 space-y-1">
|
||||
<button onClick={() => setCurrentView('overview')} className={`${currentView === 'overview' ? 'bg-brand-50 text-brand-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'} group flex items-center px-2 py-2 text-sm font-medium rounded-md w-full`}>
|
||||
<LayoutDashboard className={`${currentView === 'overview' ? 'text-brand-500' : 'text-gray-400 group-hover:text-gray-500'} mr-3 flex-shrink-0 h-5 w-5`} />
|
||||
Tableau de Bord
|
||||
</button>
|
||||
<button onClick={() => setCurrentView('profile')} className={`${currentView === 'profile' ? 'bg-brand-50 text-brand-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'} group flex items-center px-2 py-2 text-sm font-medium rounded-md w-full`}>
|
||||
<Edit3 className={`${currentView === 'profile' ? 'text-brand-500' : 'text-gray-400 group-hover:text-gray-500'} mr-3 flex-shrink-0 h-5 w-5`} />
|
||||
Éditer mon profil
|
||||
</button>
|
||||
<button onClick={() => setCurrentView('offers')} className={`${currentView === 'offers' ? 'bg-brand-50 text-brand-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'} group flex items-center px-2 py-2 text-sm font-medium rounded-md w-full`}>
|
||||
<ShoppingBag className={`${currentView === 'offers' ? 'text-brand-500' : 'text-gray-400 group-hover:text-gray-500'} mr-3 flex-shrink-0 h-5 w-5`} />
|
||||
Mes Offres
|
||||
</button>
|
||||
<button onClick={() => setCurrentView('subscription')} className={`${currentView === 'subscription' ? 'bg-brand-50 text-brand-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'} group flex items-center px-2 py-2 text-sm font-medium rounded-md w-full`}>
|
||||
<CreditCard className={`${currentView === 'subscription' ? 'text-brand-500' : 'text-gray-400 group-hover:text-gray-500'} mr-3 flex-shrink-0 h-5 w-5`} />
|
||||
Abonnement
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex-shrink-0 flex border-t border-gray-200 p-4">
|
||||
<button onClick={() => { logout(); router.push('/'); }} className="flex-shrink-0 w-full group block text-gray-600 hover:text-red-600 transition-colors">
|
||||
<div className="flex items-center">
|
||||
<LogOut className="inline-block h-5 w-5 mr-2" />
|
||||
<span className="text-sm font-medium">Déconnexion</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2. Main Content */}
|
||||
<div className="flex-1 flex flex-col md:pl-64 overflow-hidden">
|
||||
<header className="bg-white shadow-sm z-10 flex justify-between items-center px-6 py-4 sticky top-0">
|
||||
<h1 className="text-2xl font-bold text-gray-900 sm:truncate">
|
||||
{currentView === 'overview' && 'Vue d\'ensemble'}
|
||||
{currentView === 'profile' && 'Mon Profil'}
|
||||
{currentView === 'offers' && 'Gestion des Offres'}
|
||||
{currentView === 'subscription' && 'Mon Abonnement'}
|
||||
</h1>
|
||||
<div className="flex items-center space-x-4">
|
||||
<a href={`/directory`} target="_blank" rel="noopener noreferrer" className="hidden sm:inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none">
|
||||
<Eye className="-ml-1 mr-2 h-4 w-4 text-gray-500" />
|
||||
Voir ma fiche
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 overflow-y-auto p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{currentView === 'overview' && <DashboardOverview business={business} />}
|
||||
{currentView === 'profile' && <DashboardProfile business={business} setBusiness={setBusiness} />}
|
||||
{currentView === 'offers' && <DashboardOffers />}
|
||||
{currentView === 'subscription' && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white shadow rounded-lg p-6 flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-gray-900">Abonnement Actuel : <span className="text-brand-600 font-bold">Starter (Gratuit)</span></h3>
|
||||
<p className="text-sm text-gray-500">Passez au niveau supérieur pour débloquer plus de fonctionnalités.</p>
|
||||
</div>
|
||||
<span className="inline-flex items-center px-3 py-0.5 rounded-full text-sm font-medium bg-green-100 text-green-800">
|
||||
Actif
|
||||
</span>
|
||||
</div>
|
||||
<PricingSection />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPage;
|
||||
356
app/directory/[id]/page.tsx
Normal file
356
app/directory/[id]/page.tsx
Normal file
@@ -0,0 +1,356 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { ArrowLeft, MapPin, Globe, Mail, Phone, CheckCircle, Star, Facebook, Linkedin, Instagram, Play, Share2, Send, Award, Quote } from 'lucide-react';
|
||||
import { MOCK_BUSINESSES, MOCK_OFFERS } from '../../../lib/mockData';
|
||||
import { OfferType } from '../../../types';
|
||||
|
||||
const BusinessDetailPage = () => {
|
||||
const { id } = useParams();
|
||||
const business = MOCK_BUSINESSES.find(b => b.id === id);
|
||||
|
||||
const [contactForm, setContactForm] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
message: ''
|
||||
});
|
||||
const [formStatus, setFormStatus] = useState<'idle' | 'sending' | 'success'>('idle');
|
||||
|
||||
const businessOffers = useMemo(() => {
|
||||
return MOCK_OFFERS.filter(o => o.businessId === id && o.active);
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [id]);
|
||||
|
||||
if (!business) {
|
||||
return (
|
||||
<div className="min-h-[60vh] flex flex-col items-center justify-center">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-4">Entreprise introuvable</h2>
|
||||
<Link href="/directory" className="text-brand-600 hover:text-brand-700 font-medium flex items-center">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" /> Retour à l'annuaire
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to get YouTube embed URL
|
||||
const getEmbedUrl = (url: string) => {
|
||||
const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
|
||||
const match = url.match(regExp);
|
||||
return (match && match[2].length === 11) ? `https://www.youtube.com/embed/${match[2]}` : null;
|
||||
};
|
||||
|
||||
const videoEmbedUrl = business.videoUrl ? getEmbedUrl(business.videoUrl) : null;
|
||||
|
||||
const handleContactSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setFormStatus('sending');
|
||||
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
setFormStatus('success');
|
||||
setContactForm({ name: '', email: '', message: '' });
|
||||
|
||||
// Reset status after 3 seconds
|
||||
setTimeout(() => setFormStatus('idle'), 3000);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-50 min-h-screen pb-12">
|
||||
{/* Header Banner */}
|
||||
<div className="h-48 md:h-64 bg-gradient-to-r from-gray-900 to-gray-800 relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-20 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')]"></div>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-full flex flex-col justify-between py-6">
|
||||
<Link href="/directory" className="inline-flex items-center text-white/80 hover:text-white transition-colors w-fit">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" /> Retour
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 -mt-20 relative z-10">
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-100 overflow-hidden">
|
||||
<div className="p-6 md:p-8">
|
||||
{/* Profile Header */}
|
||||
<div className="flex flex-col md:flex-row gap-6 items-start">
|
||||
<div className="w-32 h-32 md:w-40 md:h-40 rounded-xl bg-white p-1 shadow-md -mt-16 md:-mt-24 border border-gray-100 flex-shrink-0 relative">
|
||||
<img src={business.logoUrl} alt={business.name} className="w-full h-full object-cover rounded-lg bg-gray-50" />
|
||||
{business.isFeatured && (
|
||||
<div className="absolute -top-3 -right-3 bg-yellow-400 text-yellow-900 rounded-full p-2 shadow-md" title="Entrepreneur du Mois">
|
||||
<Award className="w-6 h-6" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 w-full">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-3xl font-serif font-bold text-gray-900 flex items-center gap-2">
|
||||
{business.name}
|
||||
</h1>
|
||||
{business.verified && <span title="Entreprise Vérifiée"><CheckCircle className="w-6 h-6 text-blue-500" /></span>}
|
||||
</div>
|
||||
<div className="text-brand-600 font-medium mt-1 uppercase tracking-wide text-sm">{business.category}</div>
|
||||
{business.isFeatured && (
|
||||
<div className="inline-flex items-center px-2 py-1 rounded bg-yellow-100 text-yellow-800 text-xs font-bold mt-2">
|
||||
<Award className="w-3 h-3 mr-1" /> ENTREPRENEUR DU MOIS
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button className="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
<Share2 className="w-4 h-4 mr-2" /> Partager
|
||||
</button>
|
||||
<a href="#contact-form" className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-brand-600 hover:bg-brand-700">
|
||||
Contacter
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 mt-6 text-sm text-gray-500 border-t border-gray-100 pt-4">
|
||||
<div className="flex items-center">
|
||||
<MapPin className="w-4 h-4 mr-1 text-gray-400" />
|
||||
{business.location}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Star className="w-4 h-4 mr-1 text-yellow-400 fill-current" />
|
||||
<span className="font-bold text-gray-700 mr-1">{business.rating}</span>
|
||||
({business.viewCount} vues)
|
||||
</div>
|
||||
{business.tags.map(tag => (
|
||||
<span key={tag} className="bg-gray-100 text-gray-600 px-2 py-1 rounded text-xs">#{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 mt-8">
|
||||
{/* Left Column: Main Content */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
|
||||
{/* Presentation */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 md:p-8">
|
||||
<h2 className="text-xl font-bold font-serif text-gray-900 mb-4">À propos</h2>
|
||||
<p className="text-gray-600 leading-relaxed whitespace-pre-line">
|
||||
{business.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Founder Section (Specifically for Featured or if data exists) */}
|
||||
{(business.founderName || business.founderImageUrl) && (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 md:p-8 overflow-hidden relative">
|
||||
<div className="absolute top-0 right-0 p-4 opacity-10">
|
||||
<Quote className="w-24 h-24 text-brand-500" />
|
||||
</div>
|
||||
<h2 className="text-xl font-bold font-serif text-gray-900 mb-6 relative z-10">Le Fondateur</h2>
|
||||
<div className="flex flex-col sm:flex-row gap-6 items-center sm:items-start relative z-10">
|
||||
{business.founderImageUrl && (
|
||||
<img
|
||||
src={business.founderImageUrl}
|
||||
alt={business.founderName}
|
||||
className="w-32 h-32 rounded-full object-cover border-4 border-gray-50 shadow-md"
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900">{business.founderName}</h3>
|
||||
<p className="text-brand-600 text-sm font-medium mb-3">CEO & Fondateur</p>
|
||||
<p className="text-gray-600 italic">
|
||||
"Notre mission est d'apporter des solutions concrètes et adaptées aux réalités locales, tout en visant l'excellence internationale."
|
||||
</p>
|
||||
{business.keyMetric && (
|
||||
<div className="mt-4 inline-block bg-brand-50 text-brand-700 px-3 py-1 rounded-full text-xs font-bold">
|
||||
🚀 {business.keyMetric}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Video */}
|
||||
{videoEmbedUrl && (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 md:p-8">
|
||||
<h2 className="text-xl font-bold font-serif text-gray-900 mb-4 flex items-center">
|
||||
<Play className="w-5 h-5 mr-2 text-brand-600" /> Présentation Vidéo
|
||||
</h2>
|
||||
<div className="aspect-w-16 aspect-h-9 bg-gray-100 rounded-lg overflow-hidden">
|
||||
<iframe
|
||||
src={videoEmbedUrl}
|
||||
title={`Présentation de ${business.name}`}
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
className="w-full h-64 md:h-96 rounded-lg"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Offers */}
|
||||
{businessOffers.length > 0 && (
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 md:p-8">
|
||||
<h2 className="text-xl font-bold font-serif text-gray-900 mb-6">Produits & Services</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
{businessOffers.map(offer => (
|
||||
<div key={offer.id} className="border border-gray-200 rounded-lg overflow-hidden hover:shadow-md transition-shadow">
|
||||
<div className="h-40 bg-gray-100 relative">
|
||||
<img src={offer.imageUrl} alt={offer.title} className="w-full h-full object-cover" />
|
||||
<span className={`absolute top-2 right-2 px-2 py-1 text-xs font-bold rounded uppercase ${offer.type === OfferType.PRODUCT ? 'bg-blue-100 text-blue-800' : 'bg-purple-100 text-purple-800'}`}>
|
||||
{offer.type === OfferType.PRODUCT ? 'Produit' : 'Service'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h3 className="font-bold text-gray-900 line-clamp-1">{offer.title}</h3>
|
||||
<p className="text-brand-600 font-bold mt-1">
|
||||
{new Intl.NumberFormat('fr-FR').format(offer.price)} {offer.currency}
|
||||
</p>
|
||||
<button className="mt-3 w-full block text-center bg-gray-50 text-gray-700 py-2 rounded text-sm font-medium hover:bg-gray-100 transition-colors">
|
||||
Commander
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right Column: Sidebar */}
|
||||
<div className="space-y-8">
|
||||
{/* Contact Card */}
|
||||
<div id="contact-form" className="bg-white rounded-xl shadow-sm border border-gray-100 p-6 sticky top-24">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">Contact</h3>
|
||||
|
||||
{/* Direct Info */}
|
||||
<div className="space-y-4 mb-6">
|
||||
<a href={`mailto:${business.contactEmail}`} className="flex items-center text-gray-600 hover:text-brand-600 transition-colors">
|
||||
<div className="w-8 h-8 rounded-full bg-brand-50 flex items-center justify-center mr-3 text-brand-600 flex-shrink-0">
|
||||
<Mail className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="text-sm truncate">{business.contactEmail}</span>
|
||||
</a>
|
||||
{business.contactPhone && (
|
||||
<a href={`tel:${business.contactPhone}`} className="flex items-center text-gray-600 hover:text-brand-600 transition-colors">
|
||||
<div className="w-8 h-8 rounded-full bg-brand-50 flex items-center justify-center mr-3 text-brand-600 flex-shrink-0">
|
||||
<Phone className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="text-sm">{business.contactPhone}</span>
|
||||
</a>
|
||||
)}
|
||||
{business.socialLinks?.website && (
|
||||
<a href={business.socialLinks.website} target="_blank" rel="noopener noreferrer" className="flex items-center text-gray-600 hover:text-brand-600 transition-colors">
|
||||
<div className="w-8 h-8 rounded-full bg-brand-50 flex items-center justify-center mr-3 text-brand-600 flex-shrink-0">
|
||||
<Globe className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="text-sm truncate">Site Web</span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Contact Form */}
|
||||
<div className="border-t border-gray-100 pt-6">
|
||||
<h4 className="text-sm font-semibold text-gray-900 mb-3">Envoyer un message</h4>
|
||||
{formStatus === 'success' ? (
|
||||
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded-md text-sm">
|
||||
Message envoyé avec succès !
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleContactSubmit} className="space-y-3">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Votre nom"
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"
|
||||
value={contactForm.name}
|
||||
onChange={(e) => setContactForm({ ...contactForm, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Votre email"
|
||||
required
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-brand-500"
|
||||
value={contactForm.email}
|
||||
onChange={(e) => setContactForm({ ...contactForm, email: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<textarea
|
||||
placeholder="Votre message..."
|
||||
required
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-brand-500 resize-none"
|
||||
value={contactForm.message}
|
||||
onChange={(e) => setContactForm({ ...contactForm, message: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={formStatus === 'sending'}
|
||||
className="w-full flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-gray-900 hover:bg-gray-800 focus:outline-none transition-colors disabled:opacity-70"
|
||||
>
|
||||
{formStatus === 'sending' ? 'Envoi...' : <><Send className="w-3 h-3 mr-2" /> Envoyer</>}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-gray-100">
|
||||
<div className="flex justify-center space-x-6">
|
||||
{business.socialLinks?.facebook && (
|
||||
<a href={business.socialLinks.facebook} target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-[#1877F2] transition-colors">
|
||||
<Facebook className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
{business.socialLinks?.linkedin && (
|
||||
<a href={business.socialLinks.linkedin} target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-[#0A66C2] transition-colors">
|
||||
<Linkedin className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
{business.socialLinks?.instagram && (
|
||||
<a href={business.socialLinks.instagram} target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-[#E4405F] transition-colors">
|
||||
<Instagram className="w-5 h-5" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Similar Businesses (Mock) */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-4">Dans la même catégorie</h3>
|
||||
<div className="space-y-4">
|
||||
{MOCK_BUSINESSES
|
||||
.filter(b => b.category === business.category && b.id !== business.id)
|
||||
.slice(0, 3)
|
||||
.map(similar => (
|
||||
<Link key={similar.id} href={`/directory/${similar.id}`} className="flex items-center group">
|
||||
<div className="w-12 h-12 rounded bg-gray-100 overflow-hidden flex-shrink-0">
|
||||
<img src={similar.logoUrl} alt={similar.name} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className="ml-3 overflow-hidden">
|
||||
<p className="text-sm font-medium text-gray-900 truncate group-hover:text-brand-600 transition-colors">{similar.name}</p>
|
||||
<p className="text-xs text-gray-500 truncate">{similar.location}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BusinessDetailPage;
|
||||
128
app/directory/page.tsx
Normal file
128
app/directory/page.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import React, { useState, useEffect, useMemo, Suspense } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Search } from 'lucide-react';
|
||||
import { MOCK_BUSINESSES } from '../../lib/mockData';
|
||||
import { CATEGORIES } from '../../types';
|
||||
import BusinessCard from '../../components/BusinessCard';
|
||||
import DirectoryHero from '../../components/DirectoryHero';
|
||||
|
||||
const DirectoryPageContent = () => {
|
||||
const [filterCategory, setFilterCategory] = useState('All');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
const q = searchParams.get('q');
|
||||
if (q) setSearchQuery(q);
|
||||
}, [searchParams]);
|
||||
|
||||
const featuredBusiness = useMemo(() => {
|
||||
return MOCK_BUSINESSES.find(b => b.isFeatured);
|
||||
}, []);
|
||||
|
||||
const filteredBusinesses = useMemo(() => {
|
||||
return MOCK_BUSINESSES.filter(b => {
|
||||
const matchesCategory = filterCategory === 'All' || b.category === filterCategory;
|
||||
const matchesSearch = b.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
b.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
b.tags.some(t => t.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
return matchesCategory && matchesSearch;
|
||||
});
|
||||
}, [filterCategory, searchQuery]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* New Hero Section */}
|
||||
{featuredBusiness && <DirectoryHero featuredBusiness={featuredBusiness} />}
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="flex flex-col md:flex-row gap-8">
|
||||
{/* Filters Sidebar */}
|
||||
<div className="w-full md:w-64 flex-shrink-0">
|
||||
<div className="bg-white p-6 rounded-xl shadow-sm border border-gray-100 sticky top-24">
|
||||
<h3 className="font-bold text-lg mb-4 flex items-center"><Search className="w-4 h-4 mr-2" /> Filtres</h3>
|
||||
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Recherche</label>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full border-gray-300 rounded-md shadow-sm focus:ring-brand-500 focus:border-brand-500 sm:text-sm p-2 border"
|
||||
placeholder="Nom, mot-clé..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">Catégorie</label>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="cat-all"
|
||||
name="category"
|
||||
type="radio"
|
||||
checked={filterCategory === 'All'}
|
||||
onChange={() => setFilterCategory('All')}
|
||||
className="focus:ring-brand-500 h-4 w-4 text-brand-600 border-gray-300"
|
||||
/>
|
||||
<label htmlFor="cat-all" className="ml-3 text-sm text-gray-600">Toutes</label>
|
||||
</div>
|
||||
{CATEGORIES.map(cat => (
|
||||
<div key={cat} className="flex items-center">
|
||||
<input
|
||||
id={`cat-${cat}`}
|
||||
name="category"
|
||||
type="radio"
|
||||
checked={filterCategory === cat}
|
||||
onChange={() => setFilterCategory(cat)}
|
||||
className="focus:ring-brand-500 h-4 w-4 text-brand-600 border-gray-300"
|
||||
/>
|
||||
<label htmlFor={`cat-${cat}`} className="ml-3 text-sm text-gray-600 truncate" title={cat}>{cat}</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results Grid */}
|
||||
<div className="flex-1">
|
||||
<div className="mb-4 flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold font-serif text-gray-900">Annuaire des entreprises</h1>
|
||||
<span className="text-sm text-gray-500">{filteredBusinesses.length} résultats</span>
|
||||
</div>
|
||||
|
||||
{filteredBusinesses.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredBusinesses.map(biz => (
|
||||
<BusinessCard key={biz.id} business={biz} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20 bg-white rounded-xl border border-gray-100 border-dashed">
|
||||
<div className="mx-auto h-12 w-12 text-gray-400">
|
||||
<Search className="h-12 w-12" />
|
||||
</div>
|
||||
<h3 className="mt-2 text-sm font-medium text-gray-900">Aucun résultat</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Essayez d'ajuster vos filtres de recherche.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DirectoryPage = () => {
|
||||
return (
|
||||
<Suspense fallback={<div className="min-h-screen flex items-center justify-center">Chargement...</div>}>
|
||||
<DirectoryPageContent />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default DirectoryPage;
|
||||
25
app/globals.css
Normal file
25
app/globals.css
Normal file
@@ -0,0 +1,25 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-serif: 'Playfair Display', serif;
|
||||
|
||||
--color-brand-50: #fff7ed;
|
||||
--color-brand-100: #ffedd5;
|
||||
--color-brand-200: #fed7aa;
|
||||
--color-brand-500: #f97316;
|
||||
--color-brand-600: #ea580c;
|
||||
--color-brand-700: #c2410c;
|
||||
--color-brand-900: #7c2d12;
|
||||
|
||||
--color-dark-800: #1e1e1e;
|
||||
--color-dark-900: #121212;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--color-gray-50);
|
||||
color: var(--color-gray-900);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
32
app/layout.tsx
Normal file
32
app/layout.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import Script from 'next/script';
|
||||
import Navbar from '../components/Navbar';
|
||||
import Footer from '../components/Footer';
|
||||
import { UserProvider } from '../components/UserProvider';
|
||||
import { Metadata } from 'next';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Afropreunariat - L\'Annuaire 2.0',
|
||||
description: 'Annuaire Afropreunariat',
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<link rel="icon" type="image/svg+xml" href="https://cdn-icons-png.flaticon.com/512/1022/1022235.png" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Playfair+Display:wght@600;700&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body className="bg-gray-50 text-gray-900 antialiased min-h-screen flex flex-col font-sans">
|
||||
<UserProvider>
|
||||
<Navbar />
|
||||
<main className="flex-grow">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</UserProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
68
app/login/page.tsx
Normal file
68
app/login/page.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useUser } from '../../components/UserProvider';
|
||||
|
||||
const LoginPage = () => {
|
||||
const { login } = useUser();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// Mock login
|
||||
login();
|
||||
router.push('/dashboard');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-[80vh] flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-md w-full space-y-8">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto h-12 w-12 bg-brand-600 rounded-lg flex items-center justify-center text-white font-bold text-2xl">A</div>
|
||||
<h2 className="mt-6 text-3xl font-extrabold text-gray-900 font-serif">Connexion à votre espace</h2>
|
||||
<p className="mt-2 text-sm text-gray-600">
|
||||
Ou <a href="#" className="font-medium text-brand-600 hover:text-brand-500">créez votre compte entreprise pour 1€</a>
|
||||
</p>
|
||||
</div>
|
||||
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
|
||||
<div className="rounded-md shadow-sm -space-y-px">
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-brand-500 focus:border-brand-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Adresse email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-brand-500 focus:border-brand-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Mot de passe"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-brand-600 hover:bg-brand-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-brand-500">
|
||||
Se connecter
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-sm text-center text-gray-500">
|
||||
(Compte démo : n'importe quel email/mdp fonctionne)
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
98
app/page.tsx
Normal file
98
app/page.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Search, MapPin, Briefcase, TrendingUp } from 'lucide-react';
|
||||
import { CATEGORIES } from '../types';
|
||||
import { MOCK_BUSINESSES } from '../lib/mockData';
|
||||
import BusinessCard from '../components/BusinessCard';
|
||||
|
||||
const HomePage = () => {
|
||||
const router = useRouter();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
router.push(`/directory?q=${searchTerm}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Hero Section */}
|
||||
<div className="relative bg-dark-900 overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-40">
|
||||
{/* UPDATED IMAGE: Group of diverse/African professionals */}
|
||||
<img src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?ixlib=rb-4.0.3&auto=format&fit=crop&w=1740&q=80" className="w-full h-full object-cover" alt="African entrepreneurs team" />
|
||||
</div>
|
||||
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24 lg:py-32">
|
||||
<h1 className="text-4xl md:text-6xl font-serif font-bold text-white mb-6 tracking-tight">
|
||||
Boostez votre visibilité dans <br />
|
||||
<span className="text-brand-500">l'écosystème africain</span>
|
||||
</h1>
|
||||
<p className="text-xl text-gray-300 mb-8 max-w-2xl">
|
||||
L'annuaire 2.0 qui connecte les talents, les entrepreneurs et les entreprises de la diaspora et du continent.
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSearch} className="max-w-3xl bg-white p-2 rounded-lg shadow-xl flex flex-col md:flex-row gap-2">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-3 text-gray-400 w-5 h-5" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Que recherchez-vous ? (ex: Développeur, Traiteur...)"
|
||||
className="w-full pl-10 pr-4 py-3 rounded-md focus:outline-none text-gray-900"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:w-1/3 relative border-t md:border-t-0 md:border-l border-gray-200">
|
||||
<MapPin className="absolute left-3 top-3 text-gray-400 w-5 h-5" />
|
||||
<input type="text" placeholder="Localisation (ex: Abidjan)" className="w-full pl-10 pr-4 py-3 rounded-md focus:outline-none text-gray-900" />
|
||||
</div>
|
||||
<button type="submit" className="bg-brand-600 text-white px-8 py-3 rounded-md font-semibold hover:bg-brand-700 transition-colors">
|
||||
Rechercher
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Featured Categories */}
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<h2 className="text-3xl font-bold text-gray-900 mb-8 font-serif">Secteurs en vedette</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{CATEGORIES.slice(0, 4).map((cat, idx) => (
|
||||
<div key={idx} className="group cursor-pointer bg-white p-6 rounded-xl border border-gray-100 shadow-sm hover:shadow-md hover:border-brand-200 transition-all text-center">
|
||||
<div className="w-12 h-12 bg-brand-50 text-brand-600 rounded-full flex items-center justify-center mx-auto mb-4 group-hover:bg-brand-600 group-hover:text-white transition-colors">
|
||||
<Briefcase className="w-6 h-6" />
|
||||
</div>
|
||||
<h3 className="font-semibold text-gray-900">{cat}</h3>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Featured Businesses */}
|
||||
<div className="bg-gray-50 py-16">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-end mb-8">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 font-serif">Entreprises à la une</h2>
|
||||
<p className="text-gray-500 mt-2">Découvrez les pépites de notre communauté.</p>
|
||||
</div>
|
||||
<Link href="/directory" className="text-brand-600 font-medium hover:text-brand-700 flex items-center">
|
||||
Voir tout l'annuaire <TrendingUp className="w-4 h-4 ml-1" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{MOCK_BUSINESSES.map(biz => (
|
||||
<BusinessCard key={biz.id} business={biz} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
38
app/subscription/page.tsx
Normal file
38
app/subscription/page.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
|
||||
import React from 'react';
|
||||
import PricingSection from '../../components/PricingSection';
|
||||
|
||||
const SubscriptionPage = () => {
|
||||
return (
|
||||
<div className="bg-white min-h-screen">
|
||||
<div className="bg-dark-900 pt-20 pb-10 text-center">
|
||||
<h1 className="text-3xl font-bold text-white font-serif">Nos Offres</h1>
|
||||
<p className="text-gray-400 mt-2">Rejoignez la plus grande communauté d'entrepreneurs.</p>
|
||||
</div>
|
||||
<PricingSection />
|
||||
|
||||
{/* FAQ Section */}
|
||||
<div className="max-w-4xl mx-auto px-4 py-16">
|
||||
<h2 className="text-2xl font-bold text-gray-900 text-center mb-10">Questions Fréquentes</h2>
|
||||
<div className="space-y-6">
|
||||
<div className="bg-gray-50 p-6 rounded-lg">
|
||||
<h3 className="font-bold text-gray-900 mb-2">Puis-je changer d'abonnement plus tard ?</h3>
|
||||
<p className="text-gray-600">Oui, vous pouvez passer d'un plan à l'autre à tout moment depuis votre tableau de bord. La différence sera calculée au prorata.</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 p-6 rounded-lg">
|
||||
<h3 className="font-bold text-gray-900 mb-2">Quels moyens de paiement acceptez-vous ?</h3>
|
||||
<p className="text-gray-600">Nous acceptons les cartes bancaires (Visa, Mastercard) ainsi que tous les principaux Mobile Money (Orange Money, MTN, Wave, Moov) disponibles en Afrique de l'Ouest et Centrale.</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 p-6 rounded-lg">
|
||||
<h3 className="font-bold text-gray-900 mb-2">Comment fonctionne le badge "Vérifié" ?</h3>
|
||||
<p className="text-gray-600">Une fois abonné au plan Booster ou Empire, notre équipe effectuera une vérification rapide de votre entreprise (identité légale) sous 48h pour activer votre badge.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubscriptionPage;
|
||||
Reference in New Issue
Block a user