diff --git a/.env.local.example b/.env.local.example old mode 100644 new mode 100755 index bde281e..1a7bfd0 --- a/.env.local.example +++ b/.env.local.example @@ -1,3 +1,3 @@ -NEXT_PUBLIC_API_BASE=https://acc-api.jurmanovic.com/v1/api -SERVER_ID=2ea19460-f3b6-47ae-a667-c26876425268 -ACCESS_TOKEN=EB41048A021F7A7707ED4E5EF37052C401F9ACD5DCDBC94BF16D0456FAAF111A +VITE_API_BASE=https://acc-api.jurmanovic.com/v1/api +VITE_SERVER_ID=your-server-id-here +VITE_ACCESS_TOKEN=your-access-token-here diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml old mode 100644 new mode 100755 index 8d706e2..83d8602 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -12,6 +12,7 @@ jobs: uses: https://github.com/actions/setup-node@v4 with: node-version: 20 + cache: 'npm' - name: Install dependencies run: npm ci @@ -19,13 +20,13 @@ jobs: - name: Build run: npm run build env: - NEXT_PUBLIC_API_BASE: ${{ secrets.API_BASE }} - NEXT_PUBLIC_SERVER_ID: ${{ secrets.SERVER_ID }} - NEXT_PUBLIC_ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} + VITE_API_BASE: ${{ secrets.API_BASE }} + VITE_SERVER_ID: ${{ secrets.SERVER_ID }} + VITE_ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} - name: Publish to Cloudflare uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy ./out --project-name=grid-gladiators-leaderboard + command: pages deploy ./dist --project-name=grid-gladiators-leaderboard diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index d62bd09..98ab94f --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,8 @@ node_modules/ npm-debug.log yarn-error.log -# Next.js +# Build output +dist/ .next/ out/ diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..ad7c96e --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,24 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; + +export default tseslint.config( + { ignores: ['dist', 'node_modules'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], + }, + }, +); diff --git a/index.html b/index.html new file mode 100644 index 0000000..2aaa62f --- /dev/null +++ b/index.html @@ -0,0 +1,17 @@ + + +
+ + + +Loading leaderboard...
+{error}
+ +| Pos | -Color | -Driver | -Initials | -Points | +Pos | +Color | +Driver | +Initials | +Points |
|---|---|---|---|---|---|---|---|---|---|
| {pos} | - + | {driver.name} | {driver.initials} | diff --git a/src/app/components/PointsKey.tsx b/src/components/PointsKey.tsx similarity index 72% rename from src/app/components/PointsKey.tsx rename to src/components/PointsKey.tsx index a4a6702..7287f31 100644 --- a/src/app/components/PointsKey.tsx +++ b/src/components/PointsKey.tsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { PointStyle } from '@/lib/types'; interface PointsKeyProps { @@ -8,23 +9,27 @@ interface PointsKeyProps { } export default function PointsKey({ pointsTable, flPoints, flColor, flTextColor }: PointsKeyProps) { - const sortedPoints = [...pointsTable].sort((a, b) => (a.priority || 99) - (b.priority || 99)); + const sortedPoints = useMemo( + () => [...pointsTable].sort((a, b) => (a.priority || 99) - (b.priority || 99)), + [pointsTable] + ); return (
| Points | +|||
|---|---|---|---|
|
@@ -36,13 +41,13 @@ export default function PointsKey({ pointsTable, flPoints, flColor, flTextColor
))}
- |
diff --git a/src/app/components/RaceResultsTable.tsx b/src/components/RaceResultsTable.tsx
similarity index 66%
rename from src/app/components/RaceResultsTable.tsx
rename to src/components/RaceResultsTable.tsx
index a1a8259..69a2f92 100644
--- a/src/app/components/RaceResultsTable.tsx
+++ b/src/components/RaceResultsTable.tsx
@@ -1,11 +1,32 @@
-import { AppData } from "@/lib/types";
-import { parsePoints, findStyleByPoints } from "@/lib/leaderboard";
+import { useMemo } from 'react';
+import { AppData } from '@/lib/types';
+import { parsePoints, findStyleByPoints } from '@/lib/leaderboard';
interface RaceResultsTableProps {
appData: AppData;
}
export default function RaceResultsTable({ appData }: RaceResultsTableProps) {
+ const driverById = useMemo(() => {
+ const map = new Map
+
FL (+{flPoints})
{driver.name}
@@ -36,28 +57,20 @@ export default function RaceResultsTable({ appData }: RaceResultsTableProps) {
{track.name}
|
- {flDriver?.initials || ""}
+ {flDriver?.initials || ''}
|
{appData.drivers.map((driver) => {
- const raceResult = track.results.find(
- (r) => r.driverId === driver.id
- );
- let result = raceResult?.score;
- // DNS fallback
- if (result === undefined || result === null || result === "") {
- result = "DNS";
- }
+ const driverMap = resultsByTrackAndDriver.get(track.id);
+ const score = driverMap?.get(driver.id);
+ const result = score === null || score === undefined ? 'DNS' : score;
const pointsNum = parsePoints(result);
const style = findStyleByPoints(appData, pointsNum);
@@ -66,9 +79,10 @@ export default function RaceResultsTable({ appData }: RaceResultsTableProps) {
color: style.textColor,
};
- let hasFL = false;
- if (track.fastestLapDriverId === driver.id && appData.flPoints) {
- hasFL = true;
+ const hasFL =
+ track.fastestLapDriverId === driver.id && appData.flPoints;
+
+ if (hasFL) {
const flColor = appData.flColor;
if (pointsNum === 0) {
cellStyles = {
@@ -86,17 +100,17 @@ export default function RaceResultsTable({ appData }: RaceResultsTableProps) {
if (hasFL) {
cellStyles = {
...cellStyles,
- display: "flex",
- justifyContent: "space-between",
- alignItems: "center",
- flexDirection: "row",
- paddingLeft: "0.75rem",
- paddingRight: "0.75rem",
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ flexDirection: 'row',
+ paddingLeft: '0.75rem',
+ paddingRight: '0.75rem',
};
} else {
cellStyles = {
...cellStyles,
- textAlign: "center",
+ textAlign: 'center',
};
}
@@ -105,10 +119,11 @@ export default function RaceResultsTable({ appData }: RaceResultsTableProps) {
key={driver.id}
className="py-3 px-4 font-mono"
style={cellStyles}
+ title={`${result} - ${style.label}`}
>
{hasFL ? (
<>
-
+
{result}
{
- const API_BASE = process.env.NEXT_PUBLIC_API_BASE || '';
- const SERVER_ID = process.env.NEXT_PUBLIC_SERVER_ID || '';
- const ACCESS_TOKEN = process.env.NEXT_PUBLIC_ACCESS_TOKEN || '';
+ const API_BASE = import.meta.env.VITE_API_BASE || '';
+ const SERVER_ID = import.meta.env.VITE_SERVER_ID || '';
+ const ACCESS_TOKEN = import.meta.env.VITE_ACCESS_TOKEN || '';
if (!API_BASE || !SERVER_ID || !ACCESS_TOKEN) {
- console.warn('Missing API configuration');
- return null;
+ console.warn('Missing API configuration, using default data');
+ return defaultAppData;
}
try {
const res = await fetch(`${API_BASE}/server/${SERVER_ID}/leaderboard`, {
headers: {
- "Access-Key": ACCESS_TOKEN
- }
+ 'Access-Key': ACCESS_TOKEN,
+ },
});
- if (!res.ok) return null;
+ if (!res.ok) {
+ console.warn('API request failed, using default data');
+ return defaultAppData;
+ }
const data = await res.json();
// Treat empty leaderboard (no drivers) as null to fall back to defaults
- if (!data || !data.drivers || data.drivers.length === 0) return null;
+ if (!data || !data.drivers || data.drivers.length === 0) {
+ console.warn('Empty API response, using default data');
+ return defaultAppData;
+ }
return data;
} catch (e) {
- console.error("Failed to load leaderboard from API:", e);
- return null;
+ console.error('Failed to load leaderboard from API:', e);
+ return defaultAppData;
}
}
diff --git a/src/lib/defaultData.ts b/src/lib/defaultData.ts
old mode 100644
new mode 100755
diff --git a/src/lib/leaderboard.ts b/src/lib/leaderboard.ts
old mode 100644
new mode 100755
index f22b291..757ec14
--- a/src/lib/leaderboard.ts
+++ b/src/lib/leaderboard.ts
@@ -1,8 +1,12 @@
-import { AppData, PointStyle, DriverStanding, RaceHistoryEntry } from "./types";
+import { AppData, PointStyle, DriverStanding, RaceHistoryEntry } from './types';
-export function parsePoints(value: any): number {
- const num = parseInt(value, 10);
- return isNaN(num) ? 0 : num;
+export function parsePoints(value: number | string | null): number {
+ if (typeof value === 'number') return value;
+ if (typeof value === 'string') {
+ const num = parseInt(value, 10);
+ return isNaN(num) ? 0 : num;
+ }
+ return 0;
}
export function findStyleByPoints(
@@ -11,34 +15,44 @@ export function findStyleByPoints(
): PointStyle {
return (
appData.pointsTable.find((p) => p.points === points) || {
- id: "unknown",
+ id: 'unknown',
points: 0,
- label: "???",
- color: "#374151",
- textColor: "#e5e7eb",
+ label: '???',
+ color: '#374151',
+ textColor: '#e5e7eb',
priority: 99,
}
);
}
export function calculateStandingsData(appData: AppData): DriverStanding[] {
+ const resultsByTrackAndDriver = new Map<
+ string,
+ Map |