diff --git a/biome.json b/biome.json index 033c04c..a77b9b6 100644 --- a/biome.json +++ b/biome.json @@ -1,35 +1,36 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.5/schema.json", - "vcs": { - "enabled": false, - "clientKind": "git", - "useIgnoreFile": false - }, - "files": { - "ignoreUnknown": false, - "includes": [ - "**/src/**/*", - "**/.vscode/**/*", - "**/index.html", - "**/vite.config.ts", - "!**/src/routeTree.gen.ts", - "!**/src/styles.css" - ] - }, - "formatter": { - "enabled": true, - "indentStyle": "tab" - }, - "assist": { "actions": { "source": { "organizeImports": "on" } } }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double" - } - } + "$schema": "https://biomejs.dev/schemas/2.4.5/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "includes": [ + "**/src/**/*", + "**/.vscode/**/*", + "**/index.html", + "**/vite.config.ts", + "!**/src/routeTree.gen.ts", + "!**/src/styles.css" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } } diff --git a/index.html b/index.html index da64509..d088c69 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ By Other Means Control Timer -
+
diff --git a/package-lock.json b/package-lock.json index ff98876..015a195 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,21 @@ "vite": "^8.0.0" } }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/compat-data": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", @@ -49,6 +64,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -74,21 +90,6 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/generator": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", @@ -250,21 +251,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/template/node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/traverse": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", @@ -284,21 +270,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/types": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", @@ -476,6 +447,39 @@ "node": ">=14.21.3" } }, + "node_modules/@emnapi/core": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", + "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "2.0.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", + "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", + "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1494,47 +1498,6 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "inBundle": true, - "license": "0BSD", - "optional": true - }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", diff --git a/package.json b/package.json index 903a8c3..dc0e5f5 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "#/*": "./src/*" }, "scripts": { - "dev": "vite dev --port 3000", + "dev": "vite dev", "build": "vite build", "preview": "vite preview", "format": "biome format --write .", diff --git a/src/components/Phase.tsx b/src/components/Phase.tsx new file mode 100644 index 0000000..177bef7 --- /dev/null +++ b/src/components/Phase.tsx @@ -0,0 +1,49 @@ +import type { FC } from "react"; +import { useAppDispatch, useAppSelector } from "../store/hooks"; +import { + clearSelectedPhase, + setSelectedPhase, +} from "../store/slices/phaseSlice"; +import type { PhaseType } from "../utils/types"; + +interface PropType { + phase: PhaseType; + current: boolean; +} + +const Phase: FC = ({ phase, current }) => { + const dispatch = useAppDispatch(); + const selected = useAppSelector((state) => state.phase.selected); + const thisSelected = selected === phase.name; + + const phaseClick = () => { + if (thisSelected) { + dispatch(clearSelectedPhase()); + } else { + dispatch(setSelectedPhase(phase.name)); + } + }; + + return ( + + ); +}; + +export default Phase; diff --git a/src/main.tsx b/src/main.tsx index 2946a1d..f4ffc58 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -11,14 +11,14 @@ const router = getRouter(); const rootElement = document.getElementById("app"); if (!rootElement) - throw new Error("Could not find #app element to mount the app"); + throw new Error("Could not find #app element to mount the app"); createRoot(rootElement).render( - - - - - - - , + + + + + + + , ); diff --git a/src/pages/Welcome.tsx b/src/pages/Welcome.tsx index d79f588..1d84445 100644 --- a/src/pages/Welcome.tsx +++ b/src/pages/Welcome.tsx @@ -1,70 +1,82 @@ import { DateTime } from "luxon"; -import { - type EventHandler, - type FC, - type MouseEventHandler, - useEffect, - useState, -} from "react"; +import { type FC, type MouseEventHandler, useEffect, useState } from "react"; +import Phase from "../components/Phase"; import { useAppDispatch, useAppSelector } from "../store/hooks"; import { clearStartTime, setStartTime } from "../store/slices/timeSlice"; -import { ROUND_LENGTH_SECONDS } from "../utils/constants"; -import { formatElapsed } from "../utils/functions"; +import { PHASES, ROUND_LENGTH_SECONDS } from "../utils/constants"; +import { + formatElapsed, + getCurrentPhaseIndex, + getElapsedSeconds, +} from "../utils/functions"; const Welcome: FC = () => { - const dispatch = useAppDispatch(); - const startTime = useAppSelector((state) => state.time.startTime); - const [now, setNow] = useState(() => DateTime.now()); + const dispatch = useAppDispatch(); + const startTime = useAppSelector((state) => state.time.startTime); + const [now, setNow] = useState(() => DateTime.now()); - useEffect(() => { - if (!startTime) return; - const interval = setInterval(() => setNow(DateTime.now()), 1000); - return () => clearInterval(interval); - }, [startTime]); + useEffect(() => { + if (!startTime) return; + const interval = setInterval(() => setNow(DateTime.now()), 1000); + return () => clearInterval(interval); + }, [startTime]); - const elapsedSeconds = startTime - ? now.diff(DateTime.fromISO(startTime), "seconds").seconds - : 0; - const isRunning = startTime !== null; - const isOvertime = elapsedSeconds > 60; + const elapsedSeconds = getElapsedSeconds(startTime, now); + const isRunning = startTime !== null; + const isOvertime = elapsedSeconds > ROUND_LENGTH_SECONDS; - const nextRoundClick: MouseEventHandler = (_e) => { - dispatch(clearStartTime()); - }; + const currentPhaseIndex = getCurrentPhaseIndex( + PHASES, + elapsedSeconds, + isRunning, + ); - const startRoundClick: MouseEventHandler = (_e) => { - dispatch(setStartTime(DateTime.now().toISO() ?? "")); - }; + const nextRoundClick: MouseEventHandler = (_e) => { + dispatch(clearStartTime()); + }; - return ( -
-

By Other Means Control Timer

+ const startRoundClick: MouseEventHandler = (_e) => { + dispatch(setStartTime(DateTime.now().toISO() ?? "")); + }; -
- {formatElapsed(elapsedSeconds)} -
+ return ( +
+

By Other Means Control Timer

- {isRunning ? ( - - ) : ( - - )} -
- ); +
+ {formatElapsed(elapsedSeconds)} +
+ + {isRunning ? ( + + ) : ( + + )} + {PHASES.map((phase, index) => { + return ( + + ); + })} +
+ ); }; export default Welcome; diff --git a/src/router.tsx b/src/router.tsx index 0b1d0fb..8ceb1ca 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -2,18 +2,18 @@ import { createRouter as createTanStackRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen"; export function getRouter() { - const router = createTanStackRouter({ - routeTree, - scrollRestoration: true, - defaultPreload: "intent", - defaultPreloadStaleTime: 0, - }); + const router = createTanStackRouter({ + routeTree, + scrollRestoration: true, + defaultPreload: "intent", + defaultPreloadStaleTime: 0, + }); - return router; + return router; } declare module "@tanstack/react-router" { - interface Register { - router: ReturnType; - } + interface Register { + router: ReturnType; + } } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 8f4cfc2..e3ed382 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -3,26 +3,26 @@ import { createRootRoute, Outlet } from "@tanstack/react-router"; import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; export const Route = createRootRoute({ - component: RootComponent, + component: RootComponent, }); function RootComponent() { - return ( - <> - - {import.meta.env.VITE_TANSTACK_DEVTOOLS === "true" && ( - , - }, - ]} - /> - )} - - ); + return ( + <> + + {import.meta.env.VITE_TANSTACK_DEVTOOLS === "true" && ( + , + }, + ]} + /> + )} + + ); } diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 9c360e2..fd609e1 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -2,9 +2,9 @@ import { createFileRoute } from "@tanstack/react-router"; import Welcome from "../pages/Welcome"; export const Route = createFileRoute("/")({ - component: RouteComponent, + component: RouteComponent, }); function RouteComponent() { - return ; + return ; } diff --git a/src/store/slices/phaseSlice.ts b/src/store/slices/phaseSlice.ts new file mode 100644 index 0000000..61567db --- /dev/null +++ b/src/store/slices/phaseSlice.ts @@ -0,0 +1,25 @@ +import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; + +interface StateType { + selected: string | null; +} + +const initialState: StateType = { + selected: null, +}; + +const phaseSlice = createSlice({ + name: "phase", + initialState, + reducers: { + setSelectedPhase: (state, action: PayloadAction) => { + state.selected = action.payload; + }, + clearSelectedPhase: (state) => { + state.selected = null; + }, + }, +}); + +export const { setSelectedPhase, clearSelectedPhase } = phaseSlice.actions; +export default phaseSlice.reducer; diff --git a/src/store/slices/timeSlice.ts b/src/store/slices/timeSlice.ts index 5fc51c7..8176229 100644 --- a/src/store/slices/timeSlice.ts +++ b/src/store/slices/timeSlice.ts @@ -1,24 +1,24 @@ import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; interface StateType { - startTime: string | null; + startTime: string | null; } const initialState: StateType = { - startTime: null, + startTime: null, }; const timeSlice = createSlice({ - name: "time", - initialState, - reducers: { - setStartTime: (state, action: PayloadAction) => { - state.startTime = action.payload; - }, - clearStartTime: (state) => { - state.startTime = null; - }, - }, + name: "time", + initialState, + reducers: { + setStartTime: (state, action: PayloadAction) => { + state.startTime = action.payload; + }, + clearStartTime: (state) => { + state.startTime = null; + }, + }, }); export const { setStartTime, clearStartTime } = timeSlice.actions; diff --git a/src/store/store.ts b/src/store/store.ts index c7274b8..faa00cf 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -1,75 +1,34 @@ import { combineReducers, configureStore } from "@reduxjs/toolkit"; import { persistReducer, persistStore } from "redux-persist"; +import storage from "redux-persist/es/storage"; +import phaseReducer from "./slices/phaseSlice"; import timeReducer from "./slices/timeSlice"; -const noopStorage = { - getItem() { - return Promise.resolve(null); - }, - setItem() { - return Promise.resolve(); - }, - removeItem() { - return Promise.resolve(); - }, -}; - -// redux-persist's bundled storage engine can throw or come back malformed in -// some browsers (private mode, locked-down storage), so this is a minimal -// storage engine that never touches `redux-persist/lib/storage`. -function createLocalStorageEngine() { - if (typeof window === "undefined") return noopStorage; - try { - const testKey = "__by_other_means_storage_test__"; - window.localStorage.setItem(testKey, testKey); - window.localStorage.removeItem(testKey); - } catch { - return noopStorage; - } - return { - getItem(key: string) { - return Promise.resolve(window.localStorage.getItem(key)); - }, - setItem(key: string, value: string) { - window.localStorage.setItem(key, value); - return Promise.resolve(); - }, - removeItem(key: string) { - window.localStorage.removeItem(key); - return Promise.resolve(); - }, - }; -} - -// Configure which slices to persist const persistConfig = { - key: "root", - storage: createLocalStorageEngine(), - whitelist: ["time"], + key: "root", + storage: storage, + whitelist: ["time", "phase"], }; -// Combine all reducers const rootReducer = combineReducers({ - time: timeReducer, + time: timeReducer, + phase: phaseReducer, }); -// Create persisted reducer const persistedReducer = persistReducer(persistConfig, rootReducer); export const store = configureStore({ - reducer: persistedReducer, - middleware: (getDefaultMiddleware) => - getDefaultMiddleware({ - serializableCheck: { - ignoredActions: ["persist/PERSIST", "persist/REHYDRATE"], - }, - }), + reducer: persistedReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: ["persist/PERSIST", "persist/REHYDRATE"], + }, + }), }); export const persistor = persistStore(store); -// Infer the `RootState`, `AppDispatch`, and `AppStore` types from the store itself export type RootState = ReturnType; -// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} export type AppDispatch = typeof store.dispatch; export type AppStore = typeof store; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 3b1ec7b..ffef20b 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,2 +1,42 @@ +import type { PhaseType } from "./types"; + const ROUND_LENGTH_MINUTES = 40; export const ROUND_LENGTH_SECONDS = ROUND_LENGTH_MINUTES * 60; + +export const PHASES: PhaseType[] = [ + { + minuteStart: 0, + minuteEnd: 15, + name: "Negotiation Phase", + description: + "Attend formal negotiations or Imperial Senate. Draft, formalise and ratify treaties. Apart from the first turn, orders from the previous turn will also be resolved during this phase.", + color: "bg-red-800", + borderColor: "border-red-600", + }, + { + minuteStart: 15, + minuteEnd: 20, + name: "Plenary Phase", + description: + "Chairs announce any ratified treaties. Facilitators announce any major map events. Teams collect income.", + color: "bg-yellow-600", + borderColor: "border-yellow-400", + }, + { + minuteStart: 20, + minuteEnd: 30, + name: "Discussion Phase", + description: "Freely circulate, discuss any topics as desired.", + color: "bg-green-800", + borderColor: "border-green-600", + }, + { + minuteStart: 30, + minuteEnd: 40, + name: "Orders Phase", + description: + "Issue orders, plan for the following turn. Remain at team tables during the Orders phase.", + color: "bg-sky-800", + borderColor: "border-sky-600", + }, +]; diff --git a/src/utils/functions.ts b/src/utils/functions.ts index d3725ed..890b952 100644 --- a/src/utils/functions.ts +++ b/src/utils/functions.ts @@ -1,6 +1,30 @@ +import { DateTime } from "luxon"; +import type { PhaseType } from "./types"; + export function formatElapsed(seconds: number): string { - const clamped = Math.max(0, Math.floor(seconds)); - const minutes = Math.floor(clamped / 60); - const secs = clamped % 60; - return `${minutes}:${secs.toString().padStart(2, "0")}`; + const clamped = Math.max(0, Math.floor(seconds)); + const minutes = Math.floor(clamped / 60); + const secs = clamped % 60; + return `${minutes}:${secs.toString().padStart(2, "0")}`; +} + +export function getElapsedSeconds( + startTime: string | null, + now: DateTime, +): number { + if (!startTime) return 0; + return now.diff(DateTime.fromISO(startTime), "seconds").seconds; +} + +export function getCurrentPhaseIndex( + phases: PhaseType[], + elapsedSeconds: number, + isRunning: boolean, +): number { + if (!isRunning) return -1; + const elapsedMinutes = elapsedSeconds / 60; + for (let i = 0; i < phases.length; i++) { + if (elapsedMinutes < phases[i].minuteEnd) return i; + } + return phases.length - 1; } diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 0000000..a5c9b5b --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,8 @@ +export interface PhaseType { + minuteStart: number; + minuteEnd: number; + name: string; + description: string; + color: string; + borderColor: string; +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 5b8e0c9..02c7461 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,5 +1,5 @@ /// interface ImportMetaEnv { - readonly VITE_TANSTACK_DEVTOOLS: string; + readonly VITE_TANSTACK_DEVTOOLS: string; } diff --git a/vite.config.ts b/vite.config.ts index 89d6385..d598a9e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,13 +5,16 @@ import viteReact from "@vitejs/plugin-react"; import { defineConfig } from "vite"; const config = defineConfig({ - resolve: { tsconfigPaths: true }, - plugins: [ - devtools(), - tailwindcss(), - tanstackRouter({ target: "react", autoCodeSplitting: true }), - viteReact(), - ], + resolve: { tsconfigPaths: true }, + plugins: [ + devtools(), + tailwindcss(), + tanstackRouter({ target: "react", autoCodeSplitting: true }), + viteReact(), + ], + server: { + port: 8000, + }, }); export default config;