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
+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>