mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 23:45:48 +01:00
♻️ Address pull request feedback
This commit is contained in:
@@ -180,25 +180,3 @@ const useStyles = createStyles(({ colors, colorScheme, radius }) => ({
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
/*
|
||||
<BackgroundChanger />
|
||||
|
||||
<Stack spacing="xs" my="md">
|
||||
<Text>{t('settings/customization/color-selector:colors')}</Text>
|
||||
<Grid>
|
||||
<Grid.Col sm={12} md={6}>
|
||||
<ColorSelector type="primary" defaultValue="red" />
|
||||
</Grid.Col>
|
||||
<Grid.Col sm={12} md={6}>
|
||||
<ColorSelector type="secondary" defaultValue="orange" />
|
||||
</Grid.Col>
|
||||
<Grid.Col sm={12} md={6}>
|
||||
<ShadeSelector />
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
|
||||
<DashboardTilesOpacitySelector />
|
||||
<CustomCssChanger />
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DEFAULT_THEME, MANTINE_COLORS, MantineColor } from '@mantine/core';
|
||||
import { createFormContext } from '@mantine/form';
|
||||
import { z } from 'zod';
|
||||
import { boardCustomizationSchema } from '~/validations/dashboards';
|
||||
import { boardCustomizationSchema } from '~/validations/boards';
|
||||
|
||||
export const [
|
||||
BoardCustomizationFormProvider,
|
||||
|
||||
@@ -168,8 +168,8 @@ const CitySelectModal = ({ opened, closeModal, query, onCitySelected }: CitySele
|
||||
<Center>
|
||||
<Stack align="center">
|
||||
<IconAlertTriangle />
|
||||
<Title order={6}>Nothing found</Title>
|
||||
<Text>Nothing was found, please try again</Text>
|
||||
<Title order={6}>{t('modal.table.nothingFound.title')}</Title>
|
||||
<Text>{t('modal.table.nothingFound.description')}</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Modal>
|
||||
|
||||
84
src/components/Manage/Board/create-board.modal.tsx
Normal file
84
src/components/Manage/Board/create-board.modal.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Button, Group, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { ContextModalProps, modals } from '@mantine/modals';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { getStaticFallbackConfig } from '~/tools/config/getFallbackConfig';
|
||||
import { api } from '~/utils/api';
|
||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||
import { createBoardSchemaValidation } from '~/validations/boards';
|
||||
|
||||
export const CreateBoardModal = ({ id }: ContextModalProps<{}>) => {
|
||||
const { t } = useTranslation('manage/boards');
|
||||
const utils = api.useContext();
|
||||
const { isLoading, mutate } = api.config.save.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.config.all.invalidate();
|
||||
modals.close(id);
|
||||
},
|
||||
});
|
||||
|
||||
const { i18nZodResolver } = useI18nZodResolver();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name: '',
|
||||
},
|
||||
validate: i18nZodResolver(createBoardSchemaValidation),
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
const fallbackConfig = getStaticFallbackConfig(form.values.name);
|
||||
mutate({
|
||||
name: form.values.name,
|
||||
config: fallbackConfig,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<Text>{t('modals.create.text')}</Text>
|
||||
|
||||
<TextInput
|
||||
label={t('modals.create.form.name.label')}
|
||||
withAsterisk
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
|
||||
<Group grow>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.close(id);
|
||||
}}
|
||||
variant="light"
|
||||
color="gray"
|
||||
type="button"
|
||||
>
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={async () => {}}
|
||||
disabled={isLoading}
|
||||
variant="light"
|
||||
color="green"
|
||||
>
|
||||
{t('modals.create.form.submit')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const openCreateBoardModal = () => {
|
||||
modals.openContextModal({
|
||||
modal: 'createBoardModal',
|
||||
title: (
|
||||
<Title order={4}>
|
||||
<Trans i18nKey="manage/boards:modals.create.title" />
|
||||
</Title>
|
||||
),
|
||||
innerProps: {},
|
||||
});
|
||||
};
|
||||
61
src/components/Manage/Board/delete-board.modal.tsx
Normal file
61
src/components/Manage/Board/delete-board.modal.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Button, Group, Stack, Text, Title } from '@mantine/core';
|
||||
import { ContextModalProps, modals } from '@mantine/modals';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
type InnerProps = { boardName: string; onConfirm: () => Promise<void> };
|
||||
|
||||
export const DeleteBoardModal = ({ id, innerProps }: ContextModalProps<InnerProps>) => {
|
||||
const { t } = useTranslation('manage/boards');
|
||||
const utils = api.useContext();
|
||||
const { isLoading, mutateAsync } = api.config.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.config.all.invalidate();
|
||||
modals.close(id);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Text>{t('modals.delete.text')}</Text>
|
||||
|
||||
<Group grow>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.close(id);
|
||||
}}
|
||||
variant="light"
|
||||
color="gray"
|
||||
>
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
modals.close(id);
|
||||
await innerProps.onConfirm();
|
||||
await mutateAsync({
|
||||
name: innerProps.boardName,
|
||||
});
|
||||
}}
|
||||
disabled={isLoading}
|
||||
variant="light"
|
||||
color="red"
|
||||
>
|
||||
{t('common:delete')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const openDeleteBoardModal = (innerProps: InnerProps) => {
|
||||
modals.openContextModal({
|
||||
modal: 'deleteBoardModal',
|
||||
title: (
|
||||
<Title order={4}>
|
||||
<Trans i18nKey="manage/boards:modals.delete.title" />
|
||||
</Title>
|
||||
),
|
||||
innerProps,
|
||||
});
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Button, Card, Flex, TextInput } from '@mantine/core';
|
||||
import { useForm, zodResolver } from '@mantine/form';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconArrowRight, IconAt, IconUser } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { z } from 'zod';
|
||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||
|
||||
interface CreateAccountStepProps {
|
||||
nextStep: ({ eMail, username }: { username: string; eMail: string }) => void;
|
||||
@@ -10,7 +11,14 @@ interface CreateAccountStepProps {
|
||||
defaultEmail: string;
|
||||
}
|
||||
|
||||
export const CreateAccountStep = ({ defaultEmail, defaultUsername, nextStep }: CreateAccountStepProps) => {
|
||||
export const CreateAccountStep = ({
|
||||
defaultEmail,
|
||||
defaultUsername,
|
||||
nextStep,
|
||||
}: CreateAccountStepProps) => {
|
||||
const { t } = useTranslation('manage/users/create');
|
||||
|
||||
const { i18nZodResolver } = useI18nZodResolver();
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
username: defaultUsername,
|
||||
@@ -18,11 +26,9 @@ export const CreateAccountStep = ({ defaultEmail, defaultUsername, nextStep }: C
|
||||
},
|
||||
validateInputOnBlur: true,
|
||||
validateInputOnChange: true,
|
||||
validate: zodResolver(createAccountStepValidationSchema),
|
||||
validate: i18nZodResolver(createAccountStepValidationSchema),
|
||||
});
|
||||
|
||||
const { t } = useTranslation('user/create');
|
||||
|
||||
return (
|
||||
<Card mih={400}>
|
||||
<TextInput
|
||||
|
||||
112
src/components/Manage/User/Create/review-input-step.tsx
Normal file
112
src/components/Manage/User/Create/review-input-step.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Button, Card, Group, Table, Text, Title } from '@mantine/core';
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconCheck,
|
||||
IconInfoCircle,
|
||||
IconKey,
|
||||
IconMail,
|
||||
IconUser,
|
||||
} from '@tabler/icons-react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { CreateAccountSchema } from '~/pages/manage/users/create';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
type ReviewInputStepProps = {
|
||||
values: CreateAccountSchema;
|
||||
prevStep: () => void;
|
||||
nextStep: () => void;
|
||||
};
|
||||
|
||||
export const ReviewInputStep = ({ values, prevStep, nextStep }: ReviewInputStepProps) => {
|
||||
const { t } = useTranslation('manage/users/create');
|
||||
|
||||
const utils = api.useContext();
|
||||
const { mutateAsync: createAsync, isLoading } = api.user.create.useMutation({
|
||||
onSettled: () => {
|
||||
void utils.user.all.invalidate();
|
||||
},
|
||||
onSuccess: () => {
|
||||
nextStep();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<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>{values.account.username}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Group spacing="xs">
|
||||
<IconMail size="1rem" />
|
||||
<Text>{t('steps.finish.table.header.email')}</Text>
|
||||
</Group>
|
||||
</td>
|
||||
<td>
|
||||
{values.account.eMail ? (
|
||||
<Text>{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 createAsync({
|
||||
username: values.account.username,
|
||||
password: values.security.password,
|
||||
email: values.account.eMail === '' ? undefined : values.account.eMail,
|
||||
});
|
||||
}}
|
||||
loading={isLoading}
|
||||
rightIcon={<IconCheck size="1rem" />}
|
||||
variant="light"
|
||||
px="xl"
|
||||
>
|
||||
{t('buttons.confirm')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Progress,
|
||||
Text,
|
||||
} from '@mantine/core';
|
||||
import { useForm, zodResolver } from '@mantine/form';
|
||||
import { useForm } from '@mantine/form';
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
@@ -18,21 +18,22 @@ import {
|
||||
IconKey,
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { api } from '~/utils/api';
|
||||
import { passwordSchema } from '~/validations/user';
|
||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||
import { minPasswordLength, passwordSchema } from '~/validations/user';
|
||||
|
||||
const requirements = [
|
||||
{ re: /[0-9]/, label: 'Includes number' },
|
||||
{ re: /[a-z]/, label: 'Includes lowercase letter' },
|
||||
{ re: /[A-Z]/, label: 'Includes uppercase letter' },
|
||||
{ re: /[$&+,:;=?@#|'<>.^*()%!-]/, label: 'Includes special symbol' },
|
||||
{ re: /[0-9]/, label: 'number' },
|
||||
{ re: /[a-z]/, label: 'lowercase' },
|
||||
{ re: /[A-Z]/, label: 'uppercase' },
|
||||
{ re: /[$&+,:;=?@#|'<>.^*()%!-]/, label: 'special' },
|
||||
];
|
||||
|
||||
function getStrength(password: string) {
|
||||
let multiplier = password.length > 5 ? 0 : 1;
|
||||
let multiplier = password.length >= minPasswordLength ? 0 : 1;
|
||||
|
||||
requirements.forEach((requirement) => {
|
||||
if (!requirement.re.test(password)) {
|
||||
@@ -54,13 +55,16 @@ export const CreateAccountSecurityStep = ({
|
||||
nextStep,
|
||||
prevStep,
|
||||
}: CreateAccountSecurityStepProps) => {
|
||||
const { t } = useTranslation('manage/users/create');
|
||||
|
||||
const { i18nZodResolver } = useI18nZodResolver();
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
password: defaultPassword,
|
||||
},
|
||||
validateInputOnBlur: true,
|
||||
validateInputOnChange: true,
|
||||
validate: zodResolver(createAccountSecurityStepValidationSchema),
|
||||
validate: i18nZodResolver(createAccountSecurityStepValidationSchema),
|
||||
});
|
||||
|
||||
const { mutateAsync, isLoading } = api.password.generate.useMutation();
|
||||
@@ -74,8 +78,6 @@ export const CreateAccountSecurityStep = ({
|
||||
/>
|
||||
));
|
||||
|
||||
const { t } = useTranslation('user/create');
|
||||
|
||||
const strength = getStrength(form.values.password);
|
||||
const color = strength === 100 ? 'teal' : strength > 50 ? 'yellow' : 'red';
|
||||
|
||||
@@ -114,7 +116,7 @@ export const CreateAccountSecurityStep = ({
|
||||
variant="default"
|
||||
mt="xl"
|
||||
>
|
||||
{t('buttons.generateRandomPw')}
|
||||
{t('buttons.generateRandomPassword')}
|
||||
</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
@@ -122,8 +124,8 @@ export const CreateAccountSecurityStep = ({
|
||||
<Popover.Dropdown>
|
||||
<Progress color={color} value={strength} size={5} mb="xs" />
|
||||
<PasswordRequirement
|
||||
label={t('steps.security.password.requirement')}
|
||||
meets={form.values.password.length > 5}
|
||||
label="length"
|
||||
meets={form.values.password.length >= minPasswordLength}
|
||||
/>
|
||||
{checks}
|
||||
</Popover.Dropdown>
|
||||
@@ -152,6 +154,8 @@ export const CreateAccountSecurityStep = ({
|
||||
};
|
||||
|
||||
const PasswordRequirement = ({ meets, label }: { meets: boolean; label: string }) => {
|
||||
const { t } = useTranslation('manage/users/create');
|
||||
|
||||
return (
|
||||
<Text
|
||||
color={meets ? 'teal' : 'red'}
|
||||
@@ -159,7 +163,12 @@ const PasswordRequirement = ({ meets, label }: { meets: boolean; label: string }
|
||||
mt={7}
|
||||
size="sm"
|
||||
>
|
||||
{meets ? <IconCheck size="0.9rem" /> : <IconX size="0.9rem" />} <Box ml={10}>{label}</Box>
|
||||
{meets ? <IconCheck size="0.9rem" /> : <IconX size="0.9rem" />}{' '}
|
||||
<Box ml={10}>
|
||||
{t(`steps.security.password.requirements.${label}`, {
|
||||
count: minPasswordLength,
|
||||
})}
|
||||
</Box>
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
75
src/components/Manage/User/Invite/copy-invite.modal.tsx
Normal file
75
src/components/Manage/User/Invite/copy-invite.modal.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Button, CopyButton, Mark, Stack, Text, Title } from '@mantine/core';
|
||||
import { ContextModalProps, modals } from '@mantine/modals';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { RouterOutputs } from '~/utils/api';
|
||||
|
||||
type InnerProps = RouterOutputs['invites']['create'];
|
||||
|
||||
export const CopyInviteModal = ({ id, innerProps }: ContextModalProps<InnerProps>) => {
|
||||
const { t } = useTranslation('manage/users/invites');
|
||||
const inviteUrl = useInviteUrl(innerProps.id, innerProps.token);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Text>
|
||||
<Trans
|
||||
i18nKey="manage/users/invites:modals.copy.description"
|
||||
components={{
|
||||
b: <b />,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Link href={`/auth/invite/${innerProps.id}?token=${innerProps.token}`}>
|
||||
{t('modals.copy.invitationLink')}
|
||||
</Link>
|
||||
|
||||
<Stack spacing="xs">
|
||||
<Text weight="bold">{t('modals.copy.details.id')}:</Text>
|
||||
<Mark style={{ borderRadius: 4 }} color="gray" px={5}>
|
||||
{innerProps.id}
|
||||
</Mark>
|
||||
|
||||
<Text weight="bold">{t('modals.copy.details.token')}:</Text>
|
||||
<Mark style={{ borderRadius: 4 }} color="gray" px={5}>
|
||||
{innerProps.token}
|
||||
</Mark>
|
||||
</Stack>
|
||||
|
||||
<CopyButton value={inviteUrl}>
|
||||
{({ copy }) => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
copy();
|
||||
modals.close(id);
|
||||
}}
|
||||
variant="default"
|
||||
fullWidth
|
||||
>
|
||||
{t('modals.copy.button.close')}
|
||||
</Button>
|
||||
)}
|
||||
</CopyButton>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
const useInviteUrl = (id: string, token: string) => {
|
||||
const router = useRouter();
|
||||
|
||||
return `${window.location.href.replace(router.pathname, `/auth/invite/${id}?token=${token}`)}`;
|
||||
};
|
||||
|
||||
export const openCopyInviteModal = (data: InnerProps) => {
|
||||
modals.openContextModal({
|
||||
modal: 'copyInviteModal',
|
||||
title: (
|
||||
<Title order={4}>
|
||||
<Trans i18nKey="manage/users/invites:modals.copy.title" />
|
||||
</Title>
|
||||
),
|
||||
innerProps: data,
|
||||
});
|
||||
};
|
||||
89
src/components/Manage/User/Invite/create-invite.modal.tsx
Normal file
89
src/components/Manage/User/Invite/create-invite.modal.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Button, Group, Stack, Text, Title } from '@mantine/core';
|
||||
import { DateTimePicker } from '@mantine/dates';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { ContextModalProps, modals } from '@mantine/modals';
|
||||
import dayjs from 'dayjs';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { api } from '~/utils/api';
|
||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||
import { createInviteSchema } from '~/validations/invite';
|
||||
|
||||
import { openCopyInviteModal } from './copy-invite.modal';
|
||||
|
||||
export const CreateInviteModal = ({ id }: ContextModalProps<{}>) => {
|
||||
const { t } = useTranslation('manage/users/invites');
|
||||
const utils = api.useContext();
|
||||
const { isLoading, mutateAsync } = api.invites.create.useMutation({
|
||||
onSuccess: async (data) => {
|
||||
await utils.invites.all.invalidate();
|
||||
modals.close(id);
|
||||
|
||||
openCopyInviteModal(data);
|
||||
},
|
||||
});
|
||||
|
||||
const { i18nZodResolver } = useI18nZodResolver();
|
||||
|
||||
const minDate = dayjs().add(5, 'minutes').toDate();
|
||||
const maxDate = dayjs().add(6, 'months').toDate();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
expirationDate: dayjs().add(7, 'days').toDate(),
|
||||
},
|
||||
validate: i18nZodResolver(createInviteSchema),
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Text>{t('modals.create.description')}</Text>
|
||||
|
||||
<DateTimePicker
|
||||
popoverProps={{ withinPortal: true }}
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
withAsterisk
|
||||
valueFormat="DD MMM YYYY hh:mm A"
|
||||
label={t('modals.create.form.expires.label')}
|
||||
variant="filled"
|
||||
{...form.getInputProps('expirationDate')}
|
||||
/>
|
||||
|
||||
<Group grow>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.close(id);
|
||||
}}
|
||||
variant="light"
|
||||
color="gray"
|
||||
>
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
expiration: form.values.expirationDate,
|
||||
});
|
||||
}}
|
||||
disabled={isLoading}
|
||||
variant="light"
|
||||
color="green"
|
||||
>
|
||||
{t('modals.create.form.submit')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const openCreateInviteModal = () => {
|
||||
modals.openContextModal({
|
||||
modal: 'createInviteModal',
|
||||
title: (
|
||||
<Title order={4}>
|
||||
<Trans i18nKey="manage/users/invites:modals.create.title" />
|
||||
</Title>
|
||||
),
|
||||
innerProps: {},
|
||||
});
|
||||
};
|
||||
44
src/components/Manage/User/Invite/delete-invite.modal.tsx
Normal file
44
src/components/Manage/User/Invite/delete-invite.modal.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Button, Group, Stack, Text } from '@mantine/core';
|
||||
import { ContextModalProps, modals } from '@mantine/modals';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
export const DeleteInviteModal = ({ id, innerProps }: ContextModalProps<{ tokenId: string }>) => {
|
||||
const { t } = useTranslation('manage/users/invites');
|
||||
const utils = api.useContext();
|
||||
const { isLoading, mutateAsync: deleteAsync } = api.invites.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.invites.all.invalidate();
|
||||
modals.close(id);
|
||||
},
|
||||
});
|
||||
return (
|
||||
<Stack>
|
||||
<Text>{t('modals.delete.description')}</Text>
|
||||
|
||||
<Group grow>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.close(id);
|
||||
}}
|
||||
variant="light"
|
||||
color="gray"
|
||||
>
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await deleteAsync({
|
||||
tokenId: innerProps.tokenId,
|
||||
});
|
||||
}}
|
||||
disabled={isLoading}
|
||||
variant="light"
|
||||
color="red"
|
||||
>
|
||||
{t('common:delete')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
56
src/components/Manage/User/delete-user.modal.tsx
Normal file
56
src/components/Manage/User/delete-user.modal.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Button, Group, Stack, Text, Title } from '@mantine/core';
|
||||
import { ContextModalProps, modals } from '@mantine/modals';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
type InnerProps = { id: string; name: string };
|
||||
|
||||
export const DeleteUserModal = ({ id, innerProps }: ContextModalProps<InnerProps>) => {
|
||||
const { t } = useTranslation('manage/users');
|
||||
const utils = api.useContext();
|
||||
const { isLoading, mutateAsync } = api.user.deleteUser.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.user.all.invalidate();
|
||||
modals.close(id);
|
||||
},
|
||||
});
|
||||
return (
|
||||
<Stack>
|
||||
<Text>{t('modals.delete.text', innerProps)} </Text>
|
||||
|
||||
<Group grow>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.close(id);
|
||||
}}
|
||||
variant="light"
|
||||
color="gray"
|
||||
>
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await mutateAsync(innerProps);
|
||||
}}
|
||||
disabled={isLoading}
|
||||
variant="light"
|
||||
color="red"
|
||||
>
|
||||
{t('common:delete')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export const openDeleteUserModal = (user: InnerProps) => {
|
||||
modals.openContextModal({
|
||||
modal: 'deleteUserModal',
|
||||
title: (
|
||||
<Title order={4}>
|
||||
<Trans i18nKey="manage/users:modals.delete.title" values={{ name: user.name }} />
|
||||
</Title>
|
||||
),
|
||||
innerProps: user,
|
||||
});
|
||||
};
|
||||
@@ -22,17 +22,19 @@ import {
|
||||
IconLayoutDashboard,
|
||||
IconMailForward,
|
||||
IconQuestionMark,
|
||||
IconSettings2,
|
||||
IconUser,
|
||||
IconUsers,
|
||||
TablerIconsProps,
|
||||
} from '@tabler/icons-react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { ReactNode } from 'react';
|
||||
import { ReactNode, RefObject, forwardRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
|
||||
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
||||
|
||||
import { type navigation } from '../../../../public/locales/en/layout/manage.json';
|
||||
import { MainHeader } from '../header/Header';
|
||||
|
||||
interface ManageLayoutProps {
|
||||
@@ -40,7 +42,8 @@ interface ManageLayoutProps {
|
||||
}
|
||||
|
||||
export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
||||
const { attributes } = usePackageAttributesStore();
|
||||
const { t } = useTranslation('layout/manage');
|
||||
const packageVersion = usePackageAttributesStore((x) => x.attributes.packageVersion);
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const screenLargerThanMd = useScreenLargerThan('md');
|
||||
@@ -51,100 +54,19 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
||||
const data = useSession();
|
||||
const isAdmin = data.data?.user.isAdmin ?? false;
|
||||
|
||||
const navigationLinks = (
|
||||
<>
|
||||
<NavLink
|
||||
icon={
|
||||
<ThemeIcon size="md" variant="light" color="red">
|
||||
<IconHome size="1rem" />
|
||||
</ThemeIcon>
|
||||
}
|
||||
label="Home"
|
||||
component={Link}
|
||||
href="/manage/"
|
||||
/>
|
||||
<NavLink
|
||||
label="Boards"
|
||||
icon={
|
||||
<ThemeIcon size="md" variant="light" color="red">
|
||||
<IconLayoutDashboard size="1rem" />
|
||||
</ThemeIcon>
|
||||
}
|
||||
component={Link}
|
||||
href="/manage/boards"
|
||||
/>
|
||||
const navigationLinkComponents = Object.entries(navigationLinks).map(([name, navigationLink]) => {
|
||||
if (navigationLink.onlyAdmin && !isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
{isAdmin && (
|
||||
<>
|
||||
<NavLink
|
||||
label="Users"
|
||||
icon={
|
||||
<ThemeIcon size="md" variant="light" color="red">
|
||||
<IconUser size="1rem" />
|
||||
</ThemeIcon>
|
||||
}
|
||||
>
|
||||
<NavLink
|
||||
icon={<IconUsers size="1rem" />}
|
||||
label="Manage"
|
||||
component={Link}
|
||||
href="/manage/users"
|
||||
/>
|
||||
<NavLink
|
||||
icon={<IconMailForward size="1rem" />}
|
||||
label="Invites"
|
||||
component={Link}
|
||||
href="/manage/users/invites"
|
||||
/>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
label="Settings"
|
||||
icon={
|
||||
<ThemeIcon size="md" variant="light" color="red">
|
||||
<IconSettings2 size="1rem" />
|
||||
</ThemeIcon>
|
||||
}
|
||||
component={Link}
|
||||
href="/manage/settings"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<NavLink
|
||||
label="Help"
|
||||
icon={
|
||||
<ThemeIcon size="md" variant="light" color="red">
|
||||
<IconQuestionMark size="1rem" />
|
||||
</ThemeIcon>
|
||||
}
|
||||
>
|
||||
<NavLink
|
||||
icon={<IconBook2 size="1rem" />}
|
||||
component="a"
|
||||
href="https://homarr.dev/docs/about"
|
||||
label="Documentation"
|
||||
/>
|
||||
<NavLink
|
||||
icon={<IconBrandGithub size="1rem" />}
|
||||
component="a"
|
||||
href="https://github.com/ajnart/homarr/issues/new/choose"
|
||||
label="Report an issue / bug"
|
||||
/>
|
||||
<NavLink
|
||||
icon={<IconBrandDiscord size="1rem" />}
|
||||
component="a"
|
||||
href="https://discord.com/invite/aCsmEV5RgA"
|
||||
label="Community Discord"
|
||||
/>
|
||||
<NavLink
|
||||
icon={<IconGitFork size="1rem" />}
|
||||
component="a"
|
||||
href="https://github.com/ajnart/homarr"
|
||||
label="Contribute"
|
||||
/>
|
||||
</NavLink>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<CustomNavigationLink
|
||||
key={name}
|
||||
name={name as keyof typeof navigationLinks}
|
||||
navigationLink={navigationLink}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const burgerMenu = screenLargerThanMd ? undefined : (
|
||||
<Burger opened={burgerMenuOpen} onClick={toggleBurgerMenu} />
|
||||
@@ -161,7 +83,7 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
||||
navbar={
|
||||
<Navbar width={{ base: !screenLargerThanMd ? 0 : 220 }} hidden={!screenLargerThanMd}>
|
||||
<Navbar.Section pt="xs" grow>
|
||||
{navigationLinks}
|
||||
{navigationLinkComponents}
|
||||
</Navbar.Section>
|
||||
</Navbar>
|
||||
}
|
||||
@@ -174,9 +96,9 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
||||
<Text fw="bold" size={15}>
|
||||
Homarr
|
||||
</Text>
|
||||
{attributes.packageVersion && (
|
||||
{packageVersion && (
|
||||
<Text color="dimmed" size={13}>
|
||||
{attributes.packageVersion}
|
||||
{packageVersion}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
@@ -189,8 +111,126 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
||||
</Paper>
|
||||
</AppShell>
|
||||
<Drawer opened={burgerMenuOpen} onClose={closeBurgerMenu}>
|
||||
{navigationLinks}
|
||||
{navigationLinkComponents}
|
||||
</Drawer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type Icon = (props: TablerIconsProps) => JSX.Element;
|
||||
|
||||
type NavigationLinkHref = {
|
||||
icon: Icon;
|
||||
href: string;
|
||||
onlyAdmin?: boolean;
|
||||
};
|
||||
|
||||
type NavigationLinkItems<TItemsObject> = {
|
||||
icon: Icon;
|
||||
items: Record<keyof TItemsObject, NavigationLinkHref>;
|
||||
onlyAdmin?: boolean;
|
||||
};
|
||||
|
||||
type CustomNavigationLinkProps = {
|
||||
name: keyof typeof navigationLinks;
|
||||
navigationLink: (typeof navigationLinks)[keyof typeof navigationLinks];
|
||||
};
|
||||
|
||||
const CustomNavigationLink = forwardRef<
|
||||
HTMLAnchorElement | HTMLButtonElement,
|
||||
CustomNavigationLinkProps
|
||||
>(({ name, navigationLink }, ref) => {
|
||||
const { t } = useTranslation('layout/manage');
|
||||
|
||||
const commonProps = {
|
||||
label: t(`navigation.${name}.title`),
|
||||
icon: (
|
||||
<ThemeIcon size="md" variant="light" color="red">
|
||||
<navigationLink.icon size={16} />
|
||||
</ThemeIcon>
|
||||
),
|
||||
};
|
||||
|
||||
if ('href' in navigationLink) {
|
||||
return (
|
||||
<NavLink
|
||||
{...commonProps}
|
||||
ref={ref as RefObject<HTMLAnchorElement>}
|
||||
component={Link}
|
||||
href={navigationLink.href}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink {...commonProps} ref={ref as RefObject<HTMLButtonElement>}>
|
||||
{Object.entries(navigationLink.items).map(([itemName, item]) => {
|
||||
const commonItemProps = {
|
||||
label: t(`navigation.${name}.items.${itemName}`),
|
||||
icon: <item.icon size={16} />,
|
||||
href: item.href,
|
||||
};
|
||||
|
||||
if (item.href.startsWith('http')) {
|
||||
return <NavLink {...commonItemProps} component="a" />;
|
||||
}
|
||||
|
||||
return <NavLink {...commonItemProps} component={Link} />;
|
||||
})}
|
||||
</NavLink>
|
||||
);
|
||||
});
|
||||
|
||||
type NavigationLinks = {
|
||||
[key in keyof typeof navigation]: (typeof navigation)[key] extends {
|
||||
items: Record<string, string>;
|
||||
}
|
||||
? NavigationLinkItems<(typeof navigation)[key]['items']>
|
||||
: NavigationLinkHref;
|
||||
};
|
||||
|
||||
const navigationLinks: NavigationLinks = {
|
||||
home: {
|
||||
icon: IconHome,
|
||||
href: '/manage',
|
||||
},
|
||||
boards: {
|
||||
icon: IconLayoutDashboard,
|
||||
href: '/manage/boards',
|
||||
},
|
||||
users: {
|
||||
icon: IconUser,
|
||||
onlyAdmin: true,
|
||||
items: {
|
||||
manage: {
|
||||
icon: IconUsers,
|
||||
href: '/manage/users',
|
||||
},
|
||||
invites: {
|
||||
icon: IconMailForward,
|
||||
href: '/manage/users/invites',
|
||||
},
|
||||
},
|
||||
},
|
||||
help: {
|
||||
icon: IconQuestionMark,
|
||||
items: {
|
||||
documentation: {
|
||||
icon: IconBook2,
|
||||
href: 'https://homarr.dev/docs/about',
|
||||
},
|
||||
report: {
|
||||
icon: IconBrandGithub,
|
||||
href: 'https://github.com/ajnart/homarr/issues/new/choose',
|
||||
},
|
||||
discord: {
|
||||
icon: IconBrandDiscord,
|
||||
href: 'https://discord.com/invite/aCsmEV5RgA',
|
||||
},
|
||||
contribute: {
|
||||
icon: IconGitFork,
|
||||
href: 'https://github.com/ajnart/homarr',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ export const AvatarMenu = () => {
|
||||
{t('actions.avatar.defaultBoard')}
|
||||
</Menu.Item>
|
||||
<Menu.Item component={Link} href="/manage" icon={<IconHomeShare size="1rem" />}>
|
||||
Manage
|
||||
{t('actions.avatar.manage')}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
</>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { IconDownload, IconExternalLink, IconPlayerPlay } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Trans, useTranslation } from 'next-i18next';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useMemo } from 'react';
|
||||
@@ -69,8 +69,9 @@ export const MovieModal = ({ opened, closeModal }: MovieModalProps) => {
|
||||
type MovieResultsProps = Omit<z.infer<typeof queryParamsSchema>, 'movie'>;
|
||||
|
||||
const MovieResults = ({ search, type }: MovieResultsProps) => {
|
||||
const { t } = useTranslation('layout/header');
|
||||
const { name: configName } = useConfigContext();
|
||||
const { data: overseerrResults, isLoading } = api.overseerr.search.useQuery(
|
||||
const { data: movies, isLoading } = api.overseerr.search.useQuery(
|
||||
{
|
||||
query: search,
|
||||
configName: configName!,
|
||||
@@ -94,10 +95,20 @@ const MovieResults = ({ search, type }: MovieResultsProps) => {
|
||||
return (
|
||||
<Stack>
|
||||
<Text>
|
||||
Top {overseerrResults?.length} results for <b>{search}</b>
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="modals.movie.topResults"
|
||||
values={{
|
||||
count: movies?.length ?? 0,
|
||||
search,
|
||||
}}
|
||||
components={{
|
||||
b: <b />,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
<Grid gutter={32}>
|
||||
{overseerrResults?.map((result, index: number) => (
|
||||
{movies?.map((result, index: number) => (
|
||||
<Grid.Col key={index} span={12} sm={6} lg={4}>
|
||||
<MovieDisplay movie={result} type={type} />
|
||||
</Grid.Col>
|
||||
|
||||
Reference in New Issue
Block a user