Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/app/api/user/user.api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Agent from '@api/agent.api';
import ChangePasswordDto from '@models/user/user.model';

import { API_ROUTES } from '@/app/common/constants/api-routes.constants';
import {
Expand Down Expand Up @@ -36,9 +37,13 @@ const UserApi = {
API_ROUTES.ADMIN_AUTHORIZATION.REFRESH_TOKEN,
token,
),

adminLogout: () =>
Agent.post<void>(
API_ROUTES.ADMIN_AUTHORIZATION.LOGOUT, {},
),

changePassword: (data: ChangePasswordDto) =>
Agent.post<void>(API_ROUTES.ADMIN_AUTHORIZATION.CHANGE_PASSWORD, data),
};
export default UserApi;
export default UserApi;
1 change: 1 addition & 0 deletions src/app/common/constants/api-routes.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export const API_ROUTES = {
GOOGLE_LOGIN: "auth/google-login",
REFRESH_TOKEN: "auth/refresh-token",
LOGOUT: "auth/logout",
CHANGE_PASSWORD: "auth/change-password",
},
COMMENTS: {
GET_BY_STREETCODE_ID: "comment/getByStreetcodeId",
Expand Down
2 changes: 2 additions & 0 deletions src/app/common/constants/frontend-routes.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const FRONTEND_ROUTES = {
EDITOR: '/admin-panel/editor',
CALENDAR: '/admin-panel/calendar',
VACANCIES: '/admin-panel/vacancies',
SETTINGS: '/admin-panel/settings',
},
ADMIN_RELATIVE: {
PARTNERS: 'partners',
Expand All @@ -26,6 +27,7 @@ const FRONTEND_ROUTES = {
CALENDAR: 'calendar',
NEWS: 'news',
VACANCIES: 'vacancies',
SETTINGS: 'settings',
},
OTHER_PAGES: {
CATALOG: '/catalog',
Expand Down
2 changes: 2 additions & 0 deletions src/app/router/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Partners from "@/features/AdminPage/PartnersPage/Partners.component";
import Streetcodes from "@/features/AdminPage/StreetcodesPage/Streetcodes.component";
import TeamPage from "@/features/AdminPage/TeamPage/TeamPage.component";
import VacanciesPage from "@/features/AdminPage/VacanciesPage/VacanciesPage.component";
import SettingsPage from "@/features/AdminPage/SettingsPage/SettingsPage.component";
import StreetcodeCatalog from "@/features/StreetcodeCatalogPage/StreetcodeCatalog.component";

const router = createBrowserRouter(
Expand All @@ -38,6 +39,7 @@ const router = createBrowserRouter(
<Route path="calendar" element={<CalendarPage />} />
<Route path="news" element={<AdminNewsPage />} />
<Route path="vacancies" element={<VacanciesPage />} />
<Route path="settings" element={<SettingsPage />} />

<Route path={FRONTEND_ROUTES.ADMIN.NEW_STREETCODE} element={<EditPge />} />
</Route>
Expand Down
27 changes: 11 additions & 16 deletions src/app/stores/user-login-store.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import { makeAutoObservable } from "mobx";
import UserApi from "@api/user/user.api";

import { ChangePasswordDto } from "@models/user/user.model";
import { RefreshTokenResponce, UserLoginResponce } from "@/models/user/user.model";

export default class UserLoginStore {
private timeoutHandler: ReturnType<typeof setTimeout> | null = null;

private static readonly tokenStorageName = "token";

private static readonly dateStorageName = "expireAt";

private static readonly refreshTokenStorageName = "refreshToken";

private static readonly userIdStorageName = "userId";

public userLoginResponce?: UserLoginResponce;

private callback?: () => void;

public constructor() {
Expand All @@ -38,10 +33,6 @@ export default class UserLoginStore {
return localStorage.setItem(UserLoginStore.tokenStorageName, newToken);
}

private static clearToken() {
localStorage.removeItem(UserLoginStore.tokenStorageName);
}

private static getRefreshToken() {
return localStorage.getItem(UserLoginStore.refreshTokenStorageName);
}
Expand All @@ -50,10 +41,6 @@ export default class UserLoginStore {
localStorage.setItem(UserLoginStore.refreshTokenStorageName, refreshToken);
}

private static clearRefreshToken() {
localStorage.removeItem(UserLoginStore.refreshTokenStorageName);
}

public setCallback(func: () => void) {
this.callback = func;
}
Expand All @@ -73,7 +60,6 @@ export default class UserLoginStore {
if (this.timeoutHandler) {
clearTimeout(this.timeoutHandler);
}

UserLoginStore.clearUserData();
}

Expand Down Expand Up @@ -122,4 +108,13 @@ export default class UserLoginStore {
UserLoginStore.setRefreshToken(refreshToken.refreshToken);
return refreshToken;
});
}

public changePassword = async (data: ChangePasswordDto) => {
try {
await UserApi.changePassword(data);
return { success: true };
} catch (error) {
return { success: false, error };
}
};
}
4 changes: 4 additions & 0 deletions src/features/AdminPage/AdminBar.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ const adminNavItems = [
title: 'Словник',
to: FRONTEND_ROUTES.ADMIN.DICTIONARY,
},
{
title: 'Налаштування',
to: FRONTEND_ROUTES.ADMIN.SETTINGS,
},
];

const AdminBar = () => {
Expand Down
149 changes: 149 additions & 0 deletions src/features/AdminPage/SettingsPage/SettingsPage.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { useState } from 'react';
import { Tabs, Form, Input, Button, message } from 'antd';
import useMobx from '@stores/root-store';
import { ChangePasswordDto } from '@models/user/user.model';
import './SettingsPage.styles.scss';

const SettingsPage = () => {
const { userLoginStore } = useMobx();
const [passwordForm] = Form.useForm();
const [profileForm] = Form.useForm();

const [loading, setLoading] = useState(false);
const [isEditing, setIsEditing] = useState(false);

const loginResponse = userLoginStore.userLoginResponce;

const userData = loginResponse?.user;

const initialValues = {
username: userData?.name ? `${userData.name} ${userData.surname}` : 'Адміністратор',
email: userData?.email || 'admin@streetcode.com',
};

const handlePasswordChange = async (data: ChangePasswordDto) => {
setLoading(true);

try {
const result = await userLoginStore.changePassword(data);

if (!result.success) {
message.error('Помилка зміни пароля: перевірте поточний пароль');
return;
}

message.success('Пароль успішно змінено');
passwordForm.resetFields();
} catch (error) {
console.error('Password change error:', error);
message.error('Сталася помилка');
} finally {
setLoading(false);
}
};

const onFinishProfile = (values: any) => {
console.log('Profile update:', values);
message.success('Дані профілю оновлено');
setIsEditing(false);
};

const items = [
{
key: '1',
label: 'Профіль',
children: (
<Form
form={profileForm}
layout="vertical"
onFinish={onFinishProfile}
className="settings-form"
initialValues={initialValues}
disabled={!isEditing}
>
<Form.Item label="Ім'я" name="username">
<Input />
</Form.Item>
<Form.Item label="Email" name="email">
<Input disabled />
</Form.Item>

{!isEditing ? (

Check warning on line 71 in src/features/AdminPage/SettingsPage/SettingsPage.component.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=project-studying-dotnet_Streetcode-Client-May2026&issues=AZ7T6h6RHSAYtUfIjoCh&open=AZ7T6h6RHSAYtUfIjoCh&pullRequest=25
<Button type="primary" onClick={() => setIsEditing(true)}>
Редагувати
</Button>
) : (
<div style={{ display: 'flex', gap: '10px' }}>
<Button type="primary" htmlType="submit">Зберегти</Button>
<Button onClick={() => { profileForm.resetFields(); setIsEditing(false); }}>
Скасувати
</Button>
</div>
)}
</Form>
),
},
{
key: '2',
label: 'Безпека',
children: (
<Form
form={passwordForm}
layout="vertical"
onFinish={handlePasswordChange}
className="settings-form"
>
<Form.Item
label="Старий пароль"
name="currentPassword"
rules={[{ required: true, message: 'Введіть поточний пароль' }]}
>
<Input.Password />
</Form.Item>

<Form.Item
label="Новий пароль"
name="newPassword"
rules={[{ required: true, min: 6, message: 'Мінімум 6 символів' }]}
>
<Input.Password />
</Form.Item>

<Form.Item
label="Підтвердження пароля"
name="confirmNewPassword"
dependencies={['newPassword']}
rules={[
{ required: true, message: 'Підтвердьте пароль' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('newPassword') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('Паролі не співпадають!'));
},
}),
]}
>
<Input.Password />
</Form.Item>

<Button type="primary" danger htmlType="submit" loading={loading}>
Змінити пароль
</Button>
</Form>
),
},
];

return (
<div className="settings-page">
<div className="settings-page-header">
<h2>Налаштування</h2>
</div>
<Tabs defaultActiveKey="1" items={items} className="settings-tabs" />
</div>
);
};

export default SettingsPage;
30 changes: 30 additions & 0 deletions src/features/AdminPage/SettingsPage/SettingsPage.styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
@use "@sass/variables/_variables.colors.scss" as c;

.settings-page {
width: 100%;
padding: 24px;
background: #fff;
border-radius: 8px;

.settings-page-header {
margin-bottom: 24px;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 16px;
}

.settings-form {
max-width: 400px;
margin-top: 20px;
}

.settings-tabs {
.ant-tabs-tab-active {
.ant-tabs-tab-btn {
color: c.$dark-red-color;
}
}
.ant-tabs-ink-bar {
background: c.$dark-red-color;
}
}
}
6 changes: 6 additions & 0 deletions src/models/user/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ export enum UserRole {
Administrator,
Moderator,
}

export interface ChangePasswordDto {
currentPassword: string;
newPassword: string;
confirmNewPassword: string;
}