feat: add user prefs with datetime format
This commit is contained in:
parent
6d8d487e28
commit
b9f5897280
|
|
@ -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: ' — ' }} />
|
<span dangerouslySetInnerHTML={{ __html: ' — ' }} />
|
||||||
<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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)': {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "dateFormat" TEXT NOT NULL DEFAULT 'en-GB';
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue