Add data-driven public pages (home, news, profile)
Real App Router pages reading the converted Prisma models: - home: latest 4 articles (websiteArticles) + hotel_name from settings - /news + /news/[slug]: article index and detail - /u/[username]: profile (avatar via imager helper, motto, rank, credits, online) - shared SiteNav (reads session via auth() + hotel_name), globals.css - src/lib/format.ts: avatarImageUrl + excerpt helpers (unit-tested) Verified: tsc exit 0, vitest 43/43, next build exit 0 (7 routes). Pages render against a live DB (deferred until DATABASE_URL is provided). i18n to be layered on next.
This commit is contained in:
1 parent
8fca407f0f
commit
6e34d485d4
9 files changed
+327
-7
No files matched your search
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { avatarImageUrl, excerpt } from "./format";
|
||||
|
||||
describe("avatarImageUrl", () => {
|
||||
const base = "https://www.habbo.com/habbo-imaging/avatarimage";
|
||||
|
||||
it("appends the figure and options", () => {
|
||||
const url = avatarImageUrl(base, "hr-100", { size: "l", headOnly: true });
|
||||
expect(url).toContain("figure=hr-100");
|
||||
expect(url).toContain("size=l");
|
||||
expect(url).toContain("headonly=1");
|
||||
expect(url.startsWith(`${base}?`)).toBe(true);
|
||||
});
|
||||
|
||||
it("uses & when the base already has a query string", () => {
|
||||
expect(avatarImageUrl(`${base}?x=1`, "hr-100").includes("?x=1&figure=hr-100")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("excerpt", () => {
|
||||
it("returns short text unchanged", () => {
|
||||
expect(excerpt("hello", 160)).toBe("hello");
|
||||
});
|
||||
|
||||
it("truncates on a word boundary with an ellipsis", () => {
|
||||
const out = excerpt("the quick brown fox jumps", 12);
|
||||
expect(out).toBe("the quick…");
|
||||
expect(out.length).toBeLessThanOrEqual(13);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/** Build a Habbo avatar-imager URL for a figure string. */
|
||||
export function avatarImageUrl(
|
||||
base: string,
|
||||
look: string,
|
||||
opts: { size?: "s" | "m" | "l"; headOnly?: boolean; direction?: number } = {},
|
||||
): string {
|
||||
const params = new URLSearchParams({ figure: look });
|
||||
if (opts.size) params.set("size", opts.size);
|
||||
if (opts.headOnly) params.set("headonly", "1");
|
||||
if (opts.direction !== undefined) params.set("direction", String(opts.direction));
|
||||
const sep = base.includes("?") ? "&" : "?";
|
||||
return `${base}${sep}${params.toString()}`;
|
||||
}
|
||||
|
||||
/** Trim text to `max` chars on a word boundary, adding an ellipsis. */
|
||||
export function excerpt(text: string, max = 160): string {
|
||||
if (text.length <= max) return text;
|
||||
const cut = text.slice(0, max);
|
||||
const lastSpace = cut.lastIndexOf(" ");
|
||||
return `${(lastSpace > 0 ? cut.slice(0, lastSpace) : cut).trimEnd()}…`;
|
||||
}
|
||||
Reference in new issue
Block a user