aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/ui-new/src/components/login-modal.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/ui-new/src/components/login-modal.tsx')
-rw-r--r--packages/ui-new/src/components/login-modal.tsx292
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>
);
}