add full page width preference

This commit is contained in:
Philipinho
2024-07-03 11:00:42 +01:00
parent d1ae117f76
commit 8f056d1071
10 changed files with 135 additions and 38 deletions

View File

@ -1,5 +1,6 @@
import { Avatar, Group, Menu, rem, UnstyledButton, Text } from "@mantine/core";
import { Group, Menu, UnstyledButton, Text } from "@mantine/core";
import {
IconBrush,
IconChevronDown,
IconLogout,
IconSettings,
@ -38,10 +39,7 @@ export default function TopMenu() {
<Text fw={500} size="sm" lh={1} mr={3}>
{workspace.name}
</Text>
<IconChevronDown
style={{ width: rem(12), height: rem(12) }}
stroke={1.5}
/>
<IconChevronDown size={16} />
</Group>
</UnstyledButton>
</Menu.Target>
@ -51,12 +49,7 @@ export default function TopMenu() {
<Menu.Item
component={Link}
to={APP_ROUTE.SETTINGS.WORKSPACE.GENERAL}
leftSection={
<IconSettings
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
leftSection={<IconSettings size={16} />}
>
Workspace settings
</Menu.Item>
@ -64,12 +57,7 @@ export default function TopMenu() {
<Menu.Item
component={Link}
to={APP_ROUTE.SETTINGS.WORKSPACE.MEMBERS}
leftSection={
<IconUsers
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
leftSection={<IconUsers size={16} />}
>
Manage members
</Menu.Item>
@ -98,27 +86,22 @@ export default function TopMenu() {
<Menu.Item
component={Link}
to={APP_ROUTE.SETTINGS.ACCOUNT.PROFILE}
leftSection={
<IconUserCircle
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
leftSection={<IconUserCircle size={16} />}
>
My profile
</Menu.Item>
<Menu.Item
component={Link}
to={APP_ROUTE.SETTINGS.ACCOUNT.PREFERENCES}
leftSection={<IconBrush size={16} />}
>
My preferences
</Menu.Item>
<Menu.Divider />
<Menu.Item
onClick={logout}
leftSection={
<IconLogout
style={{ width: rem(16), height: rem(16) }}
stroke={1.5}
/>
}
>
<Menu.Item onClick={logout} leftSection={<IconLogout size={16} />}>
Logout
</Menu.Item>
</Menu.Dropdown>

View File

@ -2,6 +2,9 @@ import classes from "@/features/editor/styles/editor.module.css";
import React from "react";
import { TitleEditor } from "@/features/editor/title-editor";
import PageEditor from "@/features/editor/page-editor";
import { Container } from "@mantine/core";
import { useAtom } from "jotai/index";
import { userAtom } from "@/features/user/atoms/current-user-atom.ts";
const MemoizedTitleEditor = React.memo(TitleEditor);
const MemoizedPageEditor = React.memo(PageEditor);
@ -21,8 +24,16 @@ export function FullEditor({
spaceSlug,
editable,
}: FullEditorProps) {
const [user] = useAtom(userAtom);
const fullPageWidth = user.settings?.preferences?.fullPageWidth;
return (
<div className={classes.editor}>
<Container
fluid={fullPageWidth}
{...(fullPageWidth && { mx: 80 })}
size={850}
className={classes.editor}
>
<MemoizedTitleEditor
pageId={pageId}
slugId={slugId}
@ -31,6 +42,6 @@ export function FullEditor({
editable={editable}
/>
<MemoizedPageEditor pageId={pageId} editable={editable} />
</div>
</Container>
);
}

View File

@ -1,5 +1,4 @@
.editor {
max-width: 800px;
height: 100%;
padding: 8px 20px;
margin: 64px auto;

View File

@ -1,5 +1,16 @@
import { atomWithStorage } from "jotai/utils";
import { ICurrentUser } from "@/features/user/types/user.types";
import { focusAtom } from "jotai-optics";
export const currentUserAtom = atomWithStorage<ICurrentUser | null>("currentUser", null);
export const currentUserAtom = atomWithStorage<ICurrentUser | null>(
"currentUser",
null,
);
export const userAtom = focusAtom(currentUserAtom, (optic) =>
optic.prop("user"),
);
export const workspaceAtom = focusAtom(currentUserAtom, (optic) =>
optic.prop("workspace"),
);

View File

@ -0,0 +1,42 @@
import { Group, Text, Switch } from "@mantine/core";
import { useAtom } from "jotai/index";
import { userAtom } from "@/features/user/atoms/current-user-atom.ts";
import { updateUser } from "@/features/user/services/user-service.ts";
import React, { useState } from "react";
export default function PageWidthPref() {
return (
<Group justify="space-between" wrap="nowrap" gap="xl">
<div>
<Text size="md">Full page width</Text>
<Text size="sm" c="dimmed">
Choose your preferred page width.
</Text>
</div>
<WidthToggle />
</Group>
);
}
function WidthToggle() {
const [user, setUser] = useAtom(userAtom);
const [checked, setChecked] = useState(
user.settings?.preferences?.fullPageWidth,
);
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.currentTarget.checked;
const updatedUser = await updateUser({ fullPageWidth: value });
setChecked(value);
setUser(updatedUser);
};
return (
<Switch
defaultChecked={checked}
onChange={handleChange}
aria-label="Toggle full page width"
/>
);
}

View File

@ -7,7 +7,7 @@ export interface IUser {
emailVerifiedAt: Date;
avatarUrl: string;
timezone: string;
settings: any;
settings: IUserSettings;
invitedById: string;
lastLoginAt: string;
lastActiveAt: Date;
@ -17,9 +17,16 @@ export interface IUser {
workspaceId: string;
deactivatedAt: Date;
deletedAt: Date;
fullPageWidth: boolean; // used for update
}
export interface ICurrentUser {
user: IUser;
workspace: IWorkspace;
}
export interface IUserSettings {
preferences: {
fullPageWidth: boolean;
};
}

View File

@ -1,11 +1,15 @@
import SettingsTitle from "@/components/settings/settings-title.tsx";
import AccountTheme from "@/features/user/components/account-theme.tsx";
import PageWidthPref from "@/features/user/components/page-width-pref.tsx";
import { Divider } from "@mantine/core";
export default function AccountPreferences() {
return (
<>
<SettingsTitle title="Preferences" />
<AccountTheme />
<Divider my={"md"} />
<PageWidthPref />
</>
);
}

View File

@ -1,6 +1,6 @@
import { OmitType, PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from '../../auth/dto/create-user.dto';
import { IsOptional, IsString } from 'class-validator';
import { IsBoolean, IsOptional, IsString } from 'class-validator';
export class UpdateUserDto extends PartialType(
OmitType(CreateUserDto, ['password'] as const),
@ -8,4 +8,8 @@ export class UpdateUserDto extends PartialType(
@IsOptional()
@IsString()
avatarUrl: string;
@IsOptional()
@IsBoolean()
fullPageWidth: boolean;
}

View File

@ -20,10 +20,19 @@ export class UserService {
workspaceId: string,
) {
const user = await this.userRepo.findById(userId, workspaceId);
if (!user) {
throw new NotFoundException('User not found');
}
// preference update
if (typeof updateUserDto.fullPageWidth !== 'undefined') {
return this.updateUserPageWidthPreference(
userId,
updateUserDto.fullPageWidth,
);
}
if (updateUserDto.name) {
user.name = updateUserDto.name;
}
@ -42,4 +51,12 @@ export class UserService {
await this.userRepo.updateUser(updateUserDto, userId, workspaceId);
return user;
}
async updateUserPageWidthPreference(userId: string, fullPageWidth: boolean) {
return this.userRepo.updatePreference(
userId,
'fullPageWidth',
fullPageWidth,
);
}
}

View File

@ -15,6 +15,7 @@ import {
executeWithPagination,
PaginationResult,
} from '@docmost/db/pagination/pagination';
import { sql } from 'kysely';
@Injectable()
export class UserRepo {
@ -157,6 +158,24 @@ export class UserRepo {
return result;
}
async updatePreference(
userId: string,
prefKey: string,
prefValue: string | boolean,
) {
return await this.db
.updateTable('users')
.set({
settings: sql`COALESCE(settings, '{}'::jsonb)
|| jsonb_build_object('preferences', COALESCE(settings->'preferences', '{}'::jsonb)
|| jsonb_build_object('${sql.raw(prefKey)}', ${sql.lit(prefValue)}))`,
updatedAt: new Date(),
})
.where('id', '=', userId)
.returning(this.baseFields)
.executeTakeFirst();
}
/*
async getSpaceIds(
workspaceId: string,