Update dependencies, containerize, add build workflow
All checks were successful
Build / build (push) Successful in 1m14s

This commit is contained in:
Moritz Ruth 2025-03-03 00:35:08 +01:00
parent ae1c71f4c5
commit 88f0632194
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
38 changed files with 1139 additions and 829 deletions

3
.dockerignore Normal file
View file

@ -0,0 +1,3 @@
/node_modules
/dist
/.idea

View file

@ -0,0 +1,34 @@
name: "Build"
on:
push:
branches:
- main
jobs:
build:
runs-on: "docker"
container:
image: "code.forgejo.org/oci/node:22-alpine"
steps:
- name: "Checkout"
uses: "https://code.forgejo.org/actions/checkout@v4"
- name: "Install Docker"
run: |
apk add docker
- name: "Login to the container registry"
uses: "https://code.forgejo.org/docker/login-action@v3"
with:
registry: "git.moritzruth.de"
username: "moritzruth"
password: "${{ secrets.PACKAGES_TOKEN }}"
- name: "Build and push the container image"
uses: "https://code.forgejo.org/docker/build-push-action@v6"
with:
context: "."
file: "./Containerfile"
push: true
tags: "git.moritzruth.de/${{ github.repository }}:latest"

3
.gitignore vendored
View file

@ -1,4 +1,3 @@
.idea/ .idea/
node_modules/ node_modules/
dist/ dist/
*.env

1
.nvmrc
View file

@ -1 +0,0 @@
22

22
Containerfile Normal file
View file

@ -0,0 +1,22 @@
FROM node:22-alpine AS builder
WORKDIR /app
COPY . .
RUN npm install --global pnpm
RUN pnpm install --frozen-lockfile
RUN pnpm build
FROM node:22-alpine
RUN npm install --global pnpm
WORKDIR /app
COPY --from=builder /app/package.json /app/
COPY --from=builder /app/pnpm-lock.yaml /app/
COPY --from=builder /app/tsconfig.json /app/
COPY --from=builder /app/backend /app/backend
COPY --from=builder /app/shared /app/shared
COPY --from=builder /app/dist /app/dist
RUN pnpm install --frozen-lockfile --prod
EXPOSE 3000
ENV NODE_ENV=production
ENTRYPOINT ["pnpm", "start"]

View file

@ -1,4 +1,4 @@
import createExpressApp from "express" import createExpressApp, { static as staticMiddleware } from "express"
import { listen } from "listhen" import { listen } from "listhen"
import { WebSocketServer } from "ws" import { WebSocketServer } from "ws"
import { appRouter } from "./trpc" import { appRouter } from "./trpc"
@ -6,6 +6,7 @@ import { createExpressMiddleware as createTrpcMiddleware } from "@trpc/server/ad
import { applyWSSHandler } from "@trpc/server/adapters/ws" import { applyWSSHandler } from "@trpc/server/adapters/ws"
import { createContext } from "./trpc/base" import { createContext } from "./trpc/base"
import { isDev } from "./isDev" import { isDev } from "./isDev"
import { resolve } from "node:path"
const expressApp = createExpressApp() const expressApp = createExpressApp()
@ -14,7 +15,9 @@ expressApp.use("/trpc", createTrpcMiddleware({
createContext: ({ req, res }) => createContext(res), createContext: ({ req, res }) => createContext(res),
})) }))
const { server } = await listen(expressApp, { isProd: !isDev, autoClose: false, port: 3001 }) if (!isDev) expressApp.use(staticMiddleware(resolve(import.meta.dirname, "../dist")))
const { server } = await listen(expressApp, { isProd: !isDev, autoClose: false })
const wss = new WebSocketServer({ server, path: "/ws" }) const wss = new WebSocketServer({ server, path: "/ws" })
const wssTrpcHandler = applyWSSHandler({ const wssTrpcHandler = applyWSSHandler({

View file

@ -56,11 +56,19 @@
</style> </style>
<style> <style>
body { *, *::before, *:after {
box-sizing: border-box;
}
button {
border: none;
color: inherit;
}
html, body, #app {
min-height: 100vh; min-height: 100vh;
width: 100vw; width: 100vw;
overflow-x: hidden; overflow-x: hidden;
font-size: 16px;
user-select: none; user-select: none;
} }

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -62,10 +62,10 @@
import TrashIcon from "virtual:icons/ph/trash-bold" import TrashIcon from "virtual:icons/ph/trash-bold"
import CheckIcon from "virtual:icons/ph/check-bold" import CheckIcon from "virtual:icons/ph/check-bold"
import HandPointingIcon from "virtual:icons/ph/hand-pointing-duotone" import HandPointingIcon from "virtual:icons/ph/hand-pointing-duotone"
import type { InteractionQueueItem } from "../shared/script/types" import type { InteractionQueueItem } from "../../shared/script/types"
import ObjectPicture from "./ObjectPicture.vue" import ObjectPicture from "./ObjectPicture.vue"
import { useGame } from "../game" import { useGame } from "../game"
import { findMatchingCombination } from "../shared/util" import { findMatchingCombination } from "../../shared/util"
const props = defineProps<{ const props = defineProps<{
item: InteractionQueueItem item: InteractionQueueItem

View file

@ -37,7 +37,7 @@
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import type { GameObject } from "../shared/script/types" import type { GameObject } from "../../shared/script/types"
import interact from "@interactjs/interact" import interact from "@interactjs/interact"
import { useCurrentElement } from "@vueuse/core" import { useCurrentElement } from "@vueuse/core"
import { computed, onMounted, onUnmounted, ref, useCssModule } from "vue" import { computed, onMounted, onUnmounted, ref, useCssModule } from "vue"

View file

@ -1,10 +1,10 @@
import { defineStore } from "pinia" import { defineStore } from "pinia"
import { computed, reactive, ref } from "vue" import { computed, reactive, ref } from "vue"
import { script } from "./shared/script" import { script } from "../shared/script"
import type { GameObject, Interaction, InteractionQueueItem } from "./shared/script/types" import type { GameObject, Interaction, InteractionQueueItem } from "../shared/script/types"
import { trpcClient } from "./trpc" import { trpcClient } from "./trpc"
import type { GameEvent } from "./shared/gameEvents" import type { GameEvent } from "../shared/gameEvents"
import { getInteractionQueueItemId } from "./shared/util" import { getInteractionQueueItemId } from "../shared/util"
export const useGame = defineStore("gameState", () => { export const useGame = defineStore("gameState", () => {
const currentRoom = computed(() => script.roomsById.get(currentRoomId.value)!) const currentRoom = computed(() => script.roomsById.get(currentRoomId.value)!)

View file

@ -1,4 +1,4 @@
import "virtual:windi.css" import "virtual:uno.css"
import "@interactjs/actions" import "@interactjs/actions"
import "@interactjs/auto-start" import "@interactjs/auto-start"
import "core-js/actual/iterator" import "core-js/actual/iterator"

View file

@ -3,21 +3,23 @@
<section class="w-80"> <section class="w-80">
<div class="font-bold text-2xl pb-2">Raum</div> <div class="font-bold text-2xl pb-2">Raum</div>
<div class="flex flex-col overflow-y-auto bg-dark-900 bg-opacity-50 h-80vh"> <div class="flex flex-col overflow-y-auto bg-dark-900 bg-opacity-50 h-80vh">
<div <template
v-for="room in script.roomsById.values()" v-for="(room, index) in script.roomsById.values()"
:key="room.id" :key="room.id"
class="flex-shrink-0 bg-dark-600 not-last:border-b border-solid border-dark-300 flex items-center"
> >
<div class="px-3 py-2 flex-grow"> <div v-if="index !== 0" class="w-full h-1px bg-dark-300"/>
{{ room.label }} <div class="flex-shrink-0 bg-dark-600 flex items-center">
<div class="px-3 py-2 flex-grow">
{{ room.label }}
</div>
<button v-if="game.currentRoomId === room.id" disabled class="bg-green-900 h-full px-3 text-sm cursor-not-allowed">
Aktiv
</button>
<button v-else class="bg-dark-300 h-full px-3 text-sm" @click="game.switchRoom(room.id)">
Aktivieren
</button>
</div> </div>
<button v-if="game.currentRoomId === room.id" disabled class="bg-green-900 h-full px-3 text-sm cursor-not-allowed"> </template>
Aktiv
</button>
<button v-else class="bg-dark-300 h-full px-3 text-sm" @click="game.switchRoom(room.id)">
Aktivieren
</button>
</div>
</div> </div>
</section> </section>
<section class="flex-grow"> <section class="flex-grow">
@ -61,7 +63,7 @@
</style> </style>
<script setup lang="ts"> <script setup lang="ts">
import { script } from "../shared/script" import { script } from "../../shared/script"
import { useGame } from "../game" import { useGame } from "../game"
import ObjectPicture from "../components/ObjectPicture.vue" import ObjectPicture from "../components/ObjectPicture.vue"
import InteractionQueueItemCard from "../components/InteractionQueueItemCard.vue" import InteractionQueueItemCard from "../components/InteractionQueueItemCard.vue"

View file

@ -1,6 +1,6 @@
import { createTRPCProxyClient, createWSClient, wsLink } from "@trpc/client" import { createTRPCProxyClient, createWSClient, wsLink } from "@trpc/client"
import superjson from "superjson" import superjson from "superjson"
import type { AppRouter } from "./server" import type { AppRouter } from "../backend"
const wsUrl = new URL(location.href) const wsUrl = new URL(location.href)
wsUrl.protocol = wsUrl.protocol.replace("http", "ws") wsUrl.protocol = wsUrl.protocol.replace("http", "ws")
@ -8,7 +8,7 @@ wsUrl.pathname = "/ws"
wsUrl.hash = "" wsUrl.hash = ""
if (import.meta.env.PROD) { if (import.meta.env.PROD) {
wsUrl.hostname = "server." + wsUrl.hostname wsUrl.hostname = "backend." + wsUrl.hostname
} }
export const trpcClient = createTRPCProxyClient<AppRouter>({ export const trpcClient = createTRPCProxyClient<AppRouter>({

View file

@ -4,9 +4,9 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Level Up</title> <title>Level Up</title>
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<script type="module" src="./src/index.ts"></script> <script type="module" src="frontend/index.ts"></script>
<link rel="stylesheet" href="./node_modules/@fontsource-variable/inter/wght.css"/> <link rel="stylesheet" href="./node_modules/@fontsource-variable/inter/wght.css"/>
<link rel="stylesheet" href="./node_modules/@fontsource/titan-one/400.css"/> <link rel="stylesheet" href="./node_modules/modern-normalize/modern-normalize.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View file

@ -3,50 +3,53 @@
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start:ui": "vite preview --host --port 3000", "start": "NODE_ENV=production tsx ./backend/main.ts",
"start:server": "NODE_ENV=production tsx ./src/server/main.ts", "build": "vite build",
"build:ui": "vite build", "dev:frontend": "vite",
"dev:ui": "vite --host", "dev:backend": "NODE_ENV=development tsx watch --clear-screen=false ./backend/main.ts"
"dev:server": "NODE_ENV=development tsx watch --clear-screen=false ./src/server/main.ts"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/ph": "^1.2.2", "@iconify-json/ph": "^1.2.2",
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "22.10.2", "@types/node": "^22.13.8",
"@types/ws": "^8.5.13", "@types/ws": "^8.5.14",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.1",
"sass": "^1.83.0", "sass": "^1.85.1",
"tsx": "^4.19.2", "typescript": "^5.8.2",
"typescript": "^5.7.2", "unocss": "66.1.0-beta.3",
"unplugin-icons": "^0.22.0", "unplugin-icons": "^22.1.0",
"vite": "^6.0.3", "vite": "^6.2.0"
"vite-plugin-windicss": "^1.9.4",
"windicss": "^3.5.6"
}, },
"dependencies": { "dependencies": {
"@fontsource-variable/inter": "^5.1.0", "@fontsource-variable/inter": "^5.2.5",
"@fontsource/titan-one": "^5.1.0",
"@headlessui/vue": "^1.7.23", "@headlessui/vue": "^1.7.23",
"@interactjs/actions": "^1.10.27", "@interactjs/actions": "^1.10.27",
"@interactjs/auto-start": "^1.10.27", "@interactjs/auto-start": "^1.10.27",
"@interactjs/interact": "^1.10.27", "@interactjs/interact": "^1.10.27",
"@trpc/client": "^10.45.2", "@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2", "@trpc/server": "^10.45.2",
"@vueuse/core": "^12.0.0", "@unocss/preset-wind3": "66.1.0-beta.3",
"@vueuse/integrations": "^12.0.0", "@vueuse/core": "^12.7.0",
"bufferutil": "^4.0.8", "@vueuse/integrations": "^12.7.0",
"core-js": "^3.40.0", "bufferutil": "^4.0.9",
"core-js": "^3.41.0",
"date-fns": "^4.1.0",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"express": "^4.21.2", "express": "^4.21.2",
"immer": "^10.1.1",
"listhen": "^1.9.0", "listhen": "^1.9.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"nanoid": "^5.0.9", "modern-normalize": "^3.0.1",
"pinia": "^2.3.0", "nanoid": "^5.1.2",
"pinia": "^3.0.1",
"superjson": "^2.2.2", "superjson": "^2.2.2",
"tsx": "^4.19.3",
"vue": "^3.5.13", "vue": "^3.5.13",
"ws": "^8.18.0", "vue-router": "^4.5.0",
"zod": "^3.24.1" "ws": "^8.18.1",
"zod": "^3.24.2"
},
"engines": {
"node": "22"
} }
} }

1704
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,31 +1,31 @@
{ {
"compilerOptions": { "compilerOptions": {
"declaration": true, "declaration": false,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"jsx": "preserve", "jsx": "preserve",
"lib": ["esnext", "dom"], "lib": ["esnext", "dom"],
"module": "esnext", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "bundler",
"allowJs": true, "allowJs": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"importsNotUsedAsValues": "error", "importsNotUsedAsValues": "error",
"isolatedModules": true, "isolatedModules": true,
"rootDir": "src",
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": false,
"strict": true, "strict": true,
"stripInternal": true, "stripInternal": true,
"target": "esnext", "target": "esnext",
"types": [ "types": [
"./src/types.d.ts",
"vite/client", "vite/client",
"unplugin-icons/types/vue", "unplugin-icons/types/vue",
"vite-plugin-pages/client" "vite-plugin-pages/client"
] ]
}, },
"include": [ "include": [
"src/**/*.ts", "backend/**/*.ts",
"src/**/*.vue" "shared/**/*.ts",
"frontend/**/*.ts",
"frontend/**/*.vue"
] ]
} }

31
uno.config.ts Normal file
View file

@ -0,0 +1,31 @@
import { defineConfig, transformerDirectives } from "unocss"
import { presetWind, colors } from "@unocss/preset-wind3"
export default defineConfig({
presets: [
presetWind({
arbitraryVariants: true,
preflight: true
})
],
theme: {
fontFamily: {
"normal": `"Inter Variable", sans-serif`
},
colors: {
transparent: "transparent",
white: colors.white,
black: colors.black,
gray: colors.zinc,
light: colors.light,
dark: colors.dark,
red: colors.rose,
yellow: colors.yellow,
green: colors.green,
blue: colors.indigo,
}
},
transformers: [
transformerDirectives()
]
})

View file

@ -1,20 +1,19 @@
import { defineConfig } from "vite" import { defineConfig } from "vite"
import vuePlugin from "@vitejs/plugin-vue" import vuePlugin from "@vitejs/plugin-vue"
import iconsPlugin from "unplugin-icons/vite" import iconsPlugin from "unplugin-icons/vite"
import windiPlugin from "vite-plugin-windicss" import unoCssPlugin from "unocss/vite"
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vuePlugin(), vuePlugin(),
iconsPlugin(), iconsPlugin(),
windiPlugin() unoCssPlugin()
], ],
server: { server: {
port: 3000,
proxy: { proxy: {
"/trpc": "http://localhost:3001", "/trpc": "http://localhost:3000",
"/ws": { "/ws": {
target: "http://localhost:3001", target: "http://localhost:3000",
ws: true ws: true
} }
} }

View file

@ -1,23 +0,0 @@
import { defineConfig } from "vite-plugin-windicss"
import colors from "windicss/colors"
export default defineConfig({
theme: {
fontFamily: {
"normal": ["InterVariable", "sans-serif"],
"fat": ["Titan One", "sans-serif"]
},
colors: {
transparent: "transparent",
white: "white",
black: "black",
gray: colors.zinc,
light: colors.light,
dark: colors.dark,
red: colors.rose,
yellow: colors.yellow,
green: colors.green,
blue: colors.indigo,
}
}
})