website refactor

This commit is contained in:
2026-01-12 01:01:49 +01:00
parent 5ca6023a5a
commit fefd8d1cd6
294 changed files with 4628 additions and 4991 deletions

View File

@@ -3,225 +3,245 @@
"es2022": true, "es2022": true,
"node": true "node": true
}, },
"parserOptions": { "ignorePatterns": [
"sourceType": "module", "**/dist/**",
"ecmaVersion": 2022 "**/*.d.ts"
}, ],
"ignorePatterns": ["**/dist/**", "**/*.d.ts"],
"settings": {
"import/resolver": {
"typescript": {}
},
"boundaries/elements": [
{
"type": "website",
"pattern": "apps/website/**/*"
},
{
"type": "api",
"pattern": "apps/api/**/*"
},
{
"type": "adapters",
"pattern": ["adapters/**/*", "@adapters/**/*"]
},
{
"type": "core",
"pattern": ["core/**/*", "@core/**/*"]
}
]
},
"overrides": [ "overrides": [
{ {
"files": ["**/index.ts", "**/index.tsx"], "files": [
"**/index.ts",
"**/index.tsx"
],
"rules": { "rules": {
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "Program", "message": "index.ts files are forbidden. Use explicit file names instead (e.g., UserService.ts, not index.ts).",
"message": "index.ts files are forbidden. Use explicit file names instead (e.g., UserService.ts, not index.ts)." "selector": "Program"
} }
] ]
} }
}, },
{ {
"files": ["core/*/application/ports/*/*.ts"], "files": [
"core/*/application/ports/*/*.ts"
],
"rules": { "rules": {
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "TSInterfaceDeclaration[id.name=/^Get.*Port$/]", "message": "Port interface names should not start with 'Get'. Use descriptive names without the 'Get' prefix.",
"message": "Port interface names should not start with 'Get'. Use descriptive names without the 'Get' prefix." "selector": "TSInterfaceDeclaration[id.name=/^Get.*Port$/]"
} }
] ]
} }
}, },
{ {
"files": ["core/**/*.ts"], "files": [
"core/**/*.ts"
],
"rules": { "rules": {
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "TSClassDeclaration[id.name=/Blocker$/], TSInterfaceDeclaration[id.name=/Blocker$/]", "message": "Blocker classes/interfaces are not allowed in core. Use Guards in backend.",
"message": "Blocker classes/interfaces are not allowed in core. Use Guards in backend." "selector": "TSClassDeclaration[id.name=/Blocker$/], TSInterfaceDeclaration[id.name=/Blocker$/]"
}, },
{ {
"selector": "TSClassDeclaration[id.name=/Presenter$/], TSInterfaceDeclaration[id.name=/Presenter$/]", "message": "Presenter classes/interfaces are not allowed in core. Presenters belong in API or frontend layers.",
"message": "Presenter classes/interfaces are not allowed in core. Presenters belong in API or frontend layers." "selector": "TSClassDeclaration[id.name=/Presenter$/], TSInterfaceDeclaration[id.name=/Presenter$/]"
}, },
{ {
"selector": "TSClassDeclaration[id.name=/Dto$/], TSInterfaceDeclaration[id.name=/Dto$/]", "message": "DTO classes/interfaces are not allowed in core. DTOs belong in API or frontend layers.",
"message": "DTO classes/interfaces are not allowed in core. DTOs belong in API or frontend layers." "selector": "TSClassDeclaration[id.name=/Dto$/], TSInterfaceDeclaration[id.name=/Dto$/]"
}, },
{ {
"selector": "TSClassDeclaration[id.name=/ViewModel$/], TSInterfaceDeclaration[id.name=/ViewModel$/]", "message": "ViewModel classes/interfaces are not allowed in core. View Models belong in frontend.",
"message": "ViewModel classes/interfaces are not allowed in core. View Models belong in frontend." "selector": "TSClassDeclaration[id.name=/ViewModel$/], TSInterfaceDeclaration[id.name=/ViewModel$/]"
}, },
{ {
"selector": "TSClassDeclaration[id.name=/CommandModel$/], TSInterfaceDeclaration[id.name=/CommandModel$/]", "message": "CommandModel classes/interfaces are not allowed in core. Command Models belong in frontend.",
"message": "CommandModel classes/interfaces are not allowed in core. Command Models belong in frontend." "selector": "TSClassDeclaration[id.name=/CommandModel$/], TSInterfaceDeclaration[id.name=/CommandModel$/]"
} }
] ]
} }
}, },
{ {
"files": ["core/**/application/dto/**/*.ts", "core/**/application/dtos/**/*.ts"], "files": [
"core/**/application/dto/**/*.ts",
"core/**/application/dtos/**/*.ts"
],
"rules": { "rules": {
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "Program", "message": "core/*/application/dto is forbidden. Use application result models + output ports; DTOs belong in API/website layers.",
"message": "core/*/application/dto is forbidden. Use application result models + output ports; DTOs belong in API/website layers." "selector": "Program"
} }
] ]
} }
}, },
{ {
"files": ["core/**/infrastructure/**/*.ts"], "files": [
"core/**/infrastructure/**/*.ts"
],
"rules": { "rules": {
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "Program", "message": "core/*/infrastructure is forbidden. Implementations must live in adapters/ and be wired in apps/.",
"message": "core/*/infrastructure is forbidden. Implementations must live in adapters/ and be wired in apps/." "selector": "Program"
} }
] ]
} }
}, },
{ {
"files": ["core/**/domain/ports/**/*.ts"], "files": [
"core/**/domain/ports/**/*.ts"
],
"rules": { "rules": {
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "Program", "message": "core/*/domain/ports is forbidden. Ports belong in application/ports (or shared application layer), not domain.",
"message": "core/*/domain/ports is forbidden. Ports belong in application/ports (or shared application layer), not domain." "selector": "Program"
} }
] ]
} }
}, },
{ {
"files": ["core/**/shared/presentation/**/*.ts"], "files": [
"core/**/shared/presentation/**/*.ts"
],
"rules": { "rules": {
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "Program", "message": "core/shared/presentation is forbidden. Presentation belongs in API or website layers.",
"message": "core/shared/presentation is forbidden. Presentation belongs in API or website layers." "selector": "Program"
} }
] ]
} }
}, },
{ {
"files": ["apps/website/**/*.ts"], "files": [
"apps/website/**/*.ts"
],
"rules": { "rules": {
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "TSClassDeclaration[id.name=/Guard$/], TSInterfaceDeclaration[id.name=/Guard$/]", "message": "Guard classes/interfaces are not allowed in frontend. Use Blockers in frontend.",
"message": "Guard classes/interfaces are not allowed in frontend. Use Blockers in frontend." "selector": "TSClassDeclaration[id.name=/Guard$/], TSInterfaceDeclaration[id.name=/Guard$/]"
} }
] ]
} }
}, },
{ {
"files": ["apps/api/**/*.ts", "apps/website/lib/dtos/**/*.ts"], "files": [
"apps/api/**/*.ts",
"apps/website/lib/dtos/**/*.ts"
],
"rules": { "rules": {
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "TSEnumDeclaration[id.name=/^(?!.*Enum$).+/]", "message": "Transport enums must end with 'Enum'.",
"message": "Transport enums must end with 'Enum'." "selector": "TSEnumDeclaration[id.name=/^(?!.*Enum$).+/]"
} }
] ]
} }
}, },
{ {
"files": ["core/*/application/use-cases/*.ts"], "files": [
"core/*/application/use-cases/*.ts"
],
"rules": { "rules": {
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "TSClassDeclaration[id.name=/^(?!.*UseCase$).+/]", "message": "Use Case classes must end with 'UseCase'.",
"message": "Use Case classes must end with 'UseCase'." "selector": "TSClassDeclaration[id.name=/^(?!.*UseCase$).+/]"
} }
] ]
} }
}, },
{ {
"files": ["core/*/application/services/*.ts"], "files": [
"core/*/application/services/*.ts"
],
"rules": { "rules": {
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "TSClassDeclaration[id.name=/^(?!.*Service$).+/]", "message": "Application Service classes must end with 'Service'.",
"message": "Application Service classes must end with 'Service'." "selector": "TSClassDeclaration[id.name=/^(?!.*Service$).+/]"
} }
] ]
} }
}, },
{ {
"files": ["apps/website/lib/view-models/*.ts"], "files": [
"apps/website/lib/view-models/*.ts"
],
"rules": { "rules": {
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "TSClassDeclaration[id.name=/^(?!.*ViewModel$).+/]", "message": "View Model classes must end with 'ViewModel'.",
"message": "View Model classes must end with 'ViewModel'." "selector": "TSClassDeclaration[id.name=/^(?!.*ViewModel$).+/]"
} }
] ]
} }
}, },
{ {
"files": ["apps/website/lib/commands/*.ts"], "files": [
"apps/website/lib/commands/*.ts"
],
"rules": { "rules": {
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "TSClassDeclaration[id.name=/^(?!.*CommandModel$).+/]", "message": "Command Model classes must end with 'CommandModel'.",
"message": "Command Model classes must end with 'CommandModel'." "selector": "TSClassDeclaration[id.name=/^(?!.*CommandModel$).+/]"
} }
] ]
} }
}, },
{ {
"files": ["apps/website/app/**/page.tsx", "apps/website/app/**/page.ts", "apps/website/app/**/layout.tsx", "apps/website/app/**/layout.ts"], "files": [
"apps/website/app/**/page.tsx",
"apps/website/app/**/page.ts",
"apps/website/app/**/layout.tsx",
"apps/website/app/**/layout.ts"
],
"rules": { "rules": {
"import/no-default-export": "off", "import/no-default-export": "off",
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "TSInterfaceDeclaration[id.name=/^I[A-Z]/]", "message": "Interface names should not start with 'I'. Use descriptive names without the 'I' prefix (e.g., 'LiverCompositor' instead of 'ILiveryCompositor').",
"message": "Interface names should not start with 'I'. Use descriptive names without the 'I' prefix (e.g., 'LiverCompositor' instead of 'ILiveryCompositor')." "selector": "TSInterfaceDeclaration[id.name=/^I[A-Z]/]"
} }
] ]
} }
}, },
{ {
"files": ["**/*.ts", "**/*.tsx"], "extends": [
"plugin:import/recommended",
"plugin:import/typescript"
],
"files": [
"**/*.ts",
"**/*.tsx"
],
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "boundaries", "import"], "plugins": [
"extends": ["plugin:import/recommended", "plugin:import/typescript"], "@typescript-eslint",
"boundaries",
"import"
],
"rules": { "rules": {
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-unused-vars": "error",
@@ -231,20 +251,39 @@
"default": "disallow", "default": "disallow",
"rules": [ "rules": [
{ {
"from": ["website"], "allow": [
"allow": ["website"] "website"
],
"from": [
"website"
]
}, },
{ {
"from": ["api"], "allow": [
"allow": ["api", "adapters", "core"] "api",
"adapters",
"core"
],
"from": [
"api"
]
}, },
{ {
"from": ["adapters"], "allow": [
"allow": ["adapters", "core"] "adapters",
"core"
],
"from": [
"adapters"
]
}, },
{ {
"from": ["core"], "allow": [
"allow": ["core"] "core"
],
"from": [
"core"
]
} }
] ]
} }
@@ -254,171 +293,240 @@
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "ExportDefaultDeclaration", "message": "Default exports are forbidden. Use named exports instead.",
"message": "Default exports are forbidden. Use named exports instead." "selector": "ExportDefaultDeclaration"
}, },
{ {
"selector": "TSInterfaceDeclaration[id.name=/^I[A-Z]/]", "message": "Interface names should not start with 'I'. Use descriptive names without the 'I' prefix (e.g., 'LiverCompositor' instead of 'ILiveryCompositor').",
"message": "Interface names should not start with 'I'. Use descriptive names without the 'I' prefix (e.g., 'LiverCompositor' instead of 'ILiveryCompositor')." "selector": "TSInterfaceDeclaration[id.name=/^I[A-Z]/]"
} }
] ]
} }
}, },
{ {
"files": ["core/**/*.ts"], "files": [
"core/**/*.ts"
],
"rules": { "rules": {
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
"selector": "ExportDefaultDeclaration", "message": "Default exports are forbidden. Use named exports instead.",
"message": "Default exports are forbidden. Use named exports instead." "selector": "ExportDefaultDeclaration"
} }
] ]
} }
}, },
{ {
"files": ["apps/website/**/*.tsx", "apps/website/**/*.ts"], "files": [
"apps/website/**/*.tsx",
"apps/website/**/*.ts"
],
"rules": { "rules": {
"no-restricted-syntax": [
"error",
{
"selector": "TSInterfaceDeclaration[id.name=/ViewModel$/], TSTypeAliasDeclaration[id.name=/ViewModel$/], TSClassDeclaration[id.name=/ViewModel$/]",
"message": "ViewModel types must be defined in apps/website/lib/view-models, not in components."
},
{
"selector": "TSInterfaceDeclaration[id.name=/DTO$/], TSTypeAliasDeclaration[id.name=/DTO$/], TSClassDeclaration[id.name=/DTO$/]",
"message": "DTO types are forbidden in website components. Use ViewModels instead."
}
],
"no-restricted-imports": [ "no-restricted-imports": [
"error", "error",
{ {
"paths": [ "paths": [
{ {
"name": "@core/racing", "message": "Imports from @core are forbidden in website components",
"message": "Imports from @core are forbidden in website components" "name": "@core/racing"
}, },
{ {
"name": "@core/analytics", "message": "Imports from @core are forbidden in website components",
"message": "Imports from @core are forbidden in website components" "name": "@core/analytics"
}, },
{ {
"name": "@core/identity", "message": "Imports from @core are forbidden in website components",
"message": "Imports from @core are forbidden in website components" "name": "@core/identity"
}, },
{ {
"name": "@core/media", "message": "Imports from @core are forbidden in website components",
"message": "Imports from @core are forbidden in website components" "name": "@core/media"
}, },
{ {
"name": "@core/notifications", "message": "Imports from @core are forbidden in website components",
"message": "Imports from @core are forbidden in website components" "name": "@core/notifications"
}, },
{ {
"name": "@core/payments", "message": "Imports from @core are forbidden in website components",
"message": "Imports from @core are forbidden in website components" "name": "@core/payments"
}, },
{ {
"name": "@core/shared", "message": "Imports from @core are forbidden in website components",
"message": "Imports from @core are forbidden in website components" "name": "@core/shared"
}, },
{ {
"name": "@core/social", "message": "Imports from @core are forbidden in website components",
"message": "Imports from @core are forbidden in website components" "name": "@core/social"
}, },
{ {
"name": "@adapters", "message": "Imports from @adapters are forbidden in website components",
"message": "Imports from @adapters are forbidden in website components" "name": "@adapters"
}, },
{ {
"name": "@api", "message": "Imports from @api are forbidden in website components",
"message": "Imports from @api are forbidden in website components" "name": "@api"
} }
], ],
"patterns": [ "patterns": [
{ {
"group": ["@core/*"], "group": [
"@core/*"
],
"message": "Imports from @core are forbidden in website components" "message": "Imports from @core are forbidden in website components"
}, },
{ {
"group": ["@adapters/*"], "group": [
"@adapters/*"
],
"message": "Imports from @adapters are forbidden in website components" "message": "Imports from @adapters are forbidden in website components"
}, },
{ {
"group": ["@api/*"], "group": [
"@api/*"
],
"message": "Imports from @api are forbidden in website components" "message": "Imports from @api are forbidden in website components"
} }
] ]
} }
],
"no-restricted-syntax": [
"error",
{
"message": "ViewModel types must be defined in apps/website/lib/view-models, not in components.",
"selector": "TSInterfaceDeclaration[id.name=/ViewModel$/], TSTypeAliasDeclaration[id.name=/ViewModel$/], TSClassDeclaration[id.name=/ViewModel$/]"
},
{
"message": "DTO types are forbidden in website components. Use ViewModels instead.",
"selector": "TSInterfaceDeclaration[id.name=/DTO$/], TSTypeAliasDeclaration[id.name=/DTO$/], TSClassDeclaration[id.name=/DTO$/]"
}
]
}
},
{
"files": [
"apps/api/**/*.test.ts",
"apps/api/**/*.test.tsx"
],
"rules": {
"@typescript-eslint/no-explicit-any": "error",
"no-restricted-syntax": "error"
}
},
{
"files": [
"tests/**/*.ts"
],
"rules": {
"no-restricted-imports": [
"error",
{
"paths": [
{
"message": "Integration tests must use in-memory adapters, not core directly",
"name": "@core/*"
},
{
"message": "Integration tests must use in-memory adapters only",
"name": "@adapters/*"
}
]
}
] ]
} }
}, },
{ {
"files": ["apps/api/**/*.test.ts", "apps/api/**/*.test.tsx"], "files": [
"tests/e2e/**/*.ts"
],
"rules": { "rules": {
"@typescript-eslint/no-explicit-any": "off", "no-restricted-imports": [
"no-restricted-syntax": "off" "error",
{
"patterns": [
{
"group": [
"**/inmemory/**"
],
"message": "E2E tests must use TypeORM/PostgreSQL, not in-memory adapters"
}
]
}
]
} }
}, },
{ {
"files": ["tests/**/*.ts"], "files": [
"core/**/*.ts"
],
"rules": { "rules": {
"no-restricted-imports": ["error", { "no-restricted-imports": [
"paths": [ "error",
{ {
"name": "@core/*", "paths": [
"message": "Integration tests must use in-memory adapters, not core directly" {
}, "message": "Use @testing/* from adapters/testing",
{ "name": "testing"
"name": "@adapters/*", },
"message": "Integration tests must use in-memory adapters only" {
} "message": "Core layer should not depend on testing utilities",
] "name": "@testing/*"
}] }
]
}
]
} }
}, },
{ {
"files": ["tests/e2e/**/*.ts"], "files": [
"adapters/**/*.ts"
],
"rules": { "rules": {
"no-restricted-imports": ["error", { "no-restricted-imports": [
"patterns": [ "error",
{ {
"group": ["**/inmemory/**"], "paths": [
"message": "E2E tests must use TypeORM/PostgreSQL, not in-memory adapters" {
} "message": "Use @testing/* from adapters/testing",
] "name": "testing"
}] }
} ]
}, }
{ ]
"files": ["core/**/*.ts"],
"rules": {
"no-restricted-imports": ["error", {
"paths": [
{
"name": "testing",
"message": "Use @testing/* from adapters/testing"
},
{
"name": "@testing/*",
"message": "Core layer should not depend on testing utilities"
}
]
}]
}
},
{
"files": ["adapters/**/*.ts"],
"rules": {
"no-restricted-imports": ["error", {
"paths": [
{
"name": "testing",
"message": "Use @testing/* from adapters/testing"
}
]
}]
} }
} }
] ],
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},
"settings": {
"boundaries/elements": [
{
"pattern": "apps/website/**/*",
"type": "website"
},
{
"pattern": "apps/api/**/*",
"type": "api"
},
{
"pattern": [
"adapters/**/*",
"@adapters/**/*"
],
"type": "adapters"
},
{
"pattern": [
"core/**/*",
"@core/**/*"
],
"type": "core"
}
],
"import/resolver": {
"typescript": {}
}
}
} }

View File

@@ -1,44 +1,14 @@
{ {
"root": true, "extends": [
"ignorePatterns": ["lib/types/generated/**", "**/*.test.ts", "**/*.test.tsx"], "next/core-web-vitals",
"extends": ["next/core-web-vitals", "plugin:import/recommended", "plugin:import/typescript"], "plugin:import/recommended",
"plugins": ["boundaries", "import", "@typescript-eslint", "unused-imports"], "plugin:import/typescript"
"settings": { ],
"import/resolver": { "ignorePatterns": [
"typescript": {} "lib/types/generated/**",
}, "**/*.test.ts",
"boundaries/elements": [ "**/*.test.tsx"
{ ],
"type": "website",
"pattern": ["**/*"]
}
]
},
"rules": {
"react/no-unescaped-entities": "off",
"@next/next/no-img-element": "off",
"react-hooks/exhaustive-deps": "off",
"react-hooks/rules-of-hooks": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "off",
"import/no-default-export": "off",
"import/no-named-as-default-member": "off",
"no-restricted-syntax": "off",
"boundaries/element-types": [
2,
{
"default": "disallow",
"rules": [
{
"from": ["website"],
"allow": ["website"]
}
]
}
],
"unused-imports/no-unused-imports": "off",
"unused-imports/no-unused-vars": "off"
},
"overrides": [ "overrides": [
{ {
"files": [ "files": [
@@ -56,5 +26,54 @@
"no-restricted-syntax": "off" "no-restricted-syntax": "off"
} }
} }
] ],
"plugins": [
"boundaries",
"import",
"@typescript-eslint",
"unused-imports"
],
"root": true,
"rules": {
"@next/next/no-img-element": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": "error",
"boundaries/element-types": [
2,
{
"default": "disallow",
"rules": [
{
"allow": [
"website"
],
"from": [
"website"
]
}
]
}
],
"import/no-default-export": "error",
"import/no-named-as-default-member": "error",
"no-restricted-syntax": "error",
"react-hooks/exhaustive-deps": "error",
"react-hooks/rules-of-hooks": "error",
"react/no-unescaped-entities": "error",
"unused-imports/no-unused-imports": "off",
"unused-imports/no-unused-vars": "off"
},
"settings": {
"boundaries/elements": [
{
"pattern": [
"**/*"
],
"type": "website"
}
],
"import/resolver": {
"typescript": {}
}
}
} }

View File

@@ -0,0 +1,42 @@
'use server';
import { redirect } from 'next/navigation';
import { AuthApiClient } from '@/lib/api/auth/AuthApiClient';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
/**
* Server action for logout
*
* Performs the logout mutation by calling the API and redirects to login.
* Follows the write boundary contract: all writes enter through server actions.
*/
export async function logoutAction(): Promise<void> {
try {
// Create required dependencies for API client
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: false,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
// Get API base URL from environment
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001';
// Create API client instance
const apiClient = new AuthApiClient(baseUrl, errorReporter, logger);
// Call the logout API endpoint
await apiClient.logout();
// Redirect to login page after successful logout
redirect('/auth/login');
} catch (error) {
// Log error for debugging
console.error('Logout action failed:', error);
// Still redirect even if logout fails - user should be able to leave
redirect('/auth/login');
}
}

View File

@@ -3,7 +3,7 @@
import { useState, useEffect, FormEvent, type ChangeEvent } from 'react'; import { useState, useEffect, FormEvent, type ChangeEvent } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useAuth } from '@/lib/auth/AuthContext'; import { useAuth } from '@/lib/auth/AuthContext';
import { useForgotPassword } from '@/hooks/auth/useForgotPassword'; import { useForgotPassword } from "@/lib/hooks/auth/useForgotPassword";
import Link from 'next/link'; import Link from 'next/link';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { import {

View File

@@ -20,7 +20,7 @@ import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import { useAuth } from '@/lib/auth/AuthContext'; import { useAuth } from '@/lib/auth/AuthContext';
import { useLogin } from '@/hooks/auth/useLogin'; import { useLogin } from "@/lib/hooks/auth/useLogin";
import AuthWorkflowMockup from '@/components/auth/AuthWorkflowMockup'; import AuthWorkflowMockup from '@/components/auth/AuthWorkflowMockup';
import UserRolesPreview from '@/components/auth/UserRolesPreview'; import UserRolesPreview from '@/components/auth/UserRolesPreview';
import { EnhancedFormError } from '@/components/errors/EnhancedFormError'; import { EnhancedFormError } from '@/components/errors/EnhancedFormError';

View File

@@ -20,7 +20,7 @@ import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import { useAuth } from '@/lib/auth/AuthContext'; import { useAuth } from '@/lib/auth/AuthContext';
import { useResetPassword } from '@/hooks/auth/useResetPassword'; import { useResetPassword } from "@/lib/hooks/auth/useResetPassword";
interface FormErrors { interface FormErrors {
newPassword?: string; newPassword?: string;

View File

@@ -27,7 +27,7 @@ import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import { useAuth } from '@/lib/auth/AuthContext'; import { useAuth } from '@/lib/auth/AuthContext';
import { useSignup } from '@/hooks/auth/useSignup'; import { useSignup } from "@/lib/hooks/auth/useSignup";
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper'; import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
interface FormErrors { interface FormErrors {

View File

@@ -1,95 +1,23 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React from 'react';
import type { DashboardViewData } from './DashboardViewData'; import type { DashboardViewData } from '@/templates/view-data/DashboardViewData';
import type { DashboardOverviewViewModelData } from '@/lib/view-models/DashboardOverviewViewModelData'; import type { DashboardPageDto } from '@/lib/page-queries/page-dtos/DashboardPageDto';
import { DashboardOverviewViewModel } from '@/lib/view-models/DashboardOverviewViewModel'; import { DashboardPresenter } from '@/lib/presenters/DashboardPresenter';
import { DashboardTemplate } from '@/templates/DashboardTemplate'; import { DashboardTemplate } from '@/templates/DashboardTemplate';
interface DashboardPageClientProps { interface DashboardPageClientProps {
initialViewData: DashboardViewData; pageDto: DashboardPageDto;
dto: DashboardOverviewViewModelData;
} }
/** /**
* Dashboard Page Client Component * Dashboard Page Client Component
* *
* Two-phase render: * Uses Presenter to transform Page DTO into ViewData
* 1. Initial SSR: Uses ViewData built directly from DTO (no ViewModel) * Presenter is deterministic and side-effect free
* 2. Post-hydration: Instantiates ViewModel and re-renders with enhanced data
*/ */
export function DashboardPageClient({ initialViewData, dto }: DashboardPageClientProps) { export function DashboardPageClient({ pageDto }: DashboardPageClientProps) {
const [viewData, setViewData] = useState<DashboardViewData>(initialViewData); const viewData: DashboardViewData = DashboardPresenter.createViewData(pageDto);
useEffect(() => {
// Phase 2: After hydration, instantiate ViewModel and enhance data
const viewModel = new DashboardOverviewViewModel(dto);
const enhancedViewData: DashboardViewData = {
currentDriver: {
name: viewModel.currentDriverName,
avatarUrl: viewModel.currentDriverAvatarUrl,
country: viewModel.currentDriverCountry,
rating: viewModel.currentDriverRating,
rank: viewModel.currentDriverRank,
totalRaces: viewModel.currentDriverTotalRaces,
wins: viewModel.currentDriverWins,
podiums: viewModel.currentDriverPodiums,
consistency: viewModel.currentDriverConsistency,
},
nextRace: viewModel.nextRace ? {
id: viewModel.nextRace.id,
track: viewModel.nextRace.track,
car: viewModel.nextRace.car,
scheduledAt: viewModel.nextRace.scheduledAt,
formattedDate: viewModel.nextRace.formattedDate,
formattedTime: viewModel.nextRace.formattedTime,
timeUntil: viewModel.nextRace.timeUntil,
isMyLeague: viewModel.nextRace.isMyLeague,
} : null,
upcomingRaces: viewModel.upcomingRaces.map((race) => ({
id: race.id,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt,
formattedDate: race.formattedDate,
formattedTime: race.formattedTime,
timeUntil: race.timeUntil,
isMyLeague: race.isMyLeague,
})),
leagueStandings: viewModel.leagueStandings.map((standing) => ({
leagueId: standing.leagueId,
leagueName: standing.leagueName,
position: standing.position,
points: standing.points,
totalDrivers: standing.totalDrivers,
})),
feedItems: viewModel.feedItems.map((item) => ({
id: item.id,
type: item.type,
headline: item.headline,
body: item.body,
timestamp: item.timestamp,
formattedTime: item.formattedTime,
ctaHref: item.ctaHref,
ctaLabel: item.ctaLabel,
})),
friends: viewModel.friends.map((friend) => ({
id: friend.id,
name: friend.name,
avatarUrl: friend.avatarUrl,
country: friend.country,
})),
activeLeaguesCount: viewModel.activeLeaguesCount,
friendCount: viewModel.friendCount,
hasUpcomingRaces: viewModel.hasUpcomingRaces,
hasLeagueStandings: viewModel.hasLeagueStandings,
hasFeedItems: viewModel.hasFeedItems,
hasFriends: viewModel.hasFriends,
};
setViewData(enhancedViewData);
}, [dto]);
return <DashboardTemplate data={viewData} />; return <DashboardTemplate data={viewData} />;
} }

View File

@@ -1,88 +0,0 @@
import type { DashboardOverviewViewModelData } from '@/lib/view-models/DashboardOverviewViewModelData';
import type { DashboardViewData } from './DashboardViewData';
import {
formatDashboardDate,
formatRating,
formatRank,
formatConsistency,
formatRaceCount,
formatFriendCount,
formatLeaguePosition,
formatPoints,
formatTotalDrivers,
} from '@/lib/display-objects/DashboardDisplay';
/**
* Build DashboardViewData directly from ViewModelData
* Used for SSR phase - no ViewModel instantiation
*/
export function buildDashboardViewData(viewModelData: DashboardOverviewViewModelData): DashboardViewData {
return {
currentDriver: {
name: viewModelData.currentDriver?.name || '',
avatarUrl: viewModelData.currentDriver?.avatarUrl || '',
country: viewModelData.currentDriver?.country || '',
rating: viewModelData.currentDriver ? formatRating(viewModelData.currentDriver.rating) : '0.0',
rank: viewModelData.currentDriver ? formatRank(viewModelData.currentDriver.globalRank) : '0',
totalRaces: viewModelData.currentDriver ? formatRaceCount(viewModelData.currentDriver.totalRaces) : '0',
wins: viewModelData.currentDriver ? formatRaceCount(viewModelData.currentDriver.wins) : '0',
podiums: viewModelData.currentDriver ? formatRaceCount(viewModelData.currentDriver.podiums) : '0',
consistency: viewModelData.currentDriver ? formatConsistency(viewModelData.currentDriver.consistency) : '0%',
},
nextRace: viewModelData.nextRace ? (() => {
const dateInfo = formatDashboardDate(new Date(viewModelData.nextRace.scheduledAt));
return {
id: viewModelData.nextRace.id,
track: viewModelData.nextRace.track,
car: viewModelData.nextRace.car,
scheduledAt: viewModelData.nextRace.scheduledAt,
formattedDate: dateInfo.date,
formattedTime: dateInfo.time,
timeUntil: dateInfo.relative,
isMyLeague: viewModelData.nextRace.isMyLeague,
};
})() : null,
upcomingRaces: viewModelData.upcomingRaces.map((race) => {
const dateInfo = formatDashboardDate(new Date(race.scheduledAt));
return {
id: race.id,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt,
formattedDate: dateInfo.date,
formattedTime: dateInfo.time,
timeUntil: dateInfo.relative,
isMyLeague: race.isMyLeague,
};
}),
leagueStandings: viewModelData.leagueStandingsSummaries.map((standing) => ({
leagueId: standing.leagueId,
leagueName: standing.leagueName,
position: formatLeaguePosition(standing.position),
points: formatPoints(standing.points),
totalDrivers: formatTotalDrivers(standing.totalDrivers),
})),
feedItems: viewModelData.feedSummary.items.map((item) => ({
id: item.id,
type: item.type,
headline: item.headline,
body: item.body,
timestamp: item.timestamp,
formattedTime: formatDashboardDate(new Date(item.timestamp)).relative,
ctaHref: item.ctaHref,
ctaLabel: item.ctaLabel,
})),
friends: viewModelData.friends.map((friend) => ({
id: friend.id,
name: friend.name,
avatarUrl: friend.avatarUrl,
country: friend.country,
})),
activeLeaguesCount: formatRaceCount(viewModelData.activeLeaguesCount),
friendCount: formatFriendCount(viewModelData.friends.length),
hasUpcomingRaces: viewModelData.upcomingRaces.length > 0,
hasLeagueStandings: viewModelData.leagueStandingsSummaries.length > 0,
hasFeedItems: viewModelData.feedSummary.items.length > 0,
hasFriends: viewModelData.friends.length > 0,
};
}

View File

@@ -1,7 +1,6 @@
import { notFound, redirect } from 'next/navigation'; import { notFound, redirect } from 'next/navigation';
import { DashboardPageQuery } from '@/lib/page-queries/DashboardPageQuery'; import { DashboardPageQuery } from '@/lib/page-queries/page-queries/DashboardPageQuery';
import { DashboardPageClient } from './DashboardPageClient'; import { DashboardPageClient } from './DashboardPageClient';
import { buildDashboardViewData } from './DashboardViewDataBuilder';
export default async function DashboardPage() { export default async function DashboardPage() {
const result = await DashboardPageQuery.execute(); const result = await DashboardPageQuery.execute();
@@ -9,23 +8,18 @@ export default async function DashboardPage() {
// Handle result based on status // Handle result based on status
switch (result.status) { switch (result.status) {
case 'ok': case 'ok':
const viewModelData = result.data; // Pass Page DTO to client component
return <DashboardPageClient pageDto={result.dto} />;
// Build SSR ViewData directly from ViewModelData
const ssrViewData = buildDashboardViewData(viewModelData);
// Pass both ViewData (for SSR) and ViewModelData (for client enhancement)
return <DashboardPageClient initialViewData={ssrViewData} dto={viewModelData} />;
case 'notFound': case 'notFound':
notFound(); notFound();
case 'redirect': case 'redirect':
redirect(result.destination); redirect(result.to);
case 'error': case 'error':
// For now, treat as notFound. Could also show error page // For now, treat as notFound. Could also show error page
console.error('Dashboard error:', result.error); console.error('Dashboard error:', result.errorId);
notFound(); notFound();
} }
} }

View File

@@ -2,8 +2,8 @@
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper'; import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
import { DriverProfileTemplate } from '@/templates/DriverProfileTemplate'; import { DriverProfileTemplate } from '@/templates/DriverProfileTemplate';
import { useDriverProfilePageData } from '@/hooks/driver/useDriverProfilePageData'; import { useDriverProfilePageData } from "@/lib/hooks/driver/useDriverProfilePageData";
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';

View File

@@ -19,9 +19,9 @@ export default async function DriverLeaderboardPage() {
); );
// Prepare data for template // Prepare data for template
const data: DriverLeaderboardViewModel | null = driverData as DriverLeaderboardViewModel | null; const data: DriverLeaderboardViewModel | null = driverData;
const hasData = (driverData as any)?.drivers?.length > 0; const hasData = (driverData?.drivers?.length ?? 0) > 0;
// Handle loading state (should be fast since we're using async/await) // Handle loading state (should be fast since we're using async/await)
const isLoading = false; const isLoading = false;

View File

@@ -37,11 +37,11 @@ export default async function LeaderboardsPage() {
// Prepare data for template // Prepare data for template
const data: LeaderboardsPageData = { const data: LeaderboardsPageData = {
drivers: driverData as DriverLeaderboardViewModel | null, drivers: driverData,
teams: teamsData as TeamSummaryViewModel[] | null, teams: teamsData,
}; };
const hasData = (driverData as any)?.drivers?.length > 0 || (teamsData as any)?.length > 0; const hasData = (driverData?.drivers?.length ?? 0) > 0 || (teamsData?.length ?? 0) > 0;
// Handle loading state (should be fast since we're using async/await) // Handle loading state (should be fast since we're using async/await)
const isLoading = false; const isLoading = false;

View File

@@ -2,8 +2,8 @@
import Breadcrumbs from '@/components/layout/Breadcrumbs'; import Breadcrumbs from '@/components/layout/Breadcrumbs';
import LeagueHeader from '@/components/leagues/LeagueHeader'; import LeagueHeader from '@/components/leagues/LeagueHeader';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { useLeagueDetail } from '@/hooks/league/useLeagueDetail'; import { useLeagueDetail } from "@/lib/hooks/league/useLeagueDetail";
import { useParams, usePathname, useRouter } from 'next/navigation'; import { useParams, usePathname, useRouter } from 'next/navigation';
import React from 'react'; import React from 'react';

View File

@@ -11,7 +11,7 @@ import {
useRejectJoinRequest, useRejectJoinRequest,
useUpdateMemberRole, useUpdateMemberRole,
useRemoveMember, useRemoveMember,
} from '@/hooks/league/useLeagueRosterAdmin'; } from "@/lib/hooks/league/useLeagueRosterAdmin";
const ROLE_OPTIONS: MembershipRole[] = ['owner', 'admin', 'steward', 'member']; const ROLE_OPTIONS: MembershipRole[] = ['owner', 'admin', 'steward', 'member'];

View File

@@ -8,8 +8,8 @@ import {
useLeagueAdminStatus, useLeagueAdminStatus,
useLeagueSeasons, useLeagueSeasons,
useLeagueAdminSchedule useLeagueAdminSchedule
} from '@/hooks/league/useLeagueScheduleAdminPageData'; } from "@/lib/hooks/league/useLeagueScheduleAdminPageData";
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useInject } from '@/lib/di/hooks/useInject'; import { useInject } from '@/lib/di/hooks/useInject';
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens'; import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';

View File

@@ -10,12 +10,39 @@ import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporte
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl'; import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import type { LeagueScheduleViewModel } from '@/lib/view-models/LeagueScheduleViewModel'; import { LeagueScheduleViewModel, LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
import type { LeagueScheduleDTO } from '@/lib/types/generated/LeagueScheduleDTO';
import type { RaceDTO } from '@/lib/types/generated/RaceDTO';
interface Props { interface Props {
params: { id: string }; params: { id: string };
} }
function mapRaceDtoToViewModel(race: RaceDTO): LeagueScheduleRaceViewModel {
const scheduledAt = race.date ? new Date(race.date) : new Date(0);
const now = new Date();
const isPast = scheduledAt.getTime() < now.getTime();
const isUpcoming = !isPast;
return {
id: race.id,
name: race.name,
scheduledAt,
isPast,
isUpcoming,
status: isPast ? 'completed' : 'scheduled',
track: undefined,
car: undefined,
sessionType: undefined,
isRegistered: undefined,
};
}
function mapScheduleDtoToViewModel(dto: LeagueScheduleDTO): LeagueScheduleViewModel {
const races = dto.races.map(mapRaceDtoToViewModel);
return new LeagueScheduleViewModel(races);
}
export default async function Page({ params }: Props) { export default async function Page({ params }: Props) {
// Validate params // Validate params
if (!params.id) { if (!params.id) {
@@ -52,7 +79,7 @@ export default async function Page({ params }: Props) {
if (!result) { if (!result) {
throw new Error('League schedule not found'); throw new Error('League schedule not found');
} }
return result; return mapScheduleDtoToViewModel(result);
}); });
if (!data) { if (!data) {

View File

@@ -3,14 +3,14 @@
import { ReadonlyLeagueInfo } from '@/components/leagues/ReadonlyLeagueInfo'; import { ReadonlyLeagueInfo } from '@/components/leagues/ReadonlyLeagueInfo';
import LeagueOwnershipTransfer from '@/components/leagues/LeagueOwnershipTransfer'; import LeagueOwnershipTransfer from '@/components/leagues/LeagueOwnershipTransfer';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { useParams, useRouter } from 'next/navigation'; import { useParams, useRouter } from 'next/navigation';
// Shared state components // Shared state components
import { StateContainer } from '@/components/shared/state/StateContainer'; import { StateContainer } from '@/components/shared/state/StateContainer';
import { LoadingWrapper } from '@/components/shared/state/LoadingWrapper'; import { LoadingWrapper } from '@/components/shared/state/LoadingWrapper';
import { useLeagueAdminStatus } from '@/hooks/league/useLeagueAdminStatus'; import { useLeagueAdminStatus } from "@/lib/hooks/league/useLeagueAdminStatus";
import { useLeagueSettings } from '@/hooks/league/useLeagueSettings'; import { useLeagueSettings } from "@/lib/hooks/league/useLeagueSettings";
import { useInject } from '@/lib/di/hooks/useInject'; import { useInject } from '@/lib/di/hooks/useInject';
import { LEAGUE_SETTINGS_SERVICE_TOKEN } from '@/lib/di/tokens'; import { LEAGUE_SETTINGS_SERVICE_TOKEN } from '@/lib/di/tokens';
import { AlertTriangle, Settings } from 'lucide-react'; import { AlertTriangle, Settings } from 'lucide-react';

View File

@@ -2,9 +2,9 @@
import { LeagueSponsorshipsSection } from '@/components/leagues/LeagueSponsorshipsSection'; import { LeagueSponsorshipsSection } from '@/components/leagues/LeagueSponsorshipsSection';
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper'; import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility'; import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
import { useLeagueSponsorshipsPageData } from '@/hooks/league/useLeagueSponsorshipsPageData'; import { useLeagueSponsorshipsPageData } from "@/lib/hooks/league/useLeagueSponsorshipsPageData";
import { ApiError } from '@/lib/api/base/ApiError'; import { ApiError } from '@/lib/api/base/ApiError';
import { Building } from 'lucide-react'; import { Building } from 'lucide-react';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';

View File

@@ -10,9 +10,11 @@ import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporte
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger'; import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl'; import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import type { LeagueStandingsViewModel } from '@/lib/view-models/LeagueStandingsViewModel'; import { StandingEntryViewModel } from '@/lib/view-models/StandingEntryViewModel';
import { DriverViewModel } from '@/lib/view-models/DriverViewModel'; import { DriverViewModel } from '@/lib/view-models/DriverViewModel';
import type { LeagueMembership } from '@/lib/types/LeagueMembership'; import type { LeagueMembership } from '@/lib/types/LeagueMembership';
import type { LeagueStandingDTO } from '@/lib/types/generated/LeagueStandingDTO';
import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
interface Props { interface Props {
params: { id: string }; params: { id: string };
@@ -49,45 +51,72 @@ export default async function Page({ params }: Props) {
racesApiClient racesApiClient
); );
// Fetch data - using empty string for currentDriverId since this is SSR without session // Fetch data
const result = await service.getLeagueStandings(params.id, ''); const standingsDto = await service.getLeagueStandings(params.id);
if (!result) { if (!standingsDto) {
throw new Error('League standings not found'); throw new Error('League standings not found');
} }
return result;
// Get memberships for transformation
const membershipsDto = await service.getLeagueMemberships(params.id);
// Transform standings to StandingEntryViewModel[]
const standings: LeagueStandingDTO[] = standingsDto.standings || [];
const leaderPoints = standings[0]?.points || 0;
const standingViewModels = standings.map((entry, index) => {
const nextPoints = standings[index + 1]?.points || entry.points;
return new StandingEntryViewModel(entry, leaderPoints, nextPoints, '', undefined);
});
// Extract unique drivers from standings and convert to DriverViewModel[]
const driverMap = new Map<string, DriverViewModel>();
standings.forEach(standing => {
if (standing.driver && !driverMap.has(standing.driver.id)) {
const driver = standing.driver;
driverMap.set(driver.id, new DriverViewModel({
id: driver.id,
name: driver.name,
avatarUrl: null, // DriverDTO doesn't have avatarUrl
iracingId: driver.iracingId,
rating: undefined, // DriverDTO doesn't have rating
country: driver.country,
}));
}
});
const drivers = Array.from(driverMap.values());
// Transform memberships
const memberships: LeagueMembership[] = (membershipsDto.members || []).map((m: LeagueMemberDTO) => ({
driverId: m.driverId,
leagueId: params.id,
role: (m.role as LeagueMembership['role']) ?? 'member',
joinedAt: m.joinedAt,
status: 'active' as const,
}));
return {
standings: standingViewModels,
drivers,
memberships,
};
}); });
if (!data) { if (!data) {
notFound(); notFound();
} }
// Transform data for template
const standings = data.standings ?? [];
const drivers: DriverViewModel[] = data.drivers?.map((d) =>
new DriverViewModel({
id: d.id,
name: d.name,
avatarUrl: d.avatarUrl || null,
iracingId: d.iracingId,
rating: d.rating,
country: d.country,
})
) ?? [];
const memberships: LeagueMembership[] = data.memberships ?? [];
// Create a wrapper component that passes data to the template // Create a wrapper component that passes data to the template
const TemplateWrapper = () => { const TemplateWrapper = () => {
return ( return (
<LeagueStandingsTemplate <LeagueStandingsTemplate
standings={standings} standings={data.standings}
drivers={drivers} drivers={data.drivers}
memberships={memberships} memberships={data.memberships}
leagueId={params.id} leagueId={params.id}
currentDriverId={null} currentDriverId={null}
isAdmin={false} isAdmin={false}
onRemoveMember={() => {}} onRemoveMember={() => {}}
onUpdateRole={() => {}} onUpdateRole={() => {}}
loading={false}
/> />
); );
}; };

View File

@@ -6,7 +6,7 @@ import { ReviewProtestModal } from '@/components/leagues/ReviewProtestModal';
import StewardingStats from '@/components/leagues/StewardingStats'; import StewardingStats from '@/components/leagues/StewardingStats';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import { useLeagueStewardingMutations } from '@/hooks/league/useLeagueStewardingMutations'; import { useLeagueStewardingMutations } from "@/lib/hooks/league/useLeagueStewardingMutations";
import { import {
AlertCircle, AlertCircle,
AlertTriangle, AlertTriangle,

View File

@@ -1,9 +1,9 @@
'use client'; 'use client';
import { useCurrentDriver } from '@/hooks/driver/useCurrentDriver'; import { useCurrentDriver } from "@/lib/hooks/driver/useCurrentDriver";
import { useLeagueAdminStatus } from '@/hooks/league/useLeagueAdminStatus'; import { useLeagueAdminStatus } from "@/lib/hooks/league/useLeagueAdminStatus";
import { useLeagueStewardingData } from '@/hooks/league/useLeagueStewardingData'; import { useLeagueStewardingData } from "@/lib/hooks/league/useLeagueStewardingData";
import { useLeagueStewardingMutations } from '@/hooks/league/useLeagueStewardingMutations'; import { useLeagueStewardingMutations } from "@/lib/hooks/league/useLeagueStewardingMutations";
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper'; import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
import { StewardingTemplate } from './StewardingTemplate'; import { StewardingTemplate } from './StewardingTemplate';
import { LoadingWrapper } from '@/components/shared/state/LoadingWrapper'; import { LoadingWrapper } from '@/components/shared/state/LoadingWrapper';

View File

@@ -2,7 +2,7 @@
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility'; import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
import { useInject } from '@/lib/di/hooks/useInject'; import { useInject } from '@/lib/di/hooks/useInject';
import { PROTEST_SERVICE_TOKEN } from '@/lib/di/tokens'; import { PROTEST_SERVICE_TOKEN } from '@/lib/di/tokens';
@@ -38,8 +38,8 @@ import { useMemo, useState } from 'react';
// Shared state components // Shared state components
import { StateContainer } from '@/components/shared/state/StateContainer'; import { StateContainer } from '@/components/shared/state/StateContainer';
import { LoadingWrapper } from '@/components/shared/state/LoadingWrapper'; import { LoadingWrapper } from '@/components/shared/state/LoadingWrapper';
import { useLeagueAdminStatus } from '@/hooks/league/useLeagueAdminStatus'; import { useLeagueAdminStatus } from "@/lib/hooks/league/useLeagueAdminStatus";
import { useProtestDetail } from '@/hooks/league/useProtestDetail'; import { useProtestDetail } from "@/lib/hooks/league/useProtestDetail";
// Timeline event types // Timeline event types
interface TimelineEvent { interface TimelineEvent {

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useParams } from 'next/navigation'; import { useParams } from 'next/navigation';
import { useLeagueWalletPageData, useLeagueWalletWithdrawal } from '@/hooks/league/useLeagueWalletPageData'; import { useLeagueWalletPageData, useLeagueWalletWithdrawal } from "@/lib/hooks/league/useLeagueWalletPageData";
import { PageWrapper } from '@/components/shared/state/PageWrapper'; import { PageWrapper } from '@/components/shared/state/PageWrapper';
import { WalletTemplate } from './WalletTemplate'; import { WalletTemplate } from './WalletTemplate';
import { Wallet } from 'lucide-react'; import { Wallet } from 'lucide-react';

View File

@@ -7,7 +7,7 @@ import OnboardingWizard from '@/components/onboarding/OnboardingWizard';
import { useAuth } from '@/lib/auth/AuthContext'; import { useAuth } from '@/lib/auth/AuthContext';
// Shared state components // Shared state components
import { useCurrentDriver } from '@/hooks/driver/useCurrentDriver'; import { useCurrentDriver } from "@/lib/hooks/driver/useCurrentDriver";
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper'; import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
// Template component that accepts data // Template component that accepts data

View File

@@ -0,0 +1,17 @@
'use client';
import type { ProfileLeaguesPageDto } from '@/lib/page-queries/page-queries/ProfileLeaguesPageQuery';
import { ProfileLeaguesPresenter } from '@/lib/presenters/ProfileLeaguesPresenter';
import { ProfileLeaguesTemplate } from '@/templates/ProfileLeaguesTemplate';
interface ProfileLeaguesPageClientProps {
pageDto: ProfileLeaguesPageDto;
}
export function ProfileLeaguesPageClient({ pageDto }: ProfileLeaguesPageClientProps) {
// Convert Page DTO to ViewData using Presenter
const viewData = ProfileLeaguesPresenter.toViewData(pageDto);
// Render Template with ViewData
return <ProfileLeaguesTemplate viewData={viewData} />;
}

View File

@@ -1,205 +1,23 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { PageWrapper } from '@/components/shared/state/PageWrapper'; import { ProfileLeaguesPageQuery } from '@/lib/page-queries/ProfileLeaguesPageQuery';
import { PageDataFetcher } from '@/lib/page/PageDataFetcher'; import { ProfileLeaguesPageClient } from './ProfileLeaguesPageClient';
import { LEAGUE_SERVICE_TOKEN, LEAGUE_MEMBERSHIP_SERVICE_TOKEN } from '@/lib/di/tokens';
import type { LeagueService } from '@/lib/services/leagues/LeagueService';
import type { LeagueMembershipService } from '@/lib/services/leagues/LeagueMembershipService';
import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
import { SessionGateway } from '@/lib/gateways/SessionGateway';
import { ContainerManager } from '@/lib/di/container';
interface LeagueWithRole {
league: LeagueSummaryViewModel;
membership: LeagueMembership;
}
interface ProfileLeaguesData {
ownedLeagues: LeagueWithRole[];
memberLeagues: LeagueWithRole[];
}
async function fetchProfileLeaguesData(): Promise<ProfileLeaguesData | null> {
try {
// Get current driver ID from session
const sessionGateway = new SessionGateway();
const session = await sessionGateway.getSession();
if (!session?.user?.primaryDriverId) {
return null;
}
const currentDriverId = session.user.primaryDriverId;
// Fetch leagues using PageDataFetcher
const leagues = await PageDataFetcher.fetch<LeagueService, 'getAllLeagues'>(
LEAGUE_SERVICE_TOKEN,
'getAllLeagues'
);
if (!leagues) {
return null;
}
// Get membership service from container
const container = ContainerManager.getInstance().getContainer();
const membershipService = container.get<LeagueMembershipService>(LEAGUE_MEMBERSHIP_SERVICE_TOKEN);
// Fetch memberships for each league
const memberships = await Promise.all(
leagues.map(async (league) => {
await membershipService.fetchLeagueMemberships(league.id);
const membership = membershipService.getMembership(league.id, currentDriverId);
return membership ? { league, membership } : null;
})
);
// Filter and categorize leagues
const owned: LeagueWithRole[] = [];
const member: LeagueWithRole[] = [];
for (const entry of memberships) {
if (!entry || !entry.membership || entry.membership.status !== 'active') {
continue;
}
if (entry.membership.role === 'owner') {
owned.push(entry);
} else {
member.push(entry);
}
}
return { ownedLeagues: owned, memberLeagues: member };
} catch (error) {
console.error('Failed to fetch profile leagues data:', error);
return null;
}
}
// Template component
function ProfileLeaguesTemplate({ data }: { data: ProfileLeaguesData }) {
return (
<div className="max-w-6xl mx-auto space-y-8">
<div>
<h1 className="text-3xl font-bold text-white mb-2">Manage leagues</h1>
<p className="text-gray-400 text-sm">
View leagues you own and participate in, and jump into league admin tools.
</p>
</div>
{/* Leagues You Own */}
<div className="bg-charcoal rounded-lg border border-charcoal-outline p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-white">Leagues you own</h2>
{data.ownedLeagues.length > 0 && (
<span className="text-xs text-gray-400">
{data.ownedLeagues.length} {data.ownedLeagues.length === 1 ? 'league' : 'leagues'}
</span>
)}
</div>
{data.ownedLeagues.length === 0 ? (
<p className="text-sm text-gray-400">
You don't own any leagues yet in this session.
</p>
) : (
<div className="space-y-3">
{data.ownedLeagues.map(({ league }) => (
<div
key={league.id}
className="flex items-center justify-between p-4 rounded-lg bg-deep-graphite border border-charcoal-outline"
>
<div>
<h3 className="text-white font-medium">{league.name}</h3>
<p className="text-xs text-gray-400 mt-1 line-clamp-2">
{league.description}
</p>
</div>
<div className="flex items-center gap-2">
<a
href={`/leagues/${league.id}`}
className="text-sm text-gray-300 hover:text-white underline-offset-2 hover:underline"
>
View
</a>
<a href={`/leagues/${league.id}?tab=admin`}>
<button className="bg-primary hover:bg-primary/90 text-white text-xs px-3 py-1.5 rounded transition-colors">
Manage
</button>
</a>
</div>
</div>
))}
</div>
)}
</div>
{/* Leagues You're In */}
<div className="bg-charcoal rounded-lg border border-charcoal-outline p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-white">Leagues you're in</h2>
{data.memberLeagues.length > 0 && (
<span className="text-xs text-gray-400">
{data.memberLeagues.length} {data.memberLeagues.length === 1 ? 'league' : 'leagues'}
</span>
)}
</div>
{data.memberLeagues.length === 0 ? (
<p className="text-sm text-gray-400">
You're not a member of any other leagues yet.
</p>
) : (
<div className="space-y-3">
{data.memberLeagues.map(({ league, membership }) => (
<div
key={league.id}
className="flex items-center justify-between p-4 rounded-lg bg-deep-graphite border border-charcoal-outline"
>
<div>
<h3 className="text-white font-medium">{league.name}</h3>
<p className="text-xs text-gray-400 mt-1 line-clamp-2">
{league.description}
</p>
<p className="text-xs text-gray-500 mt-1">
Your role:{' '}
{membership.role.charAt(0).toUpperCase() + membership.role.slice(1)}
</p>
</div>
<a
href={`/leagues/${league.id}`}
className="text-sm text-gray-300 hover:text-white underline-offset-2 hover:underline"
>
View league
</a>
</div>
))}
</div>
)}
</div>
</div>
);
}
export default async function ProfileLeaguesPage() { export default async function ProfileLeaguesPage() {
const data = await fetchProfileLeaguesData(); const result = await ProfileLeaguesPageQuery.execute();
if (!data) { switch (result.status) {
notFound(); case 'notFound':
notFound();
case 'redirect':
// Note: In Next.js, redirect would be imported from next/navigation
// For now, we'll handle this case by returning notFound
// In a full implementation, you'd use: redirect(result.to);
notFound();
case 'error':
// For now, treat errors as notFound
// In a full implementation, you might render an error page
notFound();
case 'ok':
return <ProfileLeaguesPageClient pageDto={result.dto} />;
} }
return (
<PageWrapper
data={data}
Template={ProfileLeaguesTemplate}
loading={{ variant: 'skeleton', message: 'Loading your leagues...' }}
errorConfig={{ variant: 'full-screen' }}
empty={{
title: 'No leagues found',
description: 'You are not a member of any leagues yet.',
}}
/>
);
} }

View File

@@ -6,8 +6,8 @@ import ProfileSettings from '@/components/drivers/ProfileSettings';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { useDriverProfile } from '@/hooks/driver/useDriverProfile'; import { useDriverProfile } from "@/lib/hooks/driver/useDriverProfile";
import { useInject } from '@/lib/di/hooks/useInject'; import { useInject } from '@/lib/di/hooks/useInject';
import { DRIVER_SERVICE_TOKEN, MEDIA_SERVICE_TOKEN } from '@/lib/di/tokens'; import { DRIVER_SERVICE_TOKEN, MEDIA_SERVICE_TOKEN } from '@/lib/di/tokens';
import type { import type {

View File

@@ -5,8 +5,8 @@ import { SponsorshipRequestsTemplate } from '@/templates/SponsorshipRequestsTemp
import { import {
useSponsorshipRequestsPageData, useSponsorshipRequestsPageData,
useSponsorshipRequestMutations useSponsorshipRequestMutations
} from '@/hooks/sponsor/useSponsorshipRequestsPageData'; } from "@/lib/hooks/sponsor/useSponsorshipRequestsPageData";
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
export default function SponsorshipRequestsPage() { export default function SponsorshipRequestsPage() {
const currentDriverId = useEffectiveDriverId(); const currentDriverId = useEffectiveDriverId();

View File

@@ -2,10 +2,10 @@
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper'; import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
import { RaceResultsTemplate } from '@/templates/RaceResultsTemplate'; import { RaceResultsTemplate } from '@/templates/RaceResultsTemplate';
import { useRaceResultsPageData } from '@/hooks/race/useRaceResultsPageData'; import { useRaceResultsPageData } from "@/lib/hooks/race/useRaceResultsPageData";
import { RaceResultsDataTransformer } from '@/lib/view-models/RaceResultsDataTransformer'; import { RaceResultsDataTransformer } from '@/lib/view-models/RaceResultsDataTransformer';
import { useLeagueMemberships } from '@/hooks/league/useLeagueMemberships'; import { useLeagueMemberships } from "@/lib/hooks/league/useLeagueMemberships";
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { useState } from 'react'; import { useState } from 'react';
import { notFound, useRouter } from 'next/navigation'; import { notFound, useRouter } from 'next/navigation';
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility'; import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';

View File

@@ -8,10 +8,11 @@ import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
import { RACE_STEWARDING_SERVICE_TOKEN } from '@/lib/di/tokens'; import { RACE_STEWARDING_SERVICE_TOKEN } from '@/lib/di/tokens';
import { RaceStewardingService } from '@/lib/services/races/RaceStewardingService'; import { RaceStewardingService } from '@/lib/services/races/RaceStewardingService';
import type { RaceStewardingViewModel } from '@/lib/view-models/RaceStewardingViewModel'; import type { RaceStewardingViewModel } from '@/lib/view-models/RaceStewardingViewModel';
import { useLeagueMemberships } from '@/hooks/league/useLeagueMemberships'; import { useLeagueMemberships } from "@/lib/hooks/league/useLeagueMemberships";
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility'; import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
import { Gavel } from 'lucide-react'; import { Gavel } from 'lucide-react';
import type { LeagueMembershipsDTO } from '@/lib/types/generated/LeagueMembershipsDTO';
export default function RaceStewardingPage() { export default function RaceStewardingPage() {
const router = useRouter(); const router = useRouter();
@@ -61,7 +62,7 @@ export default function RaceStewardingPage() {
// Fetch membership // Fetch membership
const { data: membershipsData } = useLeagueMemberships(pageData?.league?.id || '', currentDriverId || ''); const { data: membershipsData } = useLeagueMemberships(pageData?.league?.id || '', currentDriverId || '');
const currentMembership = membershipsData?.memberships.find(m => m.driverId === currentDriverId); const currentMembership = membershipsData?.members.find(m => m.driverId === currentDriverId);
const isAdmin = currentMembership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(currentMembership.role) : false; const isAdmin = currentMembership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(currentMembership.role) : false;
// Actions // Actions

View File

@@ -4,7 +4,7 @@ import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper'; import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
import { RacesAllTemplate, StatusFilter } from '@/templates/RacesAllTemplate'; import { RacesAllTemplate, StatusFilter } from '@/templates/RacesAllTemplate';
import { useAllRacesPageData } from '@/hooks/race/useAllRacesPageData'; import { useAllRacesPageData } from "@/lib/hooks/race/useAllRacesPageData";
import { Flag } from 'lucide-react'; import { Flag } from 'lucide-react';
const ITEMS_PER_PAGE = 10; const ITEMS_PER_PAGE = 10;

View File

@@ -10,7 +10,7 @@ import StatusBadge from '@/components/ui/StatusBadge';
import InfoBanner from '@/components/ui/InfoBanner'; import InfoBanner from '@/components/ui/InfoBanner';
import PageHeader from '@/components/ui/PageHeader'; import PageHeader from '@/components/ui/PageHeader';
import { siteConfig } from '@/lib/siteConfig'; import { siteConfig } from '@/lib/siteConfig';
import { useSponsorBilling } from '@/hooks/sponsor/useSponsorBilling'; import { useSponsorBilling } from "@/lib/hooks/sponsor/useSponsorBilling";
import { useInject } from '@/lib/di/hooks/useInject'; import { useInject } from '@/lib/di/hooks/useInject';
import { SPONSOR_SERVICE_TOKEN } from '@/lib/di/tokens'; import { SPONSOR_SERVICE_TOKEN } from '@/lib/di/tokens';
import { import {

View File

@@ -8,7 +8,7 @@ import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import StatusBadge from '@/components/ui/StatusBadge'; import StatusBadge from '@/components/ui/StatusBadge';
import InfoBanner from '@/components/ui/InfoBanner'; import InfoBanner from '@/components/ui/InfoBanner';
import { useSponsorSponsorships } from '@/hooks/sponsor/useSponsorSponsorships'; import { useSponsorSponsorships } from "@/lib/hooks/sponsor/useSponsorSponsorships";
import { import {
Megaphone, Megaphone,
Trophy, Trophy,

View File

@@ -33,6 +33,7 @@ import {
Smartphone, Smartphone,
AlertCircle AlertCircle
} from 'lucide-react'; } from 'lucide-react';
import { logoutAction } from '@/app/actions/logoutAction';
// ============================================================================ // ============================================================================
// Types // Types
@@ -174,10 +175,8 @@ export default function SponsorSettingsPage() {
const handleDeleteAccount = () => { const handleDeleteAccount = () => {
if (confirm('Are you sure you want to delete your sponsor account? This action cannot be undone. All sponsorship data will be permanently removed.')) { if (confirm('Are you sure you want to delete your sponsor account? This action cannot be undone. All sponsorship data will be permanently removed.')) {
// Call logout API to clear session // Call the logout action directly
fetch('/api/auth/logout', { method: 'POST' }).finally(() => { logoutAction();
router.push('/');
});
} }
}; };

View File

@@ -0,0 +1,91 @@
'use client';
import { useState, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import type { TeamsPageDto } from '@/lib/page-queries/page-queries/TeamsPageQuery';
import { TeamsPresenter } from '@/lib/presenters/TeamsPresenter';
import { TeamsTemplate } from '@/templates/TeamsTemplate';
import type { TeamSummaryData } from '@/templates/view-data/TeamsViewData';
interface TeamsPageClientProps {
pageDto: TeamsPageDto;
}
export function TeamsPageClient({ pageDto }: TeamsPageClientProps) {
const router = useRouter();
// Use presenter to create ViewData
const viewData = TeamsPresenter.createViewData(pageDto);
// UI state
const [searchQuery, setSearchQuery] = useState('');
const [showCreateForm, setShowCreateForm] = useState(false);
// Filter teams based on search query
const filteredTeams = useMemo(() => {
if (!searchQuery) return viewData.teams;
const query = searchQuery.toLowerCase();
return viewData.teams.filter((team: TeamSummaryData) =>
team.teamName.toLowerCase().includes(query) ||
team.leagueName.toLowerCase().includes(query)
);
}, [viewData.teams, searchQuery]);
// Update viewData with filtered teams
const templateViewData = {
...viewData,
teams: filteredTeams,
};
// Event handlers
const handleSearchChange = (query: string) => {
setSearchQuery(query);
};
const handleShowCreateForm = () => {
setShowCreateForm(true);
};
const handleHideCreateForm = () => {
setShowCreateForm(false);
};
const handleTeamClick = (teamId: string) => {
router.push(`/teams/${teamId}`);
};
const handleCreateSuccess = (teamId: string) => {
setShowCreateForm(false);
router.push(`/teams/${teamId}`);
};
const handleBrowseTeams = () => {
const element = document.getElementById('teams-list');
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
};
const handleSkillLevelClick = (level: string) => {
const element = document.getElementById(`level-${level}`);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
};
return (
<TeamsTemplate
teams={templateViewData.teams}
searchQuery={searchQuery}
showCreateForm={showCreateForm}
onSearchChange={handleSearchChange}
onShowCreateForm={handleShowCreateForm}
onHideCreateForm={handleHideCreateForm}
onTeamClick={handleTeamClick}
onCreateSuccess={handleCreateSuccess}
onBrowseTeams={handleBrowseTeams}
onSkillLevelClick={handleSkillLevelClick}
/>
);
}

View File

@@ -0,0 +1,68 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import type { TeamDetailPageDto } from '@/lib/page-queries/page-queries/TeamDetailPageQuery';
import { TeamDetailPresenter } from '@/lib/presenters/TeamDetailPresenter';
import TeamDetailTemplate from '@/templates/TeamDetailTemplate';
type Tab = 'overview' | 'roster' | 'standings' | 'admin';
interface TeamDetailPageClientProps {
pageDto: TeamDetailPageDto;
}
export function TeamDetailPageClient({ pageDto }: TeamDetailPageClientProps) {
const router = useRouter();
// Use presenter to create ViewData
const viewData = TeamDetailPresenter.createViewData(pageDto);
// UI state
const [activeTab, setActiveTab] = useState<Tab>('overview');
const [loading] = useState(false);
// Event handlers
const handleTabChange = (tab: Tab) => {
setActiveTab(tab);
};
const handleUpdate = () => {
// Trigger a refresh by reloading the page
router.refresh();
};
const handleRemoveMember = (driverId: string) => {
// This would call an API to remove the member
// For now, just log
console.log('Remove member:', driverId);
// In a real implementation, you'd have a mutation hook here
alert('Remove member functionality would be implemented here');
};
const handleChangeRole = (driverId: string, newRole: 'owner' | 'admin' | 'member') => {
// This would call an API to change the role
console.log('Change role:', driverId, newRole);
// In a real implementation, you'd have a mutation hook here
alert('Change role functionality would be implemented here');
};
const handleGoBack = () => {
router.back();
};
return (
<TeamDetailTemplate
team={viewData.team}
memberships={viewData.memberships}
activeTab={activeTab}
loading={loading}
isAdmin={viewData.isAdmin}
onTabChange={handleTabChange}
onUpdate={handleUpdate}
onRemoveMember={handleRemoveMember}
onChangeRole={handleChangeRole}
onGoBack={handleGoBack}
/>
);
}

View File

@@ -1,102 +1,22 @@
import { PageWrapper } from '@/components/shared/state/PageWrapper';
import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
import { TeamService } from '@/lib/services/teams/TeamService';
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { TeamDetailsViewModel } from '@/lib/view-models/TeamDetailsViewModel'; import { TeamDetailPageQuery } from '@/lib/page-queries/TeamDetailPageQuery';
import { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel'; import TeamDetailPageClient from './TeamDetailPageClient';
import TeamDetailTemplate from '@/templates/TeamDetailTemplate';
// Template wrapper to adapt TeamDetailTemplate for SSR
interface TeamDetailData {
team: TeamDetailsViewModel;
memberships: TeamMemberViewModel[];
isAdmin: boolean;
}
function TeamDetailTemplateWrapper({ data }: { data: TeamDetailData }) {
return (
<TeamDetailTemplate
team={data.team}
memberships={data.memberships}
activeTab="overview"
loading={false}
isAdmin={data.isAdmin}
// Event handlers are no-ops for SSR (client will handle real interactions)
onTabChange={() => {}}
onUpdate={() => {}}
onRemoveMember={() => {}}
onChangeRole={() => {}}
onGoBack={() => {}}
/>
);
}
export default async function Page({ params }: { params: { id: string } }) { export default async function Page({ params }: { params: { id: string } }) {
// Validate params const result = await TeamDetailPageQuery.execute(params.id);
if (!params.id) {
notFound();
}
// Fetch data using PageDataFetcher.fetchManual switch (result.status) {
const data = await PageDataFetcher.fetchManual(async () => { case 'ok':
// Manual dependency creation return <TeamDetailPageClient pageDto={result.dto} />;
const baseUrl = getWebsiteApiBaseUrl(); case 'notFound':
const logger = new ConsoleLogger(); notFound();
const errorReporter = new EnhancedErrorReporter(logger, { case 'redirect':
showUserNotifications: true, // This would typically use redirect() from next/navigation
logToConsole: true, // but we need to handle it at the page level
reportToExternal: process.env.NODE_ENV === 'production',
});
// Create API client
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger);
// Create service
const service = new TeamService(teamsApiClient);
// For server-side, we need a current driver ID
// This would typically come from session, but for server components we'll use a placeholder
const currentDriverId = ''; // Placeholder - would need session handling
// Fetch team details
const teamData = await service.getTeamDetails(params.id, currentDriverId);
if (!teamData) {
return null; return null;
} case 'error':
// For now, treat errors as not found
// Fetch team members // In production, you might want a proper error page
const membersData = await service.getTeamMembers(params.id, currentDriverId, teamData.ownerId || ''); notFound();
// Determine admin status
const isAdmin = teamData.isOwner ||
(membersData || []).some((m: any) => m.driverId === currentDriverId && (m.role === 'manager' || m.role === 'owner'));
return {
team: teamData,
memberships: membersData || [],
isAdmin,
};
});
if (!data) {
notFound();
} }
return (
<PageWrapper
data={data}
Template={TeamDetailTemplateWrapper}
loading={{ variant: 'skeleton', message: 'Loading team details...' }}
errorConfig={{ variant: 'full-screen' }}
empty={{
title: 'Team not found',
description: 'The team you are looking for does not exist or has been removed.',
}}
/>
);
} }

View File

@@ -19,9 +19,9 @@ export default async function TeamLeaderboardPage() {
); );
// Prepare data for template // Prepare data for template
const data: TeamSummaryViewModel[] | null = teamsData as TeamSummaryViewModel[] | null; const data: TeamSummaryViewModel[] | null = teamsData;
const hasData = (teamsData as any)?.length > 0; const hasData = (teamsData?.length ?? 0) > 0;
// Handle loading state (should be fast since we're using async/await) // Handle loading state (should be fast since we're using async/await)
const isLoading = false; const isLoading = false;

View File

@@ -1,97 +1,22 @@
import { PageWrapper } from '@/components/shared/state/PageWrapper';
import TeamsTemplate from '@/templates/TeamsTemplate';
import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
import { TeamService } from '@/lib/services/teams/TeamService';
import { TeamsApiClient } from '@/lib/api/teams/TeamsApiClient';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel'; import { TeamsPageQuery } from '@/lib/page-queries/TeamsPageQuery';
import TeamsPageClient from './TeamsPageClient';
// Helper to compute derived data for SSR
function computeDerivedData(teams: TeamSummaryViewModel[]) {
// Group teams by performance level (skill level)
const teamsByLevel = teams.reduce((acc, team) => {
const level = team.performanceLevel || 'intermediate';
if (!acc[level]) {
acc[level] = [];
}
acc[level].push(team);
return acc;
}, {} as Record<string, TeamSummaryViewModel[]>);
// Get top teams (by rating, descending)
const topTeams = [...teams]
.filter(t => t.rating !== undefined)
.sort((a, b) => (b.rating || 0) - (a.rating || 0))
.slice(0, 5);
// Count recruiting teams
const recruitingCount = teams.filter(t => t.isRecruiting).length;
// For SSR, filtered teams = all teams (no search filter applied server-side)
const filteredTeams = teams;
return {
teamsByLevel,
topTeams,
recruitingCount,
filteredTeams,
};
}
// Template wrapper for SSR
function TeamsTemplateWrapper({ data }: { data: TeamSummaryViewModel[] }) {
const derived = computeDerivedData(data);
// Provide default values for SSR
// The template will handle client-side state management
return (
<TeamsTemplate
teams={data}
isLoading={false}
searchQuery=""
showCreateForm={false}
teamsByLevel={derived.teamsByLevel}
topTeams={derived.topTeams}
recruitingCount={derived.recruitingCount}
filteredTeams={derived.filteredTeams}
// No-op handlers for SSR (client will override)
onSearchChange={() => {}}
onShowCreateForm={() => {}}
onHideCreateForm={() => {}}
onTeamClick={() => {}}
onCreateSuccess={() => {}}
onBrowseTeams={() => {}}
onSkillLevelClick={() => {}}
/>
);
}
export default async function Page() { export default async function Page() {
const data = await PageDataFetcher.fetchManual(async () => { const result = await TeamsPageQuery.execute();
// Manual dependency creation
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: process.env.NODE_ENV === 'production',
});
// Create API client switch (result.status) {
const teamsApiClient = new TeamsApiClient(baseUrl, errorReporter, logger); case 'ok':
return <TeamsPageClient pageDto={result.dto} />;
// Create service case 'notFound':
const service = new TeamService(teamsApiClient); notFound();
case 'redirect':
return await service.getAllTeams(); // This would typically use redirect() from next/navigation
}); // but we need to handle it at the page level
return null;
if (!data) { case 'error':
notFound(); // For now, treat errors as not found
// In production, you might want a proper error page
notFound();
} }
return <PageWrapper data={data} Template={TeamsTemplateWrapper} />;
} }

View File

@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { apiClient } from '@/lib/apiClient'; import { apiClient } from '@/lib/apiClient';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import { AdminViewModelService } from '@/lib/services/AdminViewModelService'; import { AdminViewModelPresenter } from '@/lib/view-models/AdminViewModelPresenter';
import { DashboardStatsViewModel } from '@/lib/view-models/AdminUserViewModel'; import { DashboardStatsViewModel } from '@/lib/view-models/AdminUserViewModel';
import { import {
Users, Users,
@@ -31,7 +31,7 @@ export function AdminDashboardPage() {
const response = await apiClient.admin.getDashboardStats(); const response = await apiClient.admin.getDashboardStats();
// Map DTO to View Model // Map DTO to View Model
const viewModel = AdminViewModelService.mapDashboardStats(response); const viewModel = AdminViewModelPresenter.mapDashboardStats(response);
setStats(viewModel); setStats(viewModel);
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : 'Failed to load stats'; const message = err instanceof Error ? err.message : 'Failed to load stats';
@@ -222,4 +222,4 @@ export function AdminDashboardPage() {
</Card> </Card>
</div> </div>
); );
} }

View File

@@ -10,6 +10,7 @@ import {
Activity Activity
} from 'lucide-react'; } from 'lucide-react';
import { useRouter, usePathname } from 'next/navigation'; import { useRouter, usePathname } from 'next/navigation';
import { logoutAction } from '@/app/actions/logoutAction';
interface AdminLayoutProps { interface AdminLayoutProps {
children: ReactNode; children: ReactNode;
@@ -62,15 +63,6 @@ export function AdminLayout({ children }: AdminLayoutProps) {
} }
}; };
const handleLogout = async () => {
try {
await fetch('/api/auth/logout', { method: 'POST' });
router.push('/');
} catch (error) {
console.error('Logout failed:', error);
}
};
return ( return (
<div className="flex h-screen bg-deep-graphite overflow-hidden"> <div className="flex h-screen bg-deep-graphite overflow-hidden">
{/* Sidebar */} {/* Sidebar */}
@@ -132,13 +124,16 @@ export function AdminLayout({ children }: AdminLayoutProps) {
{isSidebarOpen && <span className="text-sm">Toggle Sidebar</span>} {isSidebarOpen && <span className="text-sm">Toggle Sidebar</span>}
</button> </button>
<button {/* Use form with server action for logout */}
onClick={handleLogout} <form action={logoutAction}>
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-racing-red hover:bg-racing-red/10 transition-colors" <button
> type="submit"
<LogOut className="w-5 h-5" /> className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-racing-red hover:bg-racing-red/10 transition-colors"
{isSidebarOpen && <span className="text-sm">Logout</span>} >
</button> <LogOut className="w-5 h-5" />
{isSidebarOpen && <span className="text-sm">Logout</span>}
</button>
</form>
</div> </div>
</aside> </aside>

View File

@@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
import { apiClient } from '@/lib/apiClient'; import { apiClient } from '@/lib/apiClient';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import StatusBadge from '@/components/ui/StatusBadge'; import StatusBadge from '@/components/ui/StatusBadge';
import { AdminViewModelService } from '@/lib/services/AdminViewModelService'; import { AdminViewModelPresenter } from '@/lib/view-models/AdminViewModelPresenter';
import { AdminUserViewModel, UserListViewModel } from '@/lib/view-models/AdminUserViewModel'; import { AdminUserViewModel, UserListViewModel } from '@/lib/view-models/AdminUserViewModel';
import { import {
Search, Search,
@@ -47,7 +47,7 @@ export function AdminUsersPage() {
}); });
// Map DTO to View Model // Map DTO to View Model
const viewModel = AdminViewModelService.mapUserList(response); const viewModel = AdminViewModelPresenter.mapUserList(response);
setUserList(viewModel); setUserList(viewModel);
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : 'Failed to load users'; const message = err instanceof Error ? err.message : 'Failed to load users';
@@ -356,4 +356,4 @@ export function AdminUsersPage() {
)} )}
</div> </div>
); );
} }

View File

@@ -4,6 +4,18 @@ import { useState, useEffect } from 'react';
import { Bug, X, Settings, Shield, Activity } from 'lucide-react'; import { Bug, X, Settings, Shield, Activity } from 'lucide-react';
import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler'; import { getGlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
import { getGlobalApiLogger } from '@/lib/infrastructure/ApiRequestLogger'; import { getGlobalApiLogger } from '@/lib/infrastructure/ApiRequestLogger';
import type { GlobalErrorHandler } from '@/lib/infrastructure/GlobalErrorHandler';
import type { ApiRequestLogger } from '@/lib/infrastructure/ApiRequestLogger';
// Extend Window interface for debug globals
declare global {
interface Window {
__GRIDPILOT_FETCH_LOGGED__?: boolean;
__GRIDPILOT_GLOBAL_HANDLER__?: GlobalErrorHandler;
__GRIDPILOT_API_LOGGER__?: ApiRequestLogger;
__GRIDPILOT_REACT_ERRORS__?: Array<{ error: unknown; componentStack?: string }>;
}
}
interface DebugModeToggleProps { interface DebugModeToggleProps {
/** /**
@@ -74,21 +86,21 @@ export function DebugModeToggle({ show }: DebugModeToggleProps) {
globalHandler.initialize(); globalHandler.initialize();
// Override fetch with logging // Override fetch with logging
if (!(window as any).__GRIDPILOT_FETCH_LOGGED__) { if (!window.__GRIDPILOT_FETCH_LOGGED__) {
const loggedFetch = apiLogger.createLoggedFetch(); const loggedFetch = apiLogger.createLoggedFetch();
window.fetch = loggedFetch as any; window.fetch = loggedFetch as typeof fetch;
(window as any).__GRIDPILOT_FETCH_LOGGED__ = true; window.__GRIDPILOT_FETCH_LOGGED__ = true;
} }
// Expose to window for easy access // Expose to window for easy access
(window as any).__GRIDPILOT_GLOBAL_HANDLER__ = globalHandler; window.__GRIDPILOT_GLOBAL_HANDLER__ = globalHandler;
(window as any).__GRIDPILOT_API_LOGGER__ = apiLogger; window.__GRIDPILOT_API_LOGGER__ = apiLogger;
console.log('%c[DEBUG MODE] Enabled', 'color: #00ff88; font-weight: bold; font-size: 14px;'); console.log('%c[DEBUG MODE] Enabled', 'color: #00ff88; font-weight: bold; font-size: 14px;');
console.log('Available globals:', { console.log('Available globals:', {
__GRIDPILOT_GLOBAL_HANDLER__: globalHandler, __GRIDPILOT_GLOBAL_HANDLER__: globalHandler,
__GRIDPILOT_API_LOGGER__: apiLogger, __GRIDPILOT_API_LOGGER__: apiLogger,
__GRIDPILOT_REACT_ERRORS__: (window as any).__GRIDPILOT_REACT_ERRORS__ || [], __GRIDPILOT_REACT_ERRORS__: window.__GRIDPILOT_REACT_ERRORS__ || [],
}); });
}; };

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { useNotifications } from '@/components/notifications/NotificationProvider'; import { useNotifications } from '@/components/notifications/NotificationProvider';
import type { NotificationVariant } from '@/components/notifications/notificationTypes'; import type { NotificationVariant } from '@/components/notifications/notificationTypes';
import { Wrench, ChevronDown, ChevronUp, X, MessageSquare, Activity, AlertTriangle } from 'lucide-react'; import { Wrench, ChevronDown, ChevronUp, X, MessageSquare, Activity, AlertTriangle } from 'lucide-react';

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { Bell } from 'lucide-react'; import { Bell } from 'lucide-react';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { useNotifications } from '@/components/notifications/NotificationProvider'; import { useNotifications } from '@/components/notifications/NotificationProvider';
import type { NotificationVariant } from '@/components/notifications/notificationTypes'; import type { NotificationVariant } from '@/components/notifications/notificationTypes';
import type { DemoNotificationType, DemoUrgency } from '../types'; import type { DemoNotificationType, DemoUrgency } from '../types';

View File

@@ -4,7 +4,7 @@ import { useState, FormEvent } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Input from '../ui/Input'; import Input from '../ui/Input';
import Button from '../ui/Button'; import Button from '../ui/Button';
import { useCreateDriver } from '@/hooks/driver/useCreateDriver'; import { useCreateDriver } from "@/lib/hooks/driver/useCreateDriver";
interface FormErrors { interface FormErrors {
name?: string; name?: string;

View File

@@ -8,7 +8,7 @@ import ProfileStats from './ProfileStats';
import CareerHighlights from './CareerHighlights'; import CareerHighlights from './CareerHighlights';
import DriverRankings from './DriverRankings'; import DriverRankings from './DriverRankings';
import PerformanceMetrics from './PerformanceMetrics'; import PerformanceMetrics from './PerformanceMetrics';
import { useDriverProfile } from '@/hooks/driver/useDriverProfile'; import { useDriverProfile } from "@/lib/hooks/driver/useDriverProfile";
interface DriverProfileProps { interface DriverProfileProps {
driver: DriverViewModel; driver: DriverViewModel;

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useDriverProfile } from '@/hooks/driver'; import { useDriverProfile } from "@/lib/hooks/driver";
import { useMemo } from 'react'; import { useMemo } from 'react';
import Card from '../ui/Card'; import Card from '../ui/Card';
import RankBadge from './RankBadge'; import RankBadge from './RankBadge';

View File

@@ -2,7 +2,7 @@
import Container from '@/components/ui/Container'; import Container from '@/components/ui/Container';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import { useParallax } from '@/hooks/useScrollProgress'; import { useParallax } from "@/lib/hooks/useScrollProgress";
import { useRef } from 'react'; import { useRef } from 'react';
interface AlternatingSectionProps { interface AlternatingSectionProps {

View File

@@ -4,7 +4,7 @@ import { useRef } from 'react';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Container from '@/components/ui/Container'; import Container from '@/components/ui/Container';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import { useParallax } from '../../hooks/useScrollProgress'; import { useParallax } from '@/lib/hooks/useScrollProgress';
const discordUrl = process.env.NEXT_PUBLIC_DISCORD_URL || '#'; const discordUrl = process.env.NEXT_PUBLIC_DISCORD_URL || '#';

View File

@@ -4,7 +4,7 @@ import { useState, FormEvent } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Input from '../ui/Input'; import Input from '../ui/Input';
import Button from '../ui/Button'; import Button from '../ui/Button';
import { useCreateLeague } from '@/hooks/league/useCreateLeague'; import { useCreateLeague } from "@/lib/hooks/league/useCreateLeague";
import { useAuth } from '@/lib/auth/AuthContext'; import { useAuth } from '@/lib/auth/AuthContext';
import { useInject } from '@/lib/di/hooks/useInject'; import { useInject } from '@/lib/di/hooks/useInject';
import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens'; import { DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';

View File

@@ -26,8 +26,8 @@ import { FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { LeagueWizardCommandModel } from '@/lib/command-models/leagues/LeagueWizardCommandModel'; import { LeagueWizardCommandModel } from '@/lib/command-models/leagues/LeagueWizardCommandModel';
import { useCreateLeagueWizard } from '@/hooks/useLeagueWizardService'; import { useCreateLeagueWizard } from "@/lib/hooks/useLeagueWizardService";
import { useLeagueScoringPresets } from '@/hooks/useLeagueScoringPresets'; import { useLeagueScoringPresets } from "@/lib/hooks/useLeagueScoringPresets";
import { LeagueBasicsSection } from './LeagueBasicsSection'; import { LeagueBasicsSection } from './LeagueBasicsSection';
import { LeagueDropSection } from './LeagueDropSection'; import { LeagueDropSection } from './LeagueDropSection';
import { import {
@@ -316,7 +316,7 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
const validateStep = (currentStep: Step): boolean => { const validateStep = (currentStep: Step): boolean => {
// Convert form to LeagueWizardFormData for validation // Convert form to LeagueWizardFormData for validation
const formData: any = { const formData: LeagueWizardCommandModel.LeagueWizardFormData = {
leagueId: form.leagueId || '', leagueId: form.leagueId || '',
basics: { basics: {
name: form.basics?.name || '', name: form.basics?.name || '',
@@ -409,7 +409,7 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
} }
// Convert form to LeagueWizardFormData for validation // Convert form to LeagueWizardFormData for validation
const formData: any = { const formData: LeagueWizardCommandModel.LeagueWizardFormData = {
leagueId: form.leagueId || '', leagueId: form.leagueId || '',
basics: { basics: {
name: form.basics?.name || '', name: form.basics?.name || '',
@@ -520,7 +520,7 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
}; };
}); });
}; };
const steps = [ const steps = [
{ id: 1 as Step, label: 'Basics', icon: FileText, shortLabel: 'Name' }, { id: 1 as Step, label: 'Basics', icon: FileText, shortLabel: 'Name' },
{ id: 2 as Step, label: 'Visibility', icon: Award, shortLabel: 'Type' }, { id: 2 as Step, label: 'Visibility', icon: Award, shortLabel: 'Type' },
@@ -870,8 +870,8 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
{/* Championships & Drop Rules side by side on larger screens */} {/* Championships & Drop Rules side by side on larger screens */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<ChampionshipsSection form={form} onChange={setForm as any} readOnly={presetsLoading} /> <ChampionshipsSection form={form} onChange={setForm} readOnly={presetsLoading} />
<LeagueDropSection form={form} onChange={setForm as any} readOnly={false} /> <LeagueDropSection form={form} onChange={setForm} readOnly={false} />
</div> </div>
{errors.submit && ( {errors.submit && (

View File

@@ -1,9 +1,9 @@
'use client'; 'use client';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { getMembership } from '@/lib/leagueMembership'; import { getMembership } from '@/lib/leagueMembership';
import { useState } from 'react'; import { useState } from 'react';
import { useLeagueMembershipMutation } from '@/hooks/league/useLeagueMembershipMutation'; import { useLeagueMembershipMutation } from "@/lib/hooks/league/useLeagueMembershipMutation";
import Button from '../ui/Button'; import Button from '../ui/Button';
interface JoinLeagueButtonProps { interface JoinLeagueButtonProps {

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { Calendar, Award, UserPlus, UserMinus, Shield, Flag, AlertTriangle } from 'lucide-react'; import { Calendar, Award, UserPlus, UserMinus, Shield, Flag, AlertTriangle } from 'lucide-react';
import { useLeagueRaces } from '@/hooks/league/useLeagueRaces'; import { useLeagueRaces } from "@/lib/hooks/league/useLeagueRaces";
export type LeagueActivity = export type LeagueActivity =
| { type: 'race_completed'; raceId: string; raceName: string; timestamp: Date } | { type: 'race_completed'; raceId: string; raceName: string; timestamp: Date }

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import DriverIdentity from '../drivers/DriverIdentity'; import DriverIdentity from '../drivers/DriverIdentity';
import { useEffectiveDriverId } from '../../hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from '@/lib/hooks/useEffectiveDriverId';
import { useInject } from '@/lib/di/hooks/useInject'; import { useInject } from '@/lib/di/hooks/useInject';
import { LEAGUE_MEMBERSHIP_SERVICE_TOKEN, DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens'; import { LEAGUE_MEMBERSHIP_SERVICE_TOKEN, DRIVER_SERVICE_TOKEN } from '@/lib/di/tokens';
import type { LeagueMembership } from '@/lib/types/LeagueMembership'; import type { LeagueMembership } from '@/lib/types/LeagueMembership';
@@ -45,7 +45,7 @@ export default function LeagueMembers({
const byId: Record<string, DriverViewModel> = {}; const byId: Record<string, DriverViewModel> = {};
for (const dto of driverDtos) { for (const dto of driverDtos) {
byId[dto.id] = new DriverViewModel({ ...dto, avatarUrl: (dto as any).avatarUrl ?? null }); byId[dto.id] = new DriverViewModel({ ...dto, avatarUrl: dto.avatarUrl ?? null });
} }
setDriversById(byId); setDriversById(byId);
} else { } else {

View File

@@ -48,7 +48,7 @@ export default function LeagueOwnershipTransfer({
driver={new DriverViewModel({ driver={new DriverViewModel({
id: ownerSummary.driver.id, id: ownerSummary.driver.id,
name: ownerSummary.driver.name, name: ownerSummary.driver.name,
avatarUrl: (ownerSummary.driver as any).avatarUrl ?? null, avatarUrl: ownerSummary.driver.avatarUrl ?? null,
iracingId: ownerSummary.driver.iracingId, iracingId: ownerSummary.driver.iracingId,
country: ownerSummary.driver.country, country: ownerSummary.driver.country,
bio: ownerSummary.driver.bio, bio: ownerSummary.driver.bio,

View File

@@ -1,8 +1,8 @@
'use client'; 'use client';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { useRegisterForRace } from '@/hooks/race/useRegisterForRace'; import { useRegisterForRace } from "@/lib/hooks/race/useRegisterForRace";
import { useWithdrawFromRace } from '@/hooks/race/useWithdrawFromRace'; import { useWithdrawFromRace } from "@/lib/hooks/race/useWithdrawFromRace";
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import type { LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel'; import type { LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueScheduleViewModel';
@@ -10,7 +10,7 @@ import type { LeagueScheduleRaceViewModel } from '@/lib/view-models/LeagueSchedu
// Shared state components // Shared state components
import { StateContainer } from '@/components/shared/state/StateContainer'; import { StateContainer } from '@/components/shared/state/StateContainer';
import { EmptyState } from '@/components/shared/state/EmptyState'; import { EmptyState } from '@/components/shared/state/EmptyState';
import { useLeagueSchedule } from '@/hooks/league/useLeagueSchedule'; import { useLeagueSchedule } from "@/lib/hooks/league/useLeagueSchedule";
import { Calendar } from 'lucide-react'; import { Calendar } from 'lucide-react';
interface LeagueScheduleProps { interface LeagueScheduleProps {

View File

@@ -6,9 +6,9 @@ import PendingSponsorshipRequests, { type PendingRequestDTO } from '../sponsors/
import Button from '../ui/Button'; import Button from '../ui/Button';
import Input from '../ui/Input'; import Input from '../ui/Input';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { useLeagueSeasons } from '@/hooks/league/useLeagueSeasons'; import { useLeagueSeasons } from "@/lib/hooks/league/useLeagueSeasons";
import { useSponsorshipRequests } from '@/hooks/league/useSponsorshipRequests'; import { useSponsorshipRequests } from "@/lib/hooks/league/useSponsorshipRequests";
import { useInject } from '@/lib/di/hooks/useInject'; import { useInject } from '@/lib/di/hooks/useInject';
import { SPONSORSHIP_SERVICE_TOKEN } from '@/lib/di/tokens'; import { SPONSORSHIP_SERVICE_TOKEN } from '@/lib/di/tokens';

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { getMembership } from '@/lib/leagueMembership'; import { getMembership } from '@/lib/leagueMembership';
import type { MembershipRole } from '@/lib/types/MembershipRole'; import type { MembershipRole } from '@/lib/types/MembershipRole';

View File

@@ -3,7 +3,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import { usePenaltyMutation } from '@/hooks/league/usePenaltyMutation'; import { usePenaltyMutation } from "@/lib/hooks/league/usePenaltyMutation";
import { AlertTriangle, Clock, Flag, Zap } from 'lucide-react'; import { AlertTriangle, Clock, Flag, Zap } from 'lucide-react';
interface DriverOption { interface DriverOption {
@@ -52,16 +52,14 @@ export default function QuickPenaltyModal({ raceId, drivers, onClose, preSelecte
setError(null); setError(null);
try { try {
const command: any = { const command = {
raceId: selectedRaceId, raceId: selectedRaceId,
driverId: selectedDriver, driverId: selectedDriver,
adminId, stewardId: adminId,
infractionType: infractionType as any, type: infractionType,
severity: severity as any, reason: severity,
notes: notes.trim() || undefined,
}; };
if (notes.trim()) {
command.notes = notes.trim();
}
await penaltyMutation.mutateAsync(command); await penaltyMutation.mutateAsync(command);

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { usePenaltyTypesReference } from "@/hooks/usePenaltyTypesReference"; import { usePenaltyTypesReference } from "@/lib/hooks/usePenaltyTypesReference";
import type { PenaltyValueKindDTO } from "@/lib/types/PenaltyTypesReferenceDTO"; import type { PenaltyValueKindDTO } from "@/lib/types/PenaltyTypesReferenceDTO";
import { ProtestViewModel } from "../../lib/view-models/ProtestViewModel"; import { ProtestViewModel } from "../../lib/view-models/ProtestViewModel";
import Modal from "../ui/Modal"; import Modal from "../ui/Modal";

View File

@@ -4,7 +4,7 @@ import { useState } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Button from '../ui/Button'; import Button from '../ui/Button';
import Input from '../ui/Input'; import Input from '../ui/Input';
import { useAllLeagues } from '@/hooks/league/useAllLeagues'; import { useAllLeagues } from "@/lib/hooks/league/useAllLeagues";
import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel'; import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
interface ScheduleRaceFormData { interface ScheduleRaceFormData {

View File

@@ -23,9 +23,9 @@ import Input from '@/components/ui/Input';
import Heading from '@/components/ui/Heading'; import Heading from '@/components/ui/Heading';
import CountrySelect from '@/components/ui/CountrySelect'; import CountrySelect from '@/components/ui/CountrySelect';
import { useAuth } from '@/lib/auth/AuthContext'; import { useAuth } from '@/lib/auth/AuthContext';
import { useCompleteOnboarding } from '@/hooks/onboarding/useCompleteOnboarding'; import { useCompleteOnboarding } from "@/lib/hooks/onboarding/useCompleteOnboarding";
import { useGenerateAvatars } from '@/hooks/onboarding/useGenerateAvatars'; import { useGenerateAvatars } from "@/lib/hooks/onboarding/useGenerateAvatars";
import { useValidateFacePhoto } from '@/hooks/onboarding/useValidateFacePhoto'; import { useValidateFacePhoto } from "@/lib/hooks/onboarding/useValidateFacePhoto";
// ============================================================================ // ============================================================================
// TYPES // TYPES

View File

@@ -6,12 +6,11 @@ import { BarChart3, Building2, ChevronDown, CreditCard, Handshake, LogOut, Megap
import Link from 'next/link'; import Link from 'next/link';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import DriverSummaryPill from '@/components/profile/DriverSummaryPill';
import { CapabilityGate } from '@/components/shared/CapabilityGate'; import { CapabilityGate } from '@/components/shared/CapabilityGate';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from '@/lib/hooks/useEffectiveDriverId';
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel'; import type { DriverViewModel } from '@/lib/view-models/view-models/DriverViewModel';
import { DriverViewModel as DriverViewModelClass } from '@/lib/view-models/DriverViewModel'; import { DriverViewModel as DriverViewModelClass } from '@/lib/view-models/view-models/DriverViewModel';
import { useFindDriverById } from '@/hooks/driver/useFindDriverById'; import { useFindDriverById } from '@/lib/hooks/driver/useFindDriverById';
// Hook to detect demo user mode based on session // Hook to detect demo user mode based on session
function useDemoUserMode(): { isDemo: boolean; demoRole: string | null } { function useDemoUserMode(): { isDemo: boolean; demoRole: string | null } {
@@ -27,8 +26,8 @@ function useDemoUserMode(): { isDemo: boolean; demoRole: string | null } {
const email = session.user.email?.toLowerCase() || ''; const email = session.user.email?.toLowerCase() || '';
const displayName = session.user.displayName?.toLowerCase() || ''; const displayName = session.user.displayName?.toLowerCase() || '';
const primaryDriverId = (session.user as any).primaryDriverId || ''; const primaryDriverId = session.user.primaryDriverId || '';
const role = (session.user as any).role; const role = 'role' in session.user ? (session.user as { role?: string }).role : undefined;
// Check if this is a demo user // Check if this is a demo user
if (email.includes('demo') || if (email.includes('demo') ||
@@ -151,7 +150,7 @@ export default function UserPill() {
// Transform DTO to ViewModel // Transform DTO to ViewModel
const driver = useMemo(() => { const driver = useMemo(() => {
if (!driverDto) return null; if (!driverDto) return null;
return new DriverViewModelClass({ ...driverDto, avatarUrl: (driverDto as any).avatarUrl ?? null }); return new DriverViewModelClass({ ...driverDto, avatarUrl: driverDto.avatarUrl ?? null });
}, [driverDto]); }, [driverDto]);
const data = useMemo(() => { const data = useMemo(() => {

View File

@@ -4,7 +4,7 @@ import { useState } from 'react';
import Modal from '@/components/ui/Modal'; import Modal from '@/components/ui/Modal';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import type { FileProtestCommandDTO } from '@/lib/types/generated/FileProtestCommandDTO'; import type { FileProtestCommandDTO } from '@/lib/types/generated/FileProtestCommandDTO';
import { useFileProtest } from '@/hooks/race/useFileProtest'; import { useFileProtest } from "@/lib/hooks/race/useFileProtest";
import { import {
AlertTriangle, AlertTriangle,
Video, Video,

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { useCapability } from '@/hooks/useCapability'; import { useCapability } from "@/lib/hooks/useCapability";
import { useInject } from '@/lib/di/hooks/useInject'; import { useInject } from '@/lib/di/hooks/useInject';
import { POLICY_SERVICE_TOKEN } from '@/lib/di/tokens'; import { POLICY_SERVICE_TOKEN } from '@/lib/di/tokens';

View File

@@ -456,7 +456,7 @@ export function useSponsorMode(): boolean {
} }
// Check session.user.role for sponsor // Check session.user.role for sponsor
const role = (session.user as any).role; const role = session.user?.role;
if (role === 'sponsor') { if (role === 'sponsor') {
setIsSponsor(true); setIsSponsor(true);
return; return;

View File

@@ -2,8 +2,8 @@
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { useCreateTeam } from '@/hooks/team'; import { useCreateTeam } from "@/lib/hooks/team";
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';

View File

@@ -1,6 +1,5 @@
import Image from 'next/image'; import Image from 'next/image';
import { UserPlus, Users, Trophy } from 'lucide-react'; import { UserPlus, Users, Trophy } from 'lucide-react';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { getMediaUrl } from '@/lib/utilities/media'; import { getMediaUrl } from '@/lib/utilities/media';
const SKILL_LEVELS: { const SKILL_LEVELS: {
@@ -14,7 +13,7 @@ const SKILL_LEVELS: {
{ {
id: 'pro', id: 'pro',
label: 'Pro', label: 'Pro',
icon: () => null, // We'll import Crown if needed icon: () => null,
color: 'text-yellow-400', color: 'text-yellow-400',
bgColor: 'bg-yellow-400/10', bgColor: 'bg-yellow-400/10',
borderColor: 'border-yellow-400/30', borderColor: 'border-yellow-400/30',
@@ -46,7 +45,17 @@ const SKILL_LEVELS: {
]; ];
interface FeaturedRecruitingProps { interface FeaturedRecruitingProps {
teams: TeamSummaryViewModel[]; teams: Array<{
id: string;
name: string;
description?: string;
logoUrl?: string;
category?: string;
memberCount: number;
totalWins: number;
performanceLevel: string;
isRecruiting: boolean;
}>;
onTeamClick: (id: string) => void; onTeamClick: (id: string) => void;
} }

View File

@@ -1,8 +1,8 @@
'use client'; 'use client';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useEffectiveDriverId } from "@/lib/hooks/useEffectiveDriverId";
import { useTeamMembership, useJoinTeam, useLeaveTeam } from '@/hooks/team'; import { useTeamMembership, useJoinTeam, useLeaveTeam } from "@/lib/hooks/team";
import { useState } from 'react'; import { useState } from 'react';
interface JoinTeamButtonProps { interface JoinTeamButtonProps {

View File

@@ -1,6 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { ChevronRight, Users, Trophy, UserPlus } from 'lucide-react'; import { ChevronRight, Users, Trophy, UserPlus } from 'lucide-react';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import TeamCard from './TeamCard'; import TeamCard from './TeamCard';
type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner'; type SkillLevel = 'pro' | 'advanced' | 'intermediate' | 'beginner';
@@ -18,7 +17,22 @@ interface SkillLevelConfig {
interface SkillLevelSectionProps { interface SkillLevelSectionProps {
level: SkillLevelConfig; level: SkillLevelConfig;
teams: TeamSummaryViewModel[]; teams: Array<{
id: string;
name: string;
description?: string;
logoUrl?: string;
memberCount: number;
rating?: number;
totalWins: number;
totalRaces: number;
performanceLevel: string;
isRecruiting: boolean;
specialization?: string;
region?: string;
languages: string[];
category?: string;
}>;
onTeamClick: (id: string) => void; onTeamClick: (id: string) => void;
defaultExpanded?: boolean; defaultExpanded?: boolean;
} }

View File

@@ -4,12 +4,17 @@ import { useState } from 'react';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import Input from '@/components/ui/Input'; import Input from '@/components/ui/Input';
import { useTeamJoinRequests, useUpdateTeam, useApproveJoinRequest, useRejectJoinRequest } from '@/hooks/team'; import { useTeamJoinRequests, useUpdateTeam, useApproveJoinRequest, useRejectJoinRequest } from "@/lib/hooks/team";
import type { TeamJoinRequestViewModel } from '@/lib/view-models/TeamJoinRequestViewModel'; import type { TeamJoinRequestViewModel } from '@/lib/view-models/TeamJoinRequestViewModel';
import type { TeamDetailsViewModel } from '@/lib/view-models/TeamDetailsViewModel';
interface TeamAdminProps { interface TeamAdminProps {
team: Pick<TeamDetailsViewModel, 'id' | 'name' | 'tag' | 'description' | 'ownerId'>; team: {
id: string;
name: string;
tag: string;
description?: string;
ownerId: string;
};
onUpdate: () => void; onUpdate: () => void;
} }
@@ -18,7 +23,7 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
const [editedTeam, setEditedTeam] = useState({ const [editedTeam, setEditedTeam] = useState({
name: team.name, name: team.name,
tag: team.tag, tag: team.tag,
description: team.description, description: team.description || '',
}); });
// Use hooks for data fetching // Use hooks for data fetching
@@ -141,7 +146,7 @@ export default function TeamAdmin({ team, onUpdate }: TeamAdminProps) {
setEditedTeam({ setEditedTeam({
name: team.name, name: team.name,
tag: team.tag, tag: team.tag,
description: team.description, description: team.description || '',
}); });
}} }}
> >

View File

@@ -2,7 +2,6 @@ import { useRouter } from 'next/navigation';
import Image from 'next/image'; import Image from 'next/image';
import { Award, ChevronRight, Crown, Trophy, Users } from 'lucide-react'; import { Award, ChevronRight, Crown, Trophy, Users } from 'lucide-react';
import Button from '@/components/ui/Button'; import Button from '@/components/ui/Button';
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
import { getMediaUrl } from '@/lib/utilities/media'; import { getMediaUrl } from '@/lib/utilities/media';
const SKILL_LEVELS: { const SKILL_LEVELS: {
@@ -48,7 +47,17 @@ const SKILL_LEVELS: {
]; ];
interface TeamLeaderboardPreviewProps { interface TeamLeaderboardPreviewProps {
topTeams: TeamSummaryViewModel[]; topTeams: Array<{
id: string;
name: string;
logoUrl?: string;
category?: string;
memberCount: number;
totalWins: number;
isRecruiting: boolean;
rating?: number;
performanceLevel: string;
}>;
onTeamClick: (id: string) => void; onTeamClick: (id: string) => void;
} }

View File

@@ -2,7 +2,7 @@
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import DriverIdentity from '@/components/drivers/DriverIdentity'; import DriverIdentity from '@/components/drivers/DriverIdentity';
import { useTeamRoster } from '@/hooks/team'; import { useTeamRoster } from "@/lib/hooks/team";
import { useState } from 'react'; import { useState } from 'react';
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel'; import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
@@ -11,7 +11,14 @@ type TeamMemberRole = 'owner' | 'manager' | 'member';
interface TeamRosterProps { interface TeamRosterProps {
teamId: string; teamId: string;
memberships: any[]; memberships: Array<{
driverId: string;
driverName: string;
role: 'owner' | 'manager' | 'member';
joinedAt: string;
isActive: boolean;
avatarUrl: string;
}>;
isAdmin: boolean; isAdmin: boolean;
onRemoveMember?: (driverId: string) => void; onRemoveMember?: (driverId: string) => void;
onChangeRole?: (driverId: string, newRole: TeamRole) => void; onChangeRole?: (driverId: string, newRole: TeamRole) => void;

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import Card from '@/components/ui/Card'; import Card from '@/components/ui/Card';
import { useTeamStandings } from '@/hooks/team'; import { useTeamStandings } from "@/lib/hooks/team";
interface TeamStandingsProps { interface TeamStandingsProps {
teamId: string; teamId: string;

View File

@@ -1,19 +0,0 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useInject } from '@/lib/di/hooks/useInject';
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
export function useCreateLeague() {
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
const queryClient = useQueryClient();
const createLeagueMutation = useMutation({
mutationFn: (input: any) => leagueService.createLeague(input),
onSuccess: () => {
// Invalidate relevant queries to refresh data
queryClient.invalidateQueries({ queryKey: ['allLeagues'] });
queryClient.invalidateQueries({ queryKey: ['leagueMemberships'] });
},
});
return createLeagueMutation;
}

View File

@@ -1,16 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { useInject } from '@/lib/di/hooks/useInject';
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
export function useLeagueDetail(leagueId: string, currentDriverId: string) {
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
const queryResult = useQuery({
queryKey: ['leagueDetail', leagueId, currentDriverId],
queryFn: () => leagueService.getLeagueDetail(leagueId, currentDriverId),
enabled: !!leagueId && !!currentDriverId,
});
return enhanceQueryResult(queryResult);
}

View File

@@ -1,82 +0,0 @@
import { useQuery, useMutation, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
import { useInject } from '@/lib/di/hooks/useInject';
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
import { ApiError } from '@/lib/api/base/ApiError';
import type { LeagueAdminRosterJoinRequestViewModel } from '@/lib/view-models/LeagueAdminRosterJoinRequestViewModel';
import type { LeagueAdminRosterMemberViewModel } from '@/lib/view-models/LeagueAdminRosterMemberViewModel';
import type { MembershipRole } from '@/lib/types/MembershipRole';
export function useLeagueRosterJoinRequests(
leagueId: string,
options?: Omit<UseQueryOptions<LeagueAdminRosterJoinRequestViewModel[], ApiError>, 'queryKey' | 'queryFn'>
) {
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
const queryResult = useQuery({
queryKey: ['leagueRosterJoinRequests', leagueId],
queryFn: () => leagueService.getAdminRosterJoinRequests(leagueId),
...options,
});
return enhanceQueryResult(queryResult);
}
export function useLeagueRosterMembers(
leagueId: string,
options?: Omit<UseQueryOptions<LeagueAdminRosterMemberViewModel[], ApiError>, 'queryKey' | 'queryFn'>
) {
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
const queryResult = useQuery({
queryKey: ['leagueRosterMembers', leagueId],
queryFn: () => leagueService.getAdminRosterMembers(leagueId),
...options,
});
return enhanceQueryResult(queryResult);
}
export function useApproveJoinRequest(
options?: Omit<UseMutationOptions<{ success: boolean }, ApiError, { leagueId: string; joinRequestId: string }>, 'mutationFn'>
) {
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
return useMutation<{ success: boolean }, ApiError, { leagueId: string; joinRequestId: string }>({
mutationFn: ({ leagueId, joinRequestId }) => leagueService.approveJoinRequest(leagueId, joinRequestId),
...options,
});
}
export function useRejectJoinRequest(
options?: Omit<UseMutationOptions<{ success: boolean }, ApiError, { leagueId: string; joinRequestId: string }>, 'mutationFn'>
) {
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
return useMutation<{ success: boolean }, ApiError, { leagueId: string; joinRequestId: string }>({
mutationFn: ({ leagueId, joinRequestId }) => leagueService.rejectJoinRequest(leagueId, joinRequestId),
...options,
});
}
export function useUpdateMemberRole(
options?: Omit<UseMutationOptions<{ success: boolean }, ApiError, { leagueId: string; driverId: string; role: MembershipRole }>, 'mutationFn'>
) {
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
return useMutation<{ success: boolean }, ApiError, { leagueId: string; driverId: string; role: MembershipRole }>({
mutationFn: ({ leagueId, driverId, role }) => leagueService.updateMemberRole(leagueId, driverId, role),
...options,
});
}
export function useRemoveMember(
options?: Omit<UseMutationOptions<{ success: boolean }, ApiError, { leagueId: string; driverId: string }>, 'mutationFn'>
) {
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
return useMutation<{ success: boolean }, ApiError, { leagueId: string; driverId: string }>({
mutationFn: ({ leagueId, driverId }) => leagueService.removeMember(leagueId, driverId),
...options,
});
}

View File

@@ -1,16 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { useInject } from '@/lib/di/hooks/useInject';
import { LEAGUE_SERVICE_TOKEN } from '@/lib/di/tokens';
import { enhanceQueryResult } from '@/lib/di/hooks/useReactQueryWithApiError';
export function useLeagueSchedule(leagueId: string) {
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
const queryResult = useQuery({
queryKey: ['leagueSchedule', leagueId],
queryFn: () => leagueService.getLeagueSchedule(leagueId),
enabled: !!leagueId,
});
return enhanceQueryResult(queryResult);
}

View File

@@ -1,38 +0,0 @@
import { usePageData } from '@/lib/page/usePageData';
import { useInject } from '@/lib/di/hooks/useInject';
import { LEAGUE_SERVICE_TOKEN, LEAGUE_MEMBERSHIP_SERVICE_TOKEN } from '@/lib/di/tokens';
import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility';
export function useLeagueAdminStatus(leagueId: string, currentDriverId: string) {
const leagueMembershipService = useInject(LEAGUE_MEMBERSHIP_SERVICE_TOKEN);
return usePageData({
queryKey: ['admin-check', leagueId, currentDriverId],
queryFn: async () => {
await leagueMembershipService.fetchLeagueMemberships(leagueId);
const membership = leagueMembershipService.getMembership(leagueId, currentDriverId);
return membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false;
},
enabled: !!leagueId && !!currentDriverId,
});
}
export function useLeagueSeasons(leagueId: string, isAdmin: boolean) {
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
return usePageData({
queryKey: ['leagueSeasons', leagueId],
queryFn: () => leagueService.getLeagueSeasonSummaries(leagueId),
enabled: !!leagueId && !!isAdmin,
});
}
export function useLeagueAdminSchedule(leagueId: string, selectedSeasonId: string, isAdmin: boolean) {
const leagueService = useInject(LEAGUE_SERVICE_TOKEN);
return usePageData({
queryKey: ['adminSchedule', leagueId, selectedSeasonId],
queryFn: () => leagueService.getAdminSchedule(leagueId, selectedSeasonId),
enabled: !!leagueId && !!selectedSeasonId && !!isAdmin,
});
}

View File

@@ -1,47 +0,0 @@
import { usePageData, usePageMutation } from '@/lib/page/usePageData';
import { useInject } from '@/lib/di/hooks/useInject';
import { LEAGUE_WALLET_SERVICE_TOKEN } from '@/lib/di/tokens';
export function useLeagueWalletPageData(leagueId: string) {
const leagueWalletService = useInject(LEAGUE_WALLET_SERVICE_TOKEN);
const queryResult = usePageData({
queryKey: ['leagueWallet', leagueId],
queryFn: () => leagueWalletService.getWalletForLeague(leagueId),
enabled: !!leagueId,
});
return queryResult;
}
export function useLeagueWalletWithdrawal(leagueId: string, data: any, refetch: () => void) {
const leagueWalletService = useInject(LEAGUE_WALLET_SERVICE_TOKEN);
const withdrawMutation = usePageMutation(
async ({ amount }: { amount: number }) => {
if (!data) throw new Error('Wallet data not available');
const result = await leagueWalletService.withdraw(
leagueId,
amount,
data.currency,
'season-2', // Current active season
'bank-account-***1234'
);
if (!result.success) {
throw new Error(result.message || 'Withdrawal failed');
}
return result;
},
{
onSuccess: () => {
// Refetch wallet data after successful withdrawal
refetch();
},
}
);
return withdrawMutation;
}

View File

@@ -101,7 +101,7 @@ export const responseCache = new ResponseCache();
export async function withGracefulDegradation<T>( export async function withGracefulDegradation<T>(
fn: () => Promise<T>, fn: () => Promise<T>,
options: DegradationOptions<T> = {} options: DegradationOptions<T> = {}
): Promise<T> { ): Promise<T | undefined> {
const { const {
fallback, fallback,
throwOnError = false, throwOnError = false,
@@ -139,7 +139,7 @@ export async function withGracefulDegradation<T>(
} }
// Return undefined (caller must handle) // Return undefined (caller must handle)
return undefined as unknown as T; return undefined;
} }
// API is available, try to execute // API is available, try to execute
@@ -193,7 +193,7 @@ export async function withGracefulDegradation<T>(
throw error; throw error;
} }
return undefined as unknown as T; return undefined;
} }
} }

View File

@@ -12,9 +12,9 @@ import {
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import type { SessionViewModel } from '@/lib/view-models/SessionViewModel'; import type { SessionViewModel } from '@/lib/view-models/SessionViewModel';
import { useCurrentSession } from '@/hooks/auth/useCurrentSession'; import { useCurrentSession } from "@/lib/hooks/auth/useCurrentSession";
import { useLogin } from '@/hooks/auth/useLogin'; import { useLogin } from "@/lib/hooks/auth/useLogin";
import { useLogout } from '@/hooks/auth/useLogout'; import { useLogout } from "@/lib/hooks/auth/useLogout";
export type AuthContextValue = { export type AuthContextValue = {
session: SessionViewModel | null; session: SessionViewModel | null;

View File

@@ -0,0 +1,26 @@
import type { PageQueryResult } from '@/lib/page-queries/PageQueryResult';
/**
* PageQuery contract interface
*
* Defines the canonical contract for all server-side page queries.
*
* Based on WEBSITE_PAGE_QUERIES.md:
* - Server-side composition classes
* - Call services that call apps/api
* - Assemble a Page DTO
* - Return explicit result describing route outcome
* - Do not implement business rules
*
* @template TPageDto - The Page DTO type this query produces
* @template TParams - The parameters required to execute this query
*/
export interface PageQuery<TPageDto, TParams = void> {
/**
* Execute the page query
*
* @param params - Parameters required for query execution
* @returns Promise resolving to a PageQueryResult discriminated union
*/
execute(params: TParams): Promise<PageQueryResult<TPageDto>>;
}

View File

@@ -18,12 +18,12 @@ export function useInject<T extends symbol>(token: T): T extends { type: infer U
return useMemo(() => { return useMemo(() => {
try { try {
return container.get(token); return container.get(token) as T extends { type: infer U } ? U : unknown;
} catch (error) { } catch (error) {
console.error(`Failed to resolve token ${token.toString()}:`, error); console.error(`Failed to resolve token ${token.toString()}:`, error);
throw error; throw error;
} }
}, [container, token]) as any; }, [container, token]);
} }
/** /**

Some files were not shown because too many files have changed in this diff Show More