first commit

This commit is contained in:
Web-serfer 2026-03-28 16:51:42 +05:00
commit 7c46ee6909
107 changed files with 5563 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>