mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 23:15:46 +01:00
♻️ Onboarding page
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
# Prisma
|
# Prisma
|
||||||
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
# https://www.prisma.io/docs/reference/database-reference/connection-urls#env
|
||||||
DATABASE_URL="file:./db.sqlite"
|
DATABASE_URL="file:../database/db.sqlite"
|
||||||
|
|
||||||
# Next Auth
|
# Next Auth
|
||||||
# You can generate a new secret on the command line with:
|
# You can generate a new secret on the command line with:
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -58,4 +58,5 @@ public/locales/*
|
|||||||
!public/locales/en
|
!public/locales/en
|
||||||
|
|
||||||
#database
|
#database
|
||||||
prisma/db.sqlite
|
prisma/db.sqlite
|
||||||
|
database/*.sqlite
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
"@mantine/modals": "^6.0.0",
|
"@mantine/modals": "^6.0.0",
|
||||||
"@mantine/next": "^6.0.0",
|
"@mantine/next": "^6.0.0",
|
||||||
"@mantine/notifications": "^6.0.0",
|
"@mantine/notifications": "^6.0.0",
|
||||||
|
"@mantine/prism": "^6.0.19",
|
||||||
"@mantine/tiptap": "^6.0.17",
|
"@mantine/tiptap": "^6.0.17",
|
||||||
"@next-auth/prisma-adapter": "^1.0.5",
|
"@next-auth/prisma-adapter": "^1.0.5",
|
||||||
"@nivo/core": "^0.83.0",
|
"@nivo/core": "^0.83.0",
|
||||||
|
|||||||
1
public/imgs/app-icons/truenas.svg
Normal file
1
public/imgs/app-icons/truenas.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg viewBox="0 0 90 90" xmlns="http://www.w3.org/2000/svg"><g fill="none"><path fill="#31BEEC" d="M90 38.197v19.137L48.942 80.999V61.864z"/><path d="M41.086 61.863V81L0 57.333V38.197l18.566 10.687c.02.016.043.03.067.04l22.453 12.94Z" fill="#0095D5"/><path fill="#AEADAE" d="m61.621 45.506-16.607 9.576-16.622-9.576 16.622-9.575z"/><path fill="#0095D5" d="M86.086 31.416 69.464 40.99 48.942 29.15V10z"/><path fill="#31BEEC" d="M41.086 10v19.15l-20.55 11.827-16.621-9.561z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 484 B |
1
public/imgs/app-icons/unraid-alt.svg
Normal file
1
public/imgs/app-icons/unraid-alt.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><defs><linearGradient id="a" x1="100%" x2="0" y1="0" y2="100%" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ff8d30"/><stop offset="1" stop-color="#e32929"/></linearGradient></defs><circle cx="50%" cy="50%" r="50%" fill="url(#a)"/><path fill="#fff" d="M246.6 200.8h18.7v110.6h-18.7zm-182.3 0H83v110.7H64.3zm91.1 123.9h18.7V367h-18.7zm-45.7-47.5h18.7v68.5h-18.7zm91.2 0h18.6v68.4h-18.6zm228.2-76.5h18.7v110.7h-18.7zM338 145.5h18.7v42.3H338zm45.7 21.2h18.7v68.2h-18.7zm-91.5 0h18.7v68.1h-18.7z"/></svg>
|
||||||
|
After Width: | Height: | Size: 577 B |
41
src/components/Onboarding/onboarding-steps.tsx
Normal file
41
src/components/Onboarding/onboarding-steps.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Stack, Stepper } from '@mantine/core';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { StepUpdatePathMappings } from './step-update-path-mappings';
|
||||||
|
import { StepCreateAccount } from './step-create-account';
|
||||||
|
import { StepOnboardingFinished } from './step-onboarding-finished';
|
||||||
|
import { StepDockerImport } from './step-docker-import';
|
||||||
|
import { StepDocumentation } from './step-documentation';
|
||||||
|
|
||||||
|
export const OnboardingSteps = ({ isUpdate }: { isUpdate: boolean }) => {
|
||||||
|
const [currentStep, setCurrentStep] = useState(0);
|
||||||
|
const nextStep = () => setCurrentStep((current) => (current < 4 ? current + 1 : current));
|
||||||
|
const prevStep = () => setCurrentStep((current) => (current > 0 ? current - 1 : current));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack p="lg">
|
||||||
|
<Stepper active={currentStep} onStepClick={setCurrentStep} breakpoint="sm">
|
||||||
|
{isUpdate && (
|
||||||
|
<Stepper.Step
|
||||||
|
label="Update your installation"
|
||||||
|
description="Adjust path mappings and variables"
|
||||||
|
>
|
||||||
|
<StepUpdatePathMappings next={nextStep} />
|
||||||
|
</Stepper.Step>
|
||||||
|
)}
|
||||||
|
<Stepper.Step label="Your account" description="Create an account">
|
||||||
|
<StepCreateAccount next={nextStep} />
|
||||||
|
</Stepper.Step>
|
||||||
|
<Stepper.Step label="Docker import" description="Import applications from Docker">
|
||||||
|
<StepDockerImport next={nextStep} />
|
||||||
|
</Stepper.Step>
|
||||||
|
<Stepper.Step label="Documentation" description="Introduction into Homarr">
|
||||||
|
<StepDocumentation next={nextStep} />
|
||||||
|
</Stepper.Step>
|
||||||
|
<Stepper.Completed>
|
||||||
|
<StepOnboardingFinished />
|
||||||
|
</Stepper.Completed>
|
||||||
|
</Stepper>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
79
src/components/Onboarding/step-create-account.tsx
Normal file
79
src/components/Onboarding/step-create-account.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { Button, Card, PasswordInput, Stack, TextInput, Title } from '@mantine/core';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
|
import { signIn } from 'next-auth/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { api } from '~/utils/api';
|
||||||
|
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||||
|
import { signUpFormSchema } from '~/validations/user';
|
||||||
|
|
||||||
|
export const StepCreateAccount = ({ next }: { next: () => void }) => {
|
||||||
|
const [isSigninIn, setIsSigninIn] = useState(false);
|
||||||
|
const { mutateAsync } = api.user.createOwnerAccount.useMutation();
|
||||||
|
const { i18nZodResolver } = useI18nZodResolver();
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof signUpFormSchema>>({
|
||||||
|
validate: i18nZodResolver(signUpFormSchema),
|
||||||
|
validateInputOnBlur: true,
|
||||||
|
});
|
||||||
|
const handleSubmit = (values: z.infer<typeof signUpFormSchema>) => {
|
||||||
|
setIsSigninIn(true);
|
||||||
|
void mutateAsync(values, {
|
||||||
|
onSuccess: () => {
|
||||||
|
signIn('credentials', {
|
||||||
|
redirect: false,
|
||||||
|
name: values.username,
|
||||||
|
password: values.password,
|
||||||
|
callbackUrl: '/',
|
||||||
|
}).then((response) => {
|
||||||
|
if (!response?.ok) {
|
||||||
|
setIsSigninIn(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card shadow="lg" withBorder>
|
||||||
|
<Title order={2} align="center" mb="md">
|
||||||
|
Create your administrator account
|
||||||
|
</Title>
|
||||||
|
<form
|
||||||
|
style={{ width: '100%', display: 'flex', justifyContent: 'center' }}
|
||||||
|
onSubmit={form.onSubmit(handleSubmit)}
|
||||||
|
>
|
||||||
|
<Stack w={400} maw="90%" spacing="sm" align="center">
|
||||||
|
<TextInput
|
||||||
|
size="md"
|
||||||
|
w="100%"
|
||||||
|
label="Username"
|
||||||
|
withAsterisk
|
||||||
|
{...form.getInputProps('username')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PasswordInput
|
||||||
|
size="md"
|
||||||
|
w="100%"
|
||||||
|
label="Password"
|
||||||
|
withAsterisk
|
||||||
|
{...form.getInputProps('password')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PasswordInput
|
||||||
|
size="md"
|
||||||
|
w="100%"
|
||||||
|
label="Confirm password"
|
||||||
|
withAsterisk
|
||||||
|
{...form.getInputProps('passwordConfirmation')}
|
||||||
|
/>
|
||||||
|
<Button disabled={!form.isValid()} mt="sm" fullWidth type="submit" loading={isSigninIn}>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
18
src/components/Onboarding/step-docker-import.tsx
Normal file
18
src/components/Onboarding/step-docker-import.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Button, Card, Stack, Title } from '@mantine/core';
|
||||||
|
import { IconArrowRight } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
export const StepDockerImport = ({ next }: { next: () => void }) => {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Title order={2} align="center" mb="lg">
|
||||||
|
Automatic container import
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Stack align="center">
|
||||||
|
<Button onClick={next} rightIcon={<IconArrowRight size="1rem" />}>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
27
src/components/Onboarding/step-documentation.tsx
Normal file
27
src/components/Onboarding/step-documentation.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Button, Card, Divider, Stack, Text, Title } from '@mantine/core';
|
||||||
|
import { IconArrowRight, IconLink } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
export const StepDocumentation = ({ next }: { next: () => void }) => {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Title order={2} align="center" mb="lg">
|
||||||
|
Documentation
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
<Stack align="center">
|
||||||
|
<Text>We highly encourage you to read the documentation, before you continue.</Text>
|
||||||
|
<Button
|
||||||
|
component="a"
|
||||||
|
href="https://homarr.dev/docs/introduction/after-the-installation"
|
||||||
|
target="_blank"
|
||||||
|
leftIcon={<IconLink size="1rem" />}
|
||||||
|
variant="light"
|
||||||
|
>
|
||||||
|
Open documentation
|
||||||
|
</Button>
|
||||||
|
<Divider w={400} maw="100%" />
|
||||||
|
<Button onClick={next} rightIcon={<IconArrowRight size="1rem" />}>Finish</Button>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
56
src/components/Onboarding/step-onboarding-finished.tsx
Normal file
56
src/components/Onboarding/step-onboarding-finished.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { Box, Card, NavLink, Stack, Text, Title, createStyles } from '@mantine/core';
|
||||||
|
import {
|
||||||
|
IconChevronRight,
|
||||||
|
IconDashboard,
|
||||||
|
IconFileText,
|
||||||
|
IconManualGearbox,
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
export const StepOnboardingFinished = () => {
|
||||||
|
const { classes } = useStyles();
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Stack align="center">
|
||||||
|
<Image src="/imgs/logo/logo.svg" alt="" width={50} height={50} />
|
||||||
|
<Title order={2} align="center">
|
||||||
|
Congratulations, you've set Homarr up!
|
||||||
|
</Title>
|
||||||
|
<Text>Awesome! What do you want to do next?</Text>
|
||||||
|
|
||||||
|
<Stack>
|
||||||
|
<NavLink
|
||||||
|
rightSection={<IconChevronRight size="0.8rem" stroke={1.5} />}
|
||||||
|
className={classes.link}
|
||||||
|
icon={<IconDashboard />}
|
||||||
|
label="Go to your board"
|
||||||
|
variant="light"
|
||||||
|
active
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
rightSection={<IconChevronRight size="0.8rem" stroke={1.5} />}
|
||||||
|
className={classes.link}
|
||||||
|
icon={<IconManualGearbox />}
|
||||||
|
label="Go to the management dashboard"
|
||||||
|
variant="light"
|
||||||
|
active
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
rightSection={<IconChevronRight size="0.8rem" stroke={1.5} />}
|
||||||
|
className={classes.link}
|
||||||
|
icon={<IconFileText />}
|
||||||
|
label="Check out the documentation"
|
||||||
|
variant="light"
|
||||||
|
active
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStyles = createStyles((theme) => ({
|
||||||
|
link: {
|
||||||
|
borderRadius: '0.4rem',
|
||||||
|
},
|
||||||
|
}));
|
||||||
169
src/components/Onboarding/step-update-path-mappings.tsx
Normal file
169
src/components/Onboarding/step-update-path-mappings.tsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { Button, Card, Code, Group, List, Tabs, TabsValue, Text } from '@mantine/core';
|
||||||
|
import { Prism } from '@mantine/prism';
|
||||||
|
import { IconArrowRight, IconBrandDebian, IconBrandDocker, IconInfoSquareRounded } from '@tabler/icons-react';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const dockerRunCommand = `docker run \\
|
||||||
|
--name homarr \\
|
||||||
|
--restart unless-stopped \\
|
||||||
|
-p 7575:7575 \\
|
||||||
|
-v your-path/homarr/configs:/app/data/configs \\
|
||||||
|
-v your-path/homarr/data:/app/prisma \\
|
||||||
|
-v your-path/homarr/icons:/app/public/icons \\
|
||||||
|
-d ghcr.io/ajnart/homarr:latest`;
|
||||||
|
|
||||||
|
const dockerComposeCommand = `version: '3'
|
||||||
|
#---------------------------------------------------------------------#
|
||||||
|
# Homarr - A simple, yet powerful dashboard for your server. #
|
||||||
|
#---------------------------------------------------------------------#
|
||||||
|
services:
|
||||||
|
homarr:
|
||||||
|
container_name: homarr
|
||||||
|
image: ghcr.io/ajnart/homarr:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./homarr/configs:/app/data/configs
|
||||||
|
- ./homarr/data:/app/prisma
|
||||||
|
- ./homarr/icons:/app/public/icons
|
||||||
|
ports:
|
||||||
|
- '7575:7575'`;
|
||||||
|
|
||||||
|
const added = { color: 'green', label: '+' };
|
||||||
|
|
||||||
|
export const StepUpdatePathMappings = ({ next }: { next: () => void }) => {
|
||||||
|
const [selectedTab, setSelectedTab] = useState<TabsValue | null>(null);
|
||||||
|
return (
|
||||||
|
<Card shadow="lg" withBorder>
|
||||||
|
<Text color="dimmed">
|
||||||
|
Homarr has updated the location of the saved data. We detected, that your instance might
|
||||||
|
need an update to function as expected. It is recommended, that you take a backup of your
|
||||||
|
.json configuration file on the file system and copy it, in case something goes wrong.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text weight="bold" mt="xl">
|
||||||
|
What is your installation method?
|
||||||
|
</Text>
|
||||||
|
<Tabs value={selectedTab} onTabChange={(tab) => setSelectedTab(tab)} mt="xs">
|
||||||
|
<Tabs.List>
|
||||||
|
<Tabs.Tab value="standard_docker" icon={<IconBrandDocker size={16} />}>
|
||||||
|
Docker
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="docker_compose" icon={<IconBrandDocker size={16} />}>
|
||||||
|
Docker Compose
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="standalone" icon={<IconBrandDebian size={16} />}>
|
||||||
|
Standalone Linux / Windows
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab
|
||||||
|
value="unraid"
|
||||||
|
icon={<Image width={16} height={16} src="/imgs/app-icons/unraid-alt.svg" alt="" />}
|
||||||
|
>
|
||||||
|
Unraid
|
||||||
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="others" icon={<IconInfoSquareRounded size={16} />}>
|
||||||
|
Others
|
||||||
|
</Tabs.Tab>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
<Tabs.Panel value="standard_docker" pt="xs">
|
||||||
|
<List>
|
||||||
|
<List.Item>
|
||||||
|
<Text>
|
||||||
|
<b>Back up your configuration</b>. In case you didn't mount your configuration
|
||||||
|
correctly, you could risk loosing your dashboard. To back up,
|
||||||
|
<b>go on your file system and copy the directory, containing your </b>
|
||||||
|
<Code>default.json</Code> to your local machine.
|
||||||
|
</Text>
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<Text>
|
||||||
|
Before you continue, check that you still have the command, that you set up Homarr
|
||||||
|
with. Otherwise, your configuration might not be loaded correctly or icons are
|
||||||
|
missing.
|
||||||
|
</Text>
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<Text>
|
||||||
|
Run <Code>docker rm homarr</Code>, where <Code>homarr</Code> indicates the name of
|
||||||
|
your container
|
||||||
|
</Text>
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<Text>
|
||||||
|
Run <Code>docker run ...</Code> again, that you used to create the Homarr container.
|
||||||
|
Note, that you need to add a new line:
|
||||||
|
</Text>
|
||||||
|
<Prism highlightLines={{ 6: added }} language="bash" withLineNumbers>
|
||||||
|
{dockerRunCommand}
|
||||||
|
</Prism>
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>Refresh this page and click on "continue"</List.Item>
|
||||||
|
</List>
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
<Tabs.Panel value="docker_compose" pt="xs">
|
||||||
|
<List>
|
||||||
|
<List.Item>
|
||||||
|
<Text>
|
||||||
|
<b>Back up your configuration</b>. In case you didn't mount your configuration
|
||||||
|
correctly, you could risk loosing your dashboard. To back up,
|
||||||
|
<b>go on your file system and copy the directory, containing your </b>
|
||||||
|
<Code>default.json</Code> to your local machine.
|
||||||
|
</Text>
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<Text>
|
||||||
|
Navigate to the directory, where the <Code>docker-compose.yml</Code> for Homarr is
|
||||||
|
located.
|
||||||
|
</Text>
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<Text>
|
||||||
|
Run <Code>docker compose down</Code>
|
||||||
|
</Text>
|
||||||
|
</List.Item>
|
||||||
|
<List.Item>
|
||||||
|
<Text>
|
||||||
|
Edit <Code>docker-compose.yml</Code> using text editor. Use Notepad or VSC on GUI
|
||||||
|
based systems. Use <Code>nano</Code> or <Code>vim</Code> on terminal systems.
|
||||||
|
</Text>
|
||||||
|
<Prism highlightLines={{ 12: added }} language="bash" withLineNumbers>
|
||||||
|
{dockerComposeCommand}
|
||||||
|
</Prism>
|
||||||
|
</List.Item>
|
||||||
|
</List>
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
<Tabs.Panel value="standalone" pt="xs">
|
||||||
|
<Text>
|
||||||
|
You're lucky. For installation <b>without Docker</b> on Windows and Linux, there are no
|
||||||
|
additional steps required. However, be advised that your backups should start to include
|
||||||
|
the files located at <Code>/prisma</Code> too, if you run automatic backups.
|
||||||
|
</Text>
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
<Tabs.Panel value="unraid" pt="xs">
|
||||||
|
<List>
|
||||||
|
<List.Item>Click on your Homarr application and click "Edit"</List.Item>
|
||||||
|
<List.Item>Scroll down and click on the link "Add another path, port, variable or device"</List.Item>
|
||||||
|
<List.Item>After the new modal has opened, make sure that "Path" has been selected at the top</List.Item>
|
||||||
|
<List.Item>In the container path, enter <Code>/app/prisma</Code></List.Item>
|
||||||
|
<List.Item>In the host path, enter a new path on your host system. Choose a similar path, but the innermost directory should be different, than your existing mounting points (eg. <Code>/mnt/user/appdata/homarr/data</Code>)</List.Item>
|
||||||
|
</List>
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
<Tabs.Panel value="others" pt="xs">
|
||||||
|
<Text>We are sadly not able to include upgrade guides for all kind of systems. If your system was not listed, you should mount this new mounting point in your container:</Text>
|
||||||
|
<Code>/app/prisma</Code>
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
{selectedTab && (
|
||||||
|
<Group align="end" mt="lg">
|
||||||
|
<Button onClick={next} rightIcon={<IconArrowRight />}>Continue</Button>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,12 +3,14 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Center,
|
Center,
|
||||||
|
Divider,
|
||||||
Flex,
|
Flex,
|
||||||
Grid,
|
Grid,
|
||||||
Group,
|
Group,
|
||||||
Image,
|
Image,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
Stack,
|
Stack,
|
||||||
|
Stepper,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
@@ -17,32 +19,34 @@ import {
|
|||||||
useMantineTheme,
|
useMantineTheme,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { useForm } from '@mantine/form';
|
||||||
import { useMediaQuery } from '@mantine/hooks';
|
import { useDisclosure, useMediaQuery } from '@mantine/hooks';
|
||||||
import { IconLayoutDashboard, IconUserCog } from '@tabler/icons-react';
|
import { IconLayoutDashboard, IconUserCog } from '@tabler/icons-react';
|
||||||
import { IconArrowRight, IconBook2, IconUserPlus } from '@tabler/icons-react';
|
import { IconArrowRight, IconBook2, IconUserPlus } from '@tabler/icons-react';
|
||||||
import { GetServerSideProps } from 'next';
|
import fs from 'fs';
|
||||||
|
import { GetServerSideProps, GetServerSidePropsResult, InferGetServerSidePropsType } from 'next';
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from 'next-auth/react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ReactNode, useMemo, useState } from 'react';
|
import { ReactNode, useMemo, useState } from 'react';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { OnboardingSteps } from '~/components/Onboarding/onboarding-steps';
|
||||||
import { prisma } from '~/server/db';
|
import { prisma } from '~/server/db';
|
||||||
|
import { getConfig } from '~/tools/config/getConfig';
|
||||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
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 getStepContents = () => [FirstStepContent, SecondStepContent, ThirdStepContent] as const;
|
export default function OnboardPage({
|
||||||
|
configSchemaVersions,
|
||||||
export default function OnboardPage() {
|
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||||
const { fn, colors, breakpoints, colorScheme } = useMantineTheme();
|
const { fn, colors, colorScheme } = useMantineTheme();
|
||||||
const [currentStep, setStep] = useState(0);
|
|
||||||
const next = () => setStep((prev) => prev + 1);
|
|
||||||
const isSmallerThanMd = useMediaQuery(`(max-width: ${breakpoints.sm})`);
|
|
||||||
const stepContents = useMemo(() => getStepContents(), []);
|
|
||||||
const CurrentStepComponent = useMemo(() => stepContents[currentStep], [currentStep]);
|
|
||||||
const background = colorScheme === 'dark' ? 'dark.6' : 'gray.1';
|
const background = colorScheme === 'dark' ? 'dark.6' : 'gray.1';
|
||||||
|
|
||||||
|
const [onboardingSteps, { open: showOnboardingSteps }] = useDisclosure(false);
|
||||||
|
|
||||||
|
const isUpgradeFromSchemaOne = configSchemaVersions.includes(1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -50,50 +54,46 @@ export default function OnboardPage() {
|
|||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<Stack h="100dvh" bg={background} spacing={0}>
|
<Stack h="100dvh" bg={background} spacing={0}>
|
||||||
<Center bg={fn.linearGradient(145, colors.red[7], colors.red[5])} h="35%">
|
<Center bg={fn.linearGradient(145, colors.red[7], colors.red[5])} h={175}>
|
||||||
<Center bg={background} w={128} h={128} style={{ borderRadius: 64 }}>
|
<Center bg={background} w={100} h={100} style={{ borderRadius: 64 }}>
|
||||||
<Image width={96} src="/imgs/logo/logo-color.svg" alt="Homarr Logo" />
|
<Image width={70} src="/imgs/logo/logo-color.svg" alt="Homarr Logo" />
|
||||||
</Center>
|
</Center>
|
||||||
</Center>
|
</Center>
|
||||||
<Stack spacing="xl" p="md" align="center">
|
|
||||||
<Group>
|
{onboardingSteps ? (
|
||||||
{stepContents.map((_, index) => (
|
<OnboardingSteps isUpdate={isUpgradeFromSchemaOne} />
|
||||||
<Step
|
) : (
|
||||||
key={index}
|
<Center h="100%">
|
||||||
isCurrent={currentStep === index}
|
<Stack align="center" p="lg">
|
||||||
isMobile={isSmallerThanMd}
|
<Title order={1} weight={800} size="3rem" opacity={0.8}>
|
||||||
isDark={colorScheme === 'dark'}
|
Welcome to Homarr!
|
||||||
/>
|
</Title>
|
||||||
))}
|
<Text size="lg" mb={40}>
|
||||||
</Group>
|
Your favorite dashboard has received a big upgrade.
|
||||||
<CurrentStepComponent isMobile={isSmallerThanMd} next={next} />
|
<br />
|
||||||
</Stack>
|
We'll help you update within the next few steps
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={showOnboardingSteps}
|
||||||
|
rightIcon={<IconArrowRight size="1rem" />}
|
||||||
|
variant="default"
|
||||||
|
>
|
||||||
|
Start update process
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type StepProps = {
|
|
||||||
isCurrent: boolean;
|
|
||||||
isMobile: boolean;
|
|
||||||
isDark: boolean;
|
|
||||||
};
|
|
||||||
const Step = ({ isCurrent, isMobile, isDark }: StepProps) => {
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
h={isMobile ? 16 : 20}
|
|
||||||
w={isMobile ? 16 : 20}
|
|
||||||
bg={isCurrent ? 'red.6' : isDark ? 'dark.3' : 'gray.4'}
|
|
||||||
style={{ borderRadius: 10 }}
|
|
||||||
></Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type StepContentComponent = (props: { isMobile: boolean; next: () => void }) => ReactNode;
|
type StepContentComponent = (props: { isMobile: boolean; next: () => void }) => ReactNode;
|
||||||
|
|
||||||
const FirstStepContent: StepContentComponent = ({ isMobile, next }) => {
|
const FirstStepContent: StepContentComponent = ({ isMobile, next }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<Stepper.Step label="First step" description="Create an account">
|
||||||
<Stack spacing={4} align="center">
|
<Stack spacing={4} align="center">
|
||||||
<Title order={isMobile ? 3 : 1}>Hi there!</Title>
|
<Title order={isMobile ? 3 : 1}>Hi there!</Title>
|
||||||
<Title order={isMobile ? 3 : 1}>Welcome to Homarr! 👋</Title>
|
<Title order={isMobile ? 3 : 1}>Welcome to Homarr! 👋</Title>
|
||||||
@@ -104,7 +104,7 @@ const FirstStepContent: StepContentComponent = ({ isMobile, next }) => {
|
|||||||
<Button onClick={next} size="lg" mt="sm" w={400} maw="90%">
|
<Button onClick={next} size="lg" mt="sm" w={400} maw="90%">
|
||||||
Start configuration
|
Start configuration
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</Stepper.Step>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ const SecondStepContent: StepContentComponent = ({ isMobile, next }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Stepper.Step label="Second step" description="Create an account">
|
||||||
<Title order={isMobile ? 3 : 1}>Configure your credentials</Title>
|
<Title order={isMobile ? 3 : 1}>Configure your credentials</Title>
|
||||||
<form
|
<form
|
||||||
style={{ width: '100%', display: 'flex', justifyContent: 'center' }}
|
style={{ width: '100%', display: 'flex', justifyContent: 'center' }}
|
||||||
@@ -173,7 +173,7 @@ const SecondStepContent: StepContentComponent = ({ isMobile, next }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</Stepper.Step>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ const ThirdStepContent: StepContentComponent = ({ isMobile, next }) => {
|
|||||||
const { classes } = useStyles();
|
const { classes } = useStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Stepper.Step label="Third step" description="Create an account">
|
||||||
<Title order={isMobile ? 3 : 1}>Get started! 🚀</Title>
|
<Title order={isMobile ? 3 : 1}>Get started! 🚀</Title>
|
||||||
<Grid w="100%" maw={breakpoints.sm} mt="xl">
|
<Grid w="100%" maw={breakpoints.sm} mt="xl">
|
||||||
{firstActions.map((action) => (
|
{firstActions.map((action) => (
|
||||||
@@ -225,7 +225,7 @@ const ThirdStepContent: StepContentComponent = ({ isMobile, next }) => {
|
|||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
</Stepper.Step>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -245,11 +245,16 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync('./data/configs').filter((file) => file.endsWith('.json'));
|
||||||
|
const configs = files.map((file) => getConfig(file));
|
||||||
|
const configSchemaVersions = configs.map((config) => config.schemaVersion);
|
||||||
|
|
||||||
const translations = await getServerSideTranslations([], ctx.locale, ctx.req, ctx.res);
|
const translations = await getServerSideTranslations([], ctx.locale, ctx.req, ctx.res);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
...translations,
|
...translations,
|
||||||
|
configSchemaVersions: configSchemaVersions,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
25
yarn.lock
25
yarn.lock
@@ -1086,6 +1086,21 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@mantine/prism@npm:^6.0.19":
|
||||||
|
version: 6.0.19
|
||||||
|
resolution: "@mantine/prism@npm:6.0.19"
|
||||||
|
dependencies:
|
||||||
|
"@mantine/utils": 6.0.19
|
||||||
|
prism-react-renderer: ^1.2.1
|
||||||
|
peerDependencies:
|
||||||
|
"@mantine/core": 6.0.19
|
||||||
|
"@mantine/hooks": 6.0.19
|
||||||
|
react: ">=16.8.0"
|
||||||
|
react-dom: ">=16.8.0"
|
||||||
|
checksum: ae806b6341a0a34831ffd04cd5254e0d37ae3946cb3a0d3fd774b2a86feca09815616a580a581af3338cd7f3574a4861fc79680501b207d8596787e4ddc5b447
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@mantine/ssr@npm:6.0.19":
|
"@mantine/ssr@npm:6.0.19":
|
||||||
version: 6.0.19
|
version: 6.0.19
|
||||||
resolution: "@mantine/ssr@npm:6.0.19"
|
resolution: "@mantine/ssr@npm:6.0.19"
|
||||||
@@ -6179,6 +6194,7 @@ __metadata:
|
|||||||
"@mantine/modals": ^6.0.0
|
"@mantine/modals": ^6.0.0
|
||||||
"@mantine/next": ^6.0.0
|
"@mantine/next": ^6.0.0
|
||||||
"@mantine/notifications": ^6.0.0
|
"@mantine/notifications": ^6.0.0
|
||||||
|
"@mantine/prism": ^6.0.19
|
||||||
"@mantine/tiptap": ^6.0.17
|
"@mantine/tiptap": ^6.0.17
|
||||||
"@next-auth/prisma-adapter": ^1.0.5
|
"@next-auth/prisma-adapter": ^1.0.5
|
||||||
"@next/bundle-analyzer": ^13.0.0
|
"@next/bundle-analyzer": ^13.0.0
|
||||||
@@ -8460,6 +8476,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"prism-react-renderer@npm:^1.2.1":
|
||||||
|
version: 1.3.5
|
||||||
|
resolution: "prism-react-renderer@npm:1.3.5"
|
||||||
|
peerDependencies:
|
||||||
|
react: ">=0.14.9"
|
||||||
|
checksum: c18806dcbc4c0b4fd6fd15bd06b4f7c0a6da98d93af235c3e970854994eb9b59e23315abb6cfc29e69da26d36709a47e25da85ab27fed81b6812f0a52caf6dfa
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"prisma@npm:^5.0.0":
|
"prisma@npm:^5.0.0":
|
||||||
version: 5.1.1
|
version: 5.1.1
|
||||||
resolution: "prisma@npm:5.1.1"
|
resolution: "prisma@npm:5.1.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user