add auth window
49
package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "react_front",
|
"name": "react_front",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@pbe/react-yandex-maps": "^1.2.5",
|
"@pbe/react-yandex-maps": "^1.2.5",
|
||||||
"@tailwindcss/vite": "^4.2.4",
|
"@tailwindcss/vite": "^4.2.4",
|
||||||
"@tanstack/react-query": "^5.100.5",
|
"@tanstack/react-query": "^5.100.5",
|
||||||
@@ -23,12 +24,17 @@
|
|||||||
"postcss": "^8.5.12",
|
"postcss": "^8.5.12",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hook-form": "^7.74.0",
|
||||||
"react-router-dom": "^7.14.2",
|
"react-router-dom": "^7.14.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss": "^4.2.4",
|
"tailwindcss": "^4.2.4",
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
|
"zod": "^4.3.6",
|
||||||
"zustand": "^5.0.12"
|
"zustand": "^5.0.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adobe/css-tools": {
|
"node_modules/@adobe/css-tools": {
|
||||||
@@ -2500,6 +2506,18 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"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": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||||
@@ -3469,6 +3487,12 @@
|
|||||||
"@sinonjs/commons": "^1.7.0"
|
"@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": {
|
"node_modules/@surma/rollup-plugin-off-main-thread": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz",
|
"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==",
|
"integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/react-is": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
@@ -18543,6 +18583,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/zustand": {
|
||||||
"version": "5.0.12",
|
"version": "5.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@pbe/react-yandex-maps": "^1.2.5",
|
"@pbe/react-yandex-maps": "^1.2.5",
|
||||||
"@tailwindcss/vite": "^4.2.4",
|
"@tailwindcss/vite": "^4.2.4",
|
||||||
"@tanstack/react-query": "^5.100.5",
|
"@tanstack/react-query": "^5.100.5",
|
||||||
@@ -18,11 +19,13 @@
|
|||||||
"postcss": "^8.5.12",
|
"postcss": "^8.5.12",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hook-form": "^7.74.0",
|
||||||
"react-router-dom": "^7.14.2",
|
"react-router-dom": "^7.14.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss": "^4.2.4",
|
"tailwindcss": "^4.2.4",
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
|
"zod": "^4.3.6",
|
||||||
"zustand": "^5.0.12"
|
"zustand": "^5.0.12"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -48,5 +51,8 @@
|
|||||||
"last 1 firefox version",
|
"last 1 firefox version",
|
||||||
"last 1 safari version"
|
"last 1 safari version"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/App.js
@@ -1,24 +1,20 @@
|
|||||||
import logo from './logo.svg';
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import './App.css';
|
import LoginPage from './pages/LoginPage';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<BrowserRouter>
|
||||||
<header className="App-header">
|
<Routes>
|
||||||
<img src={logo} className="App-logo" alt="logo" />
|
{/* Главная → сразу редирект на логин */}
|
||||||
<p>
|
<Route path="/" element={<Navigate to="/login" replace />} />
|
||||||
Edit <code>src/App.js</code> and save to reload.
|
|
||||||
</p>
|
{/* Страница авторизации */}
|
||||||
<a
|
<Route path="/login" element={<LoginPage />} />
|
||||||
className="App-link"
|
|
||||||
href="https://reactjs.org"
|
{/* Сюда добавишь остальные страницы позже */}
|
||||||
target="_blank"
|
{/* <Route path="/dashboard" element={<Dashboard />} /> */}
|
||||||
rel="noopener noreferrer"
|
</Routes>
|
||||||
>
|
</BrowserRouter>
|
||||||
Learn React
|
|
||||||
</a>
|
|
||||||
</header>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
src/assets/bag.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" ?><svg fill="none" height="26" viewBox="0 0 28 26" width="28" xmlns="http://www.w3.org/2000/svg"><path d="M23.3 25H4.70001C2.80001 25 1.20001 23.4 1.20001 21.5V9.29999C1.20001 7.39999 2.80001 5.79999 4.70001 5.79999H23.3C25.2 5.79999 26.8 7.39999 26.8 9.29999V21.5C26.8 23.4 25.2 25 23.3 25Z" stroke="#4F4F4F" stroke-miterlimit="10" stroke-width="2"/><path d="M20.4 5.79999H7.60001V3C7.60001 1.9 8.50001 1 9.60001 1H18.4C19.5 1 20.4 1.9 20.4 3V5.79999Z" stroke="#4F4F4F" stroke-miterlimit="10" stroke-width="2"/><path d="M2 13.8H26.8" stroke="#4F4F4F" stroke-miterlimit="10" stroke-width="2"/><path d="M6.79999 12.2V16.2" stroke="#4F4F4F" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/><path d="M21.2 12.2V16.2" stroke="#4F4F4F" stroke-linecap="round" stroke-miterlimit="10" stroke-width="2"/></svg>
|
||||||
|
After Width: | Height: | Size: 836 B |
BIN
src/assets/card.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/cat.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/assets/dog.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/glasses.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
src/assets/headphones.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
BIN
src/assets/keys.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/parcel.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
src/assets/passwords_keys_6032.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src/assets/phone.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/assets/wallet.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
src/assets/watch.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
85
src/components/MagnifierLogo.jsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { s } from '../styles/loginStyles';
|
||||||
|
import keysIcon from '../assets/passwords_keys_6032.png';
|
||||||
|
|
||||||
|
export default function MagnifierLogo() {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 90 90" xmlns="http://www.w3.org/2000/svg" style={s.logoSvg}>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="glassGrad" cx="38%" cy="35%" r="60%">
|
||||||
|
<stop offset="0%" stopColor="#7ffff0"/>
|
||||||
|
<stop offset="45%" stopColor="#2de0c8"/>
|
||||||
|
<stop offset="100%" stopColor="#0a7a68"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="rimGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor="#e8eef5"/>
|
||||||
|
<stop offset="40%" stopColor="#b0bec5"/>
|
||||||
|
<stop offset="100%" stopColor="#607d8b"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="handleGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor="#4a4a4a"/>
|
||||||
|
<stop offset="25%" stopColor="#2a2a2a"/>
|
||||||
|
<stop offset="50%" stopColor="#111111"/>
|
||||||
|
<stop offset="75%" stopColor="#1e1e1e"/>
|
||||||
|
<stop offset="100%" stopColor="#000000"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="innerGlow" cx="50%" cy="50%" r="50%">
|
||||||
|
<stop offset="0%" stopColor="#ffffff" stopOpacity="0.25"/>
|
||||||
|
<stop offset="100%" stopColor="#ffffff" stopOpacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="shimmer2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor="#ffffff" stopOpacity="0.7"/>
|
||||||
|
<stop offset="100%" stopColor="#ffffff" stopOpacity="0"/>
|
||||||
|
</linearGradient>
|
||||||
|
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feDropShadow dx="1" dy="2" stdDeviation="3" floodColor="#000"
|
||||||
|
floodOpacity="0.5"/>
|
||||||
|
</filter>
|
||||||
|
<clipPath id="lensClip">
|
||||||
|
<circle cx="35" cy="35" r="23"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
{/* Ручка */}
|
||||||
|
<g filter="url(#shadow)">
|
||||||
|
<rect x="60" y="56" width="11" height="28" rx="5.5"
|
||||||
|
fill="url(#handleGrad)" transform="rotate(-45 62.5 71)"/>
|
||||||
|
<rect x="61" y="56" width="3" height="22" rx="1.5"
|
||||||
|
fill="white" opacity="0.12" transform="rotate(-45 62.5 71)" />
|
||||||
|
<g transform="rotate(-45 62.5 71)" opacity="0.08">
|
||||||
|
<line x1="60" y1="62" x2="71" y2="62" stroke="white" strokeWidth="1"/>
|
||||||
|
<line x1="60" y1="66" x2="71" y2="66" stroke="white" strokeWidth="1"/>
|
||||||
|
<line x1="60" y1="70" x2="71" y2="70" stroke="white" strokeWidth="1"/>
|
||||||
|
<line x1="60" y1="74" x2="71" y2="74" stroke="white" strokeWidth="1"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Металлический обод */}
|
||||||
|
<circle cx="35" cy="35" r="28" fill="url(#rimGrad)" filter="url(#shadow)" />
|
||||||
|
<circle cx="35" cy="35" r="26" fill="none" stroke="#ecf0f1" strokeWidth="1" opacity="0.6" />
|
||||||
|
<circle cx="35" cy="35" r="28" fill="none" stroke="#455a64" strokeWidth="1.5" opacity="0.5" />
|
||||||
|
|
||||||
|
{/* Стеклянная линза */}
|
||||||
|
<circle cx="35" cy="35" r="23" fill="url(#glassGrad)" />
|
||||||
|
<circle cx="35" cy="35" r="23" fill="url(#innerGlow)" />
|
||||||
|
|
||||||
|
{/* Иконка внутри линзы */}
|
||||||
|
<image
|
||||||
|
href={keysIcon}
|
||||||
|
x="20"
|
||||||
|
y="20"
|
||||||
|
width="29"
|
||||||
|
height="29"
|
||||||
|
clipPath="url(#lensClip)"
|
||||||
|
opacity="0.6"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Блики на стекле */}
|
||||||
|
<ellipse cx="27" cy="25" rx="9" ry="6"
|
||||||
|
fill="url(#shimmer2)" opacity="0.55" transform="rotate(-20 27 25)" />
|
||||||
|
<ellipse cx="23" cy="29" rx="3" ry="2"
|
||||||
|
fill="white" opacity="0.3" transform="rotate(-20 23 29)" />
|
||||||
|
<path d="M 14 22 Q 20 10 35 8" stroke="white" strokeWidth="1.5"
|
||||||
|
strokeLinecap="round" fill="none" opacity="0.45" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
src/config/constants.js
Normal file
@@ -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" },
|
||||||
|
];
|
||||||
|
|
||||||
54
src/hooks/useAuth.js
Normal file
@@ -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 };
|
||||||
|
}
|
||||||
82
src/pages/LoginPage.jsx
Normal file
@@ -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 (
|
||||||
|
|
||||||
|
<div style={s.page}>
|
||||||
|
<div style={s.bgGlow} />
|
||||||
|
|
||||||
|
{FLOAT_ICONS.map(({ icon, delay, size, ...pos }, i) => (
|
||||||
|
<img key={i} src={icon} alt="" className="fi" style={{ ...pos, animationDelay: delay, width: size || "7rem", height: size || "7rem", filter: "grayscale(100%) " }} />
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div style={s.card} className="card-in">
|
||||||
|
<button style={s.dotsBtn}>⋮</button>
|
||||||
|
|
||||||
|
<div style={s.logoWrap}>
|
||||||
|
<MagnifierLogo />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 style={s.title}>Находка</h1>
|
||||||
|
|
||||||
|
<form onSubmit={(e) => { e.preventDefault(); login(email, password); }} noValidate>
|
||||||
|
<div style={s.inputWrap}>
|
||||||
|
<input
|
||||||
|
type="email" placeholder="Введите email"
|
||||||
|
value={email} onChange={(e) => setEmail(e.target.value)}
|
||||||
|
style={s.input} className="li"
|
||||||
|
autoComplete="off" name="naidishchey-email-x7k"
|
||||||
|
readOnly onFocus={(e) => e.target.removeAttribute("readOnly")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ ...s.inputWrap, position: "relative" }}>
|
||||||
|
<input
|
||||||
|
type={showPass ? "text" : "password"} placeholder="Введите пароль"
|
||||||
|
value={password} onChange={(e) => setPassword(e.target.value)}
|
||||||
|
style={{ ...s.input, paddingRight: "44px" }} className="li"
|
||||||
|
autoComplete="off" name="naidishchey-pass-x7k"
|
||||||
|
readOnly onFocus={(e) => e.target.removeAttribute("readOnly")}
|
||||||
|
/>
|
||||||
|
<button type="button" style={s.eyeBtn} onClick={() => setShowPass(v => !v)}>
|
||||||
|
{showPass ? "🙈" : "👁"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <p style={s.errMsg}>{error}</p>}
|
||||||
|
|
||||||
|
<button type="submit" style={s.btnLogin} className="btn-l" disabled={loading}>
|
||||||
|
{loading ? "Входим..." : "Войти"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<button style={s.forgotBtn}>Забыли пароль?</button>
|
||||||
|
|
||||||
|
<p style={s.regRow}>
|
||||||
|
Нет аккаунта? <a href="/register" style={s.link}>Зарегистрироваться</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style={s.divider} className="div-line">
|
||||||
|
<span style={s.divText}>или</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={s.socials}>
|
||||||
|
<button onClick={loginGoogle} style={s.socBtn} className="soc">Google</button>
|
||||||
|
<button onClick={loginYandex} style={{ ...s.socBtn, color: "#ffcc00" }} className="soc">Яндекс</button>
|
||||||
|
<button onClick={loginVK} style={{ ...s.socBtn, color: "#5b9bd5" }} className="soc">ВК</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
54
src/styles/global.css
Normal file
@@ -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);
|
||||||
|
}
|
||||||
88
src/styles/loginStyles.js
Normal file
@@ -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",
|
||||||
|
},
|
||||||
|
};
|
||||||