feat: Add backend routes and styles for banner management
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Introduced `banners.ts` with CRUD operations for managing banners. - Added `/banners/active` endpoint to fetch active banners. - Secured admin-only routes for banner creation, update, and deletion. - Created `Banner.css` for banner styling.
This commit is contained in:
147
backend/src/routes/banners.ts
Normal file
147
backend/src/routes/banners.ts
Normal file
@ -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;
|
||||||
26
src/styles/components/Banner.css
Normal file
26
src/styles/components/Banner.css
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user