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/
|
/node_modules
|
||||||
/dist/
|
/dist
|
||||||
/run/
|
/.idea
|
||||||
/.idea/
|
|
||||||
./src/server/.prisma
|
|
||||||
.env
|
|
|
@ -1 +0,0 @@
|
||||||
DATABASE_FILE=file:./run/tapdb.sqlite
|
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,6 +1,3 @@
|
||||||
/node_modules/
|
/node_modules
|
||||||
/dist/
|
/dist
|
||||||
/run/
|
/.idea
|
||||||
/.idea/
|
|
||||||
./src/server/.prisma
|
|
||||||
.env
|
|
1
.nvmrc
1
.nvmrc
|
@ -1 +0,0 @@
|
||||||
18
|
|
|
@ -5,7 +5,6 @@ RUN npm install --global pnpm
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
RUN pnpm build:ui
|
RUN pnpm build:ui
|
||||||
RUN mkdir -p /data
|
RUN mkdir -p /data
|
||||||
RUN pnpm prisma generate
|
|
||||||
|
|
||||||
EXPOSE 4000
|
EXPOSE 4000
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
# Twenty-one
|
# 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
|
▶ [**twentyone.deltaa.xyz**](https://twentyone.deltaa.xyz)
|
||||||
|
|
||||||
- The `return-opponent` special card should not be allowed in the first round of a game.
|
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import EventEmitter from "eventemitter3"
|
import EventEmitter from "eventemitter3"
|
||||||
import type { GameAction } from "../shared/game/actions"
|
import type { GameAction } from "../shared/game/actions"
|
||||||
import { prismaClient } from "./prisma"
|
|
||||||
import { GameState, getNumberCardsSum, getUninitializedGameState, produceNewState } from "../shared/game/state"
|
import { GameState, getNumberCardsSum, getUninitializedGameState, produceNewState } from "../shared/game/state"
|
||||||
import type { RemoveKey } from "../shared/RemoveKey"
|
import type { RemoveKey } from "../shared/RemoveKey"
|
||||||
import { random } from "lodash-es"
|
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 { LOBBY_CODE_LENGTH, LOBBY_SIZE, SPECIAL_CARD_PROBABILITY } from "../shared/constants"
|
||||||
import type { SpecialCardId } from "../shared/game/cards"
|
import type { SpecialCardId } from "../shared/game/cards"
|
||||||
import { specialCardsMeta } from "../shared/game/cards"
|
import { specialCardsMeta } from "../shared/game/cards"
|
||||||
|
import { usersById, usersByToken } from "./user"
|
||||||
|
|
||||||
const generateLobbyCode = createNanoIdWithCustomAlphabet("ABCDEFGHKMNPQRSTUVWXYZ", LOBBY_CODE_LENGTH)
|
const generateLobbyCode = createNanoIdWithCustomAlphabet("ABCDEFGHKMNPQRSTUVWXYZ", LOBBY_CODE_LENGTH)
|
||||||
|
|
||||||
|
@ -80,18 +80,8 @@ export class Game extends EventEmitter<Events> {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getPlayers() {
|
private getPlayers() {
|
||||||
return await prismaClient.user.findMany({
|
return [...this.lobbyPlayerIds.values()].map(id => usersById.get(id)!)
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
in: [...this.lobbyPlayerIds]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addPlayer(id: string, name: string) {
|
addPlayer(id: string, name: string) {
|
||||||
|
@ -119,7 +109,7 @@ export class Game extends EventEmitter<Events> {
|
||||||
|
|
||||||
async start() {
|
async start() {
|
||||||
if (this.state.phase !== "pre-start") throw new Error(`Cannot start the game in this phase: ${this.state.phase}`)
|
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")
|
if (players.length < 2) throw new Error("At least two players are required for starting the game")
|
||||||
|
|
||||||
this.addAction({
|
this.addAction({
|
||||||
|
@ -263,10 +253,17 @@ export class Game extends EventEmitter<Events> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private dealSpecialTo(playerId: string) {
|
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({
|
this.addAction({
|
||||||
type: "deal-special",
|
type: "deal-special",
|
||||||
toPlayerId: playerId,
|
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 cookieParser from "cookie-parser"
|
||||||
import { parse as parseCookie } from "cookie"
|
import { parse as parseCookie } from "cookie"
|
||||||
import { isDev } from "./isDev"
|
import { isDev } from "./isDev"
|
||||||
import { prismaClient } from "./prisma"
|
|
||||||
import * as dateFns from "date-fns"
|
|
||||||
|
|
||||||
const expressApp = createExpressApp()
|
const expressApp = createExpressApp()
|
||||||
expressApp.use(cookieParser())
|
expressApp.use(cookieParser())
|
||||||
|
@ -19,14 +17,6 @@ expressApp.use("/trpc", createTrpcMiddleware({
|
||||||
createContext: ({ req, res }) => createContext(req.cookies.token ?? null, res),
|
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 { server } = await listen(expressApp, { isProd: !isDev, autoClose: false })
|
||||||
|
|
||||||
const wss = new WebSocketServer({ server, path: "/ws" })
|
const wss = new WebSocketServer({ server, path: "/ws" })
|
||||||
|
@ -36,6 +26,11 @@ const wssTrpcHandler = applyWSSHandler({
|
||||||
createContext: ({ req }) => {
|
createContext: ({ req }) => {
|
||||||
const cookies = parseCookie(req.headers.cookie ?? "")
|
const cookies = parseCookie(req.headers.cookie ?? "")
|
||||||
return createContext(cookies.token ?? null, undefined)
|
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 { initTRPC, TRPCError } from "@trpc/server"
|
||||||
import { prismaClient } from "../prisma"
|
|
||||||
import type { Response } from "express"
|
import type { Response } from "express"
|
||||||
|
import { type User, usersByToken } from "../user"
|
||||||
|
|
||||||
export interface Context {
|
export interface Context {
|
||||||
user: {
|
user: User | null
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
} | null
|
|
||||||
res?: Response
|
res?: Response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,13 +11,7 @@ export async function createContext(authenticationToken: string | null, res: Res
|
||||||
let user: Context["user"] = null
|
let user: Context["user"] = null
|
||||||
|
|
||||||
if (authenticationToken !== null && authenticationToken.length > 0) {
|
if (authenticationToken !== null && authenticationToken.length > 0) {
|
||||||
user = await prismaClient.user.findUnique({
|
user = usersByToken.get(authenticationToken) ?? null
|
||||||
where: { token: authenticationToken },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
|
@ -1,13 +1,13 @@
|
||||||
import { requireAuthentication, t } from "./base"
|
import { requireAuthentication, t } from "./base"
|
||||||
import { prismaClient } from "../prisma"
|
|
||||||
import z from "zod"
|
import z from "zod"
|
||||||
import { observable } from "@trpc/server/observable"
|
import { observable } from "@trpc/server/observable"
|
||||||
import type { GameAction } from "../../shared/game/actions"
|
import type { GameAction } from "../../shared/game/actions"
|
||||||
import { isDev } from "../isDev"
|
import { isDev } from "../isDev"
|
||||||
import { gameRouter } from "./game"
|
import { gameRouter } from "./game"
|
||||||
import { nanoid } from "nanoid/async"
|
import { nanoid } from "nanoid"
|
||||||
import { createGame, getGameByLobbyCode } from "../game"
|
import { createGame, getGameByLobbyCode } from "../game"
|
||||||
import type { GameEvent } from "../../shared/game/events"
|
import type { GameEvent } from "../../shared/game/events"
|
||||||
|
import { type User, usersById, usersByToken } from "../user"
|
||||||
|
|
||||||
export const appRouter = t.router({
|
export const appRouter = t.router({
|
||||||
game: gameRouter,
|
game: gameRouter,
|
||||||
|
@ -17,20 +17,21 @@ export const appRouter = t.router({
|
||||||
name: z.string().min(1).max(20)
|
name: z.string().min(1).max(20)
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user !== null) await prismaClient.user.delete({ where: { id: ctx.user.id } })
|
if (ctx.user !== null) {
|
||||||
|
usersById.delete(ctx.user.id)
|
||||||
|
usersByToken.delete(ctx.user.token)
|
||||||
|
}
|
||||||
|
|
||||||
const token = await nanoid(60)
|
const newUser: User = {
|
||||||
const user = await prismaClient.user.create({
|
id: nanoid(16),
|
||||||
data: {
|
token: nanoid(64),
|
||||||
name: input.name,
|
name: input.name
|
||||||
token
|
}
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx.res!.cookie("token", token, {
|
usersById.set(newUser.id, newUser)
|
||||||
|
usersByToken.set(newUser.token, newUser)
|
||||||
|
|
||||||
|
ctx.res!.cookie("token", newUser.token, {
|
||||||
maxAge: 60 * 60 * 24 * 365,
|
maxAge: 60 * 60 * 24 * 365,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: !isDev,
|
secure: !isDev,
|
||||||
|
@ -38,7 +39,7 @@ export const appRouter = t.router({
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id
|
id: newUser.id
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -46,21 +47,8 @@ export const appRouter = t.router({
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
if (ctx.user === null) return { user: null }
|
if (ctx.user === null) return { user: null }
|
||||||
|
|
||||||
await prismaClient.user.update({
|
|
||||||
where: { id: ctx.user.id },
|
|
||||||
data: {
|
|
||||||
lastActivityDate: new Date()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: await prismaClient.user.findUnique({
|
user: ctx.user
|
||||||
where: { id: ctx.user.id },
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
@ -80,10 +68,10 @@ export const appRouter = t.router({
|
||||||
lobbyCode: z.string().nonempty()
|
lobbyCode: z.string().nonempty()
|
||||||
}))
|
}))
|
||||||
.subscription(async ({ input, ctx }) => {
|
.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.")
|
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 => {
|
return observable<GameEvent>(emit => {
|
||||||
const handleBroadcastAction = (action: GameAction) => emit.next({ type: "action", action })
|
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>
|
<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.noise"/>
|
||||||
<div :class="$style.vignette"/>
|
<div :class="$style.vignette"/>
|
||||||
<div class="relative h-full">
|
<div class="relative">
|
||||||
<div v-if="isLoading" class="flex flex-col justify-center items-center text-4xl">
|
<div v-if="isLoading" class="flex flex-col justify-center items-center text-20">
|
||||||
<span>Loading…</span>
|
<span>Loading…</span>
|
||||||
</div>
|
</div>
|
||||||
<LoginScreen v-else-if="auth.authenticatedUser === null"/>
|
<LoginScreen v-else-if="auth.authenticatedUser === null"/>
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<style module lang="scss">
|
<style module lang="scss">
|
||||||
.noise, .vignette {
|
.noise, .vignette {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -34,11 +34,16 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body, #app {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
@ -1,11 +1,11 @@
|
||||||
import { defineStore } from "pinia"
|
import { defineStore } from "pinia"
|
||||||
import { EventBusKey, useEventBus } from "@vueuse/core"
|
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 { 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 { trpcClient } from "./trpc"
|
||||||
import { useAuth } from "./auth"
|
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 gameActionsBusKey = Symbol() as EventBusKey<GameAction>
|
||||||
const useGameActionsBus = () => useEventBus(gameActionsBusKey)
|
const useGameActionsBus = () => useEventBus(gameActionsBusKey)
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<button
|
<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"
|
:class="$style.root"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
>
|
>
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
@apply bg-gradient-to-b from-dark-600 to-dark-900;
|
background: linear-gradient(to bottom, #1c1c1e, #0f0f0f);
|
||||||
|
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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="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 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">
|
<div v-for="tag in tags" :key="tag.label" :title="tag.label">
|
||||||
|
@ -29,6 +29,10 @@
|
||||||
button:disabled {
|
button:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:is(button) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex -md:flex-col gap-15 justify-center">
|
<div class="flex <md:flex-col gap-15 justify-center">
|
||||||
<div>
|
<div>
|
||||||
<div class="font-bold text-lg pb-2 pl-2">Active special cards</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">
|
<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>
|
</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"/>
|
<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">
|
<div class="flex gap-5 justify-end items-center transform transition ease duration-500">
|
||||||
<template v-if="game.state.phase === 'end'">
|
<template v-if="game.state.phase === 'end'">
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<Modal :is-active="isActive" @close-request="isActive = false">
|
<Modal :is-active="isActive" @close-request="isActive = false">
|
||||||
<div v-if="game.state.phase === 'end'" class="p-10 text-center">
|
<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"/>
|
<CrownIcon v-if="singleWinner !== null"/>
|
||||||
<HandshakeIcon v-else/>
|
<HandshakeIcon v-else/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,15 +32,15 @@
|
||||||
import Modal from "./Modal.vue"
|
import Modal from "./Modal.vue"
|
||||||
import CrownIcon from "virtual:icons/ph/crown-simple-duotone"
|
import CrownIcon from "virtual:icons/ph/crown-simple-duotone"
|
||||||
import HandshakeIcon from "virtual:icons/ph/handshake-duotone"
|
import HandshakeIcon from "virtual:icons/ph/handshake-duotone"
|
||||||
import { getNumberCardsSum } from "../shared/game/state"
|
import { getNumberCardsSum } from "../../shared/game/state"
|
||||||
import { naturallyJoinEnumeration } from "../shared/util"
|
import { naturallyJoinEnumeration } from "../../shared/util"
|
||||||
|
|
||||||
const game = useGame()
|
const game = useGame()
|
||||||
const isActive = ref(false)
|
const isActive = ref(false)
|
||||||
|
|
||||||
const singleWinner = computed(() => {
|
const singleWinner = computed(() => {
|
||||||
const winnerIds = game.state.winnerIds
|
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 => {
|
useGameActionNotification(action => {
|
|
@ -1,8 +1,8 @@
|
||||||
<template>
|
<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
|
<input
|
||||||
v-model="lobbyCodeInput"
|
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' : ''"
|
:class="isUnknown ? 'bg-red-800' : ''"
|
||||||
:maxlength="LOBBY_CODE_LENGTH"
|
:maxlength="LOBBY_CODE_LENGTH"
|
||||||
:placeholder="'X'.repeat(LOBBY_CODE_LENGTH)"
|
:placeholder="'X'.repeat(LOBBY_CODE_LENGTH)"
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
@keypress="onKeypress"
|
@keypress="onKeypress"
|
||||||
/>
|
/>
|
||||||
<button
|
<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"
|
:class="$style.button"
|
||||||
:disabled="lobbyCodeInput.length !== LOBBY_CODE_LENGTH"
|
:disabled="lobbyCodeInput.length !== LOBBY_CODE_LENGTH"
|
||||||
@click="join()"
|
@click="join()"
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
Join
|
Join
|
||||||
</button>
|
</button>
|
||||||
<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"
|
:class="$style.button"
|
||||||
@click="create()"
|
@click="create()"
|
||||||
>
|
>
|
||||||
|
@ -33,16 +33,16 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:disabled) {
|
&:not(:disabled):hover {
|
||||||
@apply hover:bg-opacity-20;
|
--un-bg-opacity: 10%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch, watchEffect } from "vue"
|
import { computed, ref, watch, watchEffect } from "vue"
|
||||||
import { useGame } from "../clientGame"
|
import { useGame } from "../clientGame"
|
||||||
import { LOBBY_CODE_LENGTH } from "../shared/constants"
|
import { LOBBY_CODE_LENGTH } from "../../shared/constants"
|
||||||
import { useBrowserLocation } from "@vueuse/core"
|
import { useBrowserLocation } from "@vueuse/core"
|
||||||
|
|
||||||
const location = useBrowserLocation()
|
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">
|
<div class="flex flex-col h-full justify-center items-center gap-8 text-4xl md:text-8xl">
|
||||||
<input
|
<input
|
||||||
v-model="usernameInput"
|
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"
|
placeholder="Username"
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
/>
|
/>
|
||||||
<button
|
<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"
|
:class="$style.button"
|
||||||
:disabled="usernameInput.length <= 0 || usernameInput.length > 20"
|
:disabled="usernameInput.length <= 0 || usernameInput.length > 20"
|
||||||
@click="submit()"
|
@click="submit()"
|
||||||
|
@ -23,16 +23,16 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:disabled) {
|
&:not(:disabled):hover {
|
||||||
@apply hover:bg-opacity-20;
|
--un-bg-opacity: 10%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watchEffect } from "vue"
|
import { ref, watchEffect } from "vue"
|
||||||
import { useGame } from "../clientGame"
|
import { useGame } from "../clientGame"
|
||||||
import { LOBBY_CODE_LENGTH } from "../shared/constants"
|
import { LOBBY_CODE_LENGTH } from "../../shared/constants"
|
||||||
import { useBrowserLocation } from "@vueuse/core"
|
import { useBrowserLocation } from "@vueuse/core"
|
||||||
import { useAuth } from "../auth"
|
import { useAuth } from "../auth"
|
||||||
|
|
|
@ -19,13 +19,13 @@
|
||||||
<div class="flex items-center gap-5 w-full flex-wrap">
|
<div class="flex items-center gap-5 w-full flex-wrap">
|
||||||
<NumberCard
|
<NumberCard
|
||||||
v-for="(card, index) in playerState.numberCards"
|
v-for="(card, index) in playerState.numberCards"
|
||||||
:key="[index, card.number]"
|
:key="[index, card.number].join('-')"
|
||||||
:number="card.number"
|
:number="card.number"
|
||||||
:is-covert="card.isCovert"
|
:is-covert="card.isCovert"
|
||||||
:is-own="isYou"
|
:is-own="isYou"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<div
|
||||||
v-if="playerState.specialCards.length === 0"
|
v-if="playerState.specialCards.length === 0"
|
||||||
class="font-fat opacity-40 text-2xl h-32 flex px-4 py-5"
|
class="font-fat opacity-40 text-2xl h-32 flex px-4 py-5"
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import NumberCard from "./NumberCard.vue"
|
import NumberCard from "./NumberCard.vue"
|
||||||
import { useAuth } from "../auth"
|
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 CrownIcon from "virtual:icons/ph/crown-simple-bold"
|
||||||
import DotsThreeOutlineIcon from "virtual:icons/ph/dots-three-outline"
|
import DotsThreeOutlineIcon from "virtual:icons/ph/dots-three-outline"
|
||||||
import SpecialCard from "./SpecialCard.vue"
|
import SpecialCard from "./SpecialCard.vue"
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useGame } from "../clientGame"
|
import { useGame } from "../clientGame"
|
||||||
import { naturallyJoinEnumeration } from "../shared/util"
|
import { naturallyJoinEnumeration } from "../../shared/util"
|
||||||
import BigButton from "./BigButton.vue"
|
import BigButton from "./BigButton.vue"
|
||||||
|
|
||||||
const game = useGame()
|
const game = useGame()
|
|
@ -24,9 +24,9 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Card from "./Card.vue"
|
import Card from "./Card.vue"
|
||||||
import type { SpecialCardId } from "../shared/game/cards"
|
import type { SpecialCardId } from "../../shared/game/cards"
|
||||||
import { specialCardIcons } from "../icons"
|
import { specialCardIcons } from "../icons"
|
||||||
import { specialCardsMeta } from "../shared/game/cards"
|
import { specialCardsMeta } from "../../shared/game/cards"
|
||||||
import { computed } from "vue"
|
import { computed } from "vue"
|
||||||
import { useGame } from "../clientGame"
|
import { useGame } from "../clientGame"
|
||||||
import { useThrottleFn } from "@vueuse/core"
|
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 type { Component } from "vue"
|
||||||
import ArrowArcRightIcon from "virtual:icons/ph/arrow-arc-right-bold"
|
import ArrowArcRightIcon from "virtual:icons/ph/arrow-arc-right-bold"
|
||||||
import PlusCircleIcon from "virtual:icons/ph/plus-circle"
|
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 { createTRPCProxyClient, httpLink, createWSClient, wsLink, splitLink } from "@trpc/client"
|
||||||
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")
|
23
index.html
23
index.html
|
@ -1,14 +1,15 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>TwentyOne</title>
|
<title>Twenty-one</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/inter/variable.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/@fontsource/titan-one/400.css"/>
|
||||||
</head>
|
<link rel="stylesheet" href="modern-normalize/modern-normalize.css"/>
|
||||||
<body>
|
</head>
|
||||||
<div id="app"></div>
|
<body>
|
||||||
</body>
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
83
package.json
83
package.json
|
@ -3,54 +3,53 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start:ui": "vite preview --host --port 4000",
|
"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:ui": "vite",
|
"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": {
|
"devDependencies": {
|
||||||
"@iconify-json/ph": "^1.1.5",
|
"@iconify-json/ph": "^1.2.2",
|
||||||
"@types/cookie": "^0.5.1",
|
"@types/express": "^5.0.0",
|
||||||
"@types/express": "^4.17.17",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/lodash-es": "^4.17.7",
|
"@types/node": "^22.13.8",
|
||||||
"@types/node": "18",
|
"@types/ws": "^8.5.14",
|
||||||
"@types/ws": "^8.5.4",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vitejs/plugin-vue": "^4.1.0",
|
"sass": "^1.85.1",
|
||||||
"prisma": "^4.13.0",
|
"tsx": "^4.19.3",
|
||||||
"sass": "^1.62.0",
|
"typescript": "^5.8.2",
|
||||||
"tsx": "^3.12.6",
|
"unocss": "66.1.0-beta.3",
|
||||||
"typescript": "^4.9.5",
|
"unplugin-icons": "^22.1.0",
|
||||||
"unplugin-icons": "^0.16.1",
|
"vite": "^6.2.0",
|
||||||
"vite": "^4.2.2",
|
|
||||||
"vite-plugin-pages": "^0.29.0",
|
|
||||||
"vite-plugin-windicss": "^1.8.10",
|
|
||||||
"windicss": "^3.5.6"
|
"windicss": "^3.5.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^4.5.15",
|
"@fontsource-variable/inter": "^5.2.5",
|
||||||
"@fontsource/titan-one": "^4.5.9",
|
"@fontsource/titan-one": "^5.2.5",
|
||||||
"@headlessui/vue": "^1.7.13",
|
"@headlessui/vue": "^1.7.23",
|
||||||
"@prisma/client": "^4.13.0",
|
"@trpc/client": "^10.45.2",
|
||||||
"@trpc/client": "^10.20.0",
|
"@trpc/server": "^10.45.2",
|
||||||
"@trpc/server": "^10.20.0",
|
"@types/cookie-parser": "^1.4.8",
|
||||||
"@types/cookie-parser": "^1.4.3",
|
"@vueuse/core": "^12.7.0",
|
||||||
"@vueuse/core": "^10.0.2",
|
"@vueuse/integrations": "^12.7.0",
|
||||||
"@vueuse/integrations": "^10.0.2",
|
"bufferutil": "^4.0.9",
|
||||||
"bufferutil": "^4.0.7",
|
"cookie": "^1.0.2",
|
||||||
"cookie": "^0.5.0",
|
"cookie-parser": "^1.4.7",
|
||||||
"cookie-parser": "^1.4.6",
|
"date-fns": "^4.1.0",
|
||||||
"date-fns": "^2.29.3",
|
"eventemitter3": "^5.0.1",
|
||||||
"eventemitter3": "^5.0.0",
|
"express": "^4.21.2",
|
||||||
"express": "^4.18.2",
|
"immer": "^10.1.1",
|
||||||
"immer": "^10.0.1",
|
"listhen": "^1.9.0",
|
||||||
"listhen": "^1.0.4",
|
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"nanoid": "^4.0.2",
|
"modern-normalize": "^3.0.1",
|
||||||
"pinia": "^2.0.34",
|
"nanoid": "^5.1.2",
|
||||||
"vue": "^3.2.47",
|
"pinia": "^3.0.1",
|
||||||
"vue-router": "^4.1.6",
|
"vue": "^3.5.13",
|
||||||
"ws": "^8.13.0",
|
"vue-router": "^4.5.0",
|
||||||
"zod": "^3.21.4"
|
"ws": "^8.18.1",
|
||||||
|
"zod": "^3.24.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "22"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
5758
pnpm-lock.yaml
generated
5758
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": {
|
"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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,22 @@
|
||||||
import { defineConfig } from "vite-plugin-windicss"
|
import { defineConfig, presetWind } from "unocss"
|
||||||
import colors from "windicss/colors"
|
import colors from "windicss/colors"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
presets: [
|
||||||
|
presetWind({
|
||||||
|
arbitraryVariants: true,
|
||||||
|
preflight: true
|
||||||
|
})
|
||||||
|
],
|
||||||
theme: {
|
theme: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
"normal": ["InterVariable", "sans-serif"],
|
"normal": `"Inter Variable", sans-serif`,
|
||||||
"fat": ["Titan One", "sans-serif"]
|
"fat": `"Titan One", sans-serif`
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
transparent: "transparent",
|
transparent: "transparent",
|
||||||
white: "white",
|
white: colors.white,
|
||||||
black: "black",
|
black: colors.black,
|
||||||
gray: colors.zinc,
|
gray: colors.zinc,
|
||||||
light: colors.light,
|
light: colors.light,
|
||||||
dark: colors.dark,
|
dark: colors.dark,
|
||||||
|
@ -20,4 +26,4 @@ export default defineConfig({
|
||||||
blue: colors.indigo,
|
blue: colors.indigo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
|
@ -1,21 +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 pagesPlugin from "vite-plugin-pages"
|
import unoCssPlugin from "unocss/vite"
|
||||||
import windiPlugin from "vite-plugin-windicss"
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vuePlugin(),
|
vuePlugin(),
|
||||||
iconsPlugin(),
|
iconsPlugin(),
|
||||||
pagesPlugin(),
|
unoCssPlugin()
|
||||||
windiPlugin()
|
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/trpc": "http://127.0.0.1:3000",
|
"/trpc": "http://localhost:3000",
|
||||||
"/ws": {
|
"/ws": {
|
||||||
target: "http://127.0.0.1:3000",
|
target: "http://localhost:3000",
|
||||||
ws: true
|
ws: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue