feat: require owner login and add onboarding

This commit is contained in:
2026-09-28 19:14:14 +09:00
parent 57882cf5f9
commit 85d23a328b
42 changed files with 2400 additions and 23 deletions
+151
View File
@@ -0,0 +1,151 @@
import { useRouteContext } from '@tanstack/react-router'
import { ArrowRight, BookOpen, House, MessagesSquare } from 'lucide-react'
import { type FormEvent, useState } from 'react'
import { Button } from '#/components/ui/button'
import { Input } from '#/components/ui/input'
import { Label } from '#/components/ui/label'
import { authRequest } from './auth-client'
import { AuthFrame } from './auth-page'
export function OnboardingPage() {
const { auth } = useRouteContext({ from: '__root__' })
const [name, setName] = useState(auth.owner?.name ?? '')
const [step, setStep] = useState(1)
const [pending, setPending] = useState(false)
const [error, setError] = useState('')
async function finish() {
setPending(true)
setError('')
try {
await authRequest({ action: 'onboard', name: name.trim() })
window.location.assign('/')
} catch (cause) {
setError(
cause instanceof Error
? cause.message
: 'Unable to save. Please try again.',
)
setPending(false)
}
}
async function signOut() {
setPending(true)
setError('')
try {
await authRequest({ action: 'logout' })
window.location.assign('/login')
} catch (cause) {
setError(
cause instanceof Error
? cause.message
: 'Unable to sign out. Please try again.',
)
setPending(false)
}
}
function next(event: FormEvent<HTMLFormElement>) {
event.preventDefault()
if (!name.trim()) return
setStep(2)
}
return (
<AuthFrame>
<p className="auth-step">{step} of 2</p>
{step === 1 ? (
<>
<header className="auth-heading">
<h1>A little context, to start.</h1>
<p>Your day, conversations, and reading, together in one place.</p>
</header>
<form className="auth-form" onSubmit={next}>
<div className="auth-field">
<Label htmlFor="displayName">What should we call you?</Label>
<Input
id="displayName"
name="displayName"
value={name}
onChange={(event) => setName(event.target.value)}
autoComplete="given-name"
required
maxLength={80}
disabled={pending}
/>
</div>
<Button
type="submit"
className="auth-submit"
size="lg"
disabled={pending || !name.trim()}
>
Continue
<ArrowRight aria-hidden="true" />
</Button>
</form>
</>
) : (
<>
<header className="auth-heading">
<h1>Start with today.</h1>
<p>You can take it one thing at a time.</p>
</header>
<ul className="auth-overview">
<li>
<House aria-hidden="true" />
<div>
<strong>A place to begin</strong>
<p>A brief and flexible tasks help you find your next step.</p>
</div>
</li>
<li>
<MessagesSquare aria-hidden="true" />
<div>
<strong>Room for the everyday</strong>
<p>Keep conversations and quick notes close at hand.</p>
</div>
</li>
<li>
<BookOpen aria-hidden="true" />
<div>
<strong>Follow your curiosity</strong>
<p>Explore reading and research alongside your day.</p>
</div>
</li>
</ul>
<p className="auth-prototype">
Home, contacts, notes, reading, and vitals currently use sample
data. You can add connections from your profile menu whenever you’re
ready.
</p>
<div className="auth-actions">
<Button
variant="ghost"
onClick={() => setStep(1)}
disabled={pending}
>
Back
</Button>
<Button size="lg" onClick={finish} disabled={pending}>
{pending ? 'Saving…' : 'Open workspace'}
<ArrowRight aria-hidden="true" />
</Button>
</div>
</>
)}
{error && (
<p className="auth-error" role="alert">
{error}
</p>
)}
<footer className="auth-footer">
<span>{auth.owner?.email}</span>
<Button variant="ghost" size="sm" onClick={signOut} disabled={pending}>
Sign out
</Button>
</footer>
</AuthFrame>
)
}