add files
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# jetbrains setting folder
|
||||||
|
.idea/
|
||||||
4
.vscode/extensions.json
vendored
Normal file
4
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
||||||
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
43
README.md
Normal file
43
README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Astro Starter Kit: Minimal
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm create astro@latest -- --template minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||||
|
|
||||||
|
## 🚀 Project Structure
|
||||||
|
|
||||||
|
Inside of your Astro project, you'll see the following folders and files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/
|
||||||
|
├── public/
|
||||||
|
├── src/
|
||||||
|
│ └── pages/
|
||||||
|
│ └── index.astro
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||||
|
|
||||||
|
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||||
|
|
||||||
|
Any static assets, like images, can be placed in the `public/` directory.
|
||||||
|
|
||||||
|
## 🧞 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :------------------------ | :----------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||||
|
|
||||||
|
## 👀 Want to learn more?
|
||||||
|
|
||||||
|
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||||
11
astro.config.mjs
Normal file
11
astro.config.mjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// @ts-check
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
import node from '@astrojs/node';
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
adapter: node({
|
||||||
|
mode: 'standalone'
|
||||||
|
})
|
||||||
|
});
|
||||||
5271
package-lock.json
generated
Normal file
5271
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
package.json
Normal file
15
package.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/node": "^9.5.1",
|
||||||
|
"astro": "^5.16.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
public/favicon.svg
Normal file
9
public/favicon.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 749 B |
117
src/components/ContentGrid.astro
Normal file
117
src/components/ContentGrid.astro
Normal 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>
|
||||||
275
src/components/EndnewsList.astro
Normal file
275
src/components/EndnewsList.astro
Normal 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
365
src/components/Footer.astro
Normal 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>
|
||||||
53
src/components/Header/CurrentDate.astro
Normal file
53
src/components/Header/CurrentDate.astro
Normal 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>
|
||||||
27
src/components/Header/Header.astro
Normal file
27
src/components/Header/Header.astro
Normal 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>
|
||||||
76
src/components/Header/LazyStores.astro
Normal file
76
src/components/Header/LazyStores.astro
Normal 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>
|
||||||
0
src/components/Header/MenuBar.astro
Normal file
0
src/components/Header/MenuBar.astro
Normal file
21
src/components/Header/Stores.astro
Normal file
21
src/components/Header/Stores.astro
Normal 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>
|
||||||
|
|
||||||
58
src/layouts/MainLayout.astro
Normal file
58
src/layouts/MainLayout.astro
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
const { title, description } = Astro.props;
|
||||||
|
|
||||||
|
import Header from '../components/Header/Header.astro';
|
||||||
|
import CurrentDate from '../components/Header/CurrentDate.astro';
|
||||||
|
import Footer from '../components/Footer.astro';
|
||||||
|
|
||||||
|
import '../styles/global.css';
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<title>{`${title}`}</title>
|
||||||
|
<meta name="description" content={description}>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header-info">
|
||||||
|
<div class="header__widgets">
|
||||||
|
<CurrentDate />
|
||||||
|
<div class="header__currency">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<Header />
|
||||||
|
<main>
|
||||||
|
<hr>
|
||||||
|
<slot></slot>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Footer
|
||||||
|
publicationName="ТехноВестник"
|
||||||
|
organization="Учредитель: ИДР. Все права защищены."
|
||||||
|
menuItems={[
|
||||||
|
{ text: "О нас", url: "/about" },
|
||||||
|
{ text: "Контакты", url: "/contacts" },
|
||||||
|
{ text: "Политика конфиденциальности", url: "/privacy" },
|
||||||
|
{ text: "Архив", url: "/archive" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
main {
|
||||||
|
width:100%;
|
||||||
|
max-width: 1024px;
|
||||||
|
margin:auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
24
src/lib/api/endnews.ts
Normal file
24
src/lib/api/endnews.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { fetchGraphQL } from './graphql-client.js';
|
||||||
|
|
||||||
|
interface EndnewsPost {
|
||||||
|
title: string;
|
||||||
|
uri: string;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLatestEndnews(count = 12): Promise<EndnewsPost[]> {
|
||||||
|
const query = `
|
||||||
|
query GetEndnews($count: Int!) {
|
||||||
|
endnews(first: $count, where: {orderby: {field: DATE, order: DESC}}) {
|
||||||
|
nodes {
|
||||||
|
title
|
||||||
|
uri
|
||||||
|
date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const data = await fetchGraphQL(query, { count });
|
||||||
|
return data.endnews?.nodes || [];
|
||||||
|
}
|
||||||
42
src/lib/api/graphql-client.ts
Normal file
42
src/lib/api/graphql-client.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// src/lib/api/graphql-client.ts
|
||||||
|
|
||||||
|
export async function fetchGraphQL<T = any>(
|
||||||
|
query: string,
|
||||||
|
variables: Record<string, any> = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const endpoint = import.meta.env.WP_GRAPHQL_ENDPOINT;
|
||||||
|
|
||||||
|
if (!endpoint) {
|
||||||
|
throw new Error("WP_GRAPHQL_ENDPOINT is not defined in environment variables");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ query, variables }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, errors } = await response.json();
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
console.error("GraphQL Errors:", errors);
|
||||||
|
throw new Error(errors[0]?.message || "GraphQL query failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error("GraphQL request failed:", error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new Error("Unknown error in GraphQL request");
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/lib/api/load-posts.ts
Normal file
49
src/lib/api/load-posts.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// src/pages/api/load-posts/index.ts
|
||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
|
||||||
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { after = null, first = 8 } = body;
|
||||||
|
|
||||||
|
// Динамический импорт
|
||||||
|
const { getLatestPosts } = await import('../../../../lib/api/posts.js');
|
||||||
|
|
||||||
|
const { posts, pageInfo } = await getLatestPosts(first, after);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
posts,
|
||||||
|
pageInfo
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'public, max-age=60'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading posts:', error);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to load posts',
|
||||||
|
posts: [],
|
||||||
|
pageInfo: { hasNextPage: false, endCursor: null }
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
49
src/lib/api/load-posts/index.ts
Normal file
49
src/lib/api/load-posts/index.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// src/pages/api/load-posts/index.ts
|
||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
|
||||||
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { after = null, first = 8 } = body;
|
||||||
|
|
||||||
|
// Динамический импорт
|
||||||
|
const { getLatestPosts } = await import('../../../../lib/api/posts.js');
|
||||||
|
|
||||||
|
const { posts, pageInfo } = await getLatestPosts(first, after);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
posts,
|
||||||
|
pageInfo
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'public, max-age=60'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading posts:', error);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to load posts',
|
||||||
|
posts: [],
|
||||||
|
pageInfo: { hasNextPage: false, endCursor: null }
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
50
src/lib/api/menu.ts
Normal file
50
src/lib/api/menu.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { fetchGraphQL } from './graphql-client.js';
|
||||||
|
import type { MenuItem } from '../types/graphql.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Функция ДЛЯ ГЛАВНОГО МЕНЮ (максимум 5 пунктов)
|
||||||
|
export async function getMainHeaderMenu(): Promise<MenuItem[]> {
|
||||||
|
const query = `
|
||||||
|
query GetMainHeaderMenu {
|
||||||
|
menu(id: "103245", idType: DATABASE_ID) {
|
||||||
|
menuItems(
|
||||||
|
first: 5, # Берем только 5
|
||||||
|
where: {parentId: null} # Только верхний уровень
|
||||||
|
) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
url
|
||||||
|
target
|
||||||
|
order
|
||||||
|
cssClasses
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetchGraphQL(query);
|
||||||
|
const items = data.menu?.menuItems?.nodes || [];
|
||||||
|
|
||||||
|
// Сортируем по order и гарантируем максимум 5
|
||||||
|
return items
|
||||||
|
.sort((a: MenuItem, b: MenuItem) => a.order - b.order)
|
||||||
|
.slice(0, 5);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки главного меню:', error);
|
||||||
|
|
||||||
|
// Запасной вариант на случай ошибки (ровно 5 пунктов)
|
||||||
|
return [
|
||||||
|
{ id: '1', label: 'Главная', url: '/', order: 0 },
|
||||||
|
{ id: '2', label: 'Каталог', url: '/catalog', order: 1 },
|
||||||
|
{ id: '3', label: 'О нас', url: '/about', order: 2 },
|
||||||
|
{ id: '4', label: 'Контакты', url: '/contacts', order: 3 },
|
||||||
|
{ id: '5', label: 'Блог', url: '/blog', order: 4 }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/lib/api/posts.ts
Normal file
99
src/lib/api/posts.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { fetchGraphQL } from './graphql-client.js';
|
||||||
|
import type { ProfileArticle } from '../types/graphql.js';
|
||||||
|
|
||||||
|
// lib/api/posts.js
|
||||||
|
export async function getLatestPosts(first = 12, after = null) {
|
||||||
|
const query = `
|
||||||
|
query GetLatestProfileArticles($first: Int!, $after: String) {
|
||||||
|
profileArticles(
|
||||||
|
first: $first,
|
||||||
|
after: $after,
|
||||||
|
where: {orderby: { field: DATE, order: DESC }}
|
||||||
|
) {
|
||||||
|
pageInfo {
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
edges {
|
||||||
|
cursor
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
databaseId
|
||||||
|
title
|
||||||
|
uri
|
||||||
|
date
|
||||||
|
featuredImage {
|
||||||
|
node {
|
||||||
|
sourceUrl(size: LARGE)
|
||||||
|
altText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
author {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
avatar {
|
||||||
|
url
|
||||||
|
}
|
||||||
|
uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
categories {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
uri
|
||||||
|
databaseId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const data = await fetchGraphQL(query, { first, after });
|
||||||
|
|
||||||
|
// Преобразуем edges в nodes
|
||||||
|
const posts = data.profileArticles?.edges?.map(edge => edge.node) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
posts,
|
||||||
|
pageInfo: data.profileArticles?.pageInfo || { hasNextPage: false, endCursor: null }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface AnewsPost {
|
||||||
|
title: string;
|
||||||
|
uri: string;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLatestAnews(count = 12): Promise<AnewsPost[]> {
|
||||||
|
const query = `
|
||||||
|
query GetAnews($count: Int!) {
|
||||||
|
aNews(first: $count, where: {orderby: {field: DATE, order: DESC}}) {
|
||||||
|
nodes {
|
||||||
|
title
|
||||||
|
uri
|
||||||
|
date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const data = await fetchGraphQL(query, { count });
|
||||||
|
return data.aNews?.nodes || []; // Исправлено: aNews вместо anews
|
||||||
|
}
|
||||||
22
src/lib/graphql-client.js
Normal file
22
src/lib/graphql-client.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export async function fetchGraphQL(query, variables = {}) {
|
||||||
|
const endpoint = import.meta.env.WP_GRAPHQL_ENDPOINT;
|
||||||
|
|
||||||
|
if (!endpoint) {
|
||||||
|
throw new Error("WP_GRAPHQL_ENDPOINT is not defined in .env");
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ query, variables }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.errors) {
|
||||||
|
console.error("GraphQL error:", json.errors);
|
||||||
|
throw new Error("GraphQL query failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.data;
|
||||||
|
}
|
||||||
62
src/lib/types/graphql.ts
Normal file
62
src/lib/types/graphql.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
export interface FeaturedImage {
|
||||||
|
node: {
|
||||||
|
sourceUrl: string;
|
||||||
|
altText: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Author {
|
||||||
|
node: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
avatar: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
uri: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Category {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
uri: string;
|
||||||
|
databaseId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Tag {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
uri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProfileArticle {
|
||||||
|
id: string;
|
||||||
|
databaseId: number;
|
||||||
|
title: string;
|
||||||
|
uri: string;
|
||||||
|
date: string;
|
||||||
|
content?: string;
|
||||||
|
excerpt?: string;
|
||||||
|
featuredImage: FeaturedImage;
|
||||||
|
author: Author;
|
||||||
|
categories: {
|
||||||
|
nodes: Category[];
|
||||||
|
};
|
||||||
|
tags: {
|
||||||
|
nodes: Tag[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Интерфейс для пункта меню
|
||||||
|
export interface MenuItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
url: string;
|
||||||
|
target?: string;
|
||||||
|
order: number;
|
||||||
|
cssClasses?: string[];
|
||||||
|
}
|
||||||
48
src/lib/wp-api.js
Normal file
48
src/lib/wp-api.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { fetchGraphQL } from "./graphql-client.js";
|
||||||
|
|
||||||
|
export async function getSiteInfo() {
|
||||||
|
const query = `
|
||||||
|
query GetSiteInfo {
|
||||||
|
generalSettings {
|
||||||
|
title
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const data = await fetchGraphQL(query);
|
||||||
|
|
||||||
|
return data.generalSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLatestPosts(limit = 10) {
|
||||||
|
const query = `
|
||||||
|
query GetLatestProfileArticles($limit: Int!) {
|
||||||
|
profileArticles(
|
||||||
|
first: $limit
|
||||||
|
where: {
|
||||||
|
orderby: { field: DATE, order: DESC }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
databaseId
|
||||||
|
title
|
||||||
|
uri
|
||||||
|
date
|
||||||
|
featuredImage {
|
||||||
|
node {
|
||||||
|
sourceUrl(size: LARGE)
|
||||||
|
altText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const data = await fetchGraphQL(query, { limit });
|
||||||
|
|
||||||
|
// Используем profileArticles вместо posts
|
||||||
|
return data.profileArticles.nodes;
|
||||||
|
}
|
||||||
33
src/pages/index.astro
Normal file
33
src/pages/index.astro
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
import { getSiteInfo } from "../lib/wp-api.js";
|
||||||
|
import { getLatestPosts } from '../lib/api/posts.js';
|
||||||
|
|
||||||
|
const site = await getSiteInfo();
|
||||||
|
const initialPosts = await getLatestPosts(36); // Начальная загрузка 12 постов
|
||||||
|
|
||||||
|
// визуальные компоненты
|
||||||
|
import MainLayout from '../layouts/MainLayout.astro';
|
||||||
|
import ContentGrid from '../components/ContentGrid.astro';
|
||||||
|
import EndnewsList from '../components/EndnewsList.astro';
|
||||||
|
|
||||||
|
export const prerender = {
|
||||||
|
isr: { expiration: 3 } // ISR: обновлять раз в 3 секунды
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<MainLayout
|
||||||
|
title={site.title}
|
||||||
|
description="Информационное агентство Деловой журнал Профиль"
|
||||||
|
>
|
||||||
|
<h1>{site.title}</h1>
|
||||||
|
{site.description && <p>{site.description}</p>}
|
||||||
|
|
||||||
|
<div class="maimnewsline">
|
||||||
|
<EndnewsList />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<ContentGrid items={initialPosts.posts} />
|
||||||
|
|
||||||
|
|
||||||
|
</MainLayout>
|
||||||
370
src/styles/components/ContentGrid.css
Normal file
370
src/styles/components/ContentGrid.css
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
/* styles/components/ContentGrid.css */
|
||||||
|
|
||||||
|
/* Глобальные стили (только для этого компонента) */
|
||||||
|
.posts-section {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-section h2 {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #333;
|
||||||
|
font-size: 24px;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ОСНОВНОЙ СТИЛЬ: ВСЕ ПЛИТКИ КВАДРАТНЫЕ */
|
||||||
|
.post-card {
|
||||||
|
flex: 1 0 calc(25% - 15px); /* 4 колонки по умолчанию */
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
min-width: 0;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Большие плитки на десктопе - ТОЖЕ КВАДРАТНЫЕ */
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.post-card-large {
|
||||||
|
flex: 0 0 calc(50% - 10px) !important; /* Занимает 2 колонки */
|
||||||
|
aspect-ratio: 2 / 1 !important; /* Ширина в 2 раза больше высоты */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card-link {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card:hover .post-image {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Бейдж рубрики */
|
||||||
|
.post-category-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
background: #2563eb;
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
z-index: 2;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||||
|
max-width: 80%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.post-card-large .post-category-badge {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Оверлей с текстом */
|
||||||
|
.post-content-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 20px 15px 15px;
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
rgba(0, 0, 0, 0.9) 0%,
|
||||||
|
rgba(0, 0, 0, 0.7) 50%,
|
||||||
|
rgba(0, 0, 0, 0) 100%
|
||||||
|
);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.post-card-large .post-content-overlay {
|
||||||
|
padding: 25px 20px 20px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-meta-overlay {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-date-overlay {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
opacity: 0.9;
|
||||||
|
display: block;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.post-card-large .post-date-overlay {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title-overlay {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
color: white;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.post-card-large .post-title-overlay {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.4;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
margin-top: 3px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.post-card-large .author-name {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Для скринридеров */
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для индикаторов загрузки */
|
||||||
|
.loading-indicator {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid #f3f3f3;
|
||||||
|
border-top: 3px solid #2563eb;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more-posts {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 16px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность */
|
||||||
|
|
||||||
|
/* Для ноутбуков: 3 в ряд (без больших плиток) */
|
||||||
|
@media (min-width: 992px) and (max-width: 1199px) {
|
||||||
|
.posts-grid {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card {
|
||||||
|
flex: 1 0 calc(33.333% - 14px);
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card-large {
|
||||||
|
flex: 1 0 calc(33.333% - 14px) !important;
|
||||||
|
aspect-ratio: 1 / 1 !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
order: initial !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Для планшетов: 2 в ряд */
|
||||||
|
@media (min-width: 768px) and (max-width: 991px) {
|
||||||
|
.posts-grid {
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card {
|
||||||
|
flex: 1 0 calc(50% - 10px);
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card-large {
|
||||||
|
flex: 1 0 calc(50% - 10px) !important;
|
||||||
|
aspect-ratio: 1 / 1 !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
order: initial !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title-overlay {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Для мобильных: 1 в ряд */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.posts-grid {
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card {
|
||||||
|
flex: 1 0 100%;
|
||||||
|
aspect-ratio: 2 / 1; /* На мобильных горизонтальные плитки */
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card-large {
|
||||||
|
flex: 1 0 100% !important;
|
||||||
|
aspect-ratio: 2 / 1 !important;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
order: initial !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-section {
|
||||||
|
padding: 0 15px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-section h2 {
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content-overlay {
|
||||||
|
padding: 15px 12px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title-overlay {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-category-badge {
|
||||||
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-more-posts {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Для очень маленьких экранов */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.post-card {
|
||||||
|
aspect-ratio: 3 / 2; /* Немного более вертикальные на маленьких экранах */
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card-large {
|
||||||
|
aspect-ratio: 3 / 2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content-overlay {
|
||||||
|
padding: 12px 10px 10px;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title-overlay {
|
||||||
|
font-size: 14px;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-category-badge {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 9px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стиль для пустого состояния */
|
||||||
|
.no-posts {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
51
src/styles/global.css
Normal file
51
src/styles/global.css
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
@import './reset.css';
|
||||||
|
@import './components/ContentGrid.css';
|
||||||
|
|
||||||
|
html{
|
||||||
|
font-family: Roboto,sans-serif;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container{
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info{
|
||||||
|
width: 100%;
|
||||||
|
background: #303030;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header__widgets{
|
||||||
|
padding: 0.4375rem 0;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-date{
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.maimnewsline{
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
|
||||||
|
.container{
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
100
src/styles/reset.css
Normal file
100
src/styles/reset.css
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
:root {
|
||||||
|
/* CSS Custom Properties */
|
||||||
|
--font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
--font-mono: 'SF Mono', Monaco, 'Cascadia Mono', monospace;
|
||||||
|
--color-bg: #ffffff;
|
||||||
|
--color-text: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset */
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Media */
|
||||||
|
img, picture, video, canvas, svg {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms */
|
||||||
|
input, button, textarea, select {
|
||||||
|
font: inherit;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links */
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lists */
|
||||||
|
ul, ol {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accessibility */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print styles */
|
||||||
|
@media print {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
background: transparent !important;
|
||||||
|
color: #000 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
text-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
"@api/*": ["src/lib/api/*"],
|
||||||
|
"@types/*": ["src/lib/types/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
|
"exclude": ["dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user