Initial commit: restructure project with Docker Compose setup

This commit is contained in:
Admin
2026-05-26 20:16:01 +05:00
commit 47b15787f3
48 changed files with 6074 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
import { RouterProvider } from 'react-router-dom';
import { router } from './router';
function App() {
return <RouterProvider router={router} />;
}
export default App;
@@ -0,0 +1,42 @@
import { useEffect, useRef, useState } from "react";
interface ScrollRevealProps {
children: React.ReactNode;
className?: string;
delay?: number;
}
export default function ScrollReveal({ children, className = "", delay = 0 }: ScrollRevealProps) {
const ref = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ threshold: 0.1, rootMargin: "0px 0px -40px 0px" }
);
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, []);
return (
<div
ref={ref}
className={`transition-all duration-700 ease-out ${className} ${
isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8"
}`}
style={{ transitionDelay: `${delay}ms` }}
>
{children}
</div>
);
}
+19
View File
@@ -0,0 +1,19 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { ru } from './local';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
ru: { translation: ru },
},
fallbackLng: 'ru',
interpolation: {
escapeValue: false,
},
});
export default i18n;
+20
View File
@@ -0,0 +1,20 @@
export const ru = {
nav: {
about: 'Обо мне',
approach: 'Подход',
process: 'Как работаем',
issues: 'Запросы',
pricing: 'Стоимость',
contact: 'Записаться',
},
hero: {
quote: 'Терапия — не про поиск «лучшей версии себя». Это про смелость наконец-то увидеть и стать собой.',
subtitle: 'Здесь безопасное пространство для вашей внутренней работы.',
cta: 'Записаться на бесплатное знакомство',
},
form: {
namePlaceholder: 'Ваше имя',
phonePlaceholder: 'Номер телефона',
submit: 'Записаться',
},
};
+41
View File
@@ -0,0 +1,41 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
scroll-behavior: smooth;
}
body {
@apply bg-cream text-warmBrown antialiased;
font-family: 'Manrope', sans-serif;
}
}
@layer utilities {
.text-softBrown {
color: #6B6560;
}
}
@keyframes scaleIn {
from {
transform: scale(0.8);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
@keyframes fadeInUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
+18
View File
@@ -0,0 +1,18 @@
import { useLocation } from "react-router-dom";
export default function NotFound() {
const location = useLocation();
return (
<div className="relative flex flex-col items-center justify-center h-screen text-center px-4">
<h1 className="absolute bottom-0 text-9xl md:text-[12rem] font-black text-gray-50 select-none pointer-events-none z-0">
404
</h1>
<div className="relative z-10">
<h1 className="text-xl md:text-2xl font-semibold mt-6">This page has not been generated</h1>
<p className="mt-2 text-base text-gray-400 font-mono">{location.pathname}</p>
<p className="mt-4 text-lg md:text-xl text-gray-500">Tell me more about this page, so I can generate it</p>
</div>
</div>
);
}
@@ -0,0 +1,126 @@
import { useState } from "react";
import ScrollReveal from "@/components/feature/ScrollReveal";
const diplomas = [
{ label: "Диплом о высшем образовании" },
{ label: "Сертификат повышения квалификации" },
];
export default function AboutSection() {
const [previewIdx, setPreviewIdx] = useState<number | null>(null);
return (
<section id="about" className="bg-white py-20 md:py-28 lg:py-36">
<div className="w-full px-6 md:px-10 lg:px-16">
<div className="flex flex-col lg:flex-row gap-12 lg:gap-20">
{/* Left column — text */}
<div className="w-full lg:w-[45%] flex flex-col">
<ScrollReveal>
<span className="inline-block self-start px-3 py-1 text-[10px] md:text-xs font-sans font-medium uppercase tracking-[0.2em] text-mutedBrown/60 border border-greigeDark rounded-full mb-8 md:mb-12">
Обо мне
</span>
</ScrollReveal>
<ScrollReveal delay={100}>
<h2 className="font-serif text-3xl md:text-4xl lg:text-[2.5rem] font-semibold text-warmBrown leading-tight">
<span className="block">Меня зовут</span>
<span className="block font-light text-mutedBrown/70 mt-1">Ирина</span>
</h2>
</ScrollReveal>
<ScrollReveal delay={200}>
<div className="mt-8 md:mt-12 space-y-5 text-[15px] md:text-base font-sans font-normal text-softBrown leading-[1.75]">
<p>
Я дипломированный практикующий психолог, веду индивидуальные консультации более 3 лет.
</p>
<p>
Регулярно прохожу личную терапию, супервизию, интервизию. Работаю только со взрослыми от 18 лет.
</p>
<p>
Со мной мои клиенты избавляются от тревожности, приходят к стабильной самооценке, налаживают отношения, проявляются легко и свободно.
</p>
</div>
</ScrollReveal>
<ScrollReveal delay={300}>
<div className="mt-8 md:mt-12 pt-6 border-t border-greigeDark/40 flex flex-col sm:flex-row gap-6 sm:gap-10">
<div>
<p className="font-serif text-3xl md:text-4xl font-bold text-warmBrown">3+</p>
<p className="mt-1 text-[10px] md:text-xs font-sans font-medium uppercase tracking-[0.15em] text-mutedBrown/60">Лет практики</p>
</div>
<div>
<p className="font-serif text-3xl md:text-4xl font-bold text-warmBrown">100+</p>
<p className="mt-1 text-[10px] md:text-xs font-sans font-medium uppercase tracking-[0.15em] text-mutedBrown/60">Клиентов</p>
</div>
</div>
</ScrollReveal>
</div>
{/* Right column — photo + diplomas */}
<div className="w-full lg:w-[55%] flex flex-col gap-8 md:gap-10">
{/* Large photo placeholder */}
<ScrollReveal delay={200}>
<div className="w-full aspect-[3/4] max-h-[520px] bg-greige rounded-2xl flex items-center justify-center overflow-hidden relative">
<div className="absolute inset-0 bg-gradient-to-b from-white/10 to-greigeDark/20"></div>
<span className="relative z-10 font-serif text-lg md:text-xl text-mutedBrown/50 italic">
Фото психолога
</span>
</div>
</ScrollReveal>
{/* Two diplomas — clickable previews */}
<div className="flex flex-col sm:flex-row gap-4 md:gap-6">
{diplomas.map((d, i) => (
<ScrollReveal key={i} delay={300 + i * 120} className="flex-1">
<button
onClick={() => setPreviewIdx(i)}
className="group relative w-full aspect-[4/3] bg-greige rounded-xl flex items-center justify-center overflow-hidden cursor-pointer text-left"
>
<div className="absolute inset-0 bg-gradient-to-t from-black/40 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<div className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<span className="flex items-center gap-2 px-4 py-2 bg-white/90 rounded-full text-xs font-sans font-medium text-warmBrown">
<i className="ri-eye-line"></i>
Просмотреть
</span>
</div>
<span className="relative z-10 font-serif text-sm md:text-base text-mutedBrown/50 italic">
{d.label}
</span>
</button>
</ScrollReveal>
))}
</div>
</div>
</div>
</div>
{/* Diploma preview modal */}
{previewIdx !== null && (
<div
className="fixed inset-0 z-[60] bg-black/60 backdrop-blur-sm flex items-center justify-center p-6"
onClick={() => setPreviewIdx(null)}
>
<div
className="bg-white rounded-2xl p-8 md:p-12 max-w-lg w-full text-center shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<div className="w-full aspect-[3/4] bg-greige rounded-xl flex items-center justify-center mb-6">
<span className="font-serif text-lg text-mutedBrown/50 italic">
{diplomas[previewIdx].label}
</span>
</div>
<p className="text-sm font-sans font-light text-mutedBrown mb-6">
PDF будет доступен для просмотра в ближайшее время.
</p>
<button
onClick={() => setPreviewIdx(null)}
className="inline-flex items-center gap-2 px-6 py-2.5 bg-taupe hover:bg-taupeDark text-white font-sans text-xs font-medium uppercase tracking-[0.1em] rounded-md transition-colors duration-300"
>
Закрыть
</button>
</div>
</div>
)}
</section>
);
}
@@ -0,0 +1,70 @@
import ScrollReveal from "@/components/feature/ScrollReveal";
export default function ApproachSection() {
return (
<section id="approach" className="bg-cream py-20 md:py-28 lg:py-36">
<div className="w-full px-6 md:px-10 lg:px-16">
<div className="max-w-4xl mx-auto text-center">
<ScrollReveal>
<span className="inline-block px-3 py-1 text-[10px] md:text-xs font-sans font-medium uppercase tracking-[0.2em] text-mutedBrown/60 border border-greigeDark rounded-full mb-8 md:mb-10">
Мой подход
</span>
</ScrollReveal>
<ScrollReveal delay={100}>
<h2 className="font-serif text-3xl md:text-4xl lg:text-[2.5rem] font-semibold text-warmBrown leading-tight">
Я работаю в интегративном подходе.
<br />
<span className="font-light italic text-mutedBrown/70">Что это значит для вас?</span>
</h2>
</ScrollReveal>
<ScrollReveal delay={250}>
<p className="mt-8 md:mt-12 text-sm md:text-[15px] font-sans font-normal text-softBrown leading-[1.85] max-w-2xl mx-auto">
Я не загоняю в рамки одного метода, а подбираю то, что подойдёт именно вам. Это объединение лучшего из разных направлений. Такой подход помогает разобраться в вашей ситуации и максимально быстро прийти к желаемому результату в вашем темпе как через разум, так и через чувства.
</p>
</ScrollReveal>
<ScrollReveal delay={400}>
<div className="mt-12 md:mt-16 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6">
{[
{
title: "ЭОТ",
desc: "Эмоционально-образная терапия — работа с глубокими эмоциями и состояниями",
},
{
title: "Транзактный анализ",
desc: "Чтобы понять сценарии поведения",
},
{
title: "Гештальт-терапия",
desc: "Возвращение контакта с собой и своими желаниями",
},
{
title: "МАК и Арт-терапия",
desc: "Когда сложно подобрать слова, а чувства переполняют",
},
{
title: "КПТ",
desc: "Пересмотр мышления, убеждений и поведенческих паттернов",
},
].map((item, i) => (
<div
key={i}
className="bg-white/60 backdrop-blur-sm rounded-xl p-6 md:p-8 border border-greigeDark/30 text-left hover:bg-white/80 hover:-translate-y-1 hover:shadow-md transition-all duration-300"
>
<h3 className="font-serif text-lg md:text-xl font-semibold text-warmBrown">
{item.title}
</h3>
<p className="mt-3 text-sm md:text-[15px] font-sans font-normal text-mutedBrown leading-relaxed">
{item.desc}
</p>
</div>
))}
</div>
</ScrollReveal>
</div>
</div>
</section>
);
}
@@ -0,0 +1,61 @@
import ScrollReveal from "@/components/feature/ScrollReveal";
const socials = [
{ icon: "ri-telegram-line", label: "Telegram" },
{ icon: "ri-vk-line", label: "ВКонтакте" },
{ icon: "ri-instagram-line", label: "Instagram" },
{ icon: "ri-message-3-line", label: "Макси" },
{ icon: "ri-article-line", label: "b17.ru" },
];
export default function FooterSection() {
return (
<footer className="bg-greige/50 py-16 md:py-24">
<div className="w-full px-6 md:px-10 lg:px-16">
<div className="flex flex-col md:flex-row justify-between items-start md:items-end gap-10 md:gap-16">
<div>
<ScrollReveal>
<p className="text-sm md:text-base font-sans font-light text-mutedBrown/70 leading-relaxed max-w-xs">
Если остались вопросы всегда рада помочь. Пишите или звоните.
</p>
<a
href="tel:+79999999999"
className="inline-block mt-3 font-serif text-xl md:text-2xl font-medium text-warmBrown hover:text-taupe transition-colors duration-300"
>
+7 999 999-99-99
</a>
</ScrollReveal>
</div>
<ScrollReveal delay={150}>
<div className="flex flex-col items-end gap-4">
<p className="text-xs md:text-sm font-sans font-light text-mutedBrown/60 italic">
Больше обо мне в социальных сетях
</p>
<div className="flex items-center gap-3 md:gap-4">
{socials.map((s, i) => (
<span
key={i}
className="w-10 h-10 md:w-11 md:h-11 flex items-center justify-center rounded-full bg-white/70 text-warmBrown/70 hover:text-warmBrown hover:bg-white transition-all duration-300 cursor-default"
title={s.label}
>
<i className={`${s.icon} text-lg`}></i>
</span>
))}
</div>
</div>
</ScrollReveal>
</div>
<div className="mt-12 md:mt-16 pt-6 border-t border-greigeDark/40 flex flex-col sm:flex-row justify-between items-center gap-4">
<p className="text-xs font-sans font-light text-mutedBrown/60">
© 2026 Ирина. Все права защищены.
</p>
<p className="text-xs font-sans font-light text-mutedBrown/60">
Практикующий психолог
</p>
</div>
</div>
</footer>
);
}
@@ -0,0 +1,37 @@
import ScrollReveal from "@/components/feature/ScrollReveal";
export default function HeroSection() {
return (
<section className="relative min-h-screen flex items-center bg-cream overflow-hidden">
<div className="w-full px-6 md:px-10 lg:px-16 py-20 md:py-0">
<div className="flex flex-col items-center justify-center min-h-screen max-w-4xl mx-auto text-center pt-20 lg:pt-0">
<ScrollReveal>
<blockquote className="font-serif text-3xl md:text-4xl lg:text-[2.75rem] xl:text-5xl font-medium text-warmBrown leading-[1.25] tracking-tight">
«Терапия не про поиск «лучшей версии себя». Это про смелость наконец-то увидеть и&nbsp;стать&nbsp;собой.»
</blockquote>
</ScrollReveal>
<ScrollReveal delay={200}>
<p className="mt-6 md:mt-8 text-base md:text-lg font-sans font-normal text-mutedBrown leading-relaxed max-w-lg">
Здесь безопасное пространство для вашей внутренней работы.
</p>
</ScrollReveal>
<ScrollReveal delay={400}>
<div className="mt-8 md:mt-10 flex items-center justify-center gap-3">
<a
href="#contact"
className="inline-flex items-center gap-2 px-7 py-3.5 bg-taupe hover:bg-taupeDark text-white font-sans text-xs md:text-sm font-medium uppercase tracking-[0.1em] rounded-md transition-colors duration-300"
>
Записаться на бесплатное знакомство
</a>
<span className="w-10 h-10 md:w-11 md:h-11 flex items-center justify-center border border-taupe/40 rounded-md text-taupe hover:border-taupe hover:bg-taupe/5 transition-all duration-300 cursor-pointer">
<i className="ri-arrow-right-line text-lg"></i>
</span>
</div>
</ScrollReveal>
</div>
</div>
</section>
);
}
@@ -0,0 +1,54 @@
import ScrollReveal from "@/components/feature/ScrollReveal";
const issues = [
{ title: "Сложности в отношениях и общении", bg: "bg-[#F0E6E0]", icon: "ri-heart-3-line" },
{ title: "Одиночество", bg: "bg-[#E8E0EC]", icon: "ri-user-unfollow-line" },
{ title: "Потеря контакта с собой", bg: "bg-[#EDE8E0]", icon: "ri-user-search-line" },
{ title: "Неудовлетворённость жизнью", bg: "bg-[#E8E8E0]", icon: "ri-emotion-unhappy-line" },
{ title: "Неспособность справляться со своими чувствами", bg: "bg-[#F0E6E0]", icon: "ri-mental-health-line" },
{ title: "Стыд и чувство вины", bg: "bg-[#E8E0EC]", icon: "ri-shield-user-line" },
{ title: "Тревога и панические атаки", bg: "bg-[#EDE8E0]", icon: "ri-heart-pulse-line" },
{ title: "Апатия и потеря сил", bg: "bg-[#F5EDE5]", icon: "ri-battery-low-line" },
{ title: "Низкая самооценка", bg: "bg-[#E8E8E0]", icon: "ri-bar-chart-grouped-line" },
{ title: "Эмоциональная зависимость", bg: "bg-[#F0E6E0]", icon: "ri-links-line" },
{ title: "Страх", bg: "bg-[#E8E0EC]", icon: "ri-alarm-warning-line" },
{ title: "Навязчивые мысли, ком в горле, тяжесть в груди", bg: "bg-[#EDE8E0]", icon: "ri-brain-line" },
];
export default function IssuesSection() {
return (
<section id="issues" className="bg-cream py-20 md:py-28 lg:py-36">
<div className="w-full px-6 md:px-10 lg:px-16">
<ScrollReveal>
<div className="mb-12 md:mb-20">
<span className="inline-block px-3 py-1 text-[10px] md:text-xs font-sans font-medium uppercase tracking-[0.2em] text-mutedBrown/60 border border-greigeDark rounded-full mb-6 md:mb-8">
Запросы
</span>
<h2 className="font-serif text-3xl md:text-4xl lg:text-[2.5rem] font-semibold text-warmBrown leading-tight max-w-2xl">
С какими запросами
<br />
<span className="font-light italic text-mutedBrown/70">я работаю?</span>
</h2>
</div>
</ScrollReveal>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 md:gap-4">
{issues.map((issue, i) => (
<ScrollReveal key={i} delay={i * 40}>
<div
className={`${issue.bg} rounded-xl p-4 md:p-5 h-full flex items-center gap-3 md:gap-4 hover:scale-[1.02] hover:-translate-y-0.5 hover:shadow-sm transition-all duration-300 cursor-default`}
>
<div className="w-10 h-10 md:w-11 md:h-11 rounded-lg bg-white/60 flex items-center justify-center flex-shrink-0">
<i className={`${issue.icon} text-warmBrown/50 text-lg`}></i>
</div>
<h3 className="text-sm md:text-[15px] font-sans font-medium text-warmBrown leading-snug">
{issue.title}
</h3>
</div>
</ScrollReveal>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,144 @@
import { useState, useEffect } from "react";
const navLinks = [
{ label: "Обо мне", href: "#about" },
{ label: "Подход", href: "#approach" },
{ label: "Как работаем", href: "#process" },
{ label: "Запросы", href: "#issues" },
{ label: "Стоимость", href: "#pricing" },
];
export default function Navbar() {
const [scrolled, setScrolled] = useState(false);
const [mobileOpen, setMobileOpen] = useState(false);
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 60);
};
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, href: string) => {
e.preventDefault();
const el = document.querySelector(href);
if (el) {
el.scrollIntoView({ behavior: "smooth" });
}
setMobileOpen(false);
};
return (
<nav
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-500 ${
scrolled
? "bg-white/90 backdrop-blur-md shadow-sm"
: "bg-transparent"
}`}
>
<div className="w-full px-6 md:px-10 lg:px-16">
<div className="flex items-center justify-between h-16 md:h-20">
{/* Left: empty spacer for balance */}
<div className="hidden md:block w-40" />
{/* Center: nav links */}
<div className="hidden md:flex items-center gap-8">
{navLinks.map((link) => (
<a
key={link.href}
href={link.href}
onClick={(e) => handleNavClick(e, link.href)}
className="text-xs font-sans font-medium uppercase tracking-[0.15em] text-warmBrown/80 hover:text-warmBrown transition-colors duration-300"
>
{link.label}
</a>
))}
</div>
{/* Right: messengers + CTA */}
<div className="hidden md:flex items-center gap-4 w-40 justify-end">
<div className="flex items-center gap-2">
<a
href="https://t.me/"
target="_blank"
rel="noopener noreferrer"
aria-label="Telegram"
className="w-9 h-9 rounded-full bg-greige flex items-center justify-center text-warmBrown/70 hover:bg-greigeDark hover:text-warmBrown transition-all duration-300"
>
<i className="ri-telegram-line text-base"></i>
</a>
<a
href="https://max.ru/"
target="_blank"
rel="noopener noreferrer"
aria-label="МАКС"
className="w-9 h-9 rounded-full bg-greige flex items-center justify-center text-warmBrown/70 hover:bg-greigeDark hover:text-warmBrown transition-all duration-300"
>
<i className="ri-message-3-line text-base"></i>
</a>
</div>
<a
href="#contact"
onClick={(e) => handleNavClick(e, "#contact")}
className="px-5 py-2 bg-taupe hover:bg-taupeDark text-white font-sans text-xs font-medium uppercase tracking-[0.1em] rounded-full transition-colors duration-300 whitespace-nowrap"
>
Записаться
</a>
</div>
{/* Mobile burger */}
<button
className="md:hidden w-10 h-10 flex items-center justify-center text-warmBrown ml-auto"
onClick={() => setMobileOpen(!mobileOpen)}
aria-label="Menu"
>
<i className={`ri-${mobileOpen ? "close" : "menu"}-line text-xl`}></i>
</button>
</div>
</div>
{mobileOpen && (
<div className="md:hidden bg-white/95 backdrop-blur-md border-t border-greigeDark/30 px-6 py-6 flex flex-col gap-4">
{navLinks.map((link) => (
<a
key={link.href}
href={link.href}
onClick={(e) => handleNavClick(e, link.href)}
className="text-sm font-sans font-medium uppercase tracking-[0.1em] text-warmBrown/80 hover:text-warmBrown transition-colors"
>
{link.label}
</a>
))}
<div className="flex items-center gap-3 mt-2 pt-4 border-t border-greigeDark/20">
<a
href="https://t.me/"
target="_blank"
rel="noopener noreferrer"
aria-label="Telegram"
className="w-10 h-10 rounded-full bg-greige flex items-center justify-center text-warmBrown/70"
>
<i className="ri-telegram-line text-lg"></i>
</a>
<a
href="https://max.ru/"
target="_blank"
rel="noopener noreferrer"
aria-label="МАКС"
className="w-10 h-10 rounded-full bg-greige flex items-center justify-center text-warmBrown/70"
>
<i className="ri-message-3-line text-lg"></i>
</a>
<a
href="#contact"
onClick={(e) => handleNavClick(e, "#contact")}
className="flex-1 text-center px-5 py-2.5 bg-taupe hover:bg-taupeDark text-white font-sans text-xs font-medium uppercase tracking-[0.1em] rounded-full transition-colors duration-300"
>
Записаться
</a>
</div>
</div>
)}
</nav>
);
}
@@ -0,0 +1,137 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import ScrollReveal from "@/components/feature/ScrollReveal";
const API_URL = import.meta.env.VITE_API_URL || "http://localhost:3001";
export default function PricingSection() {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
setError(null);
const formData = new FormData(e.currentTarget);
const data = {
name: String(formData.get("name") || "").trim(),
phone: String(formData.get("phone") || "").trim(),
};
try {
const res = await fetch(`${API_URL}/api/mail/application`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.message || "Ошибка отправки заявки");
}
navigate("/thanks");
} catch (err: any) {
setError(err.message || "Не удалось отправить заявку. Попробуйте позже.");
} finally {
setLoading(false);
}
};
return (
<section id="pricing" className="bg-white py-20 md:py-28 lg:py-36">
<div className="w-full px-6 md:px-10 lg:px-16">
<div className="max-w-3xl mx-auto text-center">
<ScrollReveal>
<span className="inline-block px-3 py-1 text-[10px] md:text-xs font-sans font-medium uppercase tracking-[0.2em] text-mutedBrown/60 border border-greigeDark rounded-full mb-6 md:mb-8">
Стоимость
</span>
</ScrollReveal>
<ScrollReveal delay={100}>
<h2 className="font-serif text-3xl md:text-4xl lg:text-[2.5rem] font-semibold text-warmBrown leading-tight">
Стоимость и формат
</h2>
</ScrollReveal>
<ScrollReveal delay={200}>
<div className="mt-8 md:mt-12 bg-cream rounded-2xl p-8 md:p-12 border border-greigeDark/30">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6 md:gap-8">
<div className="text-center">
<span className="block font-serif text-2xl md:text-3xl font-semibold text-warmBrown">6080 мин</span>
<span className="block mt-2 text-xs md:text-sm font-sans font-normal text-mutedBrown uppercase tracking-wider">Длительность сессии</span>
</div>
<div className="text-center sm:border-x sm:border-greigeDark/30">
<span className="block font-serif text-2xl md:text-3xl font-semibold text-warmBrown">2 500 </span>
<span className="block mt-2 text-xs md:text-sm font-sans font-normal text-mutedBrown uppercase tracking-wider">Стоимость</span>
</div>
<div className="text-center">
<span className="block font-serif text-2xl md:text-3xl font-semibold text-taupe">Бесплатно</span>
<span className="block mt-2 text-xs md:text-sm font-sans font-normal text-mutedBrown uppercase tracking-wider">Первая встреча</span>
</div>
</div>
<div className="mt-8 md:mt-10 pt-6 md:pt-8 border-t border-greigeDark/30">
<p className="text-sm md:text-[15px] font-sans font-normal text-softBrown">
Более 100 клиентов решили свои запросы
</p>
</div>
</div>
</ScrollReveal>
<ScrollReveal delay={250}>
<div className="mt-6 md:mt-8 bg-cream/60 rounded-xl p-6 md:p-8 border border-greigeDark/20">
<h3 className="font-serif text-lg md:text-xl font-semibold text-warmBrown mb-3">
Условия отмены и переноса
</h3>
<p className="text-sm md:text-[15px] font-sans font-normal text-softBrown leading-relaxed">
Отмена или перенос не позднее чем за 24 часа до сессии. При отмене с моей стороны следующая сессия проводится бесплатно.
</p>
</div>
</ScrollReveal>
<ScrollReveal delay={300}>
<div className="mt-8 md:mt-10" id="contact">
<form
onSubmit={handleSubmit}
className="inline-flex flex-col sm:flex-row items-center gap-3"
>
<input
type="text"
name="name"
placeholder="Ваше имя"
required
disabled={loading}
className="px-5 py-3.5 bg-cream border border-greigeDark/50 rounded-full text-sm font-sans text-warmBrown placeholder:text-mutedBrown/50 focus:outline-none focus:border-taupe transition-colors w-64 disabled:opacity-60"
/>
<input
type="tel"
name="phone"
placeholder="Номер телефона"
required
disabled={loading}
className="px-5 py-3.5 bg-cream border border-greigeDark/50 rounded-full text-sm font-sans text-warmBrown placeholder:text-mutedBrown/50 focus:outline-none focus:border-taupe transition-colors w-64 disabled:opacity-60"
/>
<button
type="submit"
disabled={loading}
className="inline-flex items-center gap-2 px-7 py-3.5 bg-taupe hover:bg-taupeDark text-white font-sans text-xs md:text-sm font-medium uppercase tracking-[0.1em] rounded-full transition-colors duration-300 whitespace-nowrap disabled:opacity-60 disabled:cursor-not-allowed"
>
{loading ? "Отправка..." : "Записаться"}
<span className="w-5 h-5 flex items-center justify-center">
<i className="ri-arrow-right-line text-sm"></i>
</span>
</button>
</form>
{error && (
<p className="mt-3 text-sm text-red-500 font-sans">{error}</p>
)}
</div>
</ScrollReveal>
</div>
</div>
</section>
);
}
@@ -0,0 +1,73 @@
import ScrollReveal from "@/components/feature/ScrollReveal";
const steps = [
{
num: "01",
title: "Знакомство и маршрут",
desc: "На первой встрече мы проводим ревизию того, что происходит в вашей жизни сейчас. 20–30 минут — достаточно, чтобы я услышала вашу ситуацию, а вы поняли, подхожу ли я вам. Вместе переведём проблему в понятную цель, подберём формат и график встреч.",
highlight: "Это время я провожу безоплатно.",
},
{
num: "02",
title: "Психологическая диагностика",
desc: "«Чек-ап» нервной системы: 7 тестов для оценки уровня тревоги, депрессии, агрессии, невроза, перфекционизма, алекситимии и определение психотипа. Уходим от догадок к фактам и отслеживаем реальную динамику изменений.",
highlight: "Прохождение обязательно.",
},
{
num: "03",
title: "Трансформация и автономия",
desc: "Работаем над запросом и внедряем изменения в реальную жизнь — не только на сессиях, но и между ними. Цель — устойчивый результат и инструменты, которые позволят вам справляться самостоятельно.",
highlight: "",
},
];
export default function ProcessSection() {
return (
<section id="process" className="bg-white py-20 md:py-28 lg:py-36">
<div className="w-full px-6 md:px-10 lg:px-16">
<ScrollReveal>
<div className="text-center mb-12 md:mb-20">
<span className="inline-block px-3 py-1 text-[10px] md:text-xs font-sans font-medium uppercase tracking-[0.2em] text-mutedBrown/60 border border-greigeDark rounded-full mb-6 md:mb-8">
Процесс
</span>
<h2 className="font-serif text-3xl md:text-4xl lg:text-[2.5rem] font-semibold text-warmBrown leading-tight">
Как строится наша работа?
</h2>
</div>
</ScrollReveal>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8">
{steps.map((step, i) => (
<ScrollReveal key={i} delay={i * 150}>
<div className="group relative h-full bg-greige/40 rounded-2xl overflow-hidden hover:bg-greige/60 hover:-translate-y-1 hover:shadow-md transition-all duration-500">
<div className="absolute top-0 left-0 w-full h-full">
<div className="w-full h-48 md:h-56 bg-gradient-to-b from-powder/40 via-lavender/20 to-greige/60"></div>
</div>
<div className="relative z-10 p-6 md:p-8 flex flex-col h-full">
<span className="font-serif text-5xl md:text-6xl font-bold text-warmBrown/10 group-hover:text-warmBrown/15 transition-colors duration-500">
{step.num}
</span>
<h3 className="mt-4 md:mt-6 font-serif text-xl md:text-2xl font-semibold text-warmBrown">
{step.title}
</h3>
<p className="mt-3 md:mt-4 text-sm md:text-[15px] font-sans font-normal text-softBrown leading-[1.75] flex-grow">
{step.desc}
</p>
{step.highlight && (
<p className="mt-4 text-sm md:text-[15px] font-sans font-medium text-taupe italic">
{step.highlight}
</p>
)}
</div>
</div>
</ScrollReveal>
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,8 @@
export { default as Navbar } from './Navbar';
export { default as HeroSection } from './HeroSection';
export { default as AboutSection } from './AboutSection';
export { default as ApproachSection } from './ApproachSection';
export { default as ProcessSection } from './ProcessSection';
export { default as IssuesSection } from './IssuesSection';
export { default as PricingSection } from './PricingSection';
export { default as FooterSection } from './FooterSection';
+23
View File
@@ -0,0 +1,23 @@
import Navbar from "./components/Navbar";
import HeroSection from "./components/HeroSection";
import AboutSection from "./components/AboutSection";
import ApproachSection from "./components/ApproachSection";
import ProcessSection from "./components/ProcessSection";
import IssuesSection from "./components/IssuesSection";
import PricingSection from "./components/PricingSection";
import FooterSection from "./components/FooterSection";
export default function HomePage() {
return (
<div className="min-h-screen bg-cream font-sans text-warmBrown antialiased">
<Navbar />
<HeroSection />
<AboutSection />
<ApproachSection />
<ProcessSection />
<IssuesSection />
<PricingSection />
<FooterSection />
</div>
);
}
+133
View File
@@ -0,0 +1,133 @@
import { useNavigate } from "react-router-dom";
import { useEffect } from "react";
const socials = [
{ icon: "ri-telegram-line", label: "Telegram", href: "https://t.me/" },
{ icon: "ri-message-3-line", label: "МАКС", href: "https://max.ru/" },
{ icon: "ri-vk-line", label: "ВКонтакте", href: "https://vk.com/" },
{ icon: "ri-instagram-line", label: "Instagram", href: "https://instagram.com/" },
{ icon: "ri-article-line", label: "b17.ru", href: "https://b17.ru/" },
];
const steps = [
"Я свяжусь с вами в течение 24 часов",
"Мы выберем удобное время для знакомства",
"Первая встреча — бесплатно, без обязательств",
];
export default function ThanksPage() {
const navigate = useNavigate();
useEffect(() => {
document.body.style.opacity = "0";
requestAnimationFrame(() => {
document.body.style.transition = "opacity 0.7s ease";
document.body.style.opacity = "1";
});
return () => {
document.body.style.opacity = "";
document.body.style.transition = "";
};
}, []);
return (
<div className="min-h-screen bg-cream flex flex-col antialiased">
{/* Header */}
<header className="w-full px-6 md:px-10 lg:px-16 py-6 md:py-8">
<div className="flex items-center justify-between">
<span className="font-serif text-xl md:text-2xl font-medium text-warmBrown">
Ирина
</span>
<button
onClick={() => navigate("/")}
className="text-xs md:text-sm font-sans font-medium uppercase tracking-[0.1em] text-warmBrown/60 hover:text-warmBrown transition-colors duration-300"
>
На главную
</button>
</div>
</header>
{/* Main */}
<main className="flex-1 flex flex-col items-center justify-center px-6 md:px-10 lg:px-16 py-12 md:py-20 lg:py-28">
<div className="max-w-xl w-full text-center">
{/* Check icon */}
<div className="w-16 h-16 md:w-20 md:h-20 mx-auto mb-8 rounded-full bg-greige/60 flex items-center justify-center animate-[scaleIn_0.5s_ease-out]">
<i className="ri-check-line text-3xl md:text-4xl text-taupe"></i>
</div>
<h1 className="font-serif text-3xl md:text-4xl lg:text-5xl font-semibold text-warmBrown leading-tight mb-6 animate-[fadeInUp_0.6s_ease-out_0.1s_both]">
Спасибо, что написали
</h1>
<p className="text-base md:text-lg font-sans font-normal text-softBrown leading-relaxed mb-12 animate-[fadeInUp_0.6s_ease-out_0.2s_both]">
Я получила вашу заявку и напишу вам в ближайшее время. Сделайте себе чай вы уже сделали важный шаг.
</p>
{/* Steps block */}
<div className="bg-white rounded-2xl p-8 md:p-10 border border-greigeDark/30 mb-12 animate-[fadeInUp_0.6s_ease-out_0.3s_both]">
<h2 className="font-serif text-lg md:text-xl font-semibold text-warmBrown mb-6">
Что дальше
</h2>
<div className="space-y-5">
{steps.map((step, i) => (
<div
key={i}
className="flex items-start gap-4 text-left group"
>
<span className="w-8 h-8 rounded-full bg-greige/60 flex items-center justify-center flex-shrink-0 font-serif text-sm font-semibold text-taupe mt-0.5 group-hover:bg-greige transition-colors duration-300">
{i + 1}
</span>
<p className="text-sm md:text-base font-sans font-normal text-softBrown leading-relaxed">
{step}
</p>
</div>
))}
</div>
</div>
{/* Socials */}
<div className="mb-12 animate-[fadeInUp_0.6s_ease-out_0.4s_both]">
<p className="text-xs md:text-sm font-sans font-light text-mutedBrown/70 italic mb-5">
Пока ждёте загляните в мои социальные сети
</p>
<div className="flex items-center justify-center gap-3 md:gap-4">
{socials.map((s, i) => (
<a
key={i}
href={s.href}
target="_blank"
rel="noopener noreferrer"
aria-label={s.label}
className="w-11 h-11 flex items-center justify-center rounded-full bg-white border border-greigeDark/30 text-warmBrown/70 hover:text-warmBrown hover:bg-greige/40 hover:border-transparent hover:-translate-y-0.5 transition-all duration-300"
>
<i className={`${s.icon} text-lg`}></i>
</a>
))}
</div>
</div>
{/* CTA */}
<button
onClick={() => navigate("/")}
className="inline-flex items-center gap-2.5 px-8 py-4 bg-taupe hover:bg-taupeDark text-white font-sans text-sm font-medium uppercase tracking-[0.1em] rounded-full transition-all duration-300 hover:-translate-y-0.5 hover:shadow-lg animate-[fadeInUp_0.6s_ease-out_0.5s_both]"
>
<i className="ri-arrow-left-line text-sm"></i>
Вернуться на главную
</button>
</div>
</main>
{/* Footer */}
<footer className="w-full px-6 md:px-10 lg:px-16 py-8 border-t border-greigeDark/30">
<div className="flex flex-col sm:flex-row justify-between items-center gap-4">
<p className="text-xs font-sans font-light text-mutedBrown/60">
© 2026 Ирина. Все права защищены.
</p>
<p className="text-xs font-sans font-light text-mutedBrown/60">
Практикующий психолог
</p>
</div>
</footer>
</div>
);
}
+19
View File
@@ -0,0 +1,19 @@
import { createBrowserRouter } from 'react-router-dom';
import HomePage from '../pages/home/page';
import ThanksPage from '../pages/thanks/page';
import NotFound from '../pages/NotFound';
export const router = createBrowserRouter([
{
path: '/',
element: <HomePage />,
},
{
path: '/thanks',
element: <ThanksPage />,
},
{
path: '*',
element: <NotFound />,
},
]);
+1
View File
@@ -0,0 +1 @@
export { router } from './config';
+9
View File
@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}