Files
zeropost-web/components/ArticleCoverSVG.js
T
Nik (Claude) 334b2f51df feat: журнальная главная, страница Зеро, TG-баннер, stats, auto-publish UI
- Журнальная главная: hero, CategoryRow, PopularBlock, RecentBlock (Сегодня/Вчера/Неделя)
- ArticleCard: 3 размера (hero/regular/compact), цветной badge без дублей тегов
- ArticleCoverSVG: 6 брендовых палитр, аватар Зеро в углу вместо #ZEROPOST
- /about/zero: страница персонажа с галереей 8 поз
- Footer: TG-баннер с аватаром Зеро на каждой странице
- Конец статьи: блок «Понравилась? → Подписаться на канал»
- ChannelEditor: 4 вкладки (Настройки/Расписание/Авто-публикация/Ручная)
- AutoPublishTab: toggle, категории, delay, template, live preview
- ArticlePicker: typeahead с was_sent_to_channel / next_scheduled_at флагами
- /admin/channels/[id]/stats: график роста подписчиков (recharts)
- Dashboard: блок TG-статистики (подписчики, delta 24h/7d, постов)
- Header: упрощён до 2 пунктов desktop + расширенное мобильное меню
- AutogenPanel: корректные time-picker'ы, calcNextRun с учётом last_run_at
2026-06-07 14:04:09 +03:00

119 lines
4.3 KiB
JavaScript

/**
* Процедурно-сгенерированная SVG-обложка в стиле ZeroPost.
* Используется когда настоящая обложка ещё не сгенерирована.
* Каждая статья получает уникальный, воспроизводимый узор.
*/
function rng(seed) {
let t = seed + 0x6D2B79F5;
return () => {
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// Только наши бренд-палитры — никакого произвольного фиолетового
const PALETTES = [
{ bg: '#ecfdf5', accent: '#10b981', soft: '#a7f3d0', dark: '#065f46' }, // emerald (основной)
{ bg: '#f0fdfa', accent: '#14b8a6', soft: '#99f6e4', dark: '#115e59' }, // teal
{ bg: '#f8fafc', accent: '#10b981', soft: '#d1fae5', dark: '#1e293b' }, // emerald+neutral
{ bg: '#fefce8', accent: '#d97706', soft: '#fde68a', dark: '#92400e' }, // amber
{ bg: '#eff6ff', accent: '#3b82f6', soft: '#bfdbfe', dark: '#1e40af' }, // blue
];
export default function ArticleCoverSVG({ article, className = '', aspect = '16/9' }) {
const seed = (article?.id || 1) * 9301 + 49297;
const rand = rng(seed);
// Выбор палитры по id — не случайный, а детерминированный
const palette = PALETTES[(article?.id || 0) % PALETTES.length];
const layers = 3 + Math.floor(rand() * 3);
const shapes = [];
for (let i = 0; i < layers; i++) {
const kind = ['circle', 'circle', 'arc', 'rect'][Math.floor(rand() * 4)]; // circle чаще
const opacity = 0.25 + rand() * 0.45;
const colors = [palette.accent, palette.soft, palette.dark];
const fill = colors[Math.floor(rand() * colors.length)];
shapes.push({ kind, opacity, fill, r: rand });
}
return (
<div
className={`relative overflow-hidden rounded-xl ${className}`}
style={{ aspectRatio: aspect, background: palette.bg }}
>
<svg
viewBox="0 0 400 225"
preserveAspectRatio="xMidYMid slice"
className="absolute inset-0 w-full h-full"
aria-hidden="true"
>
{shapes.map((s, idx) => {
const cx = 60 + s.r() * 320;
const cy = 30 + s.r() * 165;
const size = 60 + s.r() * 180;
const rot = s.r() * 360;
if (s.kind === 'circle') {
return <circle key={idx} cx={cx} cy={cy} r={size / 2} fill={s.fill} opacity={s.opacity} />;
}
if (s.kind === 'rect') {
return (
<rect
key={idx}
x={cx - size / 2} y={cy - size / 2}
width={size} height={size * (0.5 + s.r() * 1)}
fill={s.fill} opacity={s.opacity}
rx={s.r() > 0.5 ? size / 8 : 0}
transform={`rotate(${rot} ${cx} ${cy})`}
/>
);
}
if (s.kind === 'arc') {
const r = size / 2;
return (
<path
key={idx}
d={`M ${cx - r} ${cy} A ${r} ${r} 0 0 1 ${cx + r} ${cy} Z`}
fill={s.fill} opacity={s.opacity}
transform={`rotate(${rot} ${cx} ${cy})`}
/>
);
}
const w = size, h = size * 0.4;
return (
<path
key={idx}
d={`M ${cx - w/2} ${cy} Q ${cx} ${cy - h}, ${cx + w/2} ${cy} T ${cx + w*1.5} ${cy}`}
stroke={s.fill} strokeWidth={6 + s.r() * 16}
strokeLinecap="round" fill="none" opacity={s.opacity}
/>
);
})}
{/* Точки-частицы */}
{Array.from({ length: 12 }).map((_, i) => (
<circle
key={`d-${i}`}
cx={rand() * 400} cy={rand() * 225}
r={1 + rand() * 2.5}
fill={palette.dark} opacity={0.12 + rand() * 0.15}
/>
))}
</svg>
{/* Аватар Зеро в правом нижнем углу вместо текстового тега */}
<div className="absolute bottom-3 right-3 pointer-events-none">
<img
src="/uploads/zero-avatar.webp"
alt=""
className="w-8 h-8 rounded-lg opacity-70"
style={{ boxShadow: '0 1px 4px rgba(0,0,0,0.15)' }}
/>
</div>
</div>
);
}