diff options
Diffstat (limited to 'packages/ui-new/src/components/login-modal.tsx')
| -rw-r--r-- | packages/ui-new/src/components/login-modal.tsx | 292 |
1 files changed, 162 insertions, 130 deletions
diff --git a/packages/ui-new/src/components/login-modal.tsx b/packages/ui-new/src/components/login-modal.tsx index 9152494..49596da 100644 --- a/packages/ui-new/src/components/login-modal.tsx +++ b/packages/ui-new/src/components/login-modal.tsx @@ -1,156 +1,188 @@ import { Mail, User } from "lucide-react"; -import { useAuthStore } from "@/stores/auth-store"; +import { useCallback, useState } from "react"; +import { toast } from "sonner"; +import { useAuthStore } from "@/models/auth"; +import { Button } from "./ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "./ui/dialog"; +import { + Field, + FieldDescription, + FieldError, + FieldGroup, + FieldLabel, +} from "./ui/field"; +import { Input } from "./ui/input"; -export function LoginModal() { +export interface LoginModalProps + extends Omit<React.ComponentPropsWithoutRef<typeof Dialog>, "onOpenChange"> { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function LoginModal({ onOpenChange, ...props }: LoginModalProps) { const authStore = useAuthStore(); - const handleOfflineLogin = () => { - if (authStore.offlineUsername.trim()) { - authStore.performOfflineLogin(); - } - }; + const [offlineUsername, setOfflineUsername] = useState<string>(""); + const [errorMessage, setErrorMessage] = useState<string>(""); + const [isLoggingIn, setIsLoggingIn] = useState<boolean>(false); - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - handleOfflineLogin(); + const handleMicrosoftLogin = useCallback(async () => { + setIsLoggingIn(true); + authStore.setLoginMode("microsoft"); + try { + await authStore.loginOnline(() => onOpenChange?.(false)); + } catch (error) { + const err = error as Error; + console.error("Failed to login with Microsoft:", err); + setErrorMessage(err.message); + } finally { + setIsLoggingIn(false); } - }; + }, [authStore.loginOnline, authStore.setLoginMode, onOpenChange]); - if (!authStore.isLoginModalOpen) return null; + const handleOfflineLogin = useCallback(async () => { + setIsLoggingIn(true); + try { + await authStore.loginOffline(offlineUsername); + toast.success("Logged in offline successfully"); + onOpenChange?.(false); + } catch (error) { + const err = error as Error; + console.error("Failed to login offline:", err); + setErrorMessage(err.message); + } finally { + setIsLoggingIn(false); + } + }, [authStore, offlineUsername, onOpenChange]); return ( - <div className="fixed inset-0 z-200 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4"> - <div className="bg-zinc-900 border border-zinc-700 rounded-xl shadow-2xl max-w-md w-full animate-in fade-in zoom-in-95 duration-200"> - <div className="p-6"> - {/* Header */} - <div className="flex items-center justify-between mb-6"> - <h3 className="text-xl font-bold text-white">Login</h3> - <button - type="button" - onClick={() => { - authStore.setLoginMode("select"); - authStore.setOfflineUsername(""); - authStore.cancelMicrosoftLogin(); - }} - className="text-zinc-400 hover:text-white transition-colors p-1" - > - × - </button> - </div> - - {/* Content based on mode */} - {authStore.loginMode === "select" && ( - <div className="space-y-4"> - <p className="text-zinc-400 text-sm"> - Choose your preferred login method - </p> - <button - type="button" - onClick={() => authStore.startMicrosoftLogin()} - className="w-full flex items-center justify-center gap-3 px-4 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-colors" + <Dialog onOpenChange={onOpenChange} {...props}> + <DialogContent className="md:max-w-md"> + <DialogHeader> + <DialogTitle>Login</DialogTitle> + <DialogDescription> + Login to your Minecraft account or play offline + </DialogDescription> + </DialogHeader> + <div className="p-4 w-full overflow-hidden"> + {!authStore.loginMode && ( + <div className="flex flex-col space-y-4"> + <Button size="lg" onClick={handleMicrosoftLogin}> + <Mail /> + Login with Microsoft + </Button> + <Button + variant="secondary" + onClick={() => authStore.setLoginMode("offline")} + size="lg" > - <Mail size={18} /> - <span className="font-medium">Microsoft Account</span> - </button> + <User /> + Login Offline + </Button> + </div> + )} + {authStore.loginMode === "microsoft" && ( + <div className="flex flex-col space-y-4"> <button type="button" + className="text-4xl font-bold text-center bg-accent p-4 cursor-pointer" onClick={() => { - authStore.loginMode = "offline"; + if (authStore.deviceCode?.userCode) { + navigator.clipboard?.writeText( + authStore.deviceCode?.userCode, + ); + toast.success("Copied to clipboard"); + } }} - className="w-full flex items-center justify-center gap-3 px-4 py-3 bg-zinc-800 hover:bg-zinc-700 text-white rounded-lg transition-colors" > - <User size={18} /> - <span className="font-medium">Offline Mode</span> + {authStore.deviceCode?.userCode} </button> - </div> - )} - - {authStore.loginMode === "offline" && ( - <div className="space-y-4"> - <div> - <label - htmlFor="username" - className="block text-sm font-medium text-zinc-300 mb-2" - > - Username - </label> - <input - name="username" - type="text" - value={authStore.offlineUsername} - onChange={(e) => authStore.setOfflineUsername(e.target.value)} - onKeyDown={handleKeyPress} - className="w-full px-4 py-2.5 bg-zinc-800 border border-zinc-700 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 transition-colors" - placeholder="Enter your Minecraft username" - /> - </div> - <div className="flex gap-3"> - <button - type="button" + <span className="text-muted-foreground w-full overflow-hidden text-ellipsis"> + To sign in, use a web browser to open the page{" "} + <a href={authStore.deviceCode?.verificationUri}> + {authStore.deviceCode?.verificationUri} + </a>{" "} + and enter the code{" "} + <code + className="font-semibold cursor-pointer" onClick={() => { - authStore.loginMode = "select"; - authStore.setOfflineUsername(""); + if (authStore.deviceCode?.userCode) { + navigator.clipboard?.writeText( + authStore.deviceCode?.userCode, + ); + } + }} + onKeyDown={() => { + if (authStore.deviceCode?.userCode) { + navigator.clipboard?.writeText( + authStore.deviceCode?.userCode, + ); + } }} - className="flex-1 px-4 py-2.5 text-sm font-medium text-zinc-300 hover:text-white bg-zinc-800 hover:bg-zinc-700 rounded-lg transition-colors" - > - Back - </button> - <button - type="button" - onClick={handleOfflineLogin} - disabled={!authStore.offlineUsername.trim()} - className="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-500 disabled:bg-indigo-600/50 disabled:cursor-not-allowed rounded-lg transition-colors" > - Login - </button> - </div> + {authStore.deviceCode?.userCode} + </code>{" "} + to authenticate, this code will be expired in{" "} + {authStore.deviceCode?.expiresIn} seconds. + </span> + <FieldError>{errorMessage}</FieldError> </div> )} - - {authStore.loginMode === "microsoft" && ( - <div className="space-y-4"> - {authStore.deviceCodeData && ( - <div className="bg-zinc-800/50 border border-zinc-700 rounded-lg p-4"> - <div className="text-center mb-4"> - <div className="text-xs font-mono bg-zinc-900 px-3 py-2 rounded border border-zinc-700 mb-3"> - {authStore.deviceCodeData.userCode} - </div> - <p className="text-zinc-300 text-sm font-medium"> - Your verification code - </p> - </div> - <p className="text-zinc-400 text-sm text-center"> - Visit{" "} - <a - href={authStore.deviceCodeData.verificationUri} - target="_blank" - className="text-indigo-400 hover:text-indigo-300 font-medium" - > - {authStore.deviceCodeData.verificationUri} - </a>{" "} - and enter the code above - </p> - </div> - )} - <div className="text-center"> - <p className="text-zinc-300 text-sm mb-2"> - {authStore.msLoginStatus} - </p> - <button - type="button" - onClick={() => { - authStore.cancelMicrosoftLogin(); - authStore.setLoginMode("select"); + {authStore.loginMode === "offline" && ( + <FieldGroup> + <Field> + <FieldLabel>Username</FieldLabel> + <FieldDescription> + Enter a username to play offline + </FieldDescription> + <Input + value={offlineUsername} + onChange={(e) => { + setOfflineUsername(e.target.value); + setErrorMessage(""); }} - className="text-sm text-zinc-400 hover:text-white transition-colors" - > - Cancel - </button> - </div> - </div> + aria-invalid={!!errorMessage} + /> + <FieldError>{errorMessage}</FieldError> + </Field> + </FieldGroup> )} </div> - </div> - </div> + <DialogFooter> + <div className="flex flex-col justify-center items-center"> + <span className="text-xs text-muted-foreground "> + {authStore.statusMessage} + </span> + </div> + <Button + variant="outline" + onClick={() => { + if (authStore.loginMode) { + if (authStore.loginMode === "microsoft") { + authStore.cancelLoginOnline(); + } + authStore.setLoginMode(null); + } else { + onOpenChange?.(false); + } + }} + > + Cancel + </Button> + {authStore.loginMode === "offline" && ( + <Button onClick={handleOfflineLogin} disabled={isLoggingIn}> + Login + </Button> + )} + </DialogFooter> + </DialogContent> + </Dialog> ); } |