diff --git a/frontend/src/app/(protected)/dashboard/page.tsx b/frontend/src/app/(protected)/dashboard/page.tsx
new file mode 100644
index 0000000..3eb9d70
--- /dev/null
+++ b/frontend/src/app/(protected)/dashboard/page.tsx
@@ -0,0 +1,58 @@
+'use client';
+
+import { useTenant } from '@/contexts/tenant-context';
+import { useShifts } from '@/hooks/useShifts';
+import { useTasks } from '@/hooks/useTasks';
+import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import Link from 'next/link';
+
+export default function DashboardPage() {
+ const { activeClubId, clubs } = useTenant();
+ const activeClub = clubs.find(c => c.id === activeClubId);
+
+ const { data: tasksData } = useTasks({ status: 'Open' });
+ const { data: shiftsData } = useShifts({
+ startDate: new Date().toISOString()
+ });
+
+ const openTasksCount = tasksData?.total || 0;
+ const upcomingShiftsCount = shiftsData?.total || 0;
+
+ return (
+
+
+ Welcome to {activeClub?.name || 'WorkClub'}
+
+
+
+
+
+ My Open Tasks
+
+
+ {openTasksCount}
+
+
+
+
+
+ My Upcoming Shifts
+
+
+ {upcomingShiftsCount}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx
new file mode 100644
index 0000000..d7509aa
--- /dev/null
+++ b/frontend/src/app/login/page.tsx
@@ -0,0 +1,26 @@
+'use client';
+
+import { signIn } from 'next-auth/react';
+import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+
+export default function LoginPage() {
+ const handleSignIn = () => {
+ signIn('keycloak');
+ };
+
+ return (
+
+
+
+ WorkClub Manager
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/select-club/page.tsx b/frontend/src/app/select-club/page.tsx
new file mode 100644
index 0000000..ca7aeb0
--- /dev/null
+++ b/frontend/src/app/select-club/page.tsx
@@ -0,0 +1,37 @@
+'use client';
+
+import { useTenant } from '@/contexts/tenant-context';
+import { Card, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
+import { useRouter } from 'next/navigation';
+
+export default function SelectClubPage() {
+ const { clubs, setActiveClub } = useTenant();
+ const router = useRouter();
+
+ const handleClubSelect = (clubId: string) => {
+ setActiveClub(clubId);
+ router.push('/dashboard');
+ };
+
+ return (
+
+
+
Select Your Club
+
+ {clubs.map((club) => (
+ handleClubSelect(club.id)}
+ >
+
+ {club.name}
+ {club.sportType}
+
+
+ ))}
+
+
+
+ );
+}
diff --git a/frontend/src/components/__tests__/dashboard.test.tsx b/frontend/src/components/__tests__/dashboard.test.tsx
new file mode 100644
index 0000000..f3426bf
--- /dev/null
+++ b/frontend/src/components/__tests__/dashboard.test.tsx
@@ -0,0 +1,36 @@
+import { describe, it, expect, vi } from 'vitest';
+import DashboardPage from '@/app/(protected)/dashboard/page';
+import { useTenant } from '@/contexts/tenant-context';
+import { useTasks } from '@/hooks/useTasks';
+import { useShifts } from '@/hooks/useShifts';
+
+vi.mock('@/contexts/tenant-context', () => ({
+ useTenant: vi.fn(),
+}));
+
+vi.mock('@/hooks/useTasks', () => ({
+ useTasks: vi.fn(),
+}));
+
+vi.mock('@/hooks/useShifts', () => ({
+ useShifts: vi.fn(),
+}));
+
+describe('DashboardPage', () => {
+ it('exports a valid component', () => {
+ expect(DashboardPage).toBeDefined();
+ expect(typeof DashboardPage).toBe('function');
+ });
+
+ it('uses useTenant hook', () => {
+ expect(useTenant).toBeDefined();
+ });
+
+ it('uses useTasks hook', () => {
+ expect(useTasks).toBeDefined();
+ });
+
+ it('uses useShifts hook', () => {
+ expect(useShifts).toBeDefined();
+ });
+});
diff --git a/frontend/src/components/__tests__/login.test.tsx b/frontend/src/components/__tests__/login.test.tsx
new file mode 100644
index 0000000..6652a18
--- /dev/null
+++ b/frontend/src/components/__tests__/login.test.tsx
@@ -0,0 +1,18 @@
+import { describe, it, expect, vi } from 'vitest';
+import LoginPage from '@/app/login/page';
+import { signIn } from 'next-auth/react';
+
+vi.mock('next-auth/react', () => ({
+ signIn: vi.fn(),
+}));
+
+describe('LoginPage', () => {
+ it('exports a valid component', () => {
+ expect(LoginPage).toBeDefined();
+ expect(typeof LoginPage).toBe('function');
+ });
+
+ it('uses signIn from next-auth', () => {
+ expect(signIn).toBeDefined();
+ });
+});
diff --git a/frontend/src/components/__tests__/select-club.test.tsx b/frontend/src/components/__tests__/select-club.test.tsx
new file mode 100644
index 0000000..5e505e2
--- /dev/null
+++ b/frontend/src/components/__tests__/select-club.test.tsx
@@ -0,0 +1,22 @@
+import { describe, it, expect, vi } from 'vitest';
+import SelectClubPage from '@/app/select-club/page';
+import { useTenant } from '@/contexts/tenant-context';
+
+vi.mock('@/contexts/tenant-context', () => ({
+ useTenant: vi.fn(),
+}));
+
+vi.mock('next/navigation', () => ({
+ useRouter: vi.fn(),
+}));
+
+describe('SelectClubPage', () => {
+ it('exports a valid component', () => {
+ expect(SelectClubPage).toBeDefined();
+ expect(typeof SelectClubPage).toBe('function');
+ });
+
+ it('uses useTenant hook', () => {
+ expect(useTenant).toBeDefined();
+ });
+});
diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts
index 665fd08..61b4dc6 100644
--- a/frontend/src/lib/api.ts
+++ b/frontend/src/lib/api.ts
@@ -7,9 +7,16 @@ export async function apiClient(
options: RequestInit = {}
): Promise {
const session = await getSession();
- const activeClubId = typeof window !== 'undefined'
- ? localStorage.getItem(ACTIVE_CLUB_KEY)
- : null;
+ let activeClubId: string | null = null;
+
+ try {
+ activeClubId = typeof localStorage !== 'undefined'
+ ? localStorage.getItem(ACTIVE_CLUB_KEY)
+ : null;
+ } catch {
+ // localStorage may not be available in some environments
+ activeClubId = null;
+ }
const headers: Record = {
'Content-Type': 'application/json',