added new start page

This commit is contained in:
SlimusMinus
2026-07-06 23:59:48 +03:00
parent 538a7f65eb
commit abdb7c021c
17 changed files with 705 additions and 24 deletions

122
react_front/src/App.tsx Normal file
View File

@@ -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 (
<>
<section id="center">
<div className="hero">
<img src={heroImg} className="base" width="170" height="179" alt="" />
<img src={reactLogo} className="framework" alt="React logo" />
<img src={viteLogo} className="vite" alt="Vite logo" />
</div>
<div>
<h1>Get started</h1>
<p>
Edit <code>src/App.tsx</code> and save to test <code>HMR</code>
</p>
</div>
<button
type="button"
className="counter"
onClick={() => setCount((count) => count + 1)}
>
Count is {count}
</button>
</section>
<div className="ticks"></div>
<section id="next-steps">
<div id="docs">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#documentation-icon"></use>
</svg>
<h2>Documentation</h2>
<p>Your questions, answered</p>
<ul>
<li>
<a href="https://vite.dev/" target="_blank">
<img className="logo" src={viteLogo} alt="" />
Explore Vite
</a>
</li>
<li>
<a href="https://react.dev/" target="_blank">
<img className="button-icon" src={reactLogo} alt="" />
Learn more
</a>
</li>
</ul>
</div>
<div id="social">
<svg className="icon" role="presentation" aria-hidden="true">
<use href="/icons.svg#social-icon"></use>
</svg>
<h2>Connect with us</h2>
<p>Join the Vite community</p>
<ul>
<li>
<a href="https://github.com/vitejs/vite" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#github-icon"></use>
</svg>
GitHub
</a>
</li>
<li>
<a href="https://chat.vite.dev/" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#discord-icon"></use>
</svg>
Discord
</a>
</li>
<li>
<a href="https://x.com/vite_js" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#x-icon"></use>
</svg>
X.com
</a>
</li>
<li>
<a href="https://bsky.app/profile/vite.dev" target="_blank">
<svg
className="button-icon"
role="presentation"
aria-hidden="true"
>
<use href="/icons.svg#bluesky-icon"></use>
</svg>
Bluesky
</a>
</li>
</ul>
</div>
</section>
<div className="ticks"></div>
<section id="spacer"></section>
</>
)
}
export default App

View File

@@ -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() {
<UnreadListener />
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate to="/feed" replace />} />
<Route path="/" element={<LandingPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/feed" element={<MainFeed />} />

View File

@@ -0,0 +1,10 @@
export default function FinalCta({ onClick }) {
return (
<section className="nk-final-cta">
<h2 className="nk-h2">Кто-то прямо сейчас ищет то, что вы нашли</h2>
<button className="nk-cta-primary" onClick={onClick}>
Открыть Находку
</button>
</section>
);
}

View File

@@ -0,0 +1,7 @@
export default function Footer() {
return (
<footer className="nk-footer">
© {new Date().getFullYear()} Находка. Возвращаем найденное.
</footer>
);
}

View File

@@ -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 (
<section className="nk-hero">
<div className="nk-grid" />
<div className="nk-glow" />
<RadarBeacon />
<Navbar onLoginClick={onLoginClick || onCtaClick} />
<div className="nk-hero-text">
<span className="nk-eyebrow">Сервис поиска пропавших питомцев и вещей</span>
<h1 className="nk-h1">
Потерял — не значит
<br />
<span className="nk-accent">потерял навсегда</span>
</h1>
<p className="nk-lead">
Разместите объявление о пропаже или находке — Находка сама сопоставит
похожие записи по фото, месту и описанию и покажет совпадения рядом с вами.
</p>
<div className="nk-cta-row">
<button
className="nk-cta-primary"
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
onClick={onCtaClick}
>
Начать поиск
<span
style={{
marginLeft: 8,
display: "inline-block",
transform: hover ? "translateX(3px)" : "none",
transition: "transform .2s",
}}
>
→
</span>
</button>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,37 @@
export default function Icon({ type }) {
if (type === "paw") {
return (
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
<circle cx="7" cy="7" r="2.1" />
<circle cx="12.3" cy="5.2" r="2.1" />
<circle cx="17.2" cy="7.4" r="2.1" />
<path d="M12 10.5c-3.4 0-6.2 2.4-6.2 5.4 0 1.7 1.4 2.9 3 2.5 1.2-.3 2-1 3.2-1s2 .7 3.2 1c1.6.4 3-.8 3-2.5 0-3-2.8-5.4-6.2-5.4z" />
</svg>
);
}
if (type === "bag") {
return (
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M4 10h16l-1.4 10.5a2 2 0 0 1-2 1.5H7.4a2 2 0 0 1-2-1.5L4 10z" />
<path d="M8 10V6a4 4 0 0 1 8 0v4" />
</svg>
);
}
if (type === "key") {
return (
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="1.8">
<circle cx="8" cy="8" r="4.2" />
<path d="M11 11l9 9M16.5 16.5l2.5-2.5M19 19l2-2" strokeLinecap="round" />
</svg>
);
}
return (
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="1.8">
<rect x="7" y="2.5" width="10" height="19" rx="2.2" />
<line x1="11" y1="18.5" x2="13" y2="18.5" strokeLinecap="round" />
</svg>
);
}

View File

@@ -0,0 +1,10 @@
export default function Navbar({ onLoginClick }) {
return (
<nav className="nk-nav">
<span className="nk-logo">Находка</span>
<button className="nk-nav-btn" onClick={onLoginClick}>
Войти
</button>
</nav>
);
}

View File

@@ -0,0 +1,27 @@
import Icon from "./Icon";
import { markers } from "../../config/landingConstants";
export default function RadarBeacon() {
return (
<div className="nk-radar">
<span className="nk-ring" style={{ animationDelay: "0s" }} />
<span className="nk-ring" style={{ animationDelay: "1.3s" }} />
<span className="nk-ring" style={{ animationDelay: "2.6s" }} />
<div className="nk-core" />
{markers.map((m) => (
<div
key={m.id}
className="nk-marker"
style={{
left: `${m.x}%`,
top: `${m.y}%`,
animationDelay: m.delay,
}}
>
<Icon type={m.icon} />
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,18 @@
import { steps } from "../../config/landingConstants";
export default function StepsSection() {
return (
<section className="nk-steps-section">
<h2 className="nk-h2">Как это работает</h2>
<div className="nk-steps-grid">
{steps.map((s) => (
<div className="nk-step-card" key={s.n}>
<span className="nk-step-num">{s.n}</span>
<h3 className="nk-step-title">{s.title}</h3>
<p className="nk-step-text">{s.text}</p>
</div>
))}
</div>
</section>
);
}

View File

@@ -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: "поиск работает без перерыва" },
];

View File

@@ -16,5 +16,4 @@ html, body, #root {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}

View File

@@ -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();

View File

@@ -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(() => {

View File

@@ -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 (
<div className="nk-page">
<Hero onCtaClick={goToFeed} onLoginClick={goToFeedWithAuth} />
<StepsSection />
<FinalCta onClick={goToFeed} />
<Footer />
</div>
);
}

View File

@@ -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 ── */

View File

@@ -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;
}
}