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

View file

@ -20,11 +20,12 @@ import {
Popover,
Progress,
Modal,
Badge
Badge,
Select
} from '@mantine/core';
import { AtSign, Check, Lock, Save, Trash, X } from 'react-feather';
import { requireUser } from '~/session.server';
import { updateUserEmail } from '~/models/user.server';
import { updateUserEmail, updateUserPrefs } from '~/models/user.server';
import { validateEmail } from '~/utils';
export async function loader({ request }: LoaderArgs) {
@ -37,9 +38,12 @@ export async function loader({ request }: LoaderArgs) {
export async function action({ request }: ActionArgs) {
const user = await requireUser(request);
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(
{
errors: {
@ -51,7 +55,13 @@ export async function action({ request }: ActionArgs) {
);
}
if (email && email !== user.email) {
await updateUserEmail(user.id, email);
}
if (dateFormat && dateFormat !== user.dateFormat) {
await updateUserPrefs(user.id, { dateFormat });
}
return redirect('/account/updatesuccess');
}
@ -76,14 +86,20 @@ export default function Account() {
}
}, [actionData]);
const [dateFormat, setDateFormat] = React.useState(
loaderData.user.dateFormat
);
return (
<Box sx={{ maxWidth: 300 }} mx="auto">
<Title order={2} my="lg">
Account
</Title>
<Outlet />
{loaderData.user.admin && (
<Text>
<Text mt="lg">
Role:{' '}
<Badge variant="light" mb="md">
ADMIN
@ -116,8 +132,6 @@ export default function Account() {
</Group>
</Form>
<Outlet />
<Group position="center" mt="xl">
<Button
component={Link}
@ -140,6 +154,62 @@ export default function Account() {
Delete account
</Button>
</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>
);
}

View file

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

View file

@ -264,13 +264,12 @@ export default function Users() {
</td>
<td>
<Text>
{Intl.DateTimeFormat('it-IT', {
{Intl.DateTimeFormat(data.user.dateFormat, {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
minute: '2-digit'
}).format(new Date(user.createdAt))}
</Text>
</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
admin Boolean @default(false)
dateFormat String @default("en-GB")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt