From 16d66c045680ed67bc75c2e1078cc8c3212e96b2 Mon Sep 17 00:00:00 2001 From: streaper2 Date: Sun, 3 May 2026 23:15:14 +0200 Subject: [PATCH] migration correction et maj --- admin/prisma/migrations/0_init/migration.sql | Bin 0 -> 35984 bytes .../20260427214502_init/migration.sql | 459 +++++++ .../migration.sql | 8 + .../migration.sql | 11 + admin/prisma/migrations/migration_lock.toml | 3 + admin/prisma/schema.prisma | 2 + admin/src/app/actions/business.ts | 20 + admin/src/app/users/page.tsx | 2 + admin/src/components/BusinessSeoModal.tsx | 120 ++ admin/src/components/EntrepreneurActions.tsx | 31 +- app/HomeClient.tsx | 160 +++ app/annuaire/[id]/BusinessDetailClient.tsx | 469 +++++++ app/annuaire/[id]/page.tsx | 1217 +---------------- app/page.tsx | 270 +--- components/UserProvider.tsx | 6 +- lib/mockData.ts | 24 + prisma/migrations/0_init/migration.sql | Bin 0 -> 35984 bytes prisma/schema.prisma | 2 + types.ts | 33 + 19 files changed, 1482 insertions(+), 1355 deletions(-) create mode 100644 admin/prisma/migrations/0_init/migration.sql create mode 100644 admin/prisma/migrations/20260427214502_init/migration.sql create mode 100644 admin/prisma/migrations/20260503171918_add_published_at/migration.sql create mode 100644 admin/prisma/migrations/20260503172958_add_content_status/migration.sql create mode 100644 admin/prisma/migrations/migration_lock.toml create mode 100644 admin/src/components/BusinessSeoModal.tsx create mode 100644 app/HomeClient.tsx create mode 100644 app/annuaire/[id]/BusinessDetailClient.tsx create mode 100644 prisma/migrations/0_init/migration.sql diff --git a/admin/prisma/migrations/0_init/migration.sql b/admin/prisma/migrations/0_init/migration.sql new file mode 100644 index 0000000000000000000000000000000000000000..d14cf34644381de5b3a78be4e7446ccd813188a7 GIT binary patch literal 35984 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 literal 0 HcmV?d00001 diff --git a/admin/prisma/migrations/20260427214502_init/migration.sql b/admin/prisma/migrations/20260427214502_init/migration.sql new file mode 100644 index 0000000..220c22c --- /dev/null +++ b/admin/prisma/migrations/20260427214502_init/migration.sql @@ -0,0 +1,459 @@ +-- CreateEnum +CREATE TYPE "RatingStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED'); + +-- CreateEnum +CREATE TYPE "ReportStatus" AS ENUM ('PENDING', 'RESOLVED', 'DISMISSED'); + +-- CreateEnum +CREATE TYPE "UserRole" AS ENUM ('VISITOR', 'ENTREPRENEUR', 'ADMIN'); + +-- CreateEnum +CREATE TYPE "Plan" AS ENUM ('STARTER', 'BOOSTER', 'EMPIRE'); + +-- CreateEnum +CREATE TYPE "OfferType" AS ENUM ('PRODUCT', 'SERVICE'); + +-- CreateEnum +CREATE TYPE "InterviewType" AS ENUM ('VIDEO', 'ARTICLE'); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "role" "UserRole" NOT NULL DEFAULT 'VISITOR', + "avatar" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "bio" TEXT, + "location" TEXT, + "phone" TEXT, + "isSuspended" BOOLEAN NOT NULL DEFAULT false, + "suspensionReason" TEXT, + "countryId" TEXT, + "resetToken" TEXT, + "resetTokenExpiry" TIMESTAMP(3), + "emailVerified" BOOLEAN NOT NULL DEFAULT false, + "verificationToken" TEXT, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Business" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "category" TEXT NOT NULL, + "location" TEXT NOT NULL, + "description" TEXT NOT NULL, + "logoUrl" TEXT NOT NULL, + "videoUrl" TEXT, + "contactEmail" TEXT NOT NULL, + "contactPhone" TEXT, + "socialLinks" JSONB, + "verified" BOOLEAN NOT NULL DEFAULT false, + "viewCount" INTEGER NOT NULL DEFAULT 0, + "rating" DOUBLE PRECISION NOT NULL DEFAULT 0.0, + "ratingCount" INTEGER NOT NULL DEFAULT 0, + "tags" TEXT[], + "isFeatured" BOOLEAN NOT NULL DEFAULT false, + "founderName" TEXT, + "founderImageUrl" TEXT, + "keyMetric" TEXT, + "ownerId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "slug" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT false, + "showEmail" BOOLEAN NOT NULL DEFAULT true, + "showPhone" BOOLEAN NOT NULL DEFAULT true, + "showSocials" BOOLEAN NOT NULL DEFAULT true, + "websiteUrl" TEXT, + "isSuspended" BOOLEAN NOT NULL DEFAULT false, + "suspensionReason" TEXT, + "plan" "Plan" NOT NULL DEFAULT 'STARTER', + "city" TEXT, + "countryId" TEXT, + "coverUrl" TEXT, + "coverPosition" TEXT DEFAULT '50% 50%', + "coverZoom" DOUBLE PRECISION DEFAULT 1.0, + "suggestedCategory" TEXT, + "categoryId" TEXT, + + CONSTRAINT "Business_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "BusinessCategory" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "icon" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "BusinessCategory_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Country" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "code" TEXT NOT NULL, + "flag" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "description" TEXT, + + CONSTRAINT "Country_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PricingPlan" ( + "id" TEXT NOT NULL, + "tier" "Plan" NOT NULL, + "name" TEXT NOT NULL, + "priceXOF" TEXT NOT NULL, + "priceEUR" TEXT NOT NULL, + "description" TEXT NOT NULL, + "features" TEXT[], + "offerLimit" INTEGER NOT NULL DEFAULT 1, + "recommended" BOOLEAN NOT NULL DEFAULT false, + "color" TEXT NOT NULL DEFAULT 'gray', + "updatedAt" TIMESTAMP(3) NOT NULL, + "yearlyPriceEUR" TEXT, + "yearlyPriceXOF" TEXT, + + CONSTRAINT "PricingPlan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Offer" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "type" "OfferType" NOT NULL, + "price" DOUBLE PRECISION NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'XOF', + "description" TEXT, + "imageUrl" TEXT NOT NULL, + "active" BOOLEAN NOT NULL DEFAULT true, + "businessId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Offer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "BlogPost" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "excerpt" TEXT NOT NULL, + "content" TEXT NOT NULL, + "author" TEXT NOT NULL, + "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "imageUrl" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "metaDescription" TEXT, + "metaTitle" TEXT, + "slug" TEXT, + "tags" TEXT[] DEFAULT ARRAY[]::TEXT[], + + CONSTRAINT "BlogPost_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Interview" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "guestName" TEXT NOT NULL, + "companyName" TEXT NOT NULL, + "role" TEXT NOT NULL, + "type" "InterviewType" NOT NULL, + "thumbnailUrl" TEXT NOT NULL, + "videoUrl" TEXT, + "content" TEXT, + "excerpt" TEXT NOT NULL, + "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "duration" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "metaDescription" TEXT, + "metaTitle" TEXT, + "slug" TEXT, + "tags" TEXT[] DEFAULT ARRAY[]::TEXT[], + + CONSTRAINT "Interview_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Comment" ( + "id" TEXT NOT NULL, + "content" TEXT NOT NULL, + "authorId" TEXT NOT NULL, + "businessId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AnalyticsEvent" ( + "id" TEXT NOT NULL, + "type" TEXT NOT NULL, + "path" TEXT NOT NULL, + "label" TEXT, + "value" DOUBLE PRECISION, + "metadata" JSONB, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "browser" TEXT, + "city" TEXT, + "country" TEXT, + "device" TEXT, + "ip" TEXT, + "os" TEXT, + "referrer" TEXT, + "userId" TEXT, + + CONSTRAINT "AnalyticsEvent_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Rating" ( + "id" TEXT NOT NULL, + "value" INTEGER NOT NULL, + "userId" TEXT NOT NULL, + "businessId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "comment" TEXT, + "reply" TEXT, + "replyAt" TIMESTAMP(3), + "status" "RatingStatus" NOT NULL DEFAULT 'PENDING', + + CONSTRAINT "Rating_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Conversation" ( + "id" TEXT NOT NULL, + "businessId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Conversation_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ConversationParticipant" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "conversationId" TEXT NOT NULL, + "isArchived" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "ConversationParticipant_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Message" ( + "id" TEXT NOT NULL, + "content" TEXT NOT NULL, + "senderId" TEXT NOT NULL, + "conversationId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "read" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "Message_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MessageReport" ( + "id" TEXT NOT NULL, + "reason" TEXT, + "messageId" TEXT NOT NULL, + "reporterId" TEXT NOT NULL, + "status" "ReportStatus" NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "MessageReport_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SiteSetting" ( + "id" TEXT NOT NULL DEFAULT 'singleton', + "siteName" TEXT NOT NULL DEFAULT 'Afrohub', + "siteSlogan" TEXT NOT NULL DEFAULT 'La plateforme de référence pour l''entrepreneuriat africain.', + "contactEmail" TEXT NOT NULL DEFAULT 'support@afrohub.com', + "contactPhone" TEXT DEFAULT '+225 00 00 00 00 00', + "address" TEXT DEFAULT 'Abidjan, Côte d''Ivoire', + "facebookUrl" TEXT, + "twitterUrl" TEXT, + "instagramUrl" TEXT, + "linkedinUrl" TEXT, + "footerText" TEXT DEFAULT '© 2025 Afrohub. Tous droits réservés.', + "updatedAt" TIMESTAMP(3) NOT NULL, + "homeBlogCount" INTEGER NOT NULL DEFAULT 3, + "homeBlogShow" BOOLEAN NOT NULL DEFAULT true, + "homeBlogSubtitle" TEXT NOT NULL DEFAULT 'Toutes les actualités de l''entrepreneuriat africain.', + "homeBlogTitle" TEXT NOT NULL DEFAULT 'Derniers Articles', + "homeBlogCategories" TEXT[] DEFAULT ARRAY[]::TEXT[], + "homeBlogIds" TEXT[] DEFAULT ARRAY[]::TEXT[], + "homeBlogSelection" TEXT NOT NULL DEFAULT 'latest', + "homeCategories" TEXT[] DEFAULT ARRAY['Technologie & IT', 'Agriculture & Agrobusiness', 'Mode & Textile', 'Cosmétique & Beauté']::TEXT[], + "resetPasswordSubject" TEXT NOT NULL DEFAULT 'Réinitialisation de votre mot de passe - Afrohub', + "resetPasswordTemplate" TEXT NOT NULL DEFAULT '

{siteName}

Bonjour {name},

Vous avez demandé la réinitialisation de votre mot de passe. Cliquez sur le bouton ci-dessous pour choisir un nouveau mot de passe :

Ce lien expirera dans 1 heure. Si vous n''avez pas demandé cette réinitialisation, vous pouvez ignorer cet email.


© 2025 {siteName}. Tous droits réservés.

', + "verifyEmailSubject" TEXT NOT NULL DEFAULT 'Vérification de votre compte - Afrohub', + "verifyEmailTemplate" TEXT NOT NULL DEFAULT '

{siteName}

Bonjour {name},

Merci de vous être inscrit sur {siteName}. Pour finaliser la création de votre compte, merci de vérifier votre adresse email en cliquant sur le bouton ci-dessous :

Si vous n''avez pas créé de compte sur notre plateforme, vous pouvez ignorer cet email.


© 2025 {siteName}. Tous droits réservés.

', + + CONSTRAINT "SiteSetting_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "LegalDocument" ( + "id" TEXT NOT NULL, + "type" TEXT NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "LegalDocument_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Event" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "date" TIMESTAMP(3) NOT NULL, + "location" TEXT NOT NULL, + "thumbnailUrl" TEXT NOT NULL, + "link" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "metaDescription" TEXT, + "metaTitle" TEXT, + "slug" TEXT, + "tags" TEXT[] DEFAULT ARRAY[]::TEXT[], + + CONSTRAINT "Event_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Favorite" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "businessId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Favorite_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_resetToken_key" ON "User"("resetToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_verificationToken_key" ON "User"("verificationToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "Business_ownerId_key" ON "Business"("ownerId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Business_slug_key" ON "Business"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "BusinessCategory_name_key" ON "BusinessCategory"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "BusinessCategory_slug_key" ON "BusinessCategory"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "Country_name_key" ON "Country"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "Country_code_key" ON "Country"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "PricingPlan_tier_key" ON "PricingPlan"("tier"); + +-- CreateIndex +CREATE UNIQUE INDEX "BlogPost_slug_key" ON "BlogPost"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "Interview_slug_key" ON "Interview"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "Rating_userId_businessId_key" ON "Rating"("userId", "businessId"); + +-- CreateIndex +CREATE UNIQUE INDEX "ConversationParticipant_userId_conversationId_key" ON "ConversationParticipant"("userId", "conversationId"); + +-- CreateIndex +CREATE UNIQUE INDEX "LegalDocument_type_key" ON "LegalDocument"("type"); + +-- CreateIndex +CREATE UNIQUE INDEX "Event_slug_key" ON "Event"("slug"); + +-- CreateIndex +CREATE UNIQUE INDEX "Favorite_userId_businessId_key" ON "Favorite"("userId", "businessId"); + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_countryId_fkey" FOREIGN KEY ("countryId") REFERENCES "Country"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Business" ADD CONSTRAINT "Business_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "BusinessCategory"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Business" ADD CONSTRAINT "Business_countryId_fkey" FOREIGN KEY ("countryId") REFERENCES "Country"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Business" ADD CONSTRAINT "Business_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Offer" ADD CONSTRAINT "Offer_businessId_fkey" FOREIGN KEY ("businessId") REFERENCES "Business"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_businessId_fkey" FOREIGN KEY ("businessId") REFERENCES "Business"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Rating" ADD CONSTRAINT "Rating_businessId_fkey" FOREIGN KEY ("businessId") REFERENCES "Business"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Rating" ADD CONSTRAINT "Rating_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Conversation" ADD CONSTRAINT "Conversation_businessId_fkey" FOREIGN KEY ("businessId") REFERENCES "Business"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ConversationParticipant" ADD CONSTRAINT "ConversationParticipant_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ConversationParticipant" ADD CONSTRAINT "ConversationParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Message" ADD CONSTRAINT "Message_conversationId_fkey" FOREIGN KEY ("conversationId") REFERENCES "Conversation"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Message" ADD CONSTRAINT "Message_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MessageReport" ADD CONSTRAINT "MessageReport_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "Message"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MessageReport" ADD CONSTRAINT "MessageReport_reporterId_fkey" FOREIGN KEY ("reporterId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Favorite" ADD CONSTRAINT "Favorite_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Favorite" ADD CONSTRAINT "Favorite_businessId_fkey" FOREIGN KEY ("businessId") REFERENCES "Business"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/admin/prisma/migrations/20260503171918_add_published_at/migration.sql b/admin/prisma/migrations/20260503171918_add_published_at/migration.sql new file mode 100644 index 0000000..5c2a930 --- /dev/null +++ b/admin/prisma/migrations/20260503171918_add_published_at/migration.sql @@ -0,0 +1,8 @@ +-- AlterTable +ALTER TABLE "BlogPost" ADD COLUMN "publishedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "Event" ADD COLUMN "publishedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "Interview" ADD COLUMN "publishedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/admin/prisma/migrations/20260503172958_add_content_status/migration.sql b/admin/prisma/migrations/20260503172958_add_content_status/migration.sql new file mode 100644 index 0000000..f70ae9d --- /dev/null +++ b/admin/prisma/migrations/20260503172958_add_content_status/migration.sql @@ -0,0 +1,11 @@ +-- CreateEnum +CREATE TYPE "ContentStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED'); + +-- AlterTable +ALTER TABLE "BlogPost" ADD COLUMN "status" "ContentStatus" NOT NULL DEFAULT 'PUBLISHED'; + +-- AlterTable +ALTER TABLE "Event" ADD COLUMN "status" "ContentStatus" NOT NULL DEFAULT 'PUBLISHED'; + +-- AlterTable +ALTER TABLE "Interview" ADD COLUMN "status" "ContentStatus" NOT NULL DEFAULT 'PUBLISHED'; diff --git a/admin/prisma/migrations/migration_lock.toml b/admin/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/admin/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/admin/prisma/schema.prisma b/admin/prisma/schema.prisma index ca723e3..ce93b4f 100644 --- a/admin/prisma/schema.prisma +++ b/admin/prisma/schema.prisma @@ -68,6 +68,8 @@ model Business { websiteUrl String? isSuspended Boolean @default(false) suspensionReason String? + metaTitle String? + metaDescription String? plan Plan @default(STARTER) city String? countryId String? diff --git a/admin/src/app/actions/business.ts b/admin/src/app/actions/business.ts index 3ff72d6..7469b6d 100644 --- a/admin/src/app/actions/business.ts +++ b/admin/src/app/actions/business.ts @@ -79,3 +79,23 @@ export async function getPendingVerificationCount() { return 0; } } +export async function updateBusinessSeo(id: string, data: { + metaTitle: string; + metaDescription: string; +}) { + try { + await prisma.business.update({ + where: { id }, + data: { + metaTitle: data.metaTitle, + metaDescription: data.metaDescription, + }, + }); + revalidatePath("/users"); + revalidatePath(`/annuaire/${id}`); + return { success: true }; + } catch (error) { + console.error("Failed to update business SEO:", error); + return { success: false, error: "Erreur lors de la mise à jour SEO" }; + } +} diff --git a/admin/src/app/users/page.tsx b/admin/src/app/users/page.tsx index ad40879..5830950 100644 --- a/admin/src/app/users/page.tsx +++ b/admin/src/app/users/page.tsx @@ -180,6 +180,8 @@ export default async function UsersPage({ userName={business.owner.name} isBusinessSuspended={!!business.isSuspended} isUserSuspended={!!business.owner?.isSuspended} + metaTitle={business.metaTitle} + metaDescription={business.metaDescription} /> diff --git a/admin/src/components/BusinessSeoModal.tsx b/admin/src/components/BusinessSeoModal.tsx new file mode 100644 index 0000000..6dcde76 --- /dev/null +++ b/admin/src/components/BusinessSeoModal.tsx @@ -0,0 +1,120 @@ +"use client"; + +import React, { useState, useTransition } from 'react'; +import { X, Save, Loader2, Globe } from 'lucide-react'; +import { updateBusinessSeo } from '@/app/actions/business'; +import { toast } from 'react-hot-toast'; + +interface BusinessSeoModalProps { + isOpen: boolean; + onClose: () => void; + business: { + id: string; + name: string; + metaTitle?: string | null; + metaDescription?: string | null; + }; +} + +export default function BusinessSeoModal({ isOpen, onClose, business }: BusinessSeoModalProps) { + const [isPending, startTransition] = useTransition(); + const [metaTitle, setMetaTitle] = useState(business.metaTitle || ''); + const [metaDescription, setMetaDescription] = useState(business.metaDescription || ''); + + if (!isOpen) return null; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + startTransition(async () => { + const result = await updateBusinessSeo(business.id, { + metaTitle, + metaDescription + }); + if (result.success) { + toast.success("Configuration SEO mise à jour"); + onClose(); + } else { + toast.error(result.error || "Erreur lors de la mise à jour"); + } + }); + }; + + return ( +
+
+ +
+
+
+
+ +
+
+

SEO & Partage

+

Personnaliser l'aperçu WhatsApp pour {business.name}

+
+
+ +
+ +
+
+ + setMetaTitle(e.target.value)} + placeholder={`Ex: ${business.name} - Expert en ...`} + className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500 transition-all" + /> +

Recommandé : Moins de 60 caractères.

+
+ +
+ +