refactor: card title and layout tweaks (#675)

* refactor: card title and layout tweaks

* chore: review comments

* refactor: update domain warning screen
This commit is contained in:
Stavros
2026-03-01 13:36:43 +02:00
committed by GitHub
parent 89da4028bb
commit d0e39c6149
20 changed files with 356 additions and 142 deletions

View File

@@ -18,6 +18,11 @@ import { useOIDCParams } from "@/lib/hooks/oidc";
import { useTranslation } from "react-i18next";
import { TFunction } from "i18next";
import { Mail, Shield, User, Users } from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
type Scope = {
id: string;
@@ -27,7 +32,7 @@ type Scope = {
};
const scopeMapIconProps = {
className: "stroke-card stroke-2.5",
className: "stroke-muted-foreground stroke-[1.75] h-4",
};
const createScopeMap = (t: TFunction<"translation", undefined>): Scope[] => {
@@ -124,13 +129,15 @@ export const AuthorizePage = () => {
if (getClientInfo.isLoading) {
return (
<Card className="min-w-xs sm:min-w-sm">
<Card className="gap-0">
<CardHeader>
<CardTitle className="text-3xl">
<CardTitle className="text-xl">
{t("authorizeLoadingTitle")}
</CardTitle>
<CardDescription>{t("authorizeLoadingSubtitle")}</CardDescription>
</CardHeader>
<CardContent>
<CardDescription>{t("authorizeLoadingSubtitle")}</CardDescription>
</CardContent>
</Card>
);
}
@@ -145,41 +152,46 @@ export const AuthorizePage = () => {
}
return (
<Card className="min-w-xs sm:min-w-sm mx-4">
<CardHeader>
<CardTitle className="text-3xl">
{t("authorizeCardTitle", {
app: getClientInfo.data?.name || "Unknown",
})}
</CardTitle>
<CardDescription>
{scopes.includes("openid")
? t("authorizeSubtitle")
: t("authorizeSubtitleOAuth")}
</CardDescription>
<Card>
<CardHeader className="mb-2">
<div className="flex flex-col gap-3 items-center justify-center text-center">
<div className="bg-accent-foreground text-muted text-xl font-bold font-sans rounded-lg px-4 py-3">
{getClientInfo.data?.name.slice(0, 1)}
</div>
<CardTitle className="text-xl">
{t("authorizeCardTitle", {
app: getClientInfo.data?.name || "Unknown",
})}
</CardTitle>
<CardDescription className="text-sm max-w-sm">
{scopes.includes("openid")
? t("authorizeSubtitle")
: t("authorizeSubtitleOAuth")}
</CardDescription>
</div>
</CardHeader>
{scopes.includes("openid") && (
<CardContent className="flex flex-col gap-4">
{scopes.map((id) => {
const scope = scopeMap.find((s) => s.id === id);
if (!scope) return null;
return (
<div key={scope.id} className="flex flex-row items-center gap-3">
<div className="p-2 flex flex-col items-center justify-center bg-card-foreground rounded-md">
{scope.icon}
</div>
<div className="flex flex-col gap-0.5">
<div className="text-md">{scope.name}</div>
<div className="text-sm text-muted-foreground">
{scope.description}
</div>
</div>
</div>
);
})}
</CardContent>
)}
<CardFooter className="flex flex-col items-stretch gap-2">
<CardContent className="mb-2">
{scopes.includes("openid") && (
<div className="flex flex-wrap gap-2 items-center justify-center">
{scopes.map((id) => {
const scope = scopeMap.find((s) => s.id === id);
if (!scope) return null;
return (
<Tooltip key={scope.id}>
<TooltipTrigger className="flex flex-row justify-center items-center gap-1 rounded-full bg-secondary font-light pl-2 pr-4 py-1 border-border border">
<div>{scope.icon}</div>
<div className="text-sm text-accent-foreground">
{scope.name}
</div>
</TooltipTrigger>
<TooltipContent>{scope.description}</TooltipContent>
</Tooltip>
);
})}
</div>
)}
</CardContent>
<CardFooter className="flex flex-col items-stretch gap-3">
<Button
onClick={() => authorizeMutation.mutate()}
loading={authorizeMutation.isPending}

View File

@@ -93,9 +93,9 @@ export const ContinuePage = () => {
if (showUntrustedWarning) {
return (
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">
<Card role="alert" aria-live="assertive">
<CardHeader className="gap-1.5">
<CardTitle className="text-xl">
{t("continueUntrustedRedirectTitle")}
</CardTitle>
<CardDescription>
@@ -110,7 +110,7 @@ export const ContinuePage = () => {
/>
</CardDescription>
</CardHeader>
<CardFooter className="flex flex-col items-stretch gap-2">
<CardFooter className="flex flex-col items-stretch gap-3">
<Button
onClick={handleRedirect}
loading={isLoading}
@@ -132,9 +132,9 @@ export const ContinuePage = () => {
if (showInsecureWarning) {
return (
<Card role="alert" aria-live="assertive" className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">
<Card role="alert" aria-live="assertive">
<CardHeader className="gap-1.5">
<CardTitle className="text-xl">
{t("continueInsecureRedirectTitle")}
</CardTitle>
<CardDescription>
@@ -147,7 +147,7 @@ export const ContinuePage = () => {
/>
</CardDescription>
</CardHeader>
<CardFooter className="flex flex-col items-stretch gap-2">
<CardFooter className="flex flex-col items-stretch gap-3">
<Button
onClick={handleRedirect}
loading={isLoading}
@@ -168,16 +168,16 @@ export const ContinuePage = () => {
}
return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">
<Card>
<CardHeader className="gap-1.5">
<CardTitle className="text-xl">
{t("continueRedirectingTitle")}
</CardTitle>
<CardDescription>{t("continueRedirectingSubtitle")}</CardDescription>
</CardHeader>
{showRedirectButton && (
<CardFooter className="flex flex-col items-stretch">
<Button onClick={handleRedirect}>
<CardFooter>
<Button className="w-full" onClick={handleRedirect}>
{t("continueRedirectManually")}
</Button>
</CardFooter>

View File

@@ -14,10 +14,10 @@ export const ErrorPage = () => {
const error = searchParams.get("error") ?? "";
return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">{t("errorTitle")}</CardTitle>
<CardDescription className="flex flex-col gap-1.5">
<Card>
<CardHeader className="gap-1.5">
<CardTitle className="text-xl">{t("errorTitle")}</CardTitle>
<CardDescription className="flex flex-col gap-3">
{error ? (
<>
<p>{t("errorSubtitleInfo")}</p>

View File

@@ -1,25 +1,47 @@
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useAppContext } from "@/context/app-context";
import { useTranslation } from "react-i18next";
import Markdown from "react-markdown";
import { useNavigate } from "react-router";
export const ForgotPasswordPage = () => {
const { forgotPasswordMessage } = useAppContext();
const { t } = useTranslation();
const navigate = useNavigate();
return (
<Card className="min-w-xs sm:min-w-sm">
<Card>
<CardHeader>
<CardTitle className="text-3xl">{t("forgotPasswordTitle")}</CardTitle>
<CardDescription>
<Markdown>{forgotPasswordMessage !== "" ? forgotPasswordMessage : t('forgotPasswordMessage')}</Markdown>
</CardDescription>
<CardTitle className="text-xl">{t("forgotPasswordTitle")}</CardTitle>
</CardHeader>
<CardContent>
<CardDescription>
<Markdown>
{forgotPasswordMessage !== ""
? forgotPasswordMessage
: t("forgotPasswordMessage")}
</Markdown>
</CardDescription>
</CardContent>
<CardFooter>
<Button
className="w-full"
variant="outline"
onClick={() => {
navigate("/login");
}}
>
{t("notFoundButton")}
</Button>
</CardFooter>
</Card>
);
};

View File

@@ -22,7 +22,7 @@ import { useOIDCParams } from "@/lib/hooks/oidc";
import { LoginSchema } from "@/schemas/login-schema";
import { useMutation } from "@tanstack/react-query";
import axios, { AxiosError } from "axios";
import { useEffect, useRef, useState } from "react";
import { useEffect, useId, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Navigate, useLocation } from "react-router";
import { toast } from "sonner";
@@ -48,6 +48,8 @@ export const LoginPage = () => {
const redirectTimer = useRef<number | null>(null);
const redirectButtonTimer = useRef<number | null>(null);
const formId = useId();
const searchParams = new URLSearchParams(search);
const {
values: props,
@@ -187,9 +189,9 @@ export const LoginPage = () => {
if (isOauthAutoRedirect) {
return (
<Card className="min-w-xs sm:min-w-sm">
<Card>
<CardHeader>
<CardTitle className="text-3xl">
<CardTitle className="text-xl">
{t("loginOauthAutoRedirectTitle")}
</CardTitle>
<CardDescription>
@@ -218,9 +220,9 @@ export const LoginPage = () => {
);
}
return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-center text-3xl">{title}</CardTitle>
<Card>
<CardHeader className="gap-1.5">
<CardTitle className="text-center text-xl">{title}</CardTitle>
{providers.length > 0 && (
<CardDescription className="text-center">
{oauthProviders.length !== 0
@@ -231,7 +233,7 @@ export const LoginPage = () => {
</CardHeader>
<CardContent className="flex flex-col gap-4">
{oauthProviders.length !== 0 && (
<div className="flex flex-col gap-2 items-center justify-center">
<div className="flex flex-col gap-2.5 items-center justify-center">
{oauthProviders.map((provider) => (
<OAuthButton
key={provider.id}
@@ -252,6 +254,7 @@ export const LoginPage = () => {
<LoginForm
onSubmit={(values) => loginMutate(values)}
loading={loginIsPending || oauthIsPending}
formId={formId}
/>
)}
{providers.length == 0 && (
@@ -260,6 +263,18 @@ export const LoginPage = () => {
</p>
)}
</CardContent>
<CardFooter>
{userAuthConfigured && (
<Button
className="w-full"
type="submit"
form={formId}
loading={loginIsPending || oauthIsPending}
>
{t("loginSubmit")}
</Button>
)}
</CardFooter>
</Card>
);
};

View File

@@ -52,9 +52,9 @@ export const LogoutPage = () => {
}
return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">{t("logoutTitle")}</CardTitle>
<Card>
<CardHeader className="gap-1.5">
<CardTitle className="text-xl">{t("logoutTitle")}</CardTitle>
<CardDescription>
{provider !== "local" && provider !== "ldap" ? (
<Trans
@@ -84,8 +84,10 @@ export const LogoutPage = () => {
)}
</CardDescription>
</CardHeader>
<CardFooter className="flex flex-col items-stretch">
<CardFooter>
<Button
className="w-full"
variant="outline"
loading={logoutMutation.isPending}
onClick={() => logoutMutation.mutate()}
>

View File

@@ -21,13 +21,20 @@ export const NotFoundPage = () => {
};
return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">{t("notFoundTitle")}</CardTitle>
<Card>
<CardHeader className="gap-1.5">
<CardTitle className="text-xl">{t("notFoundTitle")}</CardTitle>
<CardDescription>{t("notFoundSubtitle")}</CardDescription>
</CardHeader>
<CardFooter className="flex flex-col items-stretch">
<Button onClick={handleRedirect} loading={loading}>{t("notFoundButton")}</Button>
<CardFooter>
<Button
variant="outline"
className="w-full"
onClick={handleRedirect}
loading={loading}
>
{t("notFoundButton")}
</Button>
</CardFooter>
</Card>
);

View File

@@ -72,9 +72,9 @@ export const TotpPage = () => {
}
return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">{t("totpTitle")}</CardTitle>
<Card>
<CardHeader className="gap-1.5">
<CardTitle className="text-xl">{t("totpTitle")}</CardTitle>
<CardDescription>{t("totpSubtitle")}</CardDescription>
</CardHeader>
<CardContent className="flex flex-col items-center">
@@ -83,8 +83,13 @@ export const TotpPage = () => {
onSubmit={(values) => totpMutation.mutate(values)}
/>
</CardContent>
<CardFooter className="flex flex-col items-stretch">
<Button form={formId} type="submit" loading={totpMutation.isPending}>
<CardFooter>
<Button
className="w-full"
form={formId}
type="submit"
loading={totpMutation.isPending}
>
{t("continueTitle")}
</Button>
</CardFooter>

View File

@@ -47,9 +47,9 @@ export const UnauthorizedPage = () => {
}
return (
<Card className="min-w-xs sm:min-w-sm">
<CardHeader>
<CardTitle className="text-3xl">{t("unauthorizedTitle")}</CardTitle>
<Card>
<CardHeader className="gap-1.5">
<CardTitle className="text-xl">{t("unauthorizedTitle")}</CardTitle>
<CardDescription>
<Trans
i18nKey={i18nKey}
@@ -65,8 +65,13 @@ export const UnauthorizedPage = () => {
/>
</CardDescription>
</CardHeader>
<CardFooter className="flex flex-col items-stretch">
<Button onClick={handleRedirect} loading={loading}>
<CardFooter>
<Button
variant="outline"
className="w-full"
onClick={handleRedirect}
loading={loading}
>
{t("unauthorizedButton")}
</Button>
</CardFooter>