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:
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
Vendored
+7
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -0,0 +1,9 @@
|
||||
import 'vue-router';
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
requiresAuth?: boolean;
|
||||
requiresGeneral?: boolean;
|
||||
publicOnly?: boolean;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user