♻️ Address pull request feedback
This commit is contained in:
@@ -20,7 +20,7 @@ import { z } from 'zod';
|
||||
import { CommonHead } from '~/components/layout/Meta/CommonHead';
|
||||
import { env } from '~/env.js';
|
||||
import { ColorSchemeProvider } from '~/hooks/use-colorscheme';
|
||||
import { modals } from '~/modals/modals';
|
||||
import { modals } from '~/modals';
|
||||
import { queryClient } from '~/tools/server/configurations/tanstack/queryClient.tool';
|
||||
import { ConfigType } from '~/types/config';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
@@ -4,16 +4,18 @@ import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
import { IconCheck, IconX } from '@tabler/icons-react';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { z } from 'zod';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { prisma } from '~/server/db';
|
||||
import { inviteNamespaces } from '~/tools/server/translation-namespaces';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { api } from '~/utils/api';
|
||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||
import { signUpFormSchema } from '~/validations/user';
|
||||
|
||||
const notificationId = 'register';
|
||||
|
||||
export default function AuthInvitePage() {
|
||||
const { t } = useTranslation('authentication/invite');
|
||||
const { i18nZodResolver } = useI18nZodResolver();
|
||||
@@ -28,11 +30,10 @@ export default function AuthInvitePage() {
|
||||
});
|
||||
|
||||
const handleSubmit = (values: z.infer<typeof signUpFormSchema>) => {
|
||||
const notificationId = 'register';
|
||||
showNotification({
|
||||
id: notificationId,
|
||||
title: 'Creating account',
|
||||
message: 'Please wait...',
|
||||
title: t('notifications.loading.title'),
|
||||
message: `${t('notifications.loading.text')}...`,
|
||||
loading: true,
|
||||
});
|
||||
void mutateAsync(
|
||||
@@ -44,8 +45,8 @@ export default function AuthInvitePage() {
|
||||
onSuccess() {
|
||||
updateNotification({
|
||||
id: notificationId,
|
||||
title: 'Account created',
|
||||
message: 'Your account has been created successfully',
|
||||
title: t('notifications.success.title'),
|
||||
message: t('notifications.success.text'),
|
||||
color: 'teal',
|
||||
icon: <IconCheck />,
|
||||
});
|
||||
@@ -54,8 +55,8 @@ export default function AuthInvitePage() {
|
||||
onError() {
|
||||
updateNotification({
|
||||
id: notificationId,
|
||||
title: 'Error',
|
||||
message: 'Something went wrong',
|
||||
title: t('notifications.error.title'),
|
||||
message: t('notifications.error.text'),
|
||||
color: 'red',
|
||||
icon: <IconX />,
|
||||
});
|
||||
@@ -64,47 +65,55 @@ export default function AuthInvitePage() {
|
||||
);
|
||||
};
|
||||
|
||||
const metaTitle = `${t('metaTitle')} • Homarr`;
|
||||
|
||||
return (
|
||||
<Flex h="100dvh" display="flex" w="100%" direction="column" align="center" justify="center">
|
||||
<Card withBorder shadow="md" p="xl" radius="md" w="90%" maw={420}>
|
||||
<Title align="center" weight={900}>
|
||||
{t('title')}
|
||||
</Title>
|
||||
<>
|
||||
<Head>
|
||||
<title>{metaTitle}</title>
|
||||
</Head>
|
||||
|
||||
<Text color="dimmed" size="sm" align="center" mt={5} mb="md">
|
||||
{t('text')}
|
||||
</Text>
|
||||
<Flex h="100dvh" display="flex" w="100%" direction="column" align="center" justify="center">
|
||||
<Card withBorder shadow="md" p="xl" radius="md" w="90%" maw={420}>
|
||||
<Title align="center" weight={900}>
|
||||
{t('title')}
|
||||
</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
variant="filled"
|
||||
label={t('form.fields.username.label')}
|
||||
withAsterisk
|
||||
{...form.getInputProps('username')}
|
||||
/>
|
||||
<Text color="dimmed" size="sm" align="center" mt={5} mb="md">
|
||||
{t('text')}
|
||||
</Text>
|
||||
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
label={t('form.fields.password.label')}
|
||||
withAsterisk
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
variant="filled"
|
||||
label={t('form.fields.username.label')}
|
||||
withAsterisk
|
||||
{...form.getInputProps('username')}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
label={t('form.fields.passwordConfirmation.label')}
|
||||
withAsterisk
|
||||
{...form.getInputProps('passwordConfirmation')}
|
||||
/>
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
label={t('form.fields.password.label')}
|
||||
withAsterisk
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
|
||||
<Button fullWidth type="submit">
|
||||
{t('form.buttons.submit')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Card>
|
||||
</Flex>
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
label={t('form.fields.passwordConfirmation.label')}
|
||||
withAsterisk
|
||||
{...form.getInputProps('passwordConfirmation')}
|
||||
/>
|
||||
|
||||
<Button fullWidth type="submit">
|
||||
{t('form.buttons.submit')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Card>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -157,7 +166,7 @@ export const getServerSideProps: GetServerSideProps = async ({
|
||||
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale ?? '', inviteNamespaces)),
|
||||
...(await getServerSideTranslations(['authentication/invite'], locale, req, res)),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,17 +14,15 @@ import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { signIn } from 'next-auth/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||
import { signInSchema } from '~/validations/user';
|
||||
|
||||
import { loginNamespaces } from '../../tools/server/translation-namespaces';
|
||||
|
||||
export default function LoginPage() {
|
||||
const { t } = useTranslation('authentication/login');
|
||||
const queryParams = useRouter().query as { error?: 'CredentialsSignin' | (string & {}) };
|
||||
@@ -54,49 +52,54 @@ export default function LoginPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const metaTitle = `${t('metaTitle')} • Homarr`;
|
||||
|
||||
return (
|
||||
<Flex h="100dvh" display="flex" w="100%" direction="column" align="center" justify="center">
|
||||
<>
|
||||
<Head>
|
||||
<title>Login • Homarr</title>
|
||||
<title>{metaTitle}</title>
|
||||
</Head>
|
||||
<Card withBorder shadow="md" p="xl" radius="md" w="90%" maw={420}>
|
||||
<Title align="center" weight={900}>
|
||||
{t('title')}
|
||||
</Title>
|
||||
|
||||
<Text color="dimmed" size="sm" align="center" mt={5} mb="md">
|
||||
{t('text')}
|
||||
</Text>
|
||||
<Flex h="100dvh" display="flex" w="100%" direction="column" align="center" justify="center">
|
||||
<Card withBorder shadow="md" p="xl" radius="md" w="90%" maw={420}>
|
||||
<Title align="center" weight={900}>
|
||||
{t('title')}
|
||||
</Title>
|
||||
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
variant="filled"
|
||||
label={t('form.fields.username.label')}
|
||||
withAsterisk
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
<Text color="dimmed" size="sm" align="center" mt={5} mb="md">
|
||||
{t('text')}
|
||||
</Text>
|
||||
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
label={t('form.fields.password.label')}
|
||||
withAsterisk
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<TextInput
|
||||
variant="filled"
|
||||
label={t('form.fields.username.label')}
|
||||
withAsterisk
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
|
||||
<Button fullWidth type="submit" loading={isLoading}>
|
||||
{t('form.buttons.submit')}
|
||||
</Button>
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
label={t('form.fields.password.label')}
|
||||
withAsterisk
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
|
||||
{queryParams.error === 'CredentialsSignin' && (
|
||||
<Alert icon={<IconAlertTriangle size="1rem" />} color="red">
|
||||
{t('alert')}
|
||||
</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
</Card>
|
||||
</Flex>
|
||||
<Button fullWidth type="submit" loading={isLoading}>
|
||||
{t('form.buttons.submit')}
|
||||
</Button>
|
||||
|
||||
{queryParams.error === 'CredentialsSignin' && (
|
||||
<Alert icon={<IconAlertTriangle size="1rem" />} color="red">
|
||||
{t('alert')}
|
||||
</Alert>
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
</Card>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,8 +117,7 @@ export const getServerSideProps: GetServerSideProps = async ({ locale, req, res
|
||||
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale ?? 'en', loginNamespaces)),
|
||||
// Will be passed to the page component as props
|
||||
...(await getServerSideTranslations(['authentication/login'], locale, req, res)),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ import { boardNamespaces } from '~/tools/server/translation-namespaces';
|
||||
import { firstUpperCase } from '~/tools/shared/strings';
|
||||
import { api } from '~/utils/api';
|
||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||
import { boardCustomizationSchema } from '~/validations/dashboards';
|
||||
import { boardCustomizationSchema } from '~/validations/boards';
|
||||
|
||||
const notificationId = 'board-customization-notification';
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Flex,
|
||||
Group,
|
||||
LoadingOverlay,
|
||||
Menu,
|
||||
@@ -26,9 +25,11 @@ import {
|
||||
IconTrash,
|
||||
} from '@tabler/icons-react';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { openCreateBoardModal } from '~/components/Manage/Board/create-board.modal';
|
||||
import { openDeleteBoardModal } from '~/components/Manage/Board/delete-board.modal';
|
||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { sleep } from '~/tools/client/time';
|
||||
@@ -47,31 +48,26 @@ const BoardsPage = () => {
|
||||
|
||||
const [deletingDashboards, { append, filter }] = useListState<string>([]);
|
||||
|
||||
const { t } = useTranslation('boards/manage');
|
||||
const { t } = useTranslation('manage/boards');
|
||||
|
||||
const metaTitle = `${t('metaTitle')} • Homarr`;
|
||||
|
||||
return (
|
||||
<ManageLayout>
|
||||
<Head>
|
||||
<title>Boards • Homarr</title>
|
||||
<title>{metaTitle}</title>
|
||||
</Head>
|
||||
|
||||
<Title mb="xl">{t('title')}</Title>
|
||||
|
||||
<Flex justify="end" mb="md">
|
||||
<Group position="apart">
|
||||
<Title mb="xl">{t('pageTitle')}</Title>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.openContextModal({
|
||||
modal: 'createDashboardModal',
|
||||
title: <Text>{t('buttons.create')}</Text>,
|
||||
innerProps: {},
|
||||
});
|
||||
}}
|
||||
onClick={openCreateBoardModal}
|
||||
leftIcon={<IconPlus size="1rem" />}
|
||||
variant="default"
|
||||
>
|
||||
{t('buttons.create')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Group>
|
||||
|
||||
{data && (
|
||||
<SimpleGrid
|
||||
@@ -167,17 +163,13 @@ const BoardsPage = () => {
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
onClick={async () => {
|
||||
modals.openContextModal({
|
||||
modal: 'deleteBoardModal',
|
||||
title: <Text weight={500}>{t('cards.menu.delete.modalTitle')}</Text>,
|
||||
innerProps: {
|
||||
boardName: board.name,
|
||||
onConfirm: async () => {
|
||||
append(board.name);
|
||||
// give user feedback, that it's being deleted
|
||||
await sleep(500);
|
||||
filter((item, _) => item !== board.name);
|
||||
},
|
||||
openDeleteBoardModal({
|
||||
boardName: board.name,
|
||||
onConfirm: async () => {
|
||||
append(board.name);
|
||||
// give user feedback, that it's being deleted
|
||||
await sleep(500);
|
||||
filter((item, _) => item !== board.name);
|
||||
},
|
||||
});
|
||||
}}
|
||||
@@ -213,8 +205,8 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const translations = await getServerSideTranslations(
|
||||
manageNamespaces,
|
||||
ctx.locale,
|
||||
undefined,
|
||||
undefined
|
||||
ctx.req,
|
||||
ctx.res
|
||||
);
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { IconArrowRight } from '@tabler/icons-react';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Head from 'next/head';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
@@ -19,24 +20,31 @@ import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { OnlyKeysWithStructure } from '~/types/helpers';
|
||||
|
||||
import { type quickActions } from '../../../public/locales/en/manage/index.json';
|
||||
|
||||
const ManagementPage = () => {
|
||||
const { t } = useTranslation('manage/index');
|
||||
const { classes } = useStyles();
|
||||
const largerThanMd = useScreenLargerThan('md');
|
||||
const { data: sessionData } = useSession();
|
||||
|
||||
const metaTitle = `${t('metaTitle')} • Homarr`;
|
||||
return (
|
||||
<ManageLayout>
|
||||
<Head>
|
||||
<title>Manage • Homarr</title>
|
||||
<title>{metaTitle}</title>
|
||||
</Head>
|
||||
<Box className={classes.box} w="100%" mih={150} p="xl" mb={50}>
|
||||
<Group position="apart" noWrap>
|
||||
<Stack spacing={15}>
|
||||
<Title className={classes.boxTitle} order={2}>
|
||||
Welcome back, {sessionData?.user?.name ?? 'Anonymous'}
|
||||
{t('hero.title', {
|
||||
username: sessionData?.user?.name ?? t('hero.fallbackUsername'),
|
||||
})}
|
||||
</Title>
|
||||
<Text>Welcome to Your Application Hub. Organize, Optimize, and Conquer!</Text>
|
||||
<Text>{t('hero.subtitle')}</Text>
|
||||
</Stack>
|
||||
<Box bg="blue" w={100} h="100%" pos="relative">
|
||||
<Box
|
||||
@@ -49,7 +57,7 @@ const ManagementPage = () => {
|
||||
src="/imgs/logo/logo.png"
|
||||
width={largerThanMd ? 200 : 100}
|
||||
height={largerThanMd ? 150 : 60}
|
||||
alt=""
|
||||
alt="Homarr Logo"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -57,7 +65,7 @@ const ManagementPage = () => {
|
||||
</Box>
|
||||
|
||||
<Text weight="bold" mb="md">
|
||||
Quick actions
|
||||
{t('quickActions.title')}
|
||||
</Text>
|
||||
<SimpleGrid
|
||||
cols={3}
|
||||
@@ -67,44 +75,46 @@ const ManagementPage = () => {
|
||||
{ maxWidth: '48rem', cols: 1, spacing: 'md' },
|
||||
]}
|
||||
>
|
||||
<UnstyledButton component={Link} href="/manage/boards">
|
||||
<Card className={classes.quickActionCard}>
|
||||
<Group spacing={30} noWrap>
|
||||
<Stack spacing={0}>
|
||||
<Text weight="bold">Your boards</Text>
|
||||
<Text>Show a list of all your dashboards</Text>
|
||||
</Stack>
|
||||
<IconArrowRight />
|
||||
</Group>
|
||||
</Card>
|
||||
</UnstyledButton>
|
||||
<UnstyledButton component={Link} href="/manage/users/invites">
|
||||
<Card className={classes.quickActionCard}>
|
||||
<Group spacing={30} noWrap>
|
||||
<Stack spacing={0}>
|
||||
<Text weight="bold">Invite a new user</Text>
|
||||
<Text>Create and send an invitation for registration</Text>
|
||||
</Stack>
|
||||
<IconArrowRight />
|
||||
</Group>
|
||||
</Card>
|
||||
</UnstyledButton>
|
||||
<UnstyledButton component={Link} href="/manage/users">
|
||||
<Card className={classes.quickActionCard}>
|
||||
<Group spacing={30} noWrap>
|
||||
<Stack spacing={0}>
|
||||
<Text weight="bold">Manage users</Text>
|
||||
<Text>Delete and manage your users</Text>
|
||||
</Stack>
|
||||
<IconArrowRight />
|
||||
</Group>
|
||||
</Card>
|
||||
</UnstyledButton>
|
||||
<QuickActionCard type="boards" href="/manage/boards" />
|
||||
<QuickActionCard type="inviteUsers" href="/manage/users/invites" />
|
||||
<QuickActionCard type="manageUsers" href="/manage/users" />
|
||||
</SimpleGrid>
|
||||
</ManageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
type QuickActionType = OnlyKeysWithStructure<
|
||||
typeof quickActions,
|
||||
{
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
>;
|
||||
|
||||
type QuickActionCardProps = {
|
||||
type: QuickActionType;
|
||||
href: string;
|
||||
};
|
||||
|
||||
const QuickActionCard = ({ type, href }: QuickActionCardProps) => {
|
||||
const { t } = useTranslation('manage/index');
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<UnstyledButton component={Link} href={href}>
|
||||
<Card className={classes.quickActionCard}>
|
||||
<Group position="apart" noWrap>
|
||||
<Stack spacing={0}>
|
||||
<Text weight={500}>{t(`quickActions.${type}.title`)}</Text>
|
||||
<Text>{t(`quickActions.${type}.subtitle`)}</Text>
|
||||
</Stack>
|
||||
<IconArrowRight />
|
||||
</Group>
|
||||
</Card>
|
||||
</UnstyledButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const session = await getServerAuthSession(ctx);
|
||||
|
||||
@@ -115,10 +125,10 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
}
|
||||
|
||||
const translations = await getServerSideTranslations(
|
||||
['common'],
|
||||
['layout/manage', 'manage/index'],
|
||||
ctx.locale,
|
||||
undefined,
|
||||
undefined
|
||||
ctx.req,
|
||||
ctx.res
|
||||
);
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Text, Title } from '@mantine/core';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
|
||||
const SettingsPage = () => {
|
||||
return (
|
||||
<ManageLayout>
|
||||
<Head>
|
||||
<title>Settings • Homarr</title>
|
||||
</Head>
|
||||
|
||||
<Title>Settings</Title>
|
||||
<Text>Coming soon!</Text>
|
||||
</ManageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const session = await getServerAuthSession(ctx);
|
||||
|
||||
if (!session?.user.isAdmin) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
const translations = await getServerSideTranslations(
|
||||
['common'],
|
||||
ctx.locale,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
return {
|
||||
props: {
|
||||
...translations,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
||||
@@ -1,25 +1,17 @@
|
||||
import { Alert, Button, Card, Group, Stepper, Table, Text, Title } from '@mantine/core';
|
||||
import { useForm, zodResolver } from '@mantine/form';
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconCheck,
|
||||
IconInfoCircle,
|
||||
IconKey,
|
||||
IconMail,
|
||||
IconMailCheck,
|
||||
IconUser,
|
||||
IconUserPlus,
|
||||
} from '@tabler/icons-react';
|
||||
import { Alert, Button, Group, Stepper } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconArrowLeft, IconKey, IconMailCheck, IconUser, IconUserPlus } from '@tabler/icons-react';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
CreateAccountStep,
|
||||
createAccountStepValidationSchema,
|
||||
} from '~/components/Manage/User/Create/create-account-step';
|
||||
import { ReviewInputStep } from '~/components/Manage/User/Create/review-input-step';
|
||||
import {
|
||||
CreateAccountSecurityStep,
|
||||
createAccountSecurityStepValidationSchema,
|
||||
@@ -27,15 +19,17 @@ import {
|
||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { api } from '~/utils/api';
|
||||
import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||
|
||||
const CreateNewUserPage = () => {
|
||||
const { t } = useTranslation('manage/users/create');
|
||||
const [active, setActive] = useState(0);
|
||||
const nextStep = () => setActive((current) => (current < 3 ? current + 1 : current));
|
||||
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
|
||||
const { i18nZodResolver } = useI18nZodResolver();
|
||||
|
||||
const form = useForm({
|
||||
const form = useForm<CreateAccountSchema>({
|
||||
initialValues: {
|
||||
account: {
|
||||
username: '',
|
||||
@@ -45,30 +39,14 @@ const CreateNewUserPage = () => {
|
||||
password: '',
|
||||
},
|
||||
},
|
||||
validate: zodResolver(
|
||||
z.object({
|
||||
account: createAccountStepValidationSchema,
|
||||
security: createAccountSecurityStepValidationSchema,
|
||||
})
|
||||
),
|
||||
validate: i18nZodResolver(createAccountSchema),
|
||||
});
|
||||
|
||||
const context = api.useContext();
|
||||
const { mutateAsync, isLoading } = api.user.create.useMutation({
|
||||
onSettled: () => {
|
||||
void context.user.all.invalidate();
|
||||
},
|
||||
onSuccess: () => {
|
||||
nextStep();
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useTranslation('user/create');
|
||||
|
||||
const metaTitle = `${t('metaTitle')} • Homarr`;
|
||||
return (
|
||||
<ManageLayout>
|
||||
<Head>
|
||||
<title>Create user • Homarr</title>
|
||||
<title>{metaTitle}</title>
|
||||
</Head>
|
||||
|
||||
<Stepper active={active} onStepClick={setActive} breakpoint="sm" mih="100%">
|
||||
@@ -111,92 +89,11 @@ const CreateNewUserPage = () => {
|
||||
label={t('steps.finish.title')}
|
||||
description={t('steps.finish.title')}
|
||||
>
|
||||
<Card mih={400}>
|
||||
<Title order={5}>{t('steps.finish.card.title')}</Title>
|
||||
<Text mb="xl">{t('steps.finish.card.text')}</Text>
|
||||
|
||||
<Table mb="lg" withBorder highlightOnHover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('steps.finish.table.header.property')}</th>
|
||||
<th>{t('steps.finish.table.header.value')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<Group spacing="xs">
|
||||
<IconUser size="1rem" />
|
||||
<Text>{t('steps.finish.table.header.username')}</Text>
|
||||
</Group>
|
||||
</td>
|
||||
<td>{form.values.account.username}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Group spacing="xs">
|
||||
<IconMail size="1rem" />
|
||||
<Text>{t('steps.finish.table.header.email')}</Text>
|
||||
</Group>
|
||||
</td>
|
||||
<td>
|
||||
{form.values.account.eMail ? (
|
||||
<Text>{form.values.account.eMail}</Text>
|
||||
) : (
|
||||
<Group spacing="xs">
|
||||
<IconInfoCircle size="1rem" color="orange" />
|
||||
<Text color="orange">{t('steps.finish.table.notSet')}</Text>
|
||||
</Group>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Group spacing="xs">
|
||||
<IconKey size="1rem" />
|
||||
<Text>{t('steps.finish.table.password')}</Text>
|
||||
</Group>
|
||||
</td>
|
||||
<td>
|
||||
<Group spacing="xs">
|
||||
<IconCheck size="1rem" color="green" />
|
||||
<Text color="green">{t('steps.finish.table.valid')}</Text>
|
||||
</Group>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<Group position="apart" noWrap>
|
||||
<Button
|
||||
leftIcon={<IconArrowLeft size="1rem" />}
|
||||
onClick={prevStep}
|
||||
variant="light"
|
||||
px="xl"
|
||||
>
|
||||
{t('buttons.previous')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
username: form.values.account.username,
|
||||
password: form.values.security.password,
|
||||
email: form.values.account.eMail === '' ? undefined : form.values.account.eMail,
|
||||
});
|
||||
}}
|
||||
loading={isLoading}
|
||||
rightIcon={<IconCheck size="1rem" />}
|
||||
variant="light"
|
||||
px="xl"
|
||||
>
|
||||
{t('buttons.confirm')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
<ReviewInputStep values={form.values} prevStep={prevStep} nextStep={nextStep} />
|
||||
</Stepper.Step>
|
||||
<Stepper.Completed>
|
||||
<Alert title="User was created" color="green" mb="md">
|
||||
{t('steps.finish.alertConfirmed')}
|
||||
<Alert title={t('steps.completed.alert.title')} color="green" mb="md">
|
||||
{t('steps.completed.alert.text')}
|
||||
</Alert>
|
||||
|
||||
<Group>
|
||||
@@ -225,6 +122,13 @@ const CreateNewUserPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const createAccountSchema = z.object({
|
||||
account: createAccountStepValidationSchema,
|
||||
security: createAccountSecurityStepValidationSchema,
|
||||
});
|
||||
|
||||
export type CreateAccountSchema = z.infer<typeof createAccountSchema>;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const session = await getServerAuthSession(ctx);
|
||||
|
||||
@@ -237,8 +141,8 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const translations = await getServerSideTranslations(
|
||||
manageNamespaces,
|
||||
ctx.locale,
|
||||
undefined,
|
||||
undefined
|
||||
ctx.req,
|
||||
ctx.res
|
||||
);
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -15,15 +15,16 @@ import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { openContextModal } from '@mantine/modals';
|
||||
import { IconPlus, IconTrash } from '@tabler/icons-react';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { openDeleteUserModal } from '~/components/Manage/User/delete-user.modal';
|
||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { api } from '~/utils/api';
|
||||
import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
const ManageUsersPage = () => {
|
||||
const [activePage, setActivePage] = useState(0);
|
||||
@@ -34,7 +35,7 @@ const ManageUsersPage = () => {
|
||||
search: debouncedSearch,
|
||||
});
|
||||
|
||||
const { t } = useTranslation('user/manage');
|
||||
const { t } = useTranslation('manage/users');
|
||||
|
||||
return (
|
||||
<ManageLayout>
|
||||
@@ -86,16 +87,7 @@ const ManageUsersPage = () => {
|
||||
<Group>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
openContextModal({
|
||||
modal: 'deleteUserModal',
|
||||
title: (
|
||||
<Text weight="bold">{t('modals.delete', { name: user.name })}</Text>
|
||||
),
|
||||
innerProps: {
|
||||
userId: user.id,
|
||||
username: user.name ?? '',
|
||||
},
|
||||
});
|
||||
openDeleteUserModal(user);
|
||||
}}
|
||||
color="red"
|
||||
variant="light"
|
||||
@@ -112,9 +104,7 @@ const ManageUsersPage = () => {
|
||||
<tr>
|
||||
<td colSpan={1}>
|
||||
<Box p={15}>
|
||||
<Text>
|
||||
{t('searchDoesntMatch')}
|
||||
</Text>
|
||||
<Text>{t('searchDoesntMatch')}</Text>
|
||||
</Box>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -13,9 +13,10 @@ import { modals } from '@mantine/modals';
|
||||
import { IconPlus, IconTrash } from '@tabler/icons-react';
|
||||
import dayjs from 'dayjs';
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import Head from 'next/head';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { openCreateInviteModal } from '~/components/Manage/User/Invite/create-invite.modal';
|
||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
@@ -23,40 +24,33 @@ import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
const ManageUserInvitesPage = () => {
|
||||
const { classes } = useStyles();
|
||||
const { t } = useTranslation('manage/users/invites');
|
||||
const [activePage, setActivePage] = useState(0);
|
||||
const { data } = api.invites.all.useQuery({
|
||||
const { data: invites } = api.invites.all.useQuery({
|
||||
page: activePage,
|
||||
});
|
||||
|
||||
const { classes } = useStyles();
|
||||
|
||||
const handleFetchNextPage = async () => {
|
||||
const nextPage = () => {
|
||||
setActivePage((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const handleFetchPreviousPage = async () => {
|
||||
const previousPage = () => {
|
||||
setActivePage((prev) => prev - 1);
|
||||
};
|
||||
|
||||
const { t } = useTranslation('user/invites');
|
||||
|
||||
const metaTitle = `${t('metaTitle')} • Homarr`;
|
||||
return (
|
||||
<ManageLayout>
|
||||
<Head>
|
||||
<title>User invites • Homarr</title>
|
||||
<title>{metaTitle}</title>
|
||||
</Head>
|
||||
<Title mb="md">{t('title')}</Title>
|
||||
<Text mb="xl">{t('text')}</Text>
|
||||
<Title mb="md">{t('pageTitle')}</Title>
|
||||
<Text mb="xl">{t('description')}</Text>
|
||||
|
||||
<Flex justify="end" mb="md">
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.openContextModal({
|
||||
modal: 'createInviteModal',
|
||||
title: 'Create invite',
|
||||
innerProps: {},
|
||||
});
|
||||
}}
|
||||
onClick={openCreateInviteModal}
|
||||
leftIcon={<IconPlus size="1rem" />}
|
||||
variant="default"
|
||||
>
|
||||
@@ -64,7 +58,7 @@ const ManageUserInvitesPage = () => {
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
{data && (
|
||||
{invites && (
|
||||
<>
|
||||
<Table mb="md" withBorder highlightOnHover>
|
||||
<thead>
|
||||
@@ -76,7 +70,7 @@ const ManageUserInvitesPage = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.invites.map((invite, index) => (
|
||||
{invites.invites.map((invite, index) => (
|
||||
<tr key={index}>
|
||||
<td className={classes.tableGrowCell}>
|
||||
<Text lineClamp={1}>{invite.id}</Text>
|
||||
@@ -114,9 +108,9 @@ const ManageUserInvitesPage = () => {
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{data.invites.length === 0 && (
|
||||
{invites.invites.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<td colSpan={4}>
|
||||
<Center p="md">
|
||||
<Text color="dimmed">{t('noInvites')}</Text>
|
||||
</Center>
|
||||
@@ -126,18 +120,18 @@ const ManageUserInvitesPage = () => {
|
||||
</tbody>
|
||||
</Table>
|
||||
<Pagination
|
||||
total={data.countPages}
|
||||
total={invites.countPages}
|
||||
value={activePage + 1}
|
||||
onChange={(targetPage) => {
|
||||
setActivePage(targetPage - 1);
|
||||
}}
|
||||
onNextPage={handleFetchNextPage}
|
||||
onPreviousPage={handleFetchPreviousPage}
|
||||
onNextPage={nextPage}
|
||||
onPreviousPage={previousPage}
|
||||
onFirstPage={() => {
|
||||
setActivePage(0);
|
||||
}}
|
||||
onLastPage={() => {
|
||||
setActivePage(data.countPages - 1);
|
||||
setActivePage(invites.countPages - 1);
|
||||
}}
|
||||
withEdges
|
||||
/>
|
||||
@@ -168,9 +162,10 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const translations = await getServerSideTranslations(
|
||||
manageNamespaces,
|
||||
ctx.locale,
|
||||
undefined,
|
||||
undefined
|
||||
ctx.req,
|
||||
ctx.res
|
||||
);
|
||||
|
||||
return {
|
||||
props: {
|
||||
...translations,
|
||||
|
||||
@@ -26,7 +26,6 @@ import { ReactNode, useMemo, useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { prisma } from '~/server/db';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { onboardNamespaces } from '~/tools/server/translation-namespaces';
|
||||
import { api } from '~/utils/api';
|
||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||
import { signUpFormSchema } from '~/validations/user';
|
||||
@@ -225,12 +224,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
};
|
||||
}
|
||||
|
||||
const translations = await getServerSideTranslations(
|
||||
onboardNamespaces,
|
||||
ctx.locale,
|
||||
ctx.req,
|
||||
ctx.res
|
||||
);
|
||||
const translations = await getServerSideTranslations([], ctx.locale, ctx.req, ctx.res);
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
||||
@@ -200,12 +200,7 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res, locale
|
||||
await helpers.user.withSettings.prefetch();
|
||||
await helpers.boards.all.prefetch();
|
||||
|
||||
const translations = await getServerSideTranslations(
|
||||
['user/preferences'],
|
||||
locale,
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
const translations = await getServerSideTranslations(['user/preferences'], locale, req, res);
|
||||
return {
|
||||
props: {
|
||||
...translations,
|
||||
|
||||
Reference in New Issue
Block a user