diff --git a/package-lock.json b/package-lock.json
index 74c8820..b3999eb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,7 @@
"name": "react_front",
"version": "0.1.0",
"dependencies": {
+ "@hookform/resolvers": "^5.2.2",
"@pbe/react-yandex-maps": "^1.2.5",
"@tailwindcss/vite": "^4.2.4",
"@tanstack/react-query": "^5.100.5",
@@ -23,12 +24,17 @@
"postcss": "^8.5.12",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-hook-form": "^7.74.0",
"react-router-dom": "^7.14.2",
"react-scripts": "5.0.1",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.4",
"web-vitals": "^2.1.4",
+ "zod": "^4.3.6",
"zustand": "^5.0.12"
+ },
+ "devDependencies": {
+ "@types/node": "^25.6.0"
}
},
"node_modules/@adobe/css-tools": {
@@ -2500,6 +2506,18 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@hookform/resolvers": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
+ "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/utils": "^0.3.0"
+ },
+ "peerDependencies": {
+ "react-hook-form": "^7.55.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -3469,6 +3487,12 @@
"@sinonjs/commons": "^1.7.0"
}
},
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
"node_modules/@surma/rollup-plugin-off-main-thread": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
@@ -14833,6 +14857,22 @@
"integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==",
"license": "MIT"
},
+ "node_modules/react-hook-form": {
+ "version": "7.74.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.74.0.tgz",
+ "integrity": "sha512-yR6wHr99p9wFv686jhRWVSFhUvDvNbdUf2dKlbno8/VKOCuoNobDGC6S+M2dua9A9Yo8vpcrp8assIYbsZCQ9g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -18543,6 +18583,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/zustand": {
"version": "5.0.12",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
diff --git a/package.json b/package.json
index b206d87..a768b5c 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@hookform/resolvers": "^5.2.2",
"@pbe/react-yandex-maps": "^1.2.5",
"@tailwindcss/vite": "^4.2.4",
"@tanstack/react-query": "^5.100.5",
@@ -18,11 +19,13 @@
"postcss": "^8.5.12",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-hook-form": "^7.74.0",
"react-router-dom": "^7.14.2",
"react-scripts": "5.0.1",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.4",
"web-vitals": "^2.1.4",
+ "zod": "^4.3.6",
"zustand": "^5.0.12"
},
"scripts": {
@@ -48,5 +51,8 @@
"last 1 firefox version",
"last 1 safari version"
]
+ },
+ "devDependencies": {
+ "@types/node": "^25.6.0"
}
}
diff --git a/src/App.js b/src/App.js
index 3784575..9bebcf3 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,25 +1,21 @@
-import logo from './logo.svg';
-import './App.css';
+import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
+import LoginPage from './pages/LoginPage';
function App() {
return (
-
+
+
+ {/* Главная → сразу редирект на логин */}
+ } />
+
+ {/* Страница авторизации */}
+ } />
+
+ {/* Сюда добавишь остальные страницы позже */}
+ {/* } /> */}
+
+
);
}
-export default App;
+export default App;
\ No newline at end of file
diff --git a/src/assets/bag.svg b/src/assets/bag.svg
new file mode 100644
index 0000000..ca7adb3
--- /dev/null
+++ b/src/assets/bag.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/card.png b/src/assets/card.png
new file mode 100644
index 0000000..3ec60f7
Binary files /dev/null and b/src/assets/card.png differ
diff --git a/src/assets/cat.png b/src/assets/cat.png
new file mode 100644
index 0000000..86f78d9
Binary files /dev/null and b/src/assets/cat.png differ
diff --git a/src/assets/dog.png b/src/assets/dog.png
new file mode 100644
index 0000000..e042c2d
Binary files /dev/null and b/src/assets/dog.png differ
diff --git a/src/assets/glasses.png b/src/assets/glasses.png
new file mode 100644
index 0000000..c89cae1
Binary files /dev/null and b/src/assets/glasses.png differ
diff --git a/src/assets/headphones.png b/src/assets/headphones.png
new file mode 100644
index 0000000..e8dbdcc
Binary files /dev/null and b/src/assets/headphones.png differ
diff --git a/src/assets/keys.png b/src/assets/keys.png
new file mode 100644
index 0000000..ce68cb2
Binary files /dev/null and b/src/assets/keys.png differ
diff --git a/src/assets/parcel.png b/src/assets/parcel.png
new file mode 100644
index 0000000..b8a4e99
Binary files /dev/null and b/src/assets/parcel.png differ
diff --git a/src/assets/passwords_keys_6032.png b/src/assets/passwords_keys_6032.png
new file mode 100644
index 0000000..7f37a85
Binary files /dev/null and b/src/assets/passwords_keys_6032.png differ
diff --git a/src/assets/phone.png b/src/assets/phone.png
new file mode 100644
index 0000000..afe75d0
Binary files /dev/null and b/src/assets/phone.png differ
diff --git a/src/assets/wallet.png b/src/assets/wallet.png
new file mode 100644
index 0000000..a556415
Binary files /dev/null and b/src/assets/wallet.png differ
diff --git a/src/assets/watch.png b/src/assets/watch.png
new file mode 100644
index 0000000..5cbc95c
Binary files /dev/null and b/src/assets/watch.png differ
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
deleted file mode 100644
index e69de29..0000000
diff --git a/src/components/MagnifierLogo.jsx b/src/components/MagnifierLogo.jsx
new file mode 100644
index 0000000..f394c2c
--- /dev/null
+++ b/src/components/MagnifierLogo.jsx
@@ -0,0 +1,85 @@
+import { s } from '../styles/loginStyles';
+import keysIcon from '../assets/passwords_keys_6032.png';
+
+export default function MagnifierLogo() {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/config/constants.js b/src/config/constants.js
new file mode 100644
index 0000000..e157a3e
--- /dev/null
+++ b/src/config/constants.js
@@ -0,0 +1,26 @@
+import bagIcon from '../assets/bag.svg';
+import catIcon from '../assets/cat.png';
+import dogIcon from '../assets/dog.png';
+import phoneIcon from '../assets/phone.png';
+import walletIcon from '../assets/wallet.png';
+import parcelIcon from '../assets/parcel.png';
+import cardIcon from '../assets/card.png';
+import headphonesIcon from '../assets/headphones.png';
+import watchIcon from '../assets/watch.png';
+import glassesIcon from '../assets/glasses.png';
+
+export const API_URL = process.env.REACT_APP_API_URL || "http://localhost:8080";
+
+export const FLOAT_ICONS = [
+ { icon: bagIcon, top: "5%", left: "5%", delay: "0s" },
+ { icon: catIcon, top: "5%", right: "5%", delay: "1s" },
+ { icon: dogIcon, top: "25%", left: "25%", delay: "0.4s" },
+ { icon: phoneIcon, top: "25%", right: "17%", delay: "1.4s" },
+ { icon: walletIcon, top: "45%", left: "5%", delay: "2s" },
+ { icon: parcelIcon, top: "45%", right: "25%", delay: "0.7s" },
+ { icon: cardIcon, top: "65%", left: "10%", delay: "1.7s" },
+ { icon: headphonesIcon, top: "65%", right: "2%", delay: "2.4s" },
+ { icon: watchIcon, bottom: "15%", left: "20%", delay: "3s" },
+ { icon: glassesIcon, bottom: "5%", right: "20%", delay: "3.4s" },
+];
+
diff --git a/src/hooks/useAuth.js b/src/hooks/useAuth.js
new file mode 100644
index 0000000..be49a8a
--- /dev/null
+++ b/src/hooks/useAuth.js
@@ -0,0 +1,54 @@
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { API_URL } from "../config/constants";
+
+export function useAuth() {
+ const navigate = useNavigate();
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState("");
+
+ const login = async (email, password) => {
+ if (!email || !password) { setError("Введи email и пароль"); return; }
+ setLoading(true); setError("");
+
+ try {
+ const res = await fetch(`${API_URL}/api/auth/login`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email, password }),
+ });
+
+ if (!res.ok) {
+ const data = await res.json().catch(() => ({}));
+ throw new Error(data.message || "Неверный email или пароль");
+ }
+
+ const { token, user } = await res.json();
+ localStorage.setItem("token", token);
+ localStorage.setItem("user", JSON.stringify(user));
+ navigate("/dashboard");
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const loginGoogle = () => { /* TODO: @react-oauth/google */ };
+
+ const loginYandex = () => {
+ const clientId = process.env.REACT_APP_YANDEX_CLIENT_ID;
+ const redirect = encodeURIComponent(`${window.location.origin}/auth/yandex`);
+ window.location.href =
+ `https://oauth.yandex.ru/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirect}`;
+ };
+
+ const loginVK = () => {
+ const appId = process.env.REACT_APP_VK_APP_ID;
+ const redirect = encodeURIComponent(`${window.location.origin}/auth/vk`);
+ window.location.href =
+ `https://oauth.vk.com/authorize?client_id=${appId}&redirect_uri=${redirect}&response_type=code&scope=email`;
+ };
+
+ return { login, loginGoogle, loginYandex, loginVK, loading, error };
+}
\ No newline at end of file
diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx
new file mode 100644
index 0000000..08858eb
--- /dev/null
+++ b/src/pages/LoginPage.jsx
@@ -0,0 +1,82 @@
+import { useState } from "react";
+import MagnifierLogo from "../components/MagnifierLogo";
+import { useAuth } from "../hooks/useAuth";
+import { FLOAT_ICONS } from "../config/constants";
+import { s } from "../styles/loginStyles";
+import "../styles/global.css";
+
+export default function LoginPage() {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [showPass, setShowPass] = useState(false);
+
+ const { login, loginGoogle, loginYandex, loginVK, loading, error } = useAuth();
+
+ return (
+
+
+
+
+ {FLOAT_ICONS.map(({ icon, delay, size, ...pos }, i) => (
+

+ ))}
+
+
+
+
+
+
+
+
+
Находка
+
+
+
+
+
+
+ Нет аккаунта? Зарегистрироваться
+
+
+
+ или
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/styles/global.css b/src/styles/global.css
new file mode 100644
index 0000000..46eebaf
--- /dev/null
+++ b/src/styles/global.css
@@ -0,0 +1,54 @@
+@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&display=swap');
+
+.fi {
+ position: fixed;
+ opacity: 0.15;
+ filter: grayscale(1) brightness(2);
+ pointer-events: none;
+ user-select: none;
+ animation: float 7s ease-in-out infinite;
+}
+@keyframes float {
+ 0%, 100% { transform: translateY(0) rotate(-6deg); }
+ 50% { transform: translateY(-18px) rotate(6deg); }
+}
+
+.card-in {
+ animation: cardIn .7s cubic-bezier(.34,1.56,.64,1) both;
+}
+@keyframes cardIn {
+ from { opacity: 0; transform: scale(.86) translateY(40px); }
+ to { opacity: 1; transform: scale(1) translateY(0); }
+}
+
+@keyframes logoPulse {
+ 0%, 100% { filter: drop-shadow(0 0 18px rgba(45,224,200,.45)); }
+ 50% { filter: drop-shadow(0 0 38px rgba(45,224,200,.8)); }
+}
+
+@keyframes shimmer {
+ 0% { background-position: 200% 0; }
+ 100% { background-position: -200% 0; }
+}
+
+.li:focus {
+ border-color: #2de0c8 !important;
+ background: rgba(45,224,200,.07) !important;
+ box-shadow: 0 0 0 3px rgba(45,224,200,.12) !important;
+}
+.li::placeholder { color: rgba(255,255,255,.32); }
+
+.btn-l:hover { opacity: .88; transform: translateY(-2px); transition: all .15s; }
+.btn-l:active { transform: translateY(0); }
+
+.soc:hover {
+ background: rgba(255,255,255,.1) !important;
+ border-color: rgba(255,255,255,.22) !important;
+ transform: translateY(-1px);
+ transition: all .2s;
+}
+
+.div-line::before, .div-line::after {
+ content: ''; flex: 1;
+ height: 1px; background: rgba(255,255,255,.08);
+}
\ No newline at end of file
diff --git a/src/styles/loginStyles.js b/src/styles/loginStyles.js
new file mode 100644
index 0000000..ba45b63
--- /dev/null
+++ b/src/styles/loginStyles.js
@@ -0,0 +1,88 @@
+export const s = {
+ page: {
+ fontFamily: "'Nunito', sans-serif",
+ minHeight: "100vh",
+ display: "flex", alignItems: "center", justifyContent: "center",
+ background: "radial-gradient(ellipse at 30% 40%, #1a2340 0%, #0d1018 60%)",
+ overflow: "hidden", position: "relative",
+ },
+ bgGlow: {
+ position: "fixed", top: "40%", right: 0,
+ width: "500px", height: "400px",
+ background: "radial-gradient(ellipse, #0f2a22 0%, transparent 70%)",
+ pointerEvents: "none", opacity: 0.7,
+ },
+ card: {
+ position: "relative", zIndex: 10, width: "360px",
+ background: "linear-gradient(155deg, #1d2436 0%, #131720 100%)",
+ border: "1px solid rgba(255,255,255,0.08)",
+ borderRadius: "30px", padding: "40px 38px 34px",
+ boxShadow: "0 40px 100px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.03)",
+ },
+ dotsBtn: {
+ position: "absolute", top: "18px", right: "20px",
+ background: "none", border: "none",
+ color: "rgba(255,255,255,0.25)", fontSize: "1.3rem",
+ cursor: "pointer", letterSpacing: "3px",
+ },
+ logoWrap: { textAlign: "center", marginBottom: "2px" },
+ logoSvg: {
+ width: "130px", height: "130px",
+ animation: "logoPulse 3s ease-in-out infinite",
+ },
+ title: {
+ textAlign: "center", color: "#fff",
+ fontSize: "2rem", fontWeight: 900,
+ letterSpacing: "-0.5px", margin: "14px 0 26px",
+ },
+ inputWrap: { marginBottom: "13px" },
+ input: {
+ width: "100%", boxSizing: "border-box",
+ background: "rgba(255,255,255,0.055)",
+ border: "1.5px solid rgba(255,255,255,0.09)",
+ borderRadius: "14px", padding: "14px 18px",
+ color: "#fff", fontFamily: "'Nunito', sans-serif",
+ fontSize: "0.93rem", outline: "none",
+ },
+ eyeBtn: {
+ position: "absolute", right: "13px", top: "50%",
+ transform: "translateY(-50%)",
+ background: "none", border: "none", cursor: "pointer",
+ color: "rgba(255,255,255,0.35)", fontSize: "1.05rem", lineHeight: 1,
+ },
+ errMsg: {
+ color: "#ff6b6b", fontSize: "0.83rem",
+ textAlign: "center", margin: "6px 0 10px",
+ },
+ btnLogin: {
+ width: "100%",
+ background: "linear-gradient(90deg, #1fc9b0, #2de0c8, #1fc9b0)",
+ backgroundSize: "200% 100%",
+ border: "none", borderRadius: "14px", padding: "15px",
+ color: "#082320", fontFamily: "'Nunito', sans-serif",
+ fontSize: "1.05rem", fontWeight: 900, cursor: "pointer", marginTop: "6px",
+ boxShadow: "0 6px 24px rgba(45,224,200,0.35)",
+ animation: "shimmer 3s linear infinite",
+ },
+ forgotBtn: {
+ display: "block", width: "100%", textAlign: "center",
+ color: "#2de0c8", fontSize: "0.87rem", marginTop: "14px",
+ background: "none", border: "none", cursor: "pointer",
+ fontFamily: "'Nunito', sans-serif",
+ },
+ regRow: {
+ textAlign: "center", color: "rgba(255,255,255,0.4)",
+ fontSize: "0.87rem", marginTop: "9px",
+ },
+ link: { color: "#2de0c8", textDecoration: "none" },
+ divider: { display: "flex", alignItems: "center", gap: "10px", margin: "18px 0 14px" },
+ divText: { color: "rgba(255,255,255,0.25)", fontSize: "0.78rem" },
+ socials: { display: "flex", gap: "10px" },
+ socBtn: {
+ flex: 1, padding: "11px 0", borderRadius: "12px",
+ border: "1.5px solid rgba(255,255,255,0.09)",
+ background: "rgba(255,255,255,0.04)",
+ cursor: "pointer", fontWeight: 800,
+ fontFamily: "'Nunito', sans-serif", fontSize: "0.88rem", color: "#fff",
+ },
+};
\ No newline at end of file