From 8f5c499ee24b828b8061468c36b771a9744d8d09 Mon Sep 17 00:00:00 2001 From: Nathan Stanley Date: Wed, 29 Jul 2026 00:11:37 -0400 Subject: [PATCH] WIP: timer set --- .env.development | 2 + biome.json | 3 +- package-lock.json | 278 +++++++++++++++++++++++++++++++--- package.json | 15 +- src/pages/Welcome.tsx | 56 +++++++ src/routeTree.gen.ts | 9 ++ src/router.tsx | 26 ++-- src/routes/__root.tsx | 105 +++++++------ src/routes/index.tsx | 18 +-- src/store/hooks.ts | 6 + src/store/slices/timeSlice.ts | 25 +++ src/store/store.ts | 75 +++++++++ src/utils/constants.ts | 2 + src/utils/functions.ts | 6 + src/vite-env.d.ts | 5 + vite.config.ts | 18 +-- 16 files changed, 539 insertions(+), 110 deletions(-) create mode 100644 .env.development create mode 100644 src/pages/Welcome.tsx create mode 100644 src/store/hooks.ts create mode 100644 src/store/slices/timeSlice.ts create mode 100644 src/store/store.ts create mode 100644 src/utils/constants.ts create mode 100644 src/utils/functions.ts create mode 100644 src/vite-env.d.ts diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..ba0dbab --- /dev/null +++ b/.env.development @@ -0,0 +1,2 @@ + +VITE_TANSTACK_DEVTOOLS=true diff --git a/biome.json b/biome.json index 64b4d31..033c04c 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.4.5/schema.json", "vcs": { "enabled": false, "clientKind": "git", @@ -33,4 +33,3 @@ } } } - diff --git a/package-lock.json b/package-lock.json index c687dc3..37c0485 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,22 +6,29 @@ "": { "name": "by-other-means", "dependencies": { + "@reduxjs/toolkit": "^2.12.0", "@tailwindcss/vite": "^4.1.18", "@tanstack/react-devtools": "latest", "@tanstack/react-router": "latest", "@tanstack/react-router-devtools": "latest", "@tanstack/react-start": "latest", + "luxon": "^3.7.2", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-redux": "^9.3.0", + "redux-persist": "^6.0.0", "tailwindcss": "^4.1.18" }, "devDependencies": { "@biomejs/biome": "2.4.5", "@tanstack/devtools-vite": "latest", "@tanstack/router-cli": "^1.132.0", + "@types/luxon": "^3.7.2", "@types/node": "^22.10.2", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", + "@types/react-redux": "^7.1.34", + "@types/redux-persist": "^4.0.0", "@vitejs/plugin-react": "^6.0.1", "typescript": "^6.0.2", "vite": "^8.0.0" @@ -220,6 +227,16 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", @@ -456,29 +473,6 @@ "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", @@ -953,6 +947,32 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", @@ -1306,6 +1326,18 @@ "solid-js": "^1.6.12" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@tailwindcss/node": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", @@ -2255,6 +2287,26 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz", + "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-gW+Oib+vUtGJBtNC8V9Reww0oIpusw+4m81uncg9REGZAJfqOQHfo/nkabnc7w0QReXyPqjrbWMJk6NuAkiX3Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.20.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", @@ -2286,6 +2338,58 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/react-redux": { + "version": "7.1.34", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", + "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "node_modules/@types/react-redux/node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/@types/redux-persist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/redux-persist/-/redux-persist-4.0.0.tgz", + "integrity": "sha512-VyDn75G7+Xkebp5Lxowk2t7HeT4VLSy92nyoQu1dZxk+ySBBdvV0yVR/upE4z24eVFxMp6Fo3CisQpkl4v1k3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "redux": "^3.6.0" + } + }, + "node_modules/@types/redux-persist/node_modules/redux": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz", + "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.2.1", + "lodash-es": "^4.2.1", + "loose-envify": "^1.1.0", + "symbol-observable": "^1.0.3" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", @@ -2700,6 +2804,26 @@ } } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/immer": { + "version": "11.1.15", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.15.tgz", + "integrity": "sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -3040,6 +3164,33 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3049,6 +3200,15 @@ "yallist": "^3.0.2" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -3219,6 +3379,37 @@ "react": "^19.2.8" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/readdirp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", @@ -3232,6 +3423,31 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT", + "peer": true + }, + "node_modules/redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "license": "MIT", + "peerDependencies": { + "redux": ">4.0.0" + } + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3242,6 +3458,12 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, "node_modules/rolldown": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", @@ -3411,6 +3633,16 @@ "node": ">=8" } }, + "node_modules/symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/tailwindcss": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", diff --git a/package.json b/package.json index 95865e3..aaa58f9 100644 --- a/package.json +++ b/package.json @@ -10,27 +10,34 @@ "generate-routes": "tsr generate", "build": "vite build", "preview": "vite preview", - "format": "biome format", - "lint": "biome lint", - "check": "biome check" + "format": "biome format --write .", + "lint": "biome lint .", + "check": "biome check --write ." }, "dependencies": { + "@reduxjs/toolkit": "^2.12.0", "@tailwindcss/vite": "^4.1.18", "@tanstack/react-devtools": "latest", "@tanstack/react-router": "latest", "@tanstack/react-router-devtools": "latest", "@tanstack/react-start": "latest", + "luxon": "^3.7.2", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-redux": "^9.3.0", + "redux-persist": "^6.0.0", "tailwindcss": "^4.1.18" }, "devDependencies": { "@biomejs/biome": "2.4.5", "@tanstack/devtools-vite": "latest", "@tanstack/router-cli": "^1.132.0", + "@types/luxon": "^3.7.2", "@types/node": "^22.10.2", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", + "@types/react-redux": "^7.1.34", + "@types/redux-persist": "^4.0.0", "@vitejs/plugin-react": "^6.0.1", "typescript": "^6.0.2", "vite": "^8.0.0" @@ -41,4 +48,4 @@ "lightningcss" ] } -} \ No newline at end of file +} diff --git a/src/pages/Welcome.tsx b/src/pages/Welcome.tsx new file mode 100644 index 0000000..ccdb76b --- /dev/null +++ b/src/pages/Welcome.tsx @@ -0,0 +1,56 @@ +import { DateTime } from "luxon"; +import { type FC, useEffect, useState } from "react"; +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"; + +const Welcome: FC = () => { + 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]); + + const elapsedSeconds = startTime + ? now.diff(DateTime.fromISO(startTime), "seconds").seconds + : 0; + const isRunning = startTime !== null; + const isOvertime = elapsedSeconds > 60; + + return ( +
+

By Other Means Control Timer

+ +
+ {formatElapsed(elapsedSeconds)} +
+ + {isRunning ? ( + + ) : ( + + )} +
+ ); +}; + +export default Welcome; diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index d204c26..dceedff 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -57,3 +57,12 @@ const rootRouteChildren: RootRouteChildren = { export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/src/router.tsx b/src/router.tsx index e7b1c4d..0b1d0fb 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -1,19 +1,19 @@ -import { createRouter as createTanStackRouter } from '@tanstack/react-router' -import { routeTree } from './routeTree.gen' +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 - } +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index f4b63b1..6252a97 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,54 +1,63 @@ -import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router' -import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' -import { TanStackDevtools } from '@tanstack/react-devtools' +import { TanStackDevtools } from "@tanstack/react-devtools"; +import { createRootRoute, HeadContent, Scripts } from "@tanstack/react-router"; +import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; +import { Provider } from "react-redux"; +import { PersistGate } from "redux-persist/integration/react"; -import appCss from '../styles.css?url' +import { persistor, store } from "#/store/store"; +import appCss from "../styles.css?url"; export const Route = createRootRoute({ - head: () => ({ - meta: [ - { - charSet: 'utf-8', - }, - { - name: 'viewport', - content: 'width=device-width, initial-scale=1', - }, - { - title: 'TanStack Start Starter', - }, - ], - links: [ - { - rel: 'stylesheet', - href: appCss, - }, - ], - }), - shellComponent: RootDocument, -}) + head: () => ({ + meta: [ + { + charSet: "utf-8", + }, + { + name: "viewport", + content: "width=device-width, initial-scale=1", + }, + { + title: "By Other Means Control Timer", + }, + ], + links: [ + { + rel: "stylesheet", + href: appCss, + }, + ], + }), + shellComponent: RootDocument, +}); function RootDocument({ children }: { children: React.ReactNode }) { - return ( - - - - - - {children} - , - }, - ]} - /> - - - - ) + return ( + + + + + + + + {children} + + + {import.meta.env.VITE_TANSTACK_DEVTOOLS === "true" && ( + , + }, + ]} + /> + )} + + + + ); } diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 6678649..9c360e2 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,14 +1,10 @@ -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute } from "@tanstack/react-router"; +import Welcome from "../pages/Welcome"; -export const Route = createFileRoute('/')({ component: Home }) +export const Route = createFileRoute("/")({ + component: RouteComponent, +}); -function Home() { - return ( -
-

Welcome to TanStack Start

-

- Edit src/routes/index.tsx to get started. -

-
- ) +function RouteComponent() { + return ; } diff --git a/src/store/hooks.ts b/src/store/hooks.ts new file mode 100644 index 0000000..36e02b9 --- /dev/null +++ b/src/store/hooks.ts @@ -0,0 +1,6 @@ +import { useDispatch, useSelector } from "react-redux"; +import type { AppDispatch, RootState } from "./store"; + +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useAppDispatch = useDispatch.withTypes(); +export const useAppSelector = useSelector.withTypes(); diff --git a/src/store/slices/timeSlice.ts b/src/store/slices/timeSlice.ts new file mode 100644 index 0000000..5fc51c7 --- /dev/null +++ b/src/store/slices/timeSlice.ts @@ -0,0 +1,25 @@ +import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; + +interface StateType { + startTime: string | null; +} + +const initialState: StateType = { + startTime: null, +}; + +const timeSlice = createSlice({ + name: "time", + initialState, + reducers: { + setStartTime: (state, action: PayloadAction) => { + state.startTime = action.payload; + }, + clearStartTime: (state) => { + state.startTime = null; + }, + }, +}); + +export const { setStartTime, clearStartTime } = timeSlice.actions; +export default timeSlice.reducer; diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 0000000..b32fd88 --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,75 @@ +import { combineReducers, configureStore } from "@reduxjs/toolkit"; +import { persistReducer, persistStore } from "redux-persist"; +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 needs `localStorage`, which isn't available +// during server-side rendering (and can throw in locked-down browsers), 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"], +}; + +// Combine all reducers +const rootReducer = combineReducers({ + time: timeReducer, +}); + +// Create persisted reducer +const persistedReducer = persistReducer(persistConfig, rootReducer); + +export const store = configureStore({ + 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 new file mode 100644 index 0000000..3b1ec7b --- /dev/null +++ b/src/utils/constants.ts @@ -0,0 +1,2 @@ +const ROUND_LENGTH_MINUTES = 40; +export const ROUND_LENGTH_SECONDS = ROUND_LENGTH_MINUTES * 60; diff --git a/src/utils/functions.ts b/src/utils/functions.ts new file mode 100644 index 0000000..d3725ed --- /dev/null +++ b/src/utils/functions.ts @@ -0,0 +1,6 @@ +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")}`; +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..5b8e0c9 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,5 @@ +/// + +interface ImportMetaEnv { + readonly VITE_TANSTACK_DEVTOOLS: string; +} diff --git a/vite.config.ts b/vite.config.ts index b0f53d3..a92e8ff 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,14 +1,14 @@ -import { defineConfig } from 'vite' -import { devtools } from '@tanstack/devtools-vite' +import tailwindcss from "@tailwindcss/vite"; +import { devtools } from "@tanstack/devtools-vite"; -import { tanstackStart } from '@tanstack/react-start/plugin/vite' +import { tanstackStart } from "@tanstack/react-start/plugin/vite"; -import viteReact from '@vitejs/plugin-react' -import tailwindcss from '@tailwindcss/vite' +import viteReact from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; const config = defineConfig({ - resolve: { tsconfigPaths: true }, - plugins: [devtools(), tailwindcss(), tanstackStart(), viteReact()], -}) + resolve: { tsconfigPaths: true }, + plugins: [devtools(), tailwindcss(), tanstackStart(), viteReact()], +}); -export default config +export default config;