feat: implement core platform features including business directory, admin dashboard, authentication, and API infrastructure
Some checks failed
Build and Push App / build (push) Failing after 16m2s

This commit is contained in:
2026-04-18 22:10:19 +02:00
parent 6ec1a3ae84
commit 3e2063e4fa
89 changed files with 4405 additions and 967 deletions

View File

@@ -21,11 +21,14 @@ model User {
phone String?
isSuspended Boolean @default(false)
suspensionReason String?
countryId String?
businesses Business?
comments Comment[]
conversations ConversationParticipant[]
sentMessages Message[]
reports MessageReport[]
ratings Rating[]
country Country? @relation(fields: [countryId], references: [id])
}
model Business {
@@ -42,6 +45,7 @@ model Business {
verified Boolean @default(false)
viewCount Int @default(0)
rating Float @default(0.0)
ratingCount Int @default(0)
tags String[]
isFeatured Boolean @default(false)
founderName String?
@@ -58,10 +62,41 @@ model Business {
websiteUrl String?
isSuspended Boolean @default(false)
suspensionReason String?
plan Plan @default(STARTER)
city String?
countryId String?
country Country? @relation(fields: [countryId], references: [id])
owner User @relation(fields: [ownerId], references: [id])
comments Comment[]
conversations Conversation[]
offers Offer[]
ratings Rating[]
}
model Country {
id String @id @default(uuid())
name String @unique
code String @unique
flag String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
businesses Business[]
users User[]
}
model PricingPlan {
id String @id @default(uuid())
tier Plan @unique
name String
priceXOF String
priceEUR String
description String
features String[]
offerLimit Int @default(1)
recommended Boolean @default(false)
color String @default("gray")
updatedAt DateTime @updatedAt
}
model Offer {
@@ -127,6 +162,31 @@ model AnalyticsEvent {
value Float?
metadata Json?
createdAt DateTime @default(now())
browser String?
city String?
country String?
device String?
ip String?
os String?
referrer String?
userId String?
}
model Rating {
id String @id @default(uuid())
value Int
userId String
businessId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
comment String?
reply String?
replyAt DateTime?
status RatingStatus @default(PENDING)
business Business @relation(fields: [businessId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, businessId])
}
model Conversation {
@@ -173,6 +233,33 @@ model MessageReport {
reporter User @relation(fields: [reporterId], references: [id], onDelete: Cascade)
}
model SiteSetting {
id String @id @default("singleton")
siteName String @default("Afrohub")
siteSlogan String @default("La plateforme de référence pour l'entrepreneuriat africain.")
contactEmail String @default("support@afrohub.com")
contactPhone String? @default("+225 00 00 00 00 00")
address String? @default("Abidjan, Côte d'Ivoire")
facebookUrl String?
twitterUrl String?
instagramUrl String?
linkedinUrl String?
footerText String? @default("© 2025 Afrohub. Tous droits réservés.")
updatedAt DateTime @updatedAt
}
enum Plan {
STARTER
BOOSTER
EMPIRE
}
enum RatingStatus {
PENDING
APPROVED
REJECTED
}
enum ReportStatus {
PENDING
RESOLVED
@@ -194,18 +281,10 @@ enum InterviewType {
VIDEO
ARTICLE
}
model SiteSetting {
id String @id @default("singleton")
siteName String @default("Afropreunariat")
siteSlogan String @default("La plateforme de référence pour l'entrepreneuriat africain.")
contactEmail String @default("support@afropreunariat.com")
contactPhone String? @default("+225 00 00 00 00 00")
address String? @default("Abidjan, Côte d'Ivoire")
facebookUrl String?
twitterUrl String?
instagramUrl String?
linkedinUrl String?
footerText String? @default("© 2025 Afropreunariat. Tous droits réservés.")
updatedAt DateTime @updatedAt
model LegalDocument {
id String @id @default(uuid())
type String @unique // 'CGU' or 'CGV'
title String
content String @db.Text
updatedAt DateTime @updatedAt
}

123
prisma/seed-30.ts Normal file
View File

@@ -0,0 +1,123 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import pg from 'pg';
import bcrypt from 'bcryptjs';
import dotenv from 'dotenv';
dotenv.config({ path: '.env.local' });
dotenv.config(); // Fallback to .env for other vars if needed
const connectionString = process.env.DATABASE_URL!;
console.log(`Connecting to: ${connectionString.split('@')[1] ? '***@' + connectionString.split('@')[1] : connectionString}`);
const pool = new pg.Pool({ connectionString });
const adapter = new PrismaPg(pool);
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 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 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));
async function seed() {
console.log('--- Démarrage du seeding robuste (30 entrepreneurs) ---');
const hashedPassword = await bcrypt.hash('afrohub2025', 10);
let successCount = 0;
for (let i = 0; i < 30; i++) {
const name = NAMES[i % NAMES.length] + (i > 29 ? ` ${i}` : '');
const email = `entrepreneur${i + 1}@afrohub-test.com`;
const bizName = BIZ_NAMES[i % BIZ_NAMES.length];
const category = CATEGORIES[i % CATEGORIES.length];
const location = CITIES[i % CITIES.length];
const slug = bizName.toLowerCase().replace(/ /g, '-').replace(/[^a-z0-9-]/g, '') + `-${i + 1}`;
try {
// Create or Update User
const user = await prisma.user.upsert({
where: { email },
update: { role: 'ENTREPRENEUR' },
create: {
name,
email,
password: hashedPassword,
role: 'ENTREPRENEUR',
location,
avatar: `https://i.pravatar.cc/150?u=${email}`
}
});
// Create or Update Business
await prisma.business.upsert({
where: { ownerId: user.id },
update: {
isActive: true,
verified: true,
isFeatured: i < 8
},
create: {
name: bizName,
category,
location,
description: `Société leader dans le domaine ${category}. Basés à ${location}, nous œuvrons pour le rayonnement de l'expertise africaine.`,
logoUrl: `https://picsum.photos/200/200?random=${i + 200}`,
contactEmail: email,
contactPhone: `+225 00 ${i}${i} ${i}${i} ${i}${i}`,
slug,
isActive: true,
verified: true,
isFeatured: i < 8,
ownerId: user.id,
tags: [category.split(' ')[0], 'Expertise', 'Continent'],
viewCount: Math.floor(Math.random() * 500)
}
});
successCount++;
process.stdout.write(`\rProgress: ${successCount}/30`);
await wait(100);
} 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\n✅ Mission accomplie : ${successCount}/30 boutiques actives.`);
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);
});

97
prisma/seed-plans.ts Normal file
View File

@@ -0,0 +1,97 @@
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

@@ -29,8 +29,8 @@ async function main() {
// Create Main Admin
const admin = await prisma.user.create({
data: {
name: 'Admin Afropreunariat',
email: 'admin@afropreunariat.com',
name: 'Admin Afrohub',
email: 'admin@afrohub.com',
password: hashedPassword,
role: UserRole.ADMIN,
},