diff --git a/backend/src/routes/banners.ts b/backend/src/routes/banners.ts new file mode 100644 index 0000000..7cc4c35 --- /dev/null +++ b/backend/src/routes/banners.ts @@ -0,0 +1,147 @@ +import { FastifyPluginAsync } from 'fastify'; +import { db } from '../config/database.js'; +import { banners } from '../db/schema.js'; +import { eq, and, lte, gte } from 'drizzle-orm'; + +const bannerBodyJsonSchema = { + type: 'object', + required: ['text', 'startDate', 'endDate'], + properties: { + text: { type: 'string' }, + startDate: { type: 'string' }, + endDate: { type: 'string' }, + isActive: { type: 'boolean' }, + }, +} as const; + +const bannersRoute: FastifyPluginAsync = async (fastify) => { + + // Get active banner (public endpoint) + fastify.get('/banners/active', async (request, reply) => { + const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD + + const [activeBanner] = await db + .select() + .from(banners) + .where( + and( + eq(banners.isActive, true), + lte(banners.startDate, today), + gte(banners.endDate, today) + ) + ) + .limit(1); + + if (!activeBanner) { + return { banner: null }; + } + + return { + banner: { + id: activeBanner.id, + text: activeBanner.text, + startDate: activeBanner.startDate, + endDate: activeBanner.endDate, + }, + }; + }); + + // Get all banners (admin only) + fastify.get('/banners', { + preHandler: [fastify.authenticate], + }, async (request, reply) => { + const allBanners = await db.select().from(banners); + + return { + banners: allBanners.map((b: any) => ({ + id: b.id, + text: b.text, + startDate: b.startDate, + endDate: b.endDate, + isActive: b.isActive, + createdAt: b.createdAt, + updatedAt: b.updatedAt, + })), + }; + }); + + // Create banner (admin only) + fastify.post('/banners', { + schema: { + body: bannerBodyJsonSchema, + }, + preHandler: [fastify.authenticate], + }, async (request, reply) => { + const { text, startDate, endDate, isActive = true } = request.body as any; + + const [newBanner] = await db + .insert(banners) + .values({ + text, + startDate, + endDate, + isActive, + }) + .returning(); + + return { + banner: { + id: newBanner.id, + text: newBanner.text, + startDate: newBanner.startDate, + endDate: newBanner.endDate, + isActive: newBanner.isActive, + }, + }; + }); + + // Update banner (admin only) + fastify.put('/banners/:id', { + schema: { + body: bannerBodyJsonSchema, + }, + preHandler: [fastify.authenticate], + }, async (request, reply) => { + const { id } = request.params as { id: string }; + const { text, startDate, endDate, isActive } = request.body as any; + + const [updated] = await db + .update(banners) + .set({ + text, + startDate, + endDate, + isActive, + updatedAt: new Date(), + }) + .where(eq(banners.id, id)) + .returning(); + + if (!updated) { + return reply.code(404).send({ error: 'Banner not found' }); + } + + return { + banner: { + id: updated.id, + text: updated.text, + startDate: updated.startDate, + endDate: updated.endDate, + isActive: updated.isActive, + }, + }; + }); + + // Delete banner (admin only) + fastify.delete('/banners/:id', { + preHandler: [fastify.authenticate], + }, async (request, reply) => { + const { id } = request.params as { id: string }; + + await db.delete(banners).where(eq(banners.id, id)); + + return { success: true }; + }); +}; + +export default bannersRoute; diff --git a/src/styles/components/Banner.css b/src/styles/components/Banner.css new file mode 100644 index 0000000..c8d2c62 --- /dev/null +++ b/src/styles/components/Banner.css @@ -0,0 +1,26 @@ +.banner-wrapper { + width: 100%; + background-color: var(--color-orange1); + padding: 1rem 0; +} + +.banner { + max-width: var(--container-max-width); + margin: 0 auto; + padding: 0 var(--padding-horizontal); +} + +.banner p { + color: #000; + font-size: var(--font-size-small-medium); + font-weight: 600; + margin: 0; + text-align: center; + line-height: 1.4; +} + +@media (max-width: 768px) { + .banner p { + font-size: var(--font-size-small); + } +}