06b8d9f64b
- 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>
99 lines
3.3 KiB
TypeScript
99 lines
3.3 KiB
TypeScript
// Общий 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>
|
||
);
|
||
}
|