add client rest api

This commit is contained in:
Profile Profile
2026-02-15 23:05:45 +03:00
parent 131deebffc
commit abcc214ef6
4 changed files with 309 additions and 269 deletions

View File

@@ -1,275 +1,257 @@
---
import { getLatestAnews } from '../lib/api/posts.js';
import { fetchWPRestGet } from "@/lib/api/wp-rest-get-client";
// Функции для работы с датами
// Даты/время
function formatDate(dateString: string): string {
const date = new Date(dateString);
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
if (date.toDateString() === today.toDateString()) {
return 'Сегодня';
}
if (date.toDateString() === yesterday.toDateString()) {
return 'Вчера';
}
return date.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
if (date.toDateString() === today.toDateString()) return 'Сегодня';
if (date.toDateString() === yesterday.toDateString()) return 'Вчера';
return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', year: 'numeric' });
}
function formatTime(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleTimeString('ru-RU', {
hour: '2-digit',
minute: '2-digit'
});
return date.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
}
function groupByDate(posts: Array<{ date: string }>) {
const grouped: Record<string, Array<any>> = {};
posts.forEach(post => {
const date = new Date(post.date);
const dateKey = date.toISOString().split('T')[0];
if (!grouped[dateKey]) {
grouped[dateKey] = [];
}
grouped[dateKey].push(post);
posts.forEach((post) => {
const d = new Date(post.date);
const key = d.toISOString().split('T')[0];
(grouped[key] ??= []).push(post);
});
const sortedEntries = Object.entries(grouped).sort((a, b) =>
b[0].localeCompare(a[0])
);
return Object.fromEntries(sortedEntries);
return Object.fromEntries(Object.entries(grouped).sort((a, b) => b[0].localeCompare(a[0])));
}
// Получаем данные при сборке (используем правильное имя функции)
const posts = await getLatestAnews(20);
const groupedPosts = groupByDate(posts);
const totalPosts = posts.length;
// Новости
const newsPosts = await getLatestAnews(20);
const groupedNews = groupByDate(newsPosts);
// Топ-10
type TopItem = { uri?: string; link?: string; title: string; date: string };
type ApiResp = { top?: { items?: TopItem[] } };
let topPosts: TopItem[] = [];
try {
const data = await fetchWPRestGet<ApiResp>("my/v1/news-top", { per_page: 20 });
topPosts = (data?.top?.items ?? []).slice(0, 10);
} catch {
topPosts = [];
}
const hasNews = newsPosts.length > 0;
const hasTop = topPosts.length > 0;
// По умолчанию открываем вкладку, где есть данные
const defaultTab: "news" | "top" = hasTop ? "top" : "news";
---
<!-- HTML разметка -->
<div class="endnews-container">
<!-- Шапка с заголовком и счетчиком -->
<div class="endnews-header">
<h4 class="endnews-title">Новости</h4>
</div>
{(hasNews || hasTop) && (
<div class="endnews-container">
<div class="endnews-header">
<h4 class="endnews-title">Новости</h4>
</div>
<!-- Список новостей с группировкой по датам -->
<div class="latestnews-list">
{Object.entries(groupedPosts).map(([dateKey, datePosts]) => {
const dateStr = new Date(dateKey).toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
return (
<div class="latestnews-date-group" key={dateKey}>
{/* Radio-кнопки ВНЕ .endnews-tabs для работы CSS-селекторов */}
<input
class="endnews-tab-input"
type="radio"
name="endnews-tab"
id="endnews-tab-news"
checked={defaultTab === "news"}
/>
<input
class="endnews-tab-input"
type="radio"
name="endnews-tab"
id="endnews-tab-top"
checked={defaultTab === "top"}
/>
{/* Только labels в блоке табов */}
<div class="endnews-tabs">
<label class="endnews-tab-label" for="endnews-tab-news">Новости</label>
<label class="endnews-tab-label" for="endnews-tab-top">Топ10</label>
</div>
{/* Контент: две панели, показываем нужную через :checked */}
<div class="latestnews-list">
<section class="endnews-panel endnews-panel--news" aria-label="Новости">
{Object.entries(groupedNews).map(([dateKey, datePosts]) => (
<div class="latestnews-date-group" key={dateKey}>
<div class="latestnews-date">{formatDate(dateKey + 'T00:00:00')}</div>
<!-- Список новостей за эту дату -->
{datePosts.map((post, index) => {
const postNumber = index + 1;
const isLastInGroup = postNumber === datePosts.length;
return (
{datePosts.map((post) => (
<article class="lastnews-item" key={post.uri}>
<div class="lastnews-time">
{formatTime(post.date)}
</div>
<!-- Заголовок новости -->
<div class="lastnews-time">{formatTime(post.date)}</div>
<div class="lastnews-content">
<a
href={post.uri || '#'}
class="endnews-link"
>
{post.title}
</a>
<a href={post.uri || '#'} class="endnews-link">
{post.title}
</a>
</div>
</article>
);
})}
</div>
);
})}
))}
</div>
))}
</section>
<section class="endnews-panel endnews-panel--top" aria-label="Топ-10">
{topPosts.map((post, i) => (
<article class="lastnews-item" key={(post.uri ?? post.link ?? post.title) + i}>
<div class="ltopnews-content">
<a href={post.uri ?? post.link ?? '#'} class="endnews-link">
{post.title}
</a>
</div>
</article>
))}
{!hasTop && <div class="endnews-empty">Топ пока пуст</div>}
</section>
</div>
</div>
</div>
)}
<style>
.endnews-container { width:100%; }
/* Базовые стили */
.endnews-container {
width: 100%;
.endnews-header{
padding:12px 16px;
background:#B61D1D;
border-radius:6px 6px 0 0;
}
.endnews-title{ color:#fff; margin:0; font-size:1.1rem; font-weight:600; }
/* Скрываем radio-кнопки */
.endnews-tab-input{
position:absolute;
opacity:0;
pointer-events:none;
}
.endnews-header {
padding: 12px 16px;
background: #B61D1D;
border-radius: 6px 6px 0 0;
/* Tabs */
.endnews-tabs{
display:flex;
gap:8px;
padding:10px 12px;
border:1px solid #ECECEC;
border-top:none;
background:#fff;
}
.endnews-tab-label{
cursor:pointer;
user-select:none;
font-size:0.85rem;
font-weight:700;
padding:8px 10px;
border-radius:6px;
background:#f5f5f5;
color:#505258;
transition: background 0.2s, color 0.2s;
}
.endnews-title {
color: white;
margin: 0;
font-size: 1.1rem;
font-weight: 600;
/* Активный таб - используем общий сиблинг-селектор */
#endnews-tab-news:checked ~ .endnews-tabs label[for="endnews-tab-news"],
#endnews-tab-top:checked ~ .endnews-tabs label[for="endnews-tab-top"]{
background:#B61D1D;
color:#fff;
}
.latestnews-list {
display: flex;
flex-direction: column;
border: 1px solid #ECECEC;
border-top: none;
border-radius: 0 0 6px 6px;
background: white;
/* list container */
.latestnews-list{
border:1px solid #ECECEC;
border-top:none;
border-radius:0 0 6px 6px;
background:#fff;
}
.latestnews-date-group {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
/* Панели скрыты по умолчанию */
.endnews-panel{ display:none; }
/* Показываем нужную панель по выбранному radio */
#endnews-tab-news:checked ~ .latestnews-list .endnews-panel--news{ display:block; }
#endnews-tab-top:checked ~ .latestnews-list .endnews-panel--top { display:block; }
/* Существующий стиль списка */
.latestnews-date-group{ padding:12px 16px; border-bottom:1px solid #f0f0f0; }
.latestnews-date-group:last-child{ border-bottom:none; }
.latestnews-date{
font-size:0.625rem;
line-height:1.1;
color:#505258;
margin:0 0 0.4375rem;
text-transform:uppercase;
letter-spacing:0.5px;
}
.latestnews-date-group:last-child {
border-bottom: none;
.lastnews-item{
display:flex;
align-items:flex-start;
gap:12px;
margin-bottom:10px;
contain:content;
}
.lastnews-item:last-child{ margin-bottom:0; }
.lastnews-time{
font-size:0.75rem;
font-weight:700;
line-height:1.2;
color:#B61D1D;
min-width:2.5rem;
flex-shrink:0;
margin-top:1px;
}
.latestnews-date {
font-size: 0.625rem;
line-height: 1.1;
color: #505258;
margin: 0 0 0.4375rem;
text-transform: uppercase;
letter-spacing: 0.5px;
.lastnews-content{
line-height:1.3;
font-weight:500;
font-size:0.9em;
color:#000;
transition:color 0.2s;
flex:1;
}
.lastnews-item {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 10px;
contain: content;
.topnews-content{
line-height:1.3;
font-weight:500;
font-size:0.9em;
color:#000;
transition:color 0.2s;
margin-left: 4px;
}
.lastnews-item:last-child {
margin-bottom: 0;
.endnews-link{ color:inherit; text-decoration:none; display:block; }
.endnews-link:hover{ color:#B61D1D; }
.endnews-sub{
margin-top:4px;
font-size:0.75rem;
color:#6b6d72;
font-weight:500;
}
.endnews-empty{ padding:12px 16px; color:#6b6d72; font-size:0.85rem; }
.lastnews-time {
font-size: 0.75rem;
font-weight: 700;
line-height: 1.2;
color: #B61D1D;
min-width: 2.5rem;
flex-shrink: 0;
margin-top: 1px;
/* Десктоп скролл */
@media (min-width:1024px){
.endnews-container{ height:500px; display:flex; flex-direction:column; }
.latestnews-list{ flex:1; overflow-y:auto; overflow-x:hidden; }
}
.lastnews-content {
line-height: 1.3;
font-weight: 500;
font-size: 0.9em;
color: #000;
transition: color 0.2s;
flex: 1;
@media (max-width:1023px){
.endnews-container{ height:auto; }
.latestnews-list{ overflow:visible; }
}
</style>
.endnews-link {
color: inherit;
text-decoration: none;
display: block;
}
.endnews-link:hover {
color: #B61D1D;
}
/* Десктоп: фиксированная высота со скроллом */
@media (min-width: 1024px) {
.endnews-container {
height: 500px; /* Фиксированная высота */
display: flex;
flex-direction: column;
}
.latestnews-list {
flex: 1;
overflow-y: auto; /* Вертикальный скролл */
overflow-x: hidden;
}
/* Кастомный скроллбар для красоты */
.latestnews-list::-webkit-scrollbar {
width: 6px;
}
.latestnews-list::-webkit-scrollbar-track {
background: #f5f5f5;
border-radius: 3px;
}
.latestnews-list::-webkit-scrollbar-thumb {
background: #d0d0d0;
border-radius: 3px;
}
.latestnews-list::-webkit-scrollbar-thumb:hover {
background: #b0b0b0;
}
}
/* Мобильные устройства: автоматическая высота */
@media (max-width: 1023px) {
.endnews-container {
height: auto; /* Автоматическая высота */
}
.latestnews-list {
max-height: none;
overflow: visible;
}
}
/* Планшеты (опционально) */
@media (min-width: 768px) and (max-width: 1023px) {
.endnews-container {
height: auto;
/* или можно задать другую фиксированную высоту для планшетов */
}
}
/* Анимация появления скролла (опционально) */
.latestnews-list {
scroll-behavior: smooth;
}
/* Улучшение для touch-устройств */
@media (hover: none) and (pointer: coarse) {
.endnews-link {
padding: 8px 0; /* Увеличение touch-зоны */
}
.lastnews-item {
margin: 12px;
}
}
</style>

View File

@@ -16,12 +16,13 @@ const category = mainPost?.categories?.nodes?.[0];
const categoryName = category?.name || '';
const categoryColor = category?.color || '#2271b1';
const authorName = mainPost?.author?.node?.name || '';
const formattedDate = postDate.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'short',
year: 'numeric'
}).replace(' г.', '');
---
<article
@@ -30,12 +31,12 @@ const formattedDate = postDate.toLocaleDateString('ru-RU', {
itemtype="https://schema.org/Article"
itemid={mainPost.uri}
>
<a
href={mainPost.uri}
class="post-card-link"
aria-label={`Читать статью: ${mainPost.title}`}
>
<div class="image-container">
<div class="image-container">
<a
href={mainPost.uri}
class="image-link"
aria-label={`Читать статью: ${mainPost.title}`}
>
{imageUrl ? (
<img
src={imageUrl}
@@ -49,35 +50,37 @@ const formattedDate = postDate.toLocaleDateString('ru-RU', {
) : (
<div class="image-placeholder" aria-hidden="true"></div>
)}
</a>
{categoryName && (
<div
class="category-badge"
style={`background-color: ${categoryColor}`}
>
{categoryName}
</div>
)}
<div class="content-overlay">
<div class="meta-overlay">
<time datetime={isoDate} class="date-overlay">
{formattedDate}
</time>
</div>
{categoryName && (
<div
class="category-badge"
style={`background-color: ${categoryColor}`}
>
{categoryName}
<h2 class="title-overlay">
<a href={mainPost.uri} class="title-link">
{mainPost.title}
</a>
</h2>
{authorName && (
<div class="author-overlay">
<Author post={mainPost} separator=" " />
</div>
)}
<div class="content-overlay">
<div class="meta-overlay">
<time datetime={isoDate} class="date-overlay">
{formattedDate}
</time>
</div>
<h2 class="title-overlay">
{mainPost.title}
</h2>
{authorName && (
<div class="author-overlay">
<Author post={mainPost} separator=" " />
</div>
)}
</div>
</div>
</a>
</div>
<meta itemprop="url" content={mainPost.uri} />
<meta itemprop="datePublished" content={isoDate} />
@@ -89,20 +92,12 @@ const formattedDate = postDate.toLocaleDateString('ru-RU', {
/* ОСНОВНОЕ: ограничиваем ширину виджета */
.main-post-widget {
width: 100%;
max-width: 800px; /* Вот это ключевое правило! */
margin: 0 auto; /* Центрируем */
border-radius: 8px;
overflow: hidden;
background: white;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.post-card-link {
display: block;
text-decoration: none;
color: inherit;
}
.image-container {
position: relative;
width: 100%;
@@ -110,6 +105,14 @@ const formattedDate = postDate.toLocaleDateString('ru-RU', {
overflow: hidden;
}
.image-link {
display: block;
width: 100%;
height: 100%;
text-decoration: none;
color: inherit;
}
.post-image {
width: 100%;
height: 100%;
@@ -117,7 +120,7 @@ const formattedDate = postDate.toLocaleDateString('ru-RU', {
transition: transform 0.3s ease;
}
.post-card-link:hover .post-image {
.image-link:hover .post-image {
transform: scale(1.03);
}
@@ -164,7 +167,16 @@ const formattedDate = postDate.toLocaleDateString('ru-RU', {
font-size: 1.5rem;
font-weight: 700;
line-height: 1.3;
}
.title-link {
color: white;
text-decoration: none;
transition: opacity 0.2s ease;
}
.title-link:hover {
opacity: 0.9;
}
.author-overlay {
@@ -177,7 +189,7 @@ const formattedDate = postDate.toLocaleDateString('ru-RU', {
@media (max-width: 1023px) {
.main-post-widget {
border-radius: 0;
max-width: 100%; /* На мобильных занимает всю ширину */
max-width: 100%;
}
.content-overlay {
@@ -188,4 +200,4 @@ const formattedDate = postDate.toLocaleDateString('ru-RU', {
font-size: 1.25rem;
}
}
</style>
</style>