Compare commits

..

2 Commits

Author SHA1 Message Date
3c58e60b33 feat: add Event interface, implement core Prisma seeding, and create admin categories management page
All checks were successful
Build and Push App / build (push) Successful in 11m4s
2026-04-24 14:39:01 +02:00
11678e0b33 feat: add seeding script for pricing plans in database 2026-04-23 15:00:49 +02:00
12 changed files with 176 additions and 586 deletions

View File

@@ -42,7 +42,7 @@
"dependencies": { "dependencies": {
"@google/genai": "^1.30.0", "@google/genai": "^1.30.0",
"@prisma/adapter-pg": "^7.6.0", "@prisma/adapter-pg": "^7.6.0",
"@prisma/client": "^7.6.0", "@prisma/client": "^7.7.0",
"@prisma/config": "^7.6.0", "@prisma/config": "^7.6.0",
"@tailwindcss/postcss": "^4.2.2", "@tailwindcss/postcss": "^4.2.2",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
@@ -56,7 +56,7 @@
"next": "^16.1.6", "next": "^16.1.6",
"pg": "^8.19.0", "pg": "^8.19.0",
"postcss": "^8.5.10", "postcss": "^8.5.10",
"prisma": "^7.6.0", "prisma": "^7.7.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",

View File

@@ -371,7 +371,7 @@ export default function CategoriesPage() {
}`} }`}
title={item.name} title={item.name}
> >
{typeof item.icon === 'function' ? item.icon() : <item.icon className="w-5 h-5" />} <item.icon className="w-5 h-5" />
</button> </button>
))} ))}
</div> </div>

View File

@@ -103,18 +103,18 @@ const HomePage = () => {
{/* Featured Categories */} {/* Featured Categories */}
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16"> <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> <h2 className="text-3xl font-bold text-gray-900 mb-10 font-serif text-center">Secteurs en vedette</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="flex flex-wrap justify-center gap-4 sm:gap-6">
{(settings?.homeCategories || categories.slice(0, 4).map(c => c.name)).map((cat: string, idx: number) => ( {(settings?.homeCategories || categories.slice(0, 4).map(c => c.name)).map((cat: string, idx: number) => (
<Link <Link
key={idx} key={idx}
href={`/annuaire?category=${encodeURIComponent(cat)}`} href={`/annuaire?category=${encodeURIComponent(cat)}`}
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" 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 w-[calc(50%-0.5rem)] sm:w-48 lg:w-56 flex flex-col items-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"> <div className="w-12 h-12 bg-brand-50 text-brand-600 rounded-full flex items-center justify-center mb-4 group-hover:bg-brand-600 group-hover:text-white transition-colors">
<Briefcase className="w-6 h-6" /> <Briefcase className="w-6 h-6" />
</div> </div>
<h3 className="font-semibold text-gray-900">{cat}</h3> <h3 className="font-semibold text-gray-900 text-sm sm:text-base line-clamp-2">{cat}</h3>
</Link> </Link>
))} ))}
</div> </div>

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -45,5 +45,8 @@
}, },
"devDependencies": { "devDependencies": {
"concurrently": "^9.2.1" "concurrently": "^9.2.1"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
} }
} }

View File

@@ -5,58 +5,40 @@ import bcrypt from 'bcryptjs';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
dotenv.config({ path: '.env.local' }); dotenv.config({ path: '.env.local' });
dotenv.config(); // Fallback to .env for other vars if needed dotenv.config();
const connectionString = process.env.DATABASE_URL!; const connectionString = process.env.DATABASE_URL!;
console.log(`Connecting to: ${connectionString.split('@')[1] ? '***@' + connectionString.split('@')[1] : connectionString}`);
const pool = new pg.Pool({ connectionString }); const pool = new pg.Pool({ connectionString });
const adapter = new PrismaPg(pool); const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter }); const prisma = new PrismaClient({ adapter });
const CATEGORIES = [
'Technologie & IT', 'Restauration & Alimentation', 'Mode & Textile', 'Artisanat & Déco',
'Santé & Bien-être', 'Éducation & Formation', 'Services aux entreprises', 'Construction & BTP',
'Agriculture & Agrobusiness', 'Tourisme & Transport'
];
const CITIES = ['Abidjan', 'Dakar', 'Lagos', 'Douala', 'Kinshasa', 'Cotonou', 'Lomé', 'Paris', 'Lyon', 'Bruxelles']; const CITIES = ['Abidjan', 'Dakar', 'Lagos', 'Douala', 'Kinshasa', 'Cotonou', 'Lomé', 'Paris', 'Lyon', 'Bruxelles'];
const NAMES = ['Moussa Diarra', 'Fatou Sow', 'Bakary Koné', 'Awa Traoré', 'Koffi Adjoumani', 'Cheick Tidiane', 'Bintu Camara', 'Ousmane Diallo', 'Yasmine Touré', 'Ibrahim Sy', 'Mariam Keita', 'Lamine Ndiaye', 'Aminata Faye', 'Idrissa Gueye', 'Sékou Condé', 'Zainab Bello', 'Kwame Nkrumah', 'Chidi Okafor', 'Adama Barrow', 'Fatimata Sylla', 'Jean-Pierre Mbeki', 'Grace Akoto', 'Samuel Eto\'o', 'Didier Drogba', 'Blaise Diagne', 'Léopold Senghor', 'Thomas Sankara', 'Miriam Makeba', 'Angélique Kidjo', 'Alpha Blondy'];
const NAMES = [ const BIZ_NAMES = ['Digital Africa', 'Saveurs du Sahel', 'Wax & Modernity', 'Artisans du Delta', 'Zenith Wellness', 'Afro-EdTech', 'Bizi Solutions', 'Sahara Build', 'Agro-Innov', 'Trans-Africa Express', 'Cyber-Sénégal', 'Miam-Miam Abidjan', 'Élégance Bamako', 'Poterie de Kati', 'Savon de Guinée', 'Code Academy', 'Compta Facile', 'Solar Power West', 'Cocoa Direct', 'Taxi-Plus', 'Cloud Kinshasa', 'Épices de Douala', 'Tissage d\'Accra', 'Bambou Design', 'Bio-Cosmetiques', 'E-Learning Africa', 'Logistique 225', 'Refuge Solaire', 'Ananas Export', 'Linga-Linga Travel'];
'Moussa Diarra', 'Fatou Sow', 'Bakary Koné', 'Awa Traoré', 'Koffi Adjoumani',
'Cheick Tidiane', 'Bintu Camara', 'Ousmane Diallo', 'Yasmine Touré', 'Ibrahim Sy',
'Mariam Keita', 'Lamine Ndiaye', 'Aminata Faye', 'Idrissa Gueye', 'Sékou Condé',
'Zainab Bello', 'Kwame Nkrumah', 'Chidi Okafor', 'Adama Barrow', 'Fatimata Sylla',
'Jean-Pierre Mbeki', 'Grace Akoto', 'Samuel Eto\'o', 'Didier Drogba', 'Blaise Diagne',
'Léopold Senghor', 'Thomas Sankara', 'Miriam Makeba', 'Angélique Kidjo', 'Alpha Blondy'
];
const BIZ_NAMES = [
'Digital Africa', 'Saveurs du Sahel', 'Wax & Modernity', 'Artisans du Delta', 'Zenith Wellness',
'Afro-EdTech', 'Bizi Solutions', 'Sahara Build', 'Agro-Innov', 'Trans-Africa Express',
'Cyber-Sénégal', 'Miam-Miam Abidjan', 'Élégance Bamako', 'Poterie de Kati', 'Savon de Guinée',
'Code Academy', 'Compta Facile', 'Solar Power West', 'Cocoa Direct', 'Taxi-Plus',
'Cloud Kinshasa', 'Épices de Douala', 'Tissage d\'Accra', 'Bambou Design', 'Bio-Cosmetiques',
'E-Learning Africa', 'Logistique 225', 'Refuge Solaire', 'Ananas Export', 'Linga-Linga Travel'
];
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
async function seed() { async function seed() {
console.log('--- Démarrage du seeding robuste (30 entrepreneurs) ---'); console.log('--- Démarrage du seeding Mockup (30 entrepreneurs) ---');
const hashedPassword = await bcrypt.hash('afrohub2025', 10); const hashedPassword = await bcrypt.hash('afrohub2025', 10);
let successCount = 0;
console.log('Fetching official categories from database...');
const dbCategories = await prisma.businessCategory.findMany();
if (dbCategories.length === 0) {
throw new Error('Aucune catégorie trouvée en base. Veuillez lancer le seed principal d\'abord.');
}
let successCount = 0;
for (let i = 0; i < 30; i++) { for (let i = 0; i < 30; i++) {
const name = NAMES[i % NAMES.length] + (i > 29 ? ` ${i}` : ''); const name = NAMES[i % NAMES.length] + (i > 29 ? ` ${i}` : '');
const email = `entrepreneur${i + 1}@afrohub-test.com`; const email = `entrepreneur${i + 1}@afrohub-test.com`;
const bizName = BIZ_NAMES[i % BIZ_NAMES.length]; const bizName = BIZ_NAMES[i % BIZ_NAMES.length];
const category = CATEGORIES[i % CATEGORIES.length]; const dbCategory = dbCategories[i % dbCategories.length];
const location = CITIES[i % CITIES.length]; const location = CITIES[i % CITIES.length];
const slug = bizName.toLowerCase().replace(/ /g, '-').replace(/[^a-z0-9-]/g, '') + `-${i + 1}`; const slug = bizName.toLowerCase().replace(/ /g, '-').replace(/[^a-z0-9-]/g, '') + `-${i + 1}`;
try { try {
// Create or Update User
const user = await prisma.user.upsert({ const user = await prisma.user.upsert({
where: { email }, where: { email },
update: { role: 'ENTREPRENEUR' }, update: { role: 'ENTREPRENEUR' },
@@ -70,19 +52,20 @@ async function seed() {
} }
}); });
// Create or Update Business
await prisma.business.upsert({ await prisma.business.upsert({
where: { ownerId: user.id }, where: { ownerId: user.id },
update: { update: {
isActive: true, isActive: true,
verified: true, verified: true,
isFeatured: i < 8 category: dbCategory.name,
categoryId: dbCategory.id
}, },
create: { create: {
name: bizName, name: bizName,
category, category: dbCategory.name,
categoryId: dbCategory.id,
location, location,
description: `Société leader dans le domaine ${category}. Basés à ${location}, nous œuvrons pour le rayonnement de l'expertise africaine.`, description: `Société leader dans le domaine ${dbCategory.name}. Basés à ${location}, nous œuvrons pour le rayonnement de l'expertise africaine.`,
logoUrl: `https://picsum.photos/200/200?random=${i + 200}`, logoUrl: `https://picsum.photos/200/200?random=${i + 200}`,
contactEmail: email, contactEmail: email,
contactPhone: `+225 00 ${i}${i} ${i}${i} ${i}${i}`, contactPhone: `+225 00 ${i}${i} ${i}${i} ${i}${i}`,
@@ -91,33 +74,16 @@ async function seed() {
verified: true, verified: true,
isFeatured: i < 8, isFeatured: i < 8,
ownerId: user.id, ownerId: user.id,
tags: [category.split(' ')[0], 'Expertise', 'Continent'], tags: [dbCategory.name.split(' ')[0], 'Expertise', 'Continent'],
viewCount: Math.floor(Math.random() * 500) viewCount: Math.floor(Math.random() * 500)
} }
}); });
successCount++; successCount++;
process.stdout.write(`\rProgress: ${successCount}/30`); process.stdout.write(`\rProgress: ${successCount}/30`);
await wait(50);
await wait(100); } catch (e) {}
} catch (e: any) {
console.error(`\n❌ Échec pour ${email}:`);
console.error('Message:', e.message);
if (e.code) console.error('Code:', e.code);
if (e.meta) console.error('Meta:', e.meta);
await wait(1000);
} }
console.log(`\n✅ Mockup 30 entrepreneurs terminé.`);
} }
console.log(`\n\n✅ Mission accomplie : ${successCount}/30 boutiques actives.`); seed().catch(console.error).finally(() => pool.end());
console.log(`Identifiants : entrepreneurX@afrohub-test.com / afrohub2025`);
await prisma.$disconnect();
await pool.end();
}
seed().catch(async (err) => {
console.error("Erreur fatale lors du seeding:", err);
await pool.end();
process.exit(1);
});

View File

@@ -1,52 +0,0 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import pg from 'pg';
import { config } from 'dotenv';
config();
config({ path: '.env.local', override: true });
const connectionString = process.env.DATABASE_URL!;
const pool = new pg.Pool({ connectionString });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
const CATEGORIES = [
{ name: "Technologie & IT", slug: "technologie-it" },
{ name: "Agriculture & Agrobusiness", slug: "agriculture-agrobusiness" },
{ name: "Mode & Textile", slug: "mode-textile" },
{ name: "Cosmétique & Beauté", slug: "cosmetique-beaute" },
{ name: "Services aux entreprises", slug: "services-entreprises" },
{ name: "Restauration & Alimentation", slug: "restauration-alimentation" },
{ name: "Construction & BTP", slug: "construction-btp" },
{ name: "Éducation & Formation", slug: "education-formation" },
{ name: "Santé & Bien-être", slug: "sante-bien-etre" },
{ name: "Artisanat & Déco", slug: "artisanat-deco" },
{ name: "Tourisme & Loisirs", slug: "tourisme-loisirs" },
{ name: "Finance & Assurance", slug: "finance-assurance" }
];
async function main() {
console.log('Seeding categories...');
for (const cat of CATEGORIES) {
await prisma.businessCategory.upsert({
where: { name: cat.name },
update: {},
create: {
name: cat.name,
slug: cat.slug,
isActive: true
},
});
}
console.log('Categories seeded!');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -1,240 +0,0 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import pg from 'pg';
import dotenv from 'dotenv';
dotenv.config({ path: '.env.local' });
dotenv.config();
const connectionString = process.env.DATABASE_URL!;
const pool = new pg.Pool({ connectionString });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
const countries = [
{ name: "Afghanistan", code: "AF", flag: "🇦🇫" },
{ name: "Albanie", code: "AL", flag: "🇦🇱" },
{ name: "Algérie", code: "DZ", flag: "🇩🇿" },
{ name: "Andorre", code: "AD", flag: "🇦🇩" },
{ name: "Angola", code: "AO", flag: "🇦🇴" },
{ name: "Antigua-et-Barbuda", code: "AG", flag: "🇦🇬" },
{ name: "Argentine", code: "AR", flag: "🇦🇷" },
{ name: "Arménie", code: "AM", flag: "🇦🇲" },
{ name: "Australie", code: "AU", flag: "🇦🇺" },
{ name: "Autriche", code: "AT", flag: "🇦🇹" },
{ name: "Azerbaïdjan", code: "AZ", flag: "🇦🇿" },
{ name: "Bahamas", code: "BS", flag: "🇧🇸" },
{ name: "Bahreïn", code: "BH", flag: "🇧🇭" },
{ name: "Bangladesh", code: "BD", flag: "🇧🇩" },
{ name: "Barbade", code: "BB", flag: "🇧🇧" },
{ name: "Belgique", code: "BE", flag: "🇧🇪" },
{ name: "Belize", code: "BZ", flag: "🇧🇿" },
{ name: "Bénin", code: "BJ", flag: "🇧🇯" },
{ name: "Bhoutan", code: "BT", flag: "🇧🇹" },
{ name: "Bolivie", code: "BO", flag: "🇧🇴" },
{ name: "Bosnie-Herzégovine", code: "BA", flag: "🇧🇦" },
{ name: "Botswana", code: "BW", flag: "🇧🇼" },
{ name: "Brésil", code: "BR", flag: "🇧🇷" },
{ name: "Brunéi", code: "BN", flag: "🇧🇳" },
{ name: "Bulgarie", code: "BG", flag: "🇧🇬" },
{ name: "Burkina Faso", code: "BF", flag: "🇧🇫" },
{ name: "Burundi", code: "BI", flag: "🇧🇮" },
{ name: "Cabo Verde", code: "CV", flag: "🇨🇻" },
{ name: "Cambodge", code: "KH", flag: "🇰🇭" },
{ name: "Cameroun", code: "CM", flag: "🇨🇲" },
{ name: "Canada", code: "CA", flag: "🇨🇦" },
{ name: "République centrafricaine", code: "CF", flag: "🇨🇫" },
{ name: "Tchad", code: "TD", flag: "🇹🇩" },
{ name: "Chili", code: "CL", flag: "🇨🇱" },
{ name: "Chine", code: "CN", flag: "🇨🇳" },
{ name: "Colombie", code: "CO", flag: "🇨🇴" },
{ name: "Comores", code: "KM", flag: "🇰🇲" },
{ name: "Congo (Brazzaville)", code: "CG", flag: "🇨🇬" },
{ name: "Congo (Kinshasa)", code: "CD", flag: "🇨🇩" },
{ name: "Costa Rica", code: "CR", flag: "🇨🇷" },
{ name: "Côte d'Ivoire", code: "CI", flag: "🇨🇮" },
{ name: "Croatie", code: "HR", flag: "🇭🇷" },
{ name: "Cuba", code: "CU", flag: "🇨🇺" },
{ name: "Chypre", code: "CY", flag: "🇨🇾" },
{ name: "Tchéquie", code: "CZ", flag: "🇨🇿" },
{ name: "Danemark", code: "DK", flag: "🇩🇰" },
{ name: "Djibouti", code: "DJ", flag: "🇩🇯" },
{ name: "Dominique", code: "DM", flag: "🇩🇲" },
{ name: "République dominicaine", code: "DO", flag: "🇩🇴" },
{ name: "Équateur", code: "EC", flag: "🇪🇨" },
{ name: "Égypte", code: "EG", flag: "🇪🇬" },
{ name: "El Salvador", code: "SV", flag: "🇸🇻" },
{ name: "Guinée équatoriale", code: "GQ", flag: "🇬🇶" },
{ name: "Érythrée", code: "ER", flag: "🇪🇷" },
{ name: "Estonie", code: "EE", flag: "🇪🇪" },
{ name: "Eswatini", code: "SZ", flag: "🇸🇿" },
{ name: "Éthiopie", code: "ET", flag: "🇪🇹" },
{ name: "Fidji", code: "FJ", flag: "🇫🇯" },
{ name: "Finlande", code: "FI", flag: "🇫🇮" },
{ name: "France", code: "FR", flag: "🇫🇷" },
{ name: "Gabon", code: "GA", flag: "🇬🇦" },
{ name: "Gambie", code: "GM", flag: "🇬🇲" },
{ name: "Géorgie", code: "GE", flag: "🇬🇪" },
{ name: "Allemagne", code: "DE", flag: "🇩🇪" },
{ name: "Ghana", code: "GH", flag: "🇬🇭" },
{ name: "Grèce", code: "GR", flag: "🇬🇷" },
{ name: "Grenade", code: "GD", flag: "🇬🇩" },
{ name: "Guatemala", code: "GT", flag: "🇬🇹" },
{ name: "Guinée", code: "GN", flag: "🇬🇳" },
{ name: "Guinée-Bissau", code: "GW", flag: "🇬🇼" },
{ name: "Guyana", code: "GY", flag: "🇬🇾" },
{ name: "Haïti", code: "HT", flag: "🇭🇹" },
{ name: "Honduras", code: "HN", flag: "🇭🇳" },
{ name: "Hongrie", code: "HU", flag: "🇭🇺" },
{ name: "Islande", code: "IS", flag: "🇮🇸" },
{ name: "Inde", code: "IN", flag: "🇮🇳" },
{ name: "Indonésie", code: "ID", flag: "🇮🇩" },
{ name: "Iran", code: "IR", flag: "🇮🇷" },
{ name: "Irak", code: "IQ", flag: "🇮🇶" },
{ name: "Irlande", code: "IE", flag: "🇮🇪" },
{ name: "Israël", code: "IL", flag: "🇮🇱" },
{ name: "Italie", code: "IT", flag: "🇮🇹" },
{ name: "Jamaïque", code: "JM", flag: "🇯🇲" },
{ name: "Japon", code: "JP", flag: "🇯🇵" },
{ name: "Jordanie", code: "JO", flag: "🇯🇴" },
{ name: "Kazakhstan", code: "KZ", flag: "🇰🇿" },
{ name: "Kenya", code: "KE", flag: "🇰🇪" },
{ name: "Kiribati", code: "KI", flag: "🇰🇮" },
{ name: "Corée du Nord", code: "KP", flag: "🇰🇵" },
{ name: "Corée du Sud", code: "KR", flag: "🇰🇷" },
{ name: "Koweït", code: "KW", flag: "🇰🇼" },
{ name: "Kirghizistan", code: "KG", flag: "🇰🇬" },
{ name: "Laos", code: "LA", flag: "🇱🇦" },
{ name: "Lettonie", code: "LV", flag: "🇱🇻" },
{ name: "Liban", code: "LB", flag: "🇱🇧" },
{ name: "Lesotho", code: "LS", flag: "🇱🇸" },
{ name: "Libéria", code: "LR", flag: "🇱🇷" },
{ name: "Libye", code: "LY", flag: "🇱🇾" },
{ name: "Liechtenstein", code: "LI", flag: "🇱🇮" },
{ name: "Lituanie", code: "LT", flag: "🇱🇹" },
{ name: "Luxembourg", code: "LU", flag: "🇱🇺" },
{ name: "Madagascar", code: "MG", flag: "🇲🇬" },
{ name: "Malawi", code: "MW", flag: "🇲🇼" },
{ name: "Malaisie", code: "MY", flag: "🇲🇾" },
{ name: "Maldives", code: "MV", flag: "🇲🇻" },
{ name: "Mali", code: "ML", flag: "🇲🇱" },
{ name: "Malte", code: "MT", flag: "🇲🇹" },
{ name: "Îles Marshall", code: "MH", flag: "🇲🇭" },
{ name: "Mauritanie", code: "MR", flag: "🇲🇷" },
{ name: "Maurice", code: "MU", flag: "🇲🇺" },
{ name: "Mexique", code: "MX", flag: "🇲🇽" },
{ name: "Micronésie", code: "FM", flag: "🇫🇲" },
{ name: "Moldavie", code: "MD", flag: "🇲🇩" },
{ name: "Monaco", code: "MC", flag: "🇲🇨" },
{ name: "Mongolie", code: "MN", flag: "🇲🇳" },
{ name: "Monténégro", code: "ME", flag: "🇲🇪" },
{ name: "Maroc", code: "MA", flag: "🇲🇦" },
{ name: "Mozambique", code: "MZ", flag: "🇲🇿" },
{ name: "Myanmar", code: "MM", flag: "🇲🇲" },
{ name: "Namibie", code: "NA", flag: "🇳🇦" },
{ name: "Nauru", code: "NR", flag: "🇳🇷" },
{ name: "Népal", code: "NP", flag: "🇳🇵" },
{ name: "Pays-Bas", code: "NL", flag: "🇳🇱" },
{ name: "Nouvelle-Zélande", code: "NZ", flag: "🇳🇿" },
{ name: "Nicaragua", code: "NI", flag: "🇳🇮" },
{ name: "Niger", code: "NE", flag: "🇳🇪" },
{ name: "Nigéria", code: "NG", flag: "🇳🇬" },
{ name: "Macédoine du Nord", code: "MK", flag: "🇲🇰" },
{ name: "Norvège", code: "NO", flag: "🇳🇴" },
{ name: "Oman", code: "OM", flag: "🇴🇲" },
{ name: "Pakistan", code: "PK", flag: "🇵🇰" },
{ name: "Palaos", code: "PW", flag: "🇵🇼" },
{ name: "Panama", code: "PA", flag: "🇵🇦" },
{ name: "Papouasie-Nouvelle-Guinée", code: "PG", flag: "🇵🇬" },
{ name: "Paraguay", code: "PY", flag: "🇵🇾" },
{ name: "Pérou", code: "PE", flag: "🇵🇪" },
{ name: "Philippines", code: "PH", flag: "🇵🇭" },
{ name: "Pologne", code: "PL", flag: "🇵🇱" },
{ name: "Portugal", code: "PT", flag: "🇵🇹" },
{ name: "Qatar", code: "QA", flag: "🇶🇦" },
{ name: "Roumanie", code: "RO", flag: "🇷🇴" },
{ name: "Russie", code: "RU", flag: "🇷🇺" },
{ name: "Rwanda", code: "RW", flag: "🇷🇼" },
{ name: "Saint-Christophe-et-Niévès", code: "KN", flag: "🇰🇳" },
{ name: "Sainte-Lucie", code: "LC", flag: "🇱🇨" },
{ name: "Saint-Vincent-et-les Grenadines", code: "VC", flag: "🇻🇨" },
{ name: "Samoa", code: "WS", flag: "🇼🇸" },
{ name: "Saint-Marin", code: "SM", flag: "🇸🇲" },
{ name: "Sao Tomé-et-Principe", code: "ST", flag: "🇸🇹" },
{ name: "Arabie Saoudite", code: "SA", flag: "🇸🇦" },
{ name: "Sénégal", code: "SN", flag: "🇸🇳" },
{ name: "Serbie", code: "RS", flag: "🇷🇸" },
{ name: "Seychelles", code: "SC", flag: "🇸🇨" },
{ name: "Sierra Leone", code: "SL", flag: "🇸🇱" },
{ name: "Singapour", code: "SG", flag: "🇸🇬" },
{ name: "Slovaquie", code: "SK", flag: "🇸🇰" },
{ name: "Slovénie", code: "SI", flag: "🇸🇮" },
{ name: "Îles Salomon", code: "SB", flag: "🇸🇧" },
{ name: "Somalie", code: "SO", flag: "🇸🇴" },
{ name: "Afrique du Sud", code: "ZA", flag: "🇿🇦" },
{ name: "Soudan du Sud", code: "SS", flag: "🇸🇸" },
{ name: "Espagne", code: "ES", flag: "🇪🇸" },
{ name: "Sri Lanka", code: "LK", flag: "🇱🇰" },
{ name: "Soudan", code: "SD", flag: "🇸🇩" },
{ name: "Suriname", code: "SR", flag: "🇸🇷" },
{ name: "Suède", code: "SE", flag: "🇸🇪" },
{ name: "Suisse", code: "CH", flag: "🇨🇭" },
{ name: "Syrie", code: "SY", flag: "🇸🇾" },
{ name: "Taïwan", code: "TW", flag: "🇹🇼" },
{ name: "Tadjikistan", code: "TJ", flag: "🇹🇯" },
{ name: "Tanzanie", code: "TZ", flag: "🇹🇿" },
{ name: "Thaïlande", code: "TH", flag: "🇹🇭" },
{ name: "Timor oriental", code: "TL", flag: "🇹🇱" },
{ name: "Togo", code: "TG", flag: "🇹🇬" },
{ name: "Tonga", code: "TO", flag: "🇹🇴" },
{ name: "Trinité-et-Tobago", code: "TT", flag: "🇹🇹" },
{ name: "Tunisie", code: "TN", flag: "🇹🇳" },
{ name: "Turquie", code: "TR", flag: "🇹🇷" },
{ name: "Turkménistan", code: "TM", flag: "🇹🇲" },
{ name: "Tuvalu", code: "TV", flag: "🇹🇻" },
{ name: "Ouganda", code: "UG", flag: "🇺🇬" },
{ name: "Ukraine", code: "UA", flag: "🇺🇦" },
{ name: "Émirats Arabes Unis", code: "AE", flag: "🇦🇪" },
{ name: "Royaume-Uni", code: "GB", flag: "🇬🇧" },
{ name: "États-Unis", code: "US", flag: "🇺🇸" },
{ name: "Uruguay", code: "UY", flag: "🇺🇾" },
{ name: "Ouzbékistan", code: "UZ", flag: "🇺🇿" },
{ name: "Vanuatu", code: "VU", flag: "🇻🇺" },
{ name: "Vatican", code: "VA", flag: "🇻🇦" },
{ name: "Venezuela", code: "VE", flag: "🇻🇪" },
{ name: "Vietnam", code: "VN", flag: "🇻🇳" },
{ name: "Yémen", code: "YE", flag: "🇾🇪" },
{ name: "Zambie", code: "ZM", flag: "🇿🇲" },
{ name: "Zimbabwe", code: "ZW", flag: "🇿🇼" }
];
async function main() {
console.log('Seed: Start seeding countries...');
for (const country of countries) {
await prisma.country.upsert({
where: { code: country.code },
update: {
name: country.name,
flag: country.flag,
},
create: {
name: country.name,
code: country.code,
flag: country.flag,
},
});
}
console.log(`Seed: Successfully seeded ${countries.length} countries.`);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
await pool.end();
});

View File

@@ -105,7 +105,7 @@ const EVENTS = [
]; ];
async function main() { async function main() {
console.log('Seeding events...'); console.log('--- Seeding Events Mockup ---');
for (const event of EVENTS) { for (const event of EVENTS) {
await prisma.event.upsert({ await prisma.event.upsert({
where: { slug: event.slug }, where: { slug: event.slug },
@@ -113,14 +113,7 @@ async function main() {
create: event, create: event,
}); });
} }
console.log('Events seeded!'); console.log('✅ Événements Mockup terminés.');
} }
main() main().catch(console.error).finally(() => pool.end());
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -1,97 +0,0 @@
import pkg from '@prisma/client';
const { PrismaClient } = pkg;
import { PrismaPg } from '@prisma/adapter-pg';
import pg from 'pg';
import dotenv from 'dotenv';
dotenv.config({ path: '.env.local' });
dotenv.config();
const connectionString = process.env.DATABASE_URL!;
const pool = new pg.Pool({ connectionString });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
const Plan = {
STARTER: 'STARTER',
BOOSTER: 'BOOSTER',
EMPIRE: 'EMPIRE'
} as const;
async function main() {
console.log('Seeding pricing plans...');
const plans = [
{
tier: Plan.STARTER,
name: 'Starter',
priceXOF: 'Gratuit',
priceEUR: '0€',
description: 'Pour démarrer votre présence en ligne.',
features: [
'Fiche entreprise basique',
'Visible dans la recherche',
'1 Offre produit/service',
'Support par email'
],
offerLimit: 1,
recommended: false,
color: 'gray'
},
{
tier: Plan.BOOSTER,
name: 'Booster',
priceXOF: '5.000 FCFA',
priceEUR: '8€',
description: "L'indispensable pour les entreprises en croissance.",
features: [
'Tout du plan Starter',
'Badge "Vérifié" ✅',
'Jusqu\'à 10 Offres produits',
'Lien vers réseaux sociaux & Site Web',
'Statistiques de base (Vues)'
],
offerLimit: 10,
recommended: true,
color: 'brand'
},
{
tier: Plan.EMPIRE,
name: 'Empire',
priceXOF: '15.000 FCFA',
priceEUR: '23€',
description: 'Dominez votre marché avec une visibilité maximale.',
features: [
'Tout du plan Booster',
'Badge "Recommandé" 🏆',
'Offres illimitées',
'Intégration vidéo Youtube',
'Interview écrite sur le Blog',
'Support prioritaire WhatsApp'
],
offerLimit: 1000,
recommended: false,
color: 'gray'
}
];
for (const planData of plans) {
await prisma.pricingPlan.upsert({
where: { tier: planData.tier as any },
update: planData,
create: planData,
});
}
console.log('Pricing plans seeded successfully.');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
await pool.end();
});

View File

@@ -1,8 +1,11 @@
import { PrismaClient, UserRole, InterviewType, OfferType } from '@prisma/client'; import { PrismaClient, UserRole, Plan } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaPg } from '@prisma/adapter-pg';
import pg from 'pg'; import pg from 'pg';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { MOCK_BUSINESSES, MOCK_BLOG_POSTS, MOCK_INTERVIEWS, MOCK_OFFERS } from '../lib/mockData.js'; import dotenv from 'dotenv';
dotenv.config({ path: '.env.local' });
dotenv.config();
const connectionString = process.env.DATABASE_URL!; const connectionString = process.env.DATABASE_URL!;
const pool = new pg.Pool({ connectionString }); const pool = new pg.Pool({ connectionString });
@@ -10,147 +13,149 @@ const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter }); const prisma = new PrismaClient({ adapter });
async function main() { async function main() {
console.log('🌱 Starting database seeding...'); console.log('🌱 Starting Core Database Seeding...');
const hashedPassword = await bcrypt.hash('afrohub2025', 10);
const hashedPassword = await bcrypt.hash('password123', 10); // 1. CLEANING DATA
// 1. Clean existing data
console.log('🧹 Cleaning existing data...'); console.log('🧹 Cleaning existing data...');
// Note: Only clean structure/system tables for a fresh core seed
await prisma.rating.deleteMany({});
await prisma.comment.deleteMany({}); await prisma.comment.deleteMany({});
await prisma.offer.deleteMany({}); await prisma.offer.deleteMany({});
await prisma.business.deleteMany({}); await prisma.business.deleteMany({});
await prisma.businessCategory.deleteMany({});
await prisma.event.deleteMany({});
await prisma.blogPost.deleteMany({}); await prisma.blogPost.deleteMany({});
await prisma.interview.deleteMany({}); await prisma.interview.deleteMany({});
await prisma.pricingPlan.deleteMany({});
await prisma.user.deleteMany({}); await prisma.user.deleteMany({});
await prisma.country.deleteMany({});
// 2. Create Mock Users // 2. COUNTRIES
console.log('👤 Creating users...'); console.log('🌍 Seeding countries (Core)...');
for (const country of COUNTRIES_DATA) {
await prisma.country.create({ data: country });
}
// Create Main Admin // 3. CATEGORIES
const admin = await prisma.user.create({ console.log('📂 Seeding business categories (Core)...');
for (const cat of CATEGORIES_DATA) {
await prisma.businessCategory.create({ data: { ...cat, isActive: true } });
}
// 4. PRICING PLANS
console.log('💳 Seeding pricing plans (Core)...');
for (const plan of PLANS_DATA) {
await prisma.pricingPlan.create({ data: plan });
}
// 5. ADMIN USER
console.log('👤 Creating main Admin...');
await prisma.user.create({
data: { data: {
name: 'Admin Afrohub', name: 'Admin Afrohub',
email: 'admin@afrohub.com', email: 'admin@afrohub.com',
password: hashedPassword, password: hashedPassword,
role: UserRole.ADMIN, role: UserRole.ADMIN,
}, }
}); });
// Create Users from mockup (we'll use their ownerId as IDs to match relationships) console.log('✅ CORE SEED COMPLETED SUCCESSFULLY!');
const usersToCreate = [ console.log('💡 Note: Use seed-30.ts and seed-events.ts for mockup data.');
{ id: 'u1', name: 'Jean-Marc Kouassi', email: 'jm.kouassi@example.com' }, }
{ id: 'u2', name: 'Aïssa Maïga', email: 'aissa@example.com' },
{ id: 'u3', name: 'Chidinma Okeke', email: 'chidinma@example.com' }, // --- CORE DATA ---
{ id: 'u4', name: 'Ousmane Diop', email: 'ousmane@example.com' },
{ id: 'u5', name: 'Marie-Claire Etoa', email: 'marie@example.com' }, const CATEGORIES_DATA = [
{ id: 'u6', name: 'David Ochieng', email: 'david@example.com' }, { name: "Technologie & IT", slug: "technologie-it" },
{ id: 'u7', name: 'Serge Mbemba', email: 'serge@example.com' }, { name: "Agriculture & Agrobusiness", slug: "agriculture-agrobusiness" },
{ id: 'u8', name: 'Fatimata Sylla', email: 'fatimata@example.com' }, { name: "Mode & Textile", slug: "mode-textile" },
{ name: "Cosmétique & Beauté", slug: "cosmetique-beaute" },
{ name: "Services aux entreprises", slug: "services-entreprises" },
{ name: "Restauration & Alimentation", slug: "restauration-alimentation" },
{ name: "Construction & BTP", slug: "construction-btp" },
{ name: "Éducation & Formation", slug: "education-formation" },
{ name: "Santé & Bien-être", slug: "sante-bien-etre" },
{ name: "Artisanat & Déco", slug: "artisanat-deco" },
{ name: "Tourisme & Loisirs", slug: "tourisme-loisirs" },
{ name: "Finance & Assurance", slug: "finance-assurance" }
]; ];
for (const u of usersToCreate) { const PLANS_DATA = [
await prisma.user.create({ {
data: { tier: Plan.STARTER,
id: u.id, name: 'Starter',
name: u.name, priceXOF: 'Gratuit',
email: u.email, yearlyPriceXOF: 'Gratuit',
password: hashedPassword, priceEUR: '0€',
role: UserRole.ENTREPRENEUR, yearlyPriceEUR: '0€',
description: 'Pour démarrer votre présence en ligne.',
features: ['Fiche entreprise basique', 'Visible dans la recherche', '1 Offre produit/service', 'Support par email'],
offerLimit: 1,
recommended: false,
color: 'gray'
}, },
}); {
} tier: Plan.BOOSTER,
name: 'Booster',
// 3. Create Businesses priceXOF: '3 500 FCFA',
console.log('🏢 Creating businesses...'); yearlyPriceXOF: '33 500 FCFA',
for (const b of MOCK_BUSINESSES) { priceEUR: '5€',
await prisma.business.create({ yearlyPriceEUR: '48€',
data: { description: "L'indispensable pour les entreprises en croissance.",
id: b.id, features: ['Tout du plan Starter', 'Badge "Vérifié" ✅', 'Jusqu\'à 10 Offres produits', 'Lien vers réseaux sociaux & Site Web', 'Statistiques de base (Vues)'],
name: b.name, offerLimit: 10,
slug: b.name.toLowerCase().replace(/[^a-z0-9]/g, '-'), recommended: true,
category: b.category, color: 'brand'
location: b.location,
description: b.description,
logoUrl: b.logoUrl,
videoUrl: b.videoUrl,
socialLinks: b.socialLinks as any,
contactEmail: b.contactEmail,
contactPhone: b.contactPhone,
verified: b.verified,
viewCount: b.viewCount,
rating: b.rating,
tags: b.tags,
isFeatured: b.isFeatured || false,
founderName: b.founderName,
founderImageUrl: b.founderImageUrl,
keyMetric: b.keyMetric,
ownerId: b.ownerId, // This matches the User IDs we just created
}, },
}); {
tier: Plan.EMPIRE,
name: 'Empire',
priceXOF: '13 000 FCFA',
yearlyPriceXOF: '125 000 FCFA',
priceEUR: '20€',
yearlyPriceEUR: '192€',
description: 'Dominez votre marché avec une visibilité maximale.',
features: ['Tout du plan Booster', 'Badge "Recommandé" 🏆', 'Offres illimitées', 'Intégration vidéo Youtube', 'Interview écrite sur le Blog', 'Support prioritaire WhatsApp'],
offerLimit: 1000,
recommended: false,
color: 'amber'
} }
];
// 4. Create Offers const COUNTRIES_DATA = [
console.log('🏷️ Creating offers...'); { name: "Bénin", code: "BJ", flag: "🇧🇯" },
for (const o of MOCK_OFFERS) { { name: "Burkina Faso", code: "BF", flag: "🇧🇫" },
await prisma.offer.create({ { name: "Cameroun", code: "CM", flag: "🇨🇲" },
data: { { name: "Congo-Kinshasa", code: "CD", flag: "🇨🇩" },
id: o.id, { name: "Côte d'Ivoire", code: "CI", flag: "🇨🇮" },
businessId: o.businessId, { name: "France", code: "FR", flag: "🇫🇷" },
title: o.title, { name: "Belgique", code: "BE", flag: "🇧🇪" },
type: o.type as OfferType, { name: "Suisse", code: "CH", flag: "🇨🇭" },
price: o.price, { name: "Luxembourg", code: "LU", flag: "🇱🇺" },
currency: o.currency, { name: "Allemagne", code: "DE", flag: "🇩🇪" },
description: o.description, { name: "Italie", code: "IT", flag: "🇮🇹" },
imageUrl: o.imageUrl, { name: "Espagne", code: "ES", flag: "🇪🇸" },
active: o.active, { name: "Portugal", code: "PT", flag: "🇵🇹" },
}, { name: "Royaume-Uni", code: "GB", flag: "🇬🇧" },
}); { name: "Pays-Bas", code: "NL", flag: "🇳🇱" },
} { name: "Gabon", code: "GA", flag: "🇬🇦" },
{ name: "Ghana", code: "GH", flag: "🇬🇭" },
// 5. Create Blog Posts { name: "Guinée", code: "GN", flag: "🇬🇳" },
console.log('✍️ Creating blog posts...'); { name: "Mali", code: "ML", flag: "🇲🇱" },
for (const p of MOCK_BLOG_POSTS) { { name: "Maroc", code: "MA", flag: "🇲🇦" },
await prisma.blogPost.create({ { name: "Niger", code: "NE", flag: "🇳🇪" },
data: { { name: "Nigéria", code: "NG", flag: "🇳🇬" },
id: p.id, { name: "Sénégal", code: "SN", flag: "🇸🇳" },
title: p.title, { name: "Togo", code: "TG", flag: "🇹🇬" }
excerpt: p.excerpt, ];
content: p.content,
author: p.author,
imageUrl: p.imageUrl,
},
});
}
// 6. Create Interviews
console.log('🎙️ Creating interviews...');
for (const i of MOCK_INTERVIEWS) {
await prisma.interview.create({
data: {
id: i.id,
title: i.title,
guestName: i.guestName,
companyName: i.companyName,
role: i.role,
type: i.type as InterviewType,
thumbnailUrl: i.thumbnailUrl,
videoUrl: i.videoUrl,
content: i.content,
excerpt: i.excerpt,
duration: i.duration,
},
});
}
console.log('✅ Seeding completed successfully!');
}
main() main()
.catch((e) => { .catch((e) => {
console.error('❌ Seeding failed:');
console.error(e); console.error(e);
process.exit(1); process.exit(1);
}) })
.finally(async () => { .finally(async () => {
await prisma.$disconnect(); await prisma.$disconnect();
await pool.end();
}); });

View File

@@ -96,11 +96,13 @@ export interface Offer {
export interface BlogPost { export interface BlogPost {
id: string; id: string;
title: string; title: string;
slug?: string;
excerpt: string; excerpt: string;
content: string; content: string;
author: string; author: string;
date: string; date: string;
imageUrl: string; imageUrl: string;
tags?: string[];
} }
export enum InterviewType { export enum InterviewType {
@@ -134,4 +136,14 @@ export interface Rating {
user?: User; user?: User;
createdAt: string; createdAt: string;
} }
export interface Event {
id: string;
slug?: string;
title: string;
description: string;
date: string;
location: string;
thumbnailUrl: string;
link?: string;
tags?: string[];
}