feat: unify authentication pages
This commit is contained in:
8 files changed
+110
-80
No files matched your search
@@ -220,7 +220,8 @@ Clicking the `Copy & close` button will copy the URL to your clipboard.
|
||||
|
||||
### Self-onboarding
|
||||
|
||||
Clicking the invitation link will bring up a form with fields for the username and password:
|
||||
Clicking the invitation link opens a centered registration form that uses the same visual shell as Homarr's onboarding
|
||||
and sign-in pages, with fields for the username and password:
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { Card, Center, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconUserPlus } from "@tabler/icons-react";
|
||||
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { isProviderEnabled } from "@homarr/auth/server";
|
||||
import { and, db, eq } from "@homarr/db";
|
||||
import { invites } from "@homarr/db/schema";
|
||||
import { OnboardingAuthShell } from "@homarr/onboarding";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
|
||||
import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
|
||||
import { RegistrationForm } from "./_registration-form";
|
||||
|
||||
interface InviteUsagePageProps {
|
||||
@@ -48,25 +48,14 @@ export default async function InviteUsagePage(props: InviteUsagePageProps) {
|
||||
const t = await getScopedI18n("user.page.invite");
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<Stack align="center" mt="xl">
|
||||
<HomarrLogoWithTitle size="lg" />
|
||||
<Stack gap={6} align="center">
|
||||
<Title order={3} fw={400} ta="center">
|
||||
{t("title")}
|
||||
</Title>
|
||||
<Text size="sm" c="gray.5" ta="center">
|
||||
{t("subtitle")}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Card w={64 * 6} maw="90vw">
|
||||
<RegistrationForm invite={invite} />
|
||||
</Card>
|
||||
<Text size="xs" c="gray.5" ta="center">
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
|
||||
{t("description", { username: invite.creator.name! })}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
<OnboardingAuthShell
|
||||
title={t("title")}
|
||||
description={t("subtitle")}
|
||||
icon={<IconUserPlus size={24} />}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
footer={t("description", { username: invite.creator.name! })}
|
||||
>
|
||||
<RegistrationForm invite={invite} />
|
||||
</OnboardingAuthShell>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { Alert, Card, Center, Code, Stack, Text, Title } from "@mantine/core";
|
||||
import { Alert, Code, Text } from "@mantine/core";
|
||||
import { IconLogin } from "@tabler/icons-react";
|
||||
|
||||
import { env } from "@homarr/auth/env";
|
||||
import { auth } from "@homarr/auth/next";
|
||||
import { OnboardingAuthShell } from "@homarr/onboarding";
|
||||
import { getScopedI18n } from "@homarr/translation/server";
|
||||
import { sanitizeRedirectionUrl } from "@homarr/validation/redirection-url";
|
||||
|
||||
import { env as appEnv } from "~/env";
|
||||
|
||||
import { HomarrLogoWithTitle } from "~/components/layout/logo/homarr-logo";
|
||||
import { LoginForm } from "./_login-form";
|
||||
|
||||
interface LoginProps {
|
||||
@@ -29,33 +29,20 @@ export default async function Login(props: LoginProps) {
|
||||
const t = await getScopedI18n("user.page.login");
|
||||
|
||||
return (
|
||||
<Center>
|
||||
<Stack align="center" mt="xl">
|
||||
<HomarrLogoWithTitle size="lg" />
|
||||
<Stack gap={6} align="center">
|
||||
<Title order={3} fw={400} ta="center">
|
||||
{t("title")}
|
||||
</Title>
|
||||
<Text size="sm" c="gray.5" ta="center">
|
||||
{t("subtitle")}
|
||||
<OnboardingAuthShell title={t("title")} description={t("subtitle")} icon={<IconLogin size={24} />}>
|
||||
{appEnv.DEMO_MODE ? (
|
||||
<Alert icon={<IconLogin size={18} />} color="blue" variant="light">
|
||||
<Text size="sm" fw={500}>
|
||||
Demo mode is enabled. Sign in with username <Code>demo</Code> and password <Code>demo</Code>
|
||||
</Text>
|
||||
</Stack>
|
||||
{appEnv.DEMO_MODE && (
|
||||
<Alert icon={<IconLogin size={18} />} color="blue" variant="light" w={64 * 6} maw="90vw">
|
||||
<Text size="sm" fw={500}>
|
||||
Demo mode is enabled. Sign in with username <Code>demo</Code> and password <Code>demo</Code>
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
<Card w={64 * 6} maw="90vw">
|
||||
<LoginForm
|
||||
providers={env.AUTH_PROVIDERS}
|
||||
oidcClientName={env.AUTH_OIDC_CLIENT_NAME}
|
||||
isOidcAutoLoginEnabled={env.AUTH_OIDC_AUTO_LOGIN}
|
||||
callbackUrl={searchParams.callbackUrl ?? "/"}
|
||||
/>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Alert>
|
||||
) : null}
|
||||
<LoginForm
|
||||
providers={env.AUTH_PROVIDERS}
|
||||
oidcClientName={env.AUTH_OIDC_CLIENT_NAME}
|
||||
isOidcAutoLoginEnabled={env.AUTH_OIDC_AUTO_LOGIN}
|
||||
callbackUrl={searchParams.callbackUrl ?? "/"}
|
||||
/>
|
||||
</OnboardingAuthShell>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import "~/styles/color-scheme.scss";
|
||||
import "~/styles/scroll-area.scss";
|
||||
|
||||
import { notFound } from "next/navigation";
|
||||
import { ColorSchemeScript } from "@mantine/core";
|
||||
import type { DayOfWeek } from "@mantine/dates";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
|
||||
@@ -161,17 +162,16 @@ export default async function Layout(props: {
|
||||
]);
|
||||
|
||||
return (
|
||||
// Instead of ColorSchemScript we use data-mantine-color-scheme to prevent flickering
|
||||
<html
|
||||
lang={locale}
|
||||
dir={direction}
|
||||
data-mantine-color-scheme={colorScheme}
|
||||
style={{
|
||||
backgroundColor: colorScheme === "dark" ? "#242424" : colorScheme === "auto" ? undefined : "#fff",
|
||||
}}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head>
|
||||
<ColorSchemeScript defaultColorScheme={colorScheme} />
|
||||
<meta name="homarr-website-url" content={publicUrls.homarrWebsiteUrl} />
|
||||
{session ? (
|
||||
<>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Alert, Button, Group, Paper, Stack, Text, TextInput, ThemeIcon, Title } from "@mantine/core";
|
||||
import { Alert, Button, Stack, Text, TextInput } from "@mantine/core";
|
||||
import { IconArrowRight, IconShieldCheck, IconUserPlus } from "@tabler/icons-react";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
@@ -17,10 +17,9 @@ import { groupCreateSchema } from "@homarr/validation/group";
|
||||
import { userInitSchema } from "@homarr/validation/user";
|
||||
|
||||
import { didCredentialsSignInFail } from "./account-recovery";
|
||||
import { OnboardingBackdrop } from "./onboarding-backdrop";
|
||||
import { OnboardingAuthShell } from "./onboarding-auth-shell";
|
||||
import type { OnboardingStudioProps } from "./types";
|
||||
import { useOnboardingSounds } from "./use-onboarding-sounds";
|
||||
import classes from "./onboarding-studio.module.css";
|
||||
|
||||
const AccountShell = ({
|
||||
title,
|
||||
@@ -31,29 +30,9 @@ const AccountShell = ({
|
||||
description: string;
|
||||
children: React.ReactNode;
|
||||
}) => (
|
||||
<main className={classes.page}>
|
||||
<OnboardingBackdrop />
|
||||
<div className={classes.shell}>
|
||||
<Stack mih="calc(100dvh - 6rem)" justify="center" align="center">
|
||||
<Paper className={classes.studio} radius="lg" p={{ base: "lg", sm: "xl" }} w="100%" maw="36rem">
|
||||
<Stack gap="xl">
|
||||
<Group wrap="nowrap" align="flex-start">
|
||||
<ThemeIcon size="xl" radius="lg" variant="light">
|
||||
<IconShieldCheck size={24} />
|
||||
</ThemeIcon>
|
||||
<Stack gap={4}>
|
||||
<Title order={1} size="h2">
|
||||
{title}
|
||||
</Title>
|
||||
<Text c="dimmed">{description}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
{children}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Stack>
|
||||
</div>
|
||||
</main>
|
||||
<OnboardingAuthShell title={title} description={description} icon={<IconShieldCheck size={24} />}>
|
||||
{children}
|
||||
</OnboardingAuthShell>
|
||||
);
|
||||
|
||||
export const AccountSetup = ({ environment }: OnboardingStudioProps) => {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { OnboardingAuthShell } from "./onboarding-auth-shell";
|
||||
export { OnboardingStudio } from "./onboarding-studio";
|
||||
export type { OnboardingEnvironment, OnboardingStudioProps } from "./types";
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Group, Paper, Stack, Text, ThemeIcon, Title } from "@mantine/core";
|
||||
|
||||
import { OnboardingBackdrop } from "./onboarding-backdrop";
|
||||
import { OnboardingWordmark } from "./onboarding-wordmark";
|
||||
import classes from "./onboarding-studio.module.css";
|
||||
|
||||
interface OnboardingAuthShellProps {
|
||||
title: ReactNode;
|
||||
description: ReactNode;
|
||||
icon: ReactNode;
|
||||
children: ReactNode;
|
||||
footer?: ReactNode;
|
||||
}
|
||||
|
||||
export const OnboardingAuthShell = ({ title, description, icon, children, footer }: OnboardingAuthShellProps) => (
|
||||
<main className={classes.page}>
|
||||
<OnboardingBackdrop />
|
||||
<div className={`${classes.shell} ${classes.authShell}`}>
|
||||
<Stack className={classes.authLayout} gap="xl" align="center" justify="center">
|
||||
<div className={classes.authBrand}>
|
||||
<OnboardingWordmark large />
|
||||
</div>
|
||||
<Paper className={`${classes.studio} ${classes.authCard}`} radius="lg" p={{ base: "lg", sm: "xl" }}>
|
||||
<Stack gap="xl">
|
||||
<Group wrap="nowrap" align="flex-start">
|
||||
<ThemeIcon size="xl" radius="lg" variant="light">
|
||||
{icon}
|
||||
</ThemeIcon>
|
||||
<Stack gap={4}>
|
||||
<Title order={1} size="h2">
|
||||
{title}
|
||||
</Title>
|
||||
<Text c="dimmed">{description}</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
{children}
|
||||
</Stack>
|
||||
</Paper>
|
||||
{footer ? (
|
||||
<Text className={classes.authFooter} size="sm" c="dimmed" ta="center">
|
||||
{footer}
|
||||
</Text>
|
||||
) : null}
|
||||
</Stack>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
@@ -129,6 +129,31 @@
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.authShell {
|
||||
display: grid;
|
||||
min-height: 100dvh;
|
||||
padding-block: clamp(1rem, 4vw, 3rem) calc(clamp(1rem, 4vw, 3rem) + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.authLayout {
|
||||
width: min(100%, 36rem);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.authBrand {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.authCard {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.authFooter {
|
||||
max-width: 32rem;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.studioGrid {
|
||||
display: grid;
|
||||
grid-template-columns: 15rem minmax(0, 1fr);
|
||||
|
||||
Reference in new issue
Block a user