feat: implement comprehensive CMS dashboard for managing news, events, interviews, and one-shot pricing plans
This commit is contained in:
@@ -90,8 +90,8 @@ const HomeClient = ({ initialFeatured, initialPosts, initialCategories, initialS
|
||||
<h2 className="text-3xl font-bold text-gray-900 font-serif">{settings?.homeBlogTitle || "Derniers Articles"}</h2>
|
||||
<p className="text-gray-500 mt-2">{settings?.homeBlogSubtitle || "Toutes les actualités de l'entrepreneuriat africain."}</p>
|
||||
</div>
|
||||
<Link href="/blog" className="text-brand-600 font-medium hover:text-brand-700 flex items-center">
|
||||
Voir tout le blog <ArrowRight className="w-4 h-4 ml-1" />
|
||||
<Link href="/actualites" className="text-brand-600 font-medium hover:text-brand-700 flex items-center">
|
||||
Voir les actualités <ArrowRight className="w-4 h-4 ml-1" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -108,7 +108,7 @@ const HomeClient = ({ initialFeatured, initialPosts, initialCategories, initialS
|
||||
})
|
||||
.slice(0, settings?.homeBlogCount || 3)
|
||||
.map(post => (
|
||||
<Link key={post.id} href={`/blog/${post.slug ? generateSlug(post.slug) : post.id}`} className="group bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-md transition-all">
|
||||
<Link key={post.id} href={`/actualites/${post.slug ? generateSlug(post.slug) : post.id}`} className="group bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden hover:shadow-md transition-all">
|
||||
<div className="h-48 overflow-hidden">
|
||||
<img src={post.imageUrl} alt={post.title} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" width={400} height={300} loading="lazy" />
|
||||
</div>
|
||||
|
||||
42
app/actions/events.ts
Normal file
42
app/actions/events.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { ContentStatus } from "@prisma/client";
|
||||
|
||||
export async function createEvent(data: {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
location: string;
|
||||
thumbnailUrl: string;
|
||||
link?: string;
|
||||
tags?: string[];
|
||||
}) {
|
||||
try {
|
||||
const slug = data.title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
|
||||
const event = await prisma.event.create({
|
||||
data: {
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
date: new Date(data.date),
|
||||
location: data.location,
|
||||
thumbnailUrl: data.thumbnailUrl,
|
||||
link: data.link || null,
|
||||
tags: data.tags || [],
|
||||
status: ContentStatus.PENDING, // Always pending for user submission
|
||||
slug: `${slug}-${Math.random().toString(36).substring(2, 7)}`
|
||||
}
|
||||
});
|
||||
|
||||
revalidatePath("/evenements");
|
||||
return { success: true, event };
|
||||
} catch (error) {
|
||||
console.error("Failed to create event:", error);
|
||||
return { success: false, error: "Erreur lors de la création de l'événement" };
|
||||
}
|
||||
}
|
||||
40
app/actions/news.ts
Normal file
40
app/actions/news.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { ContentStatus } from "@prisma/client";
|
||||
|
||||
export async function createNews(data: {
|
||||
title: string;
|
||||
excerpt: string;
|
||||
content: string;
|
||||
author: string;
|
||||
imageUrl: string;
|
||||
tags?: string[];
|
||||
}) {
|
||||
try {
|
||||
const slug = data.title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/(^-|-$)/g, '');
|
||||
|
||||
const post = await prisma.blogPost.create({
|
||||
data: {
|
||||
title: data.title,
|
||||
excerpt: data.excerpt,
|
||||
content: data.content,
|
||||
author: data.author,
|
||||
imageUrl: data.imageUrl,
|
||||
tags: data.tags || [],
|
||||
status: ContentStatus.PENDING, // Always pending for user submission
|
||||
slug: `${slug}-${Math.random().toString(36).substring(2, 7)}`
|
||||
}
|
||||
});
|
||||
|
||||
revalidatePath("/blog");
|
||||
return { success: true, post };
|
||||
} catch (error) {
|
||||
console.error("Failed to create news:", error);
|
||||
return { success: false, error: "Erreur lors de la création de l'actualité" };
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,7 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="mb-8">
|
||||
<Link href="/blog" className="inline-flex items-center text-gray-500 hover:text-brand-600 text-sm font-medium transition-colors">
|
||||
<Link href="/actualites" className="inline-flex items-center text-gray-500 hover:text-brand-600 text-sm font-medium transition-colors">
|
||||
<ArrowLeft className="w-4 h-4 mr-1" /> Retour aux articles
|
||||
</Link>
|
||||
</div>
|
||||
@@ -172,7 +172,7 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
<div className="mt-12 pt-8 border-t border-gray-100 flex flex-col sm:flex-row justify-between items-center gap-4">
|
||||
<p className="text-gray-500 font-medium">Vous avez aimé cet article ?</p>
|
||||
<Link
|
||||
href="/blog"
|
||||
href="/actualites"
|
||||
className="inline-flex items-center text-brand-600 hover:text-brand-700 font-semibold transition-colors"
|
||||
>
|
||||
Lire d'autres articles
|
||||
@@ -199,7 +199,7 @@ export default async function BlogPostPage({ params }: Props) {
|
||||
{relatedPosts.map((rPost) => (
|
||||
<Link
|
||||
key={rPost.id}
|
||||
href={`/blog/${rPost.slug || rPost.id}`}
|
||||
href={`/actualites/${rPost.slug || rPost.id}`}
|
||||
className="group bg-white rounded-xl shadow-sm overflow-hidden border border-gray-100 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="aspect-video relative overflow-hidden">
|
||||
@@ -26,9 +26,9 @@ export default async function BlogPage() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-3xl font-bold font-serif text-gray-900 sm:text-4xl">Le Blog de l'Entrepreneur</h1>
|
||||
<h1 className="text-3xl font-bold font-serif text-gray-900 sm:text-4xl">Nos Actualités</h1>
|
||||
<p className="mt-3 max-w-2xl mx-auto text-xl text-gray-500 sm:mt-4">
|
||||
Conseils, actualités et success stories de l'écosystème africain.
|
||||
Conseils, dossiers et news de l'écosystème africain.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +40,7 @@ export default async function BlogPage() {
|
||||
) : (
|
||||
<div className="grid gap-8 lg:grid-cols-3">
|
||||
{posts.map(post => (
|
||||
<Link key={post.id} href={`/blog/${post.slug || post.id}`} className="group flex flex-col rounded-lg shadow-lg overflow-hidden bg-white hover:shadow-xl transition-shadow duration-300">
|
||||
<Link key={post.id} href={`/actualites/${post.slug || post.id}`} className="group flex flex-col rounded-lg shadow-lg overflow-hidden bg-white hover:shadow-xl transition-shadow duration-300">
|
||||
<div className="flex-shrink-0 relative overflow-hidden h-48">
|
||||
<img className="h-full w-full object-cover group-hover:scale-105 transition-transform duration-500" src={post.imageUrl} alt={post.title} />
|
||||
<div className="absolute inset-0 bg-black/10 group-hover:bg-transparent transition-colors"></div>
|
||||
14
app/api/one-shot-plans/route.ts
Normal file
14
app/api/one-shot-plans/route.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const plans = await prisma.oneShotPlan.findMany({
|
||||
orderBy: { type: 'asc' }
|
||||
});
|
||||
return NextResponse.json(plans);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch one-shot plans:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch plans" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import DashboardProfileClient from '../../components/dashboard/DashboardProfileC
|
||||
import DashboardMessages from '../../components/dashboard/DashboardMessages';
|
||||
import DashboardOffers from '../../components/dashboard/DashboardOffers';
|
||||
import PricingSection from '../../components/PricingSection';
|
||||
import OneShotPricingSection from '../../components/OneShotPricingSection';
|
||||
import DashboardFavorites from '../../components/dashboard/DashboardFavorites';
|
||||
import { useUser } from '../../components/UserProvider';
|
||||
|
||||
@@ -242,7 +243,7 @@ const DashboardContent = () => {
|
||||
</button>
|
||||
<button onClick={() => setCurrentView('subscription')} className={`${currentView === 'subscription' ? 'bg-brand-50 text-brand-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'} group flex items-center px-2 py-2 text-sm font-medium rounded-md w-full`}>
|
||||
<CreditCard className={`${currentView === 'subscription' ? 'text-brand-500' : 'text-gray-400 group-hover:text-gray-500'} mr-3 flex-shrink-0 h-5 w-5`} />
|
||||
Abonnement
|
||||
Abonnement & Services
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
@@ -326,6 +327,7 @@ const DashboardContent = () => {
|
||||
</span>
|
||||
</div>
|
||||
<PricingSection />
|
||||
<OneShotPricingSection />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import PricingSection from '../../components/PricingSection';
|
||||
import OneShotPricingSection from '../../components/OneShotPricingSection';
|
||||
|
||||
const SubscriptionPage = () => {
|
||||
return (
|
||||
@@ -12,6 +13,7 @@ const SubscriptionPage = () => {
|
||||
<p className="text-gray-400 mt-2">Rejoignez la plus grande communauté d'entrepreneurs.</p>
|
||||
</div>
|
||||
<PricingSection />
|
||||
<OneShotPricingSection />
|
||||
|
||||
{/* FAQ Section */}
|
||||
<div className="max-w-4xl mx-auto px-4 py-16">
|
||||
|
||||
Reference in New Issue
Block a user