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,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>
|
||||
Reference in New Issue
Block a user