Improve login UI with error handling and callback page

This commit is contained in:
Viktor Barzin 2026-02-02 20:08:03 +00:00
parent ceb943f198
commit 29ba739063
No known key found for this signature in database
GPG key ID: 4056458DBDBF8863
5 changed files with 324 additions and 44 deletions

View file

@ -0,0 +1,111 @@
import React, { useEffect, useState } from 'react';
import { handleCallback, login, type AuthError } from '@/auth/authService';
import { Loader2, CheckCircle, AlertCircle, Home } from 'lucide-react';
import { Button } from './ui/button';
type CallbackState = 'processing' | 'success' | 'error';
const AuthCallback: React.FC = () => {
const [state, setState] = useState<CallbackState>('processing');
const [error, setError] = useState<AuthError | null>(null);
useEffect(() => {
const processCallback = async () => {
try {
await handleCallback();
setState('success');
// Auto-redirect after success
setTimeout(() => {
window.location.href = '/';
}, 1500);
} catch (err) {
setError(err as AuthError);
setState('error');
}
};
processCallback();
}, []);
const handleRetry = async () => {
setState('processing');
setError(null);
try {
await login();
} catch (err) {
setError(err as AuthError);
setState('error');
}
};
const handleGoHome = () => {
window.location.href = '/';
};
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4">
<div className="w-full max-w-md">
<div className="bg-card border rounded-xl shadow-lg p-8">
{state === 'processing' && (
<div className="text-center space-y-4">
<div className="flex justify-center">
<div className="p-4 bg-primary/10 rounded-full">
<Loader2 className="h-8 w-8 text-primary animate-spin" />
</div>
</div>
<div className="space-y-2">
<h1 className="text-xl font-semibold">Completing Sign In</h1>
<p className="text-muted-foreground">
Please wait while we verify your credentials...
</p>
</div>
</div>
)}
{state === 'success' && (
<div className="text-center space-y-4">
<div className="flex justify-center">
<div className="p-4 bg-green-500/10 rounded-full">
<CheckCircle className="h-8 w-8 text-green-500" />
</div>
</div>
<div className="space-y-2">
<h1 className="text-xl font-semibold">Welcome Back!</h1>
<p className="text-muted-foreground">
Redirecting you to the dashboard...
</p>
</div>
</div>
)}
{state === 'error' && (
<div className="text-center space-y-6">
<div className="flex justify-center">
<div className="p-4 bg-destructive/10 rounded-full">
<AlertCircle className="h-8 w-8 text-destructive" />
</div>
</div>
<div className="space-y-2">
<h1 className="text-xl font-semibold">Sign In Failed</h1>
<p className="text-muted-foreground">
{error?.message || 'An unexpected error occurred.'}
</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<Button onClick={handleRetry} className="gap-2">
Try Again
</Button>
<Button variant="outline" onClick={handleGoHome} className="gap-2">
<Home className="h-4 w-4" />
Go Home
</Button>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default AuthCallback;

View file

@ -1,43 +1,127 @@
import { login } from '@/auth/authService';
import { login, type AuthError } from '@/auth/authService';
import { Button } from "@/components/ui/button";
import { DialogDescription } from '@radix-ui/react-dialog';
import React, { useState } from 'react';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog';
import { Home, LogIn, AlertCircle, Loader2 } from 'lucide-react';
interface ModalProps {
isOpen: boolean;
interface LoginModalProps {
isOpen: boolean;
}
const Modal: React.FC<ModalProps> = ({
isOpen,
}) => {
if (!isOpen) return null;
const [isLoading, setIsLoading] = useState(false)
const LoginModal: React.FC<LoginModalProps> = ({ isOpen }) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<AuthError | null>(null);
return (
<Dialog open={isOpen}>
<form>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Login to Wrongmove</DialogTitle>
<DialogDescription>(We are currently in closed beta; ask Viktor to send you an invitation)</DialogDescription>
if (!isOpen) return null;
</DialogHeader>
<DialogFooter>
{isLoading && (
<div>Signing in. Please wait...</div>
)
}
<Button onClick={
() => {
setIsLoading(true)
login()
}} disabled={isLoading}>Login</Button>
</DialogFooter>
</DialogContent>
</form>
</Dialog>
)
const handleLogin = async () => {
setIsLoading(true);
setError(null);
try {
await login();
} catch (err) {
setError(err as AuthError);
setIsLoading(false);
}
};
const handleRetry = () => {
setError(null);
handleLogin();
};
const handleCancel = () => {
setError(null);
setIsLoading(false);
};
return (
<Dialog open={isOpen}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader className="space-y-4">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-lg">
<Home className="h-6 w-6 text-primary" />
</div>
<div>
<DialogTitle className="text-xl">Wrongmove</DialogTitle>
<DialogDescription className="text-sm text-muted-foreground">
Your smart property search companion
</DialogDescription>
</div>
</div>
</DialogHeader>
<div className="py-4 space-y-4">
{/* Beta Notice */}
<div className="bg-muted/50 border rounded-lg p-4 text-sm">
<p className="text-muted-foreground">
We are currently in closed beta. Please contact Viktor to request an invitation.
</p>
</div>
{/* Error State */}
{error && (
<div className="bg-destructive/10 border border-destructive/30 rounded-lg p-4 flex items-start gap-3">
<AlertCircle className="h-5 w-5 text-destructive shrink-0 mt-0.5" />
<div className="flex-1 space-y-2">
<p className="text-sm text-destructive">{error.message}</p>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={handleRetry}
className="text-destructive border-destructive/30 hover:bg-destructive/10"
>
Try Again
</Button>
<Button
size="sm"
variant="ghost"
onClick={handleCancel}
>
Cancel
</Button>
</div>
</div>
</div>
)}
{/* Loading State */}
{isLoading && !error && (
<div className="flex items-center justify-center gap-3 py-4 text-muted-foreground">
<Loader2 className="h-5 w-5 animate-spin" />
<span>Redirecting to login...</span>
</div>
)}
</div>
<DialogFooter>
{!error && (
<Button
onClick={handleLogin}
disabled={isLoading}
className="w-full gap-2"
size="lg"
>
{isLoading ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Signing in...
</>
) : (
<>
<LogIn className="h-4 w-4" />
Sign in with SSO
</>
)}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default Modal;
export default LoginModal;