Files
lms-sb/src/app/share/wrapped/[token]/page.tsx
T
admins 06b8d9f64b Add Obsidian Wrapped feature (inside-LMS, client-side vault parsing)
- WrappedRun Prisma model + migration (aggregates only, no note names/text)
- /wrapped authed route: FSA + webkitdirectory + drag-drop reader, Web Worker parser
- archetype + score heuristics, ДС-2 result card
- POST /api/wrapped/run (userId from session), public /share/wrapped/[token]
- next/og OG image (Satori+resvg-wasm, alpine-safe) + Fira Mono ttf
- middleware: /share/wrapped public

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 14:19:59 +05:00

93 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { prisma } from "@/lib/prisma";
import { notFound } from "next/navigation";
import Link from "next/link";
import type { Metadata } from "next";
const ARCHETYPE_RU: Record<string, string> = {
architect: "Архитектор",
gardener: "Садовник",
librarian: "Библиотекарь",
pragmatist: "Прагматик",
};
export async function generateMetadata({ params }: { params: Promise<{ token: string }> }): Promise<Metadata> {
const { token } = await params;
return {
title: "Моя карта знаний в Obsidian — Second Brain",
description: "Я собрал статистику своего хранилища знаний. Сделай свою карточку.",
openGraph: {
title: "Моя карта знаний в Obsidian",
description: "Statистика хранилища в Obsidian — Second Brain Wrapped.",
images: [{ url: `/share/wrapped/${token}/opengraph-image`, width: 1080, height: 1920 }],
},
twitter: { card: "summary_large_image" },
};
}
// Публичная страница карточки (без сессии). Показывает только агрегаты — никаких имён заметок.
export default async function SharePage({ params }: { params: Promise<{ token: string }> }) {
const { token } = await params;
const run = await prisma.wrappedRun.findUnique({ where: { shareToken: token } });
if (!run) notFound();
const metric = (n: number, l: string) => (
<div
key={l}
style={{ display: "flex", flexDirection: "column", alignItems: "center", padding: "16px 12px", background: "#FFFFFF", border: "2px solid var(--border)", boxShadow: "4px 4px 0 0 var(--border)" }}
>
<span style={{ fontSize: 32, fontWeight: 700 }}>{n}</span>
<span style={{ fontSize: 12, color: "var(--muted-foreground)", textTransform: "uppercase", letterSpacing: 1 }}>{l}</span>
</div>
);
return (
<main style={{ background: "var(--background)", color: "var(--foreground)", minHeight: "100vh", display: "flex", flexDirection: "column", alignItems: "center", padding: "48px 24px" }}>
<div style={{ width: "100%", maxWidth: 480 }}>
<p style={{ fontSize: 12, fontWeight: 700, textTransform: "uppercase", letterSpacing: 3, color: "var(--muted-foreground)", marginBottom: 8 }}>
Obsidian Wrapped · 2026
</p>
<h1 style={{ fontSize: 28, fontWeight: 700, margin: "0 0 4px" }}>
{run.archetype ? ARCHETYPE_RU[run.archetype] : "Карта знаний"}
</h1>
<p style={{ fontSize: 13, textTransform: "uppercase", letterSpacing: 2, color: "var(--muted-foreground)", margin: "0 0 24px" }}>
зрелость {run.score}/100
</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 12, marginBottom: 20 }}>
{metric(run.noteCount, "заметок")}
{metric(run.linkCount, "связей")}
{metric(run.tagCount, "тегов")}
</div>
{run.topTag && (
<div style={{ padding: "16px 20px", background: "var(--accent)", border: "2px solid var(--foreground)", marginBottom: 24, fontSize: 16 }}>
Топ-тег: #{run.topTag}
</div>
)}
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<Link
href="/register"
className="btn-aubade btn-aubade-accent"
style={{ textAlign: "center", fontSize: 15, fontWeight: 700, padding: "14px" }}
>
Сделать свою карточку
</Link>
<a
href={`/share/wrapped/${token}/opengraph-image`}
download={`obsidian-wrapped-${token}.png`}
className="btn-aubade"
style={{ textAlign: "center", fontSize: 14, padding: "12px" }}
>
Скачать картинку (1080×1920)
</a>
</div>
<p style={{ fontSize: 12, color: "var(--muted-foreground)", marginTop: 28, textAlign: "center" }}>
🔒 Здесь только числа. Содержимое заметок остаётся на устройстве автора.
</p>
</div>
</main>
);
}