feat: add user prefs with datetime format

This commit is contained in:
Nicola Zambello 2023-03-01 01:19:38 +01:00
parent 6d8d487e28
commit b9f5897280
Signed by: nzambello
GPG key ID: 56E4A92C2C1E50BA
7 changed files with 114 additions and 26 deletions

View file

@ -1,5 +1,7 @@
import { Box, MediaQuery } from '@mantine/core'; import { Box, MediaQuery } from '@mantine/core';
import { useMatches } from '@remix-run/react';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { User } from '~/models/user.server';
export interface Props { export interface Props {
startTime: Date | string; startTime: Date | string;
@ -7,6 +9,9 @@ export interface Props {
} }
const TimeElapsed = ({ startTime, endTime }: Props) => { const TimeElapsed = ({ startTime, endTime }: Props) => {
let user = useMatches().find((m) => m.id === 'root')?.data?.user as
| User
| undefined;
const [elapsed, setElapsed] = useState( const [elapsed, setElapsed] = useState(
(new Date(endTime || Date.now()).getTime() - (new Date(endTime || Date.now()).getTime() -
new Date(startTime).getTime()) / new Date(startTime).getTime()) /
@ -70,19 +75,17 @@ const TimeElapsed = ({ startTime, endTime }: Props) => {
}} }}
> >
<span> <span>
{Intl.DateTimeFormat('it-IT', { {Intl.DateTimeFormat(user?.dateFormat || 'en-GB', {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit'
hour12: false
}).format(new Date(startTime))} }).format(new Date(startTime))}
</span> </span>
<span dangerouslySetInnerHTML={{ __html: '&nbsp;&mdash;&nbsp;' }} /> <span dangerouslySetInnerHTML={{ __html: '&nbsp;&mdash;&nbsp;' }} />
<span> <span>
{endTime {endTime
? Intl.DateTimeFormat('it-IT', { ? Intl.DateTimeFormat(user?.dateFormat || 'en-GB', {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit'
hour12: false
}).format(new Date(endTime)) }).format(new Date(endTime))
: 'now'} : 'now'}
</span> </span>

View file

@ -35,6 +35,18 @@ export async function updateUserEmail(id: User['id'], email: string) {
}); });
} }
export async function updateUserPrefs(
id: User['id'],
prefs: {
dateFormat?: User['dateFormat'];
}
) {
return prisma.user.update({
where: { id },
data: { ...prefs }
});
}
export async function updateUserPassword(id: User['id'], password: string) { export async function updateUserPassword(id: User['id'], password: string) {
const hashedPassword = await bcrypt.hash(password, 10); const hashedPassword = await bcrypt.hash(password, 10);

View file

@ -20,11 +20,12 @@ import {
Popover, Popover,
Progress, Progress,
Modal, Modal,
Badge Badge,
Select
} from '@mantine/core'; } from '@mantine/core';
import { AtSign, Check, Lock, Save, Trash, X } from 'react-feather'; import { AtSign, Check, Lock, Save, Trash, X } from 'react-feather';
import { requireUser } from '~/session.server'; import { requireUser } from '~/session.server';
import { updateUserEmail } from '~/models/user.server'; import { updateUserEmail, updateUserPrefs } from '~/models/user.server';
import { validateEmail } from '~/utils'; import { validateEmail } from '~/utils';
export async function loader({ request }: LoaderArgs) { export async function loader({ request }: LoaderArgs) {
@ -37,9 +38,12 @@ export async function loader({ request }: LoaderArgs) {
export async function action({ request }: ActionArgs) { export async function action({ request }: ActionArgs) {
const user = await requireUser(request); const user = await requireUser(request);
const formData = await request.formData(); const formData = await request.formData();
const email = formData.get('email'); const email = (formData.get('email') || undefined) as string | undefined;
const dateFormat = (formData.get('dateFormat') || undefined) as
| string
| undefined;
if (!validateEmail(email)) { if (email && !validateEmail(email)) {
return json( return json(
{ {
errors: { errors: {
@ -51,7 +55,13 @@ export async function action({ request }: ActionArgs) {
); );
} }
if (email && email !== user.email) {
await updateUserEmail(user.id, email); await updateUserEmail(user.id, email);
}
if (dateFormat && dateFormat !== user.dateFormat) {
await updateUserPrefs(user.id, { dateFormat });
}
return redirect('/account/updatesuccess'); return redirect('/account/updatesuccess');
} }
@ -76,14 +86,20 @@ export default function Account() {
} }
}, [actionData]); }, [actionData]);
const [dateFormat, setDateFormat] = React.useState(
loaderData.user.dateFormat
);
return ( return (
<Box sx={{ maxWidth: 300 }} mx="auto"> <Box sx={{ maxWidth: 300 }} mx="auto">
<Title order={2} my="lg"> <Title order={2} my="lg">
Account Account
</Title> </Title>
<Outlet />
{loaderData.user.admin && ( {loaderData.user.admin && (
<Text> <Text mt="lg">
Role:{' '} Role:{' '}
<Badge variant="light" mb="md"> <Badge variant="light" mb="md">
ADMIN ADMIN
@ -116,8 +132,6 @@ export default function Account() {
</Group> </Group>
</Form> </Form>
<Outlet />
<Group position="center" mt="xl"> <Group position="center" mt="xl">
<Button <Button
component={Link} component={Link}
@ -140,6 +154,62 @@ export default function Account() {
Delete account Delete account
</Button> </Button>
</Group> </Group>
<Title order={3} mt="xl" mb="lg">
Preferences
</Title>
<Form method="post" noValidate>
<Select
name="dateFormat"
searchable
clearable={false}
label="Date format"
placeholder="Select date format"
defaultValue={dateFormat}
value={dateFormat}
onChange={(value) =>
setDateFormat(value || loaderData.user.dateFormat)
}
data={Intl.DateTimeFormat.supportedLocalesOf([
'en-GB',
'en-US',
'it-IT',
'de-DE',
'fr-FR',
'es-ES',
'pt-BR',
'ja-JP',
'zh-CN',
'zh-TW',
'ko-KR',
'uk-UA',
'ru-RU'
])}
/>
<p>Example:</p>
<blockquote>
{Intl.DateTimeFormat(dateFormat, {
dateStyle: 'full',
timeZone: 'UTC'
}).format(new Date(Date.now()))}
<br />
{Intl.DateTimeFormat(dateFormat, {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(new Date(Date.now()))}
</blockquote>
<Group position="center" mt="sm">
<Button type="submit" leftIcon={<Save size={14} />}>
Save
</Button>
</Group>
</Form>
</Box> </Box>
); );
} }

View file

@ -36,7 +36,7 @@ import {
Square, Square,
Trash Trash
} from 'react-feather'; } from 'react-feather';
import { requireUserId } from '~/session.server'; import { requireUser } from '~/session.server';
import { getTimeEntries, TimeEntry } from '~/models/timeEntry.server'; import { getTimeEntries, TimeEntry } from '~/models/timeEntry.server';
import TimeElapsed from '~/components/TimeElapsed'; import TimeElapsed from '~/components/TimeElapsed';
import SectionTimeElapsed from '~/components/SectionTimeElapsed'; import SectionTimeElapsed from '~/components/SectionTimeElapsed';
@ -49,8 +49,8 @@ export const meta: MetaFunction = () => {
}; };
export async function loader({ request }: LoaderArgs) { export async function loader({ request }: LoaderArgs) {
const userId = await requireUserId(request); const user = await requireUser(request);
if (!userId) return redirect('/login'); if (!user) return redirect('/login');
const url = new URL(request.url); const url = new URL(request.url);
const page = url.searchParams.get('page') const page = url.searchParams.get('page')
@ -63,10 +63,11 @@ export async function loader({ request }: LoaderArgs) {
const order = url.searchParams.get('order') || 'desc'; const order = url.searchParams.get('order') || 'desc';
return json({ return json({
user,
...(await getTimeEntries({ ...(await getTimeEntries({
page, page,
size, size,
userId, userId: user.id,
orderBy, orderBy,
order: order === 'asc' ? 'asc' : 'desc' order: order === 'asc' ? 'asc' : 'desc'
})) }))
@ -91,10 +92,9 @@ export default function TimeEntriesPage() {
{ entries: typeof data.timeEntries; total: number } { entries: typeof data.timeEntries; total: number }
> = {}; > = {};
data.timeEntries.forEach((timeEntry) => { data.timeEntries.forEach((timeEntry) => {
const date = Intl.DateTimeFormat('it-IT', { const date = Intl.DateTimeFormat(data.user.dateFormat, {
year: 'numeric', dateStyle: 'full',
month: '2-digit', timeZone: 'UTC'
day: '2-digit'
}).format(new Date(timeEntry.startTime)); }).format(new Date(timeEntry.startTime));
if (!timeEntriesPerDay[date]) if (!timeEntriesPerDay[date])
@ -320,7 +320,7 @@ export default function TimeEntriesPage() {
alignItems: 'flex-end', alignItems: 'flex-end',
flexDirection: 'column', flexDirection: 'column',
justifyContent: 'space-between', justifyContent: 'space-between',
flexShrink: 1, flexShrink: 0,
flexGrow: 0, flexGrow: 0,
'@media (min-width: 601px)': { '@media (min-width: 601px)': {

View file

@ -264,13 +264,12 @@ export default function Users() {
</td> </td>
<td> <td>
<Text> <Text>
{Intl.DateTimeFormat('it-IT', { {Intl.DateTimeFormat(data.user.dateFormat, {
day: '2-digit', day: '2-digit',
month: '2-digit', month: '2-digit',
year: 'numeric', year: 'numeric',
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit'
hour12: false
}).format(new Date(user.createdAt))} }).format(new Date(user.createdAt))}
</Text> </Text>
</td> </td>

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "dateFormat" TEXT NOT NULL DEFAULT 'en-GB';

View file

@ -12,6 +12,8 @@ model User {
email String @unique email String @unique
admin Boolean @default(false) admin Boolean @default(false)
dateFormat String @default("en-GB")
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt