-
-
- {effectiveRangeHint}
-
-
- {showLargeValue && (
-
- {clampedValue}
- {unitLabel}
-
- )}
-
- {/* Custom slider */}
-
- {/* Track background */}
-
-
- {/* Track fill with gradient */}
-
-
- {/* Tick marks */}
-
- {[0, 25, 50, 75, 100].map((tick) => (
-
= tick ? 'bg-white/40' : 'bg-charcoal-outline'
- }`}
- />
- ))}
-
-
- {/* Thumb */}
-
-
-
- {/* Value input and quick presets */}
-
-
-
- {unitLabel}
-
-
- {quickPresets.length > 0 && (
-
- {quickPresets.slice(0, 3).map((preset) => (
-
- ))}
-
- )}
-
-
- {helperText &&
{helperText}
}
- {error &&
{error}
}
-
- );
-}
\ No newline at end of file
diff --git a/apps/website/components/ui/Section.tsx b/apps/website/components/ui/Section.tsx
deleted file mode 100644
index 48bc4460d..000000000
--- a/apps/website/components/ui/Section.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React, { ReactNode } from 'react';
-
-interface SectionProps {
- variant?: 'default' | 'dark' | 'light';
- children: ReactNode;
- className?: string;
- id?: string;
-}
-
-export default function Section({
- variant = 'default',
- children,
- className = '',
- id
-}: SectionProps) {
- const variantStyles = {
- default: 'bg-deep-graphite',
- dark: 'bg-iron-gray',
- light: 'bg-charcoal-outline'
- };
-
- return (
-
- );
-}
\ No newline at end of file
diff --git a/apps/website/components/ui/SectionHeader.tsx b/apps/website/components/ui/SectionHeader.tsx
deleted file mode 100644
index ed789c6f8..000000000
--- a/apps/website/components/ui/SectionHeader.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-'use client';
-
-import React from 'react';
-
-interface SectionHeaderProps {
- icon: React.ElementType;
- title: string;
- description?: string;
- action?: React.ReactNode;
- color?: string;
-}
-
-/**
- * Section header component with icon, title, optional description and action.
- * Used at the top of card sections throughout the app.
- */
-export default function SectionHeader({
- icon: Icon,
- title,
- description,
- action,
- color = 'text-primary-blue'
-}: SectionHeaderProps) {
- return (
-
-
-
-
-
-
- {title}
-
- {description && (
-
{description}
- )}
-
- {action}
-
- );
-}
\ No newline at end of file
diff --git a/apps/website/components/ui/SegmentedControl.tsx b/apps/website/components/ui/SegmentedControl.tsx
deleted file mode 100644
index 63bcff39b..000000000
--- a/apps/website/components/ui/SegmentedControl.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-'use client';
-
-import { ButtonHTMLAttributes } from 'react';
-
-interface SegmentedControlOption {
- value: string;
- label: string;
- description?: string;
- disabled?: boolean;
-}
-
-interface SegmentedControlProps {
- options: SegmentedControlOption[];
- value: string;
- onChange?: (value: string) => void;
-}
-
-export default function SegmentedControl({
- options,
- value,
- onChange,
-}: SegmentedControlProps) {
- const handleSelect = (optionValue: string, optionDisabled?: boolean) => {
- if (!onChange || optionDisabled) return;
- if (optionValue === value) return;
- onChange(optionValue);
- };
-
- return (
-
- {options.map((option) => {
- const isSelected = option.value === value;
- const baseClasses =
- 'flex-1 min-w-[140px] px-3 py-1.5 text-xs font-medium rounded-full transition-colors text-left';
- const selectedClasses = isSelected
- ? 'bg-primary-blue text-white'
- : 'text-gray-300 hover:text-white hover:bg-charcoal-outline/80';
- const disabledClasses = option.disabled
- ? 'opacity-50 cursor-not-allowed hover:bg-transparent hover:text-gray-300'
- : '';
-
- const buttonProps: ButtonHTMLAttributes
= {
- type: 'button',
- onClick: () => handleSelect(option.value, option.disabled),
- 'aria-pressed': isSelected,
- disabled: option.disabled,
- };
-
- return (
-
- );
- })}
-
- );
-}
\ No newline at end of file
diff --git a/apps/website/components/ui/StatCard.tsx b/apps/website/components/ui/StatCard.tsx
deleted file mode 100644
index 7a0f3d669..000000000
--- a/apps/website/components/ui/StatCard.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-'use client';
-
-import React from 'react';
-import Card from './Card';
-
-interface StatCardProps {
- icon: React.ElementType;
- label: string;
- value: string;
- subValue?: string;
- color?: string;
- bgColor?: string;
- trend?: {
- value: number;
- isPositive: boolean;
- };
-}
-
-/**
- * Statistics card component for displaying metrics with icon, label, value, and optional trend.
- * Used in dashboards and overview sections.
- */
-export default function StatCard({
- icon: Icon,
- label,
- value,
- subValue,
- color = 'text-primary-blue',
- bgColor = 'bg-primary-blue/10',
- trend,
-}: StatCardProps) {
- return (
-
-
-
-
-
{value}
- {subValue && (
-
{subValue}
- )}
-
- {trend && (
-
- {trend.isPositive ? '↑' : '↓'}
- {Math.abs(trend.value)}%
-
- )}
-
-
- );
-}
\ No newline at end of file
diff --git a/apps/website/components/ui/StatusBadge.tsx b/apps/website/components/ui/StatusBadge.tsx
deleted file mode 100644
index 50ef38df6..000000000
--- a/apps/website/components/ui/StatusBadge.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-'use client';
-
-import React from 'react';
-
-type StatusType = 'success' | 'warning' | 'error' | 'info' | 'neutral' | 'pending';
-
-interface StatusBadgeProps {
- status: StatusType;
- label: string;
- icon?: React.ElementType;
- size?: 'sm' | 'md';
-}
-
-const statusConfig: Record
= {
- success: {
- color: 'text-performance-green',
- bg: 'bg-performance-green/10',
- border: 'border-performance-green/30',
- },
- warning: {
- color: 'text-warning-amber',
- bg: 'bg-warning-amber/10',
- border: 'border-warning-amber/30',
- },
- error: {
- color: 'text-racing-red',
- bg: 'bg-racing-red/10',
- border: 'border-racing-red/30',
- },
- info: {
- color: 'text-primary-blue',
- bg: 'bg-primary-blue/10',
- border: 'border-primary-blue/30',
- },
- neutral: {
- color: 'text-gray-400',
- bg: 'bg-iron-gray',
- border: 'border-charcoal-outline',
- },
- pending: {
- color: 'text-warning-amber',
- bg: 'bg-warning-amber/10',
- border: 'border-warning-amber/30',
- },
-};
-
-/**
- * Status badge component for displaying status indicators.
- * Used for showing status of items like invoices, sponsorships, etc.
- */
-export default function StatusBadge({
- status,
- label,
- icon: Icon,
- size = 'sm',
-}: StatusBadgeProps) {
- const config = statusConfig[status];
- const sizeClasses = size === 'sm'
- ? 'px-2 py-0.5 text-xs'
- : 'px-3 py-1 text-sm';
-
- return (
-
- {Icon && }
- {label}
-
- );
-}
\ No newline at end of file
diff --git a/apps/website/components/ui/TabContent.tsx b/apps/website/components/ui/TabContent.tsx
deleted file mode 100644
index 8dd6d29c5..000000000
--- a/apps/website/components/ui/TabContent.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react';
-
-interface TabContentProps {
- activeTab: string;
- children: React.ReactNode;
- className?: string;
-}
-
-export default function TabContent({ activeTab, children, className = '' }: TabContentProps) {
- return (
-
- {children}
-
- );
-}
\ No newline at end of file
diff --git a/apps/website/components/ui/TabNavigation.tsx b/apps/website/components/ui/TabNavigation.tsx
deleted file mode 100644
index 9945e6437..000000000
--- a/apps/website/components/ui/TabNavigation.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React from 'react';
-
-interface Tab {
- id: string;
- label: string;
- icon?: React.ComponentType<{ className?: string }>;
-}
-
-interface TabNavigationProps {
- tabs: Tab[];
- activeTab: string;
- onTabChange: (tabId: string) => void;
- className?: string;
-}
-
-export default function TabNavigation({ tabs, activeTab, onTabChange, className = '' }: TabNavigationProps) {
- return (
-
- {tabs.map((tab) => {
- const Icon = tab.icon;
- return (
-
- );
- })}
-
- );
-}
\ No newline at end of file
diff --git a/apps/website/components/ui/Toggle.tsx b/apps/website/components/ui/Toggle.tsx
deleted file mode 100644
index 4a0fb14d9..000000000
--- a/apps/website/components/ui/Toggle.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-'use client';
-
-import React from 'react';
-import { motion, useReducedMotion } from 'framer-motion';
-
-interface ToggleProps {
- checked: boolean;
- onChange: (checked: boolean) => void;
- label: string;
- description?: string;
- disabled?: boolean;
-}
-
-/**
- * Toggle switch component with Framer Motion animation.
- * Used for boolean settings/preferences.
- */
-export default function Toggle({
- checked,
- onChange,
- label,
- description,
- disabled = false,
-}: ToggleProps) {
- const shouldReduceMotion = useReducedMotion();
-
- return (
-
- );
-}
\ No newline at end of file
diff --git a/apps/website/eslint-rules/component-classification.js b/apps/website/eslint-rules/component-classification.js
index 756dcd12d..729e2101a 100644
--- a/apps/website/eslint-rules/component-classification.js
+++ b/apps/website/eslint-rules/component-classification.js
@@ -1,29 +1,31 @@
/**
- * ESLint rule to suggest proper component classification
+ * ESLint rule to suggest proper component classification and enforce UI component usage
*
* Architecture:
* - app/ - Pages and layouts only (no business logic)
* - components/ - App-level components (can be stateful, can use hooks)
* - ui/ - Pure, reusable UI elements (stateless, no hooks)
- * - hooks/ - Shared stateful logic
*
- * This rule provides SUGGESTIONS, not errors, for component placement.
+ * This rule enforces that components use proper UI elements instead of raw HTML and className.
*/
module.exports = {
meta: {
- type: 'suggestion',
+ type: 'problem',
docs: {
- description: 'Suggest proper component classification',
+ description: 'Enforce proper component classification and UI component usage',
category: 'Architecture',
- recommended: false,
+ recommended: true,
},
- fixable: 'code',
+ fixable: null,
schema: [],
messages: {
uiShouldBePure: 'This appears to be a pure UI element. Consider moving to ui/ for maximum reusability.',
componentShouldBeInComponents: 'This component uses state/hooks. Consider moving to components/.',
pureComponentInComponents: 'Pure component in components/. Consider moving to ui/ for better reusability.',
+ noRawHtml: 'Raw HTML tags are forbidden in components and pages. Use UI components from ui/ instead.',
+ noClassName: 'The className property is forbidden in components and pages. Use proper component props for styling.',
+ noStyle: 'The style property is forbidden in components and pages. Use proper component props for styling.',
},
},
@@ -33,10 +35,12 @@ module.exports = {
const isInComponents = filename.includes('/components/');
const isInApp = filename.includes('/app/');
- if (!isInUi && !isInComponents) return {};
+ if (!isInUi && !isInComponents && !isInApp) return {};
return {
Program(node) {
+ if (isInApp) return; // Don't check classification for app/ files
+
const sourceCode = context.getSourceCode();
const text = sourceCode.getText();
@@ -63,18 +67,64 @@ module.exports = {
messageId: 'pureComponentInComponents',
});
}
-
- if (isInComponents && !hasState && !hasEffects && !hasContext) {
- // Check if it's mostly just rendering props
- const hasManyProps = /\{\s*\.\.\.props\s*\}/.test(text) ||
- /\{\s*props\./.test(text);
-
- if (hasManyProps && hasNoLogic) {
- context.report({
- loc: { line: 1, column: 0 },
- messageId: 'uiShouldBePure',
- });
+ },
+
+ JSXOpeningElement(node) {
+ if (isInUi) return; // Allow raw HTML and className in ui/
+
+ let tagName = '';
+ if (node.name.type === 'JSXIdentifier') {
+ tagName = node.name.name;
+ } else if (node.name.type === 'JSXMemberExpression') {
+ tagName = node.name.property.name;
+ }
+
+ if (!tagName) return;
+
+ // 1. Forbid raw HTML tags (lowercase)
+ if (tagName[0] === tagName[0].toLowerCase()) {
+ // Special case for html and body in RootLayout
+ if (isInApp && (tagName === 'html' || tagName === 'body' || tagName === 'head' || tagName === 'meta' || tagName === 'link' || tagName === 'script')) {
+ return;
}
+
+ context.report({
+ node,
+ messageId: 'noRawHtml',
+ });
+ }
+
+ // 2. Forbid className property
+ const classNameAttr = node.attributes.find(
+ attr => attr.type === 'JSXAttribute' &&
+ attr.name.type === 'JSXIdentifier' &&
+ attr.name.name === 'className'
+ );
+
+ if (classNameAttr) {
+ // Special case for html and body in RootLayout
+ if (isInApp && (tagName === 'html' || tagName === 'body')) {
+ return;
+ }
+
+ context.report({
+ node: classNameAttr,
+ messageId: 'noClassName',
+ });
+ }
+
+ // 3. Forbid style property
+ const styleAttr = node.attributes.find(
+ attr => attr.type === 'JSXAttribute' &&
+ attr.name.type === 'JSXIdentifier' &&
+ attr.name.name === 'style'
+ );
+
+ if (styleAttr) {
+ context.report({
+ node: styleAttr,
+ messageId: 'noStyle',
+ });
}
},
};
diff --git a/apps/website/eslint-rules/index.js b/apps/website/eslint-rules/index.js
index 06089dd21..6ba7255ec 100644
--- a/apps/website/eslint-rules/index.js
+++ b/apps/website/eslint-rules/index.js
@@ -73,7 +73,7 @@ module.exports = {
'template-no-external-state': templatePurityRules['no-restricted-imports-in-templates'],
'template-no-global-objects': templatePurityRules['no-invalid-template-signature'],
'template-no-mutation-props': templatePurityRules['no-template-helper-exports'],
- 'template-no-unsafe-html': templatePurityRules['invalid-template-filename'],
+ 'template-no-unsafe-html': require('./template-no-unsafe-html'),
// Display Object Rules
'display-no-domain-models': displayObjectRules['no-io-in-display-objects'],
diff --git a/apps/website/eslint-rules/template-no-unsafe-html.js b/apps/website/eslint-rules/template-no-unsafe-html.js
index 1aec96a22..6c0367048 100644
--- a/apps/website/eslint-rules/template-no-unsafe-html.js
+++ b/apps/website/eslint-rules/template-no-unsafe-html.js
@@ -1,125 +1,80 @@
/**
- * ESLint rule to enforce Presenter contract
+ * ESLint rule to forbid raw HTML and className in templates
*
- * Enforces that classes ending with "Presenter" must:
- * 1. Implement Presenter interface
- * 2. Have a present(input) method
- * 3. Have 'use client' directive
+ * Templates must use proper reusable UI components instead of low level html and css.
+ * To avoid workarounds using `Box` with tailwind classes, the `className` property is forbidden.
*/
module.exports = {
meta: {
type: 'problem',
docs: {
- description: 'Enforce Presenter contract implementation',
- category: 'Best Practices',
+ description: 'Forbid raw HTML and className in templates',
+ category: 'Architecture',
recommended: true,
},
fixable: null,
schema: [],
messages: {
- missingImplements: 'Presenter class must implement Presenter interface',
- missingPresentMethod: 'Presenter class must have present(input) method',
- missingUseClient: 'Presenter must have \'use client\' directive at top-level',
+ noRawHtml: 'Raw HTML tags are forbidden in templates. Use UI components from ui/ instead.',
+ noClassName: 'The className property is forbidden in templates. Use proper component props for styling.',
+ noStyle: 'The style property is forbidden in templates. Use proper component props for styling.',
},
},
create(context) {
- const sourceCode = context.getSourceCode();
- let hasUseClient = false;
- let presenterClassNode = null;
- let hasPresentMethod = false;
- let hasImplements = false;
+ const filename = context.getFilename();
+ const isInTemplates = filename.includes('/templates/');
+
+ if (!isInTemplates) return {};
return {
- // Check for 'use client' directive
- Program(node) {
- // Check comments at the top
- const comments = sourceCode.getAllComments();
- if (comments.length > 0) {
- const firstComment = comments[0];
- if (firstComment.type === 'Line' && firstComment.value.trim() === 'use client') {
- hasUseClient = true;
- } else if (firstComment.type === 'Block' && firstComment.value.includes('use client')) {
- hasUseClient = true;
- }
+ JSXOpeningElement(node) {
+ let tagName = '';
+ if (node.name.type === 'JSXIdentifier') {
+ tagName = node.name.name;
+ } else if (node.name.type === 'JSXMemberExpression') {
+ tagName = node.name.property.name;
}
- // Also check for 'use client' string literal as first statement
- if (node.body.length > 0) {
- const firstStmt = node.body[0];
- if (firstStmt &&
- firstStmt.type === 'ExpressionStatement' &&
- firstStmt.expression.type === 'Literal' &&
- firstStmt.expression.value === 'use client') {
- hasUseClient = true;
- }
- }
- },
+ if (!tagName) return;
- // Find Presenter classes
- ClassDeclaration(node) {
- const className = node.id?.name;
-
- // Check if this is a Presenter class
- if (className && className.endsWith('Presenter')) {
- presenterClassNode = node;
-
- // Check if it implements any interface
- if (node.implements && node.implements.length > 0) {
- for (const impl of node.implements) {
- // Handle GenericTypeAnnotation for Presenter
- if (impl.expression.type === 'TSInstantiationExpression') {
- const expr = impl.expression.expression;
- if (expr.type === 'Identifier' && expr.name === 'Presenter') {
- hasImplements = true;
- }
- } else if (impl.expression.type === 'Identifier') {
- // Handle simple Presenter (without generics)
- if (impl.expression.name === 'Presenter') {
- hasImplements = true;
- }
- }
- }
- }
- }
- },
-
- // Check for present method in classes
- MethodDefinition(node) {
- if (presenterClassNode &&
- node.key.type === 'Identifier' &&
- node.key.name === 'present' &&
- node.parent === presenterClassNode) {
- hasPresentMethod = true;
- }
- },
-
- // Report violations at the end
- 'Program:exit'() {
- if (!presenterClassNode) return;
-
- if (!hasImplements) {
+ // 1. Forbid raw HTML tags (lowercase)
+ if (tagName[0] === tagName[0].toLowerCase()) {
context.report({
- node: presenterClassNode,
- messageId: 'missingImplements',
+ node,
+ messageId: 'noRawHtml',
});
}
- if (!hasPresentMethod) {
+ // 2. Forbid className property
+ const classNameAttr = node.attributes.find(
+ attr => attr.type === 'JSXAttribute' &&
+ attr.name.type === 'JSXIdentifier' &&
+ attr.name.name === 'className'
+ );
+
+ if (classNameAttr) {
context.report({
- node: presenterClassNode,
- messageId: 'missingPresentMethod',
+ node: classNameAttr,
+ messageId: 'noClassName',
});
}
- if (!hasUseClient) {
+ // 3. Forbid style property
+ const styleAttr = node.attributes.find(
+ attr => attr.type === 'JSXAttribute' &&
+ attr.name.type === 'JSXIdentifier' &&
+ attr.name.name === 'style'
+ );
+
+ if (styleAttr) {
context.report({
- node: presenterClassNode,
- messageId: 'missingUseClient',
+ node: styleAttr,
+ messageId: 'noStyle',
});
}
},
};
},
-};
\ No newline at end of file
+};
diff --git a/apps/website/eslint-rules/ui-element-purity.js b/apps/website/eslint-rules/ui-element-purity.js
index e83a1a7dd..ae9eef216 100644
--- a/apps/website/eslint-rules/ui-element-purity.js
+++ b/apps/website/eslint-rules/ui-element-purity.js
@@ -1,22 +1,23 @@
/**
- * ESLint rule to enforce UI element purity
+ * ESLint rule to enforce UI element purity and architectural boundaries
*
* UI elements in ui/ must be:
* - Stateless (no useState, useReducer)
* - No side effects (no useEffect)
* - Pure functions that only render based on props
+ * - Isolated (cannot import from outside the ui/ directory)
*
* Rationale:
* - ui/ is for reusable, pure presentation elements
- * - Stateful logic belongs in components/ or hooks/
- * - This ensures maximum reusability and testability
+ * - Isolation ensures UI elements don't depend on app-specific logic
+ * - Raw HTML and className are allowed in ui/ for implementation
*/
module.exports = {
meta: {
type: 'problem',
docs: {
- description: 'Enforce UI elements are pure and stateless',
+ description: 'Enforce UI elements are pure and isolated',
category: 'Architecture',
recommended: true,
},
@@ -26,6 +27,7 @@ module.exports = {
noStateInUi: 'UI elements in ui/ must be stateless. Use components/ for stateful wrappers.',
noHooksInUi: 'UI elements must not use hooks. Use components/ or hooks/ for stateful logic.',
noSideEffects: 'UI elements must not have side effects. Use components/ for side effect logic.',
+ noExternalImports: 'UI elements in ui/ cannot import from outside the ui/ directory. Only npm packages and other UI elements are allowed.',
},
},
@@ -36,6 +38,38 @@ module.exports = {
if (!isInUi) return {};
return {
+ // Check for imports from outside ui/
+ ImportDeclaration(node) {
+ const importPath = node.source.value;
+
+ // Allow npm packages (don't start with . or @/)
+ if (!importPath.startsWith('.') && !importPath.startsWith('@/')) {
+ return;
+ }
+
+ // Check internal imports
+ const isInternalUiImport = importPath.startsWith('@/ui/') ||
+ (importPath.startsWith('.') && !importPath.includes('..'));
+
+ // Special case for relative imports that stay within ui/
+ let staysInUi = false;
+ if (importPath.startsWith('.')) {
+ const path = require('path');
+ const absoluteImportPath = path.resolve(path.dirname(filename), importPath);
+ const uiDir = filename.split('/ui/')[0] + '/ui';
+ if (absoluteImportPath.startsWith(uiDir)) {
+ staysInUi = true;
+ }
+ }
+
+ if (!isInternalUiImport && !staysInUi) {
+ context.report({
+ node,
+ messageId: 'noExternalImports',
+ });
+ }
+ },
+
// Check for 'use client' directive
ExpressionStatement(node) {
if (node.expression.type === 'Literal' &&
@@ -77,31 +111,6 @@ module.exports = {
});
}
},
-
- // Check for class components with state
- ClassDeclaration(node) {
- if (node.superClass &&
- node.superClass.type === 'Identifier' &&
- node.superClass.name === 'Component') {
- context.report({
- node,
- messageId: 'noStateInUi',
- });
- }
- },
-
- // Check for direct state assignment (rare but possible)
- AssignmentExpression(node) {
- if (node.left.type === 'MemberExpression' &&
- node.left.property.type === 'Identifier' &&
- (node.left.property.name === 'state' ||
- node.left.property.name === 'setState')) {
- context.report({
- node,
- messageId: 'noStateInUi',
- });
- }
- },
};
},
};
diff --git a/apps/website/lib/hooks/auth/index.ts b/apps/website/hooks/auth/index.ts
similarity index 100%
rename from apps/website/lib/hooks/auth/index.ts
rename to apps/website/hooks/auth/index.ts
diff --git a/apps/website/lib/hooks/auth/useCurrentSession.ts b/apps/website/hooks/auth/useCurrentSession.ts
similarity index 100%
rename from apps/website/lib/hooks/auth/useCurrentSession.ts
rename to apps/website/hooks/auth/useCurrentSession.ts
diff --git a/apps/website/lib/hooks/auth/useForgotPassword.ts b/apps/website/hooks/auth/useForgotPassword.ts
similarity index 100%
rename from apps/website/lib/hooks/auth/useForgotPassword.ts
rename to apps/website/hooks/auth/useForgotPassword.ts
diff --git a/apps/website/lib/hooks/auth/useLogin.ts b/apps/website/hooks/auth/useLogin.ts
similarity index 100%
rename from apps/website/lib/hooks/auth/useLogin.ts
rename to apps/website/hooks/auth/useLogin.ts
diff --git a/apps/website/lib/hooks/auth/useLogout.ts b/apps/website/hooks/auth/useLogout.ts
similarity index 100%
rename from apps/website/lib/hooks/auth/useLogout.ts
rename to apps/website/hooks/auth/useLogout.ts
diff --git a/apps/website/lib/hooks/auth/useResetPassword.ts b/apps/website/hooks/auth/useResetPassword.ts
similarity index 100%
rename from apps/website/lib/hooks/auth/useResetPassword.ts
rename to apps/website/hooks/auth/useResetPassword.ts
diff --git a/apps/website/lib/hooks/auth/useSignup.ts b/apps/website/hooks/auth/useSignup.ts
similarity index 100%
rename from apps/website/lib/hooks/auth/useSignup.ts
rename to apps/website/hooks/auth/useSignup.ts
diff --git a/apps/website/lib/hooks/driver/index.ts b/apps/website/hooks/driver/index.ts
similarity index 100%
rename from apps/website/lib/hooks/driver/index.ts
rename to apps/website/hooks/driver/index.ts
diff --git a/apps/website/lib/hooks/driver/useCreateDriver.ts b/apps/website/hooks/driver/useCreateDriver.ts
similarity index 100%
rename from apps/website/lib/hooks/driver/useCreateDriver.ts
rename to apps/website/hooks/driver/useCreateDriver.ts
diff --git a/apps/website/lib/hooks/driver/useCurrentDriver.ts b/apps/website/hooks/driver/useCurrentDriver.ts
similarity index 100%
rename from apps/website/lib/hooks/driver/useCurrentDriver.ts
rename to apps/website/hooks/driver/useCurrentDriver.ts
diff --git a/apps/website/lib/hooks/driver/useDriverProfile.ts b/apps/website/hooks/driver/useDriverProfile.ts
similarity index 100%
rename from apps/website/lib/hooks/driver/useDriverProfile.ts
rename to apps/website/hooks/driver/useDriverProfile.ts
diff --git a/apps/website/lib/hooks/driver/useDriverProfilePageData.ts b/apps/website/hooks/driver/useDriverProfilePageData.ts
similarity index 100%
rename from apps/website/lib/hooks/driver/useDriverProfilePageData.ts
rename to apps/website/hooks/driver/useDriverProfilePageData.ts
diff --git a/apps/website/lib/hooks/driver/useFindDriverById.ts b/apps/website/hooks/driver/useFindDriverById.ts
similarity index 100%
rename from apps/website/lib/hooks/driver/useFindDriverById.ts
rename to apps/website/hooks/driver/useFindDriverById.ts
diff --git a/apps/website/lib/hooks/driver/useUpdateDriverProfile.ts b/apps/website/hooks/driver/useUpdateDriverProfile.ts
similarity index 100%
rename from apps/website/lib/hooks/driver/useUpdateDriverProfile.ts
rename to apps/website/hooks/driver/useUpdateDriverProfile.ts
diff --git a/apps/website/lib/hooks/league/useAllLeagues.ts b/apps/website/hooks/league/useAllLeagues.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useAllLeagues.ts
rename to apps/website/hooks/league/useAllLeagues.ts
diff --git a/apps/website/lib/hooks/league/useCreateLeague.ts b/apps/website/hooks/league/useCreateLeague.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useCreateLeague.ts
rename to apps/website/hooks/league/useCreateLeague.ts
diff --git a/apps/website/lib/hooks/league/useCreateLeagueWithBlockers.ts b/apps/website/hooks/league/useCreateLeagueWithBlockers.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useCreateLeagueWithBlockers.ts
rename to apps/website/hooks/league/useCreateLeagueWithBlockers.ts
diff --git a/apps/website/lib/hooks/league/useLeagueAdminStatus.ts b/apps/website/hooks/league/useLeagueAdminStatus.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useLeagueAdminStatus.ts
rename to apps/website/hooks/league/useLeagueAdminStatus.ts
diff --git a/apps/website/lib/hooks/league/useLeagueDetail.ts b/apps/website/hooks/league/useLeagueDetail.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useLeagueDetail.ts
rename to apps/website/hooks/league/useLeagueDetail.ts
diff --git a/apps/website/lib/hooks/league/useLeagueMembershipMutation.ts b/apps/website/hooks/league/useLeagueMembershipMutation.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useLeagueMembershipMutation.ts
rename to apps/website/hooks/league/useLeagueMembershipMutation.ts
diff --git a/apps/website/lib/hooks/league/useLeagueMemberships.ts b/apps/website/hooks/league/useLeagueMemberships.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useLeagueMemberships.ts
rename to apps/website/hooks/league/useLeagueMemberships.ts
diff --git a/apps/website/lib/hooks/league/useLeagueRaces.ts b/apps/website/hooks/league/useLeagueRaces.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useLeagueRaces.ts
rename to apps/website/hooks/league/useLeagueRaces.ts
diff --git a/apps/website/lib/hooks/league/useLeagueRosterAdmin.ts b/apps/website/hooks/league/useLeagueRosterAdmin.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useLeagueRosterAdmin.ts
rename to apps/website/hooks/league/useLeagueRosterAdmin.ts
diff --git a/apps/website/lib/hooks/league/useLeagueSchedule.ts b/apps/website/hooks/league/useLeagueSchedule.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useLeagueSchedule.ts
rename to apps/website/hooks/league/useLeagueSchedule.ts
diff --git a/apps/website/lib/hooks/league/useLeagueScheduleAdminPageData.ts b/apps/website/hooks/league/useLeagueScheduleAdminPageData.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useLeagueScheduleAdminPageData.ts
rename to apps/website/hooks/league/useLeagueScheduleAdminPageData.ts
diff --git a/apps/website/lib/hooks/league/useLeagueSeasons.ts b/apps/website/hooks/league/useLeagueSeasons.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useLeagueSeasons.ts
rename to apps/website/hooks/league/useLeagueSeasons.ts
diff --git a/apps/website/lib/hooks/league/useLeagueSettings.ts b/apps/website/hooks/league/useLeagueSettings.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useLeagueSettings.ts
rename to apps/website/hooks/league/useLeagueSettings.ts
diff --git a/apps/website/lib/hooks/league/useLeagueSponsorshipsPageData.ts b/apps/website/hooks/league/useLeagueSponsorshipsPageData.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useLeagueSponsorshipsPageData.ts
rename to apps/website/hooks/league/useLeagueSponsorshipsPageData.ts
diff --git a/apps/website/lib/hooks/league/useLeagueStewardingData.ts b/apps/website/hooks/league/useLeagueStewardingData.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useLeagueStewardingData.ts
rename to apps/website/hooks/league/useLeagueStewardingData.ts
diff --git a/apps/website/lib/hooks/league/useLeagueStewardingMutations.ts b/apps/website/hooks/league/useLeagueStewardingMutations.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useLeagueStewardingMutations.ts
rename to apps/website/hooks/league/useLeagueStewardingMutations.ts
diff --git a/apps/website/lib/hooks/league/useLeagueWalletPageData.ts b/apps/website/hooks/league/useLeagueWalletPageData.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useLeagueWalletPageData.ts
rename to apps/website/hooks/league/useLeagueWalletPageData.ts
diff --git a/apps/website/lib/hooks/league/useLeagueWalletWithdrawalWithBlockers.ts b/apps/website/hooks/league/useLeagueWalletWithdrawalWithBlockers.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useLeagueWalletWithdrawalWithBlockers.ts
rename to apps/website/hooks/league/useLeagueWalletWithdrawalWithBlockers.ts
diff --git a/apps/website/lib/hooks/league/usePenaltyMutation.ts b/apps/website/hooks/league/usePenaltyMutation.ts
similarity index 100%
rename from apps/website/lib/hooks/league/usePenaltyMutation.ts
rename to apps/website/hooks/league/usePenaltyMutation.ts
diff --git a/apps/website/lib/hooks/league/useProtestDetail.ts b/apps/website/hooks/league/useProtestDetail.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useProtestDetail.ts
rename to apps/website/hooks/league/useProtestDetail.ts
diff --git a/apps/website/lib/hooks/league/useSponsorshipRequests.ts b/apps/website/hooks/league/useSponsorshipRequests.ts
similarity index 100%
rename from apps/website/lib/hooks/league/useSponsorshipRequests.ts
rename to apps/website/hooks/league/useSponsorshipRequests.ts
diff --git a/apps/website/lib/hooks/onboarding/useCompleteOnboarding.ts b/apps/website/hooks/onboarding/useCompleteOnboarding.ts
similarity index 100%
rename from apps/website/lib/hooks/onboarding/useCompleteOnboarding.ts
rename to apps/website/hooks/onboarding/useCompleteOnboarding.ts
diff --git a/apps/website/lib/hooks/onboarding/useGenerateAvatars.ts b/apps/website/hooks/onboarding/useGenerateAvatars.ts
similarity index 100%
rename from apps/website/lib/hooks/onboarding/useGenerateAvatars.ts
rename to apps/website/hooks/onboarding/useGenerateAvatars.ts
diff --git a/apps/website/lib/hooks/race/useAllRacesPageData.ts b/apps/website/hooks/race/useAllRacesPageData.ts
similarity index 100%
rename from apps/website/lib/hooks/race/useAllRacesPageData.ts
rename to apps/website/hooks/race/useAllRacesPageData.ts
diff --git a/apps/website/lib/hooks/race/useFileProtest.ts b/apps/website/hooks/race/useFileProtest.ts
similarity index 100%
rename from apps/website/lib/hooks/race/useFileProtest.ts
rename to apps/website/hooks/race/useFileProtest.ts
diff --git a/apps/website/lib/hooks/race/useRaceResultsPageData.ts b/apps/website/hooks/race/useRaceResultsPageData.ts
similarity index 100%
rename from apps/website/lib/hooks/race/useRaceResultsPageData.ts
rename to apps/website/hooks/race/useRaceResultsPageData.ts
diff --git a/apps/website/lib/hooks/race/useRegisterForRace.ts b/apps/website/hooks/race/useRegisterForRace.ts
similarity index 100%
rename from apps/website/lib/hooks/race/useRegisterForRace.ts
rename to apps/website/hooks/race/useRegisterForRace.ts
diff --git a/apps/website/lib/hooks/race/useWithdrawFromRace.ts b/apps/website/hooks/race/useWithdrawFromRace.ts
similarity index 100%
rename from apps/website/lib/hooks/race/useWithdrawFromRace.ts
rename to apps/website/hooks/race/useWithdrawFromRace.ts
diff --git a/apps/website/lib/hooks/sponsor/index.ts b/apps/website/hooks/sponsor/index.ts
similarity index 100%
rename from apps/website/lib/hooks/sponsor/index.ts
rename to apps/website/hooks/sponsor/index.ts
diff --git a/apps/website/lib/hooks/sponsor/useAvailableLeagues.ts b/apps/website/hooks/sponsor/useAvailableLeagues.ts
similarity index 100%
rename from apps/website/lib/hooks/sponsor/useAvailableLeagues.ts
rename to apps/website/hooks/sponsor/useAvailableLeagues.ts
diff --git a/apps/website/lib/hooks/sponsor/useSponsorBilling.ts b/apps/website/hooks/sponsor/useSponsorBilling.ts
similarity index 100%
rename from apps/website/lib/hooks/sponsor/useSponsorBilling.ts
rename to apps/website/hooks/sponsor/useSponsorBilling.ts
diff --git a/apps/website/lib/hooks/sponsor/useSponsorDashboard.ts b/apps/website/hooks/sponsor/useSponsorDashboard.ts
similarity index 100%
rename from apps/website/lib/hooks/sponsor/useSponsorDashboard.ts
rename to apps/website/hooks/sponsor/useSponsorDashboard.ts
diff --git a/apps/website/lib/hooks/sponsor/useSponsorLeagueDetail.ts b/apps/website/hooks/sponsor/useSponsorLeagueDetail.ts
similarity index 100%
rename from apps/website/lib/hooks/sponsor/useSponsorLeagueDetail.ts
rename to apps/website/hooks/sponsor/useSponsorLeagueDetail.ts
diff --git a/apps/website/components/sponsors/useSponsorMode.ts b/apps/website/hooks/sponsor/useSponsorMode.ts
similarity index 100%
rename from apps/website/components/sponsors/useSponsorMode.ts
rename to apps/website/hooks/sponsor/useSponsorMode.ts
diff --git a/apps/website/lib/hooks/sponsor/useSponsorSponsorships.ts b/apps/website/hooks/sponsor/useSponsorSponsorships.ts
similarity index 100%
rename from apps/website/lib/hooks/sponsor/useSponsorSponsorships.ts
rename to apps/website/hooks/sponsor/useSponsorSponsorships.ts
diff --git a/apps/website/lib/hooks/team/index.ts b/apps/website/hooks/team/index.ts
similarity index 100%
rename from apps/website/lib/hooks/team/index.ts
rename to apps/website/hooks/team/index.ts
diff --git a/apps/website/lib/hooks/team/useAllTeams.ts b/apps/website/hooks/team/useAllTeams.ts
similarity index 100%
rename from apps/website/lib/hooks/team/useAllTeams.ts
rename to apps/website/hooks/team/useAllTeams.ts
diff --git a/apps/website/lib/hooks/team/useApproveJoinRequest.ts b/apps/website/hooks/team/useApproveJoinRequest.ts
similarity index 100%
rename from apps/website/lib/hooks/team/useApproveJoinRequest.ts
rename to apps/website/hooks/team/useApproveJoinRequest.ts
diff --git a/apps/website/lib/hooks/team/useCreateTeam.ts b/apps/website/hooks/team/useCreateTeam.ts
similarity index 100%
rename from apps/website/lib/hooks/team/useCreateTeam.ts
rename to apps/website/hooks/team/useCreateTeam.ts
diff --git a/apps/website/lib/hooks/team/useJoinTeam.ts b/apps/website/hooks/team/useJoinTeam.ts
similarity index 100%
rename from apps/website/lib/hooks/team/useJoinTeam.ts
rename to apps/website/hooks/team/useJoinTeam.ts
diff --git a/apps/website/lib/hooks/team/useLeaveTeam.ts b/apps/website/hooks/team/useLeaveTeam.ts
similarity index 100%
rename from apps/website/lib/hooks/team/useLeaveTeam.ts
rename to apps/website/hooks/team/useLeaveTeam.ts
diff --git a/apps/website/lib/hooks/team/useRejectJoinRequest.ts b/apps/website/hooks/team/useRejectJoinRequest.ts
similarity index 100%
rename from apps/website/lib/hooks/team/useRejectJoinRequest.ts
rename to apps/website/hooks/team/useRejectJoinRequest.ts
diff --git a/apps/website/lib/hooks/team/useTeamDetails.ts b/apps/website/hooks/team/useTeamDetails.ts
similarity index 100%
rename from apps/website/lib/hooks/team/useTeamDetails.ts
rename to apps/website/hooks/team/useTeamDetails.ts
diff --git a/apps/website/lib/hooks/team/useTeamJoinRequests.ts b/apps/website/hooks/team/useTeamJoinRequests.ts
similarity index 100%
rename from apps/website/lib/hooks/team/useTeamJoinRequests.ts
rename to apps/website/hooks/team/useTeamJoinRequests.ts
diff --git a/apps/website/lib/hooks/team/useTeamMembers.ts b/apps/website/hooks/team/useTeamMembers.ts
similarity index 100%
rename from apps/website/lib/hooks/team/useTeamMembers.ts
rename to apps/website/hooks/team/useTeamMembers.ts
diff --git a/apps/website/lib/hooks/team/useTeamMembership.ts b/apps/website/hooks/team/useTeamMembership.ts
similarity index 100%
rename from apps/website/lib/hooks/team/useTeamMembership.ts
rename to apps/website/hooks/team/useTeamMembership.ts
diff --git a/apps/website/lib/hooks/team/useTeamRoster.ts b/apps/website/hooks/team/useTeamRoster.ts
similarity index 100%
rename from apps/website/lib/hooks/team/useTeamRoster.ts
rename to apps/website/hooks/team/useTeamRoster.ts
diff --git a/apps/website/lib/hooks/team/useTeamStandings.ts b/apps/website/hooks/team/useTeamStandings.ts
similarity index 100%
rename from apps/website/lib/hooks/team/useTeamStandings.ts
rename to apps/website/hooks/team/useTeamStandings.ts
diff --git a/apps/website/lib/hooks/team/useUpdateTeam.ts b/apps/website/hooks/team/useUpdateTeam.ts
similarity index 100%
rename from apps/website/lib/hooks/team/useUpdateTeam.ts
rename to apps/website/hooks/team/useUpdateTeam.ts
diff --git a/apps/website/lib/hooks/useCapability.ts b/apps/website/hooks/useCapability.ts
similarity index 100%
rename from apps/website/lib/hooks/useCapability.ts
rename to apps/website/hooks/useCapability.ts
diff --git a/apps/website/lib/hooks/useDriverLeaderboard.ts b/apps/website/hooks/useDriverLeaderboard.ts
similarity index 100%
rename from apps/website/lib/hooks/useDriverLeaderboard.ts
rename to apps/website/hooks/useDriverLeaderboard.ts
diff --git a/apps/website/lib/hooks/useDriverSearch.ts b/apps/website/hooks/useDriverSearch.ts
similarity index 100%
rename from apps/website/lib/hooks/useDriverSearch.ts
rename to apps/website/hooks/useDriverSearch.ts
diff --git a/apps/website/lib/hooks/useEffectiveDriverId.ts b/apps/website/hooks/useEffectiveDriverId.ts
similarity index 100%
rename from apps/website/lib/hooks/useEffectiveDriverId.ts
rename to apps/website/hooks/useEffectiveDriverId.ts
diff --git a/apps/website/lib/hooks/useEnhancedForm.test.ts b/apps/website/hooks/useEnhancedForm.test.ts
similarity index 100%
rename from apps/website/lib/hooks/useEnhancedForm.test.ts
rename to apps/website/hooks/useEnhancedForm.test.ts
diff --git a/apps/website/lib/hooks/useEnhancedForm.ts b/apps/website/hooks/useEnhancedForm.ts
similarity index 100%
rename from apps/website/lib/hooks/useEnhancedForm.ts
rename to apps/website/hooks/useEnhancedForm.ts
diff --git a/apps/website/lib/hooks/useLeagueScoringPresets.ts b/apps/website/hooks/useLeagueScoringPresets.ts
similarity index 100%
rename from apps/website/lib/hooks/useLeagueScoringPresets.ts
rename to apps/website/hooks/useLeagueScoringPresets.ts
diff --git a/apps/website/lib/hooks/useLeagueWizardService.ts b/apps/website/hooks/useLeagueWizardService.ts
similarity index 100%
rename from apps/website/lib/hooks/useLeagueWizardService.ts
rename to apps/website/hooks/useLeagueWizardService.ts
diff --git a/apps/website/lib/hooks/usePenaltyTypesReference.ts b/apps/website/hooks/usePenaltyTypesReference.ts
similarity index 100%
rename from apps/website/lib/hooks/usePenaltyTypesReference.ts
rename to apps/website/hooks/usePenaltyTypesReference.ts
diff --git a/apps/website/lib/hooks/useScrollProgress.ts b/apps/website/hooks/useScrollProgress.ts
similarity index 100%
rename from apps/website/lib/hooks/useScrollProgress.ts
rename to apps/website/hooks/useScrollProgress.ts
diff --git a/apps/website/lib/api/races/RacesApiClient.ts b/apps/website/lib/api/races/RacesApiClient.ts
index c9e8e23e7..b8fe51272 100644
--- a/apps/website/lib/api/races/RacesApiClient.ts
+++ b/apps/website/lib/api/races/RacesApiClient.ts
@@ -16,8 +16,8 @@ import type { AllRacesPageDTO } from '../../types/generated/AllRacesPageDTO';
import type { FilteredRacesPageDataDTO } from '../../types/tbd/FilteredRacesPageDataDTO';
// Define missing types
-type RacesPageDataDTO = { races: RacesPageDataRaceDTO[] };
-type RaceDetailDTO = {
+export type RacesPageDataDTO = { races: RacesPageDataRaceDTO[] };
+export type RaceDetailDTO = {
race: RaceDetailRaceDTO | null;
league: RaceDetailLeagueDTO | null;
entryList: RaceDetailEntryDTO[];
@@ -25,7 +25,7 @@ type RaceDetailDTO = {
userResult: RaceDetailUserResultDTO | null;
error?: string;
};
-type ImportRaceResultsSummaryDTO = {
+export type ImportRaceResultsSummaryDTO = {
success: boolean;
raceId: string;
driversProcessed: number;
diff --git a/apps/website/lib/builders/view-data/RacesViewDataBuilder.ts b/apps/website/lib/builders/view-data/RacesViewDataBuilder.ts
index 53ea7c066..8cade32aa 100644
--- a/apps/website/lib/builders/view-data/RacesViewDataBuilder.ts
+++ b/apps/website/lib/builders/view-data/RacesViewDataBuilder.ts
@@ -1,39 +1,100 @@
-import { RacesViewData, RacesRace } from '@/lib/view-data/races/RacesViewData';
+import type { RacesPageDataDTO } from '@/lib/types/generated/RacesPageDataDTO';
+import type { RacesViewData, RaceViewData } from '@/lib/view-data/RacesViewData';
-/**
- * Races View Data Builder
- *
- * Transforms API DTO into ViewData for the races template.
- * Deterministic, side-effect free.
- */
export class RacesViewDataBuilder {
- static build(apiDto: any): RacesViewData {
- const races = apiDto.races.map((race: any) => ({
- id: race.id,
- track: race.track,
- car: race.car,
- scheduledAt: race.scheduledAt,
- status: race.status as 'scheduled' | 'running' | 'completed' | 'cancelled',
- sessionType: 'race',
- leagueId: race.leagueId,
- leagueName: race.leagueName,
- strengthOfField: race.strengthOfField ?? undefined,
- isUpcoming: race.status === 'scheduled',
- isLive: race.status === 'running',
- isPast: race.status === 'completed',
- }));
+ static build(apiDto: RacesPageDataDTO): RacesViewData {
+ const races = apiDto.races.map((race): RaceViewData => {
+ const scheduledAt = new Date(race.scheduledAt);
+
+ return {
+ id: race.id,
+ track: race.track,
+ car: race.car,
+ scheduledAt: race.scheduledAt,
+ scheduledAtLabel: scheduledAt.toLocaleDateString('en-US', {
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ }),
+ timeLabel: scheduledAt.toLocaleTimeString('en-US', {
+ hour: '2-digit',
+ minute: '2-digit',
+ }),
+ relativeTimeLabel: this.getRelativeTime(scheduledAt),
+ status: race.status as RaceViewData['status'],
+ statusLabel: this.getStatusLabel(race.status),
+ sessionType: 'Race',
+ leagueId: race.leagueId,
+ leagueName: race.leagueName,
+ strengthOfField: race.strengthOfField ?? null,
+ isUpcoming: race.isUpcoming,
+ isLive: race.isLive,
+ isPast: race.isPast,
+ };
+ });
- const totalCount = races.length;
- const scheduledRaces = races.filter((r: RacesRace) => r.isUpcoming);
- const runningRaces = races.filter((r: RacesRace) => r.isLive);
- const completedRaces = races.filter((r: RacesRace) => r.isPast);
+ const leagues = Array.from(
+ new Map(
+ races
+ .filter(r => r.leagueId && r.leagueName)
+ .map(r => [r.leagueId, { id: r.leagueId!, name: r.leagueName! }])
+ ).values()
+ );
+
+ const groupedRaces = new Map();
+ races.forEach((race) => {
+ const dateKey = race.scheduledAt.split('T')[0]!;
+ if (!groupedRaces.has(dateKey)) {
+ groupedRaces.set(dateKey, []);
+ }
+ groupedRaces.get(dateKey)!.push(race);
+ });
+
+ const racesByDate = Array.from(groupedRaces.entries()).map(([dateKey, dayRaces]) => ({
+ dateKey,
+ dateLabel: dayRaces[0]?.scheduledAtLabel || '',
+ races: dayRaces,
+ }));
return {
races,
- totalCount,
- scheduledRaces,
- runningRaces,
- completedRaces,
+ totalCount: races.length,
+ scheduledCount: races.filter(r => r.status === 'scheduled').length,
+ runningCount: races.filter(r => r.status === 'running').length,
+ completedCount: races.filter(r => r.status === 'completed').length,
+ leagues,
+ upcomingRaces: races.filter(r => r.isUpcoming).slice(0, 5),
+ liveRaces: races.filter(r => r.isLive),
+ recentResults: races.filter(r => r.isPast).slice(0, 5),
+ racesByDate,
};
}
-}
\ No newline at end of file
+
+ private static getStatusLabel(status: string): string {
+ switch (status) {
+ case 'scheduled': return 'Scheduled';
+ case 'running': return 'LIVE';
+ case 'completed': return 'Completed';
+ case 'cancelled': return 'Cancelled';
+ default: return status;
+ }
+ }
+
+ private static getRelativeTime(date: Date): string {
+ const now = new Date();
+ const diffMs = date.getTime() - now.getTime();
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
+
+ if (diffMs < 0) return 'Past';
+ if (diffHours < 1) return 'Starting soon';
+ if (diffHours < 24) return `In ${diffHours}h`;
+ if (diffDays === 1) return 'Tomorrow';
+ if (diffDays < 7) return `In ${diffDays} days`;
+ return date.toLocaleDateString('en-US', {
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ });
+ }
+}
diff --git a/apps/website/lib/page-queries/page-queries/ProfilePageQuery.ts b/apps/website/lib/page-queries/page-queries/ProfilePageQuery.ts
index fbc337d7b..b1832fd0b 100644
--- a/apps/website/lib/page-queries/page-queries/ProfilePageQuery.ts
+++ b/apps/website/lib/page-queries/page-queries/ProfilePageQuery.ts
@@ -27,12 +27,7 @@ export class ProfilePageQuery implements PageQuery> {
- const query = new ProfilePageQuery();
- return query.execute();
- }
-}
\ No newline at end of file
+}
diff --git a/apps/website/lib/page-queries/page-queries/TeamsPageQuery.ts b/apps/website/lib/page-queries/page-queries/TeamsPageQuery.ts
index 0b9b5c1fe..fd8541bb9 100644
--- a/apps/website/lib/page-queries/page-queries/TeamsPageQuery.ts
+++ b/apps/website/lib/page-queries/page-queries/TeamsPageQuery.ts
@@ -1,39 +1,16 @@
-import type { PageQueryResult } from '@/lib/contracts/page-queries/PageQueryResult';
+import { Result } from '@/lib/contracts/Result';
+import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
+import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
import { TeamService } from '@/lib/services/teams/TeamService';
-import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
-
-/**
- * TeamsPageDto - Raw serializable data for teams page
- * Contains only raw data, no derived/computed properties
- */
-export interface TeamsPageDto {
- teams: Array<{
- id: string;
- name: string;
- tag: string;
- memberCount: number;
- description?: string;
- totalWins: number;
- totalRaces: number;
- performanceLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro';
- isRecruiting: boolean;
- specialization?: 'endurance' | 'sprint' | 'mixed';
- region?: string;
- languages: string[];
- leagues: string[];
- logoUrl?: string;
- rating?: number;
- category?: string;
- }>;
-}
+import type { TeamsViewData } from '@/lib/view-data/TeamsViewData';
+import { TeamsViewDataBuilder } from '@/lib/builders/view-data/TeamsViewDataBuilder';
/**
* TeamsPageQuery - Server-side composition for teams list page
* Manual wiring only; no ContainerManager; no PageDataFetcher
- * Returns raw serializable DTO
*/
-export class TeamsPageQuery {
- static async execute(): Promise> {
+export class TeamsPageQuery implements PageQuery {
+ async execute(): Promise> {
try {
// Manual dependency creation
const service = new TeamService();
@@ -42,51 +19,17 @@ export class TeamsPageQuery {
const result = await service.getAllTeams();
if (result.isErr()) {
- return { status: 'error', errorId: 'TEAMS_FETCH_FAILED' };
+ return Result.err(mapToPresentationError(result.getError()));
}
const teams = result.unwrap();
- if (!teams || teams.length === 0) {
- return { status: 'notFound' };
- }
+ // Transform to ViewData using builder
+ const viewData = TeamsViewDataBuilder.build({ teams });
- // Transform to raw serializable DTO
- const dto: TeamsPageDto = {
- teams: teams.map((team: TeamSummaryViewModel) => ({
- id: team.id,
- name: team.name,
- tag: team.tag,
- memberCount: team.memberCount,
- description: team.description,
- totalWins: team.totalWins,
- totalRaces: team.totalRaces,
- performanceLevel: team.performanceLevel,
- isRecruiting: team.isRecruiting,
- specialization: team.specialization,
- region: team.region,
- languages: team.languages,
- leagues: team.leagues,
- logoUrl: team.logoUrl,
- rating: team.rating,
- category: team.category,
- })),
- };
-
- return { status: 'ok', dto };
+ return Result.ok(viewData);
} catch (error) {
- // Handle specific error types
- if (error instanceof Error) {
- const errorAny = error as { statusCode?: number; message?: string };
- if (errorAny.message?.includes('not found') || errorAny.statusCode === 404) {
- return { status: 'notFound' };
- }
- if (errorAny.message?.includes('redirect') || errorAny.statusCode === 302) {
- return { status: 'redirect', to: '/' };
- }
- return { status: 'error', errorId: 'TEAMS_FETCH_FAILED' };
- }
- return { status: 'error', errorId: 'UNKNOWN_ERROR' };
+ return Result.err('unknown');
}
}
-}
\ No newline at end of file
+}
diff --git a/apps/website/lib/page-queries/races/RacesPageQuery.ts b/apps/website/lib/page-queries/races/RacesPageQuery.ts
index 581bda413..92a4a0e16 100644
--- a/apps/website/lib/page-queries/races/RacesPageQuery.ts
+++ b/apps/website/lib/page-queries/races/RacesPageQuery.ts
@@ -1,7 +1,7 @@
import { Result } from '@/lib/contracts/Result';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { PresentationError, mapToPresentationError } from '@/lib/contracts/page-queries/PresentationError';
-import { RacesViewData } from '@/lib/view-data/races/RacesViewData';
+import { RacesViewData } from '@/lib/view-data/RacesViewData';
import { RacesService } from '@/lib/services/races/RacesService';
import { RacesViewDataBuilder } from '@/lib/builders/view-data/RacesViewDataBuilder';
@@ -27,10 +27,4 @@ export class RacesPageQuery implements PageQuery {
const viewData = RacesViewDataBuilder.build(result.unwrap());
return Result.ok(viewData);
}
-
- // Static method to avoid object construction in server code
- static async execute(): Promise> {
- const query = new RacesPageQuery();
- return await query.execute();
- }
-}
\ No newline at end of file
+}
diff --git a/apps/website/lib/services/drivers/DriverProfileService.ts b/apps/website/lib/services/drivers/DriverProfileService.ts
index 18c66d3d5..026354a42 100644
--- a/apps/website/lib/services/drivers/DriverProfileService.ts
+++ b/apps/website/lib/services/drivers/DriverProfileService.ts
@@ -1,28 +1,26 @@
import { DriversApiClient } from '@/lib/api/drivers/DriversApiClient';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
-import { isProductionEnvironment } from '@/lib/config/env';
import { Result } from '@/lib/contracts/Result';
import type { Service } from '@/lib/contracts/services/Service';
-import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
+import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import type { GetDriverProfileOutputDTO } from '@/lib/types/generated/GetDriverProfileOutputDTO';
type DriverProfileServiceError = 'notFound' | 'unauthorized' | 'serverError' | 'unknown';
export class DriverProfileService implements Service {
- async getDriverProfile(driverId: string): Promise> {
+ private apiClient: DriversApiClient;
+
+ constructor() {
+ const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
+ const errorReporter = new ConsoleErrorReporter();
+ this.apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
+ }
+ async getDriverProfile(driverId: string): Promise> {
try {
- const baseUrl = getWebsiteApiBaseUrl();
- const errorReporter = new EnhancedErrorReporter(logger, {
- showUserNotifications: true,
- logToConsole: true,
- reportToExternal: isProductionEnvironment(),
- });
-
- const apiClient = new DriversApiClient(baseUrl, errorReporter, logger);
- const dto = await apiClient.getDriverProfile(driverId);
+ const dto = await this.apiClient.getDriverProfile(driverId);
if (!dto.currentDriver) {
return Result.err('notFound');
@@ -40,8 +38,6 @@ export class DriverProfileService implements Service {
return Result.err('notFound');
}
- logger.error('DriverProfileService failed', error instanceof Error ? error : undefined, { error: errorAny });
-
if (errorAny.statusCode && errorAny.statusCode >= 500) {
return Result.err('serverError');
}
diff --git a/apps/website/lib/services/races/RacesService.ts b/apps/website/lib/services/races/RacesService.ts
index f2f3fecbb..994d27542 100644
--- a/apps/website/lib/services/races/RacesService.ts
+++ b/apps/website/lib/services/races/RacesService.ts
@@ -1,10 +1,14 @@
import { RacesApiClient } from '@/lib/api/races/RacesApiClient';
import { Result } from '@/lib/contracts/Result';
-import { DomainError } from '@/lib/contracts/services/Service';
+import { DomainError, Service } from '@/lib/contracts/services/Service';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { ApiError } from '@/lib/api/base/ApiError';
+import type { RacesPageDataDTO } from '@/lib/types/generated/RacesPageDataDTO';
+import type { RaceDetailDTO } from '@/lib/api/races/RacesApiClient';
+import type { RaceResultsDetailDTO } from '@/lib/types/generated/RaceResultsDetailDTO';
+import type { RaceWithSOFDTO } from '@/lib/types/generated/RaceWithSOFDTO';
/**
* Races Service
@@ -12,7 +16,7 @@ import { ApiError } from '@/lib/api/base/ApiError';
* Orchestration service for race-related operations.
* Returns raw API DTOs. No ViewModels or UX logic.
*/
-export class RacesService {
+export class RacesService implements Service {
private apiClient: RacesApiClient;
constructor() {
@@ -28,21 +32,12 @@ export class RacesService {
* Get races page data
* Returns races for the main races page
*/
- async getRacesPageData(): Promise> {
+ async getRacesPageData(): Promise> {
try {
const data = await this.apiClient.getPageData();
return Result.ok(data);
} catch (error) {
- if (error instanceof ApiError) {
- return Result.err({
- type: this.mapApiErrorType(error.type),
- message: error.message
- });
- }
- return Result.err({
- type: 'unknown',
- message: 'Failed to fetch races page data'
- });
+ return Result.err(this.mapError(error, 'Failed to fetch races page data'));
}
}
@@ -50,21 +45,12 @@ export class RacesService {
* Get race detail
* Returns detailed information for a specific race
*/
- async getRaceDetail(raceId: string, driverId: string): Promise> {
+ async getRaceDetail(raceId: string, driverId: string): Promise> {
try {
const data = await this.apiClient.getDetail(raceId, driverId);
return Result.ok(data);
} catch (error) {
- if (error instanceof ApiError) {
- return Result.err({
- type: this.mapApiErrorType(error.type),
- message: error.message
- });
- }
- return Result.err({
- type: 'unknown',
- message: 'Failed to fetch race detail'
- });
+ return Result.err(this.mapError(error, 'Failed to fetch race detail'));
}
}
@@ -72,21 +58,12 @@ export class RacesService {
* Get race results detail
* Returns results for a specific race
*/
- async getRaceResultsDetail(raceId: string): Promise> {
+ async getRaceResultsDetail(raceId: string): Promise> {
try {
const data = await this.apiClient.getResultsDetail(raceId);
return Result.ok(data);
} catch (error) {
- if (error instanceof ApiError) {
- return Result.err({
- type: this.mapApiErrorType(error.type),
- message: error.message
- });
- }
- return Result.err({
- type: 'unknown',
- message: 'Failed to fetch race results'
- });
+ return Result.err(this.mapError(error, 'Failed to fetch race results'));
}
}
@@ -94,21 +71,12 @@ export class RacesService {
* Get race with strength of field
* Returns race data with SOF calculation
*/
- async getRaceWithSOF(raceId: string): Promise> {
+ async getRaceWithSOF(raceId: string): Promise> {
try {
const data = await this.apiClient.getWithSOF(raceId);
return Result.ok(data);
} catch (error) {
- if (error instanceof ApiError) {
- return Result.err({
- type: this.mapApiErrorType(error.type),
- message: error.message
- });
- }
- return Result.err({
- type: 'unknown',
- message: 'Failed to fetch race SOF'
- });
+ return Result.err(this.mapError(error, 'Failed to fetch race SOF'));
}
}
@@ -116,24 +84,28 @@ export class RacesService {
* Get all races for the all races page
* Returns all races with pagination support
*/
- async getAllRacesPageData(): Promise> {
+ async getAllRacesPageData(): Promise> {
try {
const data = await this.apiClient.getPageData();
return Result.ok(data);
} catch (error) {
- if (error instanceof ApiError) {
- return Result.err({
- type: this.mapApiErrorType(error.type),
- message: error.message
- });
- }
- return Result.err({
- type: 'unknown',
- message: 'Failed to fetch all races'
- });
+ return Result.err(this.mapError(error, 'Failed to fetch all races'));
}
}
+ private mapError(error: unknown, defaultMessage: string): DomainError {
+ if (error instanceof ApiError) {
+ return {
+ type: this.mapApiErrorType(error.type),
+ message: error.message
+ };
+ }
+ return {
+ type: 'unknown',
+ message: defaultMessage
+ };
+ }
+
private mapApiErrorType(apiErrorType: string): DomainError['type'] {
switch (apiErrorType) {
case 'NOT_FOUND':
@@ -150,4 +122,4 @@ export class RacesService {
return 'unknown';
}
}
-}
\ No newline at end of file
+}
diff --git a/apps/website/lib/view-data/LeagueAdminScheduleViewData.ts b/apps/website/lib/view-data/LeagueAdminScheduleViewData.ts
new file mode 100644
index 000000000..8d8178501
--- /dev/null
+++ b/apps/website/lib/view-data/LeagueAdminScheduleViewData.ts
@@ -0,0 +1,17 @@
+export interface AdminScheduleRaceData {
+ id: string;
+ name: string;
+ track: string;
+ car: string;
+ scheduledAt: string; // ISO string
+}
+
+export interface LeagueAdminScheduleViewData {
+ published: boolean;
+ races: AdminScheduleRaceData[];
+ seasons: Array<{
+ seasonId: string;
+ name: string;
+ }>;
+ seasonId: string;
+}
diff --git a/apps/website/lib/view-data/LeagueRulebookViewData.ts b/apps/website/lib/view-data/LeagueRulebookViewData.ts
new file mode 100644
index 000000000..5c137483f
--- /dev/null
+++ b/apps/website/lib/view-data/LeagueRulebookViewData.ts
@@ -0,0 +1,19 @@
+export interface RulebookScoringConfig {
+ scoringPresetName: string | null;
+ gameName: string;
+ championships: Array<{
+ type: string;
+ sessionTypes: string[];
+ pointsPreview: Array<{
+ sessionType: string;
+ position: number;
+ points: number;
+ }>;
+ bonusSummary: string[];
+ }>;
+ dropPolicySummary: string;
+}
+
+export interface LeagueRulebookViewData {
+ scoringConfig: RulebookScoringConfig | null;
+}
diff --git a/apps/website/lib/view-data/RacesViewData.ts b/apps/website/lib/view-data/RacesViewData.ts
new file mode 100644
index 000000000..e5a91a6e1
--- /dev/null
+++ b/apps/website/lib/view-data/RacesViewData.ts
@@ -0,0 +1,35 @@
+export interface RaceViewData {
+ id: string;
+ track: string;
+ car: string;
+ scheduledAt: string;
+ scheduledAtLabel: string;
+ timeLabel: string;
+ relativeTimeLabel: string;
+ status: 'scheduled' | 'running' | 'completed' | 'cancelled';
+ statusLabel: string;
+ sessionType: string;
+ leagueId: string | null;
+ leagueName: string | null;
+ strengthOfField: number | null;
+ isUpcoming: boolean;
+ isLive: boolean;
+ isPast: boolean;
+}
+
+export interface RacesViewData {
+ races: RaceViewData[];
+ totalCount: number;
+ scheduledCount: number;
+ runningCount: number;
+ completedCount: number;
+ leagues: Array<{ id: string; name: string }>;
+ upcomingRaces: RaceViewData[];
+ liveRaces: RaceViewData[];
+ recentResults: RaceViewData[];
+ racesByDate: Array<{
+ dateKey: string;
+ dateLabel: string;
+ races: RaceViewData[];
+ }>;
+}
diff --git a/apps/website/lib/view-data/leagues/LeagueWalletViewData.ts b/apps/website/lib/view-data/leagues/LeagueWalletViewData.ts
index 868ea69f0..34501d3c8 100644
--- a/apps/website/lib/view-data/leagues/LeagueWalletViewData.ts
+++ b/apps/website/lib/view-data/leagues/LeagueWalletViewData.ts
@@ -1,13 +1,21 @@
+export interface LeagueWalletTransactionViewData {
+ id: string;
+ type: 'deposit' | 'withdrawal' | 'sponsorship' | 'prize';
+ amount: number;
+ formattedAmount: string;
+ amountColor: string;
+ description: string;
+ createdAt: string;
+ formattedDate: string;
+ status: 'completed' | 'pending' | 'failed';
+ statusColor: string;
+ typeColor: string;
+}
+
export interface LeagueWalletViewData {
leagueId: string;
balance: number;
+ formattedBalance: string;
currency: string;
- transactions: Array<{
- id: string;
- type: 'deposit' | 'withdrawal' | 'sponsorship' | 'prize';
- amount: number;
- description: string;
- createdAt: string;
- status: 'completed' | 'pending' | 'failed';
- }>;
-}
\ No newline at end of file
+ transactions: LeagueWalletTransactionViewData[];
+}
diff --git a/apps/website/tsconfig.json b/apps/website/tsconfig.json
index f851d8cfe..9a31a3eae 100644
--- a/apps/website/tsconfig.json
+++ b/apps/website/tsconfig.json
@@ -76,7 +76,7 @@
"types/",
"utilities/",
".next/types/**/*.ts"
- ],
+, "hooks/sponsor/useSponsorMode.ts" ],
"exclude": [
"**/*.test.ts",
"**/*.test.tsx",
diff --git a/apps/website/ui/Box.tsx b/apps/website/ui/Box.tsx
index 5d84c687a..e1514a91b 100644
--- a/apps/website/ui/Box.tsx
+++ b/apps/website/ui/Box.tsx
@@ -45,9 +45,9 @@ export const Box = forwardRef((
maxWidth,
...props
}: BoxProps & ComponentPropsWithoutRef,
- ref: ForwardedRef
+ ref: ForwardedRef
) => {
- const Tag = (as as any) || 'div';
+ const Tag = (as as ElementType) || 'div';
const spacingMap: Record = {
0: '0', 0.5: '0.5', 1: '1', 1.5: '1.5', 2: '2', 2.5: '2.5', 3: '3', 3.5: '3.5', 4: '4',
@@ -81,10 +81,10 @@ export const Box = forwardRef((
className
].filter(Boolean).join(' ');
- const style = maxWidth ? { maxWidth, ...((props as any).style || {}) } : (props as any).style;
+ const style = maxWidth ? { maxWidth, ...((props as Record).style as object || {}) } : (props as Record).style;
return (
-
+ } className={classes} {...props} style={style as React.CSSProperties}>
{children}
);
diff --git a/apps/website/ui/CountryFlag.tsx b/apps/website/ui/CountryFlag.tsx
index 3a2c52002..2bafce063 100644
--- a/apps/website/ui/CountryFlag.tsx
+++ b/apps/website/ui/CountryFlag.tsx
@@ -1,6 +1,6 @@
'use client';
-import React, { useState } from 'react';
+import React from 'react';
// ISO 3166-1 alpha-2 country code to full country name mapping
const countryNames: Record = {
@@ -78,8 +78,6 @@ export function CountryFlag({
className = '',
showTooltip = true
}: CountryFlagProps) {
- const [showTooltipState, setShowTooltipState] = useState(false);
-
const sizeClasses = {
sm: 'text-xs',
md: 'text-sm',
@@ -92,17 +90,9 @@ export function CountryFlag({
return (
setShowTooltipState(true)}
- onMouseLeave={() => setShowTooltipState(false)}
title={showTooltip ? countryName : undefined}
>
{flag}
- {showTooltip && showTooltipState && (
-
- {countryName}
-
-
- )}
);
}
diff --git a/apps/website/ui/Modal.tsx b/apps/website/ui/Modal.tsx
index 72c77517f..ad7c9efa7 100644
--- a/apps/website/ui/Modal.tsx
+++ b/apps/website/ui/Modal.tsx
@@ -1,8 +1,6 @@
'use client';
import React, {
- useEffect,
- useRef,
type ReactNode,
type KeyboardEvent as ReactKeyboardEvent,
} from 'react';
@@ -34,26 +32,6 @@ export function Modal({
onOpenChange,
isOpen,
}: ModalProps) {
- const dialogRef = useRef(null);
- const previouslyFocusedElementRef = useRef(null);
-
- useEffect(() => {
- if (isOpen) {
- previouslyFocusedElementRef.current = document.activeElement;
- const focusable = getFirstFocusable(dialogRef.current);
- if (focusable) {
- focusable.focus();
- } else if (dialogRef.current) {
- dialogRef.current.focus();
- }
- return;
- }
-
- if (!isOpen && previouslyFocusedElementRef.current instanceof HTMLElement) {
- previouslyFocusedElementRef.current.focus();
- }
- }, [isOpen]);
-
const handleKeyDown = (event: ReactKeyboardEvent) => {
if (event.key === 'Escape') {
if (onOpenChange) {
@@ -61,26 +39,6 @@ export function Modal({
}
return;
}
-
- if (event.key === 'Tab') {
- const focusable = getFocusableElements(dialogRef.current);
- if (focusable.length === 0) return;
-
- const first = focusable[0];
- const last = focusable[focusable.length - 1] ?? first;
-
- if (!first || !last) {
- return;
- }
-
- if (!event.shiftKey && document.activeElement === last) {
- event.preventDefault();
- first.focus();
- } else if (event.shiftKey && document.activeElement === first) {
- event.preventDefault();
- last.focus();
- }
- }
};
const handleBackdropClick = (event: React.MouseEvent) => {
@@ -104,7 +62,6 @@ export function Modal({
onClick={handleBackdropClick}
>
@@ -162,24 +119,3 @@ export function Modal({
);
}
-
-function getFocusableElements(root: HTMLElement | null): HTMLElement[] {
- if (!root) return [];
- const selectors = [
- 'a[href]',
- 'button:not([disabled])',
- 'textarea:not([disabled])',
- 'input:not([disabled])',
- 'select:not([disabled])',
- '[tabindex]:not([tabindex="-1"])',
- ];
- const nodes = Array.from(
- root.querySelectorAll(selectors.join(',')),
- );
- return nodes.filter((el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden'));
-}
-
-function getFirstFocusable(root: HTMLElement | null): HTMLElement | null {
- const elements = getFocusableElements(root);
- return elements[0] ?? null;
-}
diff --git a/apps/website/ui/Select.tsx b/apps/website/ui/Select.tsx
index fdc1ad138..26e4acde3 100644
--- a/apps/website/ui/Select.tsx
+++ b/apps/website/ui/Select.tsx
@@ -1,11 +1,11 @@
-import React, { ChangeEvent } from 'react';
+import React, { ChangeEvent, SelectHTMLAttributes } from 'react';
interface SelectOption {
value: string;
label: string;
}
-interface SelectProps extends React.SelectHTMLAttributes {
+interface SelectProps extends SelectHTMLAttributes {
id?: string;
'aria-label'?: string;
value?: string;
diff --git a/apps/website/ui/Toggle.tsx b/apps/website/ui/Toggle.tsx
index b43d4fa50..2f184b39c 100644
--- a/apps/website/ui/Toggle.tsx
+++ b/apps/website/ui/Toggle.tsx
@@ -1,7 +1,4 @@
-'use client';
-
-import React from 'react';
-import { motion, useReducedMotion } from 'framer-motion';
+import { motion } from 'framer-motion';
import { Box } from './Box';
import { Text } from './Text';
@@ -20,8 +17,6 @@ export function Toggle({
description,
disabled = false,
}: ToggleProps) {
- const shouldReduceMotion = useReducedMotion();
-
return (