website refactor

This commit is contained in:
2026-01-17 02:03:19 +01:00
parent 75ffe0798e
commit 6a49448e0a
18 changed files with 168 additions and 47 deletions

View File

@@ -17,12 +17,12 @@ export const viewport: Viewport = {
maximumScale: 1, maximumScale: 1,
userScalable: false, userScalable: false,
viewportFit: 'cover', viewportFit: 'cover',
themeColor: '#0a0a0a',
}; };
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'GridPilot - SimRacing Platform', title: 'GridPilot - SimRacing Platform',
description: 'The dedicated home for serious sim racing leagues. Automatic results, standings, team racing, and professional race control.', description: 'The dedicated home for serious sim racing leagues. Automatic results, standings, team racing, and professional race control.',
themeColor: '#0a0a0a',
appleWebApp: { appleWebApp: {
capable: true, capable: true,
statusBarStyle: 'black-translucent', statusBarStyle: 'black-translucent',

View File

@@ -607,7 +607,7 @@ export function CreateLeagueWizard({ stepName, onStepChange }: CreateLeagueWizar
Create a new league Create a new league
</Heading> </Heading>
<Text size="sm" color="text-gray-500" block> <Text size="sm" color="text-gray-500" block>
We'll also set up your first season in {steps.length} easy steps. We&apos;ll also set up your first season in {steps.length} easy steps.
</Text> </Text>
<Text size="xs" color="text-gray-500" block mt={1}> <Text size="xs" color="text-gray-500" block mt={1}>
A league is your long-term brand. Each season is a block of races you can run again and again. A league is your long-term brand. Each season is a block of races you can run again and again.

View File

@@ -1,19 +1,17 @@
import { PageWrapper } from '@/components/shared/state/PageWrapper'; import { PageWrapper } from '@/components/shared/state/PageWrapper';
import { HomeTemplate, type HomeViewData } from '@/templates/HomeTemplate'; import { HomeTemplate, type HomeViewData } from '@/templates/HomeTemplate';
import { PageDataFetcher } from '@/lib/page/PageDataFetcher'; import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
import { HomeService } from '@/lib/services/home/HomeService'; // @server-safe import { HomePageQuery } from '@/lib/page-queries/HomePageQuery';
import { notFound, redirect } from 'next/navigation'; import { notFound, redirect } from 'next/navigation';
import { routes } from '@/lib/routing/RouteConfig'; import { routes } from '@/lib/routing/RouteConfig';
export default async function Page() { export default async function Page() {
const homeService = new HomeService(); if (await HomePageQuery.shouldRedirectToDashboard()) {
if (await homeService.shouldRedirectToDashboard()) {
redirect(routes.protected.dashboard); redirect(routes.protected.dashboard);
} }
const data = await PageDataFetcher.fetchManual(async () => { const data = await PageDataFetcher.fetchManual(async () => {
const result = await homeService.getHomeData(); const result = await HomePageQuery.execute();
return result.isOk() ? result.unwrap() : null; return result.isOk() ? result.unwrap() : null;
}); });

View File

@@ -1,5 +1,5 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper'; import { PageWrapper } from '@/components/shared/state/PageWrapper';
import { RaceResultsPageQuery } from '@/lib/page-queries/races/RaceResultsPageQuery'; import { RaceResultsPageQuery } from '@/lib/page-queries/races/RaceResultsPageQuery';
import RaceResultsPageClient from './RaceResultsPageClient'; import RaceResultsPageClient from './RaceResultsPageClient';
@@ -25,13 +25,12 @@ export default async function RaceResultsPage({ params }: RaceResultsPageProps)
if (error === 'notFound') { if (error === 'notFound') {
notFound(); notFound();
} }
// For other errors, let StatefulPageWrapper handle it // For other errors, let PageWrapper handle it
return ( return (
<StatefulPageWrapper <PageWrapper
data={undefined} data={undefined}
Template={RaceResultsPageClient as any} Template={RaceResultsPageClient as any}
error={new Error('Failed to load race results')} error={new Error('Failed to load race results')}
retry={() => Promise.resolve()}
/> />
); );
} }
@@ -39,10 +38,9 @@ export default async function RaceResultsPage({ params }: RaceResultsPageProps)
const viewData = result.unwrap(); const viewData = result.unwrap();
return ( return (
<StatefulPageWrapper <PageWrapper
data={viewData} data={viewData}
Template={RaceResultsPageClient} Template={RaceResultsPageClient}
retry={() => Promise.resolve()}
/> />
); );
} }

View File

@@ -16,6 +16,7 @@ export interface BaseApiClientOptions {
timeout?: number; timeout?: number;
retry?: boolean; retry?: boolean;
retryConfig?: typeof DEFAULT_RETRY_CONFIG; retryConfig?: typeof DEFAULT_RETRY_CONFIG;
allowUnauthenticated?: boolean;
} }
export class BaseApiClient { export class BaseApiClient {

View File

@@ -28,7 +28,7 @@ export class DriversApiClient extends BaseApiClient {
/** Get current driver (based on session) */ /** Get current driver (based on session) */
getCurrent(): Promise<GetDriverOutputDTO | null> { getCurrent(): Promise<GetDriverOutputDTO | null> {
return this.get<GetDriverOutputDTO | null>('/drivers/current'); return this.get<GetDriverOutputDTO | null>('/drivers/current', { allowUnauthenticated: true });
} }
/** Get driver registration status for a specific race */ /** Get driver registration status for a specific race */

View File

@@ -0,0 +1,24 @@
import type { HomeViewData } from '@/templates/HomeTemplate';
import type { HomeDataDTO } from '@/lib/types/dtos/HomeDataDTO';
/**
* HomeViewDataBuilder
*
* Transforms HomeDataDTO to HomeViewData.
*/
export class HomeViewDataBuilder {
/**
* Build HomeViewData from HomeDataDTO
*
* @param apiDto - The API DTO
* @returns HomeViewData
*/
static build(apiDto: HomeDataDTO): HomeViewData {
return {
isAlpha: apiDto.isAlpha,
upcomingRaces: apiDto.upcomingRaces,
topLeagues: apiDto.topLeagues,
teams: apiDto.teams,
};
}
}

View File

@@ -30,8 +30,10 @@ export function getWebsiteApiBaseUrl(): string {
); );
} }
const isDocker = process.env.DOCKER === 'true';
const fallback = const fallback =
process.env.NODE_ENV === 'development' process.env.NODE_ENV === 'development' && !isDocker
? 'http://localhost:3001' ? 'http://localhost:3001'
: 'http://api:3000'; : 'http://api:3000';

View File

@@ -9,6 +9,8 @@
* Client: Reads from session context or provides mock implementation * Client: Reads from session context or provides mock implementation
*/ */
import { getWebsiteApiBaseUrl } from '../config/apiBaseUrl';
// Server-side implementation // Server-side implementation
export class FeatureFlagService { export class FeatureFlagService {
private flags: Set<string>; private flags: Set<string>;
@@ -41,11 +43,10 @@ export class FeatureFlagService {
/** /**
* Factory method to create service by fetching from API * Factory method to create service by fetching from API
* Fetches from ${NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'}/features
* On error, returns empty flags (secure by default) * On error, returns empty flags (secure by default)
*/ */
static async fromAPI(): Promise<FeatureFlagService> { static async fromAPI(): Promise<FeatureFlagService> {
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; const baseUrl = getWebsiteApiBaseUrl();
const url = `${baseUrl}/features`; const url = `${baseUrl}/features`;
try { try {

View File

@@ -0,0 +1,50 @@
import { Result } from '@/lib/contracts/Result';
import { HomeService } from '@/lib/services/home/HomeService';
import type { HomeViewData } from '@/templates/HomeTemplate';
import type { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { HomeViewDataBuilder } from '@/lib/builders/view-data/HomeViewDataBuilder';
/**
* HomePageQuery
*
* Server-side data fetcher for the home page.
* Returns Result<HomeViewData, string>
*/
export class HomePageQuery implements PageQuery<HomeViewData, void, string> {
/**
* Execute the home page query
*
* @returns Result with HomeViewData or error
*/
async execute(): Promise<Result<HomeViewData, string>> {
try {
const service = new HomeService();
const result = await service.getHomeData();
if (result.isErr()) {
return Result.err('Error');
}
const viewData = HomeViewDataBuilder.build(result.unwrap());
return Result.ok(viewData);
} catch (error) {
console.error('HomePageQuery failed:', error);
return Result.err('Error');
}
}
/**
* Static execute for convenience
*/
static async execute(): Promise<Result<HomeViewData, string>> {
return new HomePageQuery().execute();
}
/**
* Check if user should be redirected to dashboard
*/
static async shouldRedirectToDashboard(): Promise<boolean> {
const service = new HomeService();
return service.shouldRedirectToDashboard();
}
}

View File

@@ -42,7 +42,10 @@ export interface ParsedWizardParams {
} }
export class SearchParamParser { export class SearchParamParser {
private static getParam(params: URLSearchParams | Record<string, string | string[] | undefined>, key: string): string | null { private static getParam(params: URLSearchParams | Record<string, string | string[] | undefined> | undefined | null, key: string): string | null {
if (!params) {
return null;
}
if (params instanceof URLSearchParams) { if (params instanceof URLSearchParams) {
return params.get(key); return params.get(key);
} }
@@ -54,7 +57,7 @@ export class SearchParamParser {
} }
// Parse auth parameters // Parse auth parameters
static parseAuth(params: URLSearchParams | Record<string, string | string[] | undefined>): Result<ParsedAuthParams, string> { static parseAuth(params: URLSearchParams | Record<string, string | string[] | undefined> | undefined | null): Result<ParsedAuthParams, string> {
const errors: string[] = []; const errors: string[] = [];
const returnTo = this.getParam(params, 'returnTo'); const returnTo = this.getParam(params, 'returnTo');
@@ -95,7 +98,7 @@ export class SearchParamParser {
} }
// Parse sponsor parameters // Parse sponsor parameters
static parseSponsor(params: URLSearchParams | Record<string, string | string[] | undefined>): Result<ParsedSponsorParams, string> { static parseSponsor(params: URLSearchParams | Record<string, string | string[] | undefined> | undefined | null): Result<ParsedSponsorParams, string> {
const errors: string[] = []; const errors: string[] = [];
const type = this.getParam(params, 'type'); const type = this.getParam(params, 'type');
@@ -117,7 +120,7 @@ export class SearchParamParser {
} }
// Parse pagination parameters // Parse pagination parameters
static parsePagination(params: URLSearchParams | Record<string, string | string[] | undefined>): Result<ParsedPaginationParams, string> { static parsePagination(params: URLSearchParams | Record<string, string | string[] | undefined> | undefined | null): Result<ParsedPaginationParams, string> {
const result: ParsedPaginationParams = {}; const result: ParsedPaginationParams = {};
const errors: string[] = []; const errors: string[] = [];
@@ -157,7 +160,7 @@ export class SearchParamParser {
} }
// Parse sorting parameters // Parse sorting parameters
static parseSorting(params: URLSearchParams | Record<string, string | string[] | undefined>): Result<ParsedSortingParams, string> { static parseSorting(params: URLSearchParams | Record<string, string | string[] | undefined> | undefined | null): Result<ParsedSortingParams, string> {
const errors: string[] = []; const errors: string[] = [];
const order = this.getParam(params, 'order'); const order = this.getParam(params, 'order');
@@ -179,7 +182,7 @@ export class SearchParamParser {
} }
// Parse filter parameters // Parse filter parameters
static parseFilters(params: URLSearchParams | Record<string, string | string[] | undefined>): Result<ParsedFilterParams, string> { static parseFilters(params: URLSearchParams | Record<string, string | string[] | undefined> | undefined | null): Result<ParsedFilterParams, string> {
return Result.ok({ return Result.ok({
status: this.getParam(params, 'status'), status: this.getParam(params, 'status'),
role: this.getParam(params, 'role'), role: this.getParam(params, 'role'),
@@ -188,14 +191,14 @@ export class SearchParamParser {
} }
// Parse wizard parameters // Parse wizard parameters
static parseWizard(params: URLSearchParams | Record<string, string | string[] | undefined>): Result<ParsedWizardParams, string> { static parseWizard(params: URLSearchParams | Record<string, string | string[] | undefined> | undefined | null): Result<ParsedWizardParams, string> {
return Result.ok({ return Result.ok({
step: this.getParam(params, 'step'), step: this.getParam(params, 'step'),
}); });
} }
// Parse all parameters at once // Parse all parameters at once
static parseAll(params: URLSearchParams | Record<string, string | string[] | undefined>): Result< static parseAll(params: URLSearchParams | Record<string, string | string[] | undefined> | undefined | null): Result<
{ {
auth: ParsedAuthParams; auth: ParsedAuthParams;
sponsor: ParsedSponsorParams; sponsor: ParsedSponsorParams;

View File

@@ -14,11 +14,17 @@ import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorR
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl'; import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
// DTO types // DTO types
import type { HomeViewData } from '@/templates/HomeTemplate';
import { Result } from '@/lib/contracts/Result'; import { Result } from '@/lib/contracts/Result';
import type { Service } from '@/lib/contracts/services/Service';
import type { HomeDataDTO } from '@/lib/types/dtos/HomeDataDTO';
export class HomeService { /**
async getHomeData(): Promise<Result<HomeViewData, Error>> { * HomeService
*
* @server-safe
*/
export class HomeService implements Service {
async getHomeData(): Promise<Result<HomeDataDTO, Error>> {
try { try {
// Manual wiring: construct dependencies explicitly // Manual wiring: construct dependencies explicitly
const baseUrl = getWebsiteApiBaseUrl(); const baseUrl = getWebsiteApiBaseUrl();

View File

@@ -0,0 +1,20 @@
export interface HomeDataDTO {
isAlpha: boolean;
upcomingRaces: Array<{
id: string;
track: string;
car: string;
formattedDate: string;
}>;
topLeagues: Array<{
id: string;
name: string;
description: string;
}>;
teams: Array<{
id: string;
name: string;
description: string;
logoUrl?: string;
}>;
}

View File

@@ -3,6 +3,10 @@ module.exports = {
content: [ content: [
'./app/**/*.{js,ts,jsx,tsx,mdx}', './app/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}', './components/**/*.{js,ts,jsx,tsx,mdx}',
'./templates/**/*.{js,ts,jsx,tsx,mdx}',
'./ui/**/*.{js,ts,jsx,tsx,mdx}',
'./lib/**/*.{js,ts,jsx,tsx,mdx}',
'./hooks/**/*.{js,ts,jsx,tsx,mdx}',
], ],
theme: { theme: {
extend: { extend: {

View File

@@ -1,31 +1,30 @@
'use client'; 'use client';
import React from 'react';
import { LandingHero } from '@/components/landing/LandingHero';
import { AlternatingSection } from '@/components/landing/AlternatingSection'; import { AlternatingSection } from '@/components/landing/AlternatingSection';
import { FeatureGrid } from '@/components/landing/FeatureGrid';
import { DiscordCTA } from '@/ui/DiscordCTA';
import { FAQ } from '@/components/landing/FAQ'; import { FAQ } from '@/components/landing/FAQ';
import { Footer } from '@/ui/Footer'; import { FeatureGrid } from '@/components/landing/FeatureGrid';
import { LandingHero } from '@/components/landing/LandingHero';
import { FeatureItem, ResultItem, StepItem } from '@/components/landing/LandingItems';
import { CareerProgressionMockup } from '@/components/mockups/CareerProgressionMockup'; import { CareerProgressionMockup } from '@/components/mockups/CareerProgressionMockup';
import { RaceHistoryMockup } from '@/components/mockups/RaceHistoryMockup';
import { CompanionAutomationMockup } from '@/components/mockups/CompanionAutomationMockup'; import { CompanionAutomationMockup } from '@/components/mockups/CompanionAutomationMockup';
import { RaceHistoryMockup } from '@/components/mockups/RaceHistoryMockup';
import { SimPlatformMockup } from '@/components/mockups/SimPlatformMockup'; import { SimPlatformMockup } from '@/components/mockups/SimPlatformMockup';
import { Card } from '@/ui/Card'; import { ModeGuard } from '@/components/shared/ModeGuard';
import { Button } from '@/ui/Button'; import { routes } from '@/lib/routing/RouteConfig';
import { getMediaUrl } from '@/lib/utilities/media';
import { Box } from '@/ui/Box'; import { Box } from '@/ui/Box';
import { Stack } from '@/ui/Stack'; import { Button } from '@/ui/Button';
import { Text } from '@/ui/Text'; import { Card } from '@/ui/Card';
import { Container } from '@/ui/Container';
import { DiscordCTA } from '@/ui/DiscordCTA';
import { Footer } from '@/ui/Footer';
import { Grid } from '@/ui/Grid';
import { Heading } from '@/ui/Heading'; import { Heading } from '@/ui/Heading';
import { Image } from '@/ui/Image'; import { Image } from '@/ui/Image';
import { Link } from '@/ui/Link'; import { Link } from '@/ui/Link';
import { Container } from '@/ui/Container'; import { Stack } from '@/ui/Stack';
import { Grid } from '@/ui/Grid';
import { Surface } from '@/ui/Surface'; import { Surface } from '@/ui/Surface';
import { getMediaUrl } from '@/lib/utilities/media'; import { Text } from '@/ui/Text';
import { routes } from '@/lib/routing/RouteConfig';
import { FeatureItem, ResultItem, StepItem } from '@/components/landing/LandingItems';
import { ModeGuard } from '@/components/shared/ModeGuard';
export interface HomeViewData { export interface HomeViewData {
isAlpha: boolean; isAlpha: boolean;

View File

@@ -89,6 +89,8 @@ export interface BoxProps<T extends ElementType> {
group?: boolean; group?: boolean;
groupHoverBorderColor?: string; groupHoverBorderColor?: string;
groupHoverTextColor?: string; groupHoverTextColor?: string;
groupHoverScale?: boolean;
groupHoverOpacity?: number;
fontSize?: string; fontSize?: string;
transform?: string; transform?: string;
borderWidth?: string; borderWidth?: string;
@@ -111,6 +113,8 @@ export interface BoxProps<T extends ElementType> {
webkitMaskImage?: string; webkitMaskImage?: string;
backgroundSize?: string; backgroundSize?: string;
backgroundPosition?: string; backgroundPosition?: string;
backgroundColor?: string;
insetY?: Spacing | string;
} }
type ResponsiveValue<T> = { type ResponsiveValue<T> = {
@@ -136,6 +140,7 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
flexDirection, flexDirection,
alignItems, alignItems,
justifyContent, justifyContent,
flexWrap,
position, position,
top, top,
bottom, bottom,
@@ -183,6 +188,8 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
group, group,
groupHoverBorderColor, groupHoverBorderColor,
groupHoverTextColor, groupHoverTextColor,
groupHoverScale,
groupHoverOpacity,
fontSize, fontSize,
transform, transform,
borderWidth, borderWidth,
@@ -204,6 +211,8 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
webkitMaskImage, webkitMaskImage,
backgroundSize, backgroundSize,
backgroundPosition, backgroundPosition,
backgroundColor,
insetY,
...props ...props
}: BoxProps<T> & ComponentPropsWithoutRef<T>, }: BoxProps<T> & ComponentPropsWithoutRef<T>,
ref: ForwardedRef<HTMLElement> ref: ForwardedRef<HTMLElement>
@@ -321,18 +330,20 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
borderRight ? 'border-r' : '', borderRight ? 'border-r' : '',
borderColor ? borderColor : '', borderColor ? borderColor : '',
ring ? ring : '', ring ? ring : '',
bg ? bg : '', bg ? bg : (backgroundColor ? (backgroundColor.startsWith('bg-') ? backgroundColor : `bg-${backgroundColor}`) : ''),
color ? color : '', color ? color : '',
hoverColor ? `hover:${hoverColor}` : '', hoverColor ? `hover:${hoverColor}` : '',
shadow ? shadow : '', shadow ? shadow : '',
flexShrink !== undefined ? `flex-shrink-${flexShrink}` : '', flexShrink !== undefined ? `flex-shrink-${flexShrink}` : '',
flexGrow !== undefined ? `flex-grow-${flexGrow}` : '', flexGrow !== undefined ? `flex-grow-${flexGrow}` : '',
flexWrap ? `flex-${flexWrap}` : '',
hoverBorderColor ? `hover:${hoverBorderColor}` : '', hoverBorderColor ? `hover:${hoverBorderColor}` : '',
hoverTextColor ? `hover:${hoverTextColor}` : '', hoverTextColor ? `hover:${hoverTextColor}` : '',
hoverBg ? `hover:${hoverBg}` : '', hoverBg ? `hover:${hoverBg}` : '',
transition ? 'transition-all' : '', transition ? 'transition-all' : '',
lineClamp ? `line-clamp-${lineClamp}` : '', lineClamp ? `line-clamp-${lineClamp}` : '',
inset ? `inset-${inset}` : '', inset ? `inset-${inset}` : '',
insetY !== undefined && spacingMap[insetY as string | number] ? `inset-y-${spacingMap[insetY as string | number]}` : '',
bgOpacity !== undefined ? `bg-opacity-${bgOpacity * 100}` : '', bgOpacity !== undefined ? `bg-opacity-${bgOpacity * 100}` : '',
opacity !== undefined ? `opacity-${opacity * 100}` : '', opacity !== undefined ? `opacity-${opacity * 100}` : '',
blur ? `blur-${blur}` : '', blur ? `blur-${blur}` : '',
@@ -343,6 +354,8 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
group ? 'group' : '', group ? 'group' : '',
groupHoverBorderColor ? `group-hover:border-${groupHoverBorderColor}` : '', groupHoverBorderColor ? `group-hover:border-${groupHoverBorderColor}` : '',
groupHoverTextColor ? `group-hover:text-${groupHoverTextColor}` : '', groupHoverTextColor ? `group-hover:text-${groupHoverTextColor}` : '',
groupHoverScale ? 'group-hover:scale-[1.02]' : '',
groupHoverOpacity !== undefined ? `group-hover:opacity-${groupHoverOpacity * 100}` : '',
getResponsiveClasses('', display), getResponsiveClasses('', display),
getFlexDirectionClass(flexDirection), getFlexDirectionClass(flexDirection),
getAlignItemsClass(alignItems), getAlignItemsClass(alignItems),
@@ -390,6 +403,7 @@ export const Box = forwardRef(<T extends ElementType = 'div'>(
...(bottom !== undefined && !spacingMap[bottom as string | number] ? { bottom } : {}), ...(bottom !== undefined && !spacingMap[bottom as string | number] ? { bottom } : {}),
...(left !== undefined && !spacingMap[left as string | number] ? { left } : {}), ...(left !== undefined && !spacingMap[left as string | number] ? { left } : {}),
...(right !== undefined && !spacingMap[right as string | number] ? { right } : {}), ...(right !== undefined && !spacingMap[right as string | number] ? { right } : {}),
...(insetY !== undefined && !spacingMap[insetY as string | number] ? { top: insetY, bottom: insetY } : {}),
...(hideScrollbar ? { scrollbarWidth: 'none', msOverflowStyle: 'none', '&::-webkit-scrollbar': { display: 'none' } } : {}), ...(hideScrollbar ? { scrollbarWidth: 'none', msOverflowStyle: 'none', '&::-webkit-scrollbar': { display: 'none' } } : {}),
...((props as Record<string, unknown>).style as object || {}) ...((props as Record<string, unknown>).style as object || {})
}; };

View File

@@ -1,5 +1,5 @@
import React, { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react'; import React, { ReactNode, ElementType, ComponentPropsWithoutRef } from 'react';
import { BoxProps } from './Box'; import { Box, BoxProps } from './Box';
type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96; type Spacing = 0 | 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 3.5 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40 | 44 | 48 | 52 | 56 | 60 | 64 | 72 | 80 | 96;
@@ -194,5 +194,5 @@ export function Text<T extends ElementType = 'span'>({
...style ...style
}; };
return <Tag className={classes} style={combinedStyle} {...props}>{children}</Tag>; return <Box as={Tag} className={classes} style={combinedStyle} {...props}>{children}</Box>;
} }

View File

@@ -76,6 +76,7 @@ services:
- NEXT_TELEMETRY_DISABLED=1 - NEXT_TELEMETRY_DISABLED=1
- NODE_ENV=development - NODE_ENV=development
- API_BASE_URL=http://api:3000 - API_BASE_URL=http://api:3000
- DOCKER=true
ports: ports:
- "3000:3000" - "3000:3000"
volumes: volumes: