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>
138 lines
4.1 KiB
TypeScript
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'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>
|
|
);
|
|
}
|