This commit is contained in:
2026-04-11 23:30:14 +02:00
parent 10ca8c3e8c
commit b73939d381
1574 changed files with 621 additions and 57974 deletions

View File

@@ -7,6 +7,7 @@ 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 { Business, OfferType } from '../../../types';
import { toast } from 'react-hot-toast';
const BusinessDetailPage = () => {
const { id } = useParams();
@@ -78,7 +79,7 @@ const BusinessDetailPage = () => {
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">
<Link href="/annuaire" 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>
@@ -114,7 +115,7 @@ const BusinessDetailPage = () => {
<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">
<Link href="/annuaire" 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>
@@ -380,7 +381,7 @@ const BusinessDetailPage = () => {
.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">
<Link key={similar.id} href={`/annuaire/${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>

View File

@@ -6,9 +6,9 @@ import { useSearchParams } from 'next/navigation';
import { Search, Loader2 } from 'lucide-react';
import { CATEGORIES, Business } from '../../types';
import BusinessCard from '../../components/BusinessCard';
import DirectoryHero from '../../components/DirectoryHero';
import AnnuaireHero from '../../components/AnnuaireHero';
const DirectoryPageContent = () => {
const AnnuairePageContent = () => {
const [businesses, setBusinesses] = useState<Business[]>([]);
const [featuredBusiness, setFeaturedBusiness] = useState<Business | null>(null);
const [loading, setLoading] = useState(true);
@@ -67,7 +67,7 @@ const DirectoryPageContent = () => {
return (
<div>
{/* Tête d'affiche (Headliner) - Always visible regardless of filters */}
{featuredBusiness && <DirectoryHero featuredBusiness={featuredBusiness} />}
{featuredBusiness && <AnnuaireHero 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">
@@ -153,12 +153,12 @@ const DirectoryPageContent = () => {
);
};
const DirectoryPage = () => {
const AnnuairePage = () => {
return (
<Suspense fallback={<div className="min-h-screen flex items-center justify-center">Chargement...</div>}>
<DirectoryPageContent />
<AnnuairePageContent />
</Suspense>
);
};
export default DirectoryPage;
export default AnnuairePage;

View File

@@ -7,8 +7,13 @@ type Params = { params: Promise<{ id: string }> }
export async function GET(_request: NextRequest, { params }: Params) {
try {
const { id } = await params
const business = await prisma.business.findUnique({
where: { id },
const business = await prisma.business.findFirst({
where: {
OR: [
{ id: id },
{ slug: id }
]
},
include: { owner: true, offers: true },
})
if (!business) return NextResponse.json({ error: 'Non trouvé' }, { status: 404 })

View File

@@ -76,6 +76,7 @@ export async function POST(request: NextRequest) {
// We must exclude 'id', 'offers', 'owner', 'createdAt', 'updatedAt' from the data object
const cleanData: any = {
name: data.name || "Nouvelle Entreprise",
slug: data.slug || null,
category: data.category || "Autre",
location: data.location || "Ma Ville",
description: data.description || "",
@@ -93,6 +94,21 @@ export async function POST(request: NextRequest) {
founderImageUrl: data.founderImageUrl || null,
keyMetric: data.keyMetric || null,
}
// 2. Slug Uniqueness Check
if (cleanData.slug) {
const slugConflict = await prisma.business.findFirst({
where: {
slug: cleanData.slug,
NOT: { ownerId: ownerId } // Important: allow the owner to keep their own slug
}
});
if (slugConflict) {
return NextResponse.json({
error: `Le lien personnalisé "${cleanData.slug}" est déjà utilisé par une autre entreprise. Veuillez en choisir un autre.`
}, { status: 400 });
}
}
let business;
const existing = await prisma.business.findFirst({

View File

@@ -148,7 +148,7 @@ const DashboardPage = () => {
{currentView === 'subscription' && 'Mon Abonnement'}
</h1>
<div className="flex items-center space-x-4">
<a href={displayBusiness.id === 'new' ? '/directory' : `/directory/${displayBusiness.id}`} 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">
<a href={displayBusiness.id === 'new' ? '/annuaire' : `/annuaire/${displayBusiness.slug || displayBusiness.id}`} 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>

View File

@@ -5,6 +5,7 @@ import Footer from '../components/Footer';
import { UserProvider } from '../components/UserProvider';
import AnalyticsTracker from '../components/AnalyticsTracker';
import { Metadata } from 'next';
import { Toaster } from 'react-hot-toast';
import './globals.css';
export const metadata: Metadata = {
@@ -21,6 +22,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</head>
<body className="bg-gray-50 text-gray-900 antialiased min-h-screen flex flex-col font-sans">
<UserProvider>
<Toaster position="top-right" />
<AnalyticsTracker />
<Navbar />
<main className="flex-grow">

View File

@@ -32,7 +32,7 @@ const HomePage = () => {
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
router.push(`/directory?q=${searchTerm}`);
router.push(`/annuaire?q=${searchTerm}`);
};
return (
@@ -96,7 +96,7 @@ const HomePage = () => {
<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">
<Link href="/annuaire" 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>