Files
ColaFlow-Web/app/(auth)/login/page.tsx
Yaojia Wang 3fa43c5542 feat(frontend): Add SignalR Context for real-time event management
Create comprehensive SignalR Context infrastructure to support real-time updates across the application.

Changes:
- Created SignalRContext.tsx with React Context API for SignalR connection management
- Implemented useSignalREvent and useSignalREvents hooks for simplified event subscription
- Updated Kanban page to use new SignalR hooks (reduced from 150+ lines to ~50 lines)
- Updated root layout to use new SignalRProvider from SignalRContext
- Fixed login page Suspense boundary issue for Next.js 16 compatibility
- Fixed Kanban type issue: made description optional to match API response

Features:
- Auto-connect when user is authenticated
- Auto-reconnect with configurable delays (0s, 2s, 5s, 10s, 30s)
- Toast notifications for connection status changes
- Event subscription management with automatic cleanup
- Support for multiple hub connections (PROJECT, NOTIFICATION)
- TypeScript type safety with proper interfaces

Usage:
```tsx
// Subscribe to single event
useSignalREvent('TaskCreated', (task) => {
  console.log('Task created:', task);
});

// Subscribe to multiple events
useSignalREvents({
  'TaskCreated': (task) => handleTaskCreated(task),
  'TaskUpdated': (task) => handleTaskUpdated(task),
});
```

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 13:21:10 +01:00

138 lines
4.1 KiB
TypeScript

'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import Link from 'next/link';
import { useLogin } from '@/lib/hooks/useAuth';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useSearchParams } from 'next/navigation';
import { Suspense } from 'react';
const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
tenantSlug: z.string().min(1, 'Tenant slug is required'),
});
type LoginForm = z.infer<typeof loginSchema>;
function LoginContent() {
const searchParams = useSearchParams();
const registered = searchParams.get('registered');
const { mutate: login, isPending, error } = useLogin();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginForm>({
resolver: zodResolver(loginSchema),
});
const onSubmit = (data: LoginForm) => {
login(data);
};
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 px-4">
<div className="w-full max-w-md space-y-8">
<div className="text-center">
<h1 className="text-3xl font-bold">ColaFlow</h1>
<p className="mt-2 text-gray-600">Sign in to your account</p>
</div>
<form
onSubmit={handleSubmit(onSubmit)}
className="mt-8 space-y-6 rounded-lg bg-white p-8 shadow"
>
{registered && (
<div className="rounded bg-green-50 p-3 text-sm text-green-600">
Registration successful! Please sign in.
</div>
)}
{error && (
<div className="rounded bg-red-50 p-3 text-sm text-red-600">
{(error as { response?: { data?: { message?: string } } })
?.response?.data?.message || 'Login failed. Please try again.'}
</div>
)}
<div>
<Label htmlFor="tenantSlug">Tenant Slug</Label>
<Input
id="tenantSlug"
type="text"
{...register('tenantSlug')}
className="mt-1"
placeholder="your-company"
/>
{errors.tenantSlug && (
<p className="mt-1 text-sm text-red-600">{errors.tenantSlug.message}</p>
)}
</div>
<div>
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
{...register('email')}
className="mt-1"
placeholder="you@example.com"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email.message}</p>
)}
</div>
<div>
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
{...register('password')}
className="mt-1"
placeholder="••••••••"
/>
{errors.password && (
<p className="mt-1 text-sm text-red-600">
{errors.password.message}
</p>
)}
</div>
<Button type="submit" className="w-full" disabled={isPending}>
{isPending ? 'Signing in...' : 'Sign in'}
</Button>
<div className="text-center text-sm">
<Link href="/register" className="text-blue-600 hover:underline">
Don&apos;t have an account? Sign up
</Link>
</div>
</form>
</div>
</div>
);
}
export default function LoginPage() {
return (
<Suspense fallback={
<div className="flex min-h-screen items-center justify-center bg-gray-50">
<div className="text-center">
<div className="mx-auto h-12 w-12 animate-spin rounded-full border-b-2 border-blue-600"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
}>
<LoginContent />
</Suspense>
);
}