first commit
This commit is contained in:
commit
7c46ee6909
107 changed files with 5563 additions and 0 deletions
88
frontend/src/components/home/Hero.astro
Normal file
88
frontend/src/components/home/Hero.astro
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
import WhatsAppButton from '@components/base/WhatsAppButton.astro'
|
||||
import TechStack from '@components/home/TechStack.astro'
|
||||
import HeroImage from '@components/home/HeroImage.astro'
|
||||
import { pb } from '@lib/pocketbase';
|
||||
|
||||
interface HeroData {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
tech_title: string;
|
||||
whatsapp_phone_number: string;
|
||||
btn_text: string;
|
||||
frontend_tech: string[];
|
||||
backend_tech: string[];
|
||||
is_active: boolean;
|
||||
[key: string]: any; // для остальных полей
|
||||
}
|
||||
|
||||
const result = await pb.collection('home_hero').getList(1, 1, {
|
||||
filter: 'is_active = true',
|
||||
sort: '-created'
|
||||
});
|
||||
|
||||
const heroData: HeroData = {
|
||||
...result.items[0],
|
||||
id: result.items[0].id,
|
||||
title: result.items[0].title,
|
||||
subtitle: result.items[0].subtitle,
|
||||
tech_title: result.items[0].tech_title,
|
||||
whatsapp_phone_number: result.items[0].whatsapp_phone_number,
|
||||
btn_text: result.items[0].btn_text,
|
||||
frontend_tech: result.items[0].frontend_tech || [],
|
||||
backend_tech: result.items[0].backend_tech || [],
|
||||
is_active: result.items[0].is_active
|
||||
};
|
||||
---
|
||||
|
||||
<section class="w-full">
|
||||
<div class="flex flex-col md:flex-row items-center justify-between gap-10 lg:gap-20">
|
||||
|
||||
<!-- ЛЕВАЯ КОЛОНКА -->
|
||||
<div class="flex-1 hero-content-animation">
|
||||
<div class="text-center md:text-left">
|
||||
<h1 class="mb-5 text-2xl md:text-4xl lg:text-5xl font-bold leading-tight dark:text-white animate-fade-in-up" style="animation-delay: 0.1s;">
|
||||
{heroData.title}
|
||||
</h1>
|
||||
<div class="mb-6 space-y-3 animate-fade-in-up" style="animation-delay: 0.2s;">
|
||||
<p class="text-base md:text-lg text-neutral-600 dark:text-neutral-400">
|
||||
{heroData.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Стек и кнопка (внутри левой колонки) -->
|
||||
<div class="mt-8 animate-fade-in-up" style="animation-delay: 0.3s;">
|
||||
<h2 class="text-xl md:text-2xl font-bold text-center md:text-left mb-6 dark:text-white">
|
||||
{heroData.tech_title}
|
||||
</h2>
|
||||
<TechStack heroData={heroData} />
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex justify-center animate-fade-in-up" style="animation-delay: 0.7s;">
|
||||
<WhatsAppButton
|
||||
phoneNumber={heroData.whatsapp_phone_number}
|
||||
btnText={heroData.btn_text}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ПРАВАЯ КОЛОНКА (Картинка) -->
|
||||
<div class="flex-1 w-full max-w-sm md:max-w-md lg:max-w-lg">
|
||||
<HeroImage heroData={heroData} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.animate-fade-in-up {
|
||||
opacity: 0;
|
||||
animation: fadeInUp 0.8s ease-out forwards;
|
||||
}
|
||||
</style>
|
||||
70
frontend/src/components/home/HeroImage.astro
Normal file
70
frontend/src/components/home/HeroImage.astro
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
---
|
||||
import { getImageUrl } from '@lib/pocketbase';
|
||||
|
||||
const { heroData } = Astro.props;
|
||||
|
||||
const imageUrl = getImageUrl(heroData, heroData.image);
|
||||
---
|
||||
|
||||
<div class="relative z-10 w-full max-w-sm mx-auto hero-image-container">
|
||||
<!-- Эмодзи 👋 -->
|
||||
<div class="absolute -top-4 -left-4 z-40 w-16 h-16 rounded-full">
|
||||
<span class="relative z-20 flex items-center justify-center w-full h-full text-2xl border-8 border-white rounded-full dark:border-neutral-950 bg-neutral-100 dark:bg-neutral-900 wave-emoji">
|
||||
<span class="flex items-center justify-center w-full h-full bg-white border border-dashed rounded-full dark:bg-neutral-950 border-neutral-300 dark:border-neutral-700">
|
||||
👋
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="relative z-30">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={heroData.img_alt}
|
||||
width={790}
|
||||
height={1193}
|
||||
loading="eager"
|
||||
decoding="auto"
|
||||
class="relative z-30 w-full h-auto hero-image"
|
||||
/>
|
||||
|
||||
<span
|
||||
class="absolute bottom-[45%] left-[40%] -translate-x-1/2 z-40 rounded-full bg-white/80 dark:bg-black/80 px-3 py-1 text-sm font-semibold text-neutral-800 dark:text-neutral-200 opacity-0 greeting-text-animation shadow-lg"
|
||||
>
|
||||
{heroData.greeting_text}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Декоративная рамка -->
|
||||
<div class="absolute inset-0 z-20 -m-4 border border-dashed rounded-2xl bg-gradient-to-r dark:from-neutral-950 dark:via-black dark:to-neutral-950 from-white via-neutral-50 to-white border-neutral-300 dark:border-neutral-700 hero-border">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes fadeInScale {
|
||||
0% { opacity: 0; transform: scale(0.8) rotate(-2deg); }
|
||||
70% { opacity: 1; }
|
||||
100% { opacity: 1; transform: scale(1) rotate(0deg); }
|
||||
}
|
||||
@keyframes borderGlow {
|
||||
0% { opacity: 0; transform: scale(0.95); box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7); }
|
||||
50% { opacity: 1; box-shadow: 0 0 20px 10px rgba(99, 102, 241, 0.3); }
|
||||
100% { opacity: 1; transform: scale(1); box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); }
|
||||
}
|
||||
@keyframes wave {
|
||||
0% { transform: rotate(0deg); opacity: 0; }
|
||||
20% { opacity: 1; }
|
||||
25% { transform: rotate(-15deg); }
|
||||
75% { transform: rotate(15deg); }
|
||||
100% { transform: rotate(0deg); opacity: 1; }
|
||||
}
|
||||
@keyframes greet-fade-in {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.greeting-text-animation { animation: greet-fade-in 0.8s ease-out 2.0s forwards; }
|
||||
.hero-image-container { animation: fadeInScale 1.2s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.2s forwards; }
|
||||
.hero-image { animation: fadeInScale 1s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.4s forwards; opacity: 0; }
|
||||
.hero-border { animation: borderGlow 1.5s ease-out 0.6s forwards; opacity: 0; }
|
||||
.wave-emoji { animation: wave 1s ease-in-out 1.5s forwards; opacity: 0; }
|
||||
</style>
|
||||
37
frontend/src/components/home/Separator.astro
Normal file
37
frontend/src/components/home/Separator.astro
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
const { text } = Astro.props
|
||||
---
|
||||
|
||||
<div class="relative my-16">
|
||||
<div class="relative w-full pl-5 overflow-x-hidden md:pl-0">
|
||||
<div
|
||||
class="absolute w-full h-px bg-gradient-to-r from-transparent to-white md:from-white dark:from-transparent dark:to-neutral-950 md:dark:from-neutral-950 md:via-transparent md:dark:via-transparent md:to-white md:dark:to-neutral-950"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="w-full h-px border-t border-dashed border-neutral-300 dark:border-neutral-600"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute flex items-center justify-center w-auto h-auto px-3 py-1.5 uppercase tracking-widest space-x-1 text-[0.6rem] md:-translate-x-1/2 -translate-y-1/2 border rounded-full bg-white dark:bg-neutral-900 text-neutral-400 left-0 md:ml-0 ml-5 md:left-1/2 border-neutral-100 dark:border-neutral-800 shadow-sm"
|
||||
>
|
||||
<p class="leading-none">{text}</p>
|
||||
<div
|
||||
class="flex items-center justify-center w-5 h-5 translate-x-1 border rounded-full border-neutral-100 dark:border-neutral-800"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 4.5v15m0 0l6.75-6.75M12 19.5l-6.75-6.75"></path></svg
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
72
frontend/src/components/home/TechStack.astro
Normal file
72
frontend/src/components/home/TechStack.astro
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
interface Props {
|
||||
heroData: {
|
||||
frontend_tech: string[];
|
||||
backend_tech: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const { heroData }: Props = Astro.props;
|
||||
|
||||
const frontendTech: string[] = heroData.frontend_tech;
|
||||
const backendTech: string[] = heroData.backend_tech;
|
||||
---
|
||||
|
||||
<div class="flex flex-col md:flex-row gap-4 border border-dashed rounded-lg border-neutral-200 dark:border-neutral-700 mt-8">
|
||||
|
||||
<!-- Секция Фронтенд -->
|
||||
<div class="flex-1 p-6">
|
||||
<h3 class="mb-4 text-lg font-semibold text-center text-neutral-700 dark:text-neutral-300 md:text-left">
|
||||
Фронтенд
|
||||
</h3>
|
||||
<ul class="space-y-2 text-sm text-center text-neutral-500 dark:text-neutral-400 md:text-left">
|
||||
{
|
||||
frontendTech.map((tech, index) => (
|
||||
<li
|
||||
class="transition-all duration-200 ease-in-out hover:translate-x-2 hover:text-neutral-800 dark:hover:text-neutral-200 animate-fade-in-up"
|
||||
style={`animation-delay: ${0.4 + index * 0.05}s;`}
|
||||
>
|
||||
🔘 {tech}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Секция Бэкенд -->
|
||||
<div class="flex-1 p-6">
|
||||
<h3 class="mb-4 text-lg font-semibold text-center text-neutral-700 dark:text-neutral-300 md:text-left">
|
||||
Бэкенд
|
||||
</h3>
|
||||
<ul class="space-y-2 text-sm text-center text-neutral-500 dark:text-neutral-400 md:text-left">
|
||||
{
|
||||
backendTech.map((tech, index) => (
|
||||
<li
|
||||
class="transition-all duration-200 ease-in-out hover:translate-x-2 hover:text-neutral-800 dark:hover:text-neutral-200 animate-fade-in-up"
|
||||
style={`animation-delay: ${0.4 + index * 0.05}s;`}
|
||||
>
|
||||
🔘 {tech}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(15px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
opacity: 0;
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
}
|
||||
</style>
|
||||
94
frontend/src/components/home/Writings.astro
Normal file
94
frontend/src/components/home/Writings.astro
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
---
|
||||
import Button from '../base/Button.astro'
|
||||
import PostsLoop from '../blog/PostsLoop.astro'
|
||||
import FeaturedPostCard from '@components/blog/FeaturedPost.astro';
|
||||
import { pb } from '@lib/pocketbase';
|
||||
|
||||
// --- ШАГ 1: Получаем избранные статьи (до 5 шт) ---
|
||||
const featuredResult = await pb.collection('posts').getList(1, 5, {
|
||||
filter: 'isActive = true && isFeatured = true',
|
||||
sort: '-publishDate',
|
||||
requestKey: 'home_featured_multi'
|
||||
});
|
||||
|
||||
import type { Post } from '@globalInterfaces';
|
||||
|
||||
// ПРЕОБРАЗОВАНИЕ (MAPPING)
|
||||
// Превращаем ответ PocketBase в чистый объект для компонента
|
||||
const featuredPosts: Post[] = featuredResult.items.map(post => ({
|
||||
id: post.id, // ID нужен для исключения ниже
|
||||
title: post.title,
|
||||
slug: post.slug,
|
||||
description: post.description,
|
||||
publishDate: post.publishDate,
|
||||
tags: post.tags,
|
||||
content: post.content,
|
||||
image: post.image,
|
||||
isFeatured: post.isFeatured,
|
||||
isActive: post.isActive
|
||||
}));
|
||||
|
||||
// Собираем ID всех избранных статей, чтобы не показывать их в общем списке
|
||||
const featuredIds = featuredPosts.map(p => p.id);
|
||||
|
||||
// --- ШАГ 2: Получаем обычные статьи (3 шт) ---
|
||||
let mainFilter = 'isActive = true';
|
||||
|
||||
// Если есть избранные, добавляем условие: И id не равен ... И id не равен ...
|
||||
if (featuredIds.length > 0) {
|
||||
const exclusionQuery = featuredIds.map(id => `id != "${id}"`).join(' && ');
|
||||
mainFilter += ` && ${exclusionQuery}`;
|
||||
}
|
||||
|
||||
const recentResult = await pb.collection('posts').getList(1, 3, {
|
||||
filter: mainFilter,
|
||||
sort: '-publishDate',
|
||||
requestKey: 'home_recent'
|
||||
});
|
||||
|
||||
// ПРЕОБРАЗОВАНИЕ (MAPPING) для обычного списка
|
||||
const recentPosts: Post[] = recentResult.items.map(post => ({
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
slug: post.slug,
|
||||
description: post.description,
|
||||
publishDate: post.publishDate,
|
||||
tags: post.tags,
|
||||
content: post.content,
|
||||
image: post.image,
|
||||
isFeatured: post.isFeatured,
|
||||
isActive: post.isActive
|
||||
}));
|
||||
---
|
||||
|
||||
<section class="max-w-4xl mx-auto px-7 lg:px-0 animate-on-scroll">
|
||||
<h2 class="text-2xl text-center font-bold leading-10 tracking-tight text-neutral-900 dark:text-neutral-100">
|
||||
Мои статьи
|
||||
</h2>
|
||||
<p class="mb-6 text-base text-center text-neutral-600 dark:text-neutral-400">
|
||||
Помимо программирования, я также люблю писать о web-технологиях.
|
||||
</p>
|
||||
|
||||
<div class="w-full max-w-4xl mx-auto my-4 xl:px-0">
|
||||
<div class="flex justify-center">
|
||||
<div class="w-full md:w-2/3 space-y-7">
|
||||
|
||||
{/* БЛОК ИЗБРАННЫХ СТАТЕЙ */}
|
||||
{featuredPosts.length > 0 && (
|
||||
<div class="flex flex-col gap-6 mb-8">
|
||||
{featuredPosts.map((post) => (
|
||||
<FeaturedPostCard post={post} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* СПИСОК ОБЫЧНЫХ СТАТЕЙ */}
|
||||
<!--<PostsLoop posts={recentPosts} />-->
|
||||
|
||||
<div class="flex items-center justify-center w-full py-5">
|
||||
<Button text="Посмотреть все статьи" link="/blog" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
Loading…
Add table
Add a link
Reference in a new issue