Skip to content

Commit

Permalink
feat(UserSettingPage): settings page
Browse files Browse the repository at this point in the history
  • Loading branch information
Katrin-kudryash committed Nov 3, 2023
1 parent c31e3fb commit 62b6f04
Show file tree
Hide file tree
Showing 28 changed files with 275 additions and 20 deletions.
14 changes: 14 additions & 0 deletions prisma/migrations/20231102125908_user_settings/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "UserSettings" (
"id" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
"theme" TEXT NOT NULL DEFAULT 'system',

CONSTRAINT "UserSettings_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "UserSettings_userId_key" ON "UserSettings"("userId");

-- AddForeignKey
ALTER TABLE "UserSettings" ADD CONSTRAINT "UserSettings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
10 changes: 10 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ model User {
hiringLeadInHireStreams HireStream[] @relation("HireStreamToHiringLead")
recruiterInHireStreams HireStream[] @relation("HireStreamToRecruiter")
interviewerInSectionTypes SectionType[] @relation("SectionTypeToInterviewer")
settings UserSettings?
@@index([email])
}
Expand Down Expand Up @@ -405,3 +406,12 @@ model Attach {
section Section @relation(fields: [sectionId], references: [id])
sectionId Int
}

model UserSettings {
id String @id @default(nanoid())
user User @relation(fields: [userId], references: [id])
userId Int @unique
theme String @default("system")
}
22 changes: 20 additions & 2 deletions src/backend/modules/user/user-db-service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { User, Prisma } from '@prisma/client';
import { User, Prisma, UserSettings } from '@prisma/client';

import { prisma } from '../..';
import { ErrorWithStatus } from '../../../utils';
import { UserRoles, UserRolesInfo } from '../../user-roles';
import { idObjsToIds } from '../../utils';
import { sectionTypeDbService } from '../section-type/section-type-db-service';

import { AddProblemToFavorites, CreateUser, GetUserList } from './user-types';
import { AddProblemToFavorites, CreateUser, EditUserSettings, GetUserList } from './user-types';
import { tr } from './user.i18n';

const create = async (data: CreateUser) => {
Expand Down Expand Up @@ -124,6 +124,22 @@ const getUserRoles = async (id: number) => {
return userRoles;
};

const getSettings = async (id: number): Promise<UserSettings> => {
const settings = await prisma.userSettings.upsert({
where: { userId: id },
update: {},
create: { userId: id },
});
return settings;
};

const editSettings = (userId: number, data: EditUserSettings) => {
return prisma.userSettings.update({
where: { userId },
data: { theme: data.theme },
});
};

export const userDbService = {
create,
find,
Expand All @@ -135,4 +151,6 @@ export const userDbService = {
removeProblemFromFavorites,
getUserRoles,
getUserList,
getSettings,
editSettings,
};
8 changes: 8 additions & 0 deletions src/backend/modules/user/user-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { z } from 'zod';

import { themes } from '../../../utils/theme';

export const createUserSchema = z.object({
name: z.string().min(4),
email: z.string().email(),
Expand Down Expand Up @@ -28,3 +30,9 @@ export const getUserListSchema = z.object({
});

export type GetUserList = z.infer<typeof getUserListSchema>;

export const editUserSettingsSchema = z.object({
theme: z.enum(themes).optional(),
});

export type EditUserSettings = z.infer<typeof editUserSettingsSchema>;
21 changes: 19 additions & 2 deletions src/backend/trpc/routers/users-router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { z } from 'zod';

import { accessMiddlewares } from '../../access/access-middlewares';
import { userDbService } from '../../modules/user/user-db-service';
import { addProblemToFavoritesSchema, createUserSchema, getUserListSchema } from '../../modules/user/user-types';
import {
addProblemToFavoritesSchema,
createUserSchema,
editUserSettingsSchema,
getUserListSchema,
} from '../../modules/user/user-types';
import { protectedProcedure, router } from '../trpc-back';

export const usersRouter = router({
Expand All @@ -14,7 +21,9 @@ export const usersRouter = router({
getAll: protectedProcedure.query(() => {
return userDbService.getAll();
}),

getById: protectedProcedure.input(z.number()).query(({ input }) => {
return userDbService.find(input);
}),
getUserList: protectedProcedure.input(getUserListSchema).query(({ input }) => {
return userDbService.getUserList(input);
}),
Expand All @@ -30,4 +39,12 @@ export const usersRouter = router({
removeProblemFromFavorites: protectedProcedure.input(addProblemToFavoritesSchema).mutation(({ input, ctx }) => {
return userDbService.removeProblemFromFavorites(ctx.session.user.id, input);
}),

getSettings: protectedProcedure.query(({ ctx }) => {
return userDbService.getSettings(ctx.session.user.id);
}),

editSettings: protectedProcedure.input(editUserSettingsSchema).mutation(({ input, ctx }) => {
return userDbService.editSettings(ctx.session.user.id, input);
}),
});
3 changes: 2 additions & 1 deletion src/components/FormInput/FormGradeDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { Button, Dropdown, MenuItem } from '@taskany/bricks';
import { Grades } from '@prisma/client';

import { useGradeOptions } from '../../hooks/grades-hooks';
import { tr } from '../components.i18n';

import { tr } from './FormInput.i18n';

type FormGradeDropdownProps = {
text: React.ComponentProps<typeof Dropdown>['text'];
Expand Down
3 changes: 3 additions & 0 deletions src/components/FormInput/FormInput.i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"No grade options in database": ""
}
17 changes: 17 additions & 0 deletions src/components/FormInput/FormInput.i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable */
// Do not edit, use generator to update
import { i18n, fmt, I18nLangSet } from 'easy-typed-intl';
import getLang from '../../../utils/getLang';

import en from './en.json';
import ru from './ru.json';

export type I18nKey = keyof typeof en & keyof typeof ru;
type I18nLang = 'en' | 'ru';

const keyset: I18nLangSet<I18nKey> = {};

keyset['en'] = en;
keyset['ru'] = ru;

export const tr = i18n<I18nLang, I18nKey>(keyset, fmt, getLang);
3 changes: 3 additions & 0 deletions src/components/FormInput/FormInput.i18n/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"No grade options in database": "Нет грейдов в базе данных"
}
13 changes: 13 additions & 0 deletions src/components/PageSep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import styled from 'styled-components';
import { gapM, gray4 } from '@taskany/colors';

interface PageSepProps {
width?: number;
}

export const PageSep = styled.div<PageSepProps>`
border-top: 1px solid ${gray4};
margin: ${gapM} 0;
width: ${({ width }) => (width ? `${width}px` : 'auto')};
`;
1 change: 0 additions & 1 deletion src/components/ReactionBar/ReactionBar.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { CSSProperties, useRef, useState } from 'react';
import { User } from '@prisma/client';
import dynamic from 'next/dynamic';
import { Popup, Text } from '@taskany/bricks';

import { useSession } from '../../contexts/app-settings-context';
Expand Down
23 changes: 23 additions & 0 deletions src/components/Settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { FormCard } from '@taskany/bricks';
import { danger0, gapL, gray4, warn0 } from '@taskany/colors';
import styled from 'styled-components';

type SettingsCardViewType = 'default' | 'warning' | 'danger';

const colorsMap: Record<SettingsCardViewType, string> = {
default: gray4,
warning: warn0,
danger: danger0,
};

export const SettingsContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${gapL};
margin: 0 ${gapL};
max-width: 850px;
`;

export const SettingsCard = styled(FormCard)<{ view?: SettingsCardViewType }>`
border-color: ${({ view = 'default' }) => colorsMap[view]};
`;
3 changes: 1 addition & 2 deletions src/components/components.i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,5 @@
"You are currently offline. Check connection.": "You are currently offline. Check connection.",
"My": "My",
"Getting data error": "Getting data error",
"All": "All",
"No grade options in database": "No grade options in database"
"All": "All"
}
3 changes: 1 addition & 2 deletions src/components/components.i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,5 @@
"You are currently offline. Check connection.": "Вы сейчас не в сети. Проверьте подключение.",
"My": "Мои",
"Getting data error": "Ошибка получения данных",
"All": "Все",
"No grade options in database": "Нет грейдов в базе данных"
"All": "Все"
}
4 changes: 3 additions & 1 deletion src/components/header/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ export const PageHeader: React.FC = () => {
onMouseEnter={() => setPopupVisibility(true)}
onMouseLeave={() => setPopupVisibility(false)}
>
<UserMenu email={session?.user.email} name={session?.user.name} />
<NextLink href={Paths.USERS_SETTINGS}>
<UserMenu email={session?.user.email} name={session?.user.name} />
</NextLink>
</div>
</HeaderMenu>

Expand Down
15 changes: 9 additions & 6 deletions src/components/layout/LayoutMain.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { FC, ReactNode } from 'react';
import Head from 'next/head';
import { nullable } from '@taskany/bricks/utils/nullable';
import styled from 'styled-components';
import { useTheme } from 'next-themes';

import { trpc } from '../../utils/trpc-front';
import { TitleMenu } from '../TitleMenu/TitleMenu';
import { OfflineBanner } from '../OfflineBanner';
import { Theme } from '../Theme';
Expand Down Expand Up @@ -41,9 +42,13 @@ export const LayoutMain: FC<LayoutMainProps> = ({
hidePageHeader,
children,
}) => {
const theme: 'dark' | 'light' = 'dark';
const userSettings = trpc.users.getSettings.useQuery();
const title = pageTitle ? `${pageTitle} - Taskany Hire` : 'Taskany Hire';

const title = `${pageTitle} - Taskany Hire`;
const { resolvedTheme } = useTheme();
const theme = (
userSettings.data?.theme === 'system' ? resolvedTheme || 'dark' : userSettings.data?.theme || 'light'
) as 'dark' | 'light';

return (
<>
Expand All @@ -56,9 +61,7 @@ export const LayoutMain: FC<LayoutMainProps> = ({
<GlobalStyle />

<PageHeader />
{nullable(theme, (t) => (
<Theme theme={t} />
))}
<Theme theme={theme} />
<StyledContent>
{!hidePageHeader && (
<PageTitle title={pageTitle} gutter={headerGutter} backlink={backlink}>
Expand Down
69 changes: 69 additions & 0 deletions src/components/settings/UserSettingsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { User, UserSettings } from 'prisma/prisma-client';
import { useTheme } from 'next-themes';
import { Fieldset, Form, FormRadio, FormRadioInput } from '@taskany/bricks';

import { SettingsCard, SettingsContainer } from '../Settings';
import { Theme, themes } from '../../utils/theme';
import { LayoutMain } from '../layout/LayoutMain';
import { PageSep } from '../PageSep';
import { trpc } from '../../utils/trpc-front';
import { useEditUserSettings } from '../../hooks/user-hooks';

import { tr } from './settings.i18n';

interface UserSettingPageBaseProps {
user: User;
settings: UserSettings;
}

export const UserSettingsPageBase = ({ user, settings }: UserSettingPageBaseProps) => {
const { setTheme } = useTheme();

const editUserSettings = useEditUserSettings();

const onThemeChange = (theme: Theme) => {
editUserSettings.mutateAsync({ theme });
setTheme(theme);
};

return (
<LayoutMain pageTitle={user.name || user.email}>
<PageSep />

<SettingsContainer>
<SettingsCard>
<Form>
<Fieldset title={tr('Appearance')}>
<FormRadio
label={tr('Theme')}
name="theme"
value={settings.theme}
onChange={(v) => onThemeChange(v as Theme)}
>
{themes.map((t) => (
<FormRadioInput key={t} value={t} label={t} />
))}
</FormRadio>
</Fieldset>
</Form>
</SettingsCard>
</SettingsContainer>
</LayoutMain>
);
};

interface UserSettingPageProps {
userId: number;
}

export const UserSettingsPage = ({ userId }: UserSettingPageProps) => {
const userQuery = trpc.users.getById.useQuery(userId);
const user = userQuery.data;

const settingsQuery = trpc.users.getSettings.useQuery();
const settings = settingsQuery.data;

if (!user || !settings) return null;

return <UserSettingsPageBase user={user} settings={settings} />;
};
4 changes: 4 additions & 0 deletions src/components/settings/settings.i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"Appearance": "",
"Theme": ""
}
17 changes: 17 additions & 0 deletions src/components/settings/settings.i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable */
// Do not edit, use generator to update
import { i18n, fmt, I18nLangSet } from 'easy-typed-intl';
import getLang from '../../../utils/getLang';

import en from './en.json';
import ru from './ru.json';

export type I18nKey = keyof typeof en & keyof typeof ru;
type I18nLang = 'en' | 'ru';

const keyset: I18nLangSet<I18nKey> = {};

keyset['en'] = en;
keyset['ru'] = ru;

export const tr = i18n<I18nLang, I18nKey>(keyset, fmt, getLang);
4 changes: 4 additions & 0 deletions src/components/settings/settings.i18n/ru.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"Appearance": "Интерфейс",
"Theme": "Тема"
}
3 changes: 2 additions & 1 deletion src/hooks/hooks.i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@
"User {name} added in hire stream {hireStreamName} as recruiter": "User {name} added in hire stream {hireStreamName} as recruiter",
"User {name} is not recruiter in hire srteam {hireStreamName} anymore": "User {name} is not recruiter in hire srteam {hireStreamName} anymore",
"User {name} added in hire stream {hireStreamName} as interviewer": "User {name} added in hire stream {hireStreamName} as interviewer",
"User {name} is not interviewer in hire srteam {hireStreamName} anymore": "User {name} is not interviewer in hire srteam {hireStreamName} anymore"
"User {name} is not interviewer in hire srteam {hireStreamName} anymore": "User {name} is not interviewer in hire srteam {hireStreamName} anymore",
"Theme changed": ""
}
3 changes: 2 additions & 1 deletion src/hooks/hooks.i18n/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@
"User {name} added in hire stream {hireStreamName} as recruiter": "Пользователь {name} добавлен в поток {hireStreamName} в качестве рекрутера",
"User {name} is not recruiter in hire srteam {hireStreamName} anymore": "Пользователь {name} больше не является рекрутером в потоке {hireStreamName}",
"User {name} added in hire stream {hireStreamName} as interviewer": "Пользователь {name} добавлен в поток {hireStreamName} в качестве собеседующего",
"User {name} is not interviewer in hire srteam {hireStreamName} anymore": "Пользователь {name} больше не является собеседующим потока {hireStreamName}"
"User {name} is not interviewer in hire srteam {hireStreamName} anymore": "Пользователь {name} больше не является собеседующим потока {hireStreamName}",
"Theme changed": "Тема изменена"
}
Loading

0 comments on commit 62b6f04

Please sign in to comment.