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 { 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: ' — ' }} />
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
);
|
||||
}
|
||||
|
||||
await updateUserEmail(user.id, email);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)': {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
admin Boolean @default(false)
|
||||
|
||||
dateFormat String @default("en-GB")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue