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
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import Link from 'next/link';
|
||||
import ArticleCard from './ArticleCard';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Группировка свежих материалов по дням: «Сегодня», «Вчера», «Эта неделя», «Ранее».
|
||||
* Не рендерится, если пусто.
|
||||
*/
|
||||
function groupByPeriod(articles) {
|
||||
const now = new Date();
|
||||
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
||||
const startOfYesterday = startOfToday - 24 * 3600 * 1000;
|
||||
const startOfWeek = startOfToday - 7 * 24 * 3600 * 1000;
|
||||
|
||||
const groups = { today: [], yesterday: [], week: [], earlier: [] };
|
||||
for (const a of articles) {
|
||||
const t = new Date(a.published_at).getTime();
|
||||
if (t >= startOfToday) groups.today.push(a);
|
||||
else if (t >= startOfYesterday) groups.yesterday.push(a);
|
||||
else if (t >= startOfWeek) groups.week.push(a);
|
||||
else groups.earlier.push(a);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
const LABELS = {
|
||||
today: 'Сегодня',
|
||||
yesterday: 'Вчера',
|
||||
week: 'На этой неделе',
|
||||
earlier: 'Ранее',
|
||||
};
|
||||
|
||||
export default function RecentBlock({ articles }) {
|
||||
if (!articles || articles.length === 0) return null;
|
||||
const groups = groupByPeriod(articles);
|
||||
const nonEmpty = Object.entries(groups).filter(([, arr]) => arr.length > 0);
|
||||
if (nonEmpty.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="container-wide pb-12">
|
||||
<div className="flex items-end justify-between mb-5">
|
||||
<h2 className="text-xl sm:text-2xl font-bold ink">Свежие материалы</h2>
|
||||
<Link href="/archive" className="text-sm font-medium inline-flex items-center gap-1 accent hover:opacity-80 transition-opacity">
|
||||
Архив <ArrowRight className="w-3.5 h-3.5" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
{nonEmpty.map(([key, arr]) => (
|
||||
<div key={key}>
|
||||
<h3 className="text-xs font-semibold uppercase tracking-widest mute mb-3">
|
||||
{LABELS[key]} <span className="opacity-50">· {arr.length}</span>
|
||||
</h3>
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{arr.map(a => <ArticleCard key={a.id} article={a} />)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user