migration correction et maj
Some checks failed
Build and Push App / build (push) Failing after 11m56s

This commit is contained in:
2026-05-03 23:15:14 +02:00
parent 5f421b418e
commit 16d66c0456
19 changed files with 1482 additions and 1355 deletions

Binary file not shown.

View File

@@ -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 '<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;"><h2 style="color: #0d9488; text-align: center;">{siteName}</h2><p>Bonjour {name},</p><p>Vous avez demandé la réinitialisation de votre mot de passe. Cliquez sur le bouton ci-dessous pour choisir un nouveau mot de passe :</p><div style="text-align: center; margin: 30px 0;"><a href="{resetUrl}" style="background-color: #0d9488; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold;">Réinitialiser mon mot de passe</a></div><p>Ce lien expirera dans 1 heure. Si vous n''avez pas demandé cette réinitialisation, vous pouvez ignorer cet email.</p><hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 20px 0;"><p style="font-size: 12px; color: #6b7280; text-align: center;">&copy; 2025 {siteName}. Tous droits réservés.</p></div>',
"verifyEmailSubject" TEXT NOT NULL DEFAULT 'Vérification de votre compte - Afrohub',
"verifyEmailTemplate" TEXT NOT NULL DEFAULT '<div style="font-family: sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #e5e7eb; border-radius: 8px;"><h2 style="color: #0d9488; text-align: center;">{siteName}</h2><p>Bonjour {name},</p><p>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 :</p><div style="text-align: center; margin: 30px 0;"><a href="{verifyUrl}" style="background-color: #0d9488; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold;">Vérifier mon compte</a></div><p>Si vous n''avez pas créé de compte sur notre plateforme, vous pouvez ignorer cet email.</p><hr style="border: 0; border-top: 1px solid #e5e7eb; margin: 20px 0;"><p style="font-size: 12px; color: #6b7280; text-align: center;">&copy; 2025 {siteName}. Tous droits réservés.</p></div>',
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;

View File

@@ -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;

View File

@@ -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';

View File

@@ -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"

View File

@@ -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?

View File

@@ -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" };
}
}

View File

@@ -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}
/>
</div>
</td>

View File

@@ -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 (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<div className="absolute inset-0 bg-slate-950/80 backdrop-blur-sm" onClick={onClose} />
<div className="relative bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl animate-in zoom-in duration-200">
<div className="p-6 border-b border-slate-800 flex justify-between items-center">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-indigo-500/10 flex items-center justify-center text-indigo-500">
<Globe className="w-6 h-6" />
</div>
<div>
<h3 className="text-xl font-bold text-white">SEO & Partage</h3>
<p className="text-xs text-slate-400">Personnaliser l'aperçu WhatsApp pour {business.name}</p>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-slate-800 rounded-lg text-slate-400 hover:text-white transition-colors">
<X className="w-6 h-6" />
</button>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Meta Titre (WhatsApp/Google)</label>
<input
value={metaTitle}
onChange={(e) => 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"
/>
<p className="text-[10px] text-slate-500 italic">Recommandé : Moins de 60 caractères.</p>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Meta Description</label>
<textarea
value={metaDescription}
onChange={(e) => setMetaDescription(e.target.value)}
rows={4}
placeholder="Décrivez l'entreprise en quelques mots pour attirer les clics..."
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 resize-none"
/>
<p className="text-[10px] text-slate-500 italic">Recommandé : Entre 140 et 160 caractères.</p>
</div>
<div className="bg-indigo-500/5 border border-indigo-500/10 rounded-xl p-4">
<h4 className="text-[10px] font-bold text-indigo-400 uppercase tracking-widest mb-2">Aperçu du partage</h4>
<div className="bg-slate-950 rounded-lg p-3 border border-slate-800">
<div className="text-sm font-bold text-indigo-400 line-clamp-1">{metaTitle || business.name}</div>
<div className="text-xs text-slate-500 line-clamp-2 mt-1">{metaDescription || "Découvrez cette entreprise sur Afroprenariat..."}</div>
<div className="text-[10px] text-slate-600 mt-2">afroprenariat.com</div>
</div>
</div>
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={onClose}
className="flex-1 py-3 rounded-xl border border-slate-800 text-slate-400 font-bold hover:bg-slate-800 transition-all"
>
Annuler
</button>
<button
type="submit"
disabled={isPending}
className="flex-[2] py-3 rounded-xl bg-indigo-600 text-white font-bold hover:bg-indigo-500 transition-all flex items-center justify-center gap-2 shadow-lg shadow-indigo-600/20"
>
{isPending ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<Save className="w-5 h-5" />
)}
Enregistrer
</button>
</div>
</form>
</div>
</div>
);
}

View File

@@ -1,10 +1,11 @@
"use client";
import React, { useState } from 'react';
import { ShieldAlert, ShieldCheck, ShoppingBag, User } from 'lucide-react';
import { ShieldAlert, ShieldCheck, ShoppingBag, User, Globe } from 'lucide-react';
import { suspendUser, unsuspendUser, suspendBusiness, unsuspendBusiness } from '@/app/actions/suspension';
import { toast } from 'react-hot-toast';
import SuspensionModal from './SuspensionModal';
import BusinessSeoModal from './BusinessSeoModal';
interface EntrepreneurActionsProps {
businessId: string;
@@ -13,6 +14,8 @@ interface EntrepreneurActionsProps {
userName: string;
isBusinessSuspended: boolean;
isUserSuspended: boolean;
metaTitle?: string | null;
metaDescription?: string | null;
}
export default function EntrepreneurActions({
@@ -21,9 +24,12 @@ export default function EntrepreneurActions({
userId,
userName,
isBusinessSuspended,
isUserSuspended
isUserSuspended,
metaTitle,
metaDescription
}: EntrepreneurActionsProps) {
const [modalType, setModalType] = useState<'USER' | 'BUSINESS' | null>(null);
const [seoModalOpen, setSeoModalOpen] = useState(false);
const handleConfirm = async (reason: string) => {
if (modalType === 'USER') {
@@ -41,6 +47,16 @@ export default function EntrepreneurActions({
return (
<div className="flex flex-col gap-2">
<div className="flex gap-2 justify-end">
{/* SEO Button */}
<button
onClick={() => setSeoModalOpen(true)}
className="btn-action bg-indigo-500/10 text-indigo-500 hover:bg-indigo-500 hover:text-white"
title="Modifier le SEO / Partage WhatsApp"
>
<Globe className="w-4 h-4" />
<span className="text-[10px] uppercase font-bold">SEO</span>
</button>
{/* User Suspension */}
{isUserSuspended ? (
<button
@@ -93,6 +109,17 @@ export default function EntrepreneurActions({
? `Voulez-vous suspendre TOUT le compte de ${userName} ? Sa boutique sera également masquée.`
: `Voulez-vous masquer la boutique "${businessName}" ? L'entrepreneur gardera l'accès à son compte.`}
/>
<BusinessSeoModal
isOpen={seoModalOpen}
onClose={() => setSeoModalOpen(false)}
business={{
id: businessId,
name: businessName,
metaTitle,
metaDescription
}}
/>
</div>
);
}