add files

This commit is contained in:
Andrey Kuvshinov
2025-12-11 01:12:45 +03:00
commit 22358272c6
31 changed files with 7392 additions and 0 deletions

View File

@@ -0,0 +1,117 @@
---
export interface Props {
items: any[];
showCount?: boolean;
}
const {
items = [],
showCount = false,
} = Astro.props;
---
<section class="posts-section" id="posts-section">
<h2>
{showCount && items.length > 0 && (
<span id="posts-count"> ({items.length})</span>
)}
</h2>
<div id="posts-grid" class="posts-grid">
{items.map((item, index) => {
const postUrl = item.uri || `/blog/${item.databaseId}`;
const postDate = new Date(item.date);
// Логика для больших плиток на десктопе
let isLarge = false;
let largePosition = '';
const rowNumber = Math.floor(index / 4) + 1;
const positionInRow = index % 4;
if (index >= 8) {
const largeRowStart = (rowNumber - 3) % 3 === 0 && rowNumber >= 3;
if (largeRowStart && positionInRow === 0) {
isLarge = true;
largePosition = 'first';
}
}
return (
<article
class={`post-card ${isLarge ? 'post-card-large' : ''}`}
data-large-position={isLarge ? largePosition : ''}
data-index={index}
itemscope
itemtype="https://schema.org/BlogPosting"
>
<a href={postUrl} class="post-card-link">
<div class="post-image-container">
{item.featuredImage?.node?.sourceUrl ? (
<img loading="lazy"
src={item.featuredImage.node.sourceUrl}
alt={item.featuredImage.node.altText || item.title}
width="400"
height="400"
loading="lazy"
class="post-image"
itemprop="image"
/>
) : (
<div class="post-image-placeholder"></div>
)}
{/* Рубрика в верхнем правом углу */}
{item.categories?.nodes?.[0]?.name && (
<div class="post-category-badge">
{item.categories.nodes[0].name}
</div>
)}
{/* Оверлей с контентом */}
<div class="post-content-overlay">
<div class="post-meta-overlay">
<time
datetime={item.date}
class="post-date-overlay"
itemprop="datePublished"
>
{postDate.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'short',
year: 'numeric'
}).replace(' г.', '')}
</time>
</div>
<h3 class="post-title-overlay" itemprop="headline">
{item.title}
</h3>
{item.author?.node?.name && (
<div class="author-name" itemprop="author">
{item.author.node.name}
</div>
)}
</div>
</div>
</a>
{/* Скринридеру и SEO */}
<div class="sr-only">
<h3 itemprop="headline">
<a href={postUrl} itemprop="url">{item.title}</a>
</h3>
<time
datetime={item.date}
itemprop="datePublished"
>
{postDate.toLocaleDateString('ru-RU')}
</time>
</div>
</article>
);
})}
</div>
</section>

View File

@@ -0,0 +1,275 @@
---
import { getLatestAnews } from '../lib/api/posts.js';
// Функции для работы с датами
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'
});
}
function formatTime(dateString: string): string {
const date = new Date(dateString);
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);
});
const sortedEntries = Object.entries(grouped).sort((a, b) =>
b[0].localeCompare(a[0])
);
return Object.fromEntries(sortedEntries);
}
// Получаем данные при сборке (используем правильное имя функции)
const posts = await getLatestAnews(20);
const groupedPosts = groupByDate(posts);
const totalPosts = posts.length;
---
<!-- HTML разметка -->
<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}>
<div class="latestnews-date">{formatDate(dateKey + 'T00:00:00')}</div>
<!-- Список новостей за эту дату -->
{datePosts.map((post, index) => {
const postNumber = index + 1;
const isLastInGroup = postNumber === datePosts.length;
return (
<article class="lastnews-item" key={post.uri}>
<div class="lastnews-time">
{formatTime(post.date)}
</div>
<!-- Заголовок новости -->
<div class="lastnews-content">
<a
href={post.uri || '#'}
class="endnews-link"
>
{post.title}
</a>
</div>
</article>
);
})}
</div>
);
})}
</div>
</div>
<style>
/* Базовые стили */
.endnews-container {
width: 100%;
}
.endnews-header {
padding: 12px 16px;
background: #B61D1D;
border-radius: 6px 6px 0 0;
}
.endnews-title {
color: white;
margin: 0;
font-size: 1.1rem;
font-weight: 600;
}
.latestnews-list {
display: flex;
flex-direction: column;
border: 1px solid #ECECEC;
border-top: none;
border-radius: 0 0 6px 6px;
background: white;
}
.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;
}
.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;
}
.lastnews-content {
line-height: 1.3;
font-weight: 500;
font-size: 0.9em;
color: #000;
transition: color 0.2s;
flex: 1;
}
.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>

365
src/components/Footer.astro Normal file
View File

@@ -0,0 +1,365 @@
---
interface Props {
publicationName: string;
organization: string;
menuItems: Array<{
text: string;
url: string;
}>;
}
const { publicationName, organization, menuItems } = Astro.props;
// Получаем текущий год автоматически
const currentYear = new Date().getFullYear();
const footerId = `footer-profile`;
---
<footer id={footerId} class="footer">
<!-- Свернутое состояние -->
<div class="footer__collapsed">
<button
class="footer__toggle"
onclick="toggleFooter(this)"
aria-label="Развернуть футер"
aria-expanded="false"
aria-controls="footer-expanded-${footerId}"
>
<span class="footer__publication-name">
<a class="footer__logo" href="/">
<img loading="lazy" class="footer-logo" src="https://cdn.profile.ru/wp-content/themes/profile/assets/img/profil-logo-footer.png" width="136" height="30" alt="">
</a>
</span>
<svg
class="footer__arrow"
width="20"
height="20"
viewBox="0 0 20 20"
aria-hidden="true"
>
<!-- Стрелка вверх (направлена вверх в свернутом состоянии) -->
<path
d="M5 12.5L10 7.5L15 12.5"
stroke="currentColor"
stroke-width="2"
fill="none"
/>
</svg>
<span class="footer__copyright-collapsed">
© {currentYear}, {organization}
</span>
</button>
</div>
<!-- Раскрытое состояние -->
<div
id="footer-expanded-${footerId}"
class="footer__expanded"
hidden
>
<!-- Меню -->
<nav class="footer__menu" aria-label="Дополнительная навигация">
<ul class="footer__menu-list">
{menuItems.map((item) => (
<li class="footer__menu-item" key={item.url}>
<a
href={item.url}
class="footer__menu-link"
>
{item.text}
</a>
</li>
))}
</ul>
</nav>
<span class="footer__age">16+</span>
<!-- Полный копирайт -->
<div class="footer__copyright-expanded">
<p>
© {currentYear} {organization}. Все права защищены.
Информационное агентство "Деловой журнал "Профиль" зарегистрировано в Федеральной службе по надзору в сфере связи, информационных технологий и массовых коммуникаций. Свидетельство о государственной регистрации серии ИА № ФС 77 - 89668 от 23.06.2025
</p>
<ul class="menu-conf-docs">
<li><a href="">Положение об обработке и защите персональных данных</a></li>
<li><a href="">Политика конфиденциальности</a></li>
<li><a href="">Правила применения рекомендательных технологий</a></li>
</ul>
</div>
<!-- Кнопка сворачивания -->
<div class="footer__expanded-bottom">
<button
class="footer__collapse-btn"
onclick="toggleFooter(document.querySelector('#${footerId} .footer__toggle'))"
aria-label="Свернуть футер"
>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
aria-hidden="true"
>
<!-- Стрелка вниз (направлена вниз в развернутом состоянии) -->
<path
d="M5 7.5L10 12.5L15 7.5"
stroke="currentColor"
stroke-width="2"
fill="none"
/>
</svg>
<span>Свернуть</span>
</button>
</div>
</div>
</footer>
<style is:global>
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #303030;
border-top: 2px solid #404040;
transition: all 0.3s ease;
z-index: 100;
box-shadow: 0 -2px 15px rgba(0, 0, 0, 0.3);
color: #ffffff;
}
/* Свернутое состояние */
.footer__collapsed {
padding: 12px 0;
display: flex;
align-items: center;
justify-content: center;
}
.footer__toggle {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
max-width: 1200px;
background: none;
border: none;
cursor: pointer;
padding: 8px 16px;
transition: all 0.2s ease;
border-radius: 8px;
font-family: inherit;
font-size: inherit;
color: #ffffff;
}
.footer__toggle:hover {
background: rgba(255, 255, 255, 0.1);
}
.footer__publication-name {
font-size: 1.1rem;
font-weight: 600;
color: #ffffff;
flex: 1;
text-align: center;
margin: 0 20px;
}
.footer__arrow {
transition: transform 0.3s ease;
color: #cccccc;
}
/* При развернутом состоянии стрелка поворачивается на 180° (вниз) */
.footer__toggle[aria-expanded="true"] .footer__arrow {
transform: rotate(180deg);
}
.footer__copyright-collapsed {
font-size: 0.9rem;
color: #cccccc;
flex: 1;
text-align: right;
margin: 0 20px;
}
/* Раскрытое состояние */
.footer__expanded {
max-height: 0;
opacity: 0;
overflow: hidden;
transition: all 0.3s ease;
padding: 0 20px;
background: #303030;
}
.footer__expanded:not([hidden]) {
max-height: 400px;
opacity: 1;
padding: 20px;
border-top: 1px solid #404040;
}
.footer__menu {
margin-bottom: 20px;
}
.footer__menu-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 20px;
justify-content: center;
}
.footer__menu-item {
margin: 0;
}
.footer__menu-link {
color: #ffffff;
text-decoration: none;
font-size: 1rem;
padding: 8px 12px;
border-radius: 6px;
transition: all 0.2s ease;
}
.footer__menu-link:hover {
background: rgba(255, 255, 255, 0.15);
color: #ffffff;
}
.footer__copyright-expanded {
color: #cccccc;
font-size: 0.9rem;
line-height: 1.5;
margin-bottom: 20px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
}
.footer__copyright-expanded p {
margin: 0;
}
.footer__expanded-bottom {
display: flex;
justify-content: center;
}
.footer__collapse-btn {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.1);
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
color: #ffffff;
transition: all 0.2s ease;
font-family: inherit;
}
.footer__collapse-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.menu-conf-docs{
margin-top: 12px;
}
.footer__age {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background-color: #ffffff;
color: #000000;
border-radius: 50%;
font-size: 14px;
font-weight: 700;
border: 2px solid #404040;
margin-left: 10px;
flex-shrink: 0;
line-height: 1;
}
/* Адаптивность */
@media (max-width: 768px) {
.footer__toggle {
flex-direction: column;
gap: 8px;
text-align: center;
}
.footer__publication-name,
.footer__copyright-collapsed {
margin: 4px 0;
text-align: center;
flex: none;
width: 100%;
}
.footer__menu-list {
flex-direction: column;
align-items: center;
gap: 12px;
}
.footer__copyright-expanded {
font-size: 0.85rem;
}
}
/* Анимация для стрелки */
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-2px);
}
}
.footer__toggle:hover .footer__arrow {
animation: bounce 0.5s ease;
}
</style>
<script is:inline>
function toggleFooter(button) {
const footer = button.closest('.footer');
const expandedSection = footer.querySelector('.footer__expanded');
const isExpanded = button.getAttribute('aria-expanded') === 'true';
// Обновляем состояние кнопки
button.setAttribute('aria-expanded', !isExpanded);
button.setAttribute('aria-label', isExpanded ? 'Развернуть футер' : 'Свернуть футер');
// Показываем/скрываем контент
if (isExpanded) {
expandedSection.setAttribute('hidden', '');
} else {
expandedSection.removeAttribute('hidden');
}
}
</script>

View File

@@ -0,0 +1,53 @@
<div class="current-date" id="current-date">
<!-- Дата будет обновляться в полночь -->
</div>
<script>
function updateDate() {
const element = document.getElementById('current-date');
if (!element) return;
const today = new Date();
const dateStr = today.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
element.textContent = dateStr;
}
// Функция для расчета времени до следующей полночи
function getTimeUntilMidnight() {
const now = new Date();
const midnight = new Date();
midnight.setHours(24, 0, 0, 0); // Следующая полночь
return midnight.getTime() - now.getTime();
}
// Обновляем дату сразу
updateDate();
// Устанавливаем таймер на обновление в полночь
const timeUntilMidnight = getTimeUntilMidnight();
setTimeout(function() {
updateDate();
// После первой полночи ставим ежедневное обновление
setInterval(updateDate, 24 * 60 * 60 * 1000);
}, timeUntilMidnight);
// Дополнительно: обновляем при возвращении на вкладку после полуночи
document.addEventListener('visibilitychange', function() {
if (!document.hidden) {
const now = new Date();
const lastUpdate = new Date(localStorage.getItem('dateLastUpdate') || 0);
// Если прошло больше 12 часов с последнего обновления
if (now - lastUpdate > 12 * 60 * 60 * 1000) {
updateDate();
localStorage.setItem('dateLastUpdate', now.toISOString());
}
}
});
</script>

View File

@@ -0,0 +1,27 @@
---
import Stores from './LazyStores.astro';
const MENU_ID = 103245;
let menuItems = [];
---
<header class="header" itemscope itemtype="https://schema.org/WPHeader">
<div class="top-bar">
<img alt="Профиль" width="249" height="55" src="https://cdn.profile.ru/wp-content/themes/profile/assets/img/profile-logo-delovoy.svg">
<Stores />
</div>
</header>
<style>
.top-bar{
display: flex;
justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,76 @@
---
// Иконки загрузятся после DOM
---
<div class="header-stores" id="stores-container">
</div>
<script>
// Ждем полной загрузки DOM
document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('stores-container');
if (!container) return;
// Создаем HTML с иконками
const storesHTML = `
<a class="float-xs-none float-md-left header__store"
href="https://apps.apple.com"
target="_blank"
rel="noopener noreferrer">
<img loading="lazy"
src="https://cdn.profile.ru/wp-content/themes/profile/assets/img/appstore.svg"
width="103" height="32"
alt="AppStore">
</a>
<a class="float-xs-none float-md-left header__store"
href="https://play.google.com"
target="_blank"
rel="noopener noreferrer">
<img loading="lazy"
src="https://cdn.profile.ru/wp-content/themes/profile/assets/img/googleplay.png"
width="115" height="32"
alt="Google Play">
</a>
<a class="float-xs-none float-md-left header__store"
href="https://apps.rustore.ru"
target="_blank"
rel="noopener noreferrer">
<img loading="lazy"
src="https://cdn.profile.ru/wp-content/themes/profile/assets/img/rustore.svg"
width="98" height="32"
alt="RuStore">
</a>
`;
// Вставляем иконки
container.innerHTML = storesHTML;
});
</script>
<style>
.header-stores {
display: flex;
gap: 1rem;
align-items: center;
margin-left: auto;
}
.header__store {
display: inline-block;
transition: transform 0.2s ease;
}
.header__store:hover {
transform: translateY(-2px);
}
.header__store img {
display: block;
height: auto;
}
</style>

View File

View File

@@ -0,0 +1,21 @@
<div class="header__social clearfix">
<div class="header-stores">
<a class="float-xs-none float-md-left header__store" href="https://apps.apple.com/ru/app/id6476872853" target="_blank">
<img loading="lazy" src="https://cdn.profile.ru/wp-content/themes/profile/assets/img/appstore.svg" width="103" height="32" alt="AppStore">
</a>
<a class="float-xs-none float-md-left header__store" href="https://play.google.com/store/apps/details?id=com.profile.magazine" target="_blank">
<img loading="lazy" src="https://cdn.profile.ru/wp-content/themes/profile/assets/img/googleplay.png" width="115" height="32" alt="Google Play">
</a>
<a class="float-xs-none float-md-left header__store" href="https://apps.rustore.ru/app/com.profile.magazine" target="_blank">
<img loading="lazy" src="https://cdn.profile.ru/wp-content/themes/profile/assets/img/rustore.svg" width="98" height="32" alt="Google Play">
</a>
</div>
</div>
<style>
.header-stores{
display: flex;
gap: 1rem;
}
</style>