Compare commits

..

11 Commits

8 changed files with 104 additions and 12 deletions
+59
View File
@@ -0,0 +1,59 @@
name: Production Deploy
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
GH_USER: therealowenrees
IMAGE_NAME: therealowenrees/chess-scribe:latest
jobs:
publish:
name: publish image
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Login to Github Container Registry
run: echo "${{ secrets.GHCR_SECRET }}" | docker login ghcr.io -u ${{ env.GH_USER }} --password-stdin
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build image and publish with Cache
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VITE_API_BASE_URL=https://api.chess-scribe.org/api/v1
VITE_MATOMO_URL=https://analytics.owenrees.eu
VITE_MATOMO_SITE_ID="3"
VITE_MAINTENANCE_MODE=
VITE_MAINTENANCE_MODE_MESSAGE="ChessScribe is currently undergoing maintenance. Some features may be unavailable."
deploy:
needs: publish
name: deploy image
runs-on: ubuntu-24.04
steps:
- name: Connect and pull image from GHCR
uses: appleboy/ssh-action@v1.2.5
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.PORT }}
script: |
cd /home/${{ secrets.USERNAME }}
echo "${{ secrets.GHCR_SECRET }}" | docker login ghcr.io -u ${{ env.GH_USER }} --password-stdin
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
docker compose up -d
+15
View File
@@ -1,6 +1,21 @@
# --- Stage 1: Build Stage --- # --- Stage 1: Build Stage ---
FROM node:24-alpine AS builder FROM node:24-alpine AS builder
WORKDIR /app WORKDIR /app
# 1. Declare the build-time arguments Vite needs
ARG VITE_API_BASE_URL
ARG VITE_MATOMO_URL
ARG VITE_MATOMO_SITE_ID
ARG VITE_MAINTENANCE_MODE
ARG VITE_MAINTENANCE_MODE_MESSAGE
# 2. Assign them to environment variables so Vite detects them
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
ENV VITE_MATOMO_URL=$VITE_MATOMO_URL
ENV VITE_MATOMO_SITE_ID=$VITE_MATOMO_SITE_ID
ENV VITE_MAINTENANCE_MODE=$VITE_MAINTENANCE_MODE
ENV VITE_MAINTENANCE_MODE_MESSAGE=$VITE_MAINTENANCE_MODE_MESSAGE
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN corepack enable pnpm && pnpm i --frozen-lockfile RUN corepack enable pnpm && pnpm i --frozen-lockfile
COPY . . COPY . .
-2
View File
@@ -10,6 +10,4 @@
- react query on for study and chapter load, to allow caching - react query on for study and chapter load, to allow caching
- https://specification.website/checklist/ - https://specification.website/checklist/
- Custom headers do not work - the subtitle is appearing above every thing on first page, and the game starts 2nd page
- react query to cache the study and chapters - react query to cache the study and chapters
+11 -2
View File
@@ -1,17 +1,26 @@
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { Image } from "@unpic/react"; import { Image } from "@unpic/react";
import { z } from "zod";
import MaintenanceModeBanner from "#/components/MaintenanceModeBanner.tsx"; import MaintenanceModeBanner from "#/components/MaintenanceModeBanner.tsx";
import { env } from "#/env.ts"; import { env } from "#/env.ts";
import logo from "@/assets/images/logo.svg?url"; import logo from "@/assets/images/logo.svg?url";
export default function Header() { export default function Header() {
const maintenanceModeMessage = env.VITE_MAINTENANCE_MODE
? z
.string()
.transform((value) => (value.trim() === "" ? null : value))
.parse(env.VITE_MAINTENANCE_MODE_MESSAGE)
: null;
return ( return (
<header> <header>
{env.VITE_MAINTENANCE_MODE ? ( {maintenanceModeMessage ? (
<div className="absolute w-full"> <div className="absolute w-full">
<MaintenanceModeBanner message={env.VITE_MAINTENANCE_MODE_MESSAGE} /> <MaintenanceModeBanner message={maintenanceModeMessage} />
</div> </div>
) : null} ) : null}
<nav className="w-full max-w-screen-2xl place-self-center p-8"> <nav className="w-full max-w-screen-2xl place-self-center p-8">
<div className="container mx-auto flex items-center justify-between"> <div className="container mx-auto flex items-center justify-between">
<div className="flex items-center"> <div className="flex items-center">
+2
View File
@@ -28,6 +28,8 @@ const LichessStudyLinkInput = ({ setSelectedStudyId }: IProps) => {
return; return;
} }
console.log(result.data);
setSelectedStudyId(result.data); setSelectedStudyId(result.data);
toast.success("Study loaded — pick a chapter"); toast.success("Study loaded — pick a chapter");
}; };
+8 -4
View File
@@ -2,11 +2,11 @@ import {
type Dispatch, type Dispatch,
useActionState, useActionState,
useEffect, useEffect,
useRef, // useRef,
useState, useState,
} from "react"; } from "react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { useClickOutside } from "#/hooks/useClickOutside.ts"; // import { useClickOutside } from "#/hooks/useClickOutside.ts";
import { useLichess } from "#/hooks/useLichess.ts"; import { useLichess } from "#/hooks/useLichess.ts";
import type { IUserStudyChapter } from "#/interfaces.ts"; import type { IUserStudyChapter } from "#/interfaces.ts";
import type { GameAction } from "#/reducers/gameReducer.ts"; import type { GameAction } from "#/reducers/gameReducer.ts";
@@ -31,7 +31,7 @@ const initialChapterState: ChapterState = {
const SelectStudyChapter = ({ selectedStudyId, gameDispatch }: IProps) => { const SelectStudyChapter = ({ selectedStudyId, gameDispatch }: IProps) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const dropdownRef = useRef<HTMLDivElement>(null); // const dropdownRef = useRef<HTMLDivElement>(null);
const { getLichessStudyChapters, loadLichessStudyChapter } = useLichess(); const { getLichessStudyChapters, loadLichessStudyChapter } = useLichess();
@@ -47,6 +47,9 @@ const SelectStudyChapter = ({ selectedStudyId, gameDispatch }: IProps) => {
const result = await getLichessStudyChapters({ const result = await getLichessStudyChapters({
studyId: selectedStudyId, studyId: selectedStudyId,
}); });
console.log(result);
if (result.ok) { if (result.ok) {
return { return {
status: "success", status: "success",
@@ -54,6 +57,7 @@ const SelectStudyChapter = ({ selectedStudyId, gameDispatch }: IProps) => {
error: null, error: null,
} satisfies ChapterState; } satisfies ChapterState;
} }
return { return {
status: "error", status: "error",
chapters: [], chapters: [],
@@ -67,7 +71,7 @@ const SelectStudyChapter = ({ selectedStudyId, gameDispatch }: IProps) => {
chapter.name.toLowerCase().includes(search.toLowerCase()), chapter.name.toLowerCase().includes(search.toLowerCase()),
); );
useClickOutside({ isOpen, setIsOpen, setSearch, ref: dropdownRef }); // useClickOutside({ isOpen, setIsOpen, setSearch, ref: dropdownRef });
useEffect(() => { useEffect(() => {
if (isPending || chapterState.status === "success") { if (isPending || chapterState.status === "success") {
+4 -2
View File
@@ -1,8 +1,8 @@
import { z } from "zod"; import { z } from "zod";
const clientSchema = z.object({ const clientSchema = z.object({
VITE_API_BASE_URL: z.string("Invalid API Base URL format"), VITE_API_BASE_URL: z.url("Invalid API Base URL format"),
VITE_MATOMO_URL: z.string("Invalid URL format"), VITE_MATOMO_URL: z.url("Invalid URL format"),
VITE_MATOMO_SITE_ID: z.string("Invalid Site ID"), VITE_MATOMO_SITE_ID: z.string("Invalid Site ID"),
VITE_MAINTENANCE_MODE: z.string("Invalid Maintenance Mode"), VITE_MAINTENANCE_MODE: z.string("Invalid Maintenance Mode"),
VITE_MAINTENANCE_MODE_MESSAGE: z.string().optional(), VITE_MAINTENANCE_MODE_MESSAGE: z.string().optional(),
@@ -27,6 +27,7 @@ const getEnv = () => {
const _env = fullSchema.safeParse(rawEnv); const _env = fullSchema.safeParse(rawEnv);
if (!_env.success) { if (!_env.success) {
console.error("❌ Invalid Server Environment Variables:"); console.error("❌ Invalid Server Environment Variables:");
console.error(JSON.stringify(_env.error.format(), null, 2));
throw new Error("Invalid environment variables"); throw new Error("Invalid environment variables");
} }
return _env.data; return _env.data;
@@ -34,6 +35,7 @@ const getEnv = () => {
const _env = clientSchema.safeParse(rawEnv); const _env = clientSchema.safeParse(rawEnv);
if (!_env.success) { if (!_env.success) {
console.error("❌ Invalid Client Environment Variables:"); console.error("❌ Invalid Client Environment Variables:");
console.error(JSON.stringify(_env.error.format(), null, 2));
throw new Error("Invalid environment variables"); throw new Error("Invalid environment variables");
} }
return _env.data as z.infer<typeof fullSchema>; return _env.data as z.infer<typeof fullSchema>;
+3
View File
@@ -65,7 +65,10 @@ const Chessboard = () => {
/> />
) : null} ) : null}
{user ? ( {user ? (
<div className="flex flex-col">
<p className="text-center">or</p>
<LichessStudyLinkInput setSelectedStudyId={setSelectedStudyId} /> <LichessStudyLinkInput setSelectedStudyId={setSelectedStudyId} />
</div>
) : null} ) : null}
</Section> </Section>