init: Vite+React+Tailwind v2 site with HTML content from WP, RSS feed, external feed aggregator, prerender

This commit is contained in:
striker
2026-05-21 01:11:26 +03:00
commit 76cdeb8b48
42 changed files with 6317 additions and 0 deletions

20
src/pages/Category.jsx Normal file
View File

@@ -0,0 +1,20 @@
import React from 'react';
import { postsList, categories } from '../content';
import PostCard from '../components/PostCard';
export default function Category({ slug }) {
const cat = categories.find((c) => c.slug === slug);
const filtered = postsList.filter((p) => p.categorySlugs?.includes(slug));
return (
<div>
<h1 className="font-serif text-2xl font-bold mb-2">
Категория: {cat?.name || slug}
</h1>
<div className="text-xs text-muted mb-6 pb-4 border-b border-rule">
{filtered.length} {filtered.length === 1 ? 'запись' : filtered.length < 5 ? 'записи' : 'записей'}
</div>
{filtered.map((p) => <PostCard key={p.slug} post={p} />)}
{filtered.length === 0 && <p className="text-muted">В этой категории пока нет записей.</p>}
</div>
);
}

13
src/pages/Home.jsx Normal file
View File

@@ -0,0 +1,13 @@
import React from 'react';
import { postsList } from '../content';
import PostCard from '../components/PostCard';
export default function Home() {
return (
<div>
{postsList.map((p) => (
<PostCard key={p.slug} post={p} />
))}
</div>
);
}

54
src/pages/News.jsx Normal file
View File

@@ -0,0 +1,54 @@
import React, { useEffect, useState } from 'react';
function formatDate(s) {
const d = new Date(s);
if (Number.isNaN(d.getTime())) return s;
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' });
}
export default function News() {
const [state, setState] = useState({ loading: true, items: [], error: null });
useEffect(() => {
fetch('/api/news.json', { cache: 'no-store' })
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))))
.then((data) => setState({ loading: false, items: data.items || [], error: null }))
.catch((e) => setState({ loading: false, items: [], error: e.message }));
}, []);
return (
<div>
<h1 className="font-serif text-3xl font-bold mb-2">Новости</h1>
<p className="text-xs text-muted mb-6 pb-4 border-b border-rule">
Агрегатор новостей о Пушкино из внешних источников. Обновляется автоматически.
</p>
{state.loading && <p className="text-muted">Загружаем новости</p>}
{state.error && (
<p className="text-muted">
Не удалось загрузить новости. Загляните позже.
</p>
)}
{!state.loading && !state.error && state.items.length === 0 && (
<p className="text-muted">Пока нет новостей.</p>
)}
<ul className="space-y-6">
{state.items.map((item, i) => (
<li key={item.guid || item.link || i} className="border-b border-rule pb-4 last:border-0">
<a href={item.link} target="_blank" rel="noopener noreferrer" className="font-serif text-lg font-bold text-ink hover:text-accent no-underline">
{item.title}
</a>
<div className="text-xs text-muted mt-1">
{item.source && <span>{item.source}</span>}
{item.pubDate && <span> · {formatDate(item.pubDate)}</span>}
</div>
{item.description && (
<p className="text-sm text-ink/80 mt-2">{item.description}</p>
)}
</li>
))}
</ul>
</div>
);
}

11
src/pages/NotFound.jsx Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react';
export default function NotFound() {
return (
<div className="py-12 text-center">
<h1 className="font-serif text-4xl font-bold mb-2">404</h1>
<p className="text-muted mb-6">Такой страницы здесь нет.</p>
<a href="/" className="text-accent">На главную</a>
</div>
);
}

12
src/pages/Page.jsx Normal file
View File

@@ -0,0 +1,12 @@
import React from 'react';
export default function Page({ page }) {
return (
<article>
<h1 className="font-serif text-3xl font-bold leading-tight mb-6 pb-4 border-b border-rule">
{page.title}
</h1>
<div className="prose-article" dangerouslySetInnerHTML={{ __html: page.html }} />
</article>
);
}

30
src/pages/Post.jsx Normal file
View File

@@ -0,0 +1,30 @@
import React from 'react';
function formatDate(s) {
const d = new Date(s.replace(' ', 'T'));
if (Number.isNaN(d.getTime())) return s;
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
}
export default function Post({ post }) {
return (
<article>
<h1 className="font-serif text-3xl font-bold leading-tight mb-2">{post.title}</h1>
<div className="text-xs text-muted mb-6 pb-4 border-b border-rule">
<time dateTime={post.date}>{formatDate(post.date)}</time>
{post.categories?.length > 0 && (
<>
{' · '}
{post.categories.map((c, i) => (
<span key={c}>
{i > 0 && ', '}
<a href={`/cat/${post.categorySlugs[i]}/`} className="text-muted hover:text-accent">{c}</a>
</span>
))}
</>
)}
</div>
<div className="prose-article" dangerouslySetInnerHTML={{ __html: post.html }} />
</article>
);
}