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",
|
"title": "Create Account",
|
||||||
"text": "Please define your credentials below",
|
"text": "Please define your credentials below",
|
||||||
"form": {
|
"form": {
|
||||||
@@ -16,5 +17,19 @@
|
|||||||
"buttons": {
|
"buttons": {
|
||||||
"submit": "Create account"
|
"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!",
|
"title": "Welcome back!",
|
||||||
"text": "Please enter your credentials",
|
"text": "Please enter your credentials",
|
||||||
"form": {
|
"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",
|
"small": "small",
|
||||||
"medium": "medium",
|
"medium": "medium",
|
||||||
"large": "large"
|
"large": "large"
|
||||||
},
|
|
||||||
"header": {
|
|
||||||
"logout": "Logout",
|
|
||||||
"sign-in": "Sign in"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"switchTheme": "Switch theme",
|
"switchTheme": "Switch theme",
|
||||||
"preferences": "User preferences",
|
"preferences": "User preferences",
|
||||||
"defaultBoard": "Default dashboard",
|
"defaultBoard": "Default dashboard",
|
||||||
|
"manage": "Manage",
|
||||||
"about": {
|
"about": {
|
||||||
"label": "About",
|
"label": "About",
|
||||||
"new": "New"
|
"new": "New"
|
||||||
@@ -23,5 +24,11 @@
|
|||||||
"logout": "Logout from {{username}}",
|
"logout": "Logout from {{username}}",
|
||||||
"login": "Login"
|
"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": {
|
"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."
|
"searchDoesntMatch": "Your search does not match any entries. Please adjust your filter."
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"metaTitle": "Create user",
|
||||||
"steps": {
|
"steps": {
|
||||||
"account": {
|
"account": {
|
||||||
"title": "First step",
|
"title": "First step",
|
||||||
@@ -15,7 +16,13 @@
|
|||||||
"text": "Password",
|
"text": "Password",
|
||||||
"password": {
|
"password": {
|
||||||
"label": "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": {
|
"finish": {
|
||||||
@@ -35,15 +42,20 @@
|
|||||||
},
|
},
|
||||||
"notSet": "Not set",
|
"notSet": "Not set",
|
||||||
"valid": "Valid"
|
"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": {
|
"buttons": {
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"generateRandomPw": "Generate random",
|
"generateRandomPassword": "Generate random",
|
||||||
"createAnother": "Create another",
|
"createAnother": "Create another",
|
||||||
"goBack": "Go back to users"
|
"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": {
|
"population": {
|
||||||
"fallback": "Unknown"
|
"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}}"
|
"number": "This field must be greater than or equal to {{minimum}}"
|
||||||
},
|
},
|
||||||
"too_big": {
|
"too_big": {
|
||||||
"string": "This field must be at most {{minimum}} characters long",
|
"string": "This field must be at most {{maximum}} characters long",
|
||||||
"number": "This field must be less than or equal to {{minimum}}"
|
"number": "This field must be less than or equal to {{maximum}}"
|
||||||
},
|
},
|
||||||
"custom": {
|
"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 { DEFAULT_THEME, MANTINE_COLORS, MantineColor } from '@mantine/core';
|
||||||
import { createFormContext } from '@mantine/form';
|
import { createFormContext } from '@mantine/form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { boardCustomizationSchema } from '~/validations/dashboards';
|
import { boardCustomizationSchema } from '~/validations/boards';
|
||||||
|
|
||||||
export const [
|
export const [
|
||||||
BoardCustomizationFormProvider,
|
BoardCustomizationFormProvider,
|
||||||
|
|||||||
@@ -168,8 +168,8 @@ const CitySelectModal = ({ opened, closeModal, query, onCitySelected }: CitySele
|
|||||||
<Center>
|
<Center>
|
||||||
<Stack align="center">
|
<Stack align="center">
|
||||||
<IconAlertTriangle />
|
<IconAlertTriangle />
|
||||||
<Title order={6}>Nothing found</Title>
|
<Title order={6}>{t('modal.table.nothingFound.title')}</Title>
|
||||||
<Text>Nothing was found, please try again</Text>
|
<Text>{t('modal.table.nothingFound.description')}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Center>
|
</Center>
|
||||||
</Modal>
|
</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 { useForm } from '@mantine/form';
|
||||||
import { ContextModalProps, modals } from '@mantine/modals';
|
import { ContextModalProps, modals } from '@mantine/modals';
|
||||||
|
import { Trans, useTranslation } from 'next-i18next';
|
||||||
import { getStaticFallbackConfig } from '~/tools/config/getFallbackConfig';
|
import { getStaticFallbackConfig } from '~/tools/config/getFallbackConfig';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||||
import { createDashboardSchemaValidation } from '~/validations/dashboards';
|
import { createBoardSchemaValidation } from '~/validations/boards';
|
||||||
|
|
||||||
export const CreateDashboardModal = ({ context, id, innerProps }: ContextModalProps<{}>) => {
|
export const CreateBoardModal = ({ id }: ContextModalProps<{}>) => {
|
||||||
const apiContext = api.useContext();
|
const { t } = useTranslation('manage/boards');
|
||||||
|
const utils = api.useContext();
|
||||||
const { isLoading, mutate } = api.config.save.useMutation({
|
const { isLoading, mutate } = api.config.save.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await apiContext.config.all.invalidate();
|
await utils.config.all.invalidate();
|
||||||
modals.close(id);
|
modals.close(id);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -21,7 +23,7 @@ export const CreateDashboardModal = ({ context, id, innerProps }: ContextModalPr
|
|||||||
initialValues: {
|
initialValues: {
|
||||||
name: '',
|
name: '',
|
||||||
},
|
},
|
||||||
validate: i18nZodResolver(createDashboardSchemaValidation),
|
validate: i18nZodResolver(createBoardSchemaValidation),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
@@ -35,9 +37,13 @@ export const CreateDashboardModal = ({ context, id, innerProps }: ContextModalPr
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
<Stack>
|
<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>
|
<Group grow>
|
||||||
<Button
|
<Button
|
||||||
@@ -48,7 +54,7 @@ export const CreateDashboardModal = ({ context, id, innerProps }: ContextModalPr
|
|||||||
color="gray"
|
color="gray"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Cancel
|
{t('common:cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -57,10 +63,22 @@ export const CreateDashboardModal = ({ context, id, innerProps }: ContextModalPr
|
|||||||
variant="light"
|
variant="light"
|
||||||
color="green"
|
color="green"
|
||||||
>
|
>
|
||||||
Create
|
{t('modals.create.form.submit')}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</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 { ContextModalProps, modals } from '@mantine/modals';
|
||||||
|
import { Trans, useTranslation } from 'next-i18next';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
export const DeleteBoardModal = ({
|
type InnerProps = { boardName: string; onConfirm: () => Promise<void> };
|
||||||
context,
|
|
||||||
id,
|
export const DeleteBoardModal = ({ id, innerProps }: ContextModalProps<InnerProps>) => {
|
||||||
innerProps,
|
const { t } = useTranslation('manage/boards');
|
||||||
}: ContextModalProps<{ boardName: string; onConfirm: () => Promise<void> }>) => {
|
const utils = api.useContext();
|
||||||
const apiContext = api.useContext();
|
|
||||||
const { isLoading, mutateAsync } = api.config.delete.useMutation({
|
const { isLoading, mutateAsync } = api.config.delete.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await apiContext.config.all.invalidate();
|
await utils.config.all.invalidate();
|
||||||
modals.close(id);
|
modals.close(id);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text>
|
<Text>{t('modals.delete.text')}</Text>
|
||||||
Are you sure, that you want to delete this board? This action cannot be undone and your data
|
|
||||||
will be lost permanently.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<Button
|
<Button
|
||||||
@@ -30,7 +27,7 @@ export const DeleteBoardModal = ({
|
|||||||
variant="light"
|
variant="light"
|
||||||
color="gray"
|
color="gray"
|
||||||
>
|
>
|
||||||
Cancel
|
{t('common:cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -44,9 +41,21 @@ export const DeleteBoardModal = ({
|
|||||||
variant="light"
|
variant="light"
|
||||||
color="red"
|
color="red"
|
||||||
>
|
>
|
||||||
Delete
|
{t('common:delete')}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</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 { 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 { IconArrowRight, IconAt, IconUser } from '@tabler/icons-react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||||
|
|
||||||
interface CreateAccountStepProps {
|
interface CreateAccountStepProps {
|
||||||
nextStep: ({ eMail, username }: { username: string; eMail: string }) => void;
|
nextStep: ({ eMail, username }: { username: string; eMail: string }) => void;
|
||||||
@@ -10,7 +11,14 @@ interface CreateAccountStepProps {
|
|||||||
defaultEmail: string;
|
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({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
username: defaultUsername,
|
username: defaultUsername,
|
||||||
@@ -18,11 +26,9 @@ export const CreateAccountStep = ({ defaultEmail, defaultUsername, nextStep }: C
|
|||||||
},
|
},
|
||||||
validateInputOnBlur: true,
|
validateInputOnBlur: true,
|
||||||
validateInputOnChange: true,
|
validateInputOnChange: true,
|
||||||
validate: zodResolver(createAccountStepValidationSchema),
|
validate: i18nZodResolver(createAccountStepValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation('user/create');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card mih={400}>
|
<Card mih={400}>
|
||||||
<TextInput
|
<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,
|
Progress,
|
||||||
Text,
|
Text,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useForm, zodResolver } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import {
|
import {
|
||||||
IconArrowLeft,
|
IconArrowLeft,
|
||||||
IconArrowRight,
|
IconArrowRight,
|
||||||
@@ -18,21 +18,22 @@ import {
|
|||||||
IconKey,
|
IconKey,
|
||||||
IconX,
|
IconX,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useState } from 'react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import { useState } from 'react';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { api } from '~/utils/api';
|
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 = [
|
const requirements = [
|
||||||
{ re: /[0-9]/, label: 'Includes number' },
|
{ re: /[0-9]/, label: 'number' },
|
||||||
{ re: /[a-z]/, label: 'Includes lowercase letter' },
|
{ re: /[a-z]/, label: 'lowercase' },
|
||||||
{ re: /[A-Z]/, label: 'Includes uppercase letter' },
|
{ re: /[A-Z]/, label: 'uppercase' },
|
||||||
{ re: /[$&+,:;=?@#|'<>.^*()%!-]/, label: 'Includes special symbol' },
|
{ re: /[$&+,:;=?@#|'<>.^*()%!-]/, label: 'special' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function getStrength(password: string) {
|
function getStrength(password: string) {
|
||||||
let multiplier = password.length > 5 ? 0 : 1;
|
let multiplier = password.length >= minPasswordLength ? 0 : 1;
|
||||||
|
|
||||||
requirements.forEach((requirement) => {
|
requirements.forEach((requirement) => {
|
||||||
if (!requirement.re.test(password)) {
|
if (!requirement.re.test(password)) {
|
||||||
@@ -54,13 +55,16 @@ export const CreateAccountSecurityStep = ({
|
|||||||
nextStep,
|
nextStep,
|
||||||
prevStep,
|
prevStep,
|
||||||
}: CreateAccountSecurityStepProps) => {
|
}: CreateAccountSecurityStepProps) => {
|
||||||
|
const { t } = useTranslation('manage/users/create');
|
||||||
|
|
||||||
|
const { i18nZodResolver } = useI18nZodResolver();
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
password: defaultPassword,
|
password: defaultPassword,
|
||||||
},
|
},
|
||||||
validateInputOnBlur: true,
|
validateInputOnBlur: true,
|
||||||
validateInputOnChange: true,
|
validateInputOnChange: true,
|
||||||
validate: zodResolver(createAccountSecurityStepValidationSchema),
|
validate: i18nZodResolver(createAccountSecurityStepValidationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = api.password.generate.useMutation();
|
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 strength = getStrength(form.values.password);
|
||||||
const color = strength === 100 ? 'teal' : strength > 50 ? 'yellow' : 'red';
|
const color = strength === 100 ? 'teal' : strength > 50 ? 'yellow' : 'red';
|
||||||
|
|
||||||
@@ -114,7 +116,7 @@ export const CreateAccountSecurityStep = ({
|
|||||||
variant="default"
|
variant="default"
|
||||||
mt="xl"
|
mt="xl"
|
||||||
>
|
>
|
||||||
{t('buttons.generateRandomPw')}
|
{t('buttons.generateRandomPassword')}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,8 +124,8 @@ export const CreateAccountSecurityStep = ({
|
|||||||
<Popover.Dropdown>
|
<Popover.Dropdown>
|
||||||
<Progress color={color} value={strength} size={5} mb="xs" />
|
<Progress color={color} value={strength} size={5} mb="xs" />
|
||||||
<PasswordRequirement
|
<PasswordRequirement
|
||||||
label={t('steps.security.password.requirement')}
|
label="length"
|
||||||
meets={form.values.password.length > 5}
|
meets={form.values.password.length >= minPasswordLength}
|
||||||
/>
|
/>
|
||||||
{checks}
|
{checks}
|
||||||
</Popover.Dropdown>
|
</Popover.Dropdown>
|
||||||
@@ -152,6 +154,8 @@ export const CreateAccountSecurityStep = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PasswordRequirement = ({ meets, label }: { meets: boolean; label: string }) => {
|
const PasswordRequirement = ({ meets, label }: { meets: boolean; label: string }) => {
|
||||||
|
const { t } = useTranslation('manage/users/create');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
color={meets ? 'teal' : 'red'}
|
color={meets ? 'teal' : 'red'}
|
||||||
@@ -159,7 +163,12 @@ const PasswordRequirement = ({ meets, label }: { meets: boolean; label: string }
|
|||||||
mt={7}
|
mt={7}
|
||||||
size="sm"
|
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>
|
</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 { ContextModalProps, modals } from '@mantine/modals';
|
||||||
|
import { Trans, useTranslation } from 'next-i18next';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import { RouterOutputs } from '~/utils/api';
|
||||||
|
|
||||||
export const CopyInviteModal = ({
|
type InnerProps = RouterOutputs['invites']['create'];
|
||||||
id,
|
|
||||||
innerProps,
|
export const CopyInviteModal = ({ id, innerProps }: ContextModalProps<InnerProps>) => {
|
||||||
}: ContextModalProps<{ id: string; token: string; expire: Date }>) => {
|
const { t } = useTranslation('manage/users/invites');
|
||||||
const inviteUrl = useInviteUrl(innerProps.id, innerProps.token);
|
const inviteUrl = useInviteUrl(innerProps.id, innerProps.token);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text>
|
<Text>
|
||||||
Your invitation has been generated. After this modal closes,{' '}
|
<Trans
|
||||||
<b>you'll not be able to copy this link anymore</b>. If you do no longer wish to invite said
|
i18nKey="manage/users/invites:modals.copy.description"
|
||||||
person, you can delete this invitation any time.
|
components={{
|
||||||
|
b: <b />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Text>
|
</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">
|
<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}>
|
<Mark style={{ borderRadius: 4 }} color="gray" px={5}>
|
||||||
{innerProps.id}
|
{innerProps.id}
|
||||||
</Mark>
|
</Mark>
|
||||||
|
|
||||||
<Text weight="bold">Token:</Text>
|
<Text weight="bold">{t('modals.copy.details.token')}:</Text>
|
||||||
<Mark style={{ borderRadius: 4 }} color="gray" px={5}>
|
<Mark style={{ borderRadius: 4 }} color="gray" px={5}>
|
||||||
{innerProps.token}
|
{innerProps.token}
|
||||||
</Mark>
|
</Mark>
|
||||||
@@ -41,7 +48,7 @@ export const CopyInviteModal = ({
|
|||||||
variant="default"
|
variant="default"
|
||||||
fullWidth
|
fullWidth
|
||||||
>
|
>
|
||||||
Copy & Dismiss
|
{t('modals.copy.button.close')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</CopyButton>
|
</CopyButton>
|
||||||
@@ -54,3 +61,15 @@ const useInviteUrl = (id: string, token: string) => {
|
|||||||
|
|
||||||
return `${window.location.href.replace(router.pathname, `/auth/invite/${id}?token=${token}`)}`;
|
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 { Button, Group, Stack, Text, Title } from '@mantine/core';
|
||||||
import { DateInput, DateTimePicker } from '@mantine/dates';
|
import { DateTimePicker } from '@mantine/dates';
|
||||||
import { useForm, zodResolver } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { ContextModalProps, modals } from '@mantine/modals';
|
import { ContextModalProps, modals } from '@mantine/modals';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { Trans, useTranslation } from 'next-i18next';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||||
import { createInviteSchema } from '~/validations/invite';
|
import { createInviteSchema } from '~/validations/invite';
|
||||||
|
|
||||||
|
import { openCopyInviteModal } from './copy-invite.modal';
|
||||||
|
|
||||||
export const CreateInviteModal = ({ id }: ContextModalProps<{}>) => {
|
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({
|
const { isLoading, mutateAsync } = api.invites.create.useMutation({
|
||||||
onSuccess: async (data) => {
|
onSuccess: async (data) => {
|
||||||
await apiContext.invites.all.invalidate();
|
await utils.invites.all.invalidate();
|
||||||
modals.close(id);
|
modals.close(id);
|
||||||
|
|
||||||
modals.openContextModal({
|
openCopyInviteModal(data);
|
||||||
modal: 'copyInviteModal',
|
|
||||||
title: <Text weight="bold">Copy invitation</Text>,
|
|
||||||
innerProps: data,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -36,10 +36,7 @@ export const CreateInviteModal = ({ id }: ContextModalProps<{}>) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text>
|
<Text>{t('modals.create.description')}</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>
|
|
||||||
|
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
popoverProps={{ withinPortal: true }}
|
popoverProps={{ withinPortal: true }}
|
||||||
@@ -47,7 +44,7 @@ export const CreateInviteModal = ({ id }: ContextModalProps<{}>) => {
|
|||||||
maxDate={maxDate}
|
maxDate={maxDate}
|
||||||
withAsterisk
|
withAsterisk
|
||||||
valueFormat="DD MMM YYYY hh:mm A"
|
valueFormat="DD MMM YYYY hh:mm A"
|
||||||
label="Expiration date"
|
label={t('modals.create.form.expires.label')}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
{...form.getInputProps('expirationDate')}
|
{...form.getInputProps('expirationDate')}
|
||||||
/>
|
/>
|
||||||
@@ -60,7 +57,7 @@ export const CreateInviteModal = ({ id }: ContextModalProps<{}>) => {
|
|||||||
variant="light"
|
variant="light"
|
||||||
color="gray"
|
color="gray"
|
||||||
>
|
>
|
||||||
Cancel
|
{t('common:cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -72,9 +69,21 @@ export const CreateInviteModal = ({ id }: ContextModalProps<{}>) => {
|
|||||||
variant="light"
|
variant="light"
|
||||||
color="green"
|
color="green"
|
||||||
>
|
>
|
||||||
Create
|
{t('modals.create.form.submit')}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</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 { Button, Group, Stack, Text } from '@mantine/core';
|
||||||
import { ContextModalProps, modals } from '@mantine/modals';
|
import { ContextModalProps, modals } from '@mantine/modals';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
export const DeleteInviteModal = ({
|
export const DeleteInviteModal = ({ id, innerProps }: ContextModalProps<{ tokenId: string }>) => {
|
||||||
context,
|
const { t } = useTranslation('manage/users/invites');
|
||||||
id,
|
const utils = api.useContext();
|
||||||
innerProps,
|
const { isLoading, mutateAsync: deleteAsync } = api.invites.delete.useMutation({
|
||||||
}: ContextModalProps<{ tokenId: string }>) => {
|
|
||||||
const apiContext = api.useContext();
|
|
||||||
const { isLoading, mutateAsync } = api.invites.delete.useMutation({
|
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
await apiContext.invites.all.invalidate();
|
await utils.invites.all.invalidate();
|
||||||
modals.close(id);
|
modals.close(id);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text>
|
<Text>{t('modals.delete.description')}</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>
|
|
||||||
|
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<Button
|
<Button
|
||||||
@@ -29,11 +24,11 @@ export const DeleteInviteModal = ({
|
|||||||
variant="light"
|
variant="light"
|
||||||
color="gray"
|
color="gray"
|
||||||
>
|
>
|
||||||
Cancel
|
{t('common:cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await deleteAsync({
|
||||||
tokenId: innerProps.tokenId,
|
tokenId: innerProps.tokenId,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -41,7 +36,7 @@ export const DeleteInviteModal = ({
|
|||||||
variant="light"
|
variant="light"
|
||||||
color="red"
|
color="red"
|
||||||
>
|
>
|
||||||
Delete
|
{t('common:delete')}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</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,
|
IconLayoutDashboard,
|
||||||
IconMailForward,
|
IconMailForward,
|
||||||
IconQuestionMark,
|
IconQuestionMark,
|
||||||
IconSettings2,
|
|
||||||
IconUser,
|
IconUser,
|
||||||
IconUsers,
|
IconUsers,
|
||||||
|
TablerIconsProps,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
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 { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
|
||||||
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore';
|
||||||
|
|
||||||
|
import { type navigation } from '../../../../public/locales/en/layout/manage.json';
|
||||||
import { MainHeader } from '../header/Header';
|
import { MainHeader } from '../header/Header';
|
||||||
|
|
||||||
interface ManageLayoutProps {
|
interface ManageLayoutProps {
|
||||||
@@ -40,7 +42,8 @@ interface ManageLayoutProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ManageLayout = ({ children }: 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 theme = useMantineTheme();
|
||||||
|
|
||||||
const screenLargerThanMd = useScreenLargerThan('md');
|
const screenLargerThanMd = useScreenLargerThan('md');
|
||||||
@@ -51,100 +54,19 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
|||||||
const data = useSession();
|
const data = useSession();
|
||||||
const isAdmin = data.data?.user.isAdmin ?? false;
|
const isAdmin = data.data?.user.isAdmin ?? false;
|
||||||
|
|
||||||
const navigationLinks = (
|
const navigationLinkComponents = Object.entries(navigationLinks).map(([name, navigationLink]) => {
|
||||||
<>
|
if (navigationLink.onlyAdmin && !isAdmin) {
|
||||||
<NavLink
|
return null;
|
||||||
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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isAdmin && (
|
return (
|
||||||
<>
|
<CustomNavigationLink
|
||||||
<NavLink
|
key={name}
|
||||||
label="Users"
|
name={name as keyof typeof navigationLinks}
|
||||||
icon={
|
navigationLink={navigationLink}
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const burgerMenu = screenLargerThanMd ? undefined : (
|
const burgerMenu = screenLargerThanMd ? undefined : (
|
||||||
<Burger opened={burgerMenuOpen} onClick={toggleBurgerMenu} />
|
<Burger opened={burgerMenuOpen} onClick={toggleBurgerMenu} />
|
||||||
@@ -161,7 +83,7 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
|||||||
navbar={
|
navbar={
|
||||||
<Navbar width={{ base: !screenLargerThanMd ? 0 : 220 }} hidden={!screenLargerThanMd}>
|
<Navbar width={{ base: !screenLargerThanMd ? 0 : 220 }} hidden={!screenLargerThanMd}>
|
||||||
<Navbar.Section pt="xs" grow>
|
<Navbar.Section pt="xs" grow>
|
||||||
{navigationLinks}
|
{navigationLinkComponents}
|
||||||
</Navbar.Section>
|
</Navbar.Section>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
}
|
}
|
||||||
@@ -174,9 +96,9 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
|||||||
<Text fw="bold" size={15}>
|
<Text fw="bold" size={15}>
|
||||||
Homarr
|
Homarr
|
||||||
</Text>
|
</Text>
|
||||||
{attributes.packageVersion && (
|
{packageVersion && (
|
||||||
<Text color="dimmed" size={13}>
|
<Text color="dimmed" size={13}>
|
||||||
{attributes.packageVersion}
|
{packageVersion}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -189,8 +111,126 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => {
|
|||||||
</Paper>
|
</Paper>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
<Drawer opened={burgerMenuOpen} onClose={closeBurgerMenu}>
|
<Drawer opened={burgerMenuOpen} onClose={closeBurgerMenu}>
|
||||||
{navigationLinks}
|
{navigationLinkComponents}
|
||||||
</Drawer>
|
</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')}
|
{t('actions.avatar.defaultBoard')}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item component={Link} href="/manage" icon={<IconHomeShare size="1rem" />}>
|
<Menu.Item component={Link} href="/manage" icon={<IconHomeShare size="1rem" />}>
|
||||||
Manage
|
{t('actions.avatar.manage')}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
import { IconDownload, IconExternalLink, IconPlayerPlay } from '@tabler/icons-react';
|
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 Image from 'next/image';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
@@ -69,8 +69,9 @@ export const MovieModal = ({ opened, closeModal }: MovieModalProps) => {
|
|||||||
type MovieResultsProps = Omit<z.infer<typeof queryParamsSchema>, 'movie'>;
|
type MovieResultsProps = Omit<z.infer<typeof queryParamsSchema>, 'movie'>;
|
||||||
|
|
||||||
const MovieResults = ({ search, type }: MovieResultsProps) => {
|
const MovieResults = ({ search, type }: MovieResultsProps) => {
|
||||||
|
const { t } = useTranslation('layout/header');
|
||||||
const { name: configName } = useConfigContext();
|
const { name: configName } = useConfigContext();
|
||||||
const { data: overseerrResults, isLoading } = api.overseerr.search.useQuery(
|
const { data: movies, isLoading } = api.overseerr.search.useQuery(
|
||||||
{
|
{
|
||||||
query: search,
|
query: search,
|
||||||
configName: configName!,
|
configName: configName!,
|
||||||
@@ -94,10 +95,20 @@ const MovieResults = ({ search, type }: MovieResultsProps) => {
|
|||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text>
|
<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>
|
</Text>
|
||||||
<Grid gutter={32}>
|
<Grid gutter={32}>
|
||||||
{overseerrResults?.map((result, index: number) => (
|
{movies?.map((result, index: number) => (
|
||||||
<Grid.Col key={index} span={12} sm={6} lg={4}>
|
<Grid.Col key={index} span={12} sm={6} lg={4}>
|
||||||
<MovieDisplay movie={result} type={type} />
|
<MovieDisplay movie={result} type={type} />
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const env = createEnv({
|
|||||||
process.env.VERCEL ? z.string().min(1) : z.string().url()
|
process.env.VERCEL ? z.string().min(1) : z.string().url()
|
||||||
),
|
),
|
||||||
DOCKER_HOST: z.string().optional(),
|
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 { WidgetsRemoveModal } from '~/components/Dashboard/Tiles/Widgets/WidgetsRemoveModal';
|
||||||
import { CategoryEditModal } from '~/components/Dashboard/Wrappers/Category/CategoryEditModal';
|
import { CategoryEditModal } from '~/components/Dashboard/Wrappers/Category/CategoryEditModal';
|
||||||
|
|
||||||
import { CopyInviteModal } from './copy-invite/copy-invite.modal';
|
import { CreateBoardModal } from './components/Manage/Board/create-board.modal';
|
||||||
import { CreateDashboardModal } from './create-dashboard/create-dashboard.modal';
|
import { DeleteBoardModal } from './components/Manage/Board/delete-board.modal';
|
||||||
import { CreateInviteModal } from './create-invite/create-invite.modal';
|
import { CopyInviteModal } from './components/Manage/User/Invite/copy-invite.modal';
|
||||||
import { DeleteBoardModal } from './delete-board/delete-board.modal';
|
import { CreateInviteModal } from './components/Manage/User/Invite/create-invite.modal';
|
||||||
import { DeleteInviteModal } from './delete-invite/delete-invite.modal';
|
import { DeleteInviteModal } from './components/Manage/User/Invite/delete-invite.modal';
|
||||||
import { DeleteUserModal } from './delete-user/delete-user.modal';
|
import { DeleteUserModal } from './components/Manage/User/delete-user.modal';
|
||||||
|
|
||||||
export const modals = {
|
export const modals = {
|
||||||
editApp: EditAppModal,
|
editApp: EditAppModal,
|
||||||
@@ -24,7 +24,7 @@ export const modals = {
|
|||||||
deleteUserModal: DeleteUserModal,
|
deleteUserModal: DeleteUserModal,
|
||||||
createInviteModal: CreateInviteModal,
|
createInviteModal: CreateInviteModal,
|
||||||
deleteInviteModal: DeleteInviteModal,
|
deleteInviteModal: DeleteInviteModal,
|
||||||
createDashboardModal: CreateDashboardModal,
|
createBoardModal: CreateBoardModal,
|
||||||
copyInviteModal: CopyInviteModal,
|
copyInviteModal: CopyInviteModal,
|
||||||
deleteBoardModal: DeleteBoardModal,
|
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 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,
|
enabled: dockerEnabled,
|
||||||
});
|
});
|
||||||
useHotkeys([['mod+B', () => setOpened(!opened)]]);
|
useHotkeys([['mod+B', () => setOpened(!opened)]]);
|
||||||
@@ -42,7 +42,7 @@ export default function DockerMenuButton(props: any) {
|
|||||||
padding="xl"
|
padding="xl"
|
||||||
position="right"
|
position="right"
|
||||||
size="100%"
|
size="100%"
|
||||||
title={<ContainerActionBar isLoading selected={selection} reload={reload} />}
|
title={<ContainerActionBar isLoading={isLoading} selected={selection} reload={reload} />}
|
||||||
transitionProps={{
|
transitionProps={{
|
||||||
transition: 'pop',
|
transition: 'pop',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { z } from 'zod';
|
|||||||
import { CommonHead } from '~/components/layout/Meta/CommonHead';
|
import { CommonHead } from '~/components/layout/Meta/CommonHead';
|
||||||
import { env } from '~/env.js';
|
import { env } from '~/env.js';
|
||||||
import { ColorSchemeProvider } from '~/hooks/use-colorscheme';
|
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 { queryClient } from '~/tools/server/configurations/tanstack/queryClient.tool';
|
||||||
import { ConfigType } from '~/types/config';
|
import { ConfigType } from '~/types/config';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|||||||
@@ -4,16 +4,18 @@ import { showNotification, updateNotification } from '@mantine/notifications';
|
|||||||
import { IconCheck, IconX } from '@tabler/icons-react';
|
import { IconCheck, IconX } from '@tabler/icons-react';
|
||||||
import { GetServerSideProps } from 'next';
|
import { GetServerSideProps } from 'next';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { getServerAuthSession } from '~/server/auth';
|
import { getServerAuthSession } from '~/server/auth';
|
||||||
import { prisma } from '~/server/db';
|
import { prisma } from '~/server/db';
|
||||||
import { inviteNamespaces } from '~/tools/server/translation-namespaces';
|
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||||
import { signUpFormSchema } from '~/validations/user';
|
import { signUpFormSchema } from '~/validations/user';
|
||||||
|
|
||||||
|
const notificationId = 'register';
|
||||||
|
|
||||||
export default function AuthInvitePage() {
|
export default function AuthInvitePage() {
|
||||||
const { t } = useTranslation('authentication/invite');
|
const { t } = useTranslation('authentication/invite');
|
||||||
const { i18nZodResolver } = useI18nZodResolver();
|
const { i18nZodResolver } = useI18nZodResolver();
|
||||||
@@ -28,11 +30,10 @@ export default function AuthInvitePage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (values: z.infer<typeof signUpFormSchema>) => {
|
const handleSubmit = (values: z.infer<typeof signUpFormSchema>) => {
|
||||||
const notificationId = 'register';
|
|
||||||
showNotification({
|
showNotification({
|
||||||
id: notificationId,
|
id: notificationId,
|
||||||
title: 'Creating account',
|
title: t('notifications.loading.title'),
|
||||||
message: 'Please wait...',
|
message: `${t('notifications.loading.text')}...`,
|
||||||
loading: true,
|
loading: true,
|
||||||
});
|
});
|
||||||
void mutateAsync(
|
void mutateAsync(
|
||||||
@@ -44,8 +45,8 @@ export default function AuthInvitePage() {
|
|||||||
onSuccess() {
|
onSuccess() {
|
||||||
updateNotification({
|
updateNotification({
|
||||||
id: notificationId,
|
id: notificationId,
|
||||||
title: 'Account created',
|
title: t('notifications.success.title'),
|
||||||
message: 'Your account has been created successfully',
|
message: t('notifications.success.text'),
|
||||||
color: 'teal',
|
color: 'teal',
|
||||||
icon: <IconCheck />,
|
icon: <IconCheck />,
|
||||||
});
|
});
|
||||||
@@ -54,8 +55,8 @@ export default function AuthInvitePage() {
|
|||||||
onError() {
|
onError() {
|
||||||
updateNotification({
|
updateNotification({
|
||||||
id: notificationId,
|
id: notificationId,
|
||||||
title: 'Error',
|
title: t('notifications.error.title'),
|
||||||
message: 'Something went wrong',
|
message: t('notifications.error.text'),
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: <IconX />,
|
icon: <IconX />,
|
||||||
});
|
});
|
||||||
@@ -64,7 +65,14 @@ export default function AuthInvitePage() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const metaTitle = `${t('metaTitle')} • Homarr`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{metaTitle}</title>
|
||||||
|
</Head>
|
||||||
|
|
||||||
<Flex h="100dvh" display="flex" w="100%" direction="column" align="center" justify="center">
|
<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}>
|
<Card withBorder shadow="md" p="xl" radius="md" w="90%" maw={420}>
|
||||||
<Title align="center" weight={900}>
|
<Title align="center" weight={900}>
|
||||||
@@ -105,6 +113,7 @@ export default function AuthInvitePage() {
|
|||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +166,7 @@ export const getServerSideProps: GetServerSideProps = async ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
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 { GetServerSideProps } from 'next';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { getServerAuthSession } from '~/server/auth';
|
import { getServerAuthSession } from '~/server/auth';
|
||||||
|
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||||
import { signInSchema } from '~/validations/user';
|
import { signInSchema } from '~/validations/user';
|
||||||
|
|
||||||
import { loginNamespaces } from '../../tools/server/translation-namespaces';
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const { t } = useTranslation('authentication/login');
|
const { t } = useTranslation('authentication/login');
|
||||||
const queryParams = useRouter().query as { error?: 'CredentialsSignin' | (string & {}) };
|
const queryParams = useRouter().query as { error?: 'CredentialsSignin' | (string & {}) };
|
||||||
@@ -54,11 +52,15 @@ export default function LoginPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const metaTitle = `${t('metaTitle')} • Homarr`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex h="100dvh" display="flex" w="100%" direction="column" align="center" justify="center">
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Login • Homarr</title>
|
<title>{metaTitle}</title>
|
||||||
</Head>
|
</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}>
|
<Card withBorder shadow="md" p="xl" radius="md" w="90%" maw={420}>
|
||||||
<Title align="center" weight={900}>
|
<Title align="center" weight={900}>
|
||||||
{t('title')}
|
{t('title')}
|
||||||
@@ -97,6 +99,7 @@ export default function LoginPage() {
|
|||||||
</form>
|
</form>
|
||||||
</Card>
|
</Card>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,8 +117,7 @@ export const getServerSideProps: GetServerSideProps = async ({ locale, req, res
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...(await serverSideTranslations(locale ?? 'en', loginNamespaces)),
|
...(await getServerSideTranslations(['authentication/login'], locale, req, res)),
|
||||||
// Will be passed to the page component as props
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { boardNamespaces } from '~/tools/server/translation-namespaces';
|
|||||||
import { firstUpperCase } from '~/tools/shared/strings';
|
import { firstUpperCase } from '~/tools/shared/strings';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||||
import { boardCustomizationSchema } from '~/validations/dashboards';
|
import { boardCustomizationSchema } from '~/validations/boards';
|
||||||
|
|
||||||
const notificationId = 'board-customization-notification';
|
const notificationId = 'board-customization-notification';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Flex,
|
|
||||||
Group,
|
Group,
|
||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
Menu,
|
Menu,
|
||||||
@@ -26,9 +25,11 @@ import {
|
|||||||
IconTrash,
|
IconTrash,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { GetServerSideProps } from 'next';
|
import { GetServerSideProps } from 'next';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
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 { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||||
import { getServerAuthSession } from '~/server/auth';
|
import { getServerAuthSession } from '~/server/auth';
|
||||||
import { sleep } from '~/tools/client/time';
|
import { sleep } from '~/tools/client/time';
|
||||||
@@ -47,31 +48,26 @@ const BoardsPage = () => {
|
|||||||
|
|
||||||
const [deletingDashboards, { append, filter }] = useListState<string>([]);
|
const [deletingDashboards, { append, filter }] = useListState<string>([]);
|
||||||
|
|
||||||
const { t } = useTranslation('boards/manage');
|
const { t } = useTranslation('manage/boards');
|
||||||
|
|
||||||
|
const metaTitle = `${t('metaTitle')} • Homarr`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ManageLayout>
|
<ManageLayout>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Boards • Homarr</title>
|
<title>{metaTitle}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Title mb="xl">{t('title')}</Title>
|
<Group position="apart">
|
||||||
|
<Title mb="xl">{t('pageTitle')}</Title>
|
||||||
<Flex justify="end" mb="md">
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={openCreateBoardModal}
|
||||||
modals.openContextModal({
|
|
||||||
modal: 'createDashboardModal',
|
|
||||||
title: <Text>{t('buttons.create')}</Text>,
|
|
||||||
innerProps: {},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
leftIcon={<IconPlus size="1rem" />}
|
leftIcon={<IconPlus size="1rem" />}
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
{t('buttons.create')}
|
{t('buttons.create')}
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Group>
|
||||||
|
|
||||||
{data && (
|
{data && (
|
||||||
<SimpleGrid
|
<SimpleGrid
|
||||||
@@ -167,10 +163,7 @@ const BoardsPage = () => {
|
|||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
modals.openContextModal({
|
openDeleteBoardModal({
|
||||||
modal: 'deleteBoardModal',
|
|
||||||
title: <Text weight={500}>{t('cards.menu.delete.modalTitle')}</Text>,
|
|
||||||
innerProps: {
|
|
||||||
boardName: board.name,
|
boardName: board.name,
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
append(board.name);
|
append(board.name);
|
||||||
@@ -178,7 +171,6 @@ const BoardsPage = () => {
|
|||||||
await sleep(500);
|
await sleep(500);
|
||||||
filter((item, _) => item !== board.name);
|
filter((item, _) => item !== board.name);
|
||||||
},
|
},
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
disabled={board.name === 'default'}
|
disabled={board.name === 'default'}
|
||||||
@@ -213,8 +205,8 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|||||||
const translations = await getServerSideTranslations(
|
const translations = await getServerSideTranslations(
|
||||||
manageNamespaces,
|
manageNamespaces,
|
||||||
ctx.locale,
|
ctx.locale,
|
||||||
undefined,
|
ctx.req,
|
||||||
undefined
|
ctx.res
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { IconArrowRight } from '@tabler/icons-react';
|
import { IconArrowRight } from '@tabler/icons-react';
|
||||||
import { GetServerSideProps } from 'next';
|
import { GetServerSideProps } from 'next';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -19,24 +20,31 @@ import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
|||||||
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
|
import { useScreenLargerThan } from '~/hooks/useScreenLargerThan';
|
||||||
import { getServerAuthSession } from '~/server/auth';
|
import { getServerAuthSession } from '~/server/auth';
|
||||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||||
|
import { OnlyKeysWithStructure } from '~/types/helpers';
|
||||||
|
|
||||||
|
import { type quickActions } from '../../../public/locales/en/manage/index.json';
|
||||||
|
|
||||||
const ManagementPage = () => {
|
const ManagementPage = () => {
|
||||||
|
const { t } = useTranslation('manage/index');
|
||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
const largerThanMd = useScreenLargerThan('md');
|
const largerThanMd = useScreenLargerThan('md');
|
||||||
const { data: sessionData } = useSession();
|
const { data: sessionData } = useSession();
|
||||||
|
|
||||||
|
const metaTitle = `${t('metaTitle')} • Homarr`;
|
||||||
return (
|
return (
|
||||||
<ManageLayout>
|
<ManageLayout>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Manage • Homarr</title>
|
<title>{metaTitle}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Box className={classes.box} w="100%" mih={150} p="xl" mb={50}>
|
<Box className={classes.box} w="100%" mih={150} p="xl" mb={50}>
|
||||||
<Group position="apart" noWrap>
|
<Group position="apart" noWrap>
|
||||||
<Stack spacing={15}>
|
<Stack spacing={15}>
|
||||||
<Title className={classes.boxTitle} order={2}>
|
<Title className={classes.boxTitle} order={2}>
|
||||||
Welcome back, {sessionData?.user?.name ?? 'Anonymous'}
|
{t('hero.title', {
|
||||||
|
username: sessionData?.user?.name ?? t('hero.fallbackUsername'),
|
||||||
|
})}
|
||||||
</Title>
|
</Title>
|
||||||
<Text>Welcome to Your Application Hub. Organize, Optimize, and Conquer!</Text>
|
<Text>{t('hero.subtitle')}</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Box bg="blue" w={100} h="100%" pos="relative">
|
<Box bg="blue" w={100} h="100%" pos="relative">
|
||||||
<Box
|
<Box
|
||||||
@@ -49,7 +57,7 @@ const ManagementPage = () => {
|
|||||||
src="/imgs/logo/logo.png"
|
src="/imgs/logo/logo.png"
|
||||||
width={largerThanMd ? 200 : 100}
|
width={largerThanMd ? 200 : 100}
|
||||||
height={largerThanMd ? 150 : 60}
|
height={largerThanMd ? 150 : 60}
|
||||||
alt=""
|
alt="Homarr Logo"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -57,7 +65,7 @@ const ManagementPage = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Text weight="bold" mb="md">
|
<Text weight="bold" mb="md">
|
||||||
Quick actions
|
{t('quickActions.title')}
|
||||||
</Text>
|
</Text>
|
||||||
<SimpleGrid
|
<SimpleGrid
|
||||||
cols={3}
|
cols={3}
|
||||||
@@ -67,44 +75,46 @@ const ManagementPage = () => {
|
|||||||
{ maxWidth: '48rem', cols: 1, spacing: 'md' },
|
{ maxWidth: '48rem', cols: 1, spacing: 'md' },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<UnstyledButton component={Link} href="/manage/boards">
|
<QuickActionCard type="boards" href="/manage/boards" />
|
||||||
<Card className={classes.quickActionCard}>
|
<QuickActionCard type="inviteUsers" href="/manage/users/invites" />
|
||||||
<Group spacing={30} noWrap>
|
<QuickActionCard type="manageUsers" href="/manage/users" />
|
||||||
<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>
|
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</ManageLayout>
|
</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) => {
|
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||||
const session = await getServerAuthSession(ctx);
|
const session = await getServerAuthSession(ctx);
|
||||||
|
|
||||||
@@ -115,10 +125,10 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const translations = await getServerSideTranslations(
|
const translations = await getServerSideTranslations(
|
||||||
['common'],
|
['layout/manage', 'manage/index'],
|
||||||
ctx.locale,
|
ctx.locale,
|
||||||
undefined,
|
ctx.req,
|
||||||
undefined
|
ctx.res
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
props: {
|
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 { Alert, Button, Group, Stepper } from '@mantine/core';
|
||||||
import { useForm, zodResolver } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import {
|
import { IconArrowLeft, IconKey, IconMailCheck, IconUser, IconUserPlus } from '@tabler/icons-react';
|
||||||
IconArrowLeft,
|
|
||||||
IconCheck,
|
|
||||||
IconInfoCircle,
|
|
||||||
IconKey,
|
|
||||||
IconMail,
|
|
||||||
IconMailCheck,
|
|
||||||
IconUser,
|
|
||||||
IconUserPlus,
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { GetServerSideProps } from 'next';
|
import { GetServerSideProps } from 'next';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import {
|
import {
|
||||||
CreateAccountStep,
|
CreateAccountStep,
|
||||||
createAccountStepValidationSchema,
|
createAccountStepValidationSchema,
|
||||||
} from '~/components/Manage/User/Create/create-account-step';
|
} from '~/components/Manage/User/Create/create-account-step';
|
||||||
|
import { ReviewInputStep } from '~/components/Manage/User/Create/review-input-step';
|
||||||
import {
|
import {
|
||||||
CreateAccountSecurityStep,
|
CreateAccountSecurityStep,
|
||||||
createAccountSecurityStepValidationSchema,
|
createAccountSecurityStepValidationSchema,
|
||||||
@@ -27,15 +19,17 @@ import {
|
|||||||
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||||
import { getServerAuthSession } from '~/server/auth';
|
import { getServerAuthSession } from '~/server/auth';
|
||||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||||
import { api } from '~/utils/api';
|
|
||||||
import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
||||||
|
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||||
|
|
||||||
const CreateNewUserPage = () => {
|
const CreateNewUserPage = () => {
|
||||||
|
const { t } = useTranslation('manage/users/create');
|
||||||
const [active, setActive] = useState(0);
|
const [active, setActive] = useState(0);
|
||||||
const nextStep = () => setActive((current) => (current < 3 ? current + 1 : current));
|
const nextStep = () => setActive((current) => (current < 3 ? current + 1 : current));
|
||||||
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
|
const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current));
|
||||||
|
const { i18nZodResolver } = useI18nZodResolver();
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm<CreateAccountSchema>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
account: {
|
account: {
|
||||||
username: '',
|
username: '',
|
||||||
@@ -45,30 +39,14 @@ const CreateNewUserPage = () => {
|
|||||||
password: '',
|
password: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
validate: zodResolver(
|
validate: i18nZodResolver(createAccountSchema),
|
||||||
z.object({
|
|
||||||
account: createAccountStepValidationSchema,
|
|
||||||
security: createAccountSecurityStepValidationSchema,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const context = api.useContext();
|
const metaTitle = `${t('metaTitle')} • Homarr`;
|
||||||
const { mutateAsync, isLoading } = api.user.create.useMutation({
|
|
||||||
onSettled: () => {
|
|
||||||
void context.user.all.invalidate();
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
nextStep();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { t } = useTranslation('user/create');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ManageLayout>
|
<ManageLayout>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Create user • Homarr</title>
|
<title>{metaTitle}</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Stepper active={active} onStepClick={setActive} breakpoint="sm" mih="100%">
|
<Stepper active={active} onStepClick={setActive} breakpoint="sm" mih="100%">
|
||||||
@@ -111,92 +89,11 @@ const CreateNewUserPage = () => {
|
|||||||
label={t('steps.finish.title')}
|
label={t('steps.finish.title')}
|
||||||
description={t('steps.finish.title')}
|
description={t('steps.finish.title')}
|
||||||
>
|
>
|
||||||
<Card mih={400}>
|
<ReviewInputStep values={form.values} prevStep={prevStep} nextStep={nextStep} />
|
||||||
<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>
|
|
||||||
</Stepper.Step>
|
</Stepper.Step>
|
||||||
<Stepper.Completed>
|
<Stepper.Completed>
|
||||||
<Alert title="User was created" color="green" mb="md">
|
<Alert title={t('steps.completed.alert.title')} color="green" mb="md">
|
||||||
{t('steps.finish.alertConfirmed')}
|
{t('steps.completed.alert.text')}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<Group>
|
<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) => {
|
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||||
const session = await getServerAuthSession(ctx);
|
const session = await getServerAuthSession(ctx);
|
||||||
|
|
||||||
@@ -237,8 +141,8 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|||||||
const translations = await getServerSideTranslations(
|
const translations = await getServerSideTranslations(
|
||||||
manageNamespaces,
|
manageNamespaces,
|
||||||
ctx.locale,
|
ctx.locale,
|
||||||
undefined,
|
ctx.req,
|
||||||
undefined
|
ctx.res
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -15,15 +15,16 @@ import { useDebouncedValue } from '@mantine/hooks';
|
|||||||
import { openContextModal } from '@mantine/modals';
|
import { openContextModal } from '@mantine/modals';
|
||||||
import { IconPlus, IconTrash } from '@tabler/icons-react';
|
import { IconPlus, IconTrash } from '@tabler/icons-react';
|
||||||
import { GetServerSideProps } from 'next';
|
import { GetServerSideProps } from 'next';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useState } from 'react';
|
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 { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||||
import { getServerAuthSession } from '~/server/auth';
|
import { getServerAuthSession } from '~/server/auth';
|
||||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||||
import { api } from '~/utils/api';
|
|
||||||
import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
||||||
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
const ManageUsersPage = () => {
|
const ManageUsersPage = () => {
|
||||||
const [activePage, setActivePage] = useState(0);
|
const [activePage, setActivePage] = useState(0);
|
||||||
@@ -34,7 +35,7 @@ const ManageUsersPage = () => {
|
|||||||
search: debouncedSearch,
|
search: debouncedSearch,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t } = useTranslation('user/manage');
|
const { t } = useTranslation('manage/users');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ManageLayout>
|
<ManageLayout>
|
||||||
@@ -86,16 +87,7 @@ const ManageUsersPage = () => {
|
|||||||
<Group>
|
<Group>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
openContextModal({
|
openDeleteUserModal(user);
|
||||||
modal: 'deleteUserModal',
|
|
||||||
title: (
|
|
||||||
<Text weight="bold">{t('modals.delete', { name: user.name })}</Text>
|
|
||||||
),
|
|
||||||
innerProps: {
|
|
||||||
userId: user.id,
|
|
||||||
username: user.name ?? '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
color="red"
|
color="red"
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -112,9 +104,7 @@ const ManageUsersPage = () => {
|
|||||||
<tr>
|
<tr>
|
||||||
<td colSpan={1}>
|
<td colSpan={1}>
|
||||||
<Box p={15}>
|
<Box p={15}>
|
||||||
<Text>
|
<Text>{t('searchDoesntMatch')}</Text>
|
||||||
{t('searchDoesntMatch')}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
</Box>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ import { modals } from '@mantine/modals';
|
|||||||
import { IconPlus, IconTrash } from '@tabler/icons-react';
|
import { IconPlus, IconTrash } from '@tabler/icons-react';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { GetServerSideProps } from 'next';
|
import { GetServerSideProps } from 'next';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useState } from 'react';
|
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 { ManageLayout } from '~/components/layout/Templates/ManageLayout';
|
||||||
import { getServerAuthSession } from '~/server/auth';
|
import { getServerAuthSession } from '~/server/auth';
|
||||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||||
@@ -23,40 +24,33 @@ import { manageNamespaces } from '~/tools/server/translation-namespaces';
|
|||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
const ManageUserInvitesPage = () => {
|
const ManageUserInvitesPage = () => {
|
||||||
|
const { classes } = useStyles();
|
||||||
|
const { t } = useTranslation('manage/users/invites');
|
||||||
const [activePage, setActivePage] = useState(0);
|
const [activePage, setActivePage] = useState(0);
|
||||||
const { data } = api.invites.all.useQuery({
|
const { data: invites } = api.invites.all.useQuery({
|
||||||
page: activePage,
|
page: activePage,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { classes } = useStyles();
|
const nextPage = () => {
|
||||||
|
|
||||||
const handleFetchNextPage = async () => {
|
|
||||||
setActivePage((prev) => prev + 1);
|
setActivePage((prev) => prev + 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFetchPreviousPage = async () => {
|
const previousPage = () => {
|
||||||
setActivePage((prev) => prev - 1);
|
setActivePage((prev) => prev - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { t } = useTranslation('user/invites');
|
const metaTitle = `${t('metaTitle')} • Homarr`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ManageLayout>
|
<ManageLayout>
|
||||||
<Head>
|
<Head>
|
||||||
<title>User invites • Homarr</title>
|
<title>{metaTitle}</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Title mb="md">{t('title')}</Title>
|
<Title mb="md">{t('pageTitle')}</Title>
|
||||||
<Text mb="xl">{t('text')}</Text>
|
<Text mb="xl">{t('description')}</Text>
|
||||||
|
|
||||||
<Flex justify="end" mb="md">
|
<Flex justify="end" mb="md">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={openCreateInviteModal}
|
||||||
modals.openContextModal({
|
|
||||||
modal: 'createInviteModal',
|
|
||||||
title: 'Create invite',
|
|
||||||
innerProps: {},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
leftIcon={<IconPlus size="1rem" />}
|
leftIcon={<IconPlus size="1rem" />}
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
@@ -64,7 +58,7 @@ const ManageUserInvitesPage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{data && (
|
{invites && (
|
||||||
<>
|
<>
|
||||||
<Table mb="md" withBorder highlightOnHover>
|
<Table mb="md" withBorder highlightOnHover>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -76,7 +70,7 @@ const ManageUserInvitesPage = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.invites.map((invite, index) => (
|
{invites.invites.map((invite, index) => (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td className={classes.tableGrowCell}>
|
<td className={classes.tableGrowCell}>
|
||||||
<Text lineClamp={1}>{invite.id}</Text>
|
<Text lineClamp={1}>{invite.id}</Text>
|
||||||
@@ -114,9 +108,9 @@ const ManageUserInvitesPage = () => {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{data.invites.length === 0 && (
|
{invites.invites.length === 0 && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={3}>
|
<td colSpan={4}>
|
||||||
<Center p="md">
|
<Center p="md">
|
||||||
<Text color="dimmed">{t('noInvites')}</Text>
|
<Text color="dimmed">{t('noInvites')}</Text>
|
||||||
</Center>
|
</Center>
|
||||||
@@ -126,18 +120,18 @@ const ManageUserInvitesPage = () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
<Pagination
|
<Pagination
|
||||||
total={data.countPages}
|
total={invites.countPages}
|
||||||
value={activePage + 1}
|
value={activePage + 1}
|
||||||
onChange={(targetPage) => {
|
onChange={(targetPage) => {
|
||||||
setActivePage(targetPage - 1);
|
setActivePage(targetPage - 1);
|
||||||
}}
|
}}
|
||||||
onNextPage={handleFetchNextPage}
|
onNextPage={nextPage}
|
||||||
onPreviousPage={handleFetchPreviousPage}
|
onPreviousPage={previousPage}
|
||||||
onFirstPage={() => {
|
onFirstPage={() => {
|
||||||
setActivePage(0);
|
setActivePage(0);
|
||||||
}}
|
}}
|
||||||
onLastPage={() => {
|
onLastPage={() => {
|
||||||
setActivePage(data.countPages - 1);
|
setActivePage(invites.countPages - 1);
|
||||||
}}
|
}}
|
||||||
withEdges
|
withEdges
|
||||||
/>
|
/>
|
||||||
@@ -168,9 +162,10 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|||||||
const translations = await getServerSideTranslations(
|
const translations = await getServerSideTranslations(
|
||||||
manageNamespaces,
|
manageNamespaces,
|
||||||
ctx.locale,
|
ctx.locale,
|
||||||
undefined,
|
ctx.req,
|
||||||
undefined
|
ctx.res
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...translations,
|
...translations,
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import { ReactNode, useMemo, useState } from 'react';
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { prisma } from '~/server/db';
|
import { prisma } from '~/server/db';
|
||||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||||
import { onboardNamespaces } from '~/tools/server/translation-namespaces';
|
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||||
import { signUpFormSchema } from '~/validations/user';
|
import { signUpFormSchema } from '~/validations/user';
|
||||||
@@ -225,12 +224,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const translations = await getServerSideTranslations(
|
const translations = await getServerSideTranslations([], ctx.locale, ctx.req, ctx.res);
|
||||||
onboardNamespaces,
|
|
||||||
ctx.locale,
|
|
||||||
ctx.req,
|
|
||||||
ctx.res
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -200,12 +200,7 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res, locale
|
|||||||
await helpers.user.withSettings.prefetch();
|
await helpers.user.withSettings.prefetch();
|
||||||
await helpers.boards.all.prefetch();
|
await helpers.boards.all.prefetch();
|
||||||
|
|
||||||
const translations = await getServerSideTranslations(
|
const translations = await getServerSideTranslations(['user/preferences'], locale, req, res);
|
||||||
['user/preferences'],
|
|
||||||
locale,
|
|
||||||
undefined,
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...translations,
|
...translations,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { z } from 'zod';
|
|||||||
import { configExists } from '~/tools/config/configExists';
|
import { configExists } from '~/tools/config/configExists';
|
||||||
import { getFrontendConfig } from '~/tools/config/getFrontendConfig';
|
import { getFrontendConfig } from '~/tools/config/getFrontendConfig';
|
||||||
import { BackendConfigType, ConfigType } from '~/types/config';
|
import { BackendConfigType, ConfigType } from '~/types/config';
|
||||||
import { boardCustomizationSchema } from '~/validations/dashboards';
|
import { boardCustomizationSchema } from '~/validations/boards';
|
||||||
import { IRssWidget } from '~/widgets/rss/RssWidgetTile';
|
import { IRssWidget } from '~/widgets/rss/RssWidgetTile';
|
||||||
|
|
||||||
import { getConfig } from '../../../tools/config/getConfig';
|
import { getConfig } from '../../../tools/config/getConfig';
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await createUserInNotExist(ctx, input, {
|
await createUserIfNotPresent(ctx, input, {
|
||||||
defaultSettings: {
|
defaultSettings: {
|
||||||
colorScheme: colorSchemeParser.parse(ctx.cookies[COOKIE_COLOR_SCHEME_KEY]),
|
colorScheme: colorSchemeParser.parse(ctx.cookies[COOKIE_COLOR_SCHEME_KEY]),
|
||||||
language: ctx.cookies[COOKIE_LOCALE_KEY] ?? 'en',
|
language: ctx.cookies[COOKIE_LOCALE_KEY] ?? 'en',
|
||||||
@@ -62,7 +62,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await createUserInNotExist(ctx, input, {
|
await createUserIfNotPresent(ctx, input, {
|
||||||
defaultSettings: {
|
defaultSettings: {
|
||||||
colorScheme: colorSchemeParser.parse(ctx.cookies[COOKIE_COLOR_SCHEME_KEY]),
|
colorScheme: colorSchemeParser.parse(ctx.cookies[COOKIE_COLOR_SCHEME_KEY]),
|
||||||
language: ctx.cookies[COOKIE_LOCALE_KEY] ?? 'en',
|
language: ctx.cookies[COOKIE_LOCALE_KEY] ?? 'en',
|
||||||
@@ -235,7 +235,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
return {
|
return {
|
||||||
users: users.map((user) => ({
|
users: users.map((user) => ({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name!,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
emailVerified: user.emailVerified,
|
emailVerified: user.emailVerified,
|
||||||
})),
|
})),
|
||||||
@@ -243,25 +243,25 @@ export const userRouter = createTRPCRouter({
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
create: adminProcedure.input(createNewUserSchema).mutation(async ({ ctx, input }) => {
|
create: adminProcedure.input(createNewUserSchema).mutation(async ({ ctx, input }) => {
|
||||||
await createUserInNotExist(ctx, input);
|
await createUserIfNotPresent(ctx, input);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
deleteUser: adminProcedure
|
deleteUser: adminProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
userId: z.string(),
|
id: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
await ctx.prisma.user.delete({
|
await ctx.prisma.user.delete({
|
||||||
where: {
|
where: {
|
||||||
id: input.userId,
|
id: input.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createUserInNotExist = async (
|
const createUserIfNotPresent = async (
|
||||||
ctx: TRPCContext,
|
ctx: TRPCContext,
|
||||||
input: z.infer<typeof createNewUserSchema>,
|
input: z.infer<typeof createNewUserSchema>,
|
||||||
options: {
|
options: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PrismaAdapter } from '@next-auth/prisma-adapter';
|
import { PrismaAdapter } from '@next-auth/prisma-adapter';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
|
import Consola from 'consola';
|
||||||
import Cookies from 'cookies';
|
import Cookies from 'cookies';
|
||||||
import { type GetServerSidePropsContext, type NextApiRequest, type NextApiResponse } from 'next';
|
import { type GetServerSidePropsContext, type NextApiRequest, type NextApiResponse } from 'next';
|
||||||
import { type DefaultSession, type NextAuthOptions, getServerSession } from 'next-auth';
|
import { type DefaultSession, type NextAuthOptions, getServerSession } from 'next-auth';
|
||||||
@@ -150,15 +151,15 @@ export const constructAuthOptions = (
|
|||||||
return null;
|
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);
|
const isValidPassword = await bcrypt.compare(data.password, user.password);
|
||||||
|
|
||||||
if (!isValidPassword) {
|
if (!isValidPassword) {
|
||||||
console.log(`password for user ${user.id} was incorrect`);
|
Consola.log(`password for user ${user.id} was incorrect`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`user ${user.id} successfully authorized`);
|
Consola.log(`user ${user.id} successfully authorized`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
export const boardNamespaces = [
|
export const boardNamespaces = [
|
||||||
'common',
|
|
||||||
'zod',
|
|
||||||
'layout/element-selector/selector',
|
'layout/element-selector/selector',
|
||||||
'layout/modals/add-app',
|
'layout/modals/add-app',
|
||||||
'layout/modals/change-position',
|
'layout/modals/change-position',
|
||||||
'layout/modals/about',
|
|
||||||
'layout/common',
|
'layout/common',
|
||||||
'layout/header/actions/toggle-edit-mode',
|
'layout/header/actions/toggle-edit-mode',
|
||||||
'layout/mobile/drawer',
|
'layout/mobile/drawer',
|
||||||
@@ -42,16 +39,9 @@ export const boardNamespaces = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const manageNamespaces = [
|
export const manageNamespaces = [
|
||||||
'user/preferences',
|
'manage/common',
|
||||||
'user/manage',
|
'manage/boards',
|
||||||
'user/invites',
|
'manage/users',
|
||||||
'user/create',
|
'manage/users/invites',
|
||||||
'boards/manage',
|
'manage/users/create',
|
||||||
'zod',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
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 { DEFAULT_THEME, MANTINE_COLORS, MantineColor } from '@mantine/core';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
export const createDashboardSchemaValidation = z.object({
|
export const createBoardSchemaValidation = z.object({
|
||||||
name: z.string().min(2).max(25),
|
name: z.string().min(2).max(25),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { CustomErrorParams } from '~/utils/i18n-zod-resolver';
|
import { CustomErrorParams } from '~/utils/i18n-zod-resolver';
|
||||||
|
|
||||||
|
export const minPasswordLength = 8;
|
||||||
|
|
||||||
export const passwordSchema = z
|
export const passwordSchema = z
|
||||||
.string()
|
.string()
|
||||||
.min(8)
|
.min(minPasswordLength)
|
||||||
.max(100)
|
.max(100)
|
||||||
.refine((value) => /[0-9]/.test(value))
|
.refine((value) => /[0-9]/.test(value))
|
||||||
.refine((value) => /[a-z]/.test(value))
|
.refine((value) => /[a-z]/.test(value))
|
||||||
@@ -18,12 +20,12 @@ export const signInSchema = z.object({
|
|||||||
export const signUpFormSchema = z
|
export const signUpFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
username: z.string().min(3),
|
username: z.string().min(3),
|
||||||
password: z.string().min(8),
|
password: z.string().min(minPasswordLength),
|
||||||
passwordConfirmation: z.string().min(8),
|
passwordConfirmation: z.string().min(minPasswordLength),
|
||||||
})
|
})
|
||||||
.refine((data) => data.password === data.passwordConfirmation, {
|
.refine((data) => data.password === data.passwordConfirmation, {
|
||||||
params: {
|
params: {
|
||||||
i18n: { key: 'password_match' },
|
i18n: { key: 'passwordMatch' },
|
||||||
} satisfies CustomErrorParams,
|
} satisfies CustomErrorParams,
|
||||||
path: ['passwordConfirmation'],
|
path: ['passwordConfirmation'],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
IconSun,
|
IconSun,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { CustomTypeOptions } from 'i18next';
|
|
||||||
|
|
||||||
interface WeatherIconProps {
|
interface WeatherIconProps {
|
||||||
code: number;
|
code: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user