diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index bb72f3b..9cb3723 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,6 +8,7 @@
"name": "frontend",
"version": "0.0.0",
"dependencies": {
+ "@hookform/resolvers": "^5.2.2",
"@tailwindcss/vite": "^4.1.18",
"axios": "^1.13.5",
"class-variance-authority": "^0.7.1",
@@ -16,6 +17,7 @@
"radix-ui": "^1.4.3",
"react": "^19.2.0",
"react-dom": "^19.2.0",
+ "react-hook-form": "^7.71.1",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"zod": "^4.3.6"
@@ -1177,6 +1179,18 @@
"hono": "^4"
}
},
+ "node_modules/@hookform/resolvers": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
+ "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/utils": "^0.3.0"
+ },
+ "peerDependencies": {
+ "react-hook-form": "^7.55.0"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -3346,6 +3360,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
"node_modules/@tailwindcss/node": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
@@ -8610,6 +8630,22 @@
"react": "^19.2.4"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.71.1",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
+ "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index d003624..e55fa5c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -11,6 +11,7 @@
"test": "vitest"
},
"dependencies": {
+ "@hookform/resolvers": "^5.2.2",
"@tailwindcss/vite": "^4.1.18",
"axios": "^1.13.5",
"class-variance-authority": "^0.7.1",
@@ -19,6 +20,7 @@
"radix-ui": "^1.4.3",
"react": "^19.2.0",
"react-dom": "^19.2.0",
+ "react-hook-form": "^7.71.1",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"zod": "^4.3.6"
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 574d72e..10239de 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,10 +1,56 @@
+import { useState, useEffect } from "react"
import { MainLayout } from "./components/layout/MainLayout"
+import { LoginForm } from "./components/auth/LoginForm"
+import { RegisterForm } from "./components/auth/RegisterForm"
+import { AuthService } from "./services/auth"
function App() {
+ const [isAuthenticated, setIsAuthenticated] = useState(false)
+ const [authMode, setAuthMode] = useState<"login" | "register">("login")
+
+ useEffect(() => {
+ setIsAuthenticated(AuthService.isAuthenticated())
+ }, [])
+
+ const handleAuthSuccess = () => {
+ setIsAuthenticated(true)
+ }
+
+ const handleLogout = () => {
+ AuthService.logout()
+ setIsAuthenticated(false)
+ }
+
+ if (!isAuthenticated) {
+ return (
+
+ {authMode === "login" ? (
+ setAuthMode("register")}
+ />
+ ) : (
+ setAuthMode("login")}
+ />
+ )}
+
+ )
+ }
+
return (
-
Welcome to Election Analytics
+
+
Welcome to Election Analytics
+
+
Select a conversation from the sidebar or start a new one to begin your analysis.
diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx
new file mode 100644
index 0000000..6ebf3b4
--- /dev/null
+++ b/frontend/src/components/auth/LoginForm.tsx
@@ -0,0 +1,106 @@
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { loginSchema, type LoginInput } from "@/lib/validations/auth"
+import { AuthService } from "@/services/auth"
+import { Button } from "@/components/ui/button"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Input } from "@/components/ui/input"
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { useState } from "react"
+
+interface LoginFormProps {
+ onSuccess: () => void
+ onToggleMode: () => void
+}
+
+export function LoginForm({ onSuccess, onToggleMode }: LoginFormProps) {
+ const [error, setError] = useState
(null)
+ const [isLoading, setIsLoading] = useState(false)
+
+ const form = useForm({
+ resolver: zodResolver(loginSchema),
+ defaultValues: {
+ email: "",
+ password: "",
+ },
+ })
+
+ async function onSubmit(values: LoginInput) {
+ setIsLoading(true)
+ setError(null)
+ try {
+ await AuthService.login(values.email, values.password)
+ onSuccess()
+ } catch (err: any) {
+ setError(err.response?.data?.detail || "Login failed. Please check your credentials.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+ Login
+ Enter your email and password to access your account.
+
+
+
+
+
+ Don't have an account?{" "}
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/auth/RegisterForm.tsx b/frontend/src/components/auth/RegisterForm.tsx
new file mode 100644
index 0000000..97ba60a
--- /dev/null
+++ b/frontend/src/components/auth/RegisterForm.tsx
@@ -0,0 +1,123 @@
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { registerSchema, type RegisterInput } from "@/lib/validations/auth"
+import { AuthService } from "@/services/auth"
+import { Button } from "@/components/ui/button"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Input } from "@/components/ui/input"
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { useState } from "react"
+
+interface RegisterFormProps {
+ onSuccess: () => void
+ onToggleMode: () => void
+}
+
+export function RegisterForm({ onSuccess, onToggleMode }: RegisterFormProps) {
+ const [error, setError] = useState(null)
+ const [isLoading, setIsLoading] = useState(false)
+
+ const form = useForm({
+ resolver: zodResolver(registerSchema),
+ defaultValues: {
+ email: "",
+ password: "",
+ confirmPassword: "",
+ },
+ })
+
+ async function onSubmit(values: RegisterInput) {
+ setIsLoading(true)
+ setError(null)
+ try {
+ await AuthService.register(values.email, values.password)
+ // Automatically login after registration or just switch to login?
+ // For now, let's just switch to login or notify success
+ alert("Registration successful! Please login.")
+ onToggleMode()
+ } catch (err: any) {
+ setError(err.response?.data?.detail || "Registration failed. Please try again.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+ Create an Account
+ Enter your email and a password to get started.
+
+
+
+
+
+ Already have an account?{" "}
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/ui/card.tsx b/frontend/src/components/ui/card.tsx
new file mode 100644
index 0000000..681ad98
--- /dev/null
+++ b/frontend/src/components/ui/card.tsx
@@ -0,0 +1,92 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/frontend/src/components/ui/form.tsx b/frontend/src/components/ui/form.tsx
new file mode 100644
index 0000000..b438928
--- /dev/null
+++ b/frontend/src/components/ui/form.tsx
@@ -0,0 +1,167 @@
+"use client"
+
+import * as React from "react"
+import type { Label as LabelPrimitive } from "radix-ui"
+import { Slot } from "radix-ui"
+import {
+ Controller,
+ FormProvider,
+ useFormContext,
+ useFormState,
+ type ControllerProps,
+ type FieldPath,
+ type FieldValues,
+} from "react-hook-form"
+
+import { cn } from "@/lib/utils"
+import { Label } from "@/components/ui/label"
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+> = {
+ name: TName
+}
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue
+)
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ )
+}
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext)
+ const itemContext = React.useContext(FormItemContext)
+ const { getFieldState } = useFormContext()
+ const formState = useFormState({ name: fieldContext.name })
+ const fieldState = getFieldState(fieldContext.name, formState)
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ")
+ }
+
+ const { id } = itemContext
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ }
+}
+
+type FormItemContextValue = {
+ id: string
+}
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue
+)
+
+function FormItem({ className, ...props }: React.ComponentProps<"div">) {
+ const id = React.useId()
+
+ return (
+
+
+
+ )
+}
+
+function FormLabel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ const { error, formItemId } = useFormField()
+
+ return (
+
+ )
+}
+
+function FormControl({ ...props }: React.ComponentProps) {
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+ return (
+
+ )
+}
+
+function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
+ const { formDescriptionId } = useFormField()
+
+ return (
+
+ )
+}
+
+function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
+ const { error, formMessageId } = useFormField()
+ const body = error ? String(error?.message ?? "") : props.children
+
+ if (!body) {
+ return null
+ }
+
+ return (
+
+ {body}
+
+ )
+}
+
+export {
+ useFormField,
+ Form,
+ FormItem,
+ FormLabel,
+ FormControl,
+ FormDescription,
+ FormMessage,
+ FormField,
+}
diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx
new file mode 100644
index 0000000..f752f82
--- /dev/null
+++ b/frontend/src/components/ui/label.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+import { Label as LabelPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Label({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Label }