commit 01

This commit is contained in:
Moritz Ruth 2024-12-26 18:54:19 +01:00
commit 08b0f87596
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
38 changed files with 4021 additions and 0 deletions

3
.gitignore vendored Normal file
View file

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

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
22

14
index.html Normal file
View file

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

53
package.json Normal file
View file

@ -0,0 +1,53 @@
{
"name": "level-up",
"version": "1.0.0",
"type": "module",
"scripts": {
"start:ui": "vite preview --host --port 4000",
"start:server": "NODE_ENV=production tsx ./src/server/main.ts",
"build:ui": "vite build",
"dev:ui": "vite",
"dev:server": "NODE_ENV=development tsx watch --clear-screen=false ./src/server/main.ts"
},
"devDependencies": {
"@iconify-json/ph": "^1.2.2",
"@types/express": "^5.0.0",
"@types/lodash-es": "^4.17.12",
"@types/node": "22.10.2",
"@types/ws": "^8.5.13",
"@vitejs/plugin-vue": "^5.2.1",
"sass": "^1.83.0",
"tsx": "^4.19.2",
"typescript": "^5.7.2",
"unplugin-icons": "^0.22.0",
"vite": "^6.0.3",
"vite-plugin-pages": "^0.32.4",
"vite-plugin-windicss": "^1.9.4",
"windicss": "^3.5.6"
},
"dependencies": {
"@fontsource-variable/inter": "^5.1.0",
"@fontsource/titan-one": "^5.1.0",
"@headlessui/vue": "^1.7.23",
"@interactjs/actions": "^1.10.27",
"@interactjs/auto-start": "^1.10.27",
"@interactjs/interact": "^1.10.27",
"@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2",
"@vueuse/core": "^12.0.0",
"@vueuse/integrations": "^12.0.0",
"bufferutil": "^4.0.8",
"eventemitter3": "^5.0.1",
"express": "^4.21.2",
"immer": "^10.1.1",
"listhen": "^1.9.0",
"lodash-es": "^4.17.21",
"nanoid": "^5.0.9",
"pinia": "^2.3.0",
"superjson": "^2.2.2",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"ws": "^8.18.0",
"zod": "^3.24.1"
}
}

3068
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load diff

BIN
public/objects/haustür.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/objects/kakao.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
public/objects/milch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

93
src/App.vue Normal file
View file

@ -0,0 +1,93 @@
<template>
<div class="bg-gray-900 h-100vh w-100vw overflow-hidden text-white">
<div :class="$style.noise"/>
<div :class="$style.vignette"/>
<div class="relative h-full">
<div v-if="isLoading" class="flex flex-col justify-center items-center text-lg inset-0 absolute">
<span class="text-center">Verbindung wird hergestellt</span>
</div>
<main>
<InteractionsScreen/>
</main>
</div>
</div>
</template>
<style module lang="scss">
body {
user-select: none;
}
.noise, .vignette {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.noise {
background: url("./assets/noise.png") repeat;
opacity: 10%;
}
.vignette {
background: radial-gradient(rgb(0 0 0 / 0%) 40%, rgb(0 0 0 / 20%));
}
</style>
<style>
body {
min-height: 100vh;
width: 100vw;
overflow-x: hidden;
font-size: 16px;
user-select: none;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 200ms ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-down-enter-active,
.slide-down-leave-active {
position: absolute;
transition: 200ms ease;
transition-property: opacity, transform;
}
.slide-down-enter-from,
.slide-down-leave-to {
opacity: 0;
transform: translateY(40%);
}
</style>
<script setup lang="ts">
import { ref } from "vue"
import { trpcClient } from "./trpc"
import { useGame } from "./game"
import InteractionsScreen from "./screens/InteractionsScreen.vue"
const isLoading = ref(true)
const game = useGame()
trpcClient.join.subscribe(undefined, {
onStarted: () => {
isLoading.value = false
},
onData: game.handleGameEvent,
onError: error => {
console.error("🔴", error)
},
onStopped() {
location.reload()
},
})
</script>

BIN
src/assets/noise.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,82 @@
<template>
<div
class="flex flex-col items-center gap-2 bg-dark-600 rounded-lg p-3 border border-solid"
:class="[$style.root, borderClass]"
:style="{
transform: dragPosition === null ? undefined : `translate(${dragPosition.x}px, ${dragPosition.y}px) scale(${isOverDropzone ? 0.5 : 1})`,
transformOrigin: pointerCoordinates === null ? undefined : `${pointerCoordinates.x}px ${pointerCoordinates.y}px`,
}"
:data-object-id="object.id"
>
<img :src="`/objects/${props.object.id}.png`" :alt="object.label" class="invert filter object-contain max-w-15"/>
<div class="text-sm text-gray-300">
{{ object.label }}
</div>
</div>
</template>
<style module lang="scss">
.root {
touch-action: none;
transition: transform 50ms ease, background-color 200ms ease;
}
</style>
<script setup lang="ts">
import type { GameObject } from "../shared/script/types"
import interact from "@interactjs/interact"
import { useCurrentElement } from "@vueuse/core"
import { computed, onMounted, onUnmounted, ref } from "vue"
const props = defineProps<{
object: GameObject
isOverDropzone: boolean
markedFor: null | "use" | "combine"
}>()
const emit = defineEmits<{
"drag-start": []
"drag-end": []
}>()
const element = useCurrentElement()
const dragPosition = ref<null | { x: number; y: number }>(null)
const pointerCoordinates = ref<null | { x: number; y: number }>(null)
onMounted(() => {
const interactable = interact(element.value as HTMLElement).draggable({
listeners: {
start(event) {
const elementBounds = (element.value as HTMLElement).getBoundingClientRect()
pointerCoordinates.value = {
x: event.clientX0 - elementBounds.x,
y: event.clientY0 - elementBounds.y,
}
dragPosition.value = { x: 0, y: 0 }
emit("drag-start")
},
move(event) {
dragPosition.value = {
x: (dragPosition.value?.x ?? 0) + event.dx,
y: (dragPosition.value?.y ?? 0) + event.dy,
}
},
end(event) {
dragPosition.value = null
emit("drag-end")
}
}
})
onUnmounted(() => interactable.unset())
})
const borderClass = computed(() => {
switch (props.markedFor) {
case null: return "border-dark-600"
case "use": return "border-green-600"
case "combine": return "border-blue-900"
}
})
</script>

View file

@ -0,0 +1,64 @@
<template>
<div
class="flex items-center justify-center text-xl w-0 flex-grow py-4 bg-green-900 bg-opacity-60"
:class="$style.root"
:data-has-floating="hasFloating"
>
<span>{{ label }}</span>
</div>
</template>
<style module lang="scss">
.root {
transition: transform 200ms ease;
&[data-has-floating="true"] {
transform: scale(1.2)
}
}
</style>
<script setup lang="ts">
import interact from "@interactjs/interact"
import { useCurrentElement } from "@vueuse/core"
import { onMounted, onUnmounted } from "vue"
const props = defineProps<{
label: string
hasFloating: boolean
}>()
const emit = defineEmits<{
"object-change": [string, boolean]
"object-drop": [string]
}>()
const element = useCurrentElement()
onMounted(() => {
const interactable = interact(element.value as HTMLElement).dropzone({
overlap: "pointer",
ondragenter(event) {
const draggedElement = event.relatedTarget
const objectId = draggedElement.dataset?.objectId
if (objectId === undefined) return
emit("object-change", objectId, true)
},
ondragleave(event) {
const draggedElement = event.relatedTarget
const objectId = draggedElement.dataset?.objectId
if (objectId === undefined) return
emit("object-change", objectId, false)
},
ondrop(event) {
const draggedElement = event.relatedTarget
const objectId = draggedElement.dataset?.objectId
if (objectId === undefined) return
emit("object-change", objectId, false)
emit("object-drop", objectId)
}
})
onUnmounted(() => interactable.unset())
})
</script>

63
src/game.ts Normal file
View file

@ -0,0 +1,63 @@
import { defineStore } from "pinia"
import { computed, reactive, ref } from "vue"
import { script } from "./shared/script"
import type { Interaction, InteractionQueueItem } from "./shared/script/types"
import { trpcClient } from "./trpc"
import type { GameEvent } from "./shared/gameEvents"
import { getInteractionQueueItemId } from "./shared/util"
export const useGame = defineStore("gameState", () => {
const currentRoomId = ref(script.roomsById.values().next().value!.id)
const interactionQueue = reactive(new Map<string, InteractionQueueItem>())
const currentInteraction = ref<null | Interaction>(null)
const currentInteractionId = computed(() =>
currentInteraction.value === null ? null : getInteractionQueueItemId(currentInteraction.value))
return {
currentRoomId,
interactionQueue,
currentRoom: computed(() => script.roomsById.get(currentRoomId.value)!),
currentInteraction,
currentInteractionId,
voteForInteraction(interaction: Interaction) {
if (currentInteractionId.value === getInteractionQueueItemId(interaction)) return
if (currentInteractionId.value !== null) trpcClient.player.removeInteractionVote.mutate({ queueItemId: currentInteractionId.value })
currentInteraction.value = interaction
trpcClient.player.voteForInteraction.mutate({ interaction })
},
removeInteractionVote(queueItemId: string) {
trpcClient.player.removeInteractionVote.mutate({ queueItemId })
currentInteraction.value = null
},
switchRoom(roomId: string) {
trpcClient.director.switchRoom.mutate({ roomId })
},
removeInteractionQueueItem(id: string) {
trpcClient.director.removeInteractionQueueItem.mutate({ id })
},
handleGameEvent(event: GameEvent) {
console.log(event)
switch (event.type) {
case "room-changed":
interactionQueue.clear()
currentRoomId.value = event.roomId
currentInteraction.value = null
break
case "interaction-queued":
interactionQueue.set(event.item.id, event.item)
break
case "interaction-votes-changed":
if (event.votes <= 0) {
interactionQueue.delete(event.id)
if (currentInteractionId.value === event.id) currentInteraction.value = null
} else {
interactionQueue.get(event.id)!.votes = event.votes
}
break
}
}
}
})

19
src/index.ts Normal file
View file

@ -0,0 +1,19 @@
import "virtual:windi.css"
import "@interactjs/actions"
import "@interactjs/auto-start"
import { createApp } from "vue"
import { createRouter, createWebHistory } from "vue-router"
import App from "./App.vue"
import routes from "virtual:generated-pages"
import { createPinia } from "pinia"
const router = createRouter({
routes,
history: createWebHistory()
})
createApp(App)
.use(router)
.use(createPinia())
.mount("#app")

View file

@ -0,0 +1,11 @@
<template>
<
</template>
<style module lang="scss">
</style>
<script setup lang="ts">
</script>

View file

@ -0,0 +1,91 @@
<template>
<div class="h-100vh flex flex-col relative">
<div class="flex-shrink-0 flex order-1">
<ObjectCardDropZone
label="Use"
class="bg-green-800"
:has-floating="useFloatingObjectIds.size > 0"
@object-change="(v, i) => createSetInclusionSetter(useFloatingObjectIds)(v, i)"
@object-drop="onObjectUseDrop"
/>
<ObjectCardDropZone
label="Combine"
class="bg-blue-900"
:has-floating="combineFloatingObjectIds.size > 0"
@object-change="(v, i) => createSetInclusionSetter(combineFloatingObjectIds)(v, i)"
/>
</div>
<div ref="objectsContainerElement" class="grid gap-3 grid-cols-2 flex-grow auto-rows-min p-4">
<ObjectCard
v-for="object in game.currentRoom.initialObjects"
:key="object.id"
:object="object"
:is-over-dropzone="allFloatingObjectIds.has(object.id)"
:marked-for="getMarkedFor(object.id)"
@drag-start="onObjectDragStart"
@drag-end="onObjectDragEnd"
/>
</div>
</div>
</template>
<style module lang="scss">
</style>
<script setup lang="ts">
import ObjectCard from "../components/ObjectCard.vue"
import { useGame } from "../game"
import { computed, reactive, ref } from "vue"
import { useScrollLock } from "@vueuse/core"
import ObjectCardDropZone from "../components/ObjectCardDropZone.vue"
const game = useGame()
const dragCounter = ref(0)
const objectsContainerElement = ref<HTMLElement>()
const objectsContainerScrollLock = useScrollLock(objectsContainerElement)
const useFloatingObjectIds = reactive(new Set())
const combineFloatingObjectIds = reactive(new Set())
const allFloatingObjectIds = computed(() => new Set([...useFloatingObjectIds, ...combineFloatingObjectIds]))
function createSetInclusionSetter<T>(set: Set<T>) {
return (value: T, isIncluded: boolean) => {
if (isIncluded) set.add(value)
else set.delete(value)
}
}
function getMarkedFor(objectId: string) {
if (game.currentInteraction !== null) {
switch (game.currentInteraction.type) {
case "use":
if (game.currentInteraction.objectId === objectId) return "use"
break
case "combine":
if (game.currentInteraction.objectIds.has(objectId)) return "combine"
break
}
}
return null
}
function onObjectDragStart() {
dragCounter.value++
}
function onObjectDragEnd() {
dragCounter.value--
if (dragCounter.value <= 0) objectsContainerScrollLock.value = false
}
function onObjectUseDrop(objectId: string) {
game.voteForInteraction({
type: "use",
objectId
})
}
</script>

69
src/server/game.ts Normal file
View file

@ -0,0 +1,69 @@
import EventEmitter from "eventemitter3"
import type { GameEvent } from "../shared/gameEvents"
import type { Interaction, InteractionQueueItem } from "../shared/script/types"
import { script } from "../shared/script"
import { getInteractionQueueItemId } from "../shared/util"
interface Events {
"game-event": [GameEvent]
}
export class Game extends EventEmitter<Events> {
private currentRoomId: string = script.roomsById.values().next()!.value!.id
private interactionQueue: Map<string, InteractionQueueItem> = new Map()
constructor() {
super()
}
getInitialStateEvents(): GameEvent[] {
const events: GameEvent[] = []
events.push({ type: "room-changed", roomId: this.currentRoomId })
this.interactionQueue.forEach(item => events.push({ type: "interaction-queued", item }))
return events
}
addInteractionVote(interaction: Interaction) {
const id = getInteractionQueueItemId(interaction)
const existingItem = this.interactionQueue.get(id)
if (existingItem === undefined) {
const item = {
id,
votes: 1,
interaction
}
this.interactionQueue.set(item.id, item)
this.emit("game-event", { type: "interaction-queued", item })
} else {
existingItem.votes += 1
this.emit("game-event", { type: "interaction-votes-changed", id: existingItem.id, votes: existingItem.votes })
}
}
removeInteractionVote(id: string) {
const item = this.interactionQueue.get(id)
if (item !== undefined) {
item.votes -= 1
this.emit("game-event", { type: "interaction-votes-changed", id: item.id, votes: item.votes })
if (item.votes <= 0) this.interactionQueue.delete(id)
}
}
switchRoom(roomId: string) {
this.currentRoomId = roomId
this.interactionQueue.clear()
this.emit("game-event", { type: "room-changed", roomId })
}
removeInteractionQueueItem(id: string) {
if (!this.interactionQueue.has(id)) return
this.emit("game-event", { type: "interaction-votes-changed", id: id, votes: 0 })
this.interactionQueue.delete(id)
}
}
export const game = new Game()

1
src/server/index.ts Normal file
View file

@ -0,0 +1 @@
export type { AppRouter } from "./trpc"

1
src/server/isDev.ts Normal file
View file

@ -0,0 +1 @@
export const isDev = process.env.NODE_ENV === "development"

38
src/server/main.ts Normal file
View file

@ -0,0 +1,38 @@
import createExpressApp from "express"
import { listen } from "listhen"
import { WebSocketServer } from "ws"
import { appRouter } from "./trpc"
import { createExpressMiddleware as createTrpcMiddleware } from "@trpc/server/adapters/express"
import { applyWSSHandler } from "@trpc/server/adapters/ws"
import { createContext } from "./trpc/base"
import { isDev } from "./isDev"
const expressApp = createExpressApp()
expressApp.use("/trpc", createTrpcMiddleware({
router: appRouter,
createContext: ({ req, res }) => createContext(res),
}))
const { server } = await listen(expressApp, { isProd: !isDev, autoClose: false })
const wss = new WebSocketServer({ server, path: "/ws" })
const wssTrpcHandler = applyWSSHandler({
wss,
router: appRouter,
createContext: () => createContext(undefined)
})
const stop = () => {
console.log("Received stop signal")
server.close()
wssTrpcHandler.broadcastReconnectNotification()
wss.close(console.error)
}
process.on("SIGTERM", stop)
process.on("SIGINT", stop)
process.on("exit", () => {
console.log("exit")
})

17
src/server/trpc/base.ts Normal file
View file

@ -0,0 +1,17 @@
import { initTRPC } from "@trpc/server"
import type { Response } from "express"
import superjson from "superjson"
export interface Context {
res?: Response
}
export async function createContext(res: Response | undefined): Promise<Context> {
return {
res
}
}
export const t = initTRPC.context<Context>().create({
transformer: superjson
})

View file

@ -0,0 +1,20 @@
import { t } from "./base"
import { z } from "zod"
export const directorRouter = t.router({
switchRoom: t.procedure
.input(z.object({
roomId: z.string()
}))
.mutation(async ({ input, ctx }) => {
ctx.game.switchRoom(input.roomId)
}),
removeInteractionQueueItem: t.procedure
.input(z.object({
id: z.string()
}))
.mutation(async ({ input, ctx }) => {
ctx.game.removeInteractionQueueItem(input.id)
}),
})

29
src/server/trpc/index.ts Normal file
View file

@ -0,0 +1,29 @@
import { t } from "./base"
import { playerRouter } from "./player"
import { observable } from "@trpc/server/observable"
import type { GameEvent } from "../../shared/gameEvents"
import { directorRouter } from "./director"
import { game } from "../game"
export const appRouter = t.router({
player: playerRouter,
director: directorRouter,
join: t.procedure
.subscription(({ ctx }) => {
return observable<GameEvent>(emit => {
const handleGameEvent = (event: GameEvent) => emit.next(event)
const handleDestroyed = () => setTimeout(() => emit.complete(), 500)
game.on("game-event", handleGameEvent)
game.getInitialStateEvents().forEach(event => emit.next(event))
return () => {
game.off("game-event", handleGameEvent)
}
})
}),
})
export type AppRouter = typeof appRouter

22
src/server/trpc/player.ts Normal file
View file

@ -0,0 +1,22 @@
import { t } from "./base"
import { z } from "zod"
import { interactionSchema } from "../../shared/script/types"
import { game } from "../game"
export const playerRouter = t.router({
voteForInteraction: t.procedure
.input(z.object({
interaction: interactionSchema
}))
.mutation(async ({ input, ctx }) => {
game.addInteractionVote(input.interaction)
}),
removeInteractionVote: t.procedure
.input(z.object({
queueItemId: z.string()
}))
.mutation(async ({ input, ctx }) => {
game.removeInteractionVote(input.queueItemId)
}),
})

6
src/shared/gameEvents.ts Normal file
View file

@ -0,0 +1,6 @@
import type { InteractionQueueItem } from "./script/types"
export type GameEvent =
| { type: "room-changed"; roomId: string }
| { type: "interaction-queued"; item: InteractionQueueItem }
| { type: "interaction-votes-changed"; id: string; votes: number }

View file

@ -0,0 +1,12 @@
import type { Script } from "./types"
import { roomHauseingang } from "./rooms/hauseingang"
import { roomKueche } from "./rooms/küche"
const script: Script = {
roomsById: new Map
}
script.roomsById.set("hauseingang", roomHauseingang)
script.roomsById.set("küche", roomKueche)
export { script }

View file

@ -0,0 +1,41 @@
import type { Room } from "../types"
import { cSet } from "../../util"
export const roomHauseingang: Room = {
id: "hauseingang",
label: "Hauseingang",
initialObjects: cSet(
{
id: "schlüssel",
label: "Schlüssel",
},
{
id: "haustür",
label: "Haustür"
}
),
hiddenObjects: cSet(
{
id: "offene-haustür",
label: "Offene Haustür"
}
),
combinations: cSet(
{
id: "open-door",
inputs: cSet(
{
objectId: "schlüssel",
isConsumed: false
},
{
objectId: "haustür",
isConsumed: true
}
),
outputIds: cSet(
"offene-haustür"
)
}
)
}

View file

@ -0,0 +1,49 @@
import type { Room } from "../types"
import { cSet } from "../../util"
export const roomKueche: Room = {
id: "küche",
label: "Küche",
initialObjects: cSet(
{
id: "schüssel",
label: "Schüssel"
},
{
id: "kühlschrank",
label: "Kühlschrank"
},
{
id: "kakaopulver",
label: "Kakaopulver"
}
),
hiddenObjects: cSet(
{
id: "milch",
label: "Milch"
},
{
id: "kakao",
label: "Kakao"
}
),
combinations: cSet(
{
id: "kakao",
inputs: cSet(
{
objectId: "milch",
isConsumed: true
},
{
objectId: "kakaopulver",
isConsumed: false
}
),
outputIds: cSet(
"kakao"
)
}
)
}

View file

@ -0,0 +1,46 @@
import { z } from "zod"
export interface Script {
roomsById: Map<string, Room>
}
export interface Room {
id: string
label: string
initialObjects: Set<GameObject>
hiddenObjects: Set<GameObject>
combinations: Set<Combination>
}
export interface GameObject {
id: string
label: string
}
export interface Combination {
id: string
inputs: Set<{
objectId: string
isConsumed: boolean
}>
outputIds: Set<string>
}
export const interactionSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("use"),
objectId: z.string()
}),
z.object({
type: z.literal("combine"),
objectIds: z.set(z.string())
})
])
export type Interaction = z.infer<typeof interactionSchema>
export interface InteractionQueueItem {
id: string
votes: number
interaction: Interaction
}

12
src/shared/util.ts Normal file
View file

@ -0,0 +1,12 @@
import type { Interaction } from "./script/types"
export const cSet = <T>(...values: T[]) => new Set(values)
export const cMap = <K extends string | number | symbol, V>(object: Record<K, V>) => new Map(Object.entries(object))
export function getInteractionQueueItemId(interaction: Interaction) {
let id: string
if (interaction.type === "use") id = `use-${interaction.objectId}`
else if (interaction.type === "combine") id = `combine-${[...interaction.objectIds].sort().join("_")}`
else throw new Error("Unknown interaction type")
return id
}

19
src/trpc.ts Normal file
View file

@ -0,0 +1,19 @@
import { createTRPCProxyClient, createWSClient, wsLink } from "@trpc/client"
import superjson from "superjson"
import type { AppRouter } from "./server"
const wsUrl = new URL(location.href)
wsUrl.protocol = wsUrl.protocol.replace("http", "ws")
wsUrl.pathname = "/ws"
wsUrl.hash = ""
export const trpcClient = createTRPCProxyClient<AppRouter>({
links: [
wsLink({
client: createWSClient({
url: wsUrl.href
})
})
],
transformer: superjson
})

31
tsconfig.json Normal file
View file

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

23
vite.config.ts Normal file
View file

@ -0,0 +1,23 @@
import { defineConfig } from "vite"
import vuePlugin from "@vitejs/plugin-vue"
import iconsPlugin from "unplugin-icons/vite"
import pagesPlugin from "vite-plugin-pages"
import windiPlugin from "vite-plugin-windicss"
export default defineConfig({
plugins: [
vuePlugin(),
iconsPlugin(),
pagesPlugin(),
windiPlugin()
],
server: {
proxy: {
"/trpc": "http://localhost:3000",
"/ws": {
target: "http://localhost:3000",
ws: true
}
}
}
})

23
windi.config.ts Normal file
View file

@ -0,0 +1,23 @@
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,
}
}
})