feat: initialize game frontend with Vue, routing, and session management

- Add index.html as the entry point for the application.
- Create App.vue to serve as the main application component with RouterView.
- Implement global styles in main.css using Tailwind CSS.
- Set up TypeScript definitions for Vue components in env.d.ts.
- Configure Vue Router with routes for home, public, login, and not found views.
- Extend RouteMeta interface to include authentication-related metadata.
- Create a Pinia store for session management with status tracking.
- Implement TRPC client for API communication with session token handling.
- Develop views for Login, Main, Not Found, and Public with basic structure.
- Configure TypeScript for Node with tsconfig.node.json.
- Set up Vite configuration for the project with Vue and Tailwind CSS plugins.
This commit is contained in:
2026-01-15 17:57:41 +00:00
parent 44ec80c93b
commit 15625f2c28
18 changed files with 768 additions and 48 deletions
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sammo HiDCHe - Game</title>
</head>
<body class="bg-black text-white">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+24 -5
View File
@@ -4,18 +4,37 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"dev": "node -e \"console.log('dev not configured')\"",
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "node -e \"console.log('test not configured')\"",
"typecheck": "tsc -b"
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"@vueuse/core": "^13.9.0",
"@sammo-ts/common": "workspace:*",
"@sammo-ts/game-api": "workspace:*",
"@sammo-ts/logic": "workspace:*",
"@trpc/client": "^11.8.1",
"@trpc/server": "^11.8.1",
"@vueuse/core": "^14.1.0",
"date-fns": "^4.1.0",
"es-toolkit": "^1.43.0",
"mitt": "^3.0.1",
"zod": "^4.3.4"
"pinia": "^3.0.4",
"vue": "^3.5.26",
"vue-router": "^4.6.4",
"zod": "^4.3.5"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
"@vitejs/plugin-vue": "^6.0.3",
"autoprefixer": "^10.4.23",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vue-tsc": "^3.2.2"
}
}
+11
View File
@@ -0,0 +1,11 @@
<script setup lang="ts">
import { RouterView } from 'vue-router';
</script>
<template>
<RouterView />
</template>
<style>
/* Global styles can live in assets/main.css */
</style>
+12
View File
@@ -0,0 +1,12 @@
@import 'tailwindcss';
@theme {
--color-sammo-ink: #101010;
--color-sammo-parchment: #e8ddc4;
--color-sammo-gold: #c9a45a;
}
body {
@apply bg-sammo-ink text-sammo-parchment;
font-family: 'Galmuri11', 'Noto Serif KR', 'Malgun Gothic', serif;
}
+7
View File
@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<Record<string, never>, Record<string, never>, Record<string, never>>;
export default component;
}
+12 -1
View File
@@ -1 +1,12 @@
export {};
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import './assets/main.css';
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.mount('#app');
+65
View File
@@ -0,0 +1,65 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
import MainView from '../views/MainView.vue';
import PublicView from '../views/PublicView.vue';
import LoginView from '../views/LoginView.vue';
import NotFoundView from '../views/NotFoundView.vue';
import { useSessionStore } from '../stores/session';
const routes = [
{
path: '/',
name: 'home',
component: MainView,
meta: {
requiresAuth: true,
requiresGeneral: true,
},
},
{
path: '/public',
name: 'public',
component: PublicView,
},
{
path: '/login',
name: 'login',
component: LoginView,
meta: {
publicOnly: true,
},
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: NotFoundView,
},
] satisfies RouteRecordRaw[];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
router.beforeEach((to) => {
const session = useSessionStore();
if (!session.isReady) {
return true;
}
if (to.meta.publicOnly && session.isAuthed) {
return { name: 'home' };
}
if (to.meta.requiresAuth && !session.isAuthed) {
return { name: 'public' };
}
if (to.meta.requiresGeneral && !session.hasGeneral) {
return { name: 'public' };
}
return true;
});
export default router;
+9
View File
@@ -0,0 +1,9 @@
import 'vue-router';
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean;
requiresGeneral?: boolean;
publicOnly?: boolean;
}
}
+23
View File
@@ -0,0 +1,23 @@
import { defineStore } from 'pinia';
export type SessionStatus = 'unknown' | 'public' | 'authed' | 'general';
interface SessionState {
status: SessionStatus;
}
export const useSessionStore = defineStore('session', {
state: (): SessionState => ({
status: 'unknown',
}),
getters: {
isReady: (state) => state.status !== 'unknown',
isAuthed: (state) => state.status === 'authed' || state.status === 'general',
hasGeneral: (state) => state.status === 'general',
},
actions: {
setStatus(status: SessionStatus) {
this.status = status;
},
},
});
+22
View File
@@ -0,0 +1,22 @@
import { createTRPCProxyClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from '@sammo-ts/game-api';
const getSessionToken = (): string | null => {
if (typeof window === 'undefined') {
return null;
}
return window.localStorage.getItem('sammo-session-token');
};
export const trpc = createTRPCProxyClient<AppRouter>({
links: [
httpBatchLink({
url: '/api/trpc',
headers() {
const token = getSessionToken();
return token ? { 'x-session-token': token } : {};
},
}),
],
});
@@ -0,0 +1,8 @@
<script setup lang="ts"></script>
<template>
<main class="min-h-screen px-6 py-8">
<h1 class="text-2xl font-semibold">Login</h1>
<p class="mt-2 text-sm text-amber-200/80">Authentication entry point.</p>
</main>
</template>
+10
View File
@@ -0,0 +1,10 @@
<script setup lang="ts"></script>
<template>
<main class="min-h-screen px-6 py-8">
<h1 class="text-2xl font-semibold">Game Home</h1>
<p class="mt-2 text-sm text-amber-200/80">
Authenticated main screen placeholder.
</p>
</main>
</template>
@@ -0,0 +1,10 @@
<script setup lang="ts"></script>
<template>
<main class="min-h-screen px-6 py-8">
<h1 class="text-2xl font-semibold">Not Found</h1>
<p class="mt-2 text-sm text-amber-200/80">
The requested page does not exist.
</p>
</main>
</template>
@@ -0,0 +1,10 @@
<script setup lang="ts"></script>
<template>
<main class="min-h-screen px-6 py-8">
<h1 class="text-2xl font-semibold">Public Lobby</h1>
<p class="mt-2 text-sm text-amber-200/80">
Public cached map and world trend entry point.
</p>
</main>
</template>
+42 -5
View File
@@ -1,9 +1,46 @@
{
"extends": "../../tsconfig.paths.json",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"composite": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"]
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@sammo-ts/common": ["../../packages/common/src/index.ts"],
"@sammo-ts/common/*": ["../../packages/common/src/*"],
"@sammo-ts/infra": ["../../packages/infra/src/index.ts"],
"@sammo-ts/infra/*": ["../../packages/infra/src/*"],
"@sammo-ts/logic": ["../../packages/logic/src/index.ts"],
"@sammo-ts/logic/*": ["../../packages/logic/src/*"],
"@sammo-ts/game-engine": ["../../app/game-engine/src/index.ts"],
"@sammo-ts/game-engine/*": ["../../app/game-engine/src/*"],
"@sammo-ts/game-api": ["../../app/game-api/src/index.ts"],
"@sammo-ts/game-api/*": ["../../app/game-api/src/*"]
}
},
"include": ["src", "test", "*.ts"]
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"test/**/*.ts"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}
+17
View File
@@ -0,0 +1,17 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import tailwindcss from '@tailwindcss/vite';
import path from 'path';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3001,
},
});