This commit is contained in:
2026-04-12 22:51:57 +02:00
parent a5ccf2047b
commit c26c3b9485
37 changed files with 934 additions and 34 deletions

2
.env
View File

@@ -1,5 +1,5 @@
# Connexion locale via le Docker Compose fourni : # Connexion locale via le Docker Compose fourni :
DATABASE_URL="postgresql://admin:adminpassword@192.168.1.25:5432/afropreunariat_dev?schema=public" DATABASE_URL="postgresql://admin:adminpassword@localhost:5432/afropreunariat_dev?schema=public"
# Exemple avec Supabase ou Neon (si base distante) # Exemple avec Supabase ou Neon (si base distante)
# DATABASE_URL="postgresql://postgres:[MOT_DE_PASSE]@db.[ID_PROJET].supabase.co:5432/postgres" # DATABASE_URL="postgresql://postgres:[MOT_DE_PASSE]@db.[ID_PROJET].supabase.co:5432/postgres"

View File

View File

@@ -4,8 +4,8 @@
"dynamicRoutes": {}, "dynamicRoutes": {},
"notFoundRoutes": [], "notFoundRoutes": [],
"preview": { "preview": {
"previewModeId": "e4f2fdc8a0f279696cd4c94409e5984e", "previewModeId": "7b809f863d91f00050e5c8c0e4bada56",
"previewModeSigningKey": "be32103ec8267fdecdccfe457d2a03999c8e439d583c9ebfb243182a01ef67b7", "previewModeSigningKey": "5d82ed0a1b256e09883f544d3ee37c35354f01b40d1dc9660026db8c9b02efd7",
"previewModeEncryptionKey": "034717334772a2081e323378fa45aabe5c8553ab7b0dcf59547cc44e534605ff" "previewModeEncryptionKey": "f7bb294866ff13b369a31ff5fb86e4f6ec887e7444d7550d13f24af3eb7c12db"
} }
} }

View File

@@ -1,10 +1,5 @@
{ {
"/afrolife/page": "app/afrolife/page.js",
"/annuaire/page": "app/annuaire/page.js",
"/api/analytics/route": "app/api/analytics/route.js", "/api/analytics/route": "app/api/analytics/route.js",
"/api/businesses/route": "app/api/businesses/route.js", "/api/businesses/route": "app/api/businesses/route.js",
"/blog/[id]/page": "app/blog/[id]/page.js",
"/blog/page": "app/blog/page.js",
"/login/page": "app/login/page.js",
"/page": "app/page.js" "/page": "app/page.js"
} }

View File

@@ -4,7 +4,7 @@ R.c("server/chunks/ssr/[root-of-the-server]__06f207ab._.js")
R.c("server/chunks/ssr/a30ac_next_dist_2984d70a._.js") R.c("server/chunks/ssr/a30ac_next_dist_2984d70a._.js")
R.c("server/chunks/ssr/[externals]__7f148858._.js") R.c("server/chunks/ssr/[externals]__7f148858._.js")
R.c("server/chunks/ssr/a30ac_next_dist_client_components_builtin_global-error_c1b18c9b.js") R.c("server/chunks/ssr/a30ac_next_dist_client_components_builtin_global-error_c1b18c9b.js")
R.c("server/chunks/ssr/Documents_00 - projet_afropreunariat_a9b9fee6._.js") R.c("server/chunks/ssr/[root-of-the-server]__4128d26c._.js")
R.c("server/chunks/ssr/a30ac_next_dist_client_components_a7eb5008._.js") R.c("server/chunks/ssr/a30ac_next_dist_client_components_a7eb5008._.js")
R.c("server/chunks/ssr/a30ac_next_dist_client_components_builtin_forbidden_24897189.js") R.c("server/chunks/ssr/a30ac_next_dist_client_components_builtin_forbidden_24897189.js")
R.c("server/chunks/ssr/a30ac_next_dist_client_components_builtin_unauthorized_d9e897c8.js") R.c("server/chunks/ssr/a30ac_next_dist_client_components_builtin_unauthorized_d9e897c8.js")

File diff suppressed because one or more lines are too long

View File

@@ -4,7 +4,7 @@ R.c("server/chunks/ssr/[root-of-the-server]__9a704c34._.js")
R.c("server/chunks/ssr/a30ac_next_dist_2984d70a._.js") R.c("server/chunks/ssr/a30ac_next_dist_2984d70a._.js")
R.c("server/chunks/ssr/[externals]__7f148858._.js") R.c("server/chunks/ssr/[externals]__7f148858._.js")
R.c("server/chunks/ssr/a30ac_next_dist_client_components_builtin_global-error_c1b18c9b.js") R.c("server/chunks/ssr/a30ac_next_dist_client_components_builtin_global-error_c1b18c9b.js")
R.c("server/chunks/ssr/Documents_00 - projet_afropreunariat_a9b9fee6._.js") R.c("server/chunks/ssr/[root-of-the-server]__4128d26c._.js")
R.c("server/chunks/ssr/a30ac_next_dist_client_components_a7eb5008._.js") R.c("server/chunks/ssr/a30ac_next_dist_client_components_a7eb5008._.js")
R.c("server/chunks/ssr/a30ac_next_dist_client_components_builtin_forbidden_24897189.js") R.c("server/chunks/ssr/a30ac_next_dist_client_components_builtin_forbidden_24897189.js")
R.c("server/chunks/ssr/a30ac_next_dist_client_components_builtin_unauthorized_d9e897c8.js") R.c("server/chunks/ssr/a30ac_next_dist_client_components_builtin_unauthorized_d9e897c8.js")

File diff suppressed because one or more lines are too long

View File

@@ -16600,7 +16600,7 @@ var __turbopack_async_dependencies__ = __turbopack_handle_async_dependencies__([
; ;
// We inject the nextConfigOutput here so that we can use them in the route // We inject the nextConfigOutput here so that we can use them in the route
// module. // module.
const nextConfigOutput = ""; const nextConfigOutput = "standalone";
const routeModule = new __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$afropreunariat$2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$route$2d$modules$2f$app$2d$route$2f$module$2e$compiled$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__["AppRouteRouteModule"]({ const routeModule = new __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$afropreunariat$2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$route$2d$modules$2f$app$2d$route$2f$module$2e$compiled$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__["AppRouteRouteModule"]({
definition: { definition: {
kind: __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$afropreunariat$2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$route$2d$kind$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__["RouteKind"].APP_ROUTE, kind: __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$afropreunariat$2f$node_modules$2f$next$2f$dist$2f$esm$2f$server$2f$route$2d$kind$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__["RouteKind"].APP_ROUTE,

View File

@@ -6177,7 +6177,7 @@ var EntryStatus = /*#__PURE__*/ function(EntryStatus) {
EntryStatus[EntryStatus["Rejected"] = 3] = "Rejected"; EntryStatus[EntryStatus["Rejected"] = 3] = "Rejected";
return EntryStatus; return EntryStatus;
}({}); }({});
const isOutputExportMode = ("TURBOPACK compile-time value", "development") === 'production' && ("TURBOPACK compile-time value", void 0) === 'export'; const isOutputExportMode = ("TURBOPACK compile-time value", "development") === 'production' && ("TURBOPACK compile-time value", "standalone") === 'export';
const MetadataOnlyRequestTree = [ const MetadataOnlyRequestTree = [
'', '',
{}, {},

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"nzqFbYTCPnKfhYIIKDYG+xnFLI0t6Og2KV+8qBSJfGk=\"\n}" self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"UBny3v2My8FSswF+XKlwEFB68MjTsPZoNJbBJmHj6O8=\"\n}"

View File

@@ -1,5 +1,5 @@
{ {
"node": {}, "node": {},
"edge": {}, "edge": {},
"encryptionKey": "nzqFbYTCPnKfhYIIKDYG+xnFLI0t6Og2KV+8qBSJfGk=" "encryptionKey": "UBny3v2My8FSswF+XKlwEFB68MjTsPZoNJbBJmHj6O8="
} }

View File

@@ -66,6 +66,7 @@
--tw-backdrop-saturate: initial; --tw-backdrop-saturate: initial;
--tw-backdrop-sepia: initial; --tw-backdrop-sepia: initial;
--tw-duration: initial; --tw-duration: initial;
--tw-ease: initial;
--tw-content: ""; --tw-content: "";
} }
} }
@@ -166,6 +167,7 @@
--container-2xl: 42rem; --container-2xl: 42rem;
--container-3xl: 48rem; --container-3xl: 48rem;
--container-4xl: 56rem; --container-4xl: 56rem;
--container-5xl: 64rem;
--container-6xl: 72rem; --container-6xl: 72rem;
--container-7xl: 80rem; --container-7xl: 80rem;
--text-xs: .75rem; --text-xs: .75rem;
@@ -204,6 +206,7 @@
--radius-xl: .75rem; --radius-xl: .75rem;
--radius-2xl: 1rem; --radius-2xl: 1rem;
--radius-3xl: 1.5rem; --radius-3xl: 1.5rem;
--ease-in-out: cubic-bezier(.4, 0, .2, 1);
--animate-spin: spin 1s linear infinite; --animate-spin: spin 1s linear infinite;
--animate-ping: ping 1s cubic-bezier(0, 0, .2, 1) infinite; --animate-ping: ping 1s cubic-bezier(0, 0, .2, 1) infinite;
--animate-pulse: pulse 2s cubic-bezier(.4, 0, .6, 1) infinite; --animate-pulse: pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;
@@ -570,6 +573,10 @@
@layer components; @layer components;
@layer utilities { @layer utilities {
.pointer-events-auto {
pointer-events: auto;
}
.pointer-events-none { .pointer-events-none {
pointer-events: none; pointer-events: none;
} }
@@ -654,6 +661,10 @@
top: calc(var(--spacing) * 3); top: calc(var(--spacing) * 3);
} }
.top-3\.5 {
top: calc(var(--spacing) * 3.5);
}
.top-4 { .top-4 {
top: calc(var(--spacing) * 4); top: calc(var(--spacing) * 4);
} }
@@ -730,6 +741,10 @@
z-index: 50; z-index: 50;
} }
.z-\[100\] {
z-index: 100;
}
.container { .container {
width: 100%; width: 100%;
} }
@@ -1892,6 +1907,10 @@
max-width: var(--container-4xl); max-width: var(--container-4xl);
} }
.max-w-5xl {
max-width: var(--container-5xl);
}
.max-w-6xl { .max-w-6xl {
max-width: var(--container-6xl); max-width: var(--container-6xl);
} }
@@ -2376,6 +2395,10 @@
border-color: var(--color-gray-300); border-color: var(--color-gray-300);
} }
.border-gray-700 {
border-color: var(--color-gray-700);
}
.border-gray-800 { .border-gray-800 {
border-color: var(--color-gray-800); border-color: var(--color-gray-800);
} }
@@ -2608,6 +2631,16 @@
background-color: var(--color-brand-500); background-color: var(--color-brand-500);
} }
.bg-brand-500\/10 {
background-color: #f973161a;
}
@supports (color: color-mix(in lab, red, red)) {
.bg-brand-500\/10 {
background-color: color-mix(in oklab, var(--color-brand-500) 10%, transparent);
}
}
.bg-brand-600 { .bg-brand-600 {
background-color: var(--color-brand-600); background-color: var(--color-brand-600);
} }
@@ -2880,6 +2913,10 @@
} }
} }
.bg-slate-950 {
background-color: var(--color-slate-950);
}
.bg-slate-950\/20 { .bg-slate-950\/20 {
background-color: #02061833; background-color: #02061833;
} }
@@ -3098,6 +3135,10 @@
padding: calc(var(--spacing) * 10); padding: calc(var(--spacing) * 10);
} }
.p-20 {
padding: calc(var(--spacing) * 20);
}
.px-1 { .px-1 {
padding-inline: calc(var(--spacing) * 1); padding-inline: calc(var(--spacing) * 1);
} }
@@ -3118,6 +3159,10 @@
padding-inline: calc(var(--spacing) * 4); padding-inline: calc(var(--spacing) * 4);
} }
.px-5 {
padding-inline: calc(var(--spacing) * 5);
}
.px-6 { .px-6 {
padding-inline: calc(var(--spacing) * 6); padding-inline: calc(var(--spacing) * 6);
} }
@@ -3530,6 +3575,10 @@
color: var(--color-indigo-400); color: var(--color-indigo-400);
} }
.text-indigo-500 {
color: var(--color-indigo-500);
}
.text-indigo-600 { .text-indigo-600 {
color: var(--color-indigo-600); color: var(--color-indigo-600);
} }
@@ -3802,6 +3851,16 @@
} }
} }
.shadow-brand-500\/20 {
--tw-shadow-color: #f9731633;
}
@supports (color: color-mix(in lab, red, red)) {
.shadow-brand-500\/20 {
--tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-brand-500) 20%, transparent) var(--tw-shadow-alpha), transparent);
}
}
.shadow-gray-200\/50 { .shadow-gray-200\/50 {
--tw-shadow-color: #e5e7eb80; --tw-shadow-color: #e5e7eb80;
} }
@@ -3942,6 +4001,11 @@
transition-duration: .7s; transition-duration: .7s;
} }
.ease-in-out {
--tw-ease: var(--ease-in-out);
transition-timing-function: var(--ease-in-out);
}
.outline-none { .outline-none {
--tw-outline-style: none; --tw-outline-style: none;
outline-style: none; outline-style: none;
@@ -4145,6 +4209,12 @@
} }
} }
@media (hover: hover) {
.hover\:border-gray-500:hover {
border-color: var(--color-gray-500);
}
}
@media (hover: hover) { @media (hover: hover) {
.hover\:border-slate-500:hover { .hover\:border-slate-500:hover {
border-color: var(--color-slate-500); border-color: var(--color-slate-500);
@@ -4187,6 +4257,12 @@
} }
} }
@media (hover: hover) {
.hover\:bg-brand-600:hover {
background-color: var(--color-brand-600);
}
}
@media (hover: hover) { @media (hover: hover) {
.hover\:bg-brand-700:hover { .hover\:bg-brand-700:hover {
background-color: var(--color-brand-700); background-color: var(--color-brand-700);
@@ -4229,6 +4305,12 @@
} }
} }
@media (hover: hover) {
.hover\:bg-indigo-700:hover {
background-color: var(--color-indigo-700);
}
}
@media (hover: hover) { @media (hover: hover) {
.hover\:bg-orange-500:hover { .hover\:bg-orange-500:hover {
background-color: var(--color-orange-500); background-color: var(--color-orange-500);
@@ -4545,6 +4627,10 @@
cursor: not-allowed; cursor: not-allowed;
} }
.disabled\:bg-slate-700:disabled {
background-color: var(--color-slate-700);
}
.disabled\:opacity-50:disabled { .disabled\:opacity-50:disabled {
opacity: .5; opacity: .5;
} }
@@ -4860,6 +4946,12 @@
} }
} }
@media (min-width: 48rem) {
.md\:col-span-2 {
grid-column: span 2 / span 2;
}
}
@media (min-width: 48rem) { @media (min-width: 48rem) {
.md\:-mt-24 { .md\:-mt-24 {
margin-top: calc(var(--spacing) * -24); margin-top: calc(var(--spacing) * -24);
@@ -4938,6 +5030,18 @@
} }
} }
@media (min-width: 48rem) {
.md\:w-auto {
width: auto;
}
}
@media (min-width: 48rem) {
.md\:flex-none {
flex: none;
}
}
@media (min-width: 48rem) { @media (min-width: 48rem) {
.md\:grid-cols-2 { .md\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -4988,6 +5092,12 @@
} }
} }
@media (min-width: 48rem) {
.md\:p-6 {
padding: calc(var(--spacing) * 6);
}
}
@media (min-width: 48rem) { @media (min-width: 48rem) {
.md\:p-8 { .md\:p-8 {
padding: calc(var(--spacing) * 8); padding: calc(var(--spacing) * 8);
@@ -5461,6 +5571,11 @@ body {
inherits: false inherits: false
} }
@property --tw-ease {
syntax: "*";
inherits: false
}
@property --tw-content { @property --tw-content {
syntax: "*"; syntax: "*";
inherits: false; inherits: false;

View File

@@ -1,7 +1,7 @@
(globalThis.TURBOPACK_CHUNK_LISTS || (globalThis.TURBOPACK_CHUNK_LISTS = [])).push({ (globalThis.TURBOPACK_CHUNK_LISTS || (globalThis.TURBOPACK_CHUNK_LISTS = [])).push({
script: typeof document === "object" ? document.currentScript : undefined, script: typeof document === "object" ? document.currentScript : undefined,
chunks: [ chunks: [
"static/chunks/Documents_00 - projet_afropreunariat_8345028d._.js", "static/chunks/Documents_00 - projet_afropreunariat_c93222d5._.js",
"static/chunks/Documents_00 - projet_afropreunariat_app_globals_062f25c8.css" "static/chunks/Documents_00 - projet_afropreunariat_app_globals_062f25c8.css"
], ],
source: "dynamic" source: "dynamic"

View File

@@ -6372,7 +6372,7 @@ var EntryStatus = /*#__PURE__*/ function(EntryStatus) {
EntryStatus[EntryStatus["Rejected"] = 3] = "Rejected"; EntryStatus[EntryStatus["Rejected"] = 3] = "Rejected";
return EntryStatus; return EntryStatus;
}({}); }({});
const isOutputExportMode = ("TURBOPACK compile-time value", "development") === 'production' && ("TURBOPACK compile-time value", void 0) === 'export'; const isOutputExportMode = ("TURBOPACK compile-time value", "development") === 'production' && ("TURBOPACK compile-time value", "standalone") === 'export';
const MetadataOnlyRequestTree = [ const MetadataOnlyRequestTree = [
'', '',
{}, {},

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
// This file is generated automatically by Next.js // This file is generated automatically by Next.js
// Do not edit this file manually // Do not edit this file manually
type AppRoutes = "/" | "/afrolife" | "/afrolife/[id]" | "/annuaire" | "/annuaire/[id]" | "/blog" | "/blog/[id]" | "/dashboard" | "/login" | "/register" | "/subscription" | "/suspended" type AppRoutes = "/" | "/afrolife" | "/afrolife/[id]" | "/annuaire" | "/annuaire/[id]" | "/blog" | "/blog/[id]" | "/cgu" | "/cgv" | "/dashboard" | "/login" | "/register" | "/subscription" | "/suspended"
type AppRouteHandlerRoutes = "/api/admin/reports" | "/api/admin/reports/[id]" | "/api/analytics" | "/api/analytics/businesses/[id]" | "/api/auth/login" | "/api/auth/register" | "/api/blog" | "/api/blog/[id]" | "/api/businesses" | "/api/businesses/[id]" | "/api/businesses/[id]/view" | "/api/businesses/me" | "/api/conversations" | "/api/conversations/[id]" | "/api/conversations/[id]/archive" | "/api/gemini" | "/api/interviews" | "/api/interviews/[id]" | "/api/messages" | "/api/messages/[id]/report" | "/api/messages/unread" | "/api/offers" | "/api/offers/[id]" | "/api/users" | "/api/users/me" | "/api/users/profile" type AppRouteHandlerRoutes = "/api/admin/reports" | "/api/admin/reports/[id]" | "/api/analytics" | "/api/analytics/businesses/[id]" | "/api/auth/login" | "/api/auth/register" | "/api/blog" | "/api/blog/[id]" | "/api/businesses" | "/api/businesses/[id]" | "/api/businesses/[id]/view" | "/api/businesses/me" | "/api/conversations" | "/api/conversations/[id]" | "/api/conversations/[id]/archive" | "/api/gemini" | "/api/interviews" | "/api/interviews/[id]" | "/api/messages" | "/api/messages/[id]/report" | "/api/messages/unread" | "/api/offers" | "/api/offers/[id]" | "/api/users" | "/api/users/me" | "/api/users/profile"
type PageRoutes = never type PageRoutes = never
type LayoutRoutes = "/" type LayoutRoutes = "/"
@@ -44,6 +44,8 @@ interface ParamMap {
"/api/users/profile": {} "/api/users/profile": {}
"/blog": {} "/blog": {}
"/blog/[id]": { "id": string; } "/blog/[id]": { "id": string; }
"/cgu": {}
"/cgv": {}
"/dashboard": {} "/dashboard": {}
"/login": {} "/login": {}
"/register": {} "/register": {}

View File

@@ -101,6 +101,24 @@ type RouteHandlerConfig<Route extends AppRouteHandlerRoutes = AppRouteHandlerRou
type __Unused = __Check type __Unused = __Check
} }
// Validate ../../../app/cgu/page.tsx
{
type __IsExpected<Specific extends AppPageConfig<"/cgu">> = Specific
const handler = {} as typeof import("../../../app/cgu/page.js")
type __Check = __IsExpected<typeof handler>
// @ts-ignore
type __Unused = __Check
}
// Validate ../../../app/cgv/page.tsx
{
type __IsExpected<Specific extends AppPageConfig<"/cgv">> = Specific
const handler = {} as typeof import("../../../app/cgv/page.js")
type __Check = __IsExpected<typeof handler>
// @ts-ignore
type __Unused = __Check
}
// Validate ../../../app/dashboard/page.tsx // Validate ../../../app/dashboard/page.tsx
{ {
type __IsExpected<Specific extends AppPageConfig<"/dashboard">> = Specific type __IsExpected<Specific extends AppPageConfig<"/dashboard">> = Specific

55
Dockerfile Normal file
View File

@@ -0,0 +1,55 @@
# Base image
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies
COPY package.json package-lock.json ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Generate Prisma Client
RUN npx prisma generate
# Build Next.js
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Install openssl for Prisma
RUN apk add --no-cache openssl
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder /app/prisma ./prisma
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
# Run migrations and start the server
CMD ["sh", "-c", "npx prisma db push && node server.js"]

55
admin/Dockerfile Normal file
View File

@@ -0,0 +1,55 @@
# Base image
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies
COPY package.json package-lock.json ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Generate Prisma Client
RUN npx prisma generate
# Build Next.js
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
# Install openssl for Prisma
RUN apk add --no-cache openssl
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder /app/prisma ./prisma
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3001
ENV PORT 3001
ENV HOSTNAME "0.0.0.0"
# Run migrations and start the server
CMD ["sh", "-c", "npx prisma db push && node server.js"]

View File

@@ -2,6 +2,7 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
/* config options here */ /* config options here */
output: 'standalone',
}; };
export default nextConfig; export default nextConfig;

View File

@@ -125,6 +125,21 @@ enum InterviewType {
ARTICLE 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 Interview { model Interview {
id String @id @default(uuid()) id String @id @default(uuid())
title String title String

View File

@@ -0,0 +1,53 @@
"use server";
import { prisma } from '@/lib/prisma';
import { revalidatePath } from 'next/cache';
export async function getSiteSettings() {
try {
const settings = await prisma.siteSetting.findUnique({
where: { id: 'singleton' }
});
// Default fallback values if not initialized
const defaultSettings = {
siteName: "Afropreunariat",
siteSlogan: "La plateforme de référence pour l'entrepreneuriat africain.",
contactEmail: "support@afropreunariat.com",
contactPhone: "+225 00 00 00 00 00",
address: "Abidjan, Côte d'Ivoire",
facebookUrl: "",
twitterUrl: "",
instagramUrl: "",
linkedinUrl: "",
footerText: "© 2025 Afropreunariat. Tous droits réservés."
};
if (!settings) return defaultSettings;
return settings;
} catch (error) {
console.error("Failed to fetch settings:", error);
return null;
}
}
export async function updateSiteSettings(data: any) {
try {
const settings = await prisma.siteSetting.upsert({
where: { id: 'singleton' },
update: data,
create: {
id: 'singleton',
...data
}
});
revalidatePath('/settings');
revalidatePath('/', 'layout'); // Revalidate all public pages because footer/header might use it
return { success: true, settings };
} catch (error) {
console.error("Failed to update settings:", error);
return { success: false, error: "Erreur lors de la mise à jour des paramètres" };
}
}

View File

@@ -0,0 +1,218 @@
"use client";
import React, { useState, useEffect, useTransition } from 'react';
import { Save, Globe, Mail, Phone, MapPin, Share2, Link as LinkIcon, Loader2 } from 'lucide-react';
import { getSiteSettings, updateSiteSettings } from '@/app/actions/settings';
import { toast } from 'react-hot-toast';
export default function SettingsPage() {
const [isPending, startTransition] = useTransition();
const [settings, setSettings] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadData() {
const data = await getSiteSettings();
setSettings(data);
setLoading(false);
}
loadData();
}, []);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = {
siteName: formData.get('siteName'),
siteSlogan: formData.get('siteSlogan'),
contactEmail: formData.get('contactEmail'),
contactPhone: formData.get('contactPhone'),
address: formData.get('address'),
facebookUrl: formData.get('facebookUrl'),
twitterUrl: formData.get('twitterUrl'),
instagramUrl: formData.get('instagramUrl'),
linkedinUrl: formData.get('linkedinUrl'),
footerText: formData.get('footerText'),
};
startTransition(async () => {
const result = await updateSiteSettings(data);
if (result.success) {
toast.success("Paramètres mis à jour avec succès");
} else {
toast.error("Erreur lors de la mise à jour");
}
});
};
if (loading) {
return (
<div className="flex items-center justify-center p-20">
<Loader2 className="w-8 h-8 animate-spin text-indigo-500" />
</div>
);
}
return (
<div className="p-8 max-w-5xl mx-auto">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-bold text-white">Configuration du Site</h1>
<p className="text-slate-400">Gérez les informations globales et les coordonnées de contact du site principal.</p>
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Section Générale */}
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Globe className="w-5 h-5 text-indigo-400" />
<h2 className="font-semibold text-white">Informations Générales</h2>
</div>
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Nom du site</label>
<input
name="siteName"
defaultValue={settings?.siteName}
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Slogan du site</label>
<input
name="siteSlogan"
defaultValue={settings?.siteSlogan}
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
<div className="md:col-span-2 space-y-2">
<label className="text-sm font-medium text-slate-400">Texte du pied de page (Footer)</label>
<input
name="footerText"
defaultValue={settings?.footerText}
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
</div>
</div>
{/* Section Contact */}
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Mail className="w-5 h-5 text-emerald-400" />
<h2 className="font-semibold text-white">Coordonnées de Contact</h2>
</div>
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Email de support</label>
<div className="relative">
<Mail className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="contactEmail"
defaultValue={settings?.contactEmail}
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Téléphone de contact</label>
<div className="relative">
<Phone className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="contactPhone"
defaultValue={settings?.contactPhone}
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
</div>
<div className="md:col-span-2 space-y-2">
<label className="text-sm font-medium text-slate-400">Adresse physique</label>
<div className="relative">
<MapPin className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="address"
defaultValue={settings?.address}
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
</div>
</div>
</div>
{/* Section Réseaux Sociaux */}
<div className="bg-slate-900/50 border border-slate-700 rounded-2xl overflow-hidden">
<div className="p-4 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
<Share2 className="w-5 h-5 text-blue-400" />
<h2 className="font-semibold text-white">Réseaux Sociaux</h2>
</div>
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Facebook URL</label>
<div className="relative">
<LinkIcon className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="facebookUrl"
defaultValue={settings?.facebookUrl}
placeholder="https://facebook.com/..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Twitter (X) URL</label>
<div className="relative">
<LinkIcon className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="twitterUrl"
defaultValue={settings?.twitterUrl}
placeholder="https://twitter.com/..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">Instagram URL</label>
<div className="relative">
<LinkIcon className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="instagramUrl"
defaultValue={settings?.instagramUrl}
placeholder="https://instagram.com/..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-400">LinkedIn URL</label>
<div className="relative">
<LinkIcon className="absolute left-3 top-3.5 w-4 h-4 text-slate-500" />
<input
name="linkedinUrl"
defaultValue={settings?.linkedinUrl}
placeholder="https://linkedin.com/in/..."
className="w-full bg-slate-950 border border-slate-700 rounded-xl p-3 pl-10 text-white focus:outline-none focus:border-indigo-500"
/>
</div>
</div>
</div>
</div>
<div className="flex justify-end">
<button
type="submit"
disabled={isPending}
className="bg-indigo-600 hover:bg-indigo-700 disabled:bg-slate-700 text-white px-8 py-3 rounded-xl font-bold transition-all shadow-lg flex items-center gap-2"
>
{isPending ? (
<Loader2 className="w-5 h-5 animate-spin" />
) : (
<Save className="w-5 h-5" />
)}
Enregistrer les modifications
</button>
</div>
</form>
</div>
);
}

View File

@@ -10,7 +10,8 @@ import {
LogOut, LogOut,
ShieldCheck, ShieldCheck,
BarChart3, BarChart3,
Flag Flag,
Settings
} from 'lucide-react'; } from 'lucide-react';
const menuItems = [ const menuItems = [
@@ -21,6 +22,7 @@ const menuItems = [
{ name: 'Commentaires', icon: MessageSquare, href: '/comments' }, { name: 'Commentaires', icon: MessageSquare, href: '/comments' },
{ name: 'Modération', icon: Flag, href: '/moderation' }, { name: 'Modération', icon: Flag, href: '/moderation' },
{ name: 'Blog & Interviews', icon: BookOpen, href: '/blog' }, { name: 'Blog & Interviews', icon: BookOpen, href: '/blog' },
{ name: 'Configuration', icon: Settings, href: '/settings' },
]; ];
export default function Sidebar() { export default function Sidebar() {

77
app/cgu/page.tsx Normal file
View File

@@ -0,0 +1,77 @@
import React from 'react';
export const metadata = {
title: 'Conditions Générales d\'Utilisation - Afropreunariat',
description: 'Consultez les conditions générales d\'utilisation de la plateforme Afropreunariat.',
};
export default function CGUPage() {
return (
<div className="bg-white min-h-screen py-16 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
<h1 className="text-3xl font-serif font-bold text-gray-900 mb-8 border-b pb-4">
Conditions Générales d'Utilisation (CGU)
</h1>
<div className="prose prose-orange max-w-none text-gray-600 space-y-6">
<p className="text-sm italic text-gray-500 mb-8">Dernière mise à jour : 12 avril 2026</p>
<section>
<h2 className="text-xl font-bold text-gray-900 mb-3">1. Présentation du site</h2>
<p>
En vertu de l'article 6 de la loi n° 2004-575 du 21 juin 2004 pour la confiance dans l'économie numérique, il est précisé aux utilisateurs du site Afropreunariat l'identité des différents intervenants dans le cadre de sa réalisation et de son suivi.
</p>
</section>
<section>
<h2 className="text-xl font-bold text-gray-900 mb-3">2. Conditions générales dutilisation du site et des services proposés</h2>
<p>
Lutilisation du site Afropreunariat implique lacceptation pleine et entière des conditions générales dutilisation ci-après décrites. Ces conditions dutilisation sont susceptibles dêtre modifiées ou complétées à tout moment, les utilisateurs du site sont donc invités à les consulter de manière régulière.
</p>
</section>
<section>
<h2 className="text-xl font-bold text-gray-900 mb-3">3. Description des services fournis</h2>
<p>
Le site Afropreunariat a pour objet de fournir une plateforme de mise en relation entre entrepreneurs africains et clients, ainsi qu'un annuaire d'entreprises. Afropreunariat sefforce de fournir sur le site des informations aussi précises que possible. Toutefois, il ne pourra être tenue responsable des omissions, des inexactitudes et des carences dans la mise à jour, quelles soient de son fait ou du fait des tiers partenaires qui lui fournissent ces informations.
</p>
</section>
<section>
<h2 className="text-xl font-bold text-gray-900 mb-3">4. Limitations contractuelles sur les données techniques</h2>
<p>
Le site utilise la technologie JavaScript (Next.js). Le site Internet ne pourra être tenu responsable de dommages matériels liés à lutilisation du site. De plus, lutilisateur du site sengage à accéder au site en utilisant un matériel récent, ne contenant pas de virus et avec un navigateur de dernière génération mis à jour.
</p>
</section>
<section>
<h2 className="text-xl font-bold text-gray-900 mb-3">5. Propriété intellectuelle et contrefaçons</h2>
<p>
Afropreunariat est propriétaire des droits de propriété intellectuelle ou détient les droits dusage sur tous les éléments accessibles sur le site, notamment les textes, images, graphismes, logo, icônes, sons, logiciels. Toute reproduction, représentation, modification, publication, adaptation de tout ou partie des éléments du site, quel que soit le moyen ou le procédé utilisé, est interdite, sauf autorisation écrite préalable.
</p>
</section>
<section>
<h2 className="text-xl font-bold text-gray-900 mb-3">6. Limitations de responsabilité</h2>
<p>
Afropreunariat ne pourra être tenue responsable des dommages directs et indirects causés au matériel de lutilisateur, lors de laccès au site Afropreunariat. Un espace interactif (possibilité de poser des questions dans lespace contact) est à la disposition des utilisateurs. Afropreunariat se réserve le droit de supprimer, sans mise en demeure préalable, tout contenu déposé dans cet espace qui contreviendrait à la législation applicable en France (incluant RGPD).
</p>
</section>
<section>
<h2 className="text-xl font-bold text-gray-900 mb-3">7. Gestion des données personnelles</h2>
<p>
En France, les données personnelles sont notamment protégées par la loi n° 78-87 du 6 janvier 1978, la loi n° 2004-801 du 6 août 2004, l'article L. 226-13 du Code pénal et le Règlement Européen sur la Protection des Données (RGPD). À l'occasion de l'utilisation du site, peuvent être recueillies : l'URL des liens par l'intermédiaire desquels l'utilisateur a accédé au site, le fournisseur d'accès de l'utilisateur, l'adresse de protocole Internet (IP) de l'utilisateur.
</p>
</section>
<div className="bg-gray-50 p-6 rounded-lg border border-gray-100 mt-12">
<p className="text-xs text-center text-gray-500 italic">
Note : Ce document est un modèle simplifié. Pour toute activité commerciale réelle, il est fortement recommandé de consulter un avocat pour adapter ce document à votre situation spécifique.
</p>
</div>
</div>
</div>
</div>
);
}

77
app/cgv/page.tsx Normal file
View File

@@ -0,0 +1,77 @@
import React from 'react';
export const metadata = {
title: 'Conditions Générales de Vente - Afropreunariat',
description: 'Consultez les conditions générales de vente des services Afropreunariat.',
};
export default function CGVPage() {
return (
<div className="bg-white min-h-screen py-16 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
<h1 className="text-3xl font-serif font-bold text-gray-900 mb-8 border-b pb-4">
Conditions Générales de Vente (CGV)
</h1>
<div className="prose prose-orange max-w-none text-gray-600 space-y-6">
<p className="text-sm italic text-gray-500 mb-8">Dernière mise à jour : 12 avril 2026</p>
<section>
<h2 className="text-xl font-bold text-gray-900 mb-3">1. Objet</h2>
<p>
Les présentes conditions générales de vente (CGV) régissent les relations contractuelles entre la plateforme Afropreunariat et tout utilisateur professionnel (l'Entrepreneur) souscrivant à des services payants sur le site.
</p>
</section>
<section>
<h2 className="text-xl font-bold text-gray-900 mb-3">2. Description des services payants</h2>
<p>
Afropreunariat propose divers services payants destinés aux entrepreneurs, incluant mais ne se limitant pas à : l'inscription premium dans l'annuaire, la mise en avant de fiches entreprises, et des outils de communication avancés. Le contenu et les tarifs de ces services sont détaillés sur la page de tarification.
</p>
</section>
<section>
<h2 className="text-xl font-bold text-gray-900 mb-3">3. Tarifs et paiement</h2>
<p>
Les prix de nos services sont indiqués en euros (ou monnaie locale) toutes taxes comprises. Afropreunariat se réserve le droit de modifier ses prix à tout moment, étant toutefois entendu que le prix figurant sur le site au jour de la commande sera le seul applicable à l'Entrepreneur. Le paiement est exigible immédiatement à la commande.
</p>
</section>
<section>
<h2 className="text-xl font-bold text-gray-900 mb-3">4. Droit de rétractation</h2>
<p>
Conformément à l'article L221-28 du Code de la consommation, le droit de rétractation ne peut être exercé pour les services pleinement exécutés avant la fin du délai de rétractation ou dont l'exécution a commencé avec votre accord préalable exprès et renoncement exprès à votre droit de rétractation. Les abonnements numériques et services de mise en avant immédiate entrent dans ce cadre.
</p>
</section>
<section>
<h2 className="text-xl font-bold text-gray-900 mb-3">5. Durée et résiliation</h2>
<p>
Les services de type "Abonnement" sont conclus pour la durée choisie lors de la souscription (mensuelle ou annuelle). Sauf mention contraire, ils sont reconduits tacitement. L'Entrepreneur peut résilier son abonnement à tout moment depuis son tableau de bord, la résiliation prenant effet à la fin de la période en cours.
</p>
</section>
<section>
<h2 className="text-xl font-bold text-gray-900 mb-3">6. Responsabilité</h2>
<p>
La plateforme Afropreunariat n'est tenue que par une obligation de moyens. Elle ne saurait être tenue pour responsable des dommages résultant d'une mauvaise utilisation du service ou de l'absence de retours commerciaux suite à une mise en avant sur la plateforme.
</p>
</section>
<section>
<h2 className="text-xl font-bold text-gray-900 mb-3">7. Règlement des litiges</h2>
<p>
Les présentes conditions de vente sont soumises à la loi française. En cas de litige, compétence est attribuée aux tribunaux compétents, nonobstant pluralité de défendeurs ou appel en garantie.
</p>
</section>
<div className="bg-gray-50 p-6 rounded-lg border border-gray-100 mt-12">
<p className="text-xs text-center text-gray-500 italic">
Note : Ce document est un modèle simplifié. Pour toute activité commerciale réelle, il est fortement recommandé de consulter un avocat pour adapter ce document à votre situation spécifique.
</p>
</div>
</div>
</div>
</div>
);
}

View File

@@ -4,8 +4,10 @@ import Navbar from '../components/Navbar';
import Footer from '../components/Footer'; import Footer from '../components/Footer';
import { UserProvider } from '../components/UserProvider'; import { UserProvider } from '../components/UserProvider';
import AnalyticsTracker from '../components/AnalyticsTracker'; import AnalyticsTracker from '../components/AnalyticsTracker';
import CookieBanner from '../components/CookieBanner';
import { Metadata } from 'next'; import { Metadata } from 'next';
import { Toaster } from 'react-hot-toast'; import { Toaster } from 'react-hot-toast';
import { getSiteSettings } from '../lib/settings';
import './globals.css'; import './globals.css';
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -13,7 +15,9 @@ export const metadata: Metadata = {
description: 'Annuaire Afropreunariat', description: 'Annuaire Afropreunariat',
}; };
export default function RootLayout({ children }: { children: React.ReactNode }) { export default async function RootLayout({ children }: { children: React.ReactNode }) {
const settings = await getSiteSettings();
return ( return (
<html lang="fr"> <html lang="fr">
<head> <head>
@@ -28,7 +32,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<main className="flex-grow"> <main className="flex-grow">
{children} {children}
</main> </main>
<Footer /> <Footer settings={settings} />
<CookieBanner />
</UserProvider> </UserProvider>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,74 @@
"use client";
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { X, Cookie } from 'lucide-react';
const CookieBanner = () => {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
// Check if the user has already consented
const consent = localStorage.getItem('cookie-consent');
if (!consent) {
// Small delay for better UX
const timer = setTimeout(() => {
setIsVisible(true);
}, 1500);
return () => clearTimeout(timer);
}
}, []);
const handleConsent = (level: 'all' | 'essential') => {
localStorage.setItem('cookie-consent', level);
setIsVisible(false);
};
if (!isVisible) return null;
return (
<div className="fixed bottom-0 left-0 right-0 z-[100] p-4 md:p-6 pointer-events-none">
<div className="max-w-4xl mx-auto bg-gray-900 text-white rounded-2xl shadow-2xl border border-gray-800 p-5 md:p-6 pointer-events-auto transform transition-all duration-500 ease-in-out animate-in slide-in-from-bottom-10">
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-6">
<div className="flex items-start gap-4">
<div className="bg-brand-500/10 p-2 rounded-lg shrink-0">
<Cookie className="w-6 h-6 text-brand-500" />
</div>
<div>
<h3 className="text-lg font-bold mb-1">Nous respectons votre vie privée</h3>
<p className="text-sm text-gray-400 leading-relaxed">
Afropreunariat utilise des cookies pour améliorer votre expérience, analyser le trafic et personnaliser le contenu.
Certains sont essentiels, d'autres nous aident à grandir avec vous.
Consultez nos <Link href="/cgu" className="text-brand-500 hover:underline">CGU</Link> pour en savoir plus.
</p>
</div>
</div>
<div className="flex flex-wrap gap-3 shrink-0 w-full md:w-auto">
<button
onClick={() => handleConsent('essential')}
className="flex-1 md:flex-none px-5 py-2.5 text-sm font-medium text-gray-300 hover:text-white border border-gray-700 hover:border-gray-500 rounded-xl transition-all"
>
Uniquement essentiels
</button>
<button
onClick={() => handleConsent('all')}
className="flex-1 md:flex-none px-5 py-2.5 text-sm font-medium bg-brand-500 hover:bg-brand-600 text-white rounded-xl shadow-lg shadow-brand-500/20 transition-all active:scale-95"
>
Tout accepter
</button>
<button
onClick={() => setIsVisible(false)}
className="absolute top-4 right-4 text-gray-500 hover:text-white transition-colors p-1"
aria-label="Fermer"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
</div>
</div>
);
};
export default CookieBanner;

View File

@@ -5,19 +5,31 @@ import React from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
const Footer = () => { interface FooterProps {
settings?: any;
}
const Footer = ({ settings }: FooterProps) => {
const pathname = usePathname() || ''; const pathname = usePathname() || '';
if (pathname.startsWith('/dashboard')) return null; if (pathname.startsWith('/dashboard')) return null;
const siteInfo = settings || {
siteName: "Afropreunariat",
siteSlogan: "La plateforme de référence pour l'entrepreneuriat africain. Visibilité, connexion et croissance pour les TPE/PME.",
contactEmail: "support@afropreunariat.com",
contactPhone: "+225 00 00 00 00 00",
address: "Abidjan, Côte d'Ivoire / Paris, France",
footerText: "© 2025 Afropreunariat. Tous droits réservés."
};
return ( return (
<footer className="bg-dark-900 text-white"> <footer className="bg-dark-900 text-white">
<div className="max-w-7xl mx-auto py-12 px-4 overflow-hidden sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto py-12 px-4 overflow-hidden sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
<div> <div>
<h3 className="text-2xl font-serif font-bold text-brand-500 mb-4">Afropreunariat</h3> <h3 className="text-2xl font-serif font-bold text-brand-500 mb-4">{siteInfo.siteName}</h3>
<p className="text-gray-400 text-sm"> <p className="text-gray-400 text-sm">
La plateforme de référence pour l'entrepreneuriat africain. {siteInfo.siteSlogan}
Visibilité, connexion et croissance pour les TPE/PME.
</p> </p>
</div> </div>
<div> <div>
@@ -30,12 +42,20 @@ const Footer = () => {
</div> </div>
<div> <div>
<h4 className="font-bold text-lg mb-4">Contact</h4> <h4 className="font-bold text-lg mb-4">Contact</h4>
<p className="text-gray-400 text-sm mb-2">Abidjan, Côte d'Ivoire / Paris, France</p> <p className="text-gray-400 text-sm mb-2">{siteInfo.address}</p>
<p className="text-gray-400 text-sm">support@afropreunariat.com</p> <p className="text-gray-400 text-sm">{siteInfo.contactEmail}</p>
{siteInfo.contactPhone && <p className="text-gray-400 text-sm mt-1">{siteInfo.contactPhone}</p>}
</div>
<div>
<h4 className="font-bold text-lg mb-4">Légal</h4>
<ul className="space-y-2 text-gray-400 text-sm">
<li><Link href="/cgu" className="hover:text-brand-500 transition-colors">CGU</Link></li>
<li><Link href="/cgv" className="hover:text-brand-500 transition-colors">CGV</Link></li>
</ul>
</div> </div>
</div> </div>
<div className="mt-8 pt-8 border-t border-gray-800 text-center text-gray-500 text-sm"> <div className="mt-8 pt-8 border-t border-gray-800 text-center text-gray-500 text-sm">
&copy; 2025 Afropreunariat. Tous droits réservés. {siteInfo.footerText}
</div> </div>
</div> </div>
</footer> </footer>

View File

@@ -13,6 +13,42 @@ services:
- "5432:5432" - "5432:5432"
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d afropreunariat_dev"]
interval: 10s
timeout: 5s
retries: 5
app:
build:
context: .
dockerfile: Dockerfile
container_name: afropreunariat_app
restart: always
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://admin:adminpassword@db:5432/afropreunariat_dev?schema=public
- NEXT_PUBLIC_SITE_URL=http://localhost:3000
depends_on:
db:
condition: service_healthy
admin:
build:
context: ./admin
dockerfile: Dockerfile
container_name: afropreunariat_admin
restart: always
ports:
- "3001:3001"
environment:
- DATABASE_URL=postgresql://admin:adminpassword@db:5432/afropreunariat_dev?schema=public
- NEXT_PUBLIC_SITE_URL=http://localhost:3000
- NEXT_PUBLIC_ADMIN_URL=http://localhost:3001
depends_on:
db:
condition: service_healthy
volumes: volumes:
pgdata: pgdata:

40
lib/settings.ts Normal file
View File

@@ -0,0 +1,40 @@
import { prisma } from './prisma';
export async function getSiteSettings() {
try {
const settings = await prisma.siteSetting.findUnique({
where: { id: 'singleton' }
});
// Default fallback values if not initialized
const defaultSettings = {
siteName: "Afropreunariat",
siteSlogan: "La plateforme de référence pour l'entrepreneuriat africain. Visibilité, connexion et croissance pour les TPE/PME.",
contactEmail: "support@afropreunariat.com",
contactPhone: "+225 00 00 00 00 00",
address: "Abidjan, Côte d'Ivoire / Paris, France",
facebookUrl: "#",
twitterUrl: "#",
instagramUrl: "#",
linkedinUrl: "#",
footerText: "© 2025 Afropreunariat. Tous droits réservés."
};
if (!settings) return defaultSettings;
return settings;
} catch (error) {
console.error("Failed to fetch site settings:", error);
return {
siteName: "Afropreunariat",
siteSlogan: "La plateforme de référence pour l'entrepreneuriat africain.",
contactEmail: "support@afropreunariat.com",
contactPhone: "",
address: "",
facebookUrl: "#",
twitterUrl: "#",
instagramUrl: "#",
linkedinUrl: "#",
footerText: "© 2025 Afropreunariat. Tous droits réservés."
};
}
}

View File

@@ -1,6 +1,7 @@
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
reactStrictMode: true, reactStrictMode: true,
output: 'standalone',
}; };
export default nextConfig; export default nextConfig;

View File

@@ -193,3 +193,18 @@ enum InterviewType {
VIDEO VIDEO
ARTICLE 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
}