Last active
June 14, 2025 13:31
-
-
Save sunmeat/4b08089091e360e6f3c5a81a34c77a64 to your computer and use it in GitHub Desktop.
форма регистрации и логина на редаксе
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
App.jsx: | |
import {useSelector, useDispatch, Provider} from 'react-redux'; | |
import {configureStore, createSlice, createAsyncThunk} from '@reduxjs/toolkit'; | |
import {useState} from 'react'; | |
import './App.css'; | |
// создание асинхронного thunk для имитации логина | |
export const loginUser = createAsyncThunk( | |
'auth/loginUser', // имя действия | |
async ({username, password}, {rejectWithValue, getState}) => { | |
// имитация запроса к API | |
await new Promise(resolve => setTimeout(resolve, 1000)); | |
const {registeredUsers} = getState().auth; | |
// предопределённые учётки | |
if (username === 'sunmeat' && password === '1111') { | |
return {username}; | |
} | |
if (username === 'admin' && password === 'admin') { | |
return {username}; | |
} | |
if (username === 'guest' && (!password || password === '')) { | |
return {username}; | |
} | |
// проверка зарегистрированных пользователей | |
const user = registeredUsers.find(u => u.username === username && u.password === password); | |
if (user) { | |
return {username}; | |
} | |
return rejectWithValue('Неверное имя пользователя или пароль'); | |
} | |
); | |
// создание асинхронного thunk для имитации регистрации | |
export const registerUser = createAsyncThunk( | |
'auth/registerUser', // имя действия | |
async ({username, password}, {rejectWithValue, getState}) => { | |
// имитация запроса к API | |
await new Promise(resolve => setTimeout(resolve, 1000)); | |
const {registeredUsers} = getState().auth; | |
// проверка, не занят ли username | |
if (registeredUsers.some(u => u.username === username) || | |
['sunmeat', 'admin', 'guest'].includes(username)) { | |
return rejectWithValue('Имя пользователя уже занято'); | |
} | |
if (!username || !password) { | |
return rejectWithValue('Заполните все поля'); | |
} | |
return {username, password}; | |
} | |
); | |
// создание среза (slice) для управления аутентификацией | |
const authSlice = createSlice({ | |
name: 'auth', // имя среза | |
initialState: { // начальное состояние хранилища | |
user: null, // текущий пользователь | |
status: 'idle', // статус запроса (idle, loading, succeeded, failed) | |
error: null, // сообщение об ошибке | |
registeredUsers: [], // коллекция зарегистрированных пользователей | |
}, | |
reducers: { // редьюсеры для обработки синхронных действий | |
logout: (state) => { // действие для выхода из системы | |
state.user = null; | |
state.status = 'idle'; | |
state.error = null; | |
}, | |
clearError: (state) => { // действие для очистки ошибки | |
state.error = null; | |
}, | |
}, | |
// обработка асинхронных действий, созданных с помощью createAsyncThunk | |
// для логина и регистрации. использован паттерн builder для обработки трёх состояний | |
// каждого действия (pending, fulfilled, rejected), обновляя состояние хранилища (state) | |
// соответственно. например, при успешном логине (fulfilled) устанавливается статус | |
// succeeded и сохраняется имя пользователя, а при ошибке (rejected) — статус failed | |
// и сообщение об ошибке | |
extraReducers: (builder) => { // обработка асинхронных действий | |
builder | |
.addCase(loginUser.pending, (state) => { // обработка состояния ожидания логина | |
state.status = 'loading'; | |
state.error = null; | |
}) | |
.addCase(loginUser.fulfilled, (state, action) => { // обработка успешного логина | |
state.status = 'succeeded'; | |
state.user = action.payload.username; | |
}) | |
.addCase(loginUser.rejected, (state, action) => { // обработка ошибки логина | |
state.status = 'failed'; | |
state.error = action.payload; | |
}) | |
.addCase(registerUser.pending, (state) => { // обработка состояния ожидания регистрации | |
state.status = 'loading'; | |
state.error = null; | |
}) | |
.addCase(registerUser.fulfilled, (state, action) => { // обработка успешной регистрации | |
state.status = 'succeeded'; | |
state.user = action.payload.username; | |
state.registeredUsers.push({username: action.payload.username, password: action.payload.password}); | |
}) | |
.addCase(registerUser.rejected, (state, action) => { // обработка ошибки регистрации | |
state.status = 'failed'; | |
state.error = action.payload; | |
}); | |
}, | |
}); | |
// извлечение действий для упрощения их использования | |
const {logout} = authSlice.actions; | |
// настройка хранилища redux | |
const store = configureStore({ | |
reducer: { | |
auth: authSlice.reducer, // редьюсер для аутентификации | |
}, | |
}); | |
// компонент формы логина | |
function LoginForm({toggleForm}) { | |
const [username, setUsername] = useState(''); | |
const [password, setPassword] = useState(''); | |
const dispatch = useDispatch(); // хук для отправки действий в redux | |
const status = useSelector((state) => state.auth.status); // хук для получения статуса | |
const error = useSelector((state) => state.auth.error); // хук для получения ошибки | |
const handleLogin = () => { | |
dispatch(loginUser({username, password})); // отправка действия логина | |
setUsername(''); | |
setPassword(''); | |
}; | |
return ( | |
<div className="auth-form"> | |
<h2 className="auth-title">Вход</h2> | |
<input | |
className="auth-input" | |
type="text" | |
value={username} | |
onChange={(e) => setUsername(e.target.value)} | |
placeholder="Имя пользователя" | |
onKeyPress={(e) => e.key === 'Enter' && handleLogin()} | |
/> | |
<input | |
className="auth-input" | |
type="password" | |
value={password} | |
onChange={(e) => setPassword(e.target.value)} | |
placeholder="Пароль" | |
onKeyPress={(e) => e.key === 'Enter' && handleLogin()} | |
/> | |
<button | |
className="auth-button" | |
onClick={handleLogin} | |
disabled={status === 'loading'} | |
> | |
{status === 'loading' ? 'Загрузка...' : 'Войти'} | |
</button> | |
{error && ( | |
<p className="auth-error">{error}</p> | |
)} | |
<button | |
className="auth-toggle-button" | |
onClick={toggleForm} // переключение на форму регистрации | |
> | |
Нет аккаунта? Зарегистрируйтесь | |
</button> | |
</div> | |
); | |
} | |
// компонент формы регистрации | |
function RegisterForm({toggleForm}) { | |
const [username, setUsername] = useState(''); | |
const [password, setPassword] = useState(''); | |
const dispatch = useDispatch(); // хук для отправки действий в redux | |
const status = useSelector((state) => state.auth.status); // хук для получения статуса | |
const error = useSelector((state) => state.auth.error); // хук для получения ошибки | |
const handleRegister = () => { | |
dispatch(registerUser({username, password})); // отправка действия регистрации | |
setUsername(''); | |
setPassword(''); | |
}; | |
return ( | |
<div className="auth-form"> | |
<h2 className="auth-title">Регистрация</h2> | |
<input | |
className="auth-input" | |
type="text" | |
value={username} | |
onChange={(e) => setUsername(e.target.value)} | |
placeholder="Имя пользователя" | |
onKeyPress={(e) => e.key === 'Enter' && handleRegister()} | |
/> | |
<input | |
className="auth-input" | |
type="password" | |
value={password} | |
onChange={(e) => setPassword(e.target.value)} | |
placeholder="Пароль" | |
onKeyPress={(e) => e.key === 'Enter' && handleRegister()} | |
/> | |
<button | |
className="auth-button" | |
onClick={handleRegister} | |
disabled={status === 'loading'} | |
> | |
{status === 'loading' ? 'Загрузка...' : 'Зарегистрироваться'} | |
</button> | |
{error && ( | |
<p className="auth-error">{error}</p> | |
)} | |
<button | |
className="auth-toggle-button" | |
onClick={toggleForm} // переключение на форму логина | |
> | |
Уже есть аккаунт? Войдите | |
</button> | |
</div> | |
); | |
} | |
// компонент приветствия | |
function WelcomeScreen() { | |
const dispatch = useDispatch(); // хук для отправки действий в redux | |
const username = useSelector((state) => state.auth.user); // хук для получения имени пользователя | |
return ( | |
<div className="welcome-screen"> | |
<h2 className="welcome-title">Добро пожаловать на сайт, {username}!</h2> | |
<button | |
className="auth-button" | |
onClick={() => dispatch(logout())} // отправка действия выхода из системы | |
> | |
Выйти | |
</button> | |
</div> | |
); | |
} | |
// компонент аутентификации | |
function AuthApp() { | |
const [isLogin, setIsLogin] = useState(true); | |
const user = useSelector((state) => state.auth.user); // хук для проверки авторизации | |
const toggleForm = () => { | |
setIsLogin(!isLogin); | |
}; | |
return ( | |
<div className="auth-app"> | |
{user ? ( | |
<WelcomeScreen/> | |
) : isLogin ? ( | |
<LoginForm toggleForm={toggleForm}/> | |
) : ( | |
<RegisterForm toggleForm={toggleForm}/> | |
)} | |
</div> | |
); | |
} | |
function App() { | |
return ( | |
<Provider store={store}> | |
<AuthApp/> | |
</Provider> | |
); | |
} | |
export default App; | |
====================================================================================================================== | |
App.css: | |
@import url('https://fonts.googleapis.com/css2?family=Segoe+UI:wght@400;500;600;700&display=swap'); | |
:root { | |
--bg-gradient-start: #1c2526; | |
--bg-gradient-end: #2c3e50; | |
--text-color: #00ffcc; | |
--app-bg-start: rgba(20, 30, 40, 0.85); | |
--app-bg-end: rgba(30, 40, 50, 0.85); | |
--input-bg: rgba(0, 150, 136, 0.3); | |
--input-focus-bg: rgba(0, 170, 150, 0.4); | |
--placeholder-color: #26a69a; | |
--button-bg: linear-gradient(90deg, #00ffcc, #26a69a); | |
--button-hover: linear-gradient(90deg, #00e6b8, #219187); | |
--error-color: #ff4081; | |
--border-glow: rgba(0, 255, 204, 0.5); | |
--shadow-color: rgba(0, 0, 0, 0.4); | |
} | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
} | |
body { | |
background: linear-gradient(135deg, var(--bg-gradient-start), var(--bg-gradient-end)); | |
color: var(--text-color); | |
font-family: 'Segoe UI', sans-serif; | |
min-height: 100vh; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
padding: 20px; | |
} | |
.auth-app { | |
max-width: 400px; | |
width: 100%; | |
background: linear-gradient(145deg, var(--app-bg-start), var(--app-bg-end)); | |
backdrop-filter: blur(15px); | |
padding: 30px; | |
border-radius: 12px; | |
box-shadow: 0 10px 40px var(--shadow-color); | |
border: 1px solid var(--border-glow); | |
animation: fadeIn 0.5s ease-in-out; | |
} | |
@keyframes fadeIn { | |
from { | |
opacity: 0; | |
transform: translateY(-20px); | |
} | |
to { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
.auth-form, | |
.welcome-screen { | |
display: flex; | |
flex-direction: column; | |
gap: 20px; | |
} | |
.auth-title, | |
.welcome-title { | |
text-align: center; | |
color: var(--text-color); | |
font-size: 2em; | |
font-weight: 600; | |
margin-bottom: 10px; | |
text-shadow: 0 2px 6px var(--shadow-color); | |
} | |
.auth-input { | |
padding: 12px 16px; | |
border: none; | |
border-radius: 6px; | |
background: var(--input-bg); | |
color: var(--text-color); | |
font-size: 1.1em; | |
transition: all 0.3s ease; | |
box-shadow: inset 0 2px 4px var(--shadow-color); | |
} | |
.auth-input::placeholder { | |
color: var(--placeholder-color); | |
} | |
.auth-input:focus { | |
outline: none; | |
background: var(--input-focus-bg); | |
box-shadow: 0 0 0 3px var(--border-glow); | |
} | |
.auth-button { | |
padding: 12px 24px; | |
background: var(--button-bg); | |
color: #ffffff; | |
border: none; | |
border-radius: 6px; | |
cursor: pointer; | |
font-size: 1.1em; | |
font-weight: 500; | |
transition: all 0.3s ease; | |
box-shadow: 0 4px 12px var(--shadow-color); | |
} | |
.auth-button:hover { | |
background: var(--button-hover); | |
transform: translateY(-2px); | |
box-shadow: 0 6px 16px var(--shadow-color); | |
} | |
.auth-button:active { | |
transform: scale(0.95); | |
} | |
.auth-button:disabled { | |
background: linear-gradient(90deg, #ff4081, #f06292); | |
cursor: not-allowed; | |
transform: none; | |
box-shadow: none; | |
} | |
.auth-error { | |
color: var(--error-color); | |
font-size: 0.95em; | |
text-align: center; | |
margin: 10px 0; | |
} | |
.auth-toggle-button { | |
background: none; | |
border: none; | |
color: var(--text-color); | |
font-size: 0.95em; | |
text-decoration: underline; | |
cursor: pointer; | |
transition: all 0.3s ease; | |
text-align: center; | |
margin-top: 10px; | |
} | |
.auth-toggle-button:hover { | |
color: var(--border-glow); | |
} | |
@media (max-width: 500px) { | |
.auth-app { | |
padding: 20px; | |
} | |
.auth-button { | |
width: 100%; | |
padding: 12px; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment