Files
clientsflow/microapps/templates/pocketbase-deno-dashboard/main.ts
2026-03-23 11:47:14 +07:00

163 lines
4.5 KiB
TypeScript

import appConfig from "./app.config.ts";
import { createPocketBaseClient } from "./pocketbase/client.ts";
const port = Number(Deno.env.get("PORT") || "8080");
const pocketbaseUrl = mustGetEnv("POCKETBASE_URL");
const pocketbaseEmail = mustGetEnv("POCKETBASE_SERVICE_EMAIL");
const pocketbasePassword = mustGetEnv("POCKETBASE_SERVICE_PASSWORD");
const ownerTeamId = mustGetEnv("APP_OWNER_TEAM_ID");
const appSlug = Deno.env.get("APP_SLUG") || appConfig.slug;
const pocketbase = createPocketBaseClient({
baseUrl: pocketbaseUrl,
email: pocketbaseEmail,
password: pocketbasePassword,
});
type DashboardCard = {
id: string;
title: string;
metric: string;
description: string | null;
};
function renderHtml(cards: DashboardCard[]) {
const cardsMarkup = cards.length === 0
? `<p class="empty">No dashboard cards yet.</p>`
: cards.map((card) => `
<article class="card">
<p class="label">${escapeHtml(card.title)}</p>
<strong>${escapeHtml(card.metric)}</strong>
<span>${escapeHtml(card.description || "No description")}</span>
</article>
`).join("");
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${escapeHtml(appConfig.name)}</title>
<style>
:root {
color-scheme: light;
--bg: #f4efe5;
--panel: #fffaf2;
--ink: #17231f;
--muted: #52615a;
--accent: #0d8a62;
--border: #d7cbb8;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
background:
radial-gradient(circle at top left, rgba(13, 138, 98, 0.2), transparent 32%),
linear-gradient(180deg, #f8f3ea 0%, var(--bg) 100%);
color: var(--ink);
}
main {
max-width: 960px;
margin: 0 auto;
padding: 48px 20px 80px;
}
.hero {
padding: 28px;
border: 1px solid var(--border);
border-radius: 28px;
background: rgba(255, 250, 242, 0.92);
backdrop-filter: blur(10px);
}
.eyebrow {
margin: 0 0 10px;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 12px;
color: var(--accent);
}
h1 {
margin: 0;
font-size: clamp(32px, 5vw, 56px);
line-height: 0.96;
}
.meta {
margin-top: 18px;
color: var(--muted);
font-size: 15px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
margin-top: 24px;
}
.card {
border-radius: 20px;
padding: 20px;
border: 1px solid rgba(23, 35, 31, 0.08);
background: var(--panel);
box-shadow: 0 10px 24px rgba(23, 35, 31, 0.08);
}
.label {
margin: 0 0 12px;
color: var(--muted);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
strong {
display: block;
font-size: 34px;
margin-bottom: 8px;
}
span, .empty {
color: var(--muted);
}
</style>
</head>
<body>
<main>
<section class="hero">
<p class="eyebrow">PocketBase + Deno</p>
<h1>${escapeHtml(appConfig.name)}</h1>
<p class="meta">App slug: ${escapeHtml(appSlug)} • Team: ${escapeHtml(ownerTeamId)}</p>
</section>
<section class="grid">${cardsMarkup}</section>
</main>
</body>
</html>`;
}
function mustGetEnv(name: string) {
const value = Deno.env.get(name);
if (!value) {
throw new Error(`Missing required env: ${name}`);
}
return value;
}
function escapeHtml(value: string) {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
async function handleRequest() {
const cards = await pocketbase.listRecords<DashboardCard>({
collectionName: `${appConfig.pocketbase.collectionPrefix}_cards`,
filter: `teamId = "${ownerTeamId}"`,
});
return new Response(renderHtml(cards), {
headers: {
"content-type": "text/html; charset=utf-8",
},
});
}
Deno.serve({ port }, () => handleRequest());