hellth-hub/apps/admin/lib/page-editor.ts
Sebastian Mayer 2436dd9cac Initial commit: Hellth Hub monorepo
Mobile-first Web-App fuer persoenliches Health-, Ernaehrungs-,
Aktivitaets- und Einkaufs-Tracking im Home-Lab-Betrieb.

Stack: pnpm Monorepo - Next.js 16 + React 19 + Tailwind (apps/admin) -
Hono + tRPC + Better Auth mit 2FA und Passkeys (packages/api) -
PostgreSQL + Drizzle ORM (packages/db) - Docker fuer Postgres und pgAdmin.

Module:
- Auth mit Login, 2FA (TOTP + Backup-Codes), Passkeys und Admin-Recovery
- Tagesansicht mit Essen, Trinken, Gewicht, Aktivitaeten und Coach-Hinweisen
- Lebensmittelkatalog mit Naehrwerten, Portionen, OCR/Barcode/Open Food Facts
- Rezepte mit Live-Naehrwertberechnung und Community-Vorschlaegen
- Essensplanung und Wochenbudget mit Sportbonus
- Einkaufslisten mit Maerkten und Laufweg-Sortierung
- Hellfireclub mit Aktivitaetspunkten und gemeinsamer Aktivitaetsplanung
- Wearables-Vorbereitung (Fitbit / Google Fit / Health Connect)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 22:15:16 +02:00

82 lines
2.5 KiB
TypeScript

export type LifecycleStatus = "draft" | "scheduled" | "published" | "archived";
export function slugify(text: string): string {
return text
.toLowerCase()
.normalize("NFKD")
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.slice(0, 100)
.replace(/^-|-$/g, "");
}
function stripHtml(html: string): string {
return html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
}
export function extractH1Title(html: string): string {
const match = html.match(/<h1\b[^>]*>([\s\S]*?)<\/h1>/i);
if (!match?.[1]) return "";
return stripHtml(match[1]).slice(0, 160).trim();
}
export function toDateValue(date?: Date | string | null) {
if (!date) return "";
return new Date(date).toISOString().slice(0, 10);
}
export function toTimeValue(date?: Date | string | null) {
if (!date) return "";
return new Date(date).toISOString().slice(11, 16);
}
export function combineDateTime(date: string, time: string) {
if (!date || !time) return undefined;
return new Date(`${date}T${time}:00`);
}
export function isSchedulingVisible(status: LifecycleStatus) {
return status === "draft" || status === "scheduled";
}
export function validatePublishInputs(
status: LifecycleStatus,
publishDate: string,
publishTime: string,
) {
if (!isSchedulingVisible(status)) return null;
if (status === "scheduled" && (!publishDate || !publishTime)) {
return "Für den Status Geplant sind Datum und Uhrzeit erforderlich.";
}
if ((publishDate && !publishTime) || (!publishDate && publishTime)) {
return "Bitte Datum und Uhrzeit gemeinsam setzen.";
}
return null;
}
export function resolveSubmitStatus(
status: LifecycleStatus,
publishDate: string,
publishTime: string,
) {
if (status === "draft" && publishDate && publishTime) return "scheduled" as const;
return status;
}
export function statusLabel(status: LifecycleStatus) {
if (status === "archived") return "Archiviert";
if (status === "published") return "Veröffentlicht";
if (status === "scheduled") return "Geplant";
return "Entwurf";
}
export function statusClass(status: LifecycleStatus) {
if (status === "archived") return "bg-gray-200 text-gray-700 dark:bg-neutral-700 dark:text-neutral-300";
if (status === "published") return "bg-green-50 text-green-700 dark:bg-green-950/50 dark:text-green-400";
if (status === "scheduled") return "bg-indigo-50 text-indigo-700 dark:bg-indigo-950/50 dark:text-indigo-400";
return "bg-gray-100 text-gray-500 dark:bg-neutral-700 dark:text-neutral-400";
}