import { and, asc, desc, eq, isNull, ne, sql } from "drizzle-orm"; import type { DrizzleD1Database } from "drizzle-orm/d1"; import { blogKeywords, cases, contentItems, CONTENT_KINDS, posts, siteSettings, type BlogKeyword, type CaseStudy, type ContentItem, type ContentKind, type Post, } from "../db/schema"; import * as schema from "../db/schema"; export type Db = DrizzleD1Database; export type Settings = Record; export type HomeData = { settings: Settings; services: ContentItem[]; features: ContentItem[]; steps: ContentItem[]; faqs: ContentItem[]; cases: CaseStudy[]; }; export async function getSettings(db: Db): Promise { const rows = await db.select().from(siteSettings); return Object.fromEntries(rows.map((r) => [r.key, r.value])); } export async function getItems(db: Db, kind: ContentKind): Promise { return db .select() .from(contentItems) .where(and(eq(contentItems.kind, kind), eq(contentItems.status, "published"))) .orderBy(asc(contentItems.sortOrder)); } export async function getCases(db: Db): Promise { return db .select() .from(cases) .where(eq(cases.status, "published")) .orderBy(asc(cases.sortOrder)); } export async function getPublishedPosts(db: Db): Promise { return db .select() .from(posts) .where(eq(posts.status, "published")) .orderBy(desc(posts.publishedAt)); } export async function getPostBySlug(db: Db, slug: string): Promise { const [post] = await db .select() .from(posts) .where(and(eq(posts.slug, slug), eq(posts.status, "published"))) .limit(1); return post ?? null; } export async function getKeywords(db: Db): Promise { return db .select() .from(blogKeywords) .orderBy(asc(blogKeywords.createdAt), asc(blogKeywords.id)); } export type DashboardData = { posts: { published: number; draft: number }; cases: { published: number; draft: number }; content: { kind: ContentKind; published: number; draft: number }[]; keywords: { pending: number; generated: number; skipped: number }; recentPosts: Post[]; postsWithoutCover: number; }; /** 後台總覽用嘅統計(只讀 DB;AI secrets 由頁面層提供)。 */ export async function getDashboardData(db: Db): Promise { const [postRows, caseRows, itemRows, kwRows, recentPosts, noCover] = await Promise.all([ db.select({ status: posts.status, n: sql`count(*)` }).from(posts).groupBy(posts.status), db.select({ status: cases.status, n: sql`count(*)` }).from(cases).groupBy(cases.status), db .select({ kind: contentItems.kind, status: contentItems.status, n: sql`count(*)` }) .from(contentItems) .groupBy(contentItems.kind, contentItems.status), db .select({ status: blogKeywords.status, n: sql`count(*)` }) .from(blogKeywords) .groupBy(blogKeywords.status), db.select().from(posts).orderBy(desc(posts.updatedAt)).limit(5), db.select({ n: sql`count(*)` }).from(posts).where(isNull(posts.coverImage)), ]); const pick = (rows: { status: T; n: number }[], status: T) => Number(rows.find((r) => r.status === status)?.n ?? 0); return { posts: { published: pick(postRows, "published"), draft: pick(postRows, "draft") }, cases: { published: pick(caseRows, "published"), draft: pick(caseRows, "draft") }, content: CONTENT_KINDS.map((kind) => ({ kind, published: Number(itemRows.find((r) => r.kind === kind && r.status === "published")?.n ?? 0), draft: Number(itemRows.find((r) => r.kind === kind && r.status === "draft")?.n ?? 0), })), keywords: { pending: pick(kwRows, "pending"), generated: pick(kwRows, "generated"), skipped: pick(kwRows, "skipped"), }, recentPosts, postsWithoutCover: Number(noCover[0]?.n ?? 0), }; } export async function getHomeData(db: Db): Promise { const [settings, services, features, steps, faqs, caseList] = await Promise.all([ getSettings(db), getItems(db, "service"), getItems(db, "feature"), getItems(db, "step"), getItems(db, "faq"), getCases(db), ]); return { settings, services, features, steps, faqs, cases: caseList }; } export function digits(input: string | undefined): string { return (input ?? "").replace(/\D/g, ""); } export function whatsappHref( settings: Settings, text = "你好,我想查詢村屋太陽能安裝,想預約免費評估。", ): string { return `https://api.whatsapp.com/send?phone=${digits(settings.whatsapp)}&text=${encodeURIComponent(text)}`; } export function telHref(settings: Settings): string { const phone = (settings.phone ?? "").replace(/[^\d+]/g, ""); return `tel:${phone}`; } export function mailHref(settings: Settings): string { return `mailto:${settings.email ?? ""}?subject=${encodeURIComponent("查詢村屋太陽能安裝")}`; } export function formatDate(d: Date | null | undefined): string { if (!d) return ""; return new Date(d).toLocaleDateString("zh-Hant-HK", { year: "numeric", month: "long", day: "numeric", }); } /** slug 撞咗就加 -2、-3…;selfId 用喺更新自己時排除自己。 */ export async function uniqueSlug(db: Db, base: string, selfId?: string): Promise { let candidate = base; for (let i = 2; i < 50; i++) { const rows = await db .select({ id: posts.id }) .from(posts) .where(selfId ? and(eq(posts.slug, candidate), ne(posts.id, selfId)) : eq(posts.slug, candidate)) .limit(1); if (rows.length === 0) return candidate; candidate = `${base}-${i}`; } return `${base}-${Date.now()}`; }