Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Last active June 14, 2025 13:31
Show Gist options
  • Save sunmeat/4b08089091e360e6f3c5a81a34c77a64 to your computer and use it in GitHub Desktop.
Save sunmeat/4b08089091e360e6f3c5a81a34c77a64 to your computer and use it in GitHub Desktop.
форма регистрации и логина на редаксе
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