Update dependencies and fix issues
This commit is contained in:
parent
a200b46764
commit
14f4e87d9d
49 changed files with 3432 additions and 2757 deletions
|
@ -1,6 +1,3 @@
|
|||
/node_modules/
|
||||
/dist/
|
||||
/run/
|
||||
/.idea/
|
||||
./src/server/.prisma
|
||||
.env
|
||||
/node_modules
|
||||
/dist
|
||||
/.idea
|
|
@ -1 +0,0 @@
|
|||
DATABASE_FILE=file:./run/tapdb.sqlite
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,6 +1,3 @@
|
|||
/node_modules/
|
||||
/dist/
|
||||
/run/
|
||||
/.idea/
|
||||
./src/server/.prisma
|
||||
.env
|
||||
/node_modules
|
||||
/dist
|
||||
/.idea
|
1
.nvmrc
1
.nvmrc
|
@ -1 +0,0 @@
|
|||
18
|
|
@ -5,7 +5,6 @@ RUN npm install --global pnpm
|
|||
RUN pnpm install --frozen-lockfile
|
||||
RUN pnpm build:ui
|
||||
RUN mkdir -p /data
|
||||
RUN pnpm prisma generate
|
||||
|
||||
EXPOSE 4000
|
||||
EXPOSE 3000
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
# Twenty-one
|
||||
> The game of Twenty-one in the browser with online-multiplayer and special cards.
|
||||
|
||||
> The game of Twenty-one, in the browser, with online-multiplayer and special cards
|
||||
|
||||
## Known issues
|
||||
|
||||
- The `return-opponent` special card should not be allowed in the first round of a game.
|
||||
|
||||
▶ [**twentyone.deltaa.xyz**](https://twentyone.deltaa.xyz)
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import EventEmitter from "eventemitter3"
|
||||
import type { GameAction } from "../shared/game/actions"
|
||||
import { prismaClient } from "./prisma"
|
||||
import { GameState, getNumberCardsSum, getUninitializedGameState, produceNewState } from "../shared/game/state"
|
||||
import type { RemoveKey } from "../shared/RemoveKey"
|
||||
import { random } from "lodash-es"
|
||||
|
@ -9,6 +8,7 @@ import { customAlphabet as createNanoIdWithCustomAlphabet } from "nanoid/non-sec
|
|||
import { LOBBY_CODE_LENGTH, LOBBY_SIZE, SPECIAL_CARD_PROBABILITY } from "../shared/constants"
|
||||
import type { SpecialCardId } from "../shared/game/cards"
|
||||
import { specialCardsMeta } from "../shared/game/cards"
|
||||
import { usersById, usersByToken } from "./user"
|
||||
|
||||
const generateLobbyCode = createNanoIdWithCustomAlphabet("ABCDEFGHKMNPQRSTUVWXYZ", LOBBY_CODE_LENGTH)
|
||||
|
||||
|
@ -80,18 +80,8 @@ export class Game extends EventEmitter<Events> {
|
|||
super()
|
||||
}
|
||||
|
||||
private async getPlayers() {
|
||||
return await prismaClient.user.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: [...this.lobbyPlayerIds]
|
||||
}
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true
|
||||
}
|
||||
})
|
||||
private getPlayers() {
|
||||
return [...this.lobbyPlayerIds.values()].map(id => usersById.get(id)!)
|
||||
}
|
||||
|
||||
addPlayer(id: string, name: string) {
|
||||
|
@ -119,7 +109,7 @@ export class Game extends EventEmitter<Events> {
|
|||
|
||||
async start() {
|
||||
if (this.state.phase !== "pre-start") throw new Error(`Cannot start the game in this phase: ${this.state.phase}`)
|
||||
const players = await this.getPlayers()
|
||||
const players = this.getPlayers()
|
||||
if (players.length < 2) throw new Error("At least two players are required for starting the game")
|
||||
|
||||
this.addAction({
|
||||
|
@ -263,10 +253,17 @@ export class Game extends EventEmitter<Events> {
|
|||
}
|
||||
|
||||
private dealSpecialTo(playerId: string) {
|
||||
let cardId: SpecialCardId
|
||||
|
||||
// I know. Don't judge me.
|
||||
do {
|
||||
cardId = this.state.weightedSpecialCardIdList[random(0, this.state.weightedSpecialCardIdList.length - 1)]
|
||||
} while (!specialCardsMeta[cardId].isAllowedInFirstRound)
|
||||
|
||||
this.addAction({
|
||||
type: "deal-special",
|
||||
toPlayerId: playerId,
|
||||
cardId: this.state.weightedSpecialCardIdList[random(0, this.state.weightedSpecialCardIdList.length - 1)]
|
||||
cardId
|
||||
})
|
||||
}
|
||||
|
|
@ -8,8 +8,6 @@ import { createContext } from "./trpc/base"
|
|||
import cookieParser from "cookie-parser"
|
||||
import { parse as parseCookie } from "cookie"
|
||||
import { isDev } from "./isDev"
|
||||
import { prismaClient } from "./prisma"
|
||||
import * as dateFns from "date-fns"
|
||||
|
||||
const expressApp = createExpressApp()
|
||||
expressApp.use(cookieParser())
|
||||
|
@ -19,14 +17,6 @@ expressApp.use("/trpc", createTrpcMiddleware({
|
|||
createContext: ({ req, res }) => createContext(req.cookies.token ?? null, res),
|
||||
}))
|
||||
|
||||
await prismaClient.user.deleteMany({
|
||||
where: {
|
||||
lastActivityDate: {
|
||||
lte: dateFns.subMonths(new Date(), 1)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const { server } = await listen(expressApp, { isProd: !isDev, autoClose: false })
|
||||
|
||||
const wss = new WebSocketServer({ server, path: "/ws" })
|
||||
|
@ -36,6 +26,11 @@ const wssTrpcHandler = applyWSSHandler({
|
|||
createContext: ({ req }) => {
|
||||
const cookies = parseCookie(req.headers.cookie ?? "")
|
||||
return createContext(cookies.token ?? null, undefined)
|
||||
},
|
||||
keepAlive: {
|
||||
enabled: true,
|
||||
pingMs: 30000,
|
||||
pongWaitMs: 5000,
|
||||
}
|
||||
})
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
import { createInputMiddleware, initTRPC, TRPCError } from "@trpc/server"
|
||||
import { prismaClient } from "../prisma"
|
||||
import { initTRPC, TRPCError } from "@trpc/server"
|
||||
import type { Response } from "express"
|
||||
import { type User, usersByToken } from "../user"
|
||||
|
||||
export interface Context {
|
||||
user: {
|
||||
id: string
|
||||
name: string
|
||||
} | null
|
||||
user: User | null
|
||||
res?: Response
|
||||
}
|
||||
|
||||
|
@ -14,13 +11,7 @@ export async function createContext(authenticationToken: string | null, res: Res
|
|||
let user: Context["user"] = null
|
||||
|
||||
if (authenticationToken !== null && authenticationToken.length > 0) {
|
||||
user = await prismaClient.user.findUnique({
|
||||
where: { token: authenticationToken },
|
||||
select: {
|
||||
id: true,
|
||||
name: true
|
||||
}
|
||||
})
|
||||
user = usersByToken.get(authenticationToken) ?? null
|
||||
}
|
||||
|
||||
return {
|
|
@ -1,13 +1,13 @@
|
|||
import { requireAuthentication, t } from "./base"
|
||||
import { prismaClient } from "../prisma"
|
||||
import z from "zod"
|
||||
import { observable } from "@trpc/server/observable"
|
||||
import type { GameAction } from "../../shared/game/actions"
|
||||
import { isDev } from "../isDev"
|
||||
import { gameRouter } from "./game"
|
||||
import { nanoid } from "nanoid/async"
|
||||
import { nanoid } from "nanoid"
|
||||
import { createGame, getGameByLobbyCode } from "../game"
|
||||
import type { GameEvent } from "../../shared/game/events"
|
||||
import { type User, usersById, usersByToken } from "../user"
|
||||
|
||||
export const appRouter = t.router({
|
||||
game: gameRouter,
|
||||
|
@ -17,20 +17,21 @@ export const appRouter = t.router({
|
|||
name: z.string().min(1).max(20)
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (ctx.user !== null) await prismaClient.user.delete({ where: { id: ctx.user.id } })
|
||||
|
||||
const token = await nanoid(60)
|
||||
const user = await prismaClient.user.create({
|
||||
data: {
|
||||
name: input.name,
|
||||
token
|
||||
},
|
||||
select: {
|
||||
id: true
|
||||
if (ctx.user !== null) {
|
||||
usersById.delete(ctx.user.id)
|
||||
usersByToken.delete(ctx.user.token)
|
||||
}
|
||||
})
|
||||
|
||||
ctx.res!.cookie("token", token, {
|
||||
const newUser: User = {
|
||||
id: nanoid(16),
|
||||
token: nanoid(64),
|
||||
name: input.name
|
||||
}
|
||||
|
||||
usersById.set(newUser.id, newUser)
|
||||
usersByToken.set(newUser.token, newUser)
|
||||
|
||||
ctx.res!.cookie("token", newUser.token, {
|
||||
maxAge: 60 * 60 * 24 * 365,
|
||||
httpOnly: true,
|
||||
secure: !isDev,
|
||||
|
@ -38,7 +39,7 @@ export const appRouter = t.router({
|
|||
})
|
||||
|
||||
return {
|
||||
id: user.id
|
||||
id: newUser.id
|
||||
}
|
||||
}),
|
||||
|
||||
|
@ -46,21 +47,8 @@ export const appRouter = t.router({
|
|||
.query(async ({ ctx }) => {
|
||||
if (ctx.user === null) return { user: null }
|
||||
|
||||
await prismaClient.user.update({
|
||||
where: { id: ctx.user.id },
|
||||
data: {
|
||||
lastActivityDate: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
user: await prismaClient.user.findUnique({
|
||||
where: { id: ctx.user.id },
|
||||
select: {
|
||||
id: true,
|
||||
name: true
|
||||
}
|
||||
})
|
||||
user: ctx.user
|
||||
}
|
||||
}),
|
||||
|
||||
|
@ -80,10 +68,10 @@ export const appRouter = t.router({
|
|||
lobbyCode: z.string().nonempty()
|
||||
}))
|
||||
.subscription(async ({ input, ctx }) => {
|
||||
const game = await getGameByLobbyCode(input.lobbyCode)
|
||||
const game = getGameByLobbyCode(input.lobbyCode)
|
||||
if (game === null) throw new Error("There is no game with this code.")
|
||||
|
||||
await game.addPlayer(ctx.user.id, ctx.user.name)
|
||||
game.addPlayer(ctx.user.id, ctx.user.name)
|
||||
|
||||
return observable<GameEvent>(emit => {
|
||||
const handleBroadcastAction = (action: GameAction) => emit.next({ type: "action", action })
|
8
backend/user.ts
Normal file
8
backend/user.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export interface User {
|
||||
id: string
|
||||
token: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export const usersByToken = new Map<string, User>()
|
||||
export const usersById = new Map<string, User>()
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div class="bg-gray-900 h-100vh w-100vw overflow-y-auto text-white p-10">
|
||||
<div class="bg-gray-900 min-h-100vh overflow-y-auto text-white p-10 font-normal">
|
||||
<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-4xl">
|
||||
<div class="relative">
|
||||
<div v-if="isLoading" class="flex flex-col justify-center items-center text-20">
|
||||
<span>Loading…</span>
|
||||
</div>
|
||||
<LoginScreen v-else-if="auth.authenticatedUser === null"/>
|
||||
|
@ -16,7 +16,7 @@
|
|||
|
||||
<style module lang="scss">
|
||||
.noise, .vignette {
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
|
@ -34,11 +34,16 @@
|
|||
</style>
|
||||
|
||||
<style>
|
||||
body {
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
overflow-x: hidden;
|
||||
font-size: 16px;
|
||||
user-select: none;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
@ -1,11 +1,11 @@
|
|||
import { defineStore } from "pinia"
|
||||
import { EventBusKey, useEventBus } from "@vueuse/core"
|
||||
import type { GameAction } from "./shared/game/actions"
|
||||
import type { GameAction } from "../shared/game/actions"
|
||||
import { computed, reactive, readonly, ref } from "vue"
|
||||
import { GameState, getNumberCardsSum, getUninitializedGameState, produceNewState } from "./shared/game/state"
|
||||
import { GameState, getNumberCardsSum, getUninitializedGameState, produceNewState } from "../shared/game/state"
|
||||
import { trpcClient } from "./trpc"
|
||||
import { useAuth } from "./auth"
|
||||
import type { SpecialCardId } from "./shared/game/cards"
|
||||
import type { SpecialCardId } from "../shared/game/cards"
|
||||
|
||||
const gameActionsBusKey = Symbol() as EventBusKey<GameAction>
|
||||
const useGameActionsBus = () => useEventBus(gameActionsBusKey)
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<button
|
||||
class="py-5 px-8 md:px-15 font-fat text-4xl md:text-5xl rounded-md shadow-lg text-center"
|
||||
class="py-5 px-8 md:px-15 font-fat text-4xl text-white md:text-5xl rounded<md shadow-lg text-center border-none cursor-pointer"
|
||||
:class="$style.root"
|
||||
:disabled="disabled"
|
||||
>
|
||||
|
@ -26,7 +26,7 @@
|
|||
}
|
||||
|
||||
&::after {
|
||||
@apply bg-gradient-to-b from-dark-600 to-dark-900;
|
||||
background: linear-gradient(to bottom, #1c1c1e, #0f0f0f);
|
||||
|
||||
content: "";
|
||||
position: absolute;
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<component :is="asButton ? 'button' : 'div'" class="block flex-shrink-0 rounded-lg shadow-xl bg-gradient-to-br" :class="$style.root">
|
||||
<component :is="asButton ? 'button' : 'div'" class="block flex-shrink-0 rounded-lg shadow-xl bg-gradient-to-br border-none text-inherit" :class="$style.root">
|
||||
<div class="relative flex flex-col items-center justify-center h-full">
|
||||
<div class="absolute top-2 right-2 bg-dark-800 rounded-full flex gap-2 items-center px-2">
|
||||
<div v-for="tag in tags" :key="tag.label" :title="tag.label">
|
||||
|
@ -29,6 +29,10 @@
|
|||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:is(button) {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="flex -md:flex-col gap-15 justify-center">
|
||||
<div class="flex <md:flex-col gap-15 justify-center">
|
||||
<div>
|
||||
<div class="font-bold text-lg pb-2 pl-2">Active special cards</div>
|
||||
<div class="p-4 border border-dark-200 rounded-xl flex flex-col gap-5 flex-wrap w-full">
|
||||
|
@ -17,7 +17,7 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-14 max-w-1200px flex-grow -md:pl-3 pb-5">
|
||||
<div class="flex flex-col gap-14 max-w-1200px flex-grow <md:pl-3 pb-5">
|
||||
<PlayerCards v-for="playerId in reorderedPlayerIds" :key="playerId" :player-id="playerId"/>
|
||||
<div class="flex gap-5 justify-end items-center transform transition ease duration-500">
|
||||
<template v-if="game.state.phase === 'end'">
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<Modal :is-active="isActive" @close-request="isActive = false">
|
||||
<div v-if="game.state.phase === 'end'" class="p-10 text-center">
|
||||
<div class="h-11 transform text-10xl -translate-y-35 opacity-90">
|
||||
<div class="h-11 transform -translate-y-35 opacity-90 text-40">
|
||||
<CrownIcon v-if="singleWinner !== null"/>
|
||||
<HandshakeIcon v-else/>
|
||||
</div>
|
||||
|
@ -32,15 +32,15 @@
|
|||
import Modal from "./Modal.vue"
|
||||
import CrownIcon from "virtual:icons/ph/crown-simple-duotone"
|
||||
import HandshakeIcon from "virtual:icons/ph/handshake-duotone"
|
||||
import { getNumberCardsSum } from "../shared/game/state"
|
||||
import { naturallyJoinEnumeration } from "../shared/util"
|
||||
import { getNumberCardsSum } from "../../shared/game/state"
|
||||
import { naturallyJoinEnumeration } from "../../shared/util"
|
||||
|
||||
const game = useGame()
|
||||
const isActive = ref(false)
|
||||
|
||||
const singleWinner = computed(() => {
|
||||
const winnerIds = game.state.winnerIds
|
||||
return winnerIds?.length === 1 ? game.state.players.find(p => p.id === winnerIds[0]) : null
|
||||
return winnerIds?.length === 1 ? game.state.players.find(p => p.id === winnerIds[0])! : null
|
||||
})
|
||||
|
||||
useGameActionNotification(action => {
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div class="flex flex-col h-full justify-center items-center gap-8 text-7xl md:text-8xl">
|
||||
<div class="flex flex-col h-full justify-center items-center gap-8">
|
||||
<input
|
||||
v-model="lobbyCodeInput"
|
||||
class="uppercase tracking-wider bg-transparent text-white text-center font-fat focus:outline-none pb-2 rounded-lg transition"
|
||||
class="uppercase tracking-wider border-none text-7xl md:text-8xl bg-transparent text-white text-center font-fat focus:outline-none pb-2 rounded-lg transition"
|
||||
:class="isUnknown ? 'bg-red-800' : ''"
|
||||
:maxlength="LOBBY_CODE_LENGTH"
|
||||
:placeholder="'X'.repeat(LOBBY_CODE_LENGTH)"
|
||||
|
@ -10,7 +10,7 @@
|
|||
@keypress="onKeypress"
|
||||
/>
|
||||
<button
|
||||
class="font-bold text-4xl border border-white rounded-md bg-white bg-opacity-0 transition px-10 py-4"
|
||||
class="font-bold text-inherit font-inherit text-4xl border border-solid border-gray-500 rounded<md bg-white bg-opacity-0 transition px-10 py-4"
|
||||
:class="$style.button"
|
||||
:disabled="lobbyCodeInput.length !== LOBBY_CODE_LENGTH"
|
||||
@click="join()"
|
||||
|
@ -18,7 +18,7 @@
|
|||
Join
|
||||
</button>
|
||||
<button
|
||||
class="font-bold text-2xl border border-white rounded-md bg-white bg-opacity-0 transition px-6 py-2"
|
||||
class="font-bold text-inherit font-inherit text-2xl border border-solid border-gray-500 rounded<md bg-white bg-opacity-0 transition px-6 py-2"
|
||||
:class="$style.button"
|
||||
@click="create()"
|
||||
>
|
||||
|
@ -33,8 +33,8 @@
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
@apply hover:bg-opacity-20;
|
||||
&:not(:disabled):hover {
|
||||
--un-bg-opacity: 10%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -42,7 +42,7 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref, watch, watchEffect } from "vue"
|
||||
import { useGame } from "../clientGame"
|
||||
import { LOBBY_CODE_LENGTH } from "../shared/constants"
|
||||
import { LOBBY_CODE_LENGTH } from "../../shared/constants"
|
||||
import { useBrowserLocation } from "@vueuse/core"
|
||||
|
||||
const location = useBrowserLocation()
|
|
@ -2,12 +2,12 @@
|
|||
<div class="flex flex-col h-full justify-center items-center gap-8 text-4xl md:text-8xl">
|
||||
<input
|
||||
v-model="usernameInput"
|
||||
class="bg-transparent text-white text-center font-fat w-full focus:outline-none"
|
||||
class="bg-transparent text-white text-6xl text-center font-fat w-full focus:outline-none border-none"
|
||||
placeholder="Username"
|
||||
maxlength="20"
|
||||
/>
|
||||
<button
|
||||
class="font-bold text-2xl md:text-4xl border border-white rounded-md bg-white bg-opacity-0 transition px-10 py-4"
|
||||
class="font-bold font-inherit text-inherit text-2xl md:text-4xl border border-solid border-gray-500 rounded<md bg-white bg-opacity-0 transition px-10 py-4 cursor-pointer"
|
||||
:class="$style.button"
|
||||
:disabled="usernameInput.length <= 0 || usernameInput.length > 20"
|
||||
@click="submit()"
|
||||
|
@ -23,8 +23,8 @@
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:not(:disabled) {
|
||||
@apply hover:bg-opacity-20;
|
||||
&:not(:disabled):hover {
|
||||
--un-bg-opacity: 10%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, watchEffect } from "vue"
|
||||
import { useGame } from "../clientGame"
|
||||
import { LOBBY_CODE_LENGTH } from "../shared/constants"
|
||||
import { LOBBY_CODE_LENGTH } from "../../shared/constants"
|
||||
import { useBrowserLocation } from "@vueuse/core"
|
||||
import { useAuth } from "../auth"
|
||||
|
|
@ -19,13 +19,13 @@
|
|||
<div class="flex items-center gap-5 w-full flex-wrap">
|
||||
<NumberCard
|
||||
v-for="(card, index) in playerState.numberCards"
|
||||
:key="[index, card.number]"
|
||||
:key="[index, card.number].join('-')"
|
||||
:number="card.number"
|
||||
:is-covert="card.isCovert"
|
||||
:is-own="isYou"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="isYou" class="p-4 border border-dark-200 rounded-xl flex gap-5 flex-wrap w-full">
|
||||
<div v-if="isYou" class="py-4 border border-dark-200 rounded-xl flex gap-5 flex-wrap w-full">
|
||||
<div
|
||||
v-if="playerState.specialCards.length === 0"
|
||||
class="font-fat opacity-40 text-2xl h-32 flex px-4 py-5"
|
||||
|
@ -56,7 +56,7 @@
|
|||
import { computed } from "vue"
|
||||
import NumberCard from "./NumberCard.vue"
|
||||
import { useAuth } from "../auth"
|
||||
import { getNumberCardsSum } from "../shared/game/state"
|
||||
import { getNumberCardsSum } from "../../shared/game/state"
|
||||
import CrownIcon from "virtual:icons/ph/crown-simple-bold"
|
||||
import DotsThreeOutlineIcon from "virtual:icons/ph/dots-three-outline"
|
||||
import SpecialCard from "./SpecialCard.vue"
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useGame } from "../clientGame"
|
||||
import { naturallyJoinEnumeration } from "../shared/util"
|
||||
import { naturallyJoinEnumeration } from "../../shared/util"
|
||||
import BigButton from "./BigButton.vue"
|
||||
|
||||
const game = useGame()
|
|
@ -24,9 +24,9 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import Card from "./Card.vue"
|
||||
import type { SpecialCardId } from "../shared/game/cards"
|
||||
import type { SpecialCardId } from "../../shared/game/cards"
|
||||
import { specialCardIcons } from "../icons"
|
||||
import { specialCardsMeta } from "../shared/game/cards"
|
||||
import { specialCardsMeta } from "../../shared/game/cards"
|
||||
import { computed } from "vue"
|
||||
import { useGame } from "../clientGame"
|
||||
import { useThrottleFn } from "@vueuse/core"
|
|
@ -1,4 +1,4 @@
|
|||
import type { SpecialCardId } from "./shared/game/cards"
|
||||
import type { SpecialCardId } from "../shared/game/cards"
|
||||
import type { Component } from "vue"
|
||||
import ArrowArcRightIcon from "virtual:icons/ph/arrow-arc-right-bold"
|
||||
import PlusCircleIcon from "virtual:icons/ph/plus-circle"
|
8
frontend/index.ts
Normal file
8
frontend/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import "uno.css"
|
||||
import { createApp } from "vue"
|
||||
import App from "./App.vue"
|
||||
import { createPinia } from "pinia"
|
||||
|
||||
createApp(App)
|
||||
.use(createPinia())
|
||||
.mount("#app")
|
|
@ -1,5 +1,5 @@
|
|||
import { createTRPCProxyClient, httpLink, createWSClient, wsLink, splitLink } from "@trpc/client"
|
||||
import type { AppRouter } from "./server"
|
||||
import type { AppRouter } from "../backend"
|
||||
|
||||
const wsUrl = new URL(location.href)
|
||||
wsUrl.protocol = wsUrl.protocol.replace("http", "ws")
|
|
@ -2,11 +2,12 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>TwentyOne</title>
|
||||
<title>Twenty-one</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/inter/variable.css"/>
|
||||
<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/titan-one/400.css"/>
|
||||
<link rel="stylesheet" href="modern-normalize/modern-normalize.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
83
package.json
83
package.json
|
@ -3,54 +3,53 @@
|
|||
"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",
|
||||
"start": "NODE_ENV=production tsx ./backend/main.ts",
|
||||
"build": "vite build",
|
||||
"dev:ui": "vite",
|
||||
"dev:server": "NODE_ENV=development tsx watch --clear-screen=false ./src/server/main.ts"
|
||||
"dev:server": "NODE_ENV=development tsx watch --clear-screen=false ./backend/main.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/ph": "^1.1.5",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/node": "18",
|
||||
"@types/ws": "^8.5.4",
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
"prisma": "^4.13.0",
|
||||
"sass": "^1.62.0",
|
||||
"tsx": "^3.12.6",
|
||||
"typescript": "^4.9.5",
|
||||
"unplugin-icons": "^0.16.1",
|
||||
"vite": "^4.2.2",
|
||||
"vite-plugin-pages": "^0.29.0",
|
||||
"vite-plugin-windicss": "^1.8.10",
|
||||
"@iconify-json/ph": "^1.2.2",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^22.13.8",
|
||||
"@types/ws": "^8.5.14",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"sass": "^1.85.1",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.2",
|
||||
"unocss": "66.1.0-beta.3",
|
||||
"unplugin-icons": "^22.1.0",
|
||||
"vite": "^6.2.0",
|
||||
"windicss": "^3.5.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^4.5.15",
|
||||
"@fontsource/titan-one": "^4.5.9",
|
||||
"@headlessui/vue": "^1.7.13",
|
||||
"@prisma/client": "^4.13.0",
|
||||
"@trpc/client": "^10.20.0",
|
||||
"@trpc/server": "^10.20.0",
|
||||
"@types/cookie-parser": "^1.4.3",
|
||||
"@vueuse/core": "^10.0.2",
|
||||
"@vueuse/integrations": "^10.0.2",
|
||||
"bufferutil": "^4.0.7",
|
||||
"cookie": "^0.5.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"date-fns": "^2.29.3",
|
||||
"eventemitter3": "^5.0.0",
|
||||
"express": "^4.18.2",
|
||||
"immer": "^10.0.1",
|
||||
"listhen": "^1.0.4",
|
||||
"@fontsource-variable/inter": "^5.2.5",
|
||||
"@fontsource/titan-one": "^5.2.5",
|
||||
"@headlessui/vue": "^1.7.23",
|
||||
"@trpc/client": "^10.45.2",
|
||||
"@trpc/server": "^10.45.2",
|
||||
"@types/cookie-parser": "^1.4.8",
|
||||
"@vueuse/core": "^12.7.0",
|
||||
"@vueuse/integrations": "^12.7.0",
|
||||
"bufferutil": "^4.0.9",
|
||||
"cookie": "^1.0.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"date-fns": "^4.1.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"express": "^4.21.2",
|
||||
"immer": "^10.1.1",
|
||||
"listhen": "^1.9.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nanoid": "^4.0.2",
|
||||
"pinia": "^2.0.34",
|
||||
"vue": "^3.2.47",
|
||||
"vue-router": "^4.1.6",
|
||||
"ws": "^8.13.0",
|
||||
"zod": "^3.21.4"
|
||||
"modern-normalize": "^3.0.1",
|
||||
"nanoid": "^5.1.2",
|
||||
"pinia": "^3.0.1",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0",
|
||||
"ws": "^8.18.1",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "22"
|
||||
}
|
||||
}
|
||||
|
|
5760
pnpm-lock.yaml
generated
5760
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -1,15 +0,0 @@
|
|||
datasource db {
|
||||
url = env("DATABASE_FILE")
|
||||
provider = "sqlite"
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
token String @unique
|
||||
lastActivityDate DateTime @default(now())
|
||||
}
|
16
src/index.ts
16
src/index.ts
|
@ -1,16 +0,0 @@
|
|||
import "virtual:windi.css"
|
||||
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")
|
|
@ -1,5 +0,0 @@
|
|||
import { PrismaClient } from "@prisma/client"
|
||||
|
||||
export const prismaClient = new PrismaClient()
|
||||
|
||||
export type { Prisma } from "@prisma/client"
|
5
src/types.d.ts
vendored
5
src/types.d.ts
vendored
|
@ -1,5 +0,0 @@
|
|||
declare module "*.vue" {
|
||||
import { ConcreteComponent } from "vue"
|
||||
const C: ConcreteComponent
|
||||
export default C
|
||||
}
|
|
@ -1,31 +1,31 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declaration": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"jsx": "preserve",
|
||||
"lib": ["esnext", "dom"],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"moduleResolution": "bundler",
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"importsNotUsedAsValues": "error",
|
||||
"isolatedModules": true,
|
||||
"rootDir": "src",
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"sourceMap": false,
|
||||
"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"
|
||||
"backend/**/*.ts",
|
||||
"shared/**/*.ts",
|
||||
"frontend/**/*.ts",
|
||||
"frontend/**/*.vue"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
import { defineConfig } from "vite-plugin-windicss"
|
||||
import { defineConfig, presetWind } from "unocss"
|
||||
import colors from "windicss/colors"
|
||||
|
||||
export default defineConfig({
|
||||
presets: [
|
||||
presetWind({
|
||||
arbitraryVariants: true,
|
||||
preflight: true
|
||||
})
|
||||
],
|
||||
theme: {
|
||||
fontFamily: {
|
||||
"normal": ["InterVariable", "sans-serif"],
|
||||
"fat": ["Titan One", "sans-serif"]
|
||||
"normal": `"Inter Variable", sans-serif`,
|
||||
"fat": `"Titan One", sans-serif`
|
||||
},
|
||||
colors: {
|
||||
transparent: "transparent",
|
||||
white: "white",
|
||||
black: "black",
|
||||
white: colors.white,
|
||||
black: colors.black,
|
||||
gray: colors.zinc,
|
||||
light: colors.light,
|
||||
dark: colors.dark,
|
|
@ -1,21 +1,19 @@
|
|||
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"
|
||||
import unoCssPlugin from "unocss/vite"
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vuePlugin(),
|
||||
iconsPlugin(),
|
||||
pagesPlugin(),
|
||||
windiPlugin()
|
||||
unoCssPlugin()
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
"/trpc": "http://127.0.0.1:3000",
|
||||
"/trpc": "http://localhost:3000",
|
||||
"/ws": {
|
||||
target: "http://127.0.0.1:3000",
|
||||
target: "http://localhost:3000",
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue