From 3668713d7e340a21c0ab6626bc0902d642893e28 Mon Sep 17 00:00:00 2001 From: Djoufson CHE BENE Date: Sun, 7 Jun 2026 09:51:19 +0100 Subject: [PATCH 1/4] implement the banner feature --- src/admin/src/api/banners.ts | 43 ++ src/admin/src/api/types.ts | 33 ++ src/admin/src/components/AdminSidebar.tsx | 2 + src/admin/src/components/BannerForm.tsx | 266 +++++++++ src/admin/src/routeTree.gen.ts | 63 ++ src/admin/src/routes/banners/$id.edit.tsx | 11 + src/admin/src/routes/banners/index.tsx | 143 +++++ src/admin/src/routes/banners/new.tsx | 6 + src/app.business/Services/IBannerService.cs | 16 + .../Models/BannerAggregate/Banner.cs | 100 ++++ .../BannerAggregate/Enums/BannerVariant.cs | 10 + src/app.domain/ViewModels/BannerModel.cs | 18 + ...260607083704_Add Banners Table.Designer.cs | 540 ++++++++++++++++++ .../20260607083704_Add Banners Table.cs | 50 ++ .../Migrations/AppDbContextModelSnapshot.cs | 64 ++- .../Persistence/AppDbContext.cs | 2 + .../Configurations/BannerConfigurations.cs | 19 + .../Services/BannerService.cs | 113 ++++ src/app/Api/Admin/AdminApi.cs | 1 + src/app/Api/Admin/AdminBannersApi.cs | 119 ++++ src/app/Api/Admin/Dtos/BannerDtos.cs | 31 + .../Components/Components/BannerStack.razor | 86 +++ src/app/Components/Layout/MainLayout.razor | 1 + src/app/Extensions/Extensions.cs | 1 + src/app/wwwroot/js/app.js | 38 ++ 25 files changed, 1772 insertions(+), 4 deletions(-) create mode 100644 src/admin/src/api/banners.ts create mode 100644 src/admin/src/components/BannerForm.tsx create mode 100644 src/admin/src/routes/banners/$id.edit.tsx create mode 100644 src/admin/src/routes/banners/index.tsx create mode 100644 src/admin/src/routes/banners/new.tsx create mode 100644 src/app.business/Services/IBannerService.cs create mode 100644 src/app.domain/Models/BannerAggregate/Banner.cs create mode 100644 src/app.domain/Models/BannerAggregate/Enums/BannerVariant.cs create mode 100644 src/app.domain/ViewModels/BannerModel.cs create mode 100644 src/app.infrastructure/Migrations/20260607083704_Add Banners Table.Designer.cs create mode 100644 src/app.infrastructure/Migrations/20260607083704_Add Banners Table.cs create mode 100644 src/app.infrastructure/Persistence/Configurations/BannerConfigurations.cs create mode 100644 src/app.infrastructure/Services/BannerService.cs create mode 100644 src/app/Api/Admin/AdminBannersApi.cs create mode 100644 src/app/Api/Admin/Dtos/BannerDtos.cs create mode 100644 src/app/Components/Components/BannerStack.razor diff --git a/src/admin/src/api/banners.ts b/src/admin/src/api/banners.ts new file mode 100644 index 0000000..d5ecd8f --- /dev/null +++ b/src/admin/src/api/banners.ts @@ -0,0 +1,43 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { get, post, put, del } from './client'; +import type { PagedResponse, BannerResponse, CreateOrUpdateBannerRequest } from './types'; + +export function useBanners(page: number, size: number) { + const params = new URLSearchParams({ page: String(page), size: String(size) }); + return useQuery({ + queryKey: ['banners', page, size], + queryFn: () => get>(`/banners?${params}`), + }); +} + +export function useBanner(id: string) { + return useQuery({ + queryKey: ['banners', id], + queryFn: () => get(`/banners/${id}`), + enabled: !!id, + }); +} + +export function useCreateBanner() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateOrUpdateBannerRequest) => post('/banners', data), + onSuccess: () => qc.invalidateQueries({ queryKey: ['banners'] }), + }); +} + +export function useUpdateBanner(id: string) { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (data: CreateOrUpdateBannerRequest) => put(`/banners/${id}`, data), + onSuccess: () => qc.invalidateQueries({ queryKey: ['banners'] }), + }); +} + +export function useDeleteBanner() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (id: string) => del(`/banners/${id}`), + onSuccess: () => qc.invalidateQueries({ queryKey: ['banners'] }), + }); +} diff --git a/src/admin/src/api/types.ts b/src/admin/src/api/types.ts index fff4959..ec6b6f5 100644 --- a/src/admin/src/api/types.ts +++ b/src/admin/src/api/types.ts @@ -100,6 +100,39 @@ export interface CreateOrUpdatePartnerRequest { website: string; } +// Banners +export type BannerVariant = 'Announcement' | 'Advertisement' | 'Promo' | 'Event' | 'Maintenance'; + +export interface BannerResponse { + id: string; + messageEn: string; + messageFr: string; + variant: BannerVariant; + startDate: string; + endDate: string; + link: string | null; + linkLabelEn: string | null; + linkLabelFr: string | null; + dismissible: boolean; + priority: number; + isEnabled: boolean; + createdAt: string; +} + +export interface CreateOrUpdateBannerRequest { + messageEn: string; + messageFr: string; + variant: BannerVariant; + startDate: string; + endDate: string; + link: string | null; + linkLabelEn: string | null; + linkLabelFr: string | null; + dismissible: boolean; + priority: number; + isEnabled: boolean; +} + // External Apps export interface AppSummary { id: string; diff --git a/src/admin/src/components/AdminSidebar.tsx b/src/admin/src/components/AdminSidebar.tsx index 861f097..e31bf5f 100644 --- a/src/admin/src/components/AdminSidebar.tsx +++ b/src/admin/src/components/AdminSidebar.tsx @@ -4,12 +4,14 @@ import { Calendar, Handshake, KeyRound, + Megaphone, } from 'lucide-react'; const navItems = [ { to: '/', label: 'Dashboard', icon: LayoutDashboard, exact: true }, { to: '/events', label: 'Events', icon: Calendar, exact: false }, { to: '/partners', label: 'Partners', icon: Handshake, exact: false }, + { to: '/banners', label: 'Banners', icon: Megaphone, exact: false }, { to: '/apps', label: 'Applications', icon: KeyRound, exact: false }, ] as const; diff --git a/src/admin/src/components/BannerForm.tsx b/src/admin/src/components/BannerForm.tsx new file mode 100644 index 0000000..ff9dfb1 --- /dev/null +++ b/src/admin/src/components/BannerForm.tsx @@ -0,0 +1,266 @@ +import { useState, useEffect } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { useBanner, useCreateBanner, useUpdateBanner } from '../api/banners'; +import type { CreateOrUpdateBannerRequest, BannerVariant } from '../api/types'; +import { ArrowLeft, Save, Loader2 } from 'lucide-react'; + +interface BannerFormProps { + bannerId?: string; +} + +const variants: BannerVariant[] = ['Announcement', 'Advertisement', 'Promo', 'Event', 'Maintenance']; + +function toLocalInput(iso: string): string { + const d = new Date(iso); + const pad = (n: number) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; +} + +export function BannerForm({ bannerId }: BannerFormProps) { + const navigate = useNavigate(); + const isEdit = !!bannerId; + + const { data: existing, isLoading: loadingExisting } = useBanner(bannerId ?? ''); + const createBanner = useCreateBanner(); + const updateBanner = useUpdateBanner(bannerId ?? ''); + + const [messageEn, setMessageEn] = useState(''); + const [messageFr, setMessageFr] = useState(''); + const [variant, setVariant] = useState('Announcement'); + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + const [link, setLink] = useState(''); + const [linkLabelEn, setLinkLabelEn] = useState(''); + const [linkLabelFr, setLinkLabelFr] = useState(''); + const [dismissible, setDismissible] = useState(true); + const [priority, setPriority] = useState(0); + const [isEnabled, setIsEnabled] = useState(true); + + useEffect(() => { + if (!existing) return; + setMessageEn(existing.messageEn); + setMessageFr(existing.messageFr); + setVariant(existing.variant); + setStartDate(toLocalInput(existing.startDate)); + setEndDate(toLocalInput(existing.endDate)); + setLink(existing.link ?? ''); + setLinkLabelEn(existing.linkLabelEn ?? ''); + setLinkLabelFr(existing.linkLabelFr ?? ''); + setDismissible(existing.dismissible); + setPriority(existing.priority); + setIsEnabled(existing.isEnabled); + }, [existing]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const data: CreateOrUpdateBannerRequest = { + messageEn, + messageFr, + variant, + startDate: new Date(startDate).toISOString(), + endDate: new Date(endDate).toISOString(), + link: link.trim() || null, + linkLabelEn: linkLabelEn.trim() || null, + linkLabelFr: linkLabelFr.trim() || null, + dismissible, + priority, + isEnabled, + }; + + const onSuccess = () => navigate({ to: '/banners' }); + if (isEdit) { + updateBanner.mutate(data, { onSuccess }); + } else { + createBanner.mutate(data, { onSuccess }); + } + }; + + if (isEdit && loadingExisting) { + return ( +
+ +
+ ); + } + + const saving = createBanner.isPending || updateBanner.isPending; + + return ( +
+
+ +
+

+ {isEdit ? 'Edit Banner' : 'New Banner'} +

+

+ Scheduled site-wide announcements +

+
+
+ +
+
+

Content

+
+
+ +