142 lines
3.6 KiB
TypeScript
142 lines
3.6 KiB
TypeScript
import { z } from "zod";
|
|
import {
|
|
getCurrentAuthRedirect,
|
|
getOAuthSignedQuery,
|
|
} from "@/lib/auth-redirect";
|
|
import { isHostedClientAuthMode } from "@/lib/auth-mode";
|
|
import {
|
|
getFieldError as getSharedFieldError,
|
|
getFormError as getSharedFormError,
|
|
} from "@/client/lib/forms";
|
|
|
|
export const authRedirectSearchSchema = z.object({
|
|
redirect: z.string().optional(),
|
|
});
|
|
|
|
export function useAuthPageState(redirect: string | undefined) {
|
|
const redirectTo = getCurrentAuthRedirect(redirect);
|
|
const oauthQuery =
|
|
typeof window !== "undefined"
|
|
? getOAuthSignedQuery(window.location.search)
|
|
: null;
|
|
const isHostedMode = isHostedClientAuthMode();
|
|
|
|
return {
|
|
redirectTo,
|
|
oauthQuery,
|
|
isHostedMode,
|
|
};
|
|
}
|
|
|
|
export function getFieldError(errors: readonly unknown[]) {
|
|
return getSharedFieldError(errors);
|
|
}
|
|
|
|
export function getFormError(error: unknown) {
|
|
return getSharedFormError(error);
|
|
}
|
|
|
|
export function AuthMethodChooser({
|
|
googleLabel,
|
|
emailLabel = "Continue with email",
|
|
isBusy,
|
|
disabled,
|
|
onContinueWithGoogle,
|
|
onContinueWithEmail,
|
|
}: {
|
|
googleLabel: string;
|
|
emailLabel?: string;
|
|
isBusy?: boolean;
|
|
disabled?: boolean;
|
|
onContinueWithGoogle: () => void;
|
|
onContinueWithEmail: () => void;
|
|
}) {
|
|
return (
|
|
<div className="space-y-3">
|
|
<button
|
|
type="button"
|
|
className="btn w-full bg-white text-neutral border border-base-content/15 hover:bg-base-100 hover:border-base-content/25 disabled:bg-base-300"
|
|
onClick={onContinueWithGoogle}
|
|
disabled={disabled || isBusy}
|
|
>
|
|
<GoogleLogo />
|
|
{isBusy ? "Opening Google..." : googleLabel}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
className="btn btn-soft w-full"
|
|
onClick={onContinueWithEmail}
|
|
disabled={disabled || isBusy}
|
|
>
|
|
{emailLabel}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function GoogleLogo() {
|
|
return (
|
|
<svg aria-hidden="true" viewBox="0 0 18 18" className="size-4 shrink-0">
|
|
<path
|
|
fill="#4285F4"
|
|
d="M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.72v2.26h2.92c1.7-1.57 2.68-3.88 2.68-6.62Z"
|
|
/>
|
|
<path
|
|
fill="#34A853"
|
|
d="M9 18c2.43 0 4.47-.8 5.96-2.18l-2.92-2.26c-.8.54-1.84.86-3.04.86-2.34 0-4.33-1.58-5.04-3.72H.94v2.34A9 9 0 0 0 9 18Z"
|
|
/>
|
|
<path
|
|
fill="#FBBC05"
|
|
d="M3.96 10.7A5.4 5.4 0 0 1 3.68 9c0-.59.1-1.16.28-1.7V4.96H.94A9 9 0 0 0 0 9c0 1.45.34 2.82.94 4.04l3.02-2.34Z"
|
|
/>
|
|
<path
|
|
fill="#EA4335"
|
|
d="M9 3.58c1.32 0 2.5.45 3.44 1.35l2.58-2.58A8.64 8.64 0 0 0 9 0 9 9 0 0 0 .94 4.96L3.96 7.3C4.67 5.16 6.66 3.58 9 3.58Z"
|
|
/>
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export function AuthPageCard({
|
|
title,
|
|
helperText,
|
|
children,
|
|
footer,
|
|
}: {
|
|
title: string;
|
|
helperText?: string;
|
|
children: React.ReactNode;
|
|
footer?: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<div className="w-full max-w-xs space-y-6">
|
|
<div className="text-center space-y-3">
|
|
<img
|
|
src="/transparent-logo.png"
|
|
alt="OpenSEO"
|
|
className="mx-auto size-10 rounded-lg"
|
|
/>
|
|
<div>
|
|
<h1 className="text-xl font-semibold">{title}</h1>
|
|
{helperText ? (
|
|
<p className="text-sm text-base-content/60 mt-1">{helperText}</p>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
{children}
|
|
|
|
{footer ? <div className="text-center">{footer}</div> : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function AuthPageShell({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<div className="min-h-[100dvh] flex flex-col items-center justify-center p-4 bg-base-200">
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|