Below md width, the app now forces view-only (same canEdit mechanism as a shared view-only diagram) rather than trying to support touch editing: palette and inspector panels hide, editing controls disable, and the top bar collapses to just Diagrams + Account. A bottom tab bar swaps between a full-screen canvas and a full-screen Bill of Materials, since there's no room for both side by side. Canvas gets a few mobile-specific trims too: MiniMap and the interactivity lock button hide whenever canEdit is false (the lock button doesn't actually grant editing either way — nodesDraggable/ nodesConnectable already override it — so leaving it visible would just be misleading), and h-screen is replaced with h-dvh throughout so the layout doesn't get clipped by a mobile browser's collapsing address bar. Also fixes quick-add's anti-stack offset (24px — far smaller than a device node) to actually tile devices apart instead of leaving them nearly overlapping, and logs a deferred "Auto-arrange" layout feature in organized-ideas.md, surfaced by mobile visitors having no way to fix an overlapping diagram themselves. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
179 lines
7.0 KiB
TypeScript
179 lines
7.0 KiB
TypeScript
import { useState } from 'react'
|
|
import { supabase } from '../../data/supabaseClient'
|
|
|
|
/**
|
|
* Auth gate: Google SSO (per organized-ideas.md §2 — no username prompt
|
|
* here, since Google doesn't let us collect one before the redirect; a
|
|
* first-time Google sign-in lands with no username and App.tsx routes them
|
|
* to CompleteProfileScreen to set one) or manual email/password signup
|
|
* (username collected up front here instead, email pulled from the form).
|
|
* Manual signup respects the hard email-verification gate from
|
|
* supabase/config.toml — sign-in is blocked until confirmed.
|
|
*/
|
|
export default function LoginScreen() {
|
|
const [mode, setMode] = useState<'sign-in' | 'sign-up'>('sign-in')
|
|
const [email, setEmail] = useState('')
|
|
const [password, setPassword] = useState('')
|
|
const [username, setUsername] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [confirmSent, setConfirmSent] = useState(false)
|
|
|
|
const handleGoogleSignIn = async () => {
|
|
setError(null)
|
|
setLoading(true)
|
|
const { error } = await supabase.auth.signInWithOAuth({
|
|
provider: 'google',
|
|
options: { redirectTo: window.location.origin },
|
|
})
|
|
// On success this redirects the whole page to Google, so we only ever
|
|
// reach here if kicking off the redirect itself failed.
|
|
if (error) {
|
|
setError(error.message)
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
setError(null)
|
|
setLoading(true)
|
|
try {
|
|
if (mode === 'sign-in') {
|
|
const { error } = await supabase.auth.signInWithPassword({ email, password })
|
|
if (error) throw error
|
|
} else {
|
|
const { error } = await supabase.auth.signUp({
|
|
email,
|
|
password,
|
|
options: { data: { username: username.trim() } },
|
|
})
|
|
if (error) throw error
|
|
setConfirmSent(true)
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Something went wrong.')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
if (confirmSent) {
|
|
return (
|
|
<div className="flex h-dvh items-center justify-center bg-slate-50">
|
|
<div className="w-full max-w-sm rounded-lg border border-slate-200 bg-white p-6 text-center shadow-sm">
|
|
<h1 className="text-sm font-semibold text-slate-800">Check your email</h1>
|
|
<p className="mt-2 text-xs text-slate-500">
|
|
We sent a confirmation link to <span className="font-medium">{email}</span>. You'll need to confirm
|
|
before you can sign in.
|
|
</p>
|
|
{import.meta.env.DEV && (
|
|
<p className="mt-2 text-[11px] text-slate-400">
|
|
Local dev: open Mailpit at{' '}
|
|
<a href="http://127.0.0.1:54324" className="underline" target="_blank" rel="noreferrer">
|
|
127.0.0.1:54324
|
|
</a>{' '}
|
|
to see it — no real email is sent.
|
|
</p>
|
|
)}
|
|
<button
|
|
onClick={() => {
|
|
setConfirmSent(false)
|
|
setMode('sign-in')
|
|
}}
|
|
className="mt-4 text-xs font-medium text-indigo-600 hover:underline"
|
|
>
|
|
Back to sign in
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-dvh items-center justify-center bg-slate-50">
|
|
<form onSubmit={handleSubmit} className="w-full max-w-sm rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
|
|
<h1 className="text-sm font-semibold text-indigo-700">Diagrav</h1>
|
|
<p className="mt-1 text-xs text-slate-500">{mode === 'sign-in' ? 'Sign in' : 'Create an account'}</p>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={handleGoogleSignIn}
|
|
disabled={loading}
|
|
className="mt-4 flex w-full items-center justify-center gap-2 rounded border border-slate-300 py-1.5 text-sm font-medium text-slate-700 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
<svg viewBox="0 0 18 18" className="h-4 w-4" aria-hidden="true">
|
|
<path fill="#4285F4" d="M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.72v2.26h2.9c1.7-1.57 2.7-3.88 2.7-6.62z" />
|
|
<path fill="#34A853" d="M9 18c2.43 0 4.47-.8 5.96-2.18l-2.9-2.26c-.8.54-1.83.86-3.06.86-2.35 0-4.34-1.59-5.05-3.72H.96v2.33A9 9 0 0 0 9 18z" />
|
|
<path fill="#FBBC05" d="M3.95 10.7A5.4 5.4 0 0 1 3.67 9c0-.59.1-1.17.28-1.7V4.96H.96A9 9 0 0 0 0 9c0 1.45.35 2.83.96 4.04z" />
|
|
<path fill="#EA4335" d="M9 3.58c1.32 0 2.51.46 3.44 1.35l2.58-2.58C13.46.89 11.43 0 9 0A9 9 0 0 0 .96 4.96L3.95 7.3C4.66 5.17 6.65 3.58 9 3.58z" />
|
|
</svg>
|
|
Continue with Google
|
|
</button>
|
|
|
|
<div className="my-4 flex items-center gap-2 text-[10px] uppercase tracking-wide text-slate-400">
|
|
<div className="h-px flex-1 bg-slate-200" />
|
|
or
|
|
<div className="h-px flex-1 bg-slate-200" />
|
|
</div>
|
|
|
|
<div className="space-y-3">
|
|
{mode === 'sign-up' && (
|
|
<label className="block text-xs font-medium text-slate-600">
|
|
Username
|
|
<input
|
|
value={username}
|
|
onChange={(e) => setUsername(e.target.value)}
|
|
required
|
|
className="mt-1 w-full rounded border border-slate-300 px-2 py-1.5 text-sm"
|
|
/>
|
|
</label>
|
|
)}
|
|
<label className="block text-xs font-medium text-slate-600">
|
|
Email
|
|
<input
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
required
|
|
className="mt-1 w-full rounded border border-slate-300 px-2 py-1.5 text-sm"
|
|
/>
|
|
</label>
|
|
<label className="block text-xs font-medium text-slate-600">
|
|
Password
|
|
<input
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
required
|
|
minLength={6}
|
|
className="mt-1 w-full rounded border border-slate-300 px-2 py-1.5 text-sm"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
{error && <p className="mt-3 text-xs text-red-600">{error}</p>}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="mt-4 w-full rounded bg-indigo-600 py-1.5 text-sm font-medium text-white hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
{loading ? 'Please wait…' : mode === 'sign-in' ? 'Sign in' : 'Create account'}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setMode(mode === 'sign-in' ? 'sign-up' : 'sign-in')
|
|
setError(null)
|
|
}}
|
|
className="mt-3 w-full text-center text-xs text-slate-500 hover:underline"
|
|
>
|
|
{mode === 'sign-in' ? "Don't have an account? Create one" : 'Already have an account? Sign in'}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
)
|
|
}
|