feat(content): add announcements and public news flows
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Announcement" (
|
||||
"id" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"message" TEXT NOT NULL,
|
||||
"placement" TEXT NOT NULL,
|
||||
"priority" INTEGER NOT NULL DEFAULT 100,
|
||||
"ctaLabel" TEXT,
|
||||
"ctaHref" TEXT,
|
||||
"startsAt" TIMESTAMP(3),
|
||||
"endsAt" TIMESTAMP(3),
|
||||
"isVisible" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Announcement_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Announcement_placement_isVisible_idx" ON "Announcement"("placement", "isVisible");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Announcement_priority_idx" ON "Announcement"("priority");
|
||||
@@ -339,3 +339,21 @@ model Commission {
|
||||
@@index([customerId])
|
||||
@@index([assignedUserId])
|
||||
}
|
||||
|
||||
model Announcement {
|
||||
id String @id @default(uuid())
|
||||
title String
|
||||
message String
|
||||
placement String
|
||||
priority Int @default(100)
|
||||
ctaLabel String?
|
||||
ctaHref String?
|
||||
startsAt DateTime?
|
||||
endsAt DateTime?
|
||||
isVisible Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([placement, isVisible])
|
||||
@@index([priority])
|
||||
}
|
||||
|
||||
@@ -206,6 +206,23 @@ async function main() {
|
||||
dueAt: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
})
|
||||
|
||||
await db.announcement.upsert({
|
||||
where: {
|
||||
id: "22222222-2222-2222-2222-222222222222",
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
id: "22222222-2222-2222-2222-222222222222",
|
||||
title: "Commission Slots",
|
||||
message: "New commission slots are open for next month.",
|
||||
placement: "global_top",
|
||||
priority: 10,
|
||||
ctaLabel: "Request now",
|
||||
ctaHref: "/commissions",
|
||||
isVisible: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
54
packages/db/src/announcements.test.ts
Normal file
54
packages/db/src/announcements.test.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
74
packages/db/src/announcements.ts
Normal file
74
packages/db/src/announcements.ts
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user