WIP: can deploy?

This commit is contained in:
2026-07-29 00:30:37 -04:00
parent 8f5c499ee2
commit 4a6732798a
31 changed files with 803 additions and 739 deletions
+7
View File
@@ -0,0 +1,7 @@
node_modules
.git
.stamps
.env
.env.*
*.local
docs
+2
View File
@@ -0,0 +1,2 @@
VITE_TANSTACK_DEVTOOLS=false
+1 -5
View File
@@ -4,10 +4,6 @@ dist
dist-ssr dist-ssr
*.local *.local
.env .env
.nitro
.tanstack
.wrangler
.output
.vinxi
__unconfig* __unconfig*
todos.json todos.json
.stamps/
+4
View File
@@ -0,0 +1,4 @@
FROM nginx:stable-alpine
COPY ./dist/. /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
+75
View File
@@ -0,0 +1,75 @@
IMAGE_NAME=by-other-means
DOCKER_REGISTRY=gitea.pixelparasol.com
DOCKER_WORKSPACE=nathan
DOCKER_TARGET=$(DOCKER_REGISTRY)/$(DOCKER_WORKSPACE)/$(IMAGE_NAME)
K8S_NAMESPACE=by-other-means
TAG := $(shell git rev-parse --short HEAD)
STAMPS := .stamps
.PHONY: all
all:
@echo "Available targets:"
@echo " npm-install - Force a clean reinstall of npm dependencies"
@echo " npm-build - Build the project (skipped if sources unchanged)"
@echo " docker-build - Build the Docker image (skipped if unchanged)"
@echo " docker-push - Push the Docker image (skipped if already pushed for current commit)"
@echo " helm - Deploy or upgrade the Helm chart (skipped if charts and image unchanged)"
# Force a clean reinstall (explicit use only)
.PHONY: npm-install
npm-install:
@echo "📦 Installing npm dependencies"
@rm -rf node_modules
@npm ci
# Smart install: only runs if package files changed
node_modules/.package-lock.json: package-lock.json package.json
@echo "📦 Installing npm dependencies"
@npm ci
$(STAMPS):
@mkdir -p $@
# Smart build: only runs if source files or deps changed
SRC_FILES := $(shell find src -type f 2>/dev/null)
$(STAMPS)/npm-build: $(SRC_FILES) node_modules/.package-lock.json | $(STAMPS)
@echo "🏗️ Building the project"
@npm run build
@touch $@
.PHONY: npm-build
npm-build: $(STAMPS)/npm-build
# Smart docker build: only runs if npm build or Dockerfile changed, per tag
$(STAMPS)/docker-build-$(TAG): $(STAMPS)/npm-build Dockerfile | $(STAMPS)
@echo "🐳 Building Docker image $(DOCKER_TARGET):$(TAG)"
@docker buildx build --no-cache -f Dockerfile . --platform linux/amd64 \
--load \
-t $(DOCKER_TARGET):$(TAG) \
-t $(DOCKER_TARGET):latest
@touch $@
.PHONY: docker-build
docker-build: $(STAMPS)/docker-build-$(TAG)
# Smart push: only pushes if docker image was (re)built and not yet pushed
$(STAMPS)/docker-pushed-$(TAG): $(STAMPS)/docker-build-$(TAG) | $(STAMPS)
@echo "🔐 Logging into Docker registry $(DOCKER_REGISTRY)"
@docker login $(DOCKER_REGISTRY)
@echo "🚀 Pushing Docker image $(DOCKER_TARGET):$(TAG) and latest"
@docker push $(DOCKER_TARGET):$(TAG)
@docker push $(DOCKER_TARGET):latest
@touch $@
.PHONY: docker-push
docker-push: $(STAMPS)/docker-pushed-$(TAG)
# Smart helm deploy: only runs if chart files changed or a new image was pushed
HELM_FILES := $(shell find helm -type f 2>/dev/null)
$(STAMPS)/helm: $(HELM_FILES) $(STAMPS)/docker-pushed-$(TAG) | $(STAMPS)
@echo "🚀 Deploying Helm chart to namespace '$(K8S_NAMESPACE)'"
@helm upgrade --install $(K8S_NAMESPACE) ./helm -n $(K8S_NAMESPACE) --create-namespace
@touch $@
.PHONY: helm
helm: $(STAMPS)/helm
+30
View File
@@ -0,0 +1,30 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
# NOTES.txt
# deployment.yml
# secrets.yml
# service.yml
# persistent_volume.yml
+24
View File
@@ -0,0 +1,24 @@
apiVersion: v2
name: helm
description: By Other Means Control Timer Helm Chart
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.0.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "0.0.1"
+64
View File
@@ -0,0 +1,64 @@
## Installation
```
helm upgrade --install by-other-means ./helm -n by-other-means --create-namespace
```
## Secrets
Gitea Auth secret for pulling the container from the private registry (namespace-scoped, so this
needs to be created once per namespace):
```
kubectl -n by-other-means \
create secret docker-registry gitea-auth \
--docker-server=https://gitea.pixelparasol.com \
--docker-username=nathan \
--docker-password=<redacted> \
--docker-email=nathan@pixelparasol.com
```
## Build the image
```
npm run build
DOCKER_REGISTRY="gitea.pixelparasol.com/nathan/by-other-means" && CI_COMMIT_SHORT_SHA=$(git rev-parse --short HEAD)
docker buildx build --no-cache -f Dockerfile . --platform linux/amd64 -t $DOCKER_REGISTRY:latest -t $DOCKER_REGISTRY:$CI_COMMIT_SHORT_SHA
docker login gitea.pixelparasol.com
docker push $DOCKER_REGISTRY:$CI_COMMIT_SHORT_SHA; docker push $DOCKER_REGISTRY:latest
```
### Troubleshooting
You may find yourself in a state where you need to do secret checks to see how they render.
adding the `--dry-run=server` argument to your upgrade will render the secret checks.
```
helm upgrade --install by-other-means ./helm -n by-other-means --dry-run=server
```
## Helm Diff
Install the `helm-diff` plugin
```
helm plugin install https://github.com/databus23/helm-diff
```
view the diff if an upgrade were issued
```
helm diff upgrade by-other-means ./helm -n by-other-means
```
## Deployment
The app can be deployed by tag by specifying it with `--set`
```
helm upgrade by-other-means ./helm -n by-other-means --set deploy.app.tag="v1.0.4"
```
+23
View File
@@ -0,0 +1,23 @@
1. {{ .Release.Name }} Deployment Information:
- Release Name: {{ .Release.Name }}
- Namespace: {{ .Release.Namespace }}
- Chart Name: {{ .Chart.Name }}
- Chart Version: {{ .Chart.Version }}
2. {{ .Release.Name }} Service Information:
- Service Name: {{ .Values.system }}-service
- Service Type: LoadBalancer
- Service Port: 80
3. Useful Commands:
- Check the {{ .Release.Name }} Deployment Status:
helm status {{ .Release.Name }}
- Get Detailed Information about the {{ .Release.Name }} Deployment:
helm get all {{ .Release.Name }}
helm diff
4. Clean Up:
- To uninstall/delete the {{ .Release.Name }} deployment, run:
helm uninstall {{ .Release.Name }}
+60
View File
@@ -0,0 +1,60 @@
{{- define "system.deployment.component" }}
{{ $image := .component.image }}
{{ $component := .component }}
{{ $release := .release }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .component.name }}
labels:
{{ include "helper.metaLabels" . | indent 4 }}
spec:
replicas: 1
selector:
matchLabels:
{{ include "helper.matchLabels" . | indent 8 }}
template:
metadata:
labels:
{{ include "helper.metaLabels" . | indent 8 }}
spec:
{{- include "helper.affinity" . | indent 6 }}
{{- if $image.imagePullSecrets }}
imagePullSecrets:
- name: {{ toString $image.imagePullSecrets }}
{{- end }}
{{- if .component.volumes }}
{{- range $key, $volume := .component.volumes }}
volumes:
{{- if eq $volume.type "configMap" }}
- name: "{{ $volume.name }}"
configMap:
name: "{{ $volume.name}}"
{{- else }}
- name: "{{ $release.Name }}-{{ $component.name }}-pvc"
persistentVolumeClaim:
claimName: "{{ $release.Name }}-{{ $component.name }}-pvc"
{{- end }}
{{- end }}
{{- end }}
containers:
- name: {{ .component.name }}
{{ include "helper.deploy_tag" (dict "component" .component "values" .values "release" .release) | indent 10 }}
{{- if .component.deployment }}
{{- toYaml .component.deployment | trim | nindent 10 }}
{{- end }}
{{- if .component.volumes }}
{{- range $key, $volume := .component.volumes }}
volumeMounts:
{{- if eq $volume.type "configMap" }}
- name: {{ $volume.name }}
mountPath: {{ $volume.mountPath }}
subPath: {{ $volume.subPath }}
{{- else }}
- mountPath: {{ $volume.containerMountPath }}
name: "{{ $release.Name }}-{{ $component.name }}-pvc"
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+76
View File
@@ -0,0 +1,76 @@
{{/*
match labels
*/}}
{{- define "helper.matchLabels" -}}
app: {{ .component.name }}
{{- end }}
{{/*
meta labels
*/}}
{{- define "helper.metaLabels" -}}
app: {{ .component.name }}
tenant: {{ .values.system }}
{{- end }}
{{/*
Node Affinity helper
*/}}
{{- define "helper.affinity" -}}
{{- if .component.affinity -}}
nodeName: {{ .component.affinity }}
{{- end -}}
{{- end -}}
{{/*
Image name and tag helper
*/}}
{{- define "helper.image" -}}
image: "{{ .component.image.repository }}:{{ .component.image.tag }}"
{{- end -}}
{{- /* # returns one of the following
# - image: current_release:current_tag from lookup
# - image: current_release:deploy_tag from image lookup
# if this iteration of the loop has a .Values.deploy override tag
# - set $new_image to the override tag
# else
# - set $new_image to the current tag
*/ -}}
{{- define "helper.deploy_tag" -}}
{{- $new_deployment := "" -}}
{{- $component := .component.name -}}
{{- $deployment := (lookup "apps/v1" "Deployment" .release.Namespace $component) | default dict }}
{{- if $deployment -}}
{{- $targetContainerName := $component -}}
{{- $targetContainer := dict -}}
{{- range $deployment.spec.template.spec.containers }}
{{- if eq .name $targetContainerName }}
{{- $targetContainer = . -}}
{{- end -}}
{{- end -}}
{{- range $key, $deploy := .values.deploy -}}
{{- if eq $key $component -}}
{{- $new_deployment = $deploy.tag -}}
{{- end -}}
{{- end -}}
{{- if $new_deployment -}}
image: "{{ .component.image.repository }}:{{ $new_deployment }}"
{{- else -}}
image: {{ $targetContainer.image }}
{{- end -}}
{{- else -}}
image: "{{ .component.image.repository }}:{{ .component.image.tag }}"
{{- end -}}
{{- end -}}
{{- /* include "helper.var_dump" . */ -}}
{{- define "helper.var_dump" -}}
{{- . | mustToPrettyJson | printf "\n%s" | fail }}
{{- end -}}
+35
View File
@@ -0,0 +1,35 @@
{{- define "system.persistent_volume.component" }}
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: "{{ .release.Name }}-{{ .component.name }}-pv"
labels:
{{ include "helper.metaLabels" . | indent 4 }}
spec:
storageClassName: {{ .volume.storageClassName }}
persistentVolumeReclaimPolicy: {{ .volume.reclaimPolicy }}
capacity:
storage: {{ .volume.capacity.storage }}
accessModes:
{{ toYaml .volume.accessModes }}
{{- if .volume.hostPath }}
hostPath:
path: {{ .volume.hostPath }}
{{- end }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: "{{ .release.Name }}-{{ .component.name }}-pvc"
labels:
{{ include "helper.metaLabels" . | indent 4 }}
spec:
storageClassName: {{ .volume.storageClassName }}
volumeName: "{{ .release.Name }}-{{ .component.name }}-pv"
accessModes:
{{ toYaml .volume.accessModes }}
resources:
requests:
storage: {{ .volume.capacity.storage }}
{{- end }}
+21
View File
@@ -0,0 +1,21 @@
{{- define "system.service.component" }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .component.name }}-service
labels:
{{ include "helper.metaLabels" . | indent 4 }}
spec:
type: {{ .component.service.type }}
selector:
app: {{ .component.name }}
ports:
{{- range .component.service.ports }}
- protocol: {{ .protocol }}
port: {{ .port }}
targetPort: {{ .targetPort }}
name: {{ .name }}
{{- end }}
{{- end }}
+57
View File
@@ -0,0 +1,57 @@
{{- define "system.backup.cronjob" -}}
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ .system_name }}-{{ .backup.pvc_name }}-backup
spec:
schedule: {{ required "Values.backups.volumes[n].schedule required in crontab format" .backup.schedule }}
successfulJobsHistoryLimit: 1
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
volumes:
- name: nfs-backup
nfs:
server: {{ required "Values.backups.volumes[n].nas_ip required to make nfs connection" .nas_ip }}
path: {{ required "Values.backups.volumes[n].nas_path required to store backup to a nfs connection" .backup.nas_path }}
- name: {{ required "Values.backups.volumes[n].pvc_name required" .backup.pvc_name }}
persistentVolumeClaim:
claimName: {{ .backup.pvc_name }}
containers:
- name: config-backup
image: {{ .image | default "alpine" }}
imagePullPolicy: IfNotPresent
command:
- sh
- -c
- date;
echo "Creating backup for {{ .system_name }}";
echo -e "\nCreating {{ .system_name }} directory if it doesnt already exist";
mkdir -p /nas/backup/{{ .system_name }};
echo -e "\nProcessing {{ .backup.pvc_name }}";
echo " - Creating backup of {{ .backup.pvc_name }} volume";
tar -czf /nas/backup/{{ .system_name }}/{{ .backup.pvc_name }}_$(date +%Y-%m-%d).tar.gz /backup_source;
echo " - Enforcing backup retention policy for {{ .backup.pvc_name }} to latest {{ required "Values.backups.volumes[n].retention required as an integer" .backup.retention }}";
ls -t /nas/backup/{{ required "Values.backups.system_name required" .system_name }}/{{ .backup.pvc_name }}_*.tar.gz | sort -r | tail -n +{{ add1 .backup.retention }} | xargs rm -f;
echo -e "\nCompleted backup of {{ .backup.pvc_name }}";
date;
volumeMounts:
- name: nfs-backup
mountPath: /nas/backup
- name: {{ .backup.pvc_name }}
mountPath: /backup_source
{{- end }}
{{ if .Values.backups.enabled }}
{{- range $parent, $backup := .Values.backups.volumes -}}
{{- if $backup.enabled }}
{{ include "system.backup.cronjob" (dict "backup" $backup "image" $.Values.backups.image "nas_ip" $.Values.backups.nas_ip "system_name" $.Values.backups.system_name) }}
{{- end }}
{{- end }}
{{- end }}
+3
View File
@@ -0,0 +1,3 @@
{{- range $system, $settings := .Values.components -}}
{{ include "system.deployment.component" (dict "component" $settings "values" $.Values "release" $.Release) }}
{{- end }}
+29
View File
@@ -0,0 +1,29 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Values.system }}-ingress
labels:
app: {{ .Values.system }}
tenant: {{ .Values.system }}
annotations:
cert-manager.io/cluster-issuer: lets-encrypt-prod
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: {{ .Values.components.app.ingress.className }}
tls:
- hosts:
- "{{ .Values.components.app.ingress.domain }}"
secretName: {{ .Values.system }}-ingress-tls
rules:
- host: "{{ .Values.components.app.ingress.domain }}"
http:
paths:
- path: {{ .Values.components.app.ingress.path }}
pathType: Prefix
backend:
service:
name: {{ .Values.system }}-service
port:
number: {{ .Values.components.app.ingress.port }}
+28
View File
@@ -0,0 +1,28 @@
---
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-configmap
labels:
app: {{ .Values.system }}
tenant: {{ .Values.system }}
data:
default.conf: |
server {
listen 80;
listen [::]:80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri /index.html =404;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
+9
View File
@@ -0,0 +1,9 @@
{{- range $system, $settings := .Values.components -}}
{{ if $settings.volumes }}
{{- range $component, $volume := $settings.volumes -}}
{{- if ne $volume.type "configMap" -}}
{{ include "system.persistent_volume.component" (dict "component" $settings "volume" $volume "values" $.Values "release" $.Release) }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+33
View File
@@ -0,0 +1,33 @@
{{/*
Secret initializer and data maintainer
This helper will initialize the secret data with "initial" value if the secret is not found in the secret object.
IF the secret is found in the secret object, it will use the set value from the secret object thereby maintaining the secret data.
*/}}
{{- define "helper.secrets" -}}
{{- $secretObj := (lookup "v1" "Secret" .release.Namespace .component.name) | default dict }}
{{- range $key, $value := .component.secrets }}
{{- if empty $value -}}
{{- $secretData := (get $secretObj "data") | default dict }}
{{- $secret := (get $secretData $key) | default ("initial" | b64enc) }}
{{ $key }}: {{ $secret | quote }}
{{- else }}
{{ $key }}: {{ $value | b64enc | quote }}
{{- end -}}
{{- end }}
{{- end -}}
{{- range $system, $settings := .Values.components -}}
{{ if $settings.secrets }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ $settings.name }}
labels:
{{ include "helper.metaLabels" (dict "component" $settings "values" $.Values) | indent 4 }}
type: Opaque
data:
{{- include "helper.secrets" (dict "component" $settings "release" $.Release) }}
{{- end }}
{{- end }}
+5
View File
@@ -0,0 +1,5 @@
{{- range $system, $settings := .Values.components -}}
{{ if $settings.service }}
{{ include "system.service.component" (dict "component" $settings "values" $.Values) }}
{{- end }}
{{- end }}
+43
View File
@@ -0,0 +1,43 @@
system: by-other-means
components:
app:
name: by-other-means
affinity: null
image:
repository: gitea.pixelparasol.com/nathan/by-other-means
tag: "latest"
imagePullPolicy: "Always"
imagePullSecrets: "gitea-auth"
ingress:
tls: true
domain: bom.nathanbstanley.com
path: /
className: "nginx"
port: 80
service:
type: ClusterIP
ports:
- protocol: TCP
port: 80
targetPort: 80
name: http
deployment:
resources:
limits:
memory: "128Mi"
cpu: "500m"
volumes:
- name: nginx-configmap
type: configMap
mountPath: "/etc/nginx/conf.d/default.conf"
subPath: "default.conf"
backups:
enabled: false
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>By Other Means Control Timer</title>
</head>
<body class="bg-slate-900 text-slate-100">
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+91 -655
View File
File diff suppressed because it is too large Load Diff
+1 -3
View File
@@ -7,7 +7,6 @@
}, },
"scripts": { "scripts": {
"dev": "vite dev --port 3000", "dev": "vite dev --port 3000",
"generate-routes": "tsr generate",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"format": "biome format --write .", "format": "biome format --write .",
@@ -20,7 +19,6 @@
"@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",
"luxon": "^3.7.2", "luxon": "^3.7.2",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
@@ -31,7 +29,7 @@
"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-plugin": "^1.132.0",
"@types/luxon": "^3.7.2", "@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",
+24
View File
@@ -0,0 +1,24 @@
import { RouterProvider } from "@tanstack/react-router";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { getRouter } from "#/router";
import { persistor, store } from "#/store/store";
import "./styles.css";
const router = getRouter();
const rootElement = document.getElementById("app");
if (!rootElement)
throw new Error("Could not find #app element to mount the app");
createRoot(rootElement).render(
<StrictMode>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<RouterProvider router={router} />
</PersistGate>
</Provider>
</StrictMode>,
);
+17 -3
View File
@@ -1,5 +1,11 @@
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { type FC, useEffect, useState } from "react"; import {
type EventHandler,
type FC,
type MouseEventHandler,
useEffect,
useState,
} from "react";
import { useAppDispatch, useAppSelector } from "../store/hooks"; import { useAppDispatch, useAppSelector } from "../store/hooks";
import { clearStartTime, setStartTime } from "../store/slices/timeSlice"; import { clearStartTime, setStartTime } from "../store/slices/timeSlice";
import { ROUND_LENGTH_SECONDS } from "../utils/constants"; import { ROUND_LENGTH_SECONDS } from "../utils/constants";
@@ -22,6 +28,14 @@ const Welcome: FC = () => {
const isRunning = startTime !== null; const isRunning = startTime !== null;
const isOvertime = elapsedSeconds > 60; const isOvertime = elapsedSeconds > 60;
const nextRoundClick: MouseEventHandler<HTMLButtonElement> = (_e) => {
dispatch(clearStartTime());
};
const startRoundClick: MouseEventHandler<HTMLButtonElement> = (_e) => {
dispatch(setStartTime(DateTime.now().toISO() ?? ""));
};
return ( return (
<div className="flex min-h-screen flex-col items-center justify-center gap-8 p-8"> <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> <h1 className="text-4xl font-bold">By Other Means Control Timer</h1>
@@ -35,7 +49,7 @@ const Welcome: FC = () => {
{isRunning ? ( {isRunning ? (
<button <button
type="button" type="button"
onClick={() => dispatch(clearStartTime())} onClick={nextRoundClick}
className="rounded-lg bg-slate-700 px-6 py-3 text-lg font-semibold hover:bg-slate-600" className="rounded-lg bg-slate-700 px-6 py-3 text-lg font-semibold hover:bg-slate-600"
> >
Prepare Next Round Prepare Next Round
@@ -43,7 +57,7 @@ const Welcome: FC = () => {
) : ( ) : (
<button <button
type="button" type="button"
onClick={() => dispatch(setStartTime(DateTime.now().toISO() ?? ""))} onClick={startRoundClick}
className="rounded-lg bg-emerald-600 px-6 py-3 text-lg font-semibold hover:bg-emerald-500" className="rounded-lg bg-emerald-600 px-6 py-3 text-lg font-semibold hover:bg-emerald-500"
> >
Start Round Start Round
-9
View File
@@ -57,12 +57,3 @@ 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>>
}
}
+6 -41
View File
@@ -1,48 +1,15 @@
import { TanStackDevtools } from "@tanstack/react-devtools"; import { TanStackDevtools } from "@tanstack/react-devtools";
import { createRootRoute, HeadContent, Scripts } from "@tanstack/react-router"; import { createRootRoute, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"; import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import { Provider } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { persistor, store } from "#/store/store";
import appCss from "../styles.css?url";
export const Route = createRootRoute({ export const Route = createRootRoute({
head: () => ({ component: RootComponent,
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 }) { function RootComponent() {
return ( return (
<html lang="en"> <>
<head> <Outlet />
<HeadContent />
</head>
<body className="bg-slate-900 text-slate-100">
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
{children}
</PersistGate>
</Provider>
{import.meta.env.VITE_TANSTACK_DEVTOOLS === "true" && ( {import.meta.env.VITE_TANSTACK_DEVTOOLS === "true" && (
<TanStackDevtools <TanStackDevtools
config={{ config={{
@@ -56,8 +23,6 @@ function RootDocument({ children }: { children: React.ReactNode }) {
]} ]}
/> />
)} )}
<Scripts /> </>
</body>
</html>
); );
} }
+3 -3
View File
@@ -14,9 +14,9 @@ const noopStorage = {
}, },
}; };
// redux-persist's bundled storage needs `localStorage`, which isn't available // redux-persist's bundled storage engine can throw or come back malformed in
// during server-side rendering (and can throw in locked-down browsers), so // some browsers (private mode, locked-down storage), so this is a minimal
// this is a minimal storage engine that never touches `redux-persist/lib/storage`. // storage engine that never touches `redux-persist/lib/storage`.
function createLocalStorageEngine() { function createLocalStorageEngine() {
if (typeof window === "undefined") return noopStorage; if (typeof window === "undefined") return noopStorage;
try { try {
-3
View File
@@ -1,3 +0,0 @@
{
"target": "react"
}
+7 -4
View File
@@ -1,14 +1,17 @@
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import { devtools } from "@tanstack/devtools-vite"; import { devtools } from "@tanstack/devtools-vite";
import { tanstackRouter } from "@tanstack/router-plugin/vite";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import viteReact from "@vitejs/plugin-react"; import viteReact from "@vitejs/plugin-react";
import { defineConfig } from "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(),
tanstackRouter({ target: "react", autoCodeSplitting: true }),
viteReact(),
],
}); });
export default config; export default config;