Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import { useLoginForm } from "@/hooks/use-login-form";
import { useAuthForm } from "@/hooks/use-auth-form";
import { Button, Card, CardContent, Input, cn } from "@repo/ui";
import { Mail } from "lucide-react";
import type { ComponentProps } from "react";
import { OtpVerification } from "./otp-verification";
import { PasskeyLogin } from "./passkey-login";
import { SocialLogin } from "./social-login";
import { Link } from "@tanstack/react-router";

interface AuthFormContentProps {
onSuccess?: () => void;
className?: string;
isExternallyLoading?: boolean;
mode?: "login" | "signup";
}

function AuthFormContent({
onSuccess,
className,
isExternallyLoading,
mode = "login",
}: AuthFormContentProps) {
const {
email,
Expand All @@ -27,18 +30,27 @@ function AuthFormContent({
handleError,
sendOtp,
resetOtpFlow,
} = useLoginForm({
mode: formMode,
} = useAuthForm({
onSuccess,
isExternallyLoading,
mode,
});

const isSignup = formMode === "signup";
const heading = isSignup ? "Welcome" : "Welcome back";
const subheading = isSignup
? "Create your account"
: "Sign in to your account";
const emailButtonText = isSignup
? "Sign up with email"
: "Sign in with email";

return (
<div className={cn("flex flex-col gap-6", className)}>
<div className="flex flex-col items-center text-center">
<h1 className="text-2xl font-bold">Welcome</h1>
<p className="text-muted-foreground text-balance">
Sign in or create your account
</p>
<h1 className="text-2xl font-bold">{heading}</h1>
<p className="text-muted-foreground text-balance">{subheading}</p>
</div>

{/* Error message */}
Expand All @@ -48,12 +60,14 @@ function AuthFormContent({
</div>
)}

{/* Passkey Login - Primary CTA for returning users with passkeys */}
<PasskeyLogin
onSuccess={handleSuccess}
onError={handleError}
isDisabled={isDisabled}
/>
{/* Passkey Login - Only show for login mode (requires existing account) */}
{!isSignup && (
<PasskeyLogin
onSuccess={handleSuccess}
onError={handleError}
isDisabled={isDisabled}
/>
)}

{/* Google OAuth - Works for both new and existing accounts */}
<SocialLogin onError={handleError} isDisabled={isDisabled} />
Expand Down Expand Up @@ -84,8 +98,38 @@ function AuthFormContent({
disabled={isDisabled || !email}
>
<Mail className="mr-2 h-4 w-4" />
Continue with email
{emailButtonText}
</Button>
{/* Account Link - Show link to switch between login/signup */}
<div className="text-center text-sm text-muted-foreground">
{isSignup ? (
<>
Already have an account?{" "}
<Link
to="/login"
activeProps={{
className:
"font-medium text-primary underline-offset-4 hover:underline",
}}
>
Sign in
</Link>
</>
) : (
<>
Don't have an account?{" "}
<Link
to="/signup"
activeProps={{
className:
"font-medium text-primary underline-offset-4 hover:underline",
}}
>
Sign up
</Link>
</>
)}
</div>
</form>
) : (
<OtpVerification
Expand All @@ -94,36 +138,48 @@ function AuthFormContent({
onError={handleError}
onCancel={resetOtpFlow}
isDisabled={isDisabled}
mode={formMode}
/>
)}
</div>
);
}

interface LoginFormProps extends ComponentProps<"div"> {
interface AuthFormProps extends ComponentProps<"div"> {
variant?: "page" | "modal";
showTerms?: boolean;
onSuccess?: () => void;
isLoading?: boolean;
mode?: "login" | "signup";
/**
* Optional image path (svg, jpg, or png) to display in right panel.
* When provided, layout becomes two-column on md+.
* If empty/undefined, only single card is shown.
*/
rightPanelImage?: string;
}

export function LoginForm({
export function AuthForm({
className,
variant = "page",
showTerms,
onSuccess,
isLoading,
mode = "login",
rightPanelImage,
...props
}: LoginFormProps) {
}: AuthFormProps) {
// Default: Show terms on full page, hide in modals (unless overridden)
const shouldShowTerms = showTerms ?? variant === "page";
const hasRightPanel = Boolean(rightPanelImage);

if (variant === "modal") {
return (
<div className={cn("flex flex-col gap-4", className)} {...props}>
<AuthFormContent
onSuccess={onSuccess}
isExternallyLoading={isLoading}
mode={mode}
/>
{shouldShowTerms && (
<div className="text-center text-xs text-muted-foreground text-balance">
Expand Down Expand Up @@ -151,19 +207,31 @@ export function LoginForm({
// Default page variant with card layout
return (
<div className={cn("flex flex-col gap-6", className)} {...props}>
<Card className="overflow-hidden p-0">
<CardContent className="grid p-0 md:grid-cols-2">
<Card
className={cn(
"overflow-hidden p-0 mx-auto w-full",
hasRightPanel ? "max-w-3xl" : "max-w-md",
)}
>
<CardContent
className={cn("p-0", hasRightPanel ? "grid md:grid-cols-2" : "block")}
>
<div className="p-6 md:p-8">
<AuthFormContent
onSuccess={onSuccess}
isExternallyLoading={isLoading}
mode={mode}
/>
</div>

{/* Right panel - Hidden on mobile, provides visual balance on desktop */}
<div className="relative hidden bg-muted md:block">
<div className="absolute inset-0 bg-gradient-to-br from-primary/20 to-primary/10" />
</div>
{hasRightPanel && (
<div className="relative hidden bg-muted md:flex md:items-center md:justify-center">
<img
src={rightPanelImage}
alt="descriptive text about the image card"
className="h-full w-full object-cover"
/>
</div>
)}
</CardContent>
</Card>

Expand Down
10 changes: 6 additions & 4 deletions apps/app/components/auth/login-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
DialogTrigger,
} from "@repo/ui";
import * as React from "react";
import { LoginForm } from "./login-form";
import { AuthForm } from "./auth-form";

/**
* LoginDialog component - Renders login form in a dialog.
Expand Down Expand Up @@ -41,8 +41,9 @@ export function LoginDialog() {
Choose your preferred sign in method
</DialogDescription>
</DialogHeader>
<LoginForm
<AuthForm
variant="modal"
mode="login"
onSuccess={handleSuccess}
showTerms={true} // Override default (false for modals) to show terms
/>
Expand Down Expand Up @@ -79,10 +80,11 @@ export function useLoginDialog() {
Choose your preferred sign in method
</DialogDescription>
</DialogHeader>
<LoginForm
<AuthForm
variant="modal"
mode="login"
onSuccess={handleSuccess}
// showTerms defaults to false for modals per LoginForm implementation
// showTerms defaults to false for modals per AuthForm implementation
/>
</DialogContent>
</Dialog>
Expand Down
15 changes: 13 additions & 2 deletions apps/app/components/auth/otp-verification.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { auth } from "@/lib/auth";
import { Button, Input } from "@repo/ui";
import { useState } from "react";
import type { FormEvent } from "react";
import { useState } from "react";

interface OtpVerificationProps {
email: string;
onSuccess: () => void;
onError: (error: string) => void;
onCancel: () => void;
isDisabled?: boolean;
mode?: "login" | "signup";
}

export function OtpVerification({
Expand All @@ -17,6 +18,7 @@ export function OtpVerification({
onError,
onCancel,
isDisabled,
mode = "login",
}: OtpVerificationProps) {
const [otp, setOtp] = useState("");
const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -84,11 +86,20 @@ export function OtpVerification({
};

const disabled = isDisabled || isLoading;
const isSignup = mode === "signup";

return (
<form onSubmit={handleOtpVerification} className="grid gap-3">
<div className="text-sm text-muted-foreground">
We've sent a verification code to <strong>{email}</strong>
{isSignup ? (
<>
We've sent a verification code to verify <strong>{email}</strong>
</>
) : (
<>
We've sent a verification code to <strong>{email}</strong>
</>
)}
</div>
<Input
type="text"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ import { useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import type { FormEvent } from "react";

interface UseLoginFormOptions {
interface UseAuthFormOptions {
onSuccess?: () => void;
isExternallyLoading?: boolean;
mode?: "login" | "signup";
}

export function useLoginForm({
export function useAuthForm({
onSuccess,
isExternallyLoading,
}: UseLoginFormOptions = {}) {
mode = "login",
}: UseAuthFormOptions = {}) {
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [isLoading, setIsLoading] = useState(false);
Expand Down Expand Up @@ -86,6 +88,7 @@ export function useLoginForm({
isDisabled,
error,
showOtpInput,
mode,

// Setters
setEmail,
Expand Down
Loading