From a16bf812e40632787e6bf22973266644d2c88ecb Mon Sep 17 00:00:00 2001 From: Alexey Pavlov Date: Sun, 31 May 2026 09:07:44 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=81=D0=B2=D0=B5=D1=82=D0=BB=D0=B0?= =?UTF-8?q?=D1=8F=20=D1=82=D0=B5=D0=BC=D0=B0=20=D0=BA=D0=B0=D0=BA=20=D0=BE?= =?UTF-8?q?=D1=81=D0=BD=D0=BE=D0=B2=D0=BD=D0=B0=D1=8F=20+=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=BA=D0=BB=D1=8E=D1=87=D0=B0=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=20=D1=82=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CSS-переменные --bg, --surface, --ink, --mute, --accent для обеих тем - darkMode: 'class' в Tailwind config - ThemeToggle компонент с Sun/Moon, сохраняет выбор в localStorage - Inline-скрипт в layout.js защищает от FOUC (FlashOfUnstyledContent) - Авто-определение по prefers-color-scheme как fallback - not-found.js: красивая 404 страница вместо дефолтной Next - Обновлены все компоненты и страницы — Header, Footer, ArticleCard, page.js, blog, tag, about --- app/about/page.js | 11 +++-- app/blog/[slug]/page.js | 15 ++++--- app/globals.css | 88 +++++++++++++++++++++++++++++++++++---- app/layout.js | 14 ++++++- app/not-found.js | 22 ++++++++++ app/page.js | 23 +++++----- app/tag/[name]/page.js | 8 ++-- components/ArticleCard.js | 15 +++---- components/Footer.js | 8 ++-- components/Header.js | 24 ++++++----- components/ThemeToggle.js | 36 ++++++++++++++++ tailwind.config.js | 42 ++++++++++--------- 12 files changed, 231 insertions(+), 75 deletions(-) create mode 100644 app/not-found.js create mode 100644 components/ThemeToggle.js diff --git a/app/about/page.js b/app/about/page.js index 1ae64cf..9cb35e3 100644 --- a/app/about/page.js +++ b/app/about/page.js @@ -1,6 +1,6 @@ import Header from '@/components/Header'; import Footer from '@/components/Footer'; -import { Sparkles, Cpu, BookOpen, Zap } from 'lucide-react'; +import { Sparkles } from 'lucide-react'; export const metadata = { title: 'О проекте' }; @@ -9,13 +9,16 @@ export default function AboutPage() { <>
-
+
О ZeroPost
-

+

Эксперимент: блог, который ведёт ИИ

-
+

ZeroPost — это два связанных проекта: публичный блог, который ты сейчас читаешь, и сервис для ведения Telegram-каналов с помощью ИИ.

diff --git a/app/blog/[slug]/page.js b/app/blog/[slug]/page.js index 52f093c..6e3ba85 100644 --- a/app/blog/[slug]/page.js +++ b/app/blog/[slug]/page.js @@ -30,7 +30,6 @@ export default async function ArticlePage({ params }) { const article = await getArticle(slug); if (!article) notFound(); - // Убираю H1 из контента — он уже идёт в заголовке страницы const contentWithoutH1 = article.content.replace(/^#\s+.+$/m, '').trim(); const html = renderMarkdown(contentWithoutH1); @@ -38,7 +37,7 @@ export default async function ArticlePage({ params }) { <>
- + Все статьи @@ -48,11 +47,11 @@ export default async function ArticlePage({ params }) { ))}
-

+

{article.title}

-
+
{article.author} · {formatDate(article.published_at)} @@ -67,13 +66,13 @@ export default async function ArticlePage({ params }) {
-
-

Хочешь такой же блог или канал в Telegram?

- + diff --git a/app/globals.css b/app/globals.css index 54821e5..7dd9c8a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,18 +2,92 @@ @tailwind components; @tailwind utilities; +/* === Theme tokens === */ +:root { + --bg: 250 250 249; /* почти белый, тёплый */ + --surface: 255 255 255; /* карточки */ + --surface-2: 245 245 244; /* приглушённый фон */ + --border: 231 229 228; + --ink: 28 25 23; /* основной текст */ + --mute: 120 113 108; /* приглушённый текст */ + --accent: 16 185 129; /* emerald-500 */ + --accent-2: 5 150 105; /* emerald-600 */ + --accent-soft: 209 250 229; /* emerald-100 */ +} + +.dark { + --bg: 10 10 10; + --surface: 20 20 20; + --surface-2: 28 28 28; + --border: 42 42 42; + --ink: 229 231 235; + --mute: 156 163 175; + --accent: 16 185 129; + --accent-2: 52 211 153; + --accent-soft: 6 78 59; +} + @layer base { - html { @apply bg-bg text-ink; } + html { + background: rgb(var(--bg)); + color: rgb(var(--ink)); + } body { @apply font-sans antialiased; } - ::selection { @apply bg-accent/30; } + ::selection { background: rgb(var(--accent) / 0.25); } } @layer components { - .btn { @apply inline-flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed; } - .btn-primary { @apply btn bg-accent text-black hover:bg-accent2; } - .btn-ghost { @apply btn text-mute hover:bg-surface hover:text-white; } + .btn { + @apply inline-flex items-center justify-center gap-2 px-4 py-2 rounded-lg font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed; + } + .btn-primary { + background: rgb(var(--accent)); + color: white; + } + .btn-primary:hover { background: rgb(var(--accent-2)); } + + .btn-ghost { + color: rgb(var(--mute)); + } + .btn-ghost:hover { + background: rgb(var(--surface-2)); + color: rgb(var(--ink)); + } + .container-narrow { @apply max-w-3xl mx-auto px-4; } .container-wide { @apply max-w-6xl mx-auto px-4; } - .article-card { @apply bg-surface border border-border rounded-2xl p-6 hover:border-accent/40 transition-colors; } - .tag { @apply inline-block text-xs px-2.5 py-1 rounded-full bg-surface2 text-mute hover:bg-surface hover:text-white transition-colors; } + + .article-card { + background: rgb(var(--surface)); + border: 1px solid rgb(var(--border)); + @apply rounded-2xl p-6 transition-all duration-200; + } + .article-card:hover { + border-color: rgb(var(--accent) / 0.4); + transform: translateY(-2px); + box-shadow: 0 10px 25px -10px rgb(0 0 0 / 0.08); + } + .dark .article-card:hover { + box-shadow: 0 10px 25px -10px rgb(0 0 0 / 0.5); + } + + .tag { + display: inline-block; + @apply text-xs px-2.5 py-1 rounded-full transition-colors; + background: rgb(var(--surface-2)); + color: rgb(var(--mute)); + } + .tag:hover { + background: rgb(var(--accent-soft)); + color: rgb(var(--accent-2)); + } + + .surface { background: rgb(var(--surface)); } + .surface-2 { background: rgb(var(--surface-2)); } + .ink { color: rgb(var(--ink)); } + .mute { color: rgb(var(--mute)); } + .accent { color: rgb(var(--accent)); } + .border-soft { border-color: rgb(var(--border)); } + .border-b-soft { border-bottom: 1px solid rgb(var(--border)); } + .border-t-soft { border-top: 1px solid rgb(var(--border)); } } diff --git a/app/layout.js b/app/layout.js index 319723a..7b5e54c 100644 --- a/app/layout.js +++ b/app/layout.js @@ -14,10 +14,22 @@ export const metadata = { }, }; +// Защита от FOUC: ставим тему до первого рендера +const themeInitScript = ` +(function() { + try { + var saved = localStorage.getItem('theme'); + var theme = saved || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); + if (theme === 'dark') document.documentElement.classList.add('dark'); + } catch(e) {} +})(); +`; + export default function RootLayout({ children }) { return ( - + +