feat: auth scaffold, notifications inbox, resources CRUD, vitest baseline, typed API client
Auth - ~/lib/session.ts: Session type + loadSession/signIn/signOut/hasSession, reactive useSession hook (mock backend; replace fetch calls with your real auth endpoint when ready) - routes/login.tsx: form with email/password (mock-validated), bounces to ?next= on success - AppShell: redirects to /login when no session; account-menu Sign out now actually signs out; live session.name/email used for the appbar avatar (falls back to profile) Notifications - ~/lib/notifications.ts: persistent inbox with kinds (info/success/ warning/error), unreadCount, markRead, markAllRead, dismiss, dismissAll; seedIfEmpty for a friendly first-run - AppShell bell: 320px popover with badge, kind dots, per-row open (navigates to href) and dismiss; Mark all read + Clear actions - Hidden NotificationDispatcher in AppShell so the action bus can create real notifications via fill notif-title / notif-body / notif-kind / notif-href + click notif-create Data layer - ~/lib/api.ts: typed apiFetch<T> + api.get/post/put/patch/del, auto-attaches the session token, throws structured ApiError, signs out on 401 - ~/lib/resources.ts: example domain entity (CRUD) backed by localStorage today; each call is a 1:1 swap for api.get/post/put/del - routes/resources.tsx: real working table — search, add, inline status edit, delete; seeded demo rows on first load Tests - vitest + jsdom + @testing-library/react + @testing-library/jest-dom + vite-tsconfig-paths installed - vitest.config.ts (jsdom, globals, ~ aliases via tsconfig-paths) - vitest.setup.ts (RTL cleanup + localStorage clear between tests) - app/lib/session.test.ts and resources.test.ts as starter coverage - npm test / npm run test:watch scripts UI Control catalog - Login form, resources CRUD, notifications inbox, and the hidden notif-bridge ids tagged so the assistant can drive every new surface Threads - ThreadMessage now carries optional agentId so per-message authorship survives persona switches and handoffs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
117
app/routes/login.tsx
Normal file
117
app/routes/login.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useNavigate, useSearchParams } from "react-router"
|
||||
import { Loader2, LogIn, Sparkles } from "lucide-react"
|
||||
|
||||
import { Button } from "~/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card"
|
||||
import { Input } from "~/components/ui/input"
|
||||
import { useBrand } from "~/lib/identity"
|
||||
import { pageTitle } from "~/lib/page-meta"
|
||||
import { signIn, useSession } from "~/lib/session"
|
||||
|
||||
export const meta = () => pageTitle("Sign in")
|
||||
|
||||
export default function LoginRoute() {
|
||||
const navigate = useNavigate()
|
||||
const [params] = useSearchParams()
|
||||
const session = useSession()
|
||||
const brand = useBrand()
|
||||
const BrandIcon = brand.icon
|
||||
|
||||
const next = params.get("next") || "/"
|
||||
const [email, setEmail] = useState("you@example.com")
|
||||
const [password, setPassword] = useState("hunter2")
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Already signed in? Bounce.
|
||||
useEffect(() => {
|
||||
if (session) navigate(next, { replace: true })
|
||||
}, [session, next, navigate])
|
||||
|
||||
const onSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setSubmitting(true)
|
||||
setError(null)
|
||||
try {
|
||||
await signIn(email, password)
|
||||
navigate(next, { replace: true })
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Sign-in failed.")
|
||||
setSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative isolate flex min-h-svh items-center justify-center bg-background p-4">
|
||||
<Card className="w-full max-w-sm">
|
||||
<CardHeader className="items-center text-center">
|
||||
<div className="mb-2 flex size-10 items-center justify-center rounded-xl bg-primary text-primary-foreground">
|
||||
<BrandIcon className="size-5" />
|
||||
</div>
|
||||
<CardTitle>Sign in to {brand.name}</CardTitle>
|
||||
<CardDescription>
|
||||
Mock auth — any email + non-empty password works. Wire{" "}
|
||||
<code className="font-mono text-xs">~/lib/session.ts</code> to your
|
||||
real backend.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={onSubmit} className="flex flex-col gap-3">
|
||||
<label className="flex flex-col gap-1.5">
|
||||
<span className="text-sm font-medium">Email</span>
|
||||
<Input
|
||||
data-action="login-email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1.5">
|
||||
<span className="text-sm font-medium">Password</span>
|
||||
<Input
|
||||
data-action="login-password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
{error && (
|
||||
<p className="rounded-md border border-destructive/50 bg-destructive/10 px-2 py-1.5 text-xs text-destructive">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
<Button
|
||||
data-action="login-submit"
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="mt-1"
|
||||
>
|
||||
{submitting ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
) : (
|
||||
<LogIn className="size-4" />
|
||||
)}
|
||||
Sign in
|
||||
</Button>
|
||||
<p className="mt-1 text-center text-xs text-muted-foreground">
|
||||
<Sparkles className="mr-1 inline size-3" />
|
||||
No account needed in dev — credentials aren't checked.
|
||||
</p>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user