feat(content): add announcements and public news flows

This commit is contained in:
2026-02-12 20:08:08 +01:00
parent 994b33e081
commit dbf817c255
20 changed files with 1071 additions and 8 deletions

View File

@@ -0,0 +1,54 @@
import { beforeEach, describe, expect, it, vi } from "vitest"
const { mockDb } = vi.hoisted(() => ({
mockDb: {
announcement: {
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
findMany: vi.fn(),
},
},
}))
vi.mock("./client", () => ({
db: mockDb,
}))
import { createAnnouncement, listActiveAnnouncements } from "./announcements"
describe("announcements service", () => {
beforeEach(() => {
for (const fn of Object.values(mockDb.announcement)) {
if (typeof fn === "function") {
fn.mockReset()
}
}
})
it("creates announcements through schema parsing", async () => {
mockDb.announcement.create.mockResolvedValue({ id: "announcement-1" })
await createAnnouncement({
title: "Scheduled Notice",
message: "Commission slots are open.",
placement: "global_top",
})
expect(mockDb.announcement.create).toHaveBeenCalledTimes(1)
})
it("queries only visible announcements in the given placement", async () => {
mockDb.announcement.findMany.mockResolvedValue([])
await listActiveAnnouncements("homepage")
expect(mockDb.announcement.findMany).toHaveBeenCalledTimes(1)
expect(mockDb.announcement.findMany.mock.calls[0]?.[0]).toMatchObject({
where: {
placement: "homepage",
isVisible: true,
},
})
})
})

View File

@@ -0,0 +1,74 @@
import {
type AnnouncementPlacement,
createAnnouncementInputSchema,
updateAnnouncementInputSchema,
} from "@cms/content"
import { db } from "./client"
export type PublicAnnouncement = {
id: string
title: string
message: string
ctaLabel: string | null
ctaHref: string | null
placement: string
priority: number
}
export async function listAnnouncements(limit = 200) {
return db.announcement.findMany({
orderBy: [{ priority: "asc" }, { updatedAt: "desc" }],
take: limit,
})
}
export async function createAnnouncement(input: unknown) {
const payload = createAnnouncementInputSchema.parse(input)
return db.announcement.create({
data: payload,
})
}
export async function updateAnnouncement(input: unknown) {
const payload = updateAnnouncementInputSchema.parse(input)
const { id, ...data } = payload
return db.announcement.update({
where: { id },
data,
})
}
export async function deleteAnnouncement(id: string) {
return db.announcement.delete({
where: { id },
})
}
export async function listActiveAnnouncements(
placement: AnnouncementPlacement,
now = new Date(),
): Promise<PublicAnnouncement[]> {
const announcements = await db.announcement.findMany({
where: {
placement,
isVisible: true,
OR: [{ startsAt: null }, { startsAt: { lte: now } }],
AND: [{ OR: [{ endsAt: null }, { endsAt: { gte: now } }] }],
},
orderBy: [{ priority: "asc" }, { createdAt: "desc" }],
select: {
id: true,
title: true,
message: true,
ctaLabel: true,
ctaHref: true,
placement: true,
priority: true,
},
})
return announcements
}

View File

@@ -1,3 +1,11 @@
export type { PublicAnnouncement } from "./announcements"
export {
createAnnouncement,
deleteAnnouncement,
listActiveAnnouncements,
listAnnouncements,
updateAnnouncement,
} from "./announcements"
export { db } from "./client"
export {
commissionKanbanOrder,
@@ -44,6 +52,7 @@ export {
createPost,
deletePost,
getPostById,
getPostBySlug,
listPosts,
registerPostCrudAuditHook,
updatePost,

View File

@@ -67,6 +67,12 @@ export async function getPostById(id: string) {
return postCrudService.getById(id)
}
export async function getPostBySlug(slug: string) {
return db.post.findUnique({
where: { slug },
})
}
export async function createPost(input: unknown, context?: CrudMutationContext) {
return postCrudService.create(input, context)
}