Skip to content

Commit

Permalink
feat: add settings page
Browse files Browse the repository at this point in the history
  • Loading branch information
sashtje committed Oct 13, 2023
1 parent e691114 commit 5e4ecb2
Show file tree
Hide file tree
Showing 27 changed files with 247 additions and 7 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,5 +188,6 @@ Feature state can take only one of two values: **on** or **off**.
- [notificationButton](/src/features/notificationButton)
- [ThemeSwitcher](/src/features/ThemeSwitcher)
- [UI](/src/features/UI)
- [uiDesignSwitcher](/src/features/uiDesignSwitcher)

----
6 changes: 6 additions & 0 deletions public/locales/en/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"Настройки пользователя": "User settings",
"Новый": "New",
"Старый": "Old",
"Вариант интерфейса": "Interface option"
}
3 changes: 2 additions & 1 deletion public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@
"У Вас нет доступа к этой странице": "You do not have access to this page",
"Ваш отзыв": "Your feedback",
"Отмена": "Cancel",
"Спасибо за оценку!": "Thank you for rating!"
"Спасибо за оценку!": "Thank you for rating!",
"Настройки": "Settings"
}
6 changes: 6 additions & 0 deletions public/locales/ru/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"Настройки пользователя": "Настройки пользователя",
"Новый": "Новый",
"Старый": "Старый",
"Вариант интерфейса": "Вариант интерфейса"
}
3 changes: 2 additions & 1 deletion public/locales/ru/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@
"У Вас нет доступа к этой странице": "У Вас нет доступа к этой странице",
"Ваш отзыв": "Ваш отзыв",
"Отмена": "Отмена",
"Спасибо за оценку!": "Спасибо за оценку!"
"Спасибо за оценку!": "Спасибо за оценку!",
"Настройки": "Настройки"
}
6 changes: 6 additions & 0 deletions src/app/providers/router/config/routerConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import {
getRouteMain,
getRouteNotFound,
getRouteProfile,
getRouteSettings,
} from '@/shared/const/router';
import { SettingsPage } from '@/pages/SettingsPage/ui/SettingsPage';

import { AppRoutesProps } from './types';

Expand Down Expand Up @@ -64,6 +66,10 @@ export const routerConfig: Record<AppRoutes, AppRoutesProps> = {
authOnly: true,
roles: [UserRole.ADMIN, UserRole.MANAGER],
},
[AppRoutes.SETTINGS]: {
path: getRouteSettings(),
element: <SettingsPage />,
},
[AppRoutes.FORBIDDEN]: {
path: getRouteForbidden(),
element: <ForbiddenPage />,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Avatar } from '@/shared/ui/redesigned/Avatar';
import { Dropdown as DropdownDeprecated } from '@/shared/ui/deprecated/Popups';
import { Dropdown } from '@/shared/ui/redesigned/Popups';
import { getUserAuthData, isUserAdmin, isUserManager, userActions } from '@/entities/User';
import { getRouteAdminPanel, getRouteProfile } from '@/shared/const/router';
import { getRouteAdminPanel, getRouteProfile, getRouteSettings } from '@/shared/const/router';
import { ToggleFeatures } from '@/shared/lib/features';

interface AvatarDropdownProps {
Expand Down Expand Up @@ -43,6 +43,10 @@ export const AvatarDropdown = memo((props: AvatarDropdownProps) => {
},
]
: []),
{
content: t('Настройки'),
href: getRouteSettings(),
},
{
content: t('Профиль'),
href: getRouteProfile(authData.id),
Expand Down
1 change: 1 addition & 0 deletions src/features/uiDesignSwitcher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
## Feature for switching old and new designs
1 change: 1 addition & 0 deletions src/features/uiDesignSwitcher/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { UiDesignSwitcher } from './ui/UiDesignSwitcher';
16 changes: 16 additions & 0 deletions src/features/uiDesignSwitcher/ui/UiDesignSwitcher.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react';

import { UiDesignSwitcher } from './UiDesignSwitcher';

export default {
title: 'shared/UiDesignSwitcher',
component: UiDesignSwitcher,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof UiDesignSwitcher>;

const Template: ComponentStory<typeof UiDesignSwitcher> = (args) => <UiDesignSwitcher {...args} />;

export const Normal = Template.bind({});
Normal.args = {};
77 changes: 77 additions & 0 deletions src/features/uiDesignSwitcher/ui/UiDesignSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { memo, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';

import { ListBox } from '@/shared/ui/redesigned/Popups';
import { getFeatureFlag, updateFeatureFlags } from '@/shared/lib/features';
import { useAppDispatch } from '@/shared/lib/hooks/useAppDispatch/useAppDispatch';
import { getUserAuthData } from '@/entities/User';
import { HStack } from '@/shared/ui/redesigned/Stack';
import { Text } from '@/shared/ui/redesigned/Text';
import { Skeleton } from '@/shared/ui/redesigned/Skeleton';

interface UiDesignSwitcherProps {
className?: string;
}

export const UiDesignSwitcher = memo((props: UiDesignSwitcherProps) => {
const { className } = props;
const { t } = useTranslation('settings');
const dispatch = useAppDispatch();
const authData = useSelector(getUserAuthData);
const [isLoading, setIsLoading] = useState(false);

const isAppRedesigned = getFeatureFlag('isAppRedesigned');
const items = useMemo(
() => [
{
content: t('Новый'),
value: 'new',
},
{
content: t('Старый'),
value: 'old',
},
],
[t],
);

const onDesignChange = useCallback(
async (value: string) => {
if (authData?.id) {
setIsLoading(true);

await dispatch(
updateFeatureFlags({
userId: authData.id,
newFeatures: {
isAppRedesigned: value === 'new',
},
}),
).unwrap();

setIsLoading(false);
}
},
[authData?.id, dispatch],
);

return (
<HStack gap="8" max>
<Text text={t('Вариант интерфейса')} />

{isLoading ? (
<Skeleton width={100} height={40} />
) : (
<ListBox
onChange={onDesignChange}
value={isAppRedesigned ? 'new' : 'old'}
items={items}
className={className}
/>
)}
</HStack>
);
});

UiDesignSwitcher.displayName = 'UiDesignSwitcher';
1 change: 1 addition & 0 deletions src/pages/SettingsPage/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SettingsPageAsync as SettingsPage } from './ui/SettingsPage.async';
5 changes: 5 additions & 0 deletions src/pages/SettingsPage/ui/SettingsPage.async.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { lazy } from 'react';

export const SettingsPageAsync = lazy(() =>
import('./SettingsPage').then((module) => ({ default: module.SettingsPage })),
);
16 changes: 16 additions & 0 deletions src/pages/SettingsPage/ui/SettingsPage.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react';

import { SettingsPage } from './SettingsPage';

export default {
title: 'pages/SettingsPage',
component: SettingsPage,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof SettingsPage>;

const Template: ComponentStory<typeof SettingsPage> = (args) => <SettingsPage {...args} />;

export const Normal = Template.bind({});
Normal.args = {};
29 changes: 29 additions & 0 deletions src/pages/SettingsPage/ui/SettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

import { Text } from '@/shared/ui/redesigned/Text';
import { Page } from '@/widgets/Page';
import { VStack } from '@/shared/ui/redesigned/Stack';
import { UiDesignSwitcher } from '@/features/uiDesignSwitcher';

interface SettingsPageProps {
className?: string;
}

export const SettingsPage = memo((props: SettingsPageProps) => {
const { className } = props;

const { t } = useTranslation('settings');

return (
<Page className={className}>
<VStack gap="16" max>
<Text title={t('Настройки пользователя')} />

<UiDesignSwitcher />
</VStack>
</Page>
);
});

SettingsPage.displayName = 'SettingsPage';
2 changes: 2 additions & 0 deletions src/shared/const/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export enum AppRoutes {
ARTICLE_CREATE = 'article_create',
ARTICLE_EDIT = 'article_edit',
ADMIN_PANEL = 'admin_panel',
SETTINGS = 'settings',
FORBIDDEN = 'forbidden',

// last
Expand All @@ -21,5 +22,6 @@ export const getRouteArticlesDetails = (id: string) => `/articles/${id}`;
export const getRouteArticleCreate = () => '/articles/create';
export const getRouteArticleEdit = (id: string) => `/articles/${id}/edit`;
export const getRouteAdminPanel = () => '/admin_panel';
export const getRouteSettings = () => '/settings';
export const getRouteForbidden = () => '/forbidden';
export const getRouteNotFound = () => '*';
23 changes: 23 additions & 0 deletions src/shared/lib/features/api/featureFlagsApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { rtkApi } from '@/shared/api/rtkApi';
import { FeatureFlags } from '@/shared/types/featureFlags';

interface UpdateFeatureFlagsArg {
userId: string;
features: Partial<FeatureFlags>;
}

const featureFlagsApi = rtkApi.injectEndpoints({
endpoints: (build) => ({
updateFeatureFlags: build.mutation<void, UpdateFeatureFlagsArg>({
query: ({ userId, features }) => ({
url: `/users/${userId}`,
method: 'PATCH',
body: {
features,
},
}),
}),
}),
});

export const updateFeatureFlagsMutation = featureFlagsApi.endpoints.updateFeatureFlags.initiate;
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ReactElement } from 'react';

import { FeatureFlags } from '@/shared/types/featureFlags';

import { getFeatureFlag } from '../setGetFeatures';
import { getFeatureFlag } from '../../lib/setGetFeatures';

interface ToggleFeaturesProps {
feature: keyof FeatureFlags;
Expand Down
7 changes: 4 additions & 3 deletions src/shared/lib/features/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { getFeatureFlag, setFeatureFlags } from './setGetFeatures';
export { toggleFeatures } from './toggleFeatures';
export { ToggleFeatures } from './ToggleFeatures/ToggleFeatures';
export { getFeatureFlag, setFeatureFlags } from './lib/setGetFeatures';
export { toggleFeatures } from './lib/toggleFeatures';
export { ToggleFeatures } from './components/ToggleFeatures/ToggleFeatures';
export { updateFeatureFlags } from './services/updateFeatureFlags';
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ export function setFeatureFlags(newFeatureFlags?: FeatureFlags) {
export function getFeatureFlag(flag: keyof FeatureFlags) {
return featureFlags[flag];
}

export function getAllFeatureFlags() {
return featureFlags;
}
File renamed without changes.
38 changes: 38 additions & 0 deletions src/shared/lib/features/services/updateFeatureFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createAsyncThunk } from '@reduxjs/toolkit';

import { ThunkConfig } from '@/app/providers/StoreProvider';
import { updateFeatureFlagsMutation } from '@/shared/lib/features/api/featureFlagsApi';
import { FeatureFlags } from '@/shared/types/featureFlags';
import { getAllFeatureFlags } from '@/shared/lib/features/lib/setGetFeatures';

interface UpdateFeatureFlagsArg {
userId: string;
newFeatures: Partial<FeatureFlags>;
}

export const updateFeatureFlags = createAsyncThunk<
void,
UpdateFeatureFlagsArg,
ThunkConfig<string>
>('features/updateFeatureFlags', async ({ userId, newFeatures }, thunkAPI) => {
const { rejectWithValue, dispatch } = thunkAPI;

try {
await dispatch(
updateFeatureFlagsMutation({
userId,
features: {
...getAllFeatureFlags(),
...newFeatures,
},
}),
);

window.location.reload();

return undefined;
} catch (e) {
console.log(e);
return rejectWithValue('');
}
});
1 change: 1 addition & 0 deletions src/widgets/Page/ui/Page.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

.pageRedesigned {
overflow-y: auto;
height: 100%;
}

.trigger {
Expand Down

0 comments on commit 5e4ecb2

Please sign in to comment.