WIP: timer set

This commit is contained in:
2026-07-29 00:11:37 -04:00
parent 2990708174
commit 8f5c499ee2
16 changed files with 539 additions and 110 deletions
+2
View File
@@ -0,0 +1,2 @@
VITE_TANSTACK_DEVTOOLS=true
+1 -2
View File
@@ -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": { "vcs": {
"enabled": false, "enabled": false,
"clientKind": "git", "clientKind": "git",
@@ -33,4 +33,3 @@
} }
} }
} }
+255 -23
View File
@@ -6,22 +6,29 @@
"": { "": {
"name": "by-other-means", "name": "by-other-means",
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^2.12.0",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@tanstack/react-devtools": "latest", "@tanstack/react-devtools": "latest",
"@tanstack/react-router": "latest", "@tanstack/react-router": "latest",
"@tanstack/react-router-devtools": "latest", "@tanstack/react-router-devtools": "latest",
"@tanstack/react-start": "latest", "@tanstack/react-start": "latest",
"luxon": "^3.7.2",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-redux": "^9.3.0",
"redux-persist": "^6.0.0",
"tailwindcss": "^4.1.18" "tailwindcss": "^4.1.18"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.4.5", "@biomejs/biome": "2.4.5",
"@tanstack/devtools-vite": "latest", "@tanstack/devtools-vite": "latest",
"@tanstack/router-cli": "^1.132.0", "@tanstack/router-cli": "^1.132.0",
"@types/luxon": "^3.7.2",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/react": "^19.2.0", "@types/react": "^19.2.0",
"@types/react-dom": "^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", "@vitejs/plugin-react": "^6.0.1",
"typescript": "^6.0.2", "typescript": "^6.0.2",
"vite": "^8.0.0" "vite": "^8.0.0"
@@ -220,6 +227,16 @@
"node": ">=6.0.0" "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": { "node_modules/@babel/template": {
"version": "7.29.7", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
@@ -456,29 +473,6 @@
"node": ">=14.21.3" "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": { "node_modules/@emnapi/wasi-threads": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz",
@@ -953,6 +947,32 @@
"url": "https://github.com/sponsors/Boshen" "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": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", "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" "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": { "node_modules/@tailwindcss/node": {
"version": "4.3.3", "version": "4.3.3",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
@@ -2255,6 +2287,26 @@
"tslib": "^2.4.0" "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": { "node_modules/@types/node": {
"version": "22.20.1", "version": "22.20.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
@@ -2286,6 +2338,58 @@
"@types/react": "^19.2.0" "@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": { "node_modules/@vitejs/plugin-react": {
"version": "6.0.4", "version": "6.0.4",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", "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": { "node_modules/is-fullwidth-code-point": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "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" "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": { "node_modules/lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -3049,6 +3200,15 @@
"yallist": "^3.0.2" "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": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -3219,6 +3379,37 @@
"react": "^19.2.8" "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": { "node_modules/readdirp": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
@@ -3232,6 +3423,31 @@
"url": "https://paulmillr.com/funding/" "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": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -3242,6 +3458,12 @@
"node": ">=0.10.0" "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": { "node_modules/rolldown": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
@@ -3411,6 +3633,16 @@
"node": ">=8" "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": { "node_modules/tailwindcss": {
"version": "4.3.3", "version": "4.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
+11 -4
View File
@@ -10,27 +10,34 @@
"generate-routes": "tsr generate", "generate-routes": "tsr generate",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"format": "biome format", "format": "biome format --write .",
"lint": "biome lint", "lint": "biome lint .",
"check": "biome check" "check": "biome check --write ."
}, },
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^2.12.0",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@tanstack/react-devtools": "latest", "@tanstack/react-devtools": "latest",
"@tanstack/react-router": "latest", "@tanstack/react-router": "latest",
"@tanstack/react-router-devtools": "latest", "@tanstack/react-router-devtools": "latest",
"@tanstack/react-start": "latest", "@tanstack/react-start": "latest",
"luxon": "^3.7.2",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-redux": "^9.3.0",
"redux-persist": "^6.0.0",
"tailwindcss": "^4.1.18" "tailwindcss": "^4.1.18"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "2.4.5", "@biomejs/biome": "2.4.5",
"@tanstack/devtools-vite": "latest", "@tanstack/devtools-vite": "latest",
"@tanstack/router-cli": "^1.132.0", "@tanstack/router-cli": "^1.132.0",
"@types/luxon": "^3.7.2",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/react": "^19.2.0", "@types/react": "^19.2.0",
"@types/react-dom": "^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", "@vitejs/plugin-react": "^6.0.1",
"typescript": "^6.0.2", "typescript": "^6.0.2",
"vite": "^8.0.0" "vite": "^8.0.0"
@@ -41,4 +48,4 @@
"lightningcss" "lightningcss"
] ]
} }
} }
+56
View File
@@ -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 (
<div className="flex min-h-screen flex-col items-center justify-center gap-8 p-8">
<h1 className="text-4xl font-bold">By Other Means Control Timer</h1>
<div
className={`font-mono text-8xl tabular-nums ${isOvertime ? "text-red-500 animate-bounce" : "text-slate-100"}`}
>
{formatElapsed(elapsedSeconds)}
</div>
{isRunning ? (
<button
type="button"
onClick={() => dispatch(clearStartTime())}
className="rounded-lg bg-slate-700 px-6 py-3 text-lg font-semibold hover:bg-slate-600"
>
Prepare Next Round
</button>
) : (
<button
type="button"
onClick={() => dispatch(setStartTime(DateTime.now().toISO() ?? ""))}
className="rounded-lg bg-emerald-600 px-6 py-3 text-lg font-semibold hover:bg-emerald-500"
>
Start Round
</button>
)}
</div>
);
};
export default Welcome;
+9
View File
@@ -57,3 +57,12 @@ const rootRouteChildren: RootRouteChildren = {
export const routeTree = rootRouteImport export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>() ._addFileTypes<FileRouteTypes>()
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}
+13 -13
View File
@@ -1,19 +1,19 @@
import { createRouter as createTanStackRouter } from '@tanstack/react-router' import { createRouter as createTanStackRouter } from "@tanstack/react-router";
import { routeTree } from './routeTree.gen' import { routeTree } from "./routeTree.gen";
export function getRouter() { export function getRouter() {
const router = createTanStackRouter({ const router = createTanStackRouter({
routeTree, routeTree,
scrollRestoration: true, scrollRestoration: true,
defaultPreload: 'intent', defaultPreload: "intent",
defaultPreloadStaleTime: 0, defaultPreloadStaleTime: 0,
}) });
return router return router;
} }
declare module '@tanstack/react-router' { declare module "@tanstack/react-router" {
interface Register { interface Register {
router: ReturnType<typeof getRouter> router: ReturnType<typeof getRouter>;
} }
} }
+57 -48
View File
@@ -1,54 +1,63 @@
import { HeadContent, Scripts, createRootRoute } from '@tanstack/react-router' import { TanStackDevtools } from "@tanstack/react-devtools";
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools' import { createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
import { TanStackDevtools } from '@tanstack/react-devtools' 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({ export const Route = createRootRoute({
head: () => ({ head: () => ({
meta: [ meta: [
{ {
charSet: 'utf-8', charSet: "utf-8",
}, },
{ {
name: 'viewport', name: "viewport",
content: 'width=device-width, initial-scale=1', content: "width=device-width, initial-scale=1",
}, },
{ {
title: 'TanStack Start Starter', title: "By Other Means Control Timer",
}, },
], ],
links: [ links: [
{ {
rel: 'stylesheet', rel: "stylesheet",
href: appCss, href: appCss,
}, },
], ],
}), }),
shellComponent: RootDocument, shellComponent: RootDocument,
}) });
function RootDocument({ children }: { children: React.ReactNode }) { function RootDocument({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en"> <html lang="en">
<head> <head>
<HeadContent /> <HeadContent />
</head> </head>
<body> <body className="bg-slate-900 text-slate-100">
{children} <Provider store={store}>
<TanStackDevtools <PersistGate loading={null} persistor={persistor}>
config={{ {children}
position: 'bottom-right', </PersistGate>
}} </Provider>
plugins={[ {import.meta.env.VITE_TANSTACK_DEVTOOLS === "true" && (
{ <TanStackDevtools
name: 'Tanstack Router', config={{
render: <TanStackRouterDevtoolsPanel />, position: "bottom-right",
}, }}
]} plugins={[
/> {
<Scripts /> name: "Tanstack Router",
</body> render: <TanStackRouterDevtoolsPanel />,
</html> },
) ]}
/>
)}
<Scripts />
</body>
</html>
);
} }
+7 -11
View File
@@ -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() { function RouteComponent() {
return ( return <Welcome />;
<div className="p-8">
<h1 className="text-4xl font-bold">Welcome to TanStack Start</h1>
<p className="mt-4 text-lg">
Edit <code>src/routes/index.tsx</code> to get started.
</p>
</div>
)
} }
+6
View File
@@ -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<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
+25
View File
@@ -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<string>) => {
state.startTime = action.payload;
},
clearStartTime: (state) => {
state.startTime = null;
},
},
});
export const { setStartTime, clearStartTime } = timeSlice.actions;
export default timeSlice.reducer;
+75
View File
@@ -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<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;
export type AppStore = typeof store;
+2
View File
@@ -0,0 +1,2 @@
const ROUND_LENGTH_MINUTES = 40;
export const ROUND_LENGTH_SECONDS = ROUND_LENGTH_MINUTES * 60;
+6
View File
@@ -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")}`;
}
+5
View File
@@ -0,0 +1,5 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_TANSTACK_DEVTOOLS: string;
}
+9 -9
View File
@@ -1,14 +1,14 @@
import { defineConfig } from 'vite' import tailwindcss from "@tailwindcss/vite";
import { devtools } from '@tanstack/devtools-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 viteReact from "@vitejs/plugin-react";
import tailwindcss from '@tailwindcss/vite' import { defineConfig } from "vite";
const config = defineConfig({ const config = defineConfig({
resolve: { tsconfigPaths: true }, resolve: { tsconfigPaths: true },
plugins: [devtools(), tailwindcss(), tanstackStart(), viteReact()], plugins: [devtools(), tailwindcss(), tanstackStart(), viteReact()],
}) });
export default config export default config;