Add Hono backend with PostgreSQL, auth, cloud saves, and leaderboard

Server app (apps/server) with Hono framework and Drizzle ORM:
- PostgreSQL schema: users, saves, leaderboard, achievements tables
- Anonymous auth with UUID tokens, optional email/password linking
- Cloud save API: list, get, upsert, delete with auto-save hook
- Leaderboard API: per-category rankings with score submission
- CORS configured for dev server ports
- Typed middleware with Hono env variables

Frontend cloud save integration:
- API client with auth token management in localStorage
- useCloudSave hook auto-saves every 300 ticks when authenticated
- Vite env type declarations for VITE_API_URL

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-24 17:35:18 -04:00
parent 8c9555bc08
commit 8ea6c771a1
16 changed files with 1289 additions and 9 deletions
+44
View File
@@ -0,0 +1,44 @@
import { useEffect, useRef } from 'react';
import { useGameStore } from '@/store';
import { api, getAuthToken, setAuthToken } from '@/lib/api';
import { AUTO_SAVE_INTERVAL_TICKS } from '@ai-tycoon/shared';
export function useCloudSave() {
const tickCount = useGameStore((s) => s.meta.tickCount);
const companyName = useGameStore((s) => s.meta.companyName);
const lastSaveTick = useRef(0);
useEffect(() => {
if (!companyName) return;
if (tickCount - lastSaveTick.current < AUTO_SAVE_INTERVAL_TICKS * 5) return;
const token = getAuthToken();
if (!token) return;
lastSaveTick.current = tickCount;
const state = useGameStore.getState();
const { activePage, notifications, ...gameState } = state;
api.saves.put({
companyName: state.meta.companyName,
saveVersion: state.meta.saveVersion,
gameData: gameState,
tickCount: state.meta.tickCount,
era: state.meta.currentEra,
}).catch(() => {});
}, [tickCount, companyName]);
}
export async function ensureAuth(): Promise<string | null> {
let token = getAuthToken();
if (token) return token;
try {
const result = await api.auth.anonymous();
setAuthToken(result.token);
return result.token;
} catch {
return null;
}
}