2023-02-23 01:20:18 +01:00
|
|
|
import type { ActionArgs, LoaderArgs, MetaFunction } from '@remix-run/node';
|
|
|
|
|
import { json, redirect } from '@remix-run/node';
|
|
|
|
|
import {
|
|
|
|
|
Form,
|
|
|
|
|
Link,
|
|
|
|
|
Outlet,
|
|
|
|
|
useActionData,
|
|
|
|
|
useLoaderData,
|
|
|
|
|
useSearchParams
|
|
|
|
|
} from '@remix-run/react';
|
|
|
|
|
import * as React from 'react';
|
|
|
|
|
import {
|
|
|
|
|
TextInput,
|
|
|
|
|
Box,
|
|
|
|
|
Group,
|
|
|
|
|
Button,
|
|
|
|
|
PasswordInput,
|
|
|
|
|
Text,
|
|
|
|
|
Title,
|
|
|
|
|
Popover,
|
|
|
|
|
Progress,
|
2023-02-23 14:17:29 +01:00
|
|
|
Modal,
|
2023-03-01 01:19:38 +01:00
|
|
|
Badge,
|
2023-03-01 02:27:43 +01:00
|
|
|
Select,
|
|
|
|
|
NumberInput
|
2023-02-23 01:20:18 +01:00
|
|
|
} from '@mantine/core';
|
|
|
|
|
import { AtSign, Check, Lock, Save, Trash, X } from 'react-feather';
|
|
|
|
|
import { requireUser } from '~/session.server';
|
2023-03-01 01:19:38 +01:00
|
|
|
import { updateUserEmail, updateUserPrefs } from '~/models/user.server';
|
2023-02-23 01:20:18 +01:00
|
|
|
import { validateEmail } from '~/utils';
|
|
|
|
|
|
|
|
|
|
export async function loader({ request }: LoaderArgs) {
|
|
|
|
|
const user = await requireUser(request);
|
|
|
|
|
if (!user) return redirect('/login');
|
|
|
|
|
|
|
|
|
|
return json({ user });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function action({ request }: ActionArgs) {
|
|
|
|
|
const user = await requireUser(request);
|
|
|
|
|
const formData = await request.formData();
|
2023-03-01 01:19:38 +01:00
|
|
|
const email = (formData.get('email') || undefined) as string | undefined;
|
|
|
|
|
const dateFormat = (formData.get('dateFormat') || undefined) as
|
|
|
|
|
| string
|
|
|
|
|
| undefined;
|
2023-03-01 02:27:43 +01:00
|
|
|
const currency = (formData.get('currency') || undefined) as
|
|
|
|
|
| string
|
|
|
|
|
| undefined;
|
|
|
|
|
const defaultHourlyRate = (formData.get('defaultHourlyRate') || undefined) as
|
|
|
|
|
| string
|
|
|
|
|
| undefined;
|
2023-02-23 01:20:18 +01:00
|
|
|
|
2023-03-01 01:19:38 +01:00
|
|
|
if (email && !validateEmail(email)) {
|
2023-02-23 01:20:18 +01:00
|
|
|
return json(
|
|
|
|
|
{
|
|
|
|
|
errors: {
|
|
|
|
|
email: 'Email is invalid'
|
|
|
|
|
},
|
|
|
|
|
user
|
|
|
|
|
},
|
|
|
|
|
{ status: 400 }
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 01:19:38 +01:00
|
|
|
if (email && email !== user.email) {
|
|
|
|
|
await updateUserEmail(user.id, email);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-01 02:27:43 +01:00
|
|
|
const prefs = {
|
|
|
|
|
dateFormat:
|
|
|
|
|
dateFormat && dateFormat !== user.dateFormat ? dateFormat : undefined,
|
|
|
|
|
currency: currency && currency !== user.currency ? currency : undefined,
|
|
|
|
|
defaultHourlyRate:
|
|
|
|
|
defaultHourlyRate &&
|
|
|
|
|
parseInt(defaultHourlyRate || '-1', 10) !== user.defaultHourlyRate
|
|
|
|
|
? parseInt(defaultHourlyRate, 10)
|
|
|
|
|
: undefined
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (Object.values(prefs).some((v) => v !== undefined)) {
|
|
|
|
|
await updateUserPrefs(user.id, prefs);
|
2023-03-01 01:19:38 +01:00
|
|
|
}
|
2023-02-23 01:20:18 +01:00
|
|
|
|
|
|
|
|
return redirect('/account/updatesuccess');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const meta: MetaFunction = () => {
|
|
|
|
|
return {
|
|
|
|
|
title: 'Account | WorkTimer',
|
|
|
|
|
description:
|
|
|
|
|
'Manage your account settings and change your password for WorkTimer'
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default function Account() {
|
|
|
|
|
const actionData = useActionData<typeof action>();
|
|
|
|
|
const loaderData = useLoaderData<typeof loader>();
|
|
|
|
|
|
|
|
|
|
const emailRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
|
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
if (actionData?.errors?.email) {
|
|
|
|
|
emailRef.current?.focus();
|
|
|
|
|
}
|
|
|
|
|
}, [actionData]);
|
|
|
|
|
|
2023-03-01 01:19:38 +01:00
|
|
|
const [dateFormat, setDateFormat] = React.useState(
|
|
|
|
|
loaderData.user.dateFormat
|
|
|
|
|
);
|
|
|
|
|
|
2023-03-01 02:27:43 +01:00
|
|
|
const [isHydrated, setIsHydrated] = React.useState(false);
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
setIsHydrated(true);
|
|
|
|
|
}, []);
|
|
|
|
|
|
2023-02-23 01:20:18 +01:00
|
|
|
return (
|
|
|
|
|
<Box sx={{ maxWidth: 300 }} mx="auto">
|
|
|
|
|
<Title order={2} my="lg">
|
|
|
|
|
Account
|
|
|
|
|
</Title>
|
2023-02-23 14:17:29 +01:00
|
|
|
|
2023-03-01 01:19:38 +01:00
|
|
|
<Outlet />
|
|
|
|
|
|
2023-02-23 14:17:29 +01:00
|
|
|
{loaderData.user.admin && (
|
2023-03-01 01:19:38 +01:00
|
|
|
<Text mt="lg">
|
2023-02-23 14:17:29 +01:00
|
|
|
Role:{' '}
|
|
|
|
|
<Badge variant="light" mb="md">
|
|
|
|
|
ADMIN
|
|
|
|
|
</Badge>
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
|
2023-02-23 01:20:18 +01:00
|
|
|
<Form method="post" noValidate>
|
|
|
|
|
<TextInput
|
|
|
|
|
mb={12}
|
|
|
|
|
label="Email address"
|
|
|
|
|
placeholder="your@email.com"
|
|
|
|
|
icon={<AtSign size={16} />}
|
|
|
|
|
id="email"
|
|
|
|
|
ref={emailRef}
|
|
|
|
|
autoFocus={true}
|
|
|
|
|
defaultValue={actionData?.user?.email || loaderData?.user?.email}
|
|
|
|
|
name="email"
|
|
|
|
|
type="email"
|
|
|
|
|
autoComplete="off"
|
|
|
|
|
aria-invalid={actionData?.errors?.email ? true : undefined}
|
|
|
|
|
error={actionData?.errors?.email}
|
|
|
|
|
errorProps={{ children: actionData?.errors?.email }}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Group position="center" mt="sm">
|
|
|
|
|
<Button type="submit" leftIcon={<Save size={14} />}>
|
|
|
|
|
Update email
|
|
|
|
|
</Button>
|
|
|
|
|
</Group>
|
|
|
|
|
</Form>
|
|
|
|
|
|
|
|
|
|
<Group position="center" mt="xl">
|
|
|
|
|
<Button
|
|
|
|
|
component={Link}
|
|
|
|
|
to="/account/updatepassword"
|
|
|
|
|
variant="light"
|
|
|
|
|
leftIcon={<Lock size={14} />}
|
|
|
|
|
>
|
|
|
|
|
Change password
|
|
|
|
|
</Button>
|
|
|
|
|
</Group>
|
|
|
|
|
|
|
|
|
|
<Group position="center" mt="md">
|
|
|
|
|
<Button
|
|
|
|
|
component={Link}
|
|
|
|
|
to="/account/delete"
|
|
|
|
|
variant="light"
|
|
|
|
|
color="red"
|
|
|
|
|
leftIcon={<Trash size={14} />}
|
|
|
|
|
>
|
|
|
|
|
Delete account
|
|
|
|
|
</Button>
|
|
|
|
|
</Group>
|
2023-03-01 01:19:38 +01:00
|
|
|
|
|
|
|
|
<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'
|
|
|
|
|
])}
|
|
|
|
|
/>
|
|
|
|
|
|
2023-03-01 02:27:43 +01:00
|
|
|
{isHydrated && (
|
|
|
|
|
<p>
|
|
|
|
|
Example:{' '}
|
|
|
|
|
{Intl.DateTimeFormat(dateFormat, {
|
|
|
|
|
dateStyle: 'full',
|
|
|
|
|
timeStyle: 'short',
|
|
|
|
|
timeZone: 'UTC'
|
|
|
|
|
}).format(new Date(Date.now()))}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
2023-03-01 01:19:38 +01:00
|
|
|
|
2023-03-01 02:27:43 +01:00
|
|
|
<TextInput
|
|
|
|
|
name="currency"
|
|
|
|
|
label="Currency"
|
|
|
|
|
placeholder="Select your currency"
|
|
|
|
|
defaultValue={loaderData.user.currency}
|
|
|
|
|
mb="lg"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<NumberInput
|
|
|
|
|
name="defaultHourlyRate"
|
|
|
|
|
label="Hourly rate"
|
|
|
|
|
placeholder="Enter your hourly rate"
|
|
|
|
|
defaultValue={loaderData.user.defaultHourlyRate || undefined}
|
|
|
|
|
min={0}
|
|
|
|
|
mb="lg"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Group position="center" mt="lg">
|
2023-03-01 01:19:38 +01:00
|
|
|
<Button type="submit" leftIcon={<Save size={14} />}>
|
|
|
|
|
Save
|
|
|
|
|
</Button>
|
|
|
|
|
</Group>
|
|
|
|
|
</Form>
|
2023-02-23 01:20:18 +01:00
|
|
|
</Box>
|
|
|
|
|
);
|
|
|
|
|
}
|