feat: implement manageable homepage banner slider with support for businesses and custom slides
This commit is contained in:
112
admin/src/app/actions/slides.ts
Normal file
112
admin/src/app/actions/slides.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export async function getSlides() {
|
||||
try {
|
||||
return await prisma.homeSlide.findMany({
|
||||
orderBy: { order: 'asc' },
|
||||
include: { business: true }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to get slides:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSlide(data: any) {
|
||||
try {
|
||||
await prisma.homeSlide.create({
|
||||
data: {
|
||||
type: data.type,
|
||||
businessId: data.businessId || null,
|
||||
title: data.title || null,
|
||||
subtitle: data.subtitle || null,
|
||||
imageUrl: data.imageUrl || null,
|
||||
linkUrl: data.linkUrl || null,
|
||||
order: data.order || 0,
|
||||
isActive: data.isActive ?? true,
|
||||
}
|
||||
});
|
||||
revalidatePath("/slides");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Failed to create slide:", error);
|
||||
return { success: false, error: "Erreur lors de la création" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateSlide(id: string, data: any) {
|
||||
try {
|
||||
await prisma.homeSlide.update({
|
||||
where: { id },
|
||||
data: {
|
||||
type: data.type,
|
||||
businessId: data.businessId || null,
|
||||
title: data.title || null,
|
||||
subtitle: data.subtitle || null,
|
||||
imageUrl: data.imageUrl || null,
|
||||
linkUrl: data.linkUrl || null,
|
||||
order: data.order || 0,
|
||||
isActive: data.isActive ?? true,
|
||||
}
|
||||
});
|
||||
revalidatePath("/slides");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Failed to update slide:", error);
|
||||
return { success: false, error: "Erreur lors de la mise à jour" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteSlide(id: string) {
|
||||
try {
|
||||
await prisma.homeSlide.delete({
|
||||
where: { id }
|
||||
});
|
||||
revalidatePath("/slides");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Failed to delete slide:", error);
|
||||
return { success: false, error: "Erreur lors de la suppression" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleSlideStatus(id: string, currentStatus: boolean) {
|
||||
try {
|
||||
await prisma.homeSlide.update({
|
||||
where: { id },
|
||||
data: { isActive: !currentStatus }
|
||||
});
|
||||
revalidatePath("/slides");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Failed to toggle slide status:", error);
|
||||
return { success: false, error: "Erreur lors de la mise à jour" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchBusinesses(query: string) {
|
||||
try {
|
||||
return await prisma.business.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ name: { contains: query, mode: 'insensitive' } },
|
||||
{ location: { contains: query, mode: 'insensitive' } },
|
||||
],
|
||||
isActive: true,
|
||||
},
|
||||
take: 10,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
location: true,
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to search businesses:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
16
admin/src/app/slides/edit/[id]/page.tsx
Normal file
16
admin/src/app/slides/edit/[id]/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import SlideForm from "@/components/SlideForm";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export default async function EditSlidePage({ params }: { params: { id: string } }) {
|
||||
const slide = await prisma.homeSlide.findUnique({
|
||||
where: { id: params.id },
|
||||
include: { business: true }
|
||||
});
|
||||
|
||||
if (!slide) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return <SlideForm initialData={slide} />;
|
||||
}
|
||||
5
admin/src/app/slides/new/page.tsx
Normal file
5
admin/src/app/slides/new/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import SlideForm from "@/components/SlideForm";
|
||||
|
||||
export default function NewSlidePage() {
|
||||
return <SlideForm />;
|
||||
}
|
||||
115
admin/src/app/slides/page.tsx
Normal file
115
admin/src/app/slides/page.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
"use server";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import Link from 'next/link';
|
||||
import { Plus, Image as ImageIcon, Trash2, Edit, ExternalLink, Power, PowerOff } from 'lucide-react';
|
||||
import DeleteSlideButton from '@/components/DeleteSlideButton';
|
||||
import ToggleSlideStatusButton from '@/components/ToggleSlideStatusButton';
|
||||
|
||||
async function getData() {
|
||||
try {
|
||||
return await prisma.homeSlide.findMany({
|
||||
orderBy: { order: 'asc' },
|
||||
include: { business: true }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch slides:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default async function SlidesPage() {
|
||||
const slides = await getData();
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-end mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white mb-2">Gestion de la Bannière</h1>
|
||||
<p className="text-slate-400">Gérez les slides qui s'affichent sur la page d'accueil (Slider).</p>
|
||||
</div>
|
||||
<Link href="/slides/new" className="btn-verify flex items-center gap-2 bg-indigo-600">
|
||||
<Plus className="w-4 h-4" />
|
||||
Nouvelle Slide
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<ImageIcon className="text-indigo-400 w-5 h-5" />
|
||||
<h2 className="text-xl font-bold text-white">Slides Actuelles ({slides.length})</h2>
|
||||
</div>
|
||||
|
||||
{slides.length === 0 ? (
|
||||
<div className="text-center py-12 bg-slate-800/20 rounded-xl border border-dashed border-slate-700">
|
||||
<p className="text-slate-500">Aucune slide configurée. Le bandeau par défaut sera affiché.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="data-table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left py-3 px-4">Ordre</th>
|
||||
<th className="text-left py-3 px-4">Type</th>
|
||||
<th className="text-left py-3 px-4">Contenu</th>
|
||||
<th className="text-left py-3 px-4">Statut</th>
|
||||
<th className="text-right py-3 px-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-800">
|
||||
{slides.map((slide) => (
|
||||
<tr key={slide.id} className="hover:bg-slate-800/30 transition-colors">
|
||||
<td className="py-4 px-4">
|
||||
<span className="text-white font-mono bg-slate-800 px-2 py-1 rounded">#{slide.order}</span>
|
||||
</td>
|
||||
<td className="py-4 px-4">
|
||||
<span className={`px-2 py-0.5 rounded-full text-[10px] font-bold ${slide.type === 'BUSINESS' ? 'bg-indigo-500/10 text-indigo-400 border border-indigo-500/20' : 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'}`}>
|
||||
{slide.type === 'BUSINESS' ? 'ENTREPRISE' : 'PERSONNALISÉ'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-4 px-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{slide.type === 'BUSINESS' ? (
|
||||
<>
|
||||
<div className="w-12 h-12 rounded-lg bg-slate-800 overflow-hidden flex-shrink-0 border border-slate-700">
|
||||
<img src={slide.business?.logoUrl || '/placeholder-biz.png'} className="w-full h-full object-cover" alt="" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-bold text-white">{slide.business?.name}</div>
|
||||
<div className="text-xs text-slate-500">{slide.business?.location}</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-12 h-12 rounded-lg bg-slate-800 overflow-hidden flex-shrink-0 border border-slate-700">
|
||||
<img src={slide.imageUrl || '/placeholder-img.png'} className="w-full h-full object-cover" alt="" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-bold text-white">{slide.title || 'Sans titre'}</div>
|
||||
<div className="text-xs text-slate-500 truncate max-w-[250px]">{slide.linkUrl || 'Pas de lien'}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-4 px-4">
|
||||
<ToggleSlideStatusButton id={slide.id} currentStatus={slide.isActive} />
|
||||
</td>
|
||||
<td className="py-4 px-4 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Link href={`/slides/edit/${slide.id}`} className="p-2 text-slate-400 hover:text-indigo-400 transition-colors">
|
||||
<Edit className="w-5 h-5" />
|
||||
</Link>
|
||||
<DeleteSlideButton id={slide.id} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user