Files
lms-sb/src/lib/wrapped-card.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

99 lines
3.3 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.
// Общий JSX карточки Obsidian Wrapped для next/og ImageResponse (1080×1920, ДС-2).
// Flexbox + inline-стили (Satori не поддерживает grid/внешний CSS). Без emoji
// (Satori их не рендерит без эмодзи-шрифта). Используется серверным OG-роутом.
import type { Archetype, Period } from "@/app/(student)/wrapped/_client/types";
export interface CardData {
noteCount: number;
linkCount: number;
tagCount: number;
topTag: string | null;
archetype: Archetype | null;
score: number;
period: Period;
}
const ARCHETYPE_RU: Record<string, string> = {
architect: "Архитектор",
gardener: "Садовник",
librarian: "Библиотекарь",
pragmatist: "Прагматик",
};
const PERIOD_RU: Record<string, string> = { year: "год", quarter: "квартал", all: "всё время" };
const BG = "#F5F5F0";
const FG = "#323232";
const ACCENT = "#E8F0D8";
const BORDER = "#AAAAAA";
const MUTED = "#666666";
function Metric({ n, l }: { n: number; l: string }) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: "30px 18px",
background: "#FFFFFF",
border: `3px solid ${FG}`,
boxShadow: `8px 8px 0 0 ${BORDER}`,
width: 285,
}}
>
<span style={{ fontSize: 70, fontWeight: 700, color: FG }}>{n}</span>
<span style={{ fontSize: 24, color: MUTED, textTransform: "uppercase", letterSpacing: 2 }}>{l}</span>
</div>
);
}
export function wrappedCard(d: CardData) {
return (
<div
style={{
width: 1080,
height: 1920,
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
background: BG,
color: FG,
padding: "90px 70px",
fontFamily: "Fira Mono",
}}
>
<div style={{ display: "flex", flexDirection: "column" }}>
<span style={{ fontSize: 34, fontWeight: 700, textTransform: "uppercase", letterSpacing: 6, color: MUTED }}>
Obsidian Wrapped · 2026
</span>
<span style={{ fontSize: 96, fontWeight: 700, marginTop: 36 }}>
{d.archetype ? ARCHETYPE_RU[d.archetype] : "Твой год"}
</span>
<span style={{ fontSize: 42, color: MUTED, marginTop: 14, textTransform: "uppercase", letterSpacing: 3 }}>
зрелость {d.score}/100 · {PERIOD_RU[d.period] ?? "год"}
</span>
</div>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Metric n={d.noteCount} l="заметок" />
<Metric n={d.linkCount} l="связей" />
<Metric n={d.tagCount} l="тегов" />
</div>
{d.topTag ? (
<div style={{ display: "flex", padding: "30px 36px", background: ACCENT, border: `3px solid ${FG}` }}>
<span style={{ fontSize: 46 }}>Топ-тег: #{d.topTag}</span>
</div>
) : (
<div style={{ display: "flex" }} />
)}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-end" }}>
<span style={{ fontSize: 34, fontWeight: 700, color: FG }}>second-brain.ru</span>
<span style={{ fontSize: 28, color: MUTED }}>сделай свою на /wrapped</span>
</div>
</div>
);
}