feat : init Project
This commit is contained in:
24
nest-intra/.gitignore
vendored
Normal file
24
nest-intra/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -1,32 +1,60 @@
|
|||||||
# CrowMate Intranet
|
# CrowMate Intranet
|
||||||
|
|
||||||
This folder contains the intranet section separated from the main website repository.
|
Internal staff portal for CrowMate Studio — Headless Hazard.
|
||||||
|
|
||||||
## Files Moved
|
## Quick Start
|
||||||
|
|
||||||
### Pages
|
```bash
|
||||||
- `src/pages/intranet/IntranetDashboard.tsx`
|
npm install
|
||||||
- `src/pages/intranet/IntranetBugs.tsx`
|
npm run dev
|
||||||
- `src/pages/intranet/IntranetFeed.tsx`
|
```
|
||||||
- `src/pages/intranet/IntranetEvents.tsx`
|
|
||||||
- `src/pages/intranet/IntranetUsers.tsx`
|
|
||||||
- `src/pages/intranet/IntranetModeration.tsx`
|
|
||||||
|
|
||||||
### Components
|
The intranet runs on **http://localhost:5174** (port 5174 to avoid conflicts with the public site on 5173).
|
||||||
- `src/components/layout/IntranetLayout.tsx`
|
|
||||||
|
|
||||||
## Next Steps
|
## Demo Accounts
|
||||||
|
|
||||||
To set up this as a separate repository:
|
| Account | Email | Role |
|
||||||
|
|---------|-------|------|
|
||||||
|
| Kestrel (Admin) | `kestrel@crowmate.dev` | dev + admin |
|
||||||
|
| Vesper (Staff) | `vesper@crowmate.dev` | com |
|
||||||
|
|
||||||
1. Initialize Git repository:
|
Regular user accounts are blocked from intranet access.
|
||||||
```bash
|
|
||||||
cd /home/norsys/Delivery/website-intranet
|
|
||||||
git init
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Copy necessary configuration files from the main website (package.json, tsconfig, vite.config, etc.)
|
## Project Structure
|
||||||
|
|
||||||
3. Install dependencies and set up the project structure
|
```
|
||||||
|
src/
|
||||||
|
App.tsx # Router (login + intranet routes)
|
||||||
|
main.tsx # Entry point
|
||||||
|
index.css # Design tokens & global styles
|
||||||
|
contexts/
|
||||||
|
AuthContext.tsx # Auth state (staff-only login)
|
||||||
|
components/
|
||||||
|
layout/
|
||||||
|
IntranetLayout.tsx # Sidebar + main content layout
|
||||||
|
shared/
|
||||||
|
ProtectedRoute.tsx # Route guard (auth + staff check)
|
||||||
|
PageLoader.tsx # Loading spinner
|
||||||
|
pages/
|
||||||
|
LoginPage.tsx # Staff login page
|
||||||
|
intranet/
|
||||||
|
IntranetDashboard.tsx # Overview stats & nav tiles
|
||||||
|
IntranetBugs.tsx # Bug triage (filter, assign, notes)
|
||||||
|
IntranetFeed.tsx # Staff-only activity feed
|
||||||
|
IntranetEvents.tsx # Create/manage events & polls
|
||||||
|
IntranetUsers.tsx # User management (promote/ban)
|
||||||
|
IntranetModeration.tsx # Forum moderation (pin/lock/delete)
|
||||||
|
data/
|
||||||
|
mockData.ts # All mock data
|
||||||
|
types/
|
||||||
|
index.ts # TypeScript interfaces
|
||||||
|
utils/
|
||||||
|
format.ts # Date formatting helpers
|
||||||
|
```
|
||||||
|
|
||||||
4. Update imports and routing as needed for the standalone intranet application
|
## Scripts
|
||||||
|
|
||||||
|
- `npm run dev` — Start dev server
|
||||||
|
- `npm run build` — Type-check + production build
|
||||||
|
- `npm run lint` — Run ESLint
|
||||||
|
- `npm run preview` — Preview production build
|
||||||
|
|||||||
23
nest-intra/eslint.config.js
Normal file
23
nest-intra/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
16
nest-intra/index.html
Normal file
16
nest-intra/index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#0a0d0a" />
|
||||||
|
<meta name="description" content="CrowMate Studio — Internal Intranet" />
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
<title>CrowMate Intranet</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3833
nest-intra/package-lock.json
generated
Normal file
3833
nest-intra/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
nest-intra/package.json
Normal file
33
nest-intra/package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "nest-intra",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0",
|
||||||
|
"react-router-dom": "^6.30.3",
|
||||||
|
"tailwindcss": "^4.1.18"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/react": "^19.2.7",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.48.0",
|
||||||
|
"vite": "^7.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
54
nest-intra/src/App.tsx
Normal file
54
nest-intra/src/App.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { lazy, Suspense } from 'react';
|
||||||
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
|
import { ProtectedRoute } from './components/shared/ProtectedRoute';
|
||||||
|
import { IntranetLayout } from './components/layout/IntranetLayout';
|
||||||
|
import { PageLoader } from './components/shared/PageLoader';
|
||||||
|
|
||||||
|
// ── Pages (lazy-loaded) ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const LoginPage = lazy(() => import('./pages/LoginPage'));
|
||||||
|
const IntranetDashboard = lazy(() => import('./pages/intranet/IntranetDashboard'));
|
||||||
|
const IntranetBugs = lazy(() => import('./pages/intranet/IntranetBugs'));
|
||||||
|
const IntranetFeed = lazy(() => import('./pages/intranet/IntranetFeed'));
|
||||||
|
const IntranetEvents = lazy(() => import('./pages/intranet/IntranetEvents'));
|
||||||
|
const IntranetUsers = lazy(() => import('./pages/intranet/IntranetUsers'));
|
||||||
|
const IntranetModeration = lazy(() => import('./pages/intranet/IntranetModeration'));
|
||||||
|
|
||||||
|
// ── App ────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<AuthProvider>
|
||||||
|
<Suspense fallback={<PageLoader />}>
|
||||||
|
<Routes>
|
||||||
|
{/* Login */}
|
||||||
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
|
||||||
|
{/* Intranet (staff only) */}
|
||||||
|
<Route
|
||||||
|
path="/intranet"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute staffOnly redirectTo="/login">
|
||||||
|
<IntranetLayout />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Route index element={<IntranetDashboard />} />
|
||||||
|
<Route path="bugs" element={<IntranetBugs />} />
|
||||||
|
<Route path="feed" element={<IntranetFeed />} />
|
||||||
|
<Route path="events" element={<IntranetEvents />} />
|
||||||
|
<Route path="users" element={<IntranetUsers />} />
|
||||||
|
<Route path="moderation" element={<IntranetModeration />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
{/* Redirect root to intranet */}
|
||||||
|
<Route path="/" element={<Navigate to="/intranet" replace />} />
|
||||||
|
|
||||||
|
{/* Catch-all: redirect to intranet */}
|
||||||
|
<Route path="*" element={<Navigate to="/intranet" replace />} />
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
|
</AuthProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
25
nest-intra/src/components/shared/PageLoader.tsx
Normal file
25
nest-intra/src/components/shared/PageLoader.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export function PageLoader() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 flex items-center justify-center"
|
||||||
|
style={{ background: 'var(--color-bg)' }}
|
||||||
|
role="status"
|
||||||
|
aria-label="Loading"
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<div
|
||||||
|
className="text-4xl font-bold mb-4 cursor-blink"
|
||||||
|
style={{
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
color: 'var(--color-amber)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
LOADING
|
||||||
|
</div>
|
||||||
|
<div style={{ color: 'var(--color-text-muted)', fontSize: '0.75rem', letterSpacing: '0.2em' }}>
|
||||||
|
CROWMATE STUDIO / INTRANET
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
nest-intra/src/components/shared/ProtectedRoute.tsx
Normal file
29
nest-intra/src/components/shared/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Navigate, useLocation } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
|
||||||
|
interface ProtectedRouteProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
/** If true, requires staff role (dev or com). */
|
||||||
|
staffOnly?: boolean;
|
||||||
|
/** Redirect destination when access is denied. Defaults to /login. */
|
||||||
|
redirectTo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProtectedRoute({
|
||||||
|
children,
|
||||||
|
staffOnly = false,
|
||||||
|
redirectTo = '/login',
|
||||||
|
}: ProtectedRouteProps) {
|
||||||
|
const { isAuthenticated, isStaff } = useAuth();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to={redirectTo} state={{ from: location }} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (staffOnly && !isStaff) {
|
||||||
|
return <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
122
nest-intra/src/contexts/AuthContext.tsx
Normal file
122
nest-intra/src/contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import type { User, UserRole } from '../types';
|
||||||
|
import { MOCK_USERS } from '../data/mockData';
|
||||||
|
|
||||||
|
// ── Types ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface AuthContextValue {
|
||||||
|
user: User | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
isStaff: boolean;
|
||||||
|
isAdmin: boolean;
|
||||||
|
login: (email: string, password: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
logout: () => void;
|
||||||
|
updateUsername: (username: string) => void;
|
||||||
|
devSetRole: (role: UserRole) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Context ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||||
|
|
||||||
|
// ── Provider ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'crowmate_intra_user';
|
||||||
|
|
||||||
|
function loadUserFromStorage(): User | null {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) return null;
|
||||||
|
return JSON.parse(raw) as User;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveUserToStorage(user: User | null): void {
|
||||||
|
if (user) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(user));
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [user, setUser] = useState<User | null>(loadUserFromStorage);
|
||||||
|
|
||||||
|
const isAuthenticated = user !== null;
|
||||||
|
const isStaff = user?.role === 'dev' || user?.role === 'com';
|
||||||
|
const isAdmin = user?.isAdmin === true;
|
||||||
|
|
||||||
|
const login = useCallback(
|
||||||
|
async (email: string, _password: string): Promise<{ success: boolean; error?: string }> => {
|
||||||
|
// Simulate network delay
|
||||||
|
await new Promise((r) => setTimeout(r, 400));
|
||||||
|
|
||||||
|
const found = MOCK_USERS.find(
|
||||||
|
(u) => u.email.toLowerCase() === email.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
return { success: false, error: 'No account found with that email address.' };
|
||||||
|
}
|
||||||
|
if (found.isBanned) {
|
||||||
|
return { success: false, error: 'This account has been suspended.' };
|
||||||
|
}
|
||||||
|
if (found.role === 'user') {
|
||||||
|
return { success: false, error: 'Access denied. Staff accounts only.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
setUser(found);
|
||||||
|
saveUserToStorage(found);
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const logout = useCallback(() => {
|
||||||
|
setUser(null);
|
||||||
|
saveUserToStorage(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateUsername = useCallback((username: string) => {
|
||||||
|
setUser((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
const updated = { ...prev, username };
|
||||||
|
saveUserToStorage(updated);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const devSetRole = useCallback((role: UserRole) => {
|
||||||
|
setUser((prev) => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
const updated = { ...prev, role, isAdmin: role === 'dev' };
|
||||||
|
saveUserToStorage(updated);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = useMemo<AuthContextValue>(
|
||||||
|
() => ({ user, isAuthenticated, isStaff, isAdmin, login, logout, updateUsername, devSetRole }),
|
||||||
|
[user, isAuthenticated, isStaff, isAdmin, login, logout, updateUsername, devSetRole]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hook ───────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function useAuth(): AuthContextValue {
|
||||||
|
const ctx = useContext(AuthContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('useAuth must be used inside <AuthProvider>');
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
797
nest-intra/src/data/mockData.ts
Normal file
797
nest-intra/src/data/mockData.ts
Normal file
@@ -0,0 +1,797 @@
|
|||||||
|
import type {
|
||||||
|
User,
|
||||||
|
ForumCategory,
|
||||||
|
ForumThread,
|
||||||
|
ForumReply,
|
||||||
|
BugReport,
|
||||||
|
BugComment,
|
||||||
|
BugReportNote,
|
||||||
|
StaffPost,
|
||||||
|
TeamMember,
|
||||||
|
EventPost,
|
||||||
|
Poll,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
// ── Mock Users ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const MOCK_USERS: User[] = [
|
||||||
|
{
|
||||||
|
id: 'u1',
|
||||||
|
username: 'Kestrel',
|
||||||
|
email: 'kestrel@crowmate.dev',
|
||||||
|
role: 'dev',
|
||||||
|
isAdmin: true,
|
||||||
|
isBanned: false,
|
||||||
|
createdAt: '2023-09-01T08:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'u2',
|
||||||
|
username: 'Vesper',
|
||||||
|
email: 'vesper@crowmate.dev',
|
||||||
|
role: 'com',
|
||||||
|
isAdmin: false,
|
||||||
|
isBanned: false,
|
||||||
|
createdAt: '2023-09-03T10:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'u3',
|
||||||
|
username: 'GlitchHunter',
|
||||||
|
email: 'glitch@mail.com',
|
||||||
|
role: 'user',
|
||||||
|
isAdmin: false,
|
||||||
|
isBanned: false,
|
||||||
|
createdAt: '2024-01-15T14:22:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'u4',
|
||||||
|
username: 'NullPointer',
|
||||||
|
email: 'null@mail.com',
|
||||||
|
role: 'user',
|
||||||
|
isAdmin: false,
|
||||||
|
isBanned: false,
|
||||||
|
createdAt: '2024-02-20T09:11:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'u5',
|
||||||
|
username: 'XenoArch',
|
||||||
|
email: 'xeno@mail.com',
|
||||||
|
role: 'user',
|
||||||
|
isAdmin: false,
|
||||||
|
isBanned: false,
|
||||||
|
createdAt: '2024-03-05T16:44:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'u6',
|
||||||
|
username: 'Phantom404',
|
||||||
|
email: 'phantom@mail.com',
|
||||||
|
role: 'user',
|
||||||
|
isAdmin: false,
|
||||||
|
isBanned: true,
|
||||||
|
createdAt: '2024-04-12T12:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'u7',
|
||||||
|
username: 'NeonCrawler',
|
||||||
|
email: 'neon@mail.com',
|
||||||
|
role: 'user',
|
||||||
|
isAdmin: false,
|
||||||
|
isBanned: false,
|
||||||
|
createdAt: '2024-05-01T08:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'u8',
|
||||||
|
username: 'ByteWitch',
|
||||||
|
email: 'byte@mail.com',
|
||||||
|
role: 'dev',
|
||||||
|
isAdmin: false,
|
||||||
|
isBanned: false,
|
||||||
|
createdAt: '2023-09-10T11:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Quick-access login credentials for demo
|
||||||
|
// Any email + password combo works. Role determined by MOCK_USERS list.
|
||||||
|
export const DEMO_CREDENTIALS: Record<string, string> = {
|
||||||
|
admin: 'admin@crowmate.dev', // kestrel — dev + admin
|
||||||
|
staff: 'vesper@crowmate.dev', // vesper — com
|
||||||
|
user: 'glitch@mail.com', // glitchhunter — user
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Mock Forum Categories ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const MOCK_CATEGORIES: ForumCategory[] = [
|
||||||
|
{
|
||||||
|
id: 'cat1',
|
||||||
|
name: 'General Discussion',
|
||||||
|
description: 'Everything and anything about Headless Hazard.',
|
||||||
|
icon: '///',
|
||||||
|
threadCount: 42,
|
||||||
|
lastActivity: '2026-02-17T18:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cat2',
|
||||||
|
name: 'Game Suggestions',
|
||||||
|
description: 'Share your ideas to improve the game.',
|
||||||
|
icon: '[!]',
|
||||||
|
threadCount: 27,
|
||||||
|
lastActivity: '2026-02-16T09:15:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cat3',
|
||||||
|
name: 'Multiplayer',
|
||||||
|
description: 'Find teammates, share strategies, report cheaters.',
|
||||||
|
icon: '>>',
|
||||||
|
threadCount: 19,
|
||||||
|
lastActivity: '2026-02-18T07:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cat4',
|
||||||
|
name: 'Lore & Theories',
|
||||||
|
description: 'Dig into the deep lore of the corporate complex.',
|
||||||
|
icon: '[?]',
|
||||||
|
threadCount: 33,
|
||||||
|
lastActivity: '2026-02-15T21:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cat5',
|
||||||
|
name: 'Off Topic',
|
||||||
|
description: 'Chat about anything not related to the game.',
|
||||||
|
icon: '~',
|
||||||
|
threadCount: 14,
|
||||||
|
lastActivity: '2026-02-14T13:45:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cat6',
|
||||||
|
name: 'Technical Support',
|
||||||
|
description: 'Having trouble running the game? Ask here.',
|
||||||
|
icon: '[X]',
|
||||||
|
threadCount: 8,
|
||||||
|
lastActivity: '2026-02-17T10:20:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Mock Forum Threads ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const MOCK_THREADS: ForumThread[] = [
|
||||||
|
{
|
||||||
|
id: 'th1',
|
||||||
|
title: 'Official Welcome Thread — Read Before Posting!',
|
||||||
|
authorId: 'u2',
|
||||||
|
authorName: 'Vesper',
|
||||||
|
categoryId: 'cat1',
|
||||||
|
categoryName: 'General Discussion',
|
||||||
|
content: `Welcome to the official Headless Hazard community forum. Before you post, please read our community guidelines.\n\n1. Be respectful\n2. No spoilers without tags\n3. Use the bug report page for bugs, not the forum\n\nHappy gaming — and watch out for rogue security protocols.`,
|
||||||
|
isPinned: true,
|
||||||
|
isLocked: false,
|
||||||
|
replyCount: 12,
|
||||||
|
createdAt: '2025-11-01T10:00:00Z',
|
||||||
|
updatedAt: '2026-01-10T08:00:00Z',
|
||||||
|
lastReplyAuthor: 'NeonCrawler',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'th2',
|
||||||
|
title: 'The head physics feel clunky — how do you control it?',
|
||||||
|
authorId: 'u3',
|
||||||
|
authorName: 'GlitchHunter',
|
||||||
|
categoryId: 'cat1',
|
||||||
|
categoryName: 'General Discussion',
|
||||||
|
content: `I've been playing for 3 hours and I still can't wrap my head (lol) around the head movement controls. The inertia system is wild. Anyone have tips?\n\nI keep slamming the head into walls when I try to look around corners.`,
|
||||||
|
isPinned: false,
|
||||||
|
isLocked: false,
|
||||||
|
replyCount: 8,
|
||||||
|
createdAt: '2026-01-20T15:30:00Z',
|
||||||
|
updatedAt: '2026-01-22T11:00:00Z',
|
||||||
|
lastReplyAuthor: 'XenoArch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'th3',
|
||||||
|
title: 'SUGGESTION: Let us name the girl!',
|
||||||
|
authorId: 'u4',
|
||||||
|
authorName: 'NullPointer',
|
||||||
|
categoryId: 'cat2',
|
||||||
|
categoryName: 'Game Suggestions',
|
||||||
|
content: `She's referred to as "the girl" throughout the whole game. I think we should be able to name her. It would add so much to the emotional connection.\n\nSome candidates I thought of: Zara, Pip, Elodie, Mira...`,
|
||||||
|
isPinned: false,
|
||||||
|
isLocked: false,
|
||||||
|
replyCount: 31,
|
||||||
|
createdAt: '2026-01-25T09:00:00Z',
|
||||||
|
updatedAt: '2026-02-10T17:30:00Z',
|
||||||
|
lastReplyAuthor: 'ByteWitch',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'th4',
|
||||||
|
title: 'Looking for co-op partner — Floor 3 boss is brutal',
|
||||||
|
authorId: 'u5',
|
||||||
|
authorName: 'XenoArch',
|
||||||
|
categoryId: 'cat3',
|
||||||
|
categoryName: 'Multiplayer',
|
||||||
|
content: `Floor 3 boss: The Sentinel Prime. It tracks the body AND the head simultaneously. Solo is nearly impossible on hard mode.\n\nAnyone want to team up? I'm online weekday evenings UTC+1.`,
|
||||||
|
isPinned: false,
|
||||||
|
isLocked: false,
|
||||||
|
replyCount: 5,
|
||||||
|
createdAt: '2026-02-01T20:00:00Z',
|
||||||
|
updatedAt: '2026-02-03T09:15:00Z',
|
||||||
|
lastReplyAuthor: 'NeonCrawler',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'th5',
|
||||||
|
title: 'THEORY: The girl is the daughter of the [BLEEP] CEO',
|
||||||
|
authorId: 'u7',
|
||||||
|
authorName: 'NeonCrawler',
|
||||||
|
categoryId: 'cat4',
|
||||||
|
categoryName: 'Lore & Theories',
|
||||||
|
content: `Hear me out. There's a family portrait in the executive suite on floor 5. The girl in the painting has the same red hair as our protagonist. AND the security clearance codes found in the vault match a name that's always been redacted...\n\nI think her father IS the corporation. This changes everything about why the protocols went haywire.`,
|
||||||
|
isPinned: false,
|
||||||
|
isLocked: false,
|
||||||
|
replyCount: 44,
|
||||||
|
createdAt: '2026-02-05T11:00:00Z',
|
||||||
|
updatedAt: '2026-02-17T22:10:00Z',
|
||||||
|
lastReplyAuthor: 'GlitchHunter',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'th6',
|
||||||
|
title: 'Game crashes on startup — Windows 11 RTX 4080',
|
||||||
|
authorId: 'u3',
|
||||||
|
authorName: 'GlitchHunter',
|
||||||
|
categoryId: 'cat6',
|
||||||
|
categoryName: 'Technical Support',
|
||||||
|
content: `Getting a DirectX 12 error on startup. Already tried reinstalling, verifying files, and updating drivers.\n\nError: DXGI_ERROR_DEVICE_REMOVED\nOS: Windows 11 22H2\nGPU: RTX 4080`,
|
||||||
|
isPinned: false,
|
||||||
|
isLocked: false,
|
||||||
|
replyCount: 3,
|
||||||
|
createdAt: '2026-02-10T14:20:00Z',
|
||||||
|
updatedAt: '2026-02-11T10:00:00Z',
|
||||||
|
lastReplyAuthor: 'Vesper',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'th7',
|
||||||
|
title: 'The VHS aesthetic is a MASTERPIECE — appreciation post',
|
||||||
|
authorId: 'u4',
|
||||||
|
authorName: 'NullPointer',
|
||||||
|
categoryId: 'cat1',
|
||||||
|
categoryName: 'General Discussion',
|
||||||
|
content: `Just want to say: whoever did the visual design deserves an award. The scan lines, the color grading, the way the CRT flickers when the head rolls across a monitor screen — *chef's kiss*.\n\nThis is what indie games are about.`,
|
||||||
|
isPinned: false,
|
||||||
|
isLocked: false,
|
||||||
|
replyCount: 17,
|
||||||
|
createdAt: '2026-02-12T08:45:00Z',
|
||||||
|
updatedAt: '2026-02-16T19:30:00Z',
|
||||||
|
lastReplyAuthor: 'Kestrel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'th8',
|
||||||
|
title: 'Speedrun strats — sub 45min run possible?',
|
||||||
|
authorId: 'u5',
|
||||||
|
authorName: 'XenoArch',
|
||||||
|
categoryId: 'cat3',
|
||||||
|
categoryName: 'Multiplayer',
|
||||||
|
content: `Current WR is 48:32 by user HexBlade (not on this forum). I think sub-45 is possible with the elevator skip on floor 2 and the head-throw glitch to open the airlock.\n\nLet's document all known skips here.`,
|
||||||
|
isPinned: false,
|
||||||
|
isLocked: false,
|
||||||
|
replyCount: 9,
|
||||||
|
createdAt: '2026-02-14T17:00:00Z',
|
||||||
|
updatedAt: '2026-02-18T06:00:00Z',
|
||||||
|
lastReplyAuthor: 'NullPointer',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Mock Replies ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const MOCK_REPLIES: ForumReply[] = [
|
||||||
|
// Thread 1 replies
|
||||||
|
{
|
||||||
|
id: 'r1',
|
||||||
|
content: 'Thanks for the welcome! Excited to dive into this game. The concept sounds wild.',
|
||||||
|
authorId: 'u3',
|
||||||
|
authorName: 'GlitchHunter',
|
||||||
|
threadId: 'th1',
|
||||||
|
createdAt: '2025-11-02T09:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'r2',
|
||||||
|
content: 'Read the guidelines. Solid rules. Looking forward to theorizing about the lore!',
|
||||||
|
authorId: 'u7',
|
||||||
|
authorName: 'NeonCrawler',
|
||||||
|
threadId: 'th1',
|
||||||
|
createdAt: '2025-11-05T14:00:00Z',
|
||||||
|
},
|
||||||
|
// Thread 2 replies
|
||||||
|
{
|
||||||
|
id: 'r3',
|
||||||
|
content: 'The trick is to use short bursts. Don\'t hold the direction key — tap it. The head has massive momentum.',
|
||||||
|
authorId: 'u5',
|
||||||
|
authorName: 'XenoArch',
|
||||||
|
threadId: 'th2',
|
||||||
|
createdAt: '2026-01-20T16:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'r4',
|
||||||
|
content: 'Also check your sensitivity settings. Mine was at 100% by default which is insane. I dropped it to 35%.',
|
||||||
|
authorId: 'u4',
|
||||||
|
authorName: 'NullPointer',
|
||||||
|
threadId: 'th2',
|
||||||
|
createdAt: '2026-01-21T09:30:00Z',
|
||||||
|
},
|
||||||
|
// Thread 3 replies
|
||||||
|
{
|
||||||
|
id: 'r5',
|
||||||
|
content: 'I\'ve been calling her Mira since day one. Feels right.',
|
||||||
|
authorId: 'u7',
|
||||||
|
authorName: 'NeonCrawler',
|
||||||
|
threadId: 'th3',
|
||||||
|
createdAt: '2026-01-25T10:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'r6',
|
||||||
|
content: 'Hard disagree — the ambiguity is part of the point. She\'s every lost child. Naming her loses that.',
|
||||||
|
authorId: 'u8',
|
||||||
|
authorName: 'ByteWitch',
|
||||||
|
threadId: 'th3',
|
||||||
|
createdAt: '2026-01-25T11:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'r7',
|
||||||
|
content: 'Voting for Pip. Short, punchy, and weirdly adorable for someone causing this much chaos.',
|
||||||
|
authorId: 'u5',
|
||||||
|
authorName: 'XenoArch',
|
||||||
|
threadId: 'th3',
|
||||||
|
createdAt: '2026-01-26T08:15:00Z',
|
||||||
|
},
|
||||||
|
// Thread 5 replies
|
||||||
|
{
|
||||||
|
id: 'r8',
|
||||||
|
content: 'I noticed that too! The hair is definitely the same shade. And there\'s a memo on floor 3 signed with a heavily redacted name with only the initials visible...',
|
||||||
|
authorId: 'u3',
|
||||||
|
authorName: 'GlitchHunter',
|
||||||
|
threadId: 'th5',
|
||||||
|
createdAt: '2026-02-05T12:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'r9',
|
||||||
|
content: 'The developers confirmed on stream that the girl\'s backstory will be explored in a DLC. This theory might be onto something.',
|
||||||
|
authorId: 'u8',
|
||||||
|
authorName: 'ByteWitch',
|
||||||
|
threadId: 'th5',
|
||||||
|
createdAt: '2026-02-06T15:00:00Z',
|
||||||
|
},
|
||||||
|
// Thread 6 replies
|
||||||
|
{
|
||||||
|
id: 'r10',
|
||||||
|
content: 'We\'re aware of this crash on certain Nvidia configurations. A patch is being prepared. ETA: next week. Sorry for the inconvenience.',
|
||||||
|
authorId: 'u2',
|
||||||
|
authorName: 'Vesper',
|
||||||
|
threadId: 'th6',
|
||||||
|
createdAt: '2026-02-11T10:00:00Z',
|
||||||
|
},
|
||||||
|
// Thread 7 replies
|
||||||
|
{
|
||||||
|
id: 'r11',
|
||||||
|
content: 'Agreed 100%. The moment I saw the first CRT flicker I knew this was something special.',
|
||||||
|
authorId: 'u3',
|
||||||
|
authorName: 'GlitchHunter',
|
||||||
|
threadId: 'th7',
|
||||||
|
createdAt: '2026-02-12T10:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'r12',
|
||||||
|
content: 'Thanks for the kind words! The visual team worked incredibly hard on those effects. More surprises coming in future updates.',
|
||||||
|
authorId: 'u1',
|
||||||
|
authorName: 'Kestrel',
|
||||||
|
threadId: 'th7',
|
||||||
|
createdAt: '2026-02-13T09:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Mock Bug Reports ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const MOCK_BUG_NOTES: BugReportNote[] = [
|
||||||
|
{
|
||||||
|
id: 'n1',
|
||||||
|
bugReportId: 'bug1',
|
||||||
|
authorId: 'u1',
|
||||||
|
authorName: 'Kestrel',
|
||||||
|
content: 'Reproduced internally. Relates to the head collision hitbox on slopes. Assigning to ByteWitch for physics review.',
|
||||||
|
createdAt: '2026-01-16T09:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'n2',
|
||||||
|
bugReportId: 'bug3',
|
||||||
|
authorId: 'u8',
|
||||||
|
authorName: 'ByteWitch',
|
||||||
|
content: 'This is the DirectX 12 feature level issue. Nvidia driver 566.x introduced a regression. Workaround: force DX11 via launch options.',
|
||||||
|
createdAt: '2026-02-11T11:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MOCK_BUGS: BugReport[] = [
|
||||||
|
{
|
||||||
|
id: 'bug1',
|
||||||
|
uniqueCode: 'HH-0001',
|
||||||
|
title: 'Head clips through floor geometry on ramp sections',
|
||||||
|
description:
|
||||||
|
'When rolling the head down the maintenance ramp on floor 2, the head occasionally clips through the floor and falls into the void. This causes a soft reset but loses all checkpoint progress.',
|
||||||
|
stepsToReproduce:
|
||||||
|
'1. Reach Floor 2, section C\n2. Roll head down the maintenance ramp at full speed\n3. Head clips through at approximately 80% of the way down\n4. Game enters a permanent loading state',
|
||||||
|
severity: 'high',
|
||||||
|
gameVersion: '0.9.3-alpha',
|
||||||
|
status: 'in_progress',
|
||||||
|
submittedById: 'u3',
|
||||||
|
submittedByName: 'GlitchHunter',
|
||||||
|
assignedToId: 'u8',
|
||||||
|
assignedToName: 'ByteWitch',
|
||||||
|
createdAt: '2026-01-15T18:00:00Z',
|
||||||
|
updatedAt: '2026-01-16T09:00:00Z',
|
||||||
|
notes: [MOCK_BUG_NOTES[0]],
|
||||||
|
meTooBugs: ['u4', 'u5', 'u7'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bug2',
|
||||||
|
uniqueCode: 'HH-0002',
|
||||||
|
title: 'Audio desync in co-op mode after host migration',
|
||||||
|
description:
|
||||||
|
'When the host player disconnects and host migration occurs, all audio becomes desynced. Sound effects play 2-3 seconds after the triggering event.',
|
||||||
|
stepsToReproduce:
|
||||||
|
'1. Start a co-op session with 3+ players\n2. Have the host disconnect mid-game\n3. Continue playing after host migration completes\n4. Observe audio desync within 30 seconds',
|
||||||
|
severity: 'medium',
|
||||||
|
gameVersion: '0.9.3-alpha',
|
||||||
|
status: 'open',
|
||||||
|
submittedById: 'u5',
|
||||||
|
submittedByName: 'XenoArch',
|
||||||
|
createdAt: '2026-01-28T14:00:00Z',
|
||||||
|
updatedAt: '2026-01-28T14:00:00Z',
|
||||||
|
notes: [],
|
||||||
|
meTooBugs: ['u3'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bug3',
|
||||||
|
uniqueCode: 'HH-0003',
|
||||||
|
title: 'Game crashes on startup — DirectX 12 error',
|
||||||
|
description:
|
||||||
|
'Game fails to launch with DXGI_ERROR_DEVICE_REMOVED on RTX 4080 with specific Nvidia driver versions (566.x series).',
|
||||||
|
stepsToReproduce:
|
||||||
|
'1. Install Nvidia driver 566.03 or higher\n2. Attempt to launch Headless Hazard\n3. Game shows splash screen then crashes\n4. Windows Event Viewer shows DXGI_ERROR_DEVICE_REMOVED',
|
||||||
|
severity: 'critical',
|
||||||
|
gameVersion: '0.9.3-alpha',
|
||||||
|
status: 'in_progress',
|
||||||
|
submittedById: 'u3',
|
||||||
|
submittedByName: 'GlitchHunter',
|
||||||
|
assignedToId: 'u1',
|
||||||
|
assignedToName: 'Kestrel',
|
||||||
|
createdAt: '2026-02-10T14:20:00Z',
|
||||||
|
updatedAt: '2026-02-11T11:00:00Z',
|
||||||
|
notes: [MOCK_BUG_NOTES[1]],
|
||||||
|
meTooBugs: ['u4', 'u5', 'u7', 'u2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bug4',
|
||||||
|
uniqueCode: 'HH-0004',
|
||||||
|
title: 'Girl NPC gets stuck in T-pose near elevator door',
|
||||||
|
description:
|
||||||
|
'The girl character enters a T-pose animation state when the elevator door closes while she is within 1 meter of the door. She remains stuck until the player rolls the head near her.',
|
||||||
|
stepsToReproduce:
|
||||||
|
'1. Floor 1, elevator B\n2. Position the girl next to the closed elevator door\n3. Call the elevator\n4. As doors close, T-pose triggers',
|
||||||
|
severity: 'low',
|
||||||
|
gameVersion: '0.9.2-alpha',
|
||||||
|
status: 'resolved',
|
||||||
|
submittedById: 'u4',
|
||||||
|
submittedByName: 'NullPointer',
|
||||||
|
assignedToId: 'u8',
|
||||||
|
assignedToName: 'ByteWitch',
|
||||||
|
createdAt: '2025-12-20T10:00:00Z',
|
||||||
|
updatedAt: '2026-01-05T16:00:00Z',
|
||||||
|
notes: [],
|
||||||
|
meTooBugs: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bug5',
|
||||||
|
uniqueCode: 'HH-0005',
|
||||||
|
title: 'Save corruption when quitting during cutscene',
|
||||||
|
description:
|
||||||
|
'Alt+F4 or force-quitting during any cutscene corrupts the save file. The save file becomes unreadable and the game starts fresh on next launch.',
|
||||||
|
stepsToReproduce:
|
||||||
|
'1. Trigger any in-game cutscene\n2. While the cutscene is playing, alt+F4 the game\n3. Relaunch the game\n4. Game shows "Save file corrupted" and resets progress',
|
||||||
|
severity: 'critical',
|
||||||
|
gameVersion: '0.9.3-alpha',
|
||||||
|
status: 'open',
|
||||||
|
submittedById: 'u7',
|
||||||
|
submittedByName: 'NeonCrawler',
|
||||||
|
createdAt: '2026-02-14T19:30:00Z',
|
||||||
|
updatedAt: '2026-02-14T19:30:00Z',
|
||||||
|
notes: [],
|
||||||
|
meTooBugs: ['u3', 'u4', 'u8'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bug6',
|
||||||
|
uniqueCode: 'HH-0006',
|
||||||
|
title: 'Head-camera view flickers when standing near florescent lights',
|
||||||
|
description:
|
||||||
|
'Minor visual bug: when viewing through the head camera near certain florescent light fixtures, the camera feed flickers at approximately 60hz in an uncomfortable strobing pattern.',
|
||||||
|
stepsToReproduce:
|
||||||
|
'1. Floor 0 server room\n2. Position head under the overhead fluorescent tubes\n3. Open the head camera view\n4. Observe strobing flicker',
|
||||||
|
severity: 'low',
|
||||||
|
gameVersion: '0.9.3-alpha',
|
||||||
|
status: 'closed',
|
||||||
|
submittedById: 'u5',
|
||||||
|
submittedByName: 'XenoArch',
|
||||||
|
createdAt: '2026-01-10T11:00:00Z',
|
||||||
|
updatedAt: '2026-01-12T14:00:00Z',
|
||||||
|
notes: [],
|
||||||
|
meTooBugs: ['u4'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Mock Bug Comments ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const MOCK_BUG_COMMENTS: BugComment[] = [
|
||||||
|
{
|
||||||
|
id: 'bc1',
|
||||||
|
bugReportId: 'bug1',
|
||||||
|
authorId: 'u5',
|
||||||
|
authorName: 'XenoArch',
|
||||||
|
content: 'Can confirm this happens to me too. Specifically when the head reaches full speed before the curve at the bottom.',
|
||||||
|
createdAt: '2026-01-16T10:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bc2',
|
||||||
|
bugReportId: 'bug1',
|
||||||
|
authorId: 'u4',
|
||||||
|
authorName: 'NullPointer',
|
||||||
|
content: 'Workaround: slow down before the bend. Tap the brake key twice. Annoying but it prevents the clip.',
|
||||||
|
createdAt: '2026-01-17T08:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bc3',
|
||||||
|
bugReportId: 'bug3',
|
||||||
|
authorId: 'u4',
|
||||||
|
authorName: 'NullPointer',
|
||||||
|
content: 'Downgrading to driver 565.90 fixed it for me. Not ideal but at least I can play.',
|
||||||
|
createdAt: '2026-02-11T14:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bc4',
|
||||||
|
bugReportId: 'bug3',
|
||||||
|
authorId: 'u7',
|
||||||
|
authorName: 'NeonCrawler',
|
||||||
|
content: 'The DX11 workaround mentioned in the internal note also works. Add -dx11 to launch options in Steam.',
|
||||||
|
createdAt: '2026-02-12T09:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bc5',
|
||||||
|
bugReportId: 'bug5',
|
||||||
|
authorId: 'u3',
|
||||||
|
authorName: 'GlitchHunter',
|
||||||
|
content: 'This wiped my 6-hour playthrough. Please prioritize this fix.',
|
||||||
|
createdAt: '2026-02-14T20:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bc6',
|
||||||
|
bugReportId: 'bug5',
|
||||||
|
authorId: 'u4',
|
||||||
|
authorName: 'NullPointer',
|
||||||
|
content: 'Same here. Lost floor 4 and 5 progress. The auto-save system really needs to not write mid-cutscene.',
|
||||||
|
createdAt: '2026-02-15T11:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Mock Staff Feed ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const MOCK_STAFF_POSTS: StaffPost[] = [
|
||||||
|
{
|
||||||
|
id: 'sp1',
|
||||||
|
authorId: 'u1',
|
||||||
|
authorName: 'Kestrel',
|
||||||
|
authorRole: 'dev',
|
||||||
|
content: 'Physics patch is ready for internal testing. ByteWitch, can you check the head-ramp collision fix before EOD?',
|
||||||
|
createdAt: '2026-02-18T08:30:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sp2',
|
||||||
|
authorId: 'u2',
|
||||||
|
authorName: 'Vesper',
|
||||||
|
authorRole: 'com',
|
||||||
|
content: 'Social media post about the co-op update went live. Already 400 likes in 2 hours. The community is really excited.',
|
||||||
|
createdAt: '2026-02-18T09:15:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sp3',
|
||||||
|
authorId: 'u8',
|
||||||
|
authorName: 'ByteWitch',
|
||||||
|
authorRole: 'dev',
|
||||||
|
content: 'Checked the ramp fix — collision normals are now correct. Still seeing minor jitter at the bottom but nothing game-breaking. Marking as good to merge.',
|
||||||
|
createdAt: '2026-02-18T11:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sp4',
|
||||||
|
authorId: 'u1',
|
||||||
|
authorName: 'Kestrel',
|
||||||
|
authorRole: 'dev',
|
||||||
|
content: 'Merging physics fix. Will bundle with the DirectX hotfix into patch 0.9.4. Target: Monday release.',
|
||||||
|
createdAt: '2026-02-18T11:45:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sp5',
|
||||||
|
authorId: 'u2',
|
||||||
|
authorName: 'Vesper',
|
||||||
|
authorRole: 'com',
|
||||||
|
content: 'Reminder: community AMA is scheduled for Wednesday 7pm UTC. I\'ll be handling questions, feel free to DM me answers for anything technical.',
|
||||||
|
createdAt: '2026-02-17T16:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sp6',
|
||||||
|
authorId: 'u8',
|
||||||
|
authorName: 'ByteWitch',
|
||||||
|
authorRole: 'dev',
|
||||||
|
content: 'Save corruption bug (HH-0005) root cause identified: file handle wasn\'t being closed before force-quit. Simple fix, will be in next patch.',
|
||||||
|
createdAt: '2026-02-15T14:30:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Mock Events & Polls ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const MOCK_POLLS: Poll[] = [
|
||||||
|
{
|
||||||
|
id: 'poll1',
|
||||||
|
eventId: 'evt2',
|
||||||
|
question: 'Which feature should we prioritize for the next major update?',
|
||||||
|
options: [
|
||||||
|
{ id: 'opt1', text: 'New multiplayer maps', votes: 42, votedUserIds: ['u3', 'u4', 'u5', 'u7'] },
|
||||||
|
{ id: 'opt2', text: 'Co-op campaign mode', votes: 78, votedUserIds: ['u1', 'u2', 'u8'] },
|
||||||
|
{ id: 'opt3', text: 'Advanced physics system', votes: 35, votedUserIds: [] },
|
||||||
|
{ id: 'opt4', text: 'Character customization', votes: 51, votedUserIds: [] },
|
||||||
|
],
|
||||||
|
isActive: true,
|
||||||
|
endsAt: '2026-02-28T23:59:59Z',
|
||||||
|
allowMultipleVotes: false,
|
||||||
|
createdAt: '2026-02-16T10:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'poll2',
|
||||||
|
eventId: 'evt5',
|
||||||
|
question: 'What type of content would you like to see more of in our devlogs?',
|
||||||
|
options: [
|
||||||
|
{ id: 'opt5', text: 'Behind-the-scenes coding', votes: 23, votedUserIds: [] },
|
||||||
|
{ id: 'opt6', text: 'Art process & concept art', votes: 45, votedUserIds: [] },
|
||||||
|
{ id: 'opt7', text: 'Level design breakdown', votes: 18, votedUserIds: [] },
|
||||||
|
{ id: 'opt8', text: 'Bug fix explanations', votes: 12, votedUserIds: [] },
|
||||||
|
],
|
||||||
|
isActive: false,
|
||||||
|
endsAt: '2026-02-10T23:59:59Z',
|
||||||
|
allowMultipleVotes: true,
|
||||||
|
createdAt: '2026-02-01T09:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MOCK_EVENTS: EventPost[] = [
|
||||||
|
{
|
||||||
|
id: 'evt1',
|
||||||
|
type: 'milestone',
|
||||||
|
title: 'Version 0.9.5 Released!',
|
||||||
|
content: 'We are excited to announce version 0.9.5 is now live! This update includes major performance improvements, the new "Factory District" map, and over 30 bug fixes. Check the changelog for full details. Thank you to everyone who participated in testing!',
|
||||||
|
authorId: 'u1',
|
||||||
|
authorName: 'Kestrel',
|
||||||
|
authorRole: 'dev',
|
||||||
|
createdAt: '2026-02-17T14:00:00Z',
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'evt2',
|
||||||
|
type: 'poll',
|
||||||
|
title: 'Community Poll: Next Feature Priority',
|
||||||
|
content: 'Help us decide what to work on next! We want to hear from you about which feature would enhance your experience the most. Vote below and feel free to discuss in the forum.',
|
||||||
|
authorId: 'u2',
|
||||||
|
authorName: 'Vesper',
|
||||||
|
authorRole: 'com',
|
||||||
|
createdAt: '2026-02-16T10:00:00Z',
|
||||||
|
isPublic: true,
|
||||||
|
pollId: 'poll1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'evt3',
|
||||||
|
type: 'announcement',
|
||||||
|
title: 'Server Maintenance Scheduled',
|
||||||
|
content: 'We will be performing server maintenance on February 20th from 2:00 AM to 6:00 AM UTC. Multiplayer services will be unavailable during this time. Single-player mode will remain accessible. We apologize for any inconvenience!',
|
||||||
|
authorId: 'u8',
|
||||||
|
authorName: 'ByteWitch',
|
||||||
|
authorRole: 'dev',
|
||||||
|
createdAt: '2026-02-15T16:30:00Z',
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'evt4',
|
||||||
|
type: 'update',
|
||||||
|
title: 'Co-op Mode Development Progress',
|
||||||
|
content: 'Quick update on co-op mode development: networking code is 80% complete, and we\'ve successfully tested 4-player sessions internally. Still working on some sync issues with physics objects, but overall progress is excellent. Aiming for beta testing in March!',
|
||||||
|
authorId: 'u1',
|
||||||
|
authorName: 'Kestrel',
|
||||||
|
authorRole: 'dev',
|
||||||
|
createdAt: '2026-02-14T11:20:00Z',
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'evt5',
|
||||||
|
type: 'poll',
|
||||||
|
title: 'Devlog Content Poll',
|
||||||
|
content: 'We want to make our devlogs more interesting for you! Let us know what kind of behind-the-scenes content you\'d like to see more of. You can vote for multiple options.',
|
||||||
|
authorId: 'u2',
|
||||||
|
authorName: 'Vesper',
|
||||||
|
authorRole: 'com',
|
||||||
|
createdAt: '2026-02-01T09:00:00Z',
|
||||||
|
isPublic: true,
|
||||||
|
pollId: 'poll2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'evt6',
|
||||||
|
type: 'announcement',
|
||||||
|
title: 'Community AMA This Wednesday',
|
||||||
|
content: 'Join us for a live AMA (Ask Me Anything) session this Wednesday at 7:00 PM UTC! The dev team will be answering questions about the game, upcoming features, and the development process. Post your questions in the forum thread beforehand or ask live during the session.',
|
||||||
|
authorId: 'u2',
|
||||||
|
authorName: 'Vesper',
|
||||||
|
authorRole: 'com',
|
||||||
|
createdAt: '2026-02-12T14:00:00Z',
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'evt7',
|
||||||
|
type: 'update',
|
||||||
|
title: 'New Character Model Work in Progress',
|
||||||
|
content: 'Our art team has been working on updated character models with more detailed textures while maintaining the retro aesthetic. Early tests look fantastic! We\'ll share some screenshots next week. This won\'t affect performance - we\'re being very careful about optimization.',
|
||||||
|
authorId: 'u1',
|
||||||
|
authorName: 'Kestrel',
|
||||||
|
authorRole: 'dev',
|
||||||
|
createdAt: '2026-02-08T10:15:00Z',
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ── Team Members ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const TEAM_MEMBERS: TeamMember[] = [
|
||||||
|
{
|
||||||
|
id: 'tm1',
|
||||||
|
name: 'Alexei Voronov',
|
||||||
|
role: 'Studio Director & Lead Developer',
|
||||||
|
bio: 'Former AAA engine programmer turned indie. Obsessed with physics simulations and 80s science fiction. The brains behind the detached head mechanic.',
|
||||||
|
avatarInitials: 'AV',
|
||||||
|
social: { twitter: '@alexei_dev', github: 'alexei-v' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tm2',
|
||||||
|
name: 'Sadie Mercier',
|
||||||
|
role: 'Lead Artist & Art Director',
|
||||||
|
bio: 'Pixel art veteran and VHS enthusiast. Responsible for the retro-futuristic visual identity of Headless Hazard. Also makes incredible cheese.',
|
||||||
|
avatarInitials: 'SM',
|
||||||
|
social: { twitter: '@sadie_pixels' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tm3',
|
||||||
|
name: 'Rio Tanaka',
|
||||||
|
role: 'Game Designer & Narrative Lead',
|
||||||
|
bio: 'Wrote the full lore bible for the Headless Hazard universe, including 300 pages that will never see daylight. Loves bureaucratic dystopias.',
|
||||||
|
avatarInitials: 'RT',
|
||||||
|
social: { twitter: '@rio_writes', github: 'rio-tanaka' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tm4',
|
||||||
|
name: 'Misha Devereux',
|
||||||
|
role: 'Sound Designer & Composer',
|
||||||
|
bio: 'Creates audio using a mix of synthesizers, field recordings in abandoned factories, and heavily processed VHS tapes. The soundscape of HH is entirely his.',
|
||||||
|
avatarInitials: 'MD',
|
||||||
|
social: { twitter: '@misha_sounds' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tm5',
|
||||||
|
name: 'Priya Anand',
|
||||||
|
role: 'Backend & Infrastructure Engineer',
|
||||||
|
bio: 'Keeps the multiplayer servers alive and the databases sane. Dark mode absolutist. Currently obsessed with Rust.',
|
||||||
|
avatarInitials: 'PA',
|
||||||
|
social: { github: 'priya-anand-dev' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tm6',
|
||||||
|
name: 'Camille Dupont',
|
||||||
|
role: 'Community Manager & QA Lead',
|
||||||
|
bio: 'The bridge between the studio and the players. Has played through the full game 47 times for QA purposes and still finds it fun somehow.',
|
||||||
|
avatarInitials: 'CD',
|
||||||
|
social: { twitter: '@camille_crow' },
|
||||||
|
},
|
||||||
|
];
|
||||||
227
nest-intra/src/index.css
Normal file
227
nest-intra/src/index.css
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* ── Design Tokens — White Theme ─────────────────── */
|
||||||
|
:root {
|
||||||
|
--color-bg: #ffffff;
|
||||||
|
--color-bg-alt: #f8f9fa;
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-surface-alt: #f1f3f5;
|
||||||
|
--color-border: #dee2e6;
|
||||||
|
--color-border-dim: #e9ecef;
|
||||||
|
|
||||||
|
/* Primary accent: blue */
|
||||||
|
--color-yellow: #2563eb;
|
||||||
|
--color-yellow-dim: #1d4ed8;
|
||||||
|
|
||||||
|
/* Secondary accent: dark gray */
|
||||||
|
--color-cyan: #374151;
|
||||||
|
--color-cyan-dim: #4b5563;
|
||||||
|
|
||||||
|
/* Tertiary: purple */
|
||||||
|
--color-magenta: #7c3aed;
|
||||||
|
--color-magenta-dim: #6d28d9;
|
||||||
|
|
||||||
|
/* Red for errors/danger */
|
||||||
|
--color-red: #dc2626;
|
||||||
|
|
||||||
|
/* Text */
|
||||||
|
--color-text: #1f2937;
|
||||||
|
--color-text-dim: #4b5563;
|
||||||
|
--color-text-muted: #9ca3af;
|
||||||
|
|
||||||
|
/* Legacy aliases kept so existing pages compile without changes */
|
||||||
|
--color-green: #059669;
|
||||||
|
--color-green-dim: #047857;
|
||||||
|
--color-amber: #d97706;
|
||||||
|
--color-amber-dim: #b45309;
|
||||||
|
|
||||||
|
/* Blue alias */
|
||||||
|
--color-blue: #2563eb;
|
||||||
|
|
||||||
|
--font-mono: 'JetBrains Mono', 'Courier New', monospace;
|
||||||
|
--font-heading: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Base ──────────────────────────────────────────────── */
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background-color: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 17px;
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow-x: hidden;
|
||||||
|
line-height: 1.7;
|
||||||
|
display: block;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Scrollbar ─────────────────────────────────────────── */
|
||||||
|
::-webkit-scrollbar { width: 8px; }
|
||||||
|
::-webkit-scrollbar-track { background: var(--color-bg-alt); }
|
||||||
|
::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: 4px; }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: var(--color-cyan); }
|
||||||
|
|
||||||
|
/* ── Typography ────────────────────────────────────────── */
|
||||||
|
h1, h2, h3, h4, h5 {
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-yellow);
|
||||||
|
text-decoration: underline;
|
||||||
|
transition: color 0.1s;
|
||||||
|
}
|
||||||
|
a:hover { color: var(--color-yellow-dim); }
|
||||||
|
|
||||||
|
/* ── Content box ─────────────────────────────────────── */
|
||||||
|
.crt-box {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
background: var(--color-surface);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Buttons — clean white theme style ───────────────── */
|
||||||
|
.btn-terminal {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
background: var(--color-bg);
|
||||||
|
border: 2px solid var(--color-yellow);
|
||||||
|
color: var(--color-yellow);
|
||||||
|
padding: 0.6rem 1.3rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.btn-terminal::before { display: none; }
|
||||||
|
.btn-terminal:hover {
|
||||||
|
background: var(--color-yellow);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-amber {
|
||||||
|
border-color: var(--color-amber);
|
||||||
|
color: var(--color-amber);
|
||||||
|
}
|
||||||
|
.btn-amber:hover {
|
||||||
|
background: var(--color-amber);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
border-color: var(--color-red);
|
||||||
|
color: var(--color-red);
|
||||||
|
}
|
||||||
|
.btn-danger:hover {
|
||||||
|
background: var(--color-red);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Form inputs — clean style ──────────────────── */
|
||||||
|
.input-terminal {
|
||||||
|
background: var(--color-bg);
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
padding: 0.6rem 0.9rem;
|
||||||
|
width: 100%;
|
||||||
|
transition: border-color 0.15s, box-shadow 0.15s;
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
.input-terminal::placeholder {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
.input-terminal:focus {
|
||||||
|
border-color: var(--color-yellow);
|
||||||
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
.input-terminal.error {
|
||||||
|
border-color: var(--color-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Section label ─────────────────────────────────────── */
|
||||||
|
.section-label {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Page transition ───────────────────────────────────── */
|
||||||
|
@keyframes page-enter {
|
||||||
|
from { opacity: 0; transform: translateY(6px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
.page-enter {
|
||||||
|
animation: page-enter 0.2s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Status badges — clean style ──────────────────────── */
|
||||||
|
.badge {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
.badge-open { background: #d1fae5; color: #065f46; border-color: #059669; }
|
||||||
|
.badge-progress { background: #fef3c7; color: #92400e; border-color: #d97706; }
|
||||||
|
.badge-resolved { background: #dbeafe; color: #1e40af; border-color: #2563eb; }
|
||||||
|
.badge-closed { background: #f3f4f6; color: #6b7280; border-color: #9ca3af; }
|
||||||
|
.badge-critical { background: #fee2e2; color: #991b1b; border-color: #dc2626; }
|
||||||
|
.badge-high { background: #fed7aa; color: #9a3412; border-color: #ea580c; }
|
||||||
|
.badge-medium { background: #fef3c7; color: #92400e; border-color: #d97706; }
|
||||||
|
.badge-low { background: #e0e7ff; color: #3730a3; border-color: #6366f1; }
|
||||||
|
|
||||||
|
/* ── Blink cursor ──────────────────────────────────────── */
|
||||||
|
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
||||||
|
.cursor-blink::after {
|
||||||
|
content: '_';
|
||||||
|
animation: blink 1s step-end infinite;
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Horizontal rule ───────────────────────────── */
|
||||||
|
.minitel-rule {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
16
nest-intra/src/main.tsx
Normal file
16
nest-intra/src/main.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App.tsx';
|
||||||
|
|
||||||
|
const rootElement = document.getElementById('root');
|
||||||
|
if (!rootElement) throw new Error('Root element not found');
|
||||||
|
|
||||||
|
createRoot(rootElement).render(
|
||||||
|
<StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
224
nest-intra/src/pages/LoginPage.tsx
Normal file
224
nest-intra/src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { DEMO_CREDENTIALS } from '../data/mockData';
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const { login, isAuthenticated } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Redirect if already logged in
|
||||||
|
const from = (location.state as { from?: { pathname: string } })?.from?.pathname || '/intranet';
|
||||||
|
|
||||||
|
if (isAuthenticated) {
|
||||||
|
navigate(from, { replace: true });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
if (!email.trim()) {
|
||||||
|
setError('Email is required.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const result = await login(email.trim(), password);
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
navigate(from, { replace: true });
|
||||||
|
} else {
|
||||||
|
setError(result.error || 'Login failed.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[email, password, login, navigate, from]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleQuickLogin = useCallback(
|
||||||
|
async (credEmail: string) => {
|
||||||
|
setEmail(credEmail);
|
||||||
|
setError('');
|
||||||
|
setLoading(true);
|
||||||
|
const result = await login(credEmail, 'demo');
|
||||||
|
setLoading(false);
|
||||||
|
if (result.success) {
|
||||||
|
navigate(from, { replace: true });
|
||||||
|
} else {
|
||||||
|
setError(result.error || 'Login failed.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[login, navigate, from]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: 'var(--color-bg)',
|
||||||
|
padding: '2rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ width: '100%', maxWidth: '400px' }}>
|
||||||
|
{/* Header */}
|
||||||
|
<div style={{ textAlign: 'center', marginBottom: '2.5rem' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontFamily: 'var(--font-heading)',
|
||||||
|
color: 'var(--color-amber)',
|
||||||
|
fontSize: '1.6rem',
|
||||||
|
letterSpacing: '0.08em',
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
INTRANET
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
color: 'var(--color-text-muted)',
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
letterSpacing: '0.15em',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
CROWMATE STUDIO — STAFF ACCESS
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Login form */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: 'var(--color-surface)',
|
||||||
|
border: '1px solid var(--color-border)',
|
||||||
|
padding: '2rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div style={{ marginBottom: '1.25rem' }}>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
color: 'var(--color-text-muted)',
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
marginBottom: '0.4rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
EMAIL
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
className={`input-terminal${error ? ' error' : ''}`}
|
||||||
|
placeholder="staff@crowmate.dev"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => {
|
||||||
|
setEmail(e.target.value);
|
||||||
|
setError('');
|
||||||
|
}}
|
||||||
|
disabled={loading}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '1.25rem' }}>
|
||||||
|
<label
|
||||||
|
style={{
|
||||||
|
display: 'block',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontSize: '0.7rem',
|
||||||
|
color: 'var(--color-text-muted)',
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
marginBottom: '0.4rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
PASSWORD
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className="input-terminal"
|
||||||
|
placeholder="Enter password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
color: 'var(--color-red)',
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
marginBottom: '1rem',
|
||||||
|
padding: '0.5rem 0.75rem',
|
||||||
|
background: 'rgba(220,38,38,0.06)',
|
||||||
|
border: '1px solid rgba(220,38,38,0.2)',
|
||||||
|
borderRadius: '4px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn-terminal btn-amber"
|
||||||
|
disabled={loading}
|
||||||
|
style={{ width: '100%', justifyContent: 'center', opacity: loading ? 0.6 : 1 }}
|
||||||
|
>
|
||||||
|
{loading ? 'Authenticating...' : '> Login'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick login */}
|
||||||
|
<div style={{ marginTop: '1.5rem' }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontFamily: 'var(--font-mono)',
|
||||||
|
color: 'var(--color-text-muted)',
|
||||||
|
fontSize: '0.65rem',
|
||||||
|
letterSpacing: '0.1em',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: '0.75rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
QUICK LOGIN (DEMO)
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'center' }}>
|
||||||
|
<button
|
||||||
|
className="btn-terminal"
|
||||||
|
style={{ padding: '0.35rem 0.8rem', fontSize: '0.7rem' }}
|
||||||
|
onClick={() => handleQuickLogin(DEMO_CREDENTIALS.admin)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Admin (Kestrel)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn-terminal"
|
||||||
|
style={{ padding: '0.35rem 0.8rem', fontSize: '0.7rem' }}
|
||||||
|
onClick={() => handleQuickLogin(DEMO_CREDENTIALS.staff)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Staff (Vesper)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
197
nest-intra/src/types/index.ts
Normal file
197
nest-intra/src/types/index.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
// ── User & Auth ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type UserRole = 'user' | 'dev' | 'com';
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
role: UserRole;
|
||||||
|
isAdmin: boolean;
|
||||||
|
isBanned: boolean;
|
||||||
|
createdAt: string; // ISO 8601
|
||||||
|
avatarUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthState {
|
||||||
|
user: User | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Forum ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface ForumCategory {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
threadCount: number;
|
||||||
|
lastActivity?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ForumThread {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
authorId: string;
|
||||||
|
authorName: string;
|
||||||
|
categoryId: string;
|
||||||
|
categoryName: string;
|
||||||
|
content: string;
|
||||||
|
isPinned: boolean;
|
||||||
|
isLocked: boolean;
|
||||||
|
replyCount: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
lastReplyAuthor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ForumReply {
|
||||||
|
id: string;
|
||||||
|
content: string;
|
||||||
|
authorId: string;
|
||||||
|
authorName: string;
|
||||||
|
threadId: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bug Reports ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type BugSeverity = 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
export type BugStatus = 'open' | 'in_progress' | 'resolved' | 'closed';
|
||||||
|
|
||||||
|
export interface BugReport {
|
||||||
|
id: string;
|
||||||
|
uniqueCode: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
stepsToReproduce: string;
|
||||||
|
severity: BugSeverity;
|
||||||
|
gameVersion: string;
|
||||||
|
screenshotUrl?: string;
|
||||||
|
status: BugStatus;
|
||||||
|
submittedById: string;
|
||||||
|
submittedByName: string;
|
||||||
|
assignedToId?: string;
|
||||||
|
assignedToName?: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
notes?: BugReportNote[];
|
||||||
|
/** IDs of users who clicked "I have this too" */
|
||||||
|
meTooBugs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BugComment {
|
||||||
|
id: string;
|
||||||
|
bugReportId: string;
|
||||||
|
authorId: string;
|
||||||
|
authorName: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BugReportNote {
|
||||||
|
id: string;
|
||||||
|
bugReportId: string;
|
||||||
|
authorId: string;
|
||||||
|
authorName: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BugReportFormData {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
stepsToReproduce: string;
|
||||||
|
severity: BugSeverity;
|
||||||
|
gameVersion: string;
|
||||||
|
screenshotUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Staff Feed ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface StaffPost {
|
||||||
|
id: string;
|
||||||
|
authorId: string;
|
||||||
|
authorName: string;
|
||||||
|
authorRole: UserRole;
|
||||||
|
content: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Events & Polls ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type EventType = 'announcement' | 'update' | 'milestone' | 'poll';
|
||||||
|
|
||||||
|
export interface EventPost {
|
||||||
|
id: string;
|
||||||
|
type: EventType;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
authorId: string;
|
||||||
|
authorName: string;
|
||||||
|
authorRole: UserRole;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
isPublic: boolean; // whether visible to community
|
||||||
|
pollId?: string; // reference to poll if type is 'poll'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PollOption {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
votes: number;
|
||||||
|
votedUserIds: string[]; // track who voted for this option
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Poll {
|
||||||
|
id: string;
|
||||||
|
eventId: string;
|
||||||
|
question: string;
|
||||||
|
options: PollOption[];
|
||||||
|
isActive: boolean;
|
||||||
|
endsAt?: string; // ISO 8601
|
||||||
|
allowMultipleVotes: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Team / Studio ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface TeamMember {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
bio?: string;
|
||||||
|
avatarInitials: string;
|
||||||
|
social?: {
|
||||||
|
twitter?: string;
|
||||||
|
github?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Forms ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface LoginFormData {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterFormData {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangePasswordFormData {
|
||||||
|
currentPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Filters ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface BugFilters {
|
||||||
|
status: BugStatus | 'all';
|
||||||
|
severity: BugSeverity | 'all';
|
||||||
|
assignedTo: string | 'all';
|
||||||
|
}
|
||||||
47
nest-intra/src/utils/format.ts
Normal file
47
nest-intra/src/utils/format.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Format an ISO 8601 date string to a human-readable date.
|
||||||
|
*/
|
||||||
|
export function formatDate(iso: string): string {
|
||||||
|
return new Date(iso).toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an ISO 8601 date string to a human-readable datetime.
|
||||||
|
*/
|
||||||
|
export function formatDateTime(iso: string): string {
|
||||||
|
return new Date(iso).toLocaleString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a relative time string (e.g. "3 days ago").
|
||||||
|
*/
|
||||||
|
export function timeAgo(iso: string): string {
|
||||||
|
const diff = Date.now() - new Date(iso).getTime();
|
||||||
|
const seconds = Math.floor(diff / 1000);
|
||||||
|
if (seconds < 60) return 'just now';
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
if (minutes < 60) return `${minutes}m ago`;
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
if (hours < 24) return `${hours}h ago`;
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
if (days < 30) return `${days}d ago`;
|
||||||
|
return formatDate(iso);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate a string to maxLength and append ellipsis.
|
||||||
|
*/
|
||||||
|
export function truncate(str: string, maxLength: number): string {
|
||||||
|
if (str.length <= maxLength) return str;
|
||||||
|
return str.slice(0, maxLength).trimEnd() + '...';
|
||||||
|
}
|
||||||
28
nest-intra/tsconfig.app.json
Normal file
28
nest-intra/tsconfig.app.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
7
nest-intra/tsconfig.json
Normal file
7
nest-intra/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
26
nest-intra/tsconfig.node.json
Normal file
26
nest-intra/tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
13
nest-intra/vite.config.ts
Normal file
13
nest-intra/vite.config.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
tailwindcss(),
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
port: 5174,
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user