import { useState, type FormEvent } from "react"; import { Button } from "@executor-js/react/components/button";import { Input } from "@executor-js/react/components/input";import { Label } from "@executor-js/react/components/label"; import { authClient } from "./auth-client";import { AuthLayout } from "./auth-layout";import { postLoginTarget } from "../src/auth/return-to"; // Self-host login: email + password sign-in via Better Auth. On success we// reload so the shared AuthProvider re-reads /account/me and the AuthGate swaps// in the app. (Cloud's equivalent is a WorkOS redirect — this is the// provider-specific piece injected into the shared shell.)//// There is no self-signup here: open registration is closed. New people join by// redeeming an invite — either the full /join/<code> link, or by entering the// code here ("Have an invite code?"), which forwards to the same join page.export const LoginPage = () => { // Where to go after sign-in. The gate renders this page IN PLACE of the // requested route without navigating, so the live location is what carries a // deep link (`/connect/linear`) across sign-in — see `postLoginTarget`. const postLogin = postLoginTarget(window.location); const [mode, setMode] = useState<"signin" | "code">("signin"); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [code, setCode] = useState(""); const [error, setError] = useState<string | null>(null); const [busy, setBusy] = useState(false); const signIn = async (event: FormEvent) => { event.preventDefault(); setBusy(true); setError(null); const result = await authClient.signIn.email({ email, password }); if (result.error) { setBusy(false); setError(result.error.message ?? "Sign in failed"); return; } window.location.href = postLogin; }; const redeem = (event: FormEvent) => { event.preventDefault(); const trimmed = code.trim(); if (!trimmed) return; // Forward to the join page, which collects name/email/password and redeems. window.location.href = `/join/${encodeURIComponent(trimmed)}`; }; return ( <AuthLayout> <div className="w-full max-w-sm space-y-4 rounded-xl border border-border bg-card p-6 shadow-sm"> <div className="space-y-1"> <h1 className="text-xl font-semibold tracking-tight text-foreground"> {mode === "signin" ? "Sign in" : "Join this instance"} </h1> <p className="text-sm text-muted-foreground"> {mode === "signin" ? "Welcome back. Use your instance account." : "Enter the invite code you were given."} </p> </div> {mode === "signin" ? ( <form onSubmit={signIn} className="space-y-4"> <div className="space-y-1.5"> <Label htmlFor="email">Email</Label> <Input id="email" type="email" placeholder="you@example.com" value={email} onChange={(e) => setEmail((e.target as HTMLInputElement).value)} autoComplete="email" required /> </div> <div className="space-y-1.5"> <Label htmlFor="password">Password</Label> <Input id="password" type="password" placeholder="••••••••" value={password} onChange={(e) => setPassword((e.target as HTMLInputElement).value)} autoComplete="current-password" required minLength={8} /> </div> {error && <p className="text-sm text-destructive">{error}</p>} <Button type="submit" disabled={busy} className="w-full"> {busy ? "…" : "Sign in"} </Button> </form> ) : ( <form onSubmit={redeem} className="space-y-4"> <div className="space-y-1.5"> <Label htmlFor="invite-code">Invite code</Label> <Input id="invite-code" placeholder="XXXX-XXXX-XXXX" value={code} onChange={(e) => setCode((e.target as HTMLInputElement).value)} autoFocus /> </div> <Button type="submit" disabled={!code.trim()} className="w-full"> Continue </Button> </form> )} <div className="text-center"> <Button type="button" variant="ghost" onClick={() => { setMode(mode === "signin" ? "code" : "signin"); setError(null); }} className="text-sm font-normal text-muted-foreground hover:text-foreground" > {mode === "signin" ? "Have an invite code? Join" : "Already have an account? Sign in"} </Button> </div> </div> </AuthLayout> );};
import { useState, type FormEvent } from "react";
import { Button } from "@executor-js/react/components/button";
import { Input } from "@executor-js/react/components/input";
import { Label } from "@executor-js/react/components/label";
import { authClient } from "./auth-client";
import { AuthLayout } from "./auth-layout";
import { postLoginTarget } from "../src/auth/return-to";
// Self-host login: email + password sign-in via Better Auth. On success we
// reload so the shared AuthProvider re-reads /account/me and the AuthGate swaps
// in the app. (Cloud's equivalent is a WorkOS redirect — this is the
// provider-specific piece injected into the shared shell.)
//
// There is no self-signup here: open registration is closed. New people join by
// redeeming an invite — either the full /join/<code> link, or by entering the
// code here ("Have an invite code?"), which forwards to the same join page.
export const LoginPage = () => {
// Where to go after sign-in. The gate renders this page IN PLACE of the
// requested route without navigating, so the live location is what carries a
// deep link (`/connect/linear`) across sign-in — see `postLoginTarget`.
const postLogin = postLoginTarget(window.location);
const [mode, setMode] = useState<"signin" | "code">("signin");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [code, setCode] = useState("");
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const signIn = async (event: FormEvent) => {
event.preventDefault();
setBusy(true);
setError(null);
const result = await authClient.signIn.email({ email, password });
if (result.error) {
setBusy(false);
setError(result.error.message ?? "Sign in failed");
return;
}
window.location.href = postLogin;
};
const redeem = (event: FormEvent) => {
event.preventDefault();
const trimmed = code.trim();
if (!trimmed) return;
// Forward to the join page, which collects name/email/password and redeems.
window.location.href = `/join/${encodeURIComponent(trimmed)}`;
};
return (
<AuthLayout>
<div className="w-full max-w-sm space-y-4 rounded-xl border border-border bg-card p-6 shadow-sm">
<div className="space-y-1">
<h1 className="text-xl font-semibold tracking-tight text-foreground">
{mode === "signin" ? "Sign in" : "Join this instance"}
</h1>
<p className="text-sm text-muted-foreground">
{mode === "signin"
? "Welcome back. Use your instance account."
: "Enter the invite code you were given."}
</p>
</div>
{mode === "signin" ? (
<form onSubmit={signIn} className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail((e.target as HTMLInputElement).value)}
autoComplete="email"
required
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword((e.target as HTMLInputElement).value)}
autoComplete="current-password"
required
minLength={8}
/>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
<Button type="submit" disabled={busy} className="w-full">
{busy ? "…" : "Sign in"}
</Button>
</form>
) : (
<form onSubmit={redeem} className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="invite-code">Invite code</Label>
<Input
id="invite-code"
placeholder="XXXX-XXXX-XXXX"
value={code}
onChange={(e) => setCode((e.target as HTMLInputElement).value)}
autoFocus
/>
</div>
<Button type="submit" disabled={!code.trim()} className="w-full">
Continue
</Button>
</form>
)}
<div className="text-center">
<Button
type="button"
variant="ghost"
onClick={() => {
setMode(mode === "signin" ? "code" : "signin");
setError(null);
}}
className="text-sm font-normal text-muted-foreground hover:text-foreground"
>
{mode === "signin" ? "Have an invite code? Join" : "Already have an account? Sign in"}
</Button>
</div>
</div>
</AuthLayout>
);
};