diff --git a/react_front/src/App.tsx b/react_front/src/App.tsx
new file mode 100644
index 0000000..a66b5ef
--- /dev/null
+++ b/react_front/src/App.tsx
@@ -0,0 +1,122 @@
+import { useState } from 'react'
+import reactLogo from './assets/react.svg'
+import viteLogo from './assets/vite.svg'
+import heroImg from './assets/hero.png'
+import './App.css'
+
+function App() {
+ const [count, setCount] = useState(0)
+
+ return (
+ <>
+
+
+
+
Get started
+
+ Edit src/App.tsx and save to test HMR
+
+
+ setCount((count) => count + 1)}
+ >
+ Count is {count}
+
+
+
+
+
+
+
+
+
+
+
Documentation
+
Your questions, answered
+
+
+
+
+
+
+
Connect with us
+
Join the Vite community
+
+
+
+
+
+
+ >
+ )
+}
+
+export default App
diff --git a/src/App.js b/src/App.js
index e570c32..07906e8 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,13 +1,14 @@
-import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
+import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext';
import { ThemeProvider } from './context/ThemeContext';
import { useAuthContext } from './context/AuthContext';
import { useUnreadSocket } from './hooks/useUnreadSocket';
+import LandingPage from './pages/NakhodkaLanding';
import RegisterPage from './components/Authentication';
-import MainFeed from './components/MainFeed';
-import ChatPage from './components/ChatPage';
-import ChatListPage from './components/ChatListPage';
-import ReviewsPage from './components/ReviewsPage';
+import MainFeed from './pages/MainFeed';
+import ChatPage from './pages/ChatPage';
+import ChatListPage from './pages/ChatListPage';
+import ReviewsPage from './pages/ReviewsPage';
import './styles/chat.css';
function UnreadListener() {
@@ -23,7 +24,7 @@ function App() {
- } />
+ } />
} />
} />
diff --git a/src/components/landing/FinalCta.jsx b/src/components/landing/FinalCta.jsx
new file mode 100644
index 0000000..e30e87d
--- /dev/null
+++ b/src/components/landing/FinalCta.jsx
@@ -0,0 +1,10 @@
+export default function FinalCta({ onClick }) {
+ return (
+
+ Кто-то прямо сейчас ищет то, что вы нашли
+
+ Открыть Находку
+
+
+ );
+}
diff --git a/src/components/landing/Footer.jsx b/src/components/landing/Footer.jsx
new file mode 100644
index 0000000..e894eee
--- /dev/null
+++ b/src/components/landing/Footer.jsx
@@ -0,0 +1,7 @@
+export default function Footer() {
+ return (
+
+ © {new Date().getFullYear()} Находка. Возвращаем найденное.
+
+ );
+}
diff --git a/src/components/landing/Hero.jsx b/src/components/landing/Hero.jsx
new file mode 100644
index 0000000..5526f74
--- /dev/null
+++ b/src/components/landing/Hero.jsx
@@ -0,0 +1,50 @@
+import { useState } from "react";
+import Navbar from "./Navbar";
+import RadarBeacon from "./RadarBeacon";
+
+export default function Hero({ onCtaClick, onLoginClick }) {
+ const [hover, setHover] = useState(false);
+
+ return (
+
+
+
+
+
+
+
+
+
Сервис поиска пропавших питомцев и вещей
+
+ Потерял — не значит
+
+ потерял навсегда
+
+
+ Разместите объявление о пропаже или находке — Находка сама сопоставит
+ похожие записи по фото, месту и описанию и покажет совпадения рядом с вами.
+
+
+ setHover(true)}
+ onMouseLeave={() => setHover(false)}
+ onClick={onCtaClick}
+ >
+ Начать поиск
+
+ →
+
+
+
+
+
+ );
+}
diff --git a/src/components/landing/Icon.jsx b/src/components/landing/Icon.jsx
new file mode 100644
index 0000000..db0cafe
--- /dev/null
+++ b/src/components/landing/Icon.jsx
@@ -0,0 +1,37 @@
+export default function Icon({ type }) {
+ if (type === "paw") {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ if (type === "bag") {
+ return (
+
+
+
+
+ );
+ }
+
+ if (type === "key") {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/components/landing/Navbar.jsx b/src/components/landing/Navbar.jsx
new file mode 100644
index 0000000..94dec61
--- /dev/null
+++ b/src/components/landing/Navbar.jsx
@@ -0,0 +1,10 @@
+export default function Navbar({ onLoginClick }) {
+ return (
+
+ Находка
+
+ Войти
+
+
+ );
+}
diff --git a/src/components/landing/RadarBeacon.jsx b/src/components/landing/RadarBeacon.jsx
new file mode 100644
index 0000000..b626f2a
--- /dev/null
+++ b/src/components/landing/RadarBeacon.jsx
@@ -0,0 +1,27 @@
+import Icon from "./Icon";
+import { markers } from "../../config/landingConstants";
+
+export default function RadarBeacon() {
+ return (
+
+
+
+
+
+
+ {markers.map((m) => (
+
+
+
+ ))}
+
+ );
+}
diff --git a/src/components/landing/StepsSection.jsx b/src/components/landing/StepsSection.jsx
new file mode 100644
index 0000000..da8274f
--- /dev/null
+++ b/src/components/landing/StepsSection.jsx
@@ -0,0 +1,18 @@
+import { steps } from "../../config/landingConstants";
+
+export default function StepsSection() {
+ return (
+
+ Как это работает
+
+ {steps.map((s) => (
+
+
{s.n}
+
{s.title}
+
{s.text}
+
+ ))}
+
+
+ );
+}
diff --git a/src/config/landingConstants.js b/src/config/landingConstants.js
new file mode 100644
index 0000000..5d8320f
--- /dev/null
+++ b/src/config/landingConstants.js
@@ -0,0 +1,32 @@
+
+
+export const markers = [
+ { id: "paw", x: 14, y: 18, delay: "0.4s", icon: "paw" },
+ { id: "key", x: 72, y: 22, delay: "1.1s", icon: "key" },
+ { id: "phone", x: 60, y: 62, delay: "1.8s", icon: "phone" },
+ { id: "bag", x: 26, y: 70, delay: "2.4s", icon: "bag" },
+];
+
+export const steps = [
+ {
+ n: "01",
+ title: "Разместите объявление",
+ text: "Опишите пропажу или находку: фото, место, приметы. Это занимает меньше минуты.",
+ },
+ {
+ n: "02",
+ title: "Система ищет совпадения",
+ text: "Находка сама сравнивает похожие объявления по фото, месту и описанию — по всему городу.",
+ },
+ {
+ n: "03",
+ title: "Свяжитесь и заберите",
+ text: "Как только есть совпадение — пишите в чат прямо в приложении и договоритесь о встрече.",
+ },
+];
+
+export const stats = [
+ { value: "12 480", label: "находок опубликовано" },
+ { value: "9 200+", label: "воссоединений с хозяевами" },
+ { value: "24/7", label: "поиск работает без перерыва" },
+];
diff --git a/src/index.css b/src/index.css
index e8af213..4b33d4d 100644
--- a/src/index.css
+++ b/src/index.css
@@ -16,5 +16,4 @@ html, body, #root {
height: 100%;
margin: 0;
padding: 0;
- overflow: hidden;
}
\ No newline at end of file
diff --git a/src/components/ChatListPage.jsx b/src/pages/ChatListPage.jsx
similarity index 98%
rename from src/components/ChatListPage.jsx
rename to src/pages/ChatListPage.jsx
index ec4a9e3..a0da74d 100644
--- a/src/components/ChatListPage.jsx
+++ b/src/pages/ChatListPage.jsx
@@ -6,7 +6,7 @@ import { useUnread } from "../context/UnreadContext";
import { getPostForChat } from "../stores/chatPostStore";
import { API_URL } from "../config/authConstants";
import { useDeleteChat } from "../hooks/useDeleteChat";
-import TrashIcon from "./icons/TrashIcon";
+import TrashIcon from "../components/icons/TrashIcon";
export default function ChatListPage() {
const navigate = useNavigate();
diff --git a/src/components/ChatPage.jsx b/src/pages/ChatPage.jsx
similarity index 100%
rename from src/components/ChatPage.jsx
rename to src/pages/ChatPage.jsx
diff --git a/src/components/MainFeed.jsx b/src/pages/MainFeed.jsx
similarity index 95%
rename from src/components/MainFeed.jsx
rename to src/pages/MainFeed.jsx
index 3413ee8..f84ac90 100644
--- a/src/components/MainFeed.jsx
+++ b/src/pages/MainFeed.jsx
@@ -1,17 +1,17 @@
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
-import { useNavigate } from "react-router-dom";
+import { useNavigate, useLocation } from "react-router-dom";
import "../styles/index.css";
import { API_URL } from "../config/authConstants";
-import Sidebar from "./Sidebar";
-import RightSidebar from "./RightSidebar";
-import PostOnFeed from "./PostOnFeed";
-import CreateUpdatePost from "./CreateUpdatePost";
-import ImageViewer from "./ImageViewer";
-import MenuIcon from "./icons/MenuIcon";
-import ProfileAvatar from "./profile/ProfileAvatar";
-import { ProfileModal } from "./profile";
-import AuthModal from "./AuthModal";
-import RulesModal from "./RulesModal";
+import Sidebar from "../components/Sidebar";
+import RightSidebar from "../components/RightSidebar";
+import PostOnFeed from "../components/PostOnFeed";
+import CreateUpdatePost from "../components/CreateUpdatePost";
+import ImageViewer from "../components/ImageViewer";
+import MenuIcon from "../components/icons/MenuIcon";
+import ProfileAvatar from "../components/profile/ProfileAvatar";
+import { ProfileModal } from "../components/profile";
+import AuthModal from "../components/AuthModal";
+import RulesModal from "../components/RulesModal";
import { useAuthContext } from "../context/AuthContext";
import { useDeletePost } from "../hooks/useDeletePost";
import { useFavorites } from "../hooks/useFavorites";
@@ -24,6 +24,7 @@ import { usePostStatus } from "../hooks/usePostStatus";
*/
export default function MainFeed() {
const navigate = useNavigate();
+ const location = useLocation();
const { user, token, isAuthenticated } = useAuthContext();
const { deletePost } = useDeletePost();
const { toggleFavorite, loadFavorites } = useFavorites();
@@ -139,6 +140,14 @@ export default function MainFeed() {
}
}, [isAuthenticated, token, loadFavorites]);
+ // Открываем модалку аутентификации если пришли с лендинга
+ useEffect(() => {
+ if (location.state?.openAuth) {
+ setAuthModalOpen(true);
+ window.history.replaceState({}, document.title);
+ }
+ }, [location.state]);
+
// Первичная загрузка при монтировании компонента
useEffect(() => {
const timer = setTimeout(() => {
diff --git a/src/pages/NakhodkaLanding.jsx b/src/pages/NakhodkaLanding.jsx
new file mode 100644
index 0000000..b335142
--- /dev/null
+++ b/src/pages/NakhodkaLanding.jsx
@@ -0,0 +1,22 @@
+import { useNavigate } from "react-router-dom";
+import "../styles/NakhodkaLanding.css";
+import Hero from "../components/landing/Hero";
+import StepsSection from "../components/landing/StepsSection";
+import FinalCta from "../components/landing/FinalCta";
+import Footer from "../components/landing/Footer";
+
+export default function NakhodkaLanding() {
+ const navigate = useNavigate();
+
+ const goToFeed = () => navigate("/feed");
+ const goToFeedWithAuth = () => navigate("/feed", { state: { openAuth: true } });
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/ReviewsPage.jsx b/src/pages/ReviewsPage.jsx
similarity index 97%
rename from src/components/ReviewsPage.jsx
rename to src/pages/ReviewsPage.jsx
index 2c5c0a7..f4ca240 100644
--- a/src/components/ReviewsPage.jsx
+++ b/src/pages/ReviewsPage.jsx
@@ -2,11 +2,11 @@ import { useState, useCallback, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { API_URL } from "../config/authConstants";
import { useAuthContext } from "../context/AuthContext";
-import MenuIcon from "./icons/MenuIcon";
-import Sidebar from "./Sidebar";
-import ProfileAvatar from "./profile/ProfileAvatar";
-import { ProfileModal } from "./profile";
-import AuthModal from "./AuthModal";
+import MenuIcon from "../components/icons/MenuIcon";
+import Sidebar from "../components/Sidebar";
+import ProfileAvatar from "../components/profile/ProfileAvatar";
+import { ProfileModal } from "../components/profile";
+import AuthModal from "../components/AuthModal";
import "../styles/reviews.css";
/* ── Star rating sub-component ── */
diff --git a/src/styles/NakhodkaLanding.css b/src/styles/NakhodkaLanding.css
new file mode 100644
index 0000000..3db0f20
--- /dev/null
+++ b/src/styles/NakhodkaLanding.css
@@ -0,0 +1,337 @@
+@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&display=swap');
+
+.nk-page {
+ font-family: 'Inter', sans-serif;
+ background: #0A1F1E;
+ color: #F4F7F6;
+ min-height: 100vh;
+}
+
+@keyframes beaconPulse {
+ 0% {
+ transform: scale(0.6);
+ opacity: 0.55;
+ }
+ 70% {
+ transform: scale(1.9);
+ opacity: 0;
+ }
+ 100% {
+ transform: scale(1.9);
+ opacity: 0;
+ }
+}
+
+@keyframes markerIn {
+ 0% {
+ opacity: 0;
+ transform: translateY(6px) scale(0.7);
+ }
+ 15% {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+ 85% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ }
+}
+
+@keyframes drift {
+ 0% {
+ background-position: 0 0;
+ }
+ 100% {
+ background-position: 60px 60px;
+ }
+}
+
+@keyframes riseIn {
+ 0% {
+ opacity: 0;
+ transform: translateY(16px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.nk-hero {
+ position: relative;
+ padding: 28px 6vw 0;
+ min-height: 620px;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.nk-grid {
+ position: absolute;
+ inset: 0;
+ background-image: radial-gradient(rgba(244, 247, 246, 0.10) 1px, transparent 1px);
+ background-size: 26px 26px;
+ animation: drift 14s linear infinite;
+ opacity: 0.6;
+}
+
+.nk-glow {
+ position: absolute;
+ top: -20%;
+ right: -10%;
+ width: 600px;
+ height: 600px;
+ background: radial-gradient(circle, rgba(245, 166, 35, 0.18) 0%, transparent 70%);
+ filter: blur(10px);
+}
+
+.nk-nav {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.nk-logo {
+ font-family: 'Fraunces', serif;
+ font-size: 22px;
+ font-weight: 600;
+ letter-spacing: 0.01em;
+}
+
+.nk-nav-btn {
+ background: rgba(244, 247, 246, 0.08);
+ border: 1px solid rgba(244, 247, 246, 0.16);
+ color: #F4F7F6;
+ padding: 9px 20px;
+ border-radius: 999px;
+ font-size: 14px;
+ cursor: pointer;
+ backdrop-filter: blur(6px);
+}
+
+.nk-radar {
+ position: absolute;
+ right: 4vw;
+ top: 90px;
+ width: 340px;
+ height: 340px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 3;
+}
+
+.nk-ring {
+ position: absolute;
+ width: 140px;
+ height: 140px;
+ border-radius: 50%;
+ border: 1px solid rgba(245, 166, 35, 0.55);
+ animation: beaconPulse 3.9s ease-out infinite;
+ will-change: transform;
+ transform: translateZ(0);
+}
+
+.nk-core {
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: #F5A623;
+ box-shadow: 0 0 22px 6px rgba(245, 166, 35, 0.55);
+}
+
+.nk-marker {
+ position: absolute;
+ color: #0A1F1E;
+ background: #F5A623;
+ width: 34px;
+ height: 34px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: markerIn 3.2s ease-in-out infinite;
+ box-shadow: 0 4px 14px rgba(245, 166, 35, 0.4);
+ will-change: transform;
+ transform: translateZ(0);
+}
+
+.nk-hero-text {
+ position: relative;
+ z-index: 2;
+ max-width: 620px;
+ margin-top: 90px;
+ animation: riseIn 0.8s ease both;
+}
+
+.nk-eyebrow {
+ display: inline-block;
+ font-size: 13px;
+ color: #8FADA8;
+ letter-spacing: 0.04em;
+ margin-bottom: 18px;
+ text-transform: uppercase;
+}
+
+.nk-h1 {
+ font-family: 'Fraunces', serif;
+ font-weight: 600;
+ font-size: clamp(34px, 5.2vw, 58px);
+ line-height: 1.08;
+ margin: 0 0 22px;
+}
+
+.nk-accent {
+ color: #F5A623;
+}
+
+.nk-lead {
+ font-size: 17px;
+ line-height: 1.6;
+ color: #C7DAD6;
+ max-width: 500px;
+ margin-bottom: 34px;
+}
+
+.nk-cta-row {
+ display: flex;
+ align-items: center;
+ gap: 18px;
+ flex-wrap: wrap;
+}
+
+.nk-cta-primary {
+ background: #F5A623;
+ color: #0A1F1E;
+ border: none;
+ padding: 15px 30px;
+ border-radius: 999px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.nk-cta-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 12px 28px rgba(245, 166, 35, 0.35);
+}
+
+.nk-cta-hint {
+ font-size: 13px;
+ color: #7FA39D;
+}
+
+
+.nk-steps-section {
+ padding: 0 6vw 60px;
+}
+
+.nk-h2 {
+ font-family: 'Fraunces', serif;
+ font-size: clamp(24px, 3vw, 34px);
+ font-weight: 600;
+ margin-bottom: 42px;
+ text-align: center;
+}
+
+.nk-steps-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+ gap: 22px;
+ max-width: 1000px;
+ margin: 0 auto;
+}
+
+.nk-step-card {
+ background: rgba(244, 247, 246, 0.04);
+ border: 1px solid rgba(244, 247, 246, 0.10);
+ border-radius: 20px;
+ padding: 26px 24px;
+ transition: border-color 0.2s;
+}
+
+.nk-step-card:hover {
+ border-color: rgba(244, 247, 246, 0.22);
+}
+
+.nk-step-num {
+ font-family: 'Fraunces', serif;
+ font-size: 15px;
+ color: #F5A623;
+ font-weight: 600;
+}
+
+.nk-step-title {
+ font-size: 18px;
+ font-weight: 600;
+ margin: 12px 0 8px;
+}
+
+.nk-step-text {
+ font-size: 14.5px;
+ line-height: 1.55;
+ color: #9FBAB5;
+}
+
+.nk-final-cta {
+ text-align: center;
+ padding: 70px 6vw 100px;
+ max-width: 700px;
+ margin: 0 auto;
+}
+
+.nk-footer {
+ text-align: center;
+ padding: 24px 0 34px;
+ font-size: 13px;
+ color: #5F827D;
+}
+
+@media (max-width: 768px) {
+ .nk-hero {
+ padding-bottom: 150px;
+ }
+
+ .nk-radar {
+ width: 150px;
+ height: 150px;
+ right: 10vw;
+ top: auto;
+ bottom: 100px;
+ }
+
+ .nk-ring {
+ width: 85px;
+ height: 85px;
+ }
+
+ .nk-core {
+ width: 14px;
+ height: 14px;
+ }
+
+ .nk-marker {
+ width: 30px;
+ height: 30px;
+ }
+
+ .nk-marker svg {
+ width: 18px;
+ height: 18px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .nk-ring, .nk-marker {
+ animation-duration: 8s !important;
+ }
+
+ .nk-grid {
+ animation: none !important;
+ }
+}
Connect with us
+Join the Vite community
++-
+
+
+ GitHub
+
+
+ -
+
+
+ Discord
+
+
+ -
+
+
+ X.com
+
+
+ -
+
+
+ Bluesky
+
+
+
+