mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-06 21:45:47 +01:00
♻️ Address pull request feedback
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"metaTitle": "Create Account",
|
||||
"title": "Create Account",
|
||||
"text": "Please define your credentials below",
|
||||
"form": {
|
||||
@@ -16,5 +17,19 @@
|
||||
"buttons": {
|
||||
"submit": "Create account"
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"loading": {
|
||||
"title": "Creating account",
|
||||
"text": "Please wait"
|
||||
},
|
||||
"success": {
|
||||
"title": "Account created",
|
||||
"text": "Your account has been created successfully"
|
||||
},
|
||||
"error": {
|
||||
"title": "Error",
|
||||
"text": "Something went wrong"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"metaTitle": "Login",
|
||||
"title": "Welcome back!",
|
||||
"text": "Please enter your credentials",
|
||||
"form": {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"title": "Boards",
|
||||
"cards": {
|
||||
"statistics": {
|
||||
"apps": "Apps",
|
||||
"widgets": "Widgets",
|
||||
"categories": "Categories"
|
||||
},
|
||||
"buttons": {
|
||||
"view": "View board"
|
||||
},
|
||||
"menu": {
|
||||
"setAsDefault": "Set as your default board",
|
||||
"delete": {
|
||||
"label": "Delete permanently",
|
||||
"disabled": "Deletion disabled, because older Homarr components still rely on this.",
|
||||
"modalTitle": "Delete board"
|
||||
}
|
||||
},
|
||||
"badges": {
|
||||
"fileSystem": "File system",
|
||||
"default": "Default"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"create": "Create new dashboard"
|
||||
}
|
||||
}
|
||||
@@ -35,9 +35,5 @@
|
||||
"small": "small",
|
||||
"medium": "medium",
|
||||
"large": "large"
|
||||
},
|
||||
"header": {
|
||||
"logout": "Logout",
|
||||
"sign-in": "Sign in"
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
"switchTheme": "Switch theme",
|
||||
"preferences": "User preferences",
|
||||
"defaultBoard": "Default dashboard",
|
||||
"manage": "Manage",
|
||||
"about": {
|
||||
"label": "About",
|
||||
"new": "New"
|
||||
@@ -23,5 +24,11 @@
|
||||
"logout": "Logout from {{username}}",
|
||||
"login": "Login"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"movie": {
|
||||
"title": "",
|
||||
"topResults": "Top {{count}} results for <b>{{search}}</b>."
|
||||
}
|
||||
}
|
||||
}
|
||||
26
public/locales/en/layout/manage.json
Normal file
26
public/locales/en/layout/manage.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": {
|
||||
"title": "Home"
|
||||
},
|
||||
"boards": {
|
||||
"title": "Boards"
|
||||
},
|
||||
"users": {
|
||||
"title": "Users",
|
||||
"items": {
|
||||
"manage": "Manage",
|
||||
"invites": "Invites"
|
||||
}
|
||||
},
|
||||
"help": {
|
||||
"title": "Help",
|
||||
"items": {
|
||||
"documentation": "Documentation",
|
||||
"report": "Report an issue / bug",
|
||||
"discord": "Community Discord",
|
||||
"contribute": "Contribute"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
public/locales/en/manage/boards.json
Normal file
44
public/locales/en/manage/boards.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"metaTitle": "Boards",
|
||||
"pageTitle": "Boards",
|
||||
"cards": {
|
||||
"statistics": {
|
||||
"apps": "Apps",
|
||||
"widgets": "Widgets",
|
||||
"categories": "Categories"
|
||||
},
|
||||
"buttons": {
|
||||
"view": "View board"
|
||||
},
|
||||
"menu": {
|
||||
"setAsDefault": "Set as your default board",
|
||||
"delete": {
|
||||
"label": "Delete permanently",
|
||||
"disabled": "Deletion disabled, because older Homarr components still rely on this."
|
||||
}
|
||||
},
|
||||
"badges": {
|
||||
"fileSystem": "File system",
|
||||
"default": "Default"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"create": "Create new board"
|
||||
},
|
||||
"modals": {
|
||||
"delete": {
|
||||
"title": "Delete board",
|
||||
"text": "Are you sure, that you want to delete this board? This action cannot be undone and your data will be lost permanently."
|
||||
},
|
||||
"create": {
|
||||
"title": "Create board",
|
||||
"text": "The name cannot be changed after a board has been created.",
|
||||
"form": {
|
||||
"name": {
|
||||
"label": "Name"
|
||||
},
|
||||
"submit": "Create"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
public/locales/en/manage/index.json
Normal file
23
public/locales/en/manage/index.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"metaTitle": "Manage",
|
||||
"hero": {
|
||||
"title": "Welcome back, {{username}}",
|
||||
"fallbackUsername": "Anonymous",
|
||||
"subtitle": "Welcome to Your Application Hub. Organize, Optimize and Conquer!"
|
||||
},
|
||||
"quickActions": {
|
||||
"title": "Quick actions",
|
||||
"boards": {
|
||||
"title": "Your boards",
|
||||
"subtitle": "Create and manage your boards"
|
||||
},
|
||||
"inviteUsers": {
|
||||
"title": "Invite a new user",
|
||||
"subtitle": "Create and send an invitation for registration"
|
||||
},
|
||||
"manageUsers": {
|
||||
"title": "Manage users",
|
||||
"subtitle": "Delete and manage your users"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,10 @@
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"delete": "Delete user {{name}}"
|
||||
"delete": {
|
||||
"title": "Delete user {{name}}",
|
||||
"text": "Are you sure, that you want to delete the user {{name}}? This will delete data associated with this account, but not any created dashboards by this user."
|
||||
}
|
||||
},
|
||||
"searchDoesntMatch": "Your search does not match any entries. Please adjust your filter."
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"metaTitle": "Create user",
|
||||
"steps": {
|
||||
"account": {
|
||||
"title": "First step",
|
||||
@@ -15,7 +16,13 @@
|
||||
"text": "Password",
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"requirement": "Includes at least 6 characters"
|
||||
"requirements": {
|
||||
"number": "Includes number",
|
||||
"lowercase": "Includes lowercase letter",
|
||||
"uppercase": "Includes uppercase letter",
|
||||
"special": "Includes special character",
|
||||
"length": "Includes at least {{count}} characters"
|
||||
}
|
||||
}
|
||||
},
|
||||
"finish": {
|
||||
@@ -35,15 +42,20 @@
|
||||
},
|
||||
"notSet": "Not set",
|
||||
"valid": "Valid"
|
||||
}
|
||||
},
|
||||
"alertConfirmed": "User has been created in the database. They can now log in."
|
||||
"completed": {
|
||||
"alert": {
|
||||
"title": "User was created",
|
||||
"text": "The user was created in the database. They can now log in."
|
||||
}
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"next": "Next",
|
||||
"previous": "Previous",
|
||||
"confirm": "Confirm",
|
||||
"generateRandomPw": "Generate random",
|
||||
"generateRandomPassword": "Generate random",
|
||||
"createAnother": "Create another",
|
||||
"goBack": "Go back to users"
|
||||
}
|
||||
50
public/locales/en/manage/users/invites.json
Normal file
50
public/locales/en/manage/users/invites.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"metaTitle": "User invites",
|
||||
"pageTitle": "Manage user invites",
|
||||
"description": "Using invites, you can invite users to your Homarr instance. An invitation will only be valid for a certain time-span and can be used once. The expiration must be between 5 minutes and 12 months upon creation.",
|
||||
"button": {
|
||||
"createInvite": "Create invitation",
|
||||
"deleteInvite": "Delete invite"
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"id": "ID",
|
||||
"creator": "Creator",
|
||||
"expires": "Expires",
|
||||
"action": "Actions"
|
||||
},
|
||||
"data": {
|
||||
"expiresAt": "expired {{at}}",
|
||||
"expiresIn": "in {{in}}"
|
||||
}
|
||||
},
|
||||
"modals": {
|
||||
"create": {
|
||||
"title": "Create invite",
|
||||
"description": "After the expiration, an invite will no longer be valid and the recipient of the invite won't be able to create an account.",
|
||||
"form": {
|
||||
"expires": {
|
||||
"label": "Expiration date"
|
||||
},
|
||||
"submit": "Create"
|
||||
}
|
||||
},
|
||||
"copy": {
|
||||
"title": "Copy invitation",
|
||||
"description": "Your invitation has been generated. After this modal closes, <b>you'll not be able to copy this link anymore</b>. If you do no longer wish to invite said person, you can delete this invitation any time.",
|
||||
"invitationLink": "Invitation link",
|
||||
"details": {
|
||||
"id": "ID",
|
||||
"token": "Token"
|
||||
},
|
||||
"button": {
|
||||
"close": "Copy & Dismiss"
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"title": "Delete invite",
|
||||
"description": "Are you sure, that you want to delete this invitation? Users with this link will no longer be able to create an account using that link."
|
||||
}
|
||||
},
|
||||
"noInvites": "There are no invitations yet."
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"title": "Manage user invites",
|
||||
"text": "Using invites, you can invite users to your Homarr instance. An invitation will only be valid for a certain time-span and can be used once. The expiration must be between 5 minutes and 12 months upon creation.",
|
||||
"button": {
|
||||
"createInvite": "Create invitation",
|
||||
"deleteInvite": "Delete invite"
|
||||
},
|
||||
"table": {
|
||||
"header": {
|
||||
"id": "ID",
|
||||
"creator": "Creator",
|
||||
"expires": "Expires",
|
||||
"action": "Actions"
|
||||
},
|
||||
"data": {
|
||||
"expiresAt": "expired {{at}}",
|
||||
"expiresIn": "in {{in}}"
|
||||
}
|
||||
},
|
||||
"noInvites": "There are no invitations yet."
|
||||
}
|
||||
@@ -27,6 +27,10 @@
|
||||
},
|
||||
"population": {
|
||||
"fallback": "Unknown"
|
||||
},
|
||||
"nothingFound": {
|
||||
"title": "Nothing found",
|
||||
"description": "Please try another search term"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
"number": "This field must be greater than or equal to {{minimum}}"
|
||||
},
|
||||
"too_big": {
|
||||
"string": "This field must be at most {{minimum}} characters long",
|
||||
"number": "This field must be less than or equal to {{minimum}}"
|
||||
"string": "This field must be at most {{maximum}} characters long",
|
||||
"number": "This field must be less than or equal to {{maximum}}"
|
||||
},
|
||||
"custom": {
|
||||
"password_match": "Passwords must match"
|
||||
"passwordMatch": "Passwords must match"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { Button, Group, Stack, Text, TextInput } from '@mantine/core';
|
||||
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 { createDashboardSchemaValidation } from '~/validations/dashboards';
|
||||
import { createBoardSchemaValidation } from '~/validations/boards';
|
||||
|
||||
export const CreateDashboardModal = ({ context, id, innerProps }: ContextModalProps<{}>) => {
|
||||
const apiContext = api.useContext();
|
||||
export const CreateBoardModal = ({ id }: ContextModalProps<{}>) => {
|
||||
const { t } = useTranslation('manage/boards');
|
||||
const utils = api.useContext();
|
||||
const { isLoading, mutate } = api.config.save.useMutation({
|
||||
onSuccess: async () => {
|
||||
await apiContext.config.all.invalidate();
|
||||
await utils.config.all.invalidate();
|
||||
modals.close(id);
|
||||
},
|
||||
});
|
||||
@@ -21,7 +23,7 @@ export const CreateDashboardModal = ({ context, id, innerProps }: ContextModalPr
|
||||
initialValues: {
|
||||
name: '',
|
||||
},
|
||||
validate: i18nZodResolver(createDashboardSchemaValidation),
|
||||
validate: i18nZodResolver(createBoardSchemaValidation),
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
@@ -35,9 +37,13 @@ export const CreateDashboardModal = ({ context, id, innerProps }: ContextModalPr
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack>
|
||||
<Text>A name cannot be changed after a dashboard has been created.</Text>
|
||||
<Text>{t('modals.create.text')}</Text>
|
||||
|
||||
<TextInput label="Name" withAsterisk {...form.getInputProps('name')} />
|
||||
<TextInput
|
||||
label={t('modals.create.form.name.label')}
|
||||
withAsterisk
|
||||
{...form.getInputProps('name')}
|
||||
/>
|
||||
|
||||
<Group grow>
|
||||
<Button
|
||||
@@ -48,7 +54,7 @@ export const CreateDashboardModal = ({ context, id, innerProps }: ContextModalPr
|
||||
color="gray"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -57,10 +63,22 @@ export const CreateDashboardModal = ({ context, id, innerProps }: ContextModalPr
|
||||
variant="light"
|
||||
color="green"
|
||||
>
|
||||
Create
|
||||
{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: {},
|
||||
});
|
||||
};
|
||||
@@ -1,26 +1,23 @@
|
||||
import { Button, Group, Stack, Text } from '@mantine/core';
|
||||
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';
|
||||
|
||||
export const DeleteBoardModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<{ boardName: string; onConfirm: () => Promise<void> }>) => {
|
||||
const apiContext = api.useContext();
|
||||
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 apiContext.config.all.invalidate();
|
||||
await utils.config.all.invalidate();
|
||||
modals.close(id);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Text>
|
||||
Are you sure, that you want to delete this board? This action cannot be undone and your data
|
||||
will be lost permanently.
|
||||
</Text>
|
||||
<Text>{t('modals.delete.text')}</Text>
|
||||
|
||||
<Group grow>
|
||||
<Button
|
||||
@@ -30,7 +27,7 @@ export const DeleteBoardModal = ({
|
||||
variant="light"
|
||||
color="gray"
|
||||
>
|
||||
Cancel
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
@@ -44,9 +41,21 @@ export const DeleteBoardModal = ({
|
||||
variant="light"
|
||||
color="red"
|
||||
>
|
||||
Delete
|
||||
{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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
import { Button, CopyButton, Mark, Stack, Text } from '@mantine/core';
|
||||
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';
|
||||
|
||||
export const CopyInviteModal = ({
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<{ id: string; token: string; expire: Date }>) => {
|
||||
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>
|
||||
Your invitation has been generated. After this modal closes,{' '}
|
||||
<b>you'll not be able to copy this link anymore</b>. If you do no longer wish to invite said
|
||||
person, you can delete this invitation any time.
|
||||
<Trans
|
||||
i18nKey="manage/users/invites:modals.copy.description"
|
||||
components={{
|
||||
b: <b />,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Link href={`/auth/invite/${innerProps.id}?token=${innerProps.token}`}>Invitation link</Link>
|
||||
<Link href={`/auth/invite/${innerProps.id}?token=${innerProps.token}`}>
|
||||
{t('modals.copy.invitationLink')}
|
||||
</Link>
|
||||
|
||||
<Stack spacing="xs">
|
||||
<Text weight="bold">ID:</Text>
|
||||
<Text weight="bold">{t('modals.copy.details.id')}:</Text>
|
||||
<Mark style={{ borderRadius: 4 }} color="gray" px={5}>
|
||||
{innerProps.id}
|
||||
</Mark>
|
||||
|
||||
<Text weight="bold">Token:</Text>
|
||||
<Text weight="bold">{t('modals.copy.details.token')}:</Text>
|
||||
<Mark style={{ borderRadius: 4 }} color="gray" px={5}>
|
||||
{innerProps.token}
|
||||
</Mark>
|
||||
@@ -41,7 +48,7 @@ export const CopyInviteModal = ({
|
||||
variant="default"
|
||||
fullWidth
|
||||
>
|
||||
Copy & Dismiss
|
||||
{t('modals.copy.button.close')}
|
||||
</Button>
|
||||
)}
|
||||
</CopyButton>
|
||||
@@ -54,3 +61,15 @@ const useInviteUrl = (id: string, token: string) => {
|
||||
|
||||
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,
|
||||
});
|
||||
};
|
||||
@@ -1,24 +1,24 @@
|
||||
import { Button, Group, Stack, Text } from '@mantine/core';
|
||||
import { DateInput, DateTimePicker } from '@mantine/dates';
|
||||
import { useForm, zodResolver } from '@mantine/form';
|
||||
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 apiContext = api.useContext();
|
||||
const { t } = useTranslation('manage/users/invites');
|
||||
const utils = api.useContext();
|
||||
const { isLoading, mutateAsync } = api.invites.create.useMutation({
|
||||
onSuccess: async (data) => {
|
||||
await apiContext.invites.all.invalidate();
|
||||
await utils.invites.all.invalidate();
|
||||
modals.close(id);
|
||||
|
||||
modals.openContextModal({
|
||||
modal: 'copyInviteModal',
|
||||
title: <Text weight="bold">Copy invitation</Text>,
|
||||
innerProps: data,
|
||||
});
|
||||
openCopyInviteModal(data);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -36,10 +36,7 @@ export const CreateInviteModal = ({ id }: ContextModalProps<{}>) => {
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Text>
|
||||
After the expiration, an invite will no longer be valid and the recipient of the invite
|
||||
won't be able to create an account.
|
||||
</Text>
|
||||
<Text>{t('modals.create.description')}</Text>
|
||||
|
||||
<DateTimePicker
|
||||
popoverProps={{ withinPortal: true }}
|
||||
@@ -47,7 +44,7 @@ export const CreateInviteModal = ({ id }: ContextModalProps<{}>) => {
|
||||
maxDate={maxDate}
|
||||
withAsterisk
|
||||
valueFormat="DD MMM YYYY hh:mm A"
|
||||
label="Expiration date"
|
||||
label={t('modals.create.form.expires.label')}
|
||||
variant="filled"
|
||||
{...form.getInputProps('expirationDate')}
|
||||
/>
|
||||
@@ -60,7 +57,7 @@ export const CreateInviteModal = ({ id }: ContextModalProps<{}>) => {
|
||||
variant="light"
|
||||
color="gray"
|
||||
>
|
||||
Cancel
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
@@ -72,9 +69,21 @@ export const CreateInviteModal = ({ id }: ContextModalProps<{}>) => {
|
||||
variant="light"
|
||||
color="green"
|
||||
>
|
||||
Create
|
||||
{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: {},
|
||||
});
|
||||
};
|
||||
@@ -1,25 +1,20 @@
|
||||
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 = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<{ tokenId: string }>) => {
|
||||
const apiContext = api.useContext();
|
||||
const { isLoading, mutateAsync } = api.invites.delete.useMutation({
|
||||
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 apiContext.invites.all.invalidate();
|
||||
await utils.invites.all.invalidate();
|
||||
modals.close(id);
|
||||
},
|
||||
});
|
||||
return (
|
||||
<Stack>
|
||||
<Text>
|
||||
Are you sure, that you want to delete this invitation? Users with this link will no longer
|
||||
be able to create an account using that link.
|
||||
</Text>
|
||||
<Text>{t('modals.delete.description')}</Text>
|
||||
|
||||
<Group grow>
|
||||
<Button
|
||||
@@ -29,11 +24,11 @@ export const DeleteInviteModal = ({
|
||||
variant="light"
|
||||
color="gray"
|
||||
>
|
||||
Cancel
|
||||
{t('common:cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
await deleteAsync({
|
||||
tokenId: innerProps.tokenId,
|
||||
});
|
||||
}}
|
||||
@@ -41,7 +36,7 @@ export const DeleteInviteModal = ({
|
||||
variant="light"
|
||||
color="red"
|
||||
>
|
||||
Delete
|
||||
{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>
|
||||
const navigationLinkComponents = Object.entries(navigationLinks).map(([name, navigationLink]) => {
|
||||
if (navigationLink.onlyAdmin && !isAdmin) {
|
||||
return null;
|
||||
}
|
||||
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"
|
||||
/>
|
||||
|
||||
{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"
|
||||
return (
|
||||
<CustomNavigationLink
|
||||
key={name}
|
||||
name={name as keyof typeof navigationLinks}
|
||||
navigationLink={navigationLink}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
@@ -21,7 +21,7 @@ const env = createEnv({
|
||||
process.env.VERCEL ? z.string().min(1) : z.string().url()
|
||||
),
|
||||
DOCKER_HOST: z.string().optional(),
|
||||
DOCKER_PORT: z.string().regex(/\d+/).transform(Number).optional(),
|
||||
DOCKER_PORT: portSchema,
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,12 +6,12 @@ import { WidgetsEditModal } from '~/components/Dashboard/Tiles/Widgets/WidgetsEd
|
||||
import { WidgetsRemoveModal } from '~/components/Dashboard/Tiles/Widgets/WidgetsRemoveModal';
|
||||
import { CategoryEditModal } from '~/components/Dashboard/Wrappers/Category/CategoryEditModal';
|
||||
|
||||
import { CopyInviteModal } from './copy-invite/copy-invite.modal';
|
||||
import { CreateDashboardModal } from './create-dashboard/create-dashboard.modal';
|
||||
import { CreateInviteModal } from './create-invite/create-invite.modal';
|
||||
import { DeleteBoardModal } from './delete-board/delete-board.modal';
|
||||
import { DeleteInviteModal } from './delete-invite/delete-invite.modal';
|
||||
import { DeleteUserModal } from './delete-user/delete-user.modal';
|
||||
import { CreateBoardModal } from './components/Manage/Board/create-board.modal';
|
||||
import { DeleteBoardModal } from './components/Manage/Board/delete-board.modal';
|
||||
import { CopyInviteModal } from './components/Manage/User/Invite/copy-invite.modal';
|
||||
import { CreateInviteModal } from './components/Manage/User/Invite/create-invite.modal';
|
||||
import { DeleteInviteModal } from './components/Manage/User/Invite/delete-invite.modal';
|
||||
import { DeleteUserModal } from './components/Manage/User/delete-user.modal';
|
||||
|
||||
export const modals = {
|
||||
editApp: EditAppModal,
|
||||
@@ -24,7 +24,7 @@ export const modals = {
|
||||
deleteUserModal: DeleteUserModal,
|
||||
createInviteModal: CreateInviteModal,
|
||||
deleteInviteModal: DeleteInviteModal,
|
||||
createDashboardModal: CreateDashboardModal,
|
||||
createBoardModal: CreateBoardModal,
|
||||
copyInviteModal: CopyInviteModal,
|
||||
deleteBoardModal: DeleteBoardModal,
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Button, Group, Stack, Text } from '@mantine/core';
|
||||
import { ContextModalProps, modals } from '@mantine/modals';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
export const DeleteUserModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<{ userId: string; username: string }>) => {
|
||||
const apiContext = api.useContext();
|
||||
const { isLoading, mutateAsync } = api.user.deleteUser.useMutation({
|
||||
onSuccess: async () => {
|
||||
await apiContext.user.all.invalidate();
|
||||
modals.close(id);
|
||||
},
|
||||
});
|
||||
return (
|
||||
<Stack>
|
||||
<Text>
|
||||
Are you sure, that you want to delete the user {innerProps.username}? This will delete data
|
||||
associated with this account, but not any created dashboards by this user.
|
||||
</Text>
|
||||
|
||||
<Group grow>
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.close(id);
|
||||
}}
|
||||
variant="light"
|
||||
color="gray"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
userId: innerProps.userId,
|
||||
});
|
||||
}}
|
||||
disabled={isLoading}
|
||||
variant="light"
|
||||
color="red"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -19,7 +19,7 @@ export default function DockerMenuButton(props: any) {
|
||||
|
||||
const dockerEnabled = config?.settings.customization.layout.enabledDocker || false;
|
||||
|
||||
const { data, refetch } = api.docker.containers.useQuery(undefined, {
|
||||
const { data, refetch, isLoading } = api.docker.containers.useQuery(undefined, {
|
||||
enabled: dockerEnabled,
|
||||
});
|
||||
useHotkeys([['mod+B', () => setOpened(!opened)]]);
|
||||
@@ -42,7 +42,7 @@ export default function DockerMenuButton(props: any) {
|
||||
padding="xl"
|
||||
position="right"
|
||||
size="100%"
|
||||
title={<ContainerActionBar isLoading selected={selection} reload={reload} />}
|
||||
title={<ContainerActionBar isLoading={isLoading} selected={selection} reload={reload} />}
|
||||
transitionProps={{
|
||||
transition: 'pop',
|
||||
}}
|
||||
|
||||
@@ -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,7 +65,14 @@ export default function AuthInvitePage() {
|
||||
);
|
||||
};
|
||||
|
||||
const metaTitle = `${t('metaTitle')} • Homarr`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{metaTitle}</title>
|
||||
</Head>
|
||||
|
||||
<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}>
|
||||
@@ -105,6 +113,7 @@ export default function AuthInvitePage() {
|
||||
</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,11 +52,15 @@ 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>
|
||||
|
||||
<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')}
|
||||
@@ -97,6 +99,7 @@ export default function LoginPage() {
|
||||
</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,10 +163,7 @@ const BoardsPage = () => {
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
onClick={async () => {
|
||||
modals.openContextModal({
|
||||
modal: 'deleteBoardModal',
|
||||
title: <Text weight={500}>{t('cards.menu.delete.modalTitle')}</Text>,
|
||||
innerProps: {
|
||||
openDeleteBoardModal({
|
||||
boardName: board.name,
|
||||
onConfirm: async () => {
|
||||
append(board.name);
|
||||
@@ -178,7 +171,6 @@ const BoardsPage = () => {
|
||||
await sleep(500);
|
||||
filter((item, _) => item !== board.name);
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
disabled={board.name === 'default'}
|
||||
@@ -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,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
||||
import { configExists } from '~/tools/config/configExists';
|
||||
import { getFrontendConfig } from '~/tools/config/getFrontendConfig';
|
||||
import { BackendConfigType, ConfigType } from '~/types/config';
|
||||
import { boardCustomizationSchema } from '~/validations/dashboards';
|
||||
import { boardCustomizationSchema } from '~/validations/boards';
|
||||
import { IRssWidget } from '~/widgets/rss/RssWidgetTile';
|
||||
|
||||
import { getConfig } from '../../../tools/config/getConfig';
|
||||
|
||||
@@ -28,7 +28,7 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await createUserInNotExist(ctx, input, {
|
||||
await createUserIfNotPresent(ctx, input, {
|
||||
defaultSettings: {
|
||||
colorScheme: colorSchemeParser.parse(ctx.cookies[COOKIE_COLOR_SCHEME_KEY]),
|
||||
language: ctx.cookies[COOKIE_LOCALE_KEY] ?? 'en',
|
||||
@@ -62,7 +62,7 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await createUserInNotExist(ctx, input, {
|
||||
await createUserIfNotPresent(ctx, input, {
|
||||
defaultSettings: {
|
||||
colorScheme: colorSchemeParser.parse(ctx.cookies[COOKIE_COLOR_SCHEME_KEY]),
|
||||
language: ctx.cookies[COOKIE_LOCALE_KEY] ?? 'en',
|
||||
@@ -235,7 +235,7 @@ export const userRouter = createTRPCRouter({
|
||||
return {
|
||||
users: users.map((user) => ({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
name: user.name!,
|
||||
email: user.email,
|
||||
emailVerified: user.emailVerified,
|
||||
})),
|
||||
@@ -243,25 +243,25 @@ export const userRouter = createTRPCRouter({
|
||||
};
|
||||
}),
|
||||
create: adminProcedure.input(createNewUserSchema).mutation(async ({ ctx, input }) => {
|
||||
await createUserInNotExist(ctx, input);
|
||||
await createUserIfNotPresent(ctx, input);
|
||||
}),
|
||||
|
||||
deleteUser: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
id: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
await ctx.prisma.user.delete({
|
||||
where: {
|
||||
id: input.userId,
|
||||
id: input.id,
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
const createUserInNotExist = async (
|
||||
const createUserIfNotPresent = async (
|
||||
ctx: TRPCContext,
|
||||
input: z.infer<typeof createNewUserSchema>,
|
||||
options: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { PrismaAdapter } from '@next-auth/prisma-adapter';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import Consola from 'consola';
|
||||
import Cookies from 'cookies';
|
||||
import { type GetServerSidePropsContext, type NextApiRequest, type NextApiResponse } from 'next';
|
||||
import { type DefaultSession, type NextAuthOptions, getServerSession } from 'next-auth';
|
||||
@@ -150,15 +151,15 @@ export const constructAuthOptions = (
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`user ${user.id} is trying to log in. checking password...`);
|
||||
Consola.log(`user ${user.id} is trying to log in. checking password...`);
|
||||
const isValidPassword = await bcrypt.compare(data.password, user.password);
|
||||
|
||||
if (!isValidPassword) {
|
||||
console.log(`password for user ${user.id} was incorrect`);
|
||||
Consola.log(`password for user ${user.id} was incorrect`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`user ${user.id} successfully authorized`);
|
||||
Consola.log(`user ${user.id} successfully authorized`);
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
export const boardNamespaces = [
|
||||
'common',
|
||||
'zod',
|
||||
'layout/element-selector/selector',
|
||||
'layout/modals/add-app',
|
||||
'layout/modals/change-position',
|
||||
'layout/modals/about',
|
||||
'layout/common',
|
||||
'layout/header/actions/toggle-edit-mode',
|
||||
'layout/mobile/drawer',
|
||||
@@ -42,16 +39,9 @@ export const boardNamespaces = [
|
||||
];
|
||||
|
||||
export const manageNamespaces = [
|
||||
'user/preferences',
|
||||
'user/manage',
|
||||
'user/invites',
|
||||
'user/create',
|
||||
'boards/manage',
|
||||
'zod',
|
||||
'manage/common',
|
||||
'manage/boards',
|
||||
'manage/users',
|
||||
'manage/users/invites',
|
||||
'manage/users/create',
|
||||
];
|
||||
|
||||
export const loginNamespaces = ['authentication/login', 'zod'];
|
||||
|
||||
export const inviteNamespaces = ['authentication/invite', 'zod'];
|
||||
|
||||
export const onboardNamespaces = ['common', 'zod'];
|
||||
|
||||
3
src/types/helpers.ts
Normal file
3
src/types/helpers.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type OnlyKeysWithStructure<T, TStructure> = {
|
||||
[P in keyof T]: T[P] extends TStructure ? P : never;
|
||||
}[keyof T];
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DEFAULT_THEME, MANTINE_COLORS, MantineColor } from '@mantine/core';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const createDashboardSchemaValidation = z.object({
|
||||
export const createBoardSchemaValidation = z.object({
|
||||
name: z.string().min(2).max(25),
|
||||
});
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
import { CustomErrorParams } from '~/utils/i18n-zod-resolver';
|
||||
|
||||
export const minPasswordLength = 8;
|
||||
|
||||
export const passwordSchema = z
|
||||
.string()
|
||||
.min(8)
|
||||
.min(minPasswordLength)
|
||||
.max(100)
|
||||
.refine((value) => /[0-9]/.test(value))
|
||||
.refine((value) => /[a-z]/.test(value))
|
||||
@@ -18,12 +20,12 @@ export const signInSchema = z.object({
|
||||
export const signUpFormSchema = z
|
||||
.object({
|
||||
username: z.string().min(3),
|
||||
password: z.string().min(8),
|
||||
passwordConfirmation: z.string().min(8),
|
||||
password: z.string().min(minPasswordLength),
|
||||
passwordConfirmation: z.string().min(minPasswordLength),
|
||||
})
|
||||
.refine((data) => data.password === data.passwordConfirmation, {
|
||||
params: {
|
||||
i18n: { key: 'password_match' },
|
||||
i18n: { key: 'passwordMatch' },
|
||||
} satisfies CustomErrorParams,
|
||||
path: ['passwordConfirmation'],
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
IconSun,
|
||||
} from '@tabler/icons-react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { CustomTypeOptions } from 'i18next';
|
||||
|
||||
interface WeatherIconProps {
|
||||
code: number;
|
||||
|
||||
Reference in New Issue
Block a user