feat: implement home page, business directory, and administrative management features with updated schema and API routes
Some checks failed
Build and Push App / build (push) Failing after 51s

This commit is contained in:
2026-04-23 14:40:50 +02:00
parent 88e4c13b9f
commit e6310f30de
23 changed files with 1124 additions and 222 deletions

View File

@@ -32,15 +32,16 @@ const EventTimeline = ({ events }: EventTimelineProps) => {
{sortedEvents.map((event, index) => {
const eventDate = new Date(event.date);
const isEven = index % 2 === 0;
const isPast = eventDate < new Date();
return (
<div key={event.id} className={`relative flex flex-col md:flex-row items-center ${isEven ? 'md:flex-row-reverse' : ''}`}>
<div key={event.id} className={`relative flex flex-col md:flex-row items-center ${isEven ? 'md:flex-row-reverse' : ''} ${isPast ? 'opacity-60' : ''}`}>
{/* Marker */}
<div className="absolute left-4 md:left-1/2 w-4 h-4 bg-white border-4 border-brand-600 rounded-full transform md:-translate-x-1/2 z-10 shadow-[0_0_15px_rgba(234,88,12,0.5)]"></div>
<div className={`absolute left-4 md:left-1/2 w-4 h-4 bg-white border-4 rounded-full transform md:-translate-x-1/2 z-10 ${isPast ? 'border-gray-400 shadow-none' : 'border-brand-600 shadow-[0_0_15px_rgba(234,88,12,0.5)]'}`}></div>
{/* Content Card */}
<div className={`w-full md:w-[45%] ml-12 md:ml-0 group`}>
<div className="relative bg-white/80 backdrop-blur-xl p-6 rounded-3xl border border-white shadow-[0_8px_30px_rgb(0,0,0,0.04)] hover:shadow-[0_8px_30px_rgb(0,0,0,0.12)] transition-all duration-500 group-hover:-translate-y-1">
<div className={`relative bg-white/80 backdrop-blur-xl p-6 rounded-3xl border border-white shadow-[0_8px_30px_rgb(0,0,0,0.04)] hover:shadow-[0_8px_30px_rgb(0,0,0,0.12)] transition-all duration-500 group-hover:-translate-y-1 ${isPast ? 'grayscale-[0.8]' : ''}`}>
{/* Image with overlay gradient */}
<div className="relative h-48 mb-6 rounded-2xl overflow-hidden">
<img
@@ -50,7 +51,7 @@ const EventTimeline = ({ events }: EventTimelineProps) => {
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
<div className="absolute bottom-4 left-4 right-4 flex justify-between items-end">
<span className="bg-brand-600/90 backdrop-blur-md text-white px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider flex items-center">
<span className={`${isPast ? 'bg-gray-600/90' : 'bg-brand-600/90'} backdrop-blur-md text-white px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider flex items-center`}>
<Calendar className="w-3 h-3 mr-1" />
{eventDate.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' })}
</span>
@@ -59,11 +60,12 @@ const EventTimeline = ({ events }: EventTimelineProps) => {
{/* Event Details */}
<div className="space-y-3">
<div className="flex items-center text-brand-600 text-xs font-bold uppercase tracking-widest">
<div className={`flex items-center text-xs font-bold uppercase tracking-widest ${isPast ? 'text-gray-500' : 'text-brand-600'}`}>
<Clock className="w-3 h-3 mr-1" />
{eventDate.toLocaleDateString('fr-FR', { year: 'numeric' })}
{isPast && <span className="ml-2 px-2 py-0.5 bg-gray-100 text-gray-500 rounded text-[10px]">PASSÉ</span>}
</div>
<h3 className="text-2xl font-serif font-bold text-gray-900 group-hover:text-brand-600 transition-colors leading-tight">
<h3 className={`text-2xl font-serif font-bold transition-colors leading-tight ${isPast ? 'text-gray-500' : 'text-gray-900 group-hover:text-brand-600'}`}>
{event.title}
</h3>
<div className="flex items-center text-sm text-gray-500">
@@ -78,7 +80,7 @@ const EventTimeline = ({ events }: EventTimelineProps) => {
<div className="pt-4 flex items-center justify-between">
<Link
href={`/afrolife/${event.slug || event.id}`}
className="inline-flex items-center text-brand-600 text-sm font-bold hover:underline group/link"
className={`inline-flex items-center text-sm font-bold hover:underline group/link ${isPast ? 'text-gray-400' : 'text-brand-600'}`}
>
Voir les détails
<ArrowRight className="w-4 h-4 ml-1 transform group-hover/link:translate-x-1 transition-transform" />
@@ -89,14 +91,16 @@ const EventTimeline = ({ events }: EventTimelineProps) => {
</div>
{/* Hover glow effect */}
<div className="absolute -inset-px bg-gradient-to-br from-brand-500/10 to-amber-500/10 rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
{!isPast && (
<div className="absolute -inset-px bg-gradient-to-br from-brand-500/10 to-amber-500/10 rounded-3xl opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"></div>
)}
</div>
</div>
{/* Date Column (Desktop Only) */}
<div className="hidden md:flex w-[45%] justify-center items-center px-8">
<div className={`text-center ${isEven ? 'text-right' : 'text-left'}`}>
<span className="block text-4xl font-serif font-bold text-gray-200 group-hover:text-brand-500/20 transition-colors">
<span className={`block text-4xl font-serif font-bold transition-colors ${isPast ? 'text-gray-100' : 'text-gray-200 group-hover:text-brand-500/20'}`}>
{eventDate.toLocaleDateString('fr-FR', { month: 'long' })}
</span>
<span className="block text-xl font-bold text-gray-400">

View File

@@ -9,7 +9,9 @@ interface Plan {
tier: string;
name: string;
priceXOF: string;
yearlyPriceXOF?: string;
priceEUR: string;
yearlyPriceEUR?: string;
description: string;
features: string[];
recommended: boolean;
@@ -130,12 +132,24 @@ const PricingSection = () => {
<p className="mt-4 text-sm text-gray-500">{plan.description}</p>
<div className="mt-8 flex items-baseline">
<span className="text-4xl font-extrabold text-gray-900 tracking-tight">
{plan.priceXOF === 'Gratuit' ? 'Gratuit' : (billingCycle === 'yearly' && plan.tier !== 'STARTER' ? 'Sur Devis' : plan.priceXOF)}
{plan.priceXOF === 'Gratuit'
? 'Gratuit'
: (billingCycle === 'yearly'
? (plan.yearlyPriceXOF || plan.priceXOF)
: plan.priceXOF
)
}
</span>
{plan.priceXOF !== 'Gratuit' && <span className="ml-1 text-xl font-medium text-gray-500">/mois</span>}
{plan.priceXOF !== 'Gratuit' && (
<span className="ml-1 text-xl font-medium text-gray-500">
{billingCycle === 'yearly' ? '/an' : '/mois'}
</span>
)}
</div>
{plan.priceXOF !== 'Gratuit' && (
<p className="text-xs text-gray-400 mt-1">soit env. {plan.priceEUR} /mois</p>
<p className="text-xs text-gray-400 mt-1">
soit env. {billingCycle === 'yearly' ? (plan.yearlyPriceEUR || plan.priceEUR) : plan.priceEUR} {billingCycle === 'yearly' ? '/an' : '/mois'}
</p>
)}
<ul className="mt-8 space-y-4">
@@ -193,8 +207,10 @@ const PricingSection = () => {
{paymentStep === 'method' && (
<>
<div className="bg-brand-50 p-4 rounded-md mb-6 border border-brand-100">
<p className="text-sm text-brand-800 font-medium">Vous avez choisi le plan <span className="font-bold">{selectedPlan.name}</span></p>
<p className="text-2xl font-bold text-brand-900 mt-1">{selectedPlan.priceXOF}</p>
<p className="text-sm text-brand-800 font-medium">Vous avez choisi le plan <span className="font-bold">{selectedPlan.name}</span> ({billingCycle === 'yearly' ? 'Annuel' : 'Mensuel'})</p>
<p className="text-2xl font-bold text-brand-900 mt-1">
{billingCycle === 'yearly' ? (selectedPlan.yearlyPriceXOF || selectedPlan.priceXOF) : selectedPlan.priceXOF}
</p>
</div>
<p className="text-sm font-medium text-gray-700 mb-3">Moyen de paiement</p>
@@ -292,7 +308,7 @@ const PricingSection = () => {
onClick={handlePayment}
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-brand-600 text-base font-medium text-white hover:bg-brand-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed sm:ml-3 sm:w-auto sm:text-sm"
>
Payer {selectedPlan.priceXOF}
Payer {billingCycle === 'yearly' ? (selectedPlan.yearlyPriceXOF || selectedPlan.priceXOF) : selectedPlan.priceXOF}
</button>
<button
type="button"

View File

@@ -3,7 +3,7 @@
import React, { useState } from 'react';
import { Image as ImageIcon, Sparkles, Youtube, X, Globe, Facebook, Linkedin, Instagram, CheckCircle, AlertCircle } from 'lucide-react';
import { Business, CATEGORIES, Country } from '../../types';
import { Business, Country } from '../../types';
import { generateBusinessDescription } from '../../lib/geminiService';
import { toast } from 'react-hot-toast';
import { useUser } from '../UserProvider';
@@ -18,7 +18,9 @@ const getYouTubeId = (url: string) => {
const normalizeBusinessData = (b: Business): Business => ({
...b,
name: b.name || 'Nouvelle Entreprise',
category: b.category || CATEGORIES[0],
category: b.category || 'Autre',
categoryId: b.categoryId || '',
suggestedCategory: b.suggestedCategory || '',
description: b.description || '',
location: b.location || '',
countryId: b.countryId || '',
@@ -46,23 +48,29 @@ const DashboardProfile = ({ business, setBusiness }: { business: Business, setBu
const { login } = useUser();
const [formData, setFormData] = useState<Business>(normalizeBusinessData(business));
const [countries, setCountries] = useState<Country[]>([]);
const [categories, setCategories] = useState<any[]>([]);
const [videoInput, setVideoInput] = useState(business.videoUrl || '');
const [videoPreviewId, setVideoPreviewId] = useState<string | null>(getYouTubeId(business.videoUrl || ''));
const [isGenerating, setIsGenerating] = useState(false);
const [isMounted, setIsMounted] = useState(false);
// Fetch countries
// Fetch countries and categories
React.useEffect(() => {
const fetchCountries = async () => {
const fetchData = async () => {
try {
const res = await fetch('/api/countries');
const data = await res.json();
if (!data.error) setCountries(data);
const [cRes, catRes] = await Promise.all([
fetch('/api/countries'),
fetch('/api/categories')
]);
const cData = await cRes.json();
const catData = await catRes.json();
if (!cData.error) setCountries(cData);
if (!catData.error) setCategories(catData);
} catch (error) {
console.error('Error fetching countries:', error);
console.error('Error fetching metadata:', error);
}
};
fetchCountries();
fetchData();
}, []);
// Sync formData when business prop changes (initial fetch)
@@ -333,10 +341,37 @@ const DashboardProfile = ({ business, setBusiness }: { business: Business, setBu
</div>
<div className="sm:col-span-3">
<label className="block text-sm font-medium text-gray-700">Secteur d'activité</label>
<select name="category" value={formData.category} onChange={handleInputChange} className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:ring-brand-500 focus:border-brand-500 sm:text-sm">
{CATEGORIES.map(c => <option key={c} value={c}>{c}</option>)}
<select
name="categoryId"
value={formData.categoryId || ''}
onChange={(e) => {
const catId = e.target.value;
const catName = categories.find(c => c.id === catId)?.name || 'Autre';
setFormData({ ...formData, categoryId: catId, category: catName });
}}
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:ring-brand-500 focus:border-brand-500 sm:text-sm"
>
<option value="">Sélectionner un secteur</option>
{categories.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
<option value="Autre">Autre (proposer...)</option>
</select>
</div>
{formData.category === 'Autre' && (
<div className="sm:col-span-3 animate-in fade-in slide-in-from-top-2 duration-300">
<label className="block text-sm font-medium text-brand-600 flex items-center gap-1">
<Sparkles className="w-3 h-3" /> Nom de la nouvelle catégorie
</label>
<input
type="text"
name="suggestedCategory"
value={formData.suggestedCategory || ''}
onChange={handleInputChange}
placeholder="ex: Intelligence Artificielle"
className="mt-1 block w-full border border-brand-300 bg-brand-50 rounded-md shadow-sm py-2 px-3 focus:ring-brand-500 focus:border-brand-500 sm:text-sm"
/>
<p className="mt-1 text-[10px] text-brand-500">Elle sera vérifiée par nos administrateurs avant d'être ajoutée officiellement.</p>
</div>
)}
<div className="sm:col-span-6">
<div className="flex justify-between mb-1">
<label className="block text-sm font-medium text-gray-700">Description</label>