7 Commits
Author SHA1 Message Date
John O'Keefe 5d726595fc fix(auth): center home and login layouts with full viewport height
Release / build-and-push (push) Successful in 41s
- Remove content margins when logged out in App
- Switch Home to minH 100dvh full width
- Center LoginForm vertically in full-height flex layout
- Tidy Create Game button padding and border
2026-09-08 20:25:38 -04:00
john-okeefe 315c22de65 style(theme): lighten global text-selection highlight
Release / build-and-push (push) Successful in 36s
Native ::selection was near-invisible on dark surfaces, hiding selected
text inside inputs. Add a global ::selection using the hover-selection
treatment (gray.200 light / whiteAlpha.300 dark) and leave the text
color untouched so contrast holds everywhere.
2026-09-06 16:41:15 -04:00
john-okeefe 19a80d1c74 fix(filters): inherit tag color for the search-term link
The active search pill renders its term through a Chakra Link, which
painted link-blue while every sibling filter pill renders the tag
contrast color. Set color=inherit so the term matches the other tags;
the hover underline now draws in the tag's own color too.
2026-09-06 16:41:02 -04:00
john-okeefe e85bdabf10 chore(ci): tag-triggered release workflow and helpers
Release / build-and-push (push) Successful in 50s
Add .gitea/workflows/release.yml: on v* tag push (or manual
dispatch) build the Docker image and push
git.linuxhg.com/games-database/games-frontend:<tag> plus
:latest, then create/update the Gitea Release from the
annotated tag message. No build-args needed since the build
is env-free. Main-branch pushes do nothing.

Add cliff.toml (conventional-commit release notes), Makefile
release target, and ./release wrapper (accepts 1.0 or v1.0).
Same flow as the other repos; needs a REGISTRY_TOKEN repo
Actions secret (PAT with write:package and write:repository).
2026-09-06 12:49:21 -04:00
john-okeefe e2ae729a60 chore(docker): multi-stage build and registry run template
Dockerfile builds the Vite app in an oven/bun stage and serves
dist/ from the same nginx:stable-alpine-perl base as before,
plus the runtime-config entrypoint scripts. .env stays
dockerignored, so CI builds carry no backend URL and the image
is env-independent. Custom /etc/nginx/nginx.conf mounts keep
working (separate path from /docker-entrypoint.d/).

docker-compose.example.yml is a prod run template for
git.linuxhg.com/games-database/games-frontend (IMAGE_TAG,
default latest) with VITE_API_URL env (including the /api
suffix), restart policy, and a wget / healthcheck; traefik
setups drop ports for their labels/networks.
2026-09-06 12:49:14 -04:00
john-okeefe 8b18dc55c8 feat(frontend): runtime backend URL via /config.js
Stop baking VITE_API_URL into the Vite build so one image can
run against any backend. Container entrypoint
(docker-entrypoint.d/30-frontend-config.sh, auto-run by the
nginx image) writes /usr/share/nginx/html/config.js from the
VITE_API_URL container env at every start; index.html loads
it before the bundle, and agent.ts prefers
window.__APP_CONFIG__ with the build-time env as fallback so
local dev and vite preview behave exactly as before.
public/config.js ships an empty placeholder for non-container
builds. Changing backends is now a restart with new env, no
rebuild.
2026-09-06 12:48:58 -04:00
john-okeefe 3e0385d5d9 Merge branch 'react-19.2.8' into main
React 19 + Chakra UI v3 + router v7 migration with v2 look restoration:
deps, theme system, forms/selects, nav and dashboard chrome, details,
auth screens, dashboard hover fix, menu hover treatment, double-chevron
pagination.
2026-09-06 12:28:07 -04:00
16 changed files with 297 additions and 11 deletions
+116
View File
@@ -0,0 +1,116 @@
name: Release
# Overrides the default run name (the tagged commit's message) so the Actions
# runs list shows "Release v0.1.0" instead.
run-name: "Release ${{ gitea.event.inputs.tag || gitea.ref_name }}"
# Publishes the games-frontend container image to the Gitea container registry
# AND creates a Gitea Release whose body is the annotated tag's message
# (generated locally by `make release VERSION=...` via git-cliff). Triggered
# by a version tag push, or manually via workflow_dispatch with a tag. Pushing
# to main does nothing, so work-in-progress commits never ship. Each release
# publishes two image tags: the version (e.g. v0.1.0) and "latest".
#
# The image carries no backend URL (Vite build runs with no .env); the
# backend is injected at container start from VITE_API_URL, so no
# build-args or secrets are needed here beyond the registry login.
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Tag to release (e.g. v0.1.0)'
required: true
type: string
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
# Resolve the target tag for both triggers: explicit input on manual
# dispatch, otherwise the pushed tag ref.
TAG: ${{ gitea.event.inputs.tag || gitea.ref_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Full history ensures the tag annotation (the release notes) is present.
fetch-depth: 0
ref: ${{ gitea.event.inputs.tag || gitea.ref }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: git.linuxhg.com
username: ${{ gitea.actor }}
# PAT stored as a repo Actions secret (auto GITHUB_TOKEN lacks package scope in Gitea)
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
# Publishes both the exact version (e.g. v0.1.0) and the movable "latest" tag.
# Deployments default to "latest" via ${IMAGE_TAG:-latest} in docker-compose.yml;
# pin or roll back by setting IMAGE_TAG in .env.
tags: |
git.linuxhg.com/games-database/games-frontend:${{ env.TAG }}
git.linuxhg.com/games-database/games-frontend:latest
- name: Create Gitea Release
env:
# REGISTRY_TOKEN is reused for release creation because Gitea's auto
# GITHUB_TOKEN cannot create releases on this instance. The PAT must
# carry write:repository scope. Idempotent: re-runs update an existing
# release for this tag instead of failing with 409. On any HTTP error
# the API response body is printed so a 403 names the missing scope.
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
REPO: ${{ gitea.repository }}
run: |
set -euo pipefail
: "${TAG:?TAG is required}"
API="https://git.linuxhg.com/api/v1/repos/${REPO}/releases"
AUTH="Authorization: token ${TOKEN}"
# Release body = the annotated tag's message (the git-cliff notes).
BODY="$(git tag -l --format='%(contents)' "${TAG}")"
# Tags containing a '-' (e.g. v0.1.0-rc1) are published as pre-releases.
PRE="false"; case "${TAG}" in *-*) PRE="true";; esac
PAYLOAD=$(jq -n \
--arg t "${TAG}" --arg n "${TAG}" --arg b "${BODY}" --argjson p "${PRE}" \
'{tag_name:$t, name:$n, body:$b, draft:false, prerelease:$p}')
# POST/PATCH the release, surfacing Gitea's error message on failure
# (e.g. "token does not have write scope") instead of failing silently.
api_call() {
local method="$1" url="$2" resp code rbody
resp="$(curl -sS -w '\n%{http_code}' -X "${method}" \
-H "${AUTH}" -H "Content-Type: application/json" \
-d "${PAYLOAD}" "${url}")"
code="$(printf '%s' "${resp}" | tail -n1)"
rbody="$(printf '%s' "${resp}" | sed '$d')"
if [ "${code}" -ge 400 ]; then
echo "::error::Release API ${code} (${method} ${url}): ${rbody}" >&2
return 1
fi
}
EXISTING_ID="$(curl -sS -H "${AUTH}" "${API}/tags/${TAG}" | jq -r '.id // empty' 2>/dev/null || true)"
if [ -n "${EXISTING_ID}" ]; then
api_call PATCH "${API}/${EXISTING_ID}"
echo "Updated existing release id=${EXISTING_ID} for ${TAG}"
else
api_call POST "${API}"
echo "Created new release for ${TAG}"
fi
+18 -1
View File
@@ -1,2 +1,19 @@
# Multi-stage: build the Vite app here so CI produces an env-independent
# image (no .env needed at build time — the backend URL is injected at
# container start, see docker-entrypoint.d/30-frontend-config.sh).
FROM oven/bun:1 AS build
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
FROM nginx:stable-alpine-perl
COPY ./dist /usr/share/nginx/html
COPY --from=build /app/dist /usr/share/nginx/html
# nginx's entrypoint auto-runs *.sh in /docker-entrypoint.d/ on start.
# Unaffected by mounting a custom /etc/nginx/nginx.conf (separate path).
COPY docker-entrypoint.d/ /docker-entrypoint.d/
RUN chmod +x /docker-entrypoint.d/*.sh
EXPOSE 80
+33
View File
@@ -0,0 +1,33 @@
.PHONY: help release
# Default target
help:
@echo "Available targets:"
@echo ""
@echo "Release:"
@echo " ./release v0.1.0 - Tag, push, and release (notes auto-generated from commits)"
@echo " make release VERSION=v0.1.0 - Same, explicit make form"
# Create an annotated version tag carrying auto-generated release notes (git-cliff)
# and push it. The tag push triggers .gitea/workflows/release.yml, which builds the
# image and publishes a Gitea Release whose body is this tag's message. Notes come
# entirely from Conventional Commits — no hand-written message required.
#
# git-cliff's --latest needs the tag to exist to scope the notes, so we create a
# throwaway lightweight tag, generate the notes, replace it with an annotated tag,
# then push. --cleanup=verbatim keeps the markdown "###" group headers (git's
# default cleanup would strip lines starting with "#").
#
# Requires git-cliff: https://git-cliff.org/install
# Usage: make release VERSION=v0.1.0
release:
@test -n "$(VERSION)" || { echo "Usage: make release VERSION=v0.1.0"; exit 1; }
@command -v git-cliff >/dev/null 2>&1 || { echo "git-cliff not found — install: https://git-cliff.org/install"; exit 1; }
@if git rev-parse "$(VERSION)" >/dev/null 2>&1; then echo "Tag $(VERSION) already exists locally — delete it first: git tag -d $(VERSION)"; exit 1; fi
@echo "Generating release notes for $(VERSION)..."
@git tag "$(VERSION)" HEAD && \
(git cliff --latest --config cliff.toml > .release-notes.tmp && git tag -d "$(VERSION)" >/dev/null) || \
{ git tag -d "$(VERSION)" >/dev/null 2>&1; rm -f .release-notes.tmp; echo "git-cliff failed"; exit 1; }
@git tag -a --cleanup=verbatim -F .release-notes.tmp "$(VERSION)" HEAD && rm -f .release-notes.tmp
@git push origin "$(VERSION)"
@echo "Pushed $(VERSION) — Gitea Actions will build the image and publish the Release."
+37
View File
@@ -0,0 +1,37 @@
# git-cliff configuration — generates the body of each Gitea Release from
# Conventional Commits accumulated since the previous tag. Used locally by
# `make release` with --latest so only the current tag's section is emitted
# (no full history, no header — the Gitea Release title is the tag).
# Docs: https://git-cliff.org/docs/configuration
[changelog]
header = ""
body = """
{% for group, commits in commits | group_by(attribute="group") %}\
### {{ group | upper_first }}
{% for commit in commits %}\
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}{{ commit.message | upper_first }} ({{ commit.id | truncate(length=7, end="") }})
{% endfor %}\
{% endfor %}\
"""
trim = true
footer = ""
[git]
conventional_commits = true
filter_unconventional = false
require_conventional = false
split_commits = false
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactor" },
{ message = "^docs", group = "Documentation" },
{ message = "^test", group = "Tests" },
{ message = "^chore|^ci", group = "Miscellaneous Tasks" },
{ message = ".*", group = "Other" },
]
filter_commits = false
tag_pattern = "v[0-9].*"
sort_commits = "oldest"
+25
View File
@@ -0,0 +1,25 @@
---
# Prod run template. The backend URL is injected at container start
# (docker-entrypoint.d/30-frontend-config.sh), so the image carries no env.
# Set VITE_API_URL including the /api suffix, e.g.:
# VITE_API_URL=https://gamesapi.linuxhg.com/api
# Either inline below or via env_file .env (environment wins if both set).
# Traefik setups: drop ports, add your labels/networks like the existing
# games-frontend service; custom nginx.conf mounts keep working.
services:
games-frontend:
image: git.linuxhg.com/games-database/games-frontend:${IMAGE_TAG:-latest}
container_name: games-frontend
restart: unless-stopped
environment:
- VITE_API_URL=
# env_file:
# - .env
ports:
- 80:80
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://localhost/ || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
+19
View File
@@ -0,0 +1,19 @@
#!/bin/sh
# Generates runtime frontend config from container environment.
# Runs automatically at container start via nginx's /docker-entrypoint.d/.
# Lets one image talk to any backend: set VITE_API_URL on the service
# (compose environment or .env) and restart — no rebuild needed.
# The app (src/api/agent.ts) prefers this file, falling back to the
# build-time VITE_API_URL when it is absent (local dev, vite preview).
set -eu
CONFIG_FILE="/usr/share/nginx/html/config.js"
mkdir -p "$(dirname "${CONFIG_FILE}")"
if [ -z "${VITE_API_URL:-}" ]; then
echo "frontend-config: VITE_API_URL not set, writing empty config (build-time env applies)"
printf 'window.__APP_CONFIG__ = {};\n' > "${CONFIG_FILE}"
else
echo "frontend-config: writing config.js with VITE_API_URL=${VITE_API_URL}"
printf 'window.__APP_CONFIG__ = { VITE_API_URL: "%s" };\n' "${VITE_API_URL}" > "${CONFIG_FILE}"
fi
+3
View File
@@ -32,6 +32,9 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!-- Runtime config (backend URL) injected at container start.
See docker-entrypoint.d/30-frontend-config.sh and public/config.js. -->
<script src="/config.js"></script>
<script type="module" src="/src/index.tsx"></script>
<!--
This HTML file is a template.
+6
View File
@@ -0,0 +1,6 @@
// Placeholder replaced at container start by
// docker-entrypoint.d/30-frontend-config.sh, which writes the real
// VITE_API_URL from the container environment. Served as /config.js
// (see index.html). Empty here so local builds fall back to the
// build-time import.meta.env value.
window.__APP_CONFIG__ = {};
Executable
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env sh
# Project-attached wrapper around `make release` so you can run:
# ./release v0.1.0 (or) ./release 0.1.0
# instead of:
# make release VERSION=v0.1.0
# Lives in the repo (no machine-specific alias needed).
set -eu
[ "$#" -ge 1 ] || { echo "Usage: ./release v0.1.0" >&2; exit 1; }
# Accept "0.1.0" or "v0.1.0"; ensure the tag starts with 'v' (the workflow
# only triggers on v* tags).
VERSION="v${1#v}"
exec make release "VERSION=${VERSION}"
+1 -1
View File
@@ -58,7 +58,7 @@ function App() {
<>
<ToastContainer position='bottom-right' hideProgressBar />
{loggedIn ? <Header /> : ''}
<Box mt={loadedPreferences.get().stickyNav ? "20" : "4"} mb={'4'}>
<Box mt={loggedIn ? (loadedPreferences.get().stickyNav ? "20" : "4") : "0"} mb={loggedIn ? '4' : '0'}>
{/*<Box mt={loadedPreferences.get().stickyNav ? isSmallerThan768 ? "15%" : "9%" : "4"} mb={'4'}>*/}
<Routes>
<Route path='/*' element={loggedIn ? <Navigate to='games' /> : <Home />} />
+8 -1
View File
@@ -11,7 +11,14 @@ const sleep = (delay: number) => {
setTimeout(resolve, delay)
})
}
axios.defaults.baseURL = import.meta.env.VITE_API_URL
// Backend URL: container runtime config first (/config.js, written from
// VITE_API_URL at container start), build-time env as fallback for dev.
declare global {
interface Window {
__APP_CONFIG__?: { VITE_API_URL?: string }
}
}
axios.defaults.baseURL = window.__APP_CONFIG__?.VITE_API_URL ?? import.meta.env.VITE_API_URL
axios.interceptors.request.use((config) => {
const token = userToken.get()
+1
View File
@@ -80,6 +80,7 @@ const ActiveFilters = () => {
)
}}
><Link
color="inherit"
onMouseDown={() => setSearchModalOpen.set(true)}>{searchParams}</Link>
</Tag>
</>
+3 -2
View File
@@ -191,9 +191,10 @@ export default function NavBar() {
colorPalette={'blue'}
size={'md'}
asChild
w={'70%'}
px={4}
borderWidth={0}
>
<RouteLink to="/games/create"><LuPlus/> <Text mt="1">Create Game</Text></RouteLink>
<RouteLink to="/games/create"><LuPlus/><Text mt="1">Create Game</Text></RouteLink>
</Button>
<Separator/>
<Heading as={'h3'} size={'md'}>Keyboard Shortcuts</Heading>
+1 -1
View File
@@ -4,7 +4,7 @@ import { VStack, Image, Button, Heading, Flex, Center } from '@chakra-ui/react'
const Home = () => {
return (
<Flex align={'center'} bgImage={'linear-gradient(to bottom right, #314755, #26a0da)'} height={'100vh'}>
<Flex align={'center'} bgImage={'linear-gradient(to bottom right, #314755, #26a0da)'} minH={'100dvh'} w={'100%'}>
<Center w={'100%'}>
<VStack gap={4} align={'center'} margin={'auto'}>
<Image src="/assets/Logo.svg" alt="Games Database Logo"/>
+6 -5
View File
@@ -5,7 +5,7 @@ import { login } from '../../stateManagement/userState'
import {
Container,
Button,
Input, HStack, Center,
Input, Center, Flex,
} from '@chakra-ui/react'
import {Field} from '../ui/field'
import {useColorModeValue} from '../ui/color-mode'
@@ -35,7 +35,8 @@ export default function LoginForm() {
)
return (
<Container style={{ marginTop: '7rem', paddingBottom: '5rem' }}>
<Flex minH={'100dvh'} w={'100%'} align={'center'} justify={'center'}>
<Container maxW={'md'} w={'full'} py={'4'}>
<form onSubmit={onSubmit}>
<Field label="Email" invalid={Boolean(errors.email)} errorText={errors.email?.type === 'required' ? errors.email.message?.toString() : errors.email?.type === 'pattern' ? 'Invalid Email Address' : undefined} mb={'3'}>
<Input
@@ -65,8 +66,7 @@ export default function LoginForm() {
{...register('password', { required: 'You must specify a password' })}
/>
</Field>
<br/>
<HStack>
<Center mt={'6'}>
<Button
type="submit"
colorPalette="blue"
@@ -75,11 +75,12 @@ export default function LoginForm() {
>
Submit
</Button>
</HStack>
</Center>
<Center mt={'5rem'}>
<Button asChild variant="outline"><Link to={'/forgotpassword'}>Forgot Password</Link></Button>
</Center>
</form>
</Container>
</Flex>
)
}
+5
View File
@@ -349,6 +349,11 @@ const config = defineConfig({
bg: { base: 'gray.200', _dark: 'gray.800' },
fontSize: '1.0625rem',
},
// Native text-selection was near-invisible on dark surfaces.
// Match the hover-selection treatment (gray.200 / whiteAlpha.300).
'::selection': {
bg: { base: 'gray.200', _dark: 'whiteAlpha.300' },
},
},
})