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 ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
-
+ + + {/* Главная → сразу редирект на логин */} + } /> + + {/* Страница авторизации */} + } /> + + {/* Сюда добавишь остальные страницы позже */} + {/* } /> */} + + ); } -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) => ( + + ))} + +
+ + +
+ +
+ +

Находка

+ +
{ e.preventDefault(); login(email, password); }} noValidate> +
+ setEmail(e.target.value)} + style={s.input} className="li" + autoComplete="off" name="naidishchey-email-x7k" + readOnly onFocus={(e) => e.target.removeAttribute("readOnly")} + /> +
+ +
+ setPassword(e.target.value)} + style={{ ...s.input, paddingRight: "44px" }} className="li" + autoComplete="off" name="naidishchey-pass-x7k" + readOnly onFocus={(e) => e.target.removeAttribute("readOnly")} + /> + +
+ + {error &&

{error}

} + + +
+ + + +

+ Нет аккаунта? Зарегистрироваться +

+ +
+ или +
+ +
+ + + +
+
+
+ ); +} \ 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