Initial commit: restructure project with Docker Compose setup
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,20 @@
|
||||
export const ru = {
|
||||
nav: {
|
||||
about: 'Обо мне',
|
||||
approach: 'Подход',
|
||||
process: 'Как работаем',
|
||||
issues: 'Запросы',
|
||||
pricing: 'Стоимость',
|
||||
contact: 'Записаться',
|
||||
},
|
||||
hero: {
|
||||
quote: 'Терапия — не про поиск «лучшей версии себя». Это про смелость наконец-то увидеть и стать собой.',
|
||||
subtitle: 'Здесь безопасное пространство для вашей внутренней работы.',
|
||||
cta: 'Записаться на бесплатное знакомство',
|
||||
},
|
||||
form: {
|
||||
namePlaceholder: 'Ваше имя',
|
||||
phonePlaceholder: 'Номер телефона',
|
||||
submit: 'Записаться',
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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">
|
||||
«Терапия — не про поиск «лучшей версии себя». Это про смелость наконец-то увидеть и стать собой.»
|
||||
</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">60–80 мин</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';
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />,
|
||||
},
|
||||
]);
|
||||
@@ -0,0 +1 @@
|
||||
export { router } from './config';
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
Reference in New Issue
Block a user