feat: Initialize frontend and backend structure with essential configurations
Some checks failed
Backend CI / lint-and-test (push) Failing after 2m15s
Frontend CI / lint-and-build (push) Successful in 1m1s

- Added TypeScript build info for frontend.
- Created Vite configuration for React application.
- Implemented pre-commit hook to run checks before commits.
- Set up PostgreSQL Dockerfile with PostGIS support and initialization scripts.
- Added database creation script for PostgreSQL with necessary extensions.
- Established Python project configuration with dependencies and development tools.
- Developed pre-commit script to enforce code quality checks for backend and frontend.
- Created PowerShell script to set up Git hooks path.
This commit is contained in:
2025-10-11 15:25:32 +02:00
commit fc1e874309
74 changed files with 9477 additions and 0 deletions

45
frontend/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,45 @@
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true
},
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: "latest",
sourceType: "module"
},
settings: {
react: {
version: "detect"
}
},
plugins: ["react", "@typescript-eslint", "react-hooks", "jsx-a11y", "import"],
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:jsx-a11y/recommended",
"plugin:import/typescript",
"prettier"
],
rules: {
"react/react-in-jsx-scope": "off",
"import/order": [
"warn",
{
"groups": [["builtin", "external"], "internal", ["parent", "sibling", "index"]],
"newlines-between": "always",
"alphabetize": {
"order": "asc",
"caseInsensitive": true
}
}
]
}
};

View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"singleQuote": true,
"semi": true,
"trailingComma": "es5",
"tabWidth": 2,
"printWidth": 88
}

15
frontend/index.html Normal file
View File

@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rail Game</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

5861
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
frontend/package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "rail-game-frontend",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc --noEmit && tsc --project tsconfig.node.json --noEmit && vite build",
"preview": "vite preview",
"lint": "eslint \"src/**/*.{ts,tsx}\"",
"format": "prettier --write \"src/**/*.{ts,tsx,css}\""
},
"dependencies": {
"leaflet": "^1.9.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-leaflet": "^4.2.1"
},
"devDependencies": {
"@types/node": "^20.16.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/leaflet": "^1.9.13",
"@typescript-eslint/eslint-plugin": "^8.6.0",
"@typescript-eslint/parser": "^8.6.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"prettier": "^3.3.3",
"typescript": "^5.5.3",
"vite": "^5.4.0"
}
}

88
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,88 @@
import './styles/global.css';
import { LoginForm } from './components/auth/LoginForm';
import { NetworkMap } from './components/map/NetworkMap';
import { useNetworkSnapshot } from './hooks/useNetworkSnapshot';
import { useAuth } from './state/AuthContext';
function App(): JSX.Element {
const { token, user, status: authStatus, logout } = useAuth();
const isAuthenticated = authStatus === 'authenticated' && token !== null;
const { data, status, error } = useNetworkSnapshot(isAuthenticated ? token : null);
return (
<div className="app-shell">
<header className="app-header">
<div>
<h1>Rail Game</h1>
<p>Build and manage a railway network using real-world map data.</p>
</div>
{isAuthenticated && (
<div className="auth-meta">
<span>Signed in as {user?.fullName ?? user?.username}</span>
<button type="button" onClick={() => logout()} className="ghost-button">
Sign out
</button>
</div>
)}
</header>
<main className="app-main">
{!isAuthenticated ? (
<section className="card">
<LoginForm />
</section>
) : (
<section className="card">
<h2>Network Snapshot</h2>
{status === 'loading' && <p>Loading network data</p>}
{status === 'error' && <p className="error-text">{error}</p>}
{status === 'success' && data && (
<div className="snapshot-layout">
<div className="map-wrapper">
<NetworkMap snapshot={data} />
</div>
<div className="grid">
<div>
<h3>Stations</h3>
<ul>
{data.stations.map((station) => (
<li key={station.id}>
{station.name} ({station.latitude.toFixed(3)},{' '}
{station.longitude.toFixed(3)})
</li>
))}
</ul>
</div>
<div>
<h3>Trains</h3>
<ul>
{data.trains.map((train) => (
<li key={train.id}>
{train.designation} · {train.capacity} capacity ·{' '}
{train.maxSpeedKph} km/h
</li>
))}
</ul>
</div>
<div>
<h3>Tracks</h3>
<ul>
{data.tracks.map((track) => (
<li key={track.id}>
{track.startStationId} {track.endStationId} ·{' '}
{(track.lengthMeters / 1000).toFixed(1)} km
</li>
))}
</ul>
</div>
</div>
</div>
)}
</section>
)}
</main>
</div>
);
}
export default App;

View File

@@ -0,0 +1,98 @@
import { FormEvent, useState } from 'react';
import { useAuth } from '../../state/AuthContext';
export function LoginForm(): JSX.Element {
const { login, register, logout, status, error } = useAuth();
const [mode, setMode] = useState<'login' | 'register'>('login');
const [username, setUsername] = useState('demo');
const [password, setPassword] = useState('railgame123');
const [fullName, setFullName] = useState('');
const isSubmitting = status === 'authenticating';
const isRegisterMode = mode === 'register';
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (isRegisterMode) {
await register(username, password, fullName || undefined);
} else {
await login(username, password);
}
}
function toggleMode() {
setMode((current) => (current === 'login' ? 'register' : 'login'));
setUsername('');
setPassword('');
setFullName('');
logout();
}
return (
<form className="auth-card" onSubmit={handleSubmit}>
<div className="auth-card__heading">
<h2>{isRegisterMode ? 'Create an account' : 'Sign in'}</h2>
<button
type="button"
className="link-button"
onClick={toggleMode}
disabled={isSubmitting}
>
{isRegisterMode ? 'Have an account? Sign in' : 'Need an account? Register'}
</button>
</div>
<p className="auth-helper">
{isRegisterMode
? 'Choose a username and password to create a new profile.'
: 'Use the demo credentials to explore the prototype.'}
</p>
{isRegisterMode && (
<>
<label htmlFor="fullName">Full name</label>
<input
id="fullName"
autoComplete="name"
value={fullName}
onChange={(event) => setFullName(event.target.value)}
disabled={isSubmitting}
/>
</>
)}
<label htmlFor="username">Username</label>
<input
id="username"
autoComplete="username"
value={username}
onChange={(event) => setUsername(event.target.value)}
disabled={isSubmitting}
required
/>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
autoComplete={isRegisterMode ? 'new-password' : 'current-password'}
value={password}
onChange={(event) => setPassword(event.target.value)}
disabled={isSubmitting}
required
/>
{error && <p className="error-text">{error}</p>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting
? isRegisterMode
? 'Creating account…'
: 'Signing in…'
: isRegisterMode
? 'Register'
: 'Sign in'}
</button>
</form>
);
}

View File

@@ -0,0 +1,102 @@
import type { LatLngBoundsExpression, LatLngExpression } from 'leaflet';
import { useMemo } from 'react';
import { CircleMarker, MapContainer, Polyline, TileLayer, Tooltip } from 'react-leaflet';
import type { NetworkSnapshot } from '../../services/api';
import 'leaflet/dist/leaflet.css';
interface NetworkMapProps {
readonly snapshot: NetworkSnapshot;
}
interface StationPosition {
readonly id: string;
readonly name: string;
readonly position: LatLngExpression;
}
const DEFAULT_CENTER: LatLngExpression = [51.505, -0.09];
export function NetworkMap({ snapshot }: NetworkMapProps): JSX.Element {
const stationPositions = useMemo<StationPosition[]>(() => {
return snapshot.stations.map((station) => ({
id: station.id,
name: station.name,
position: [station.latitude, station.longitude] as LatLngExpression,
}));
}, [snapshot.stations]);
const stationLookup = useMemo(() => {
const lookup = new Map<string, LatLngExpression>();
for (const station of stationPositions) {
lookup.set(station.id, station.position);
}
return lookup;
}, [stationPositions]);
const trackSegments = useMemo(() => {
return snapshot.tracks
.map((track) => {
const start = stationLookup.get(track.startStationId);
const end = stationLookup.get(track.endStationId);
if (!start || !end) {
return null;
}
return [start, end] as LatLngExpression[];
})
.filter((segment): segment is LatLngExpression[] => segment !== null);
}, [snapshot.tracks, stationLookup]);
const bounds = useMemo(() => {
if (stationPositions.length === 0) {
return null;
}
let minLat = Infinity;
let maxLat = -Infinity;
let minLng = Infinity;
let maxLng = -Infinity;
for (const { position } of stationPositions) {
const [lat, lng] = position as [number, number];
minLat = Math.min(minLat, lat);
maxLat = Math.max(maxLat, lat);
minLng = Math.min(minLng, lng);
maxLng = Math.max(maxLng, lng);
}
const padding = 0.02;
return [
[minLat - padding, minLng - padding],
[maxLat + padding, maxLng + padding],
] as LatLngBoundsExpression;
}, [stationPositions]);
return (
<MapContainer
className="network-map"
center={bounds ? undefined : DEFAULT_CENTER}
bounds={bounds ?? undefined}
scrollWheelZoom
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{trackSegments.map((segment, index) => (
<Polyline key={`track-${index}`} positions={segment} pathOptions={{ color: '#38bdf8', weight: 4 }} />
))}
{stationPositions.map((station) => (
<CircleMarker
key={station.id}
center={station.position}
radius={6}
pathOptions={{ color: '#f97316', fillColor: '#fed7aa', fillOpacity: 0.9 }}
>
<Tooltip direction="top" offset={[0, -8]}>{station.name}</Tooltip>
</CircleMarker>
))}
</MapContainer>
);
}

View File

@@ -0,0 +1,56 @@
import { useEffect, useState } from 'react';
import type { NetworkSnapshot } from '../services/api';
import { ApiError, fetchNetworkSnapshot } from '../services/api';
import { useAuth } from '../state/AuthContext';
interface NetworkSnapshotState {
readonly data: NetworkSnapshot | null;
readonly status: 'idle' | 'loading' | 'success' | 'error';
readonly error: string | null;
}
const INITIAL_STATE: NetworkSnapshotState = {
data: null,
status: 'idle',
error: null,
};
export function useNetworkSnapshot(token: string | null): NetworkSnapshotState {
const [state, setState] = useState<NetworkSnapshotState>(INITIAL_STATE);
const { logout } = useAuth();
useEffect(() => {
const abortController = new AbortController();
if (!token) {
setState(INITIAL_STATE);
return () => abortController.abort();
}
const authToken = token;
async function load() {
setState({ data: null, status: 'loading', error: null });
try {
const snapshot = await fetchNetworkSnapshot(authToken, abortController.signal);
setState({ data: snapshot, status: 'success', error: null });
} catch (error) {
if (abortController.signal.aborted) {
return;
}
if (error instanceof ApiError && error.status === 401) {
logout(error.message);
setState({ data: null, status: 'error', error: error.message });
return;
}
setState({ data: null, status: 'error', error: (error as Error).message });
}
}
load();
return () => abortController.abort();
}, [token, logout]);
return state;
}

20
frontend/src/main.tsx Normal file
View File

@@ -0,0 +1,20 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './styles/global.css';
import { AuthProvider } from './state/AuthContext';
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error('Failed to find the root element');
}
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</React.StrictMode>
);

View File

@@ -0,0 +1,84 @@
import type { AuthResponse, LoginPayload, RegisterPayload } from '../types/auth';
import type { Station, Track, Train } from '../types/domain';
export class ApiError extends Error {
readonly status: number;
constructor(message: string, status: number) {
super(message);
this.name = 'ApiError';
this.status = status;
}
}
export interface NetworkSnapshot {
readonly stations: Station[];
readonly tracks: Track[];
readonly trains: Train[];
}
const JSON_HEADERS = {
accept: 'application/json',
'content-type': 'application/json',
};
export async function login(credentials: LoginPayload): Promise<AuthResponse> {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify(credentials),
});
if (!response.ok) {
throw new ApiError('Invalid username or password', response.status);
}
return (await response.json()) as AuthResponse;
}
export async function register(credentials: RegisterPayload): Promise<AuthResponse> {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: JSON_HEADERS,
body: JSON.stringify(credentials),
});
if (!response.ok) {
const message =
response.status === 409
? 'Username already exists'
: 'Registration failed. Please try again.';
throw new ApiError(message, response.status);
}
return (await response.json()) as AuthResponse;
}
export async function fetchNetworkSnapshot(
token: string,
signal?: AbortSignal
): Promise<NetworkSnapshot> {
const response = await fetch('/api/network', {
method: 'GET',
headers: {
accept: 'application/json',
Authorization: `Bearer ${token}`,
},
signal,
});
if (!response.ok) {
const message =
response.status === 401
? 'Authorization required. Please sign in again.'
: `Failed to fetch network snapshot (status ${response.status})`;
throw new ApiError(message, response.status);
}
const data = (await response.json()) as NetworkSnapshot;
return {
stations: data.stations ?? [],
tracks: data.tracks ?? [],
trains: data.trains ?? [],
};
}

View File

@@ -0,0 +1,149 @@
import {
createContext,
type ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { login as loginRequest, register as registerRequest } from '../services/api';
import type { AuthenticatedUser } from '../types/auth';
const STORAGE_TOKEN_KEY = 'rail-game/auth/token';
const STORAGE_USER_KEY = 'rail-game/auth/user';
type AuthStatus = 'unauthenticated' | 'authenticating' | 'authenticated';
interface AuthContextValue {
readonly token: string | null;
readonly user: AuthenticatedUser | null;
readonly status: AuthStatus;
readonly error: string | null;
login: (username: string, password: string) => Promise<boolean>;
register: (
username: string,
password: string,
fullName?: string | null
) => Promise<boolean>;
logout: (reason?: string) => void;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
function readFromStorage<T>(key: string): T | null {
if (typeof window === 'undefined') {
return null;
}
const raw = window.localStorage.getItem(key);
if (!raw) {
return null;
}
try {
return JSON.parse(raw) as T;
} catch {
return null;
}
}
function writeToStorage(key: string, value: unknown): void {
if (typeof window === 'undefined') {
return;
}
window.localStorage.setItem(key, JSON.stringify(value));
}
function removeFromStorage(key: string): void {
if (typeof window === 'undefined') {
return;
}
window.localStorage.removeItem(key);
}
interface AuthProviderProps {
readonly children: ReactNode;
}
export function AuthProvider({ children }: AuthProviderProps): JSX.Element {
const [token, setToken] = useState<string | null>(null);
const [user, setUser] = useState<AuthenticatedUser | null>(null);
const [status, setStatus] = useState<AuthStatus>('unauthenticated');
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const storedToken = readFromStorage<string>(STORAGE_TOKEN_KEY);
const storedUser = readFromStorage<AuthenticatedUser>(STORAGE_USER_KEY);
if (storedToken && storedUser) {
setToken(storedToken);
setUser(storedUser);
setStatus('authenticated');
}
}, []);
const logout = useCallback((reason?: string) => {
setToken(null);
setUser(null);
setStatus('unauthenticated');
setError(reason ?? null);
removeFromStorage(STORAGE_TOKEN_KEY);
removeFromStorage(STORAGE_USER_KEY);
}, []);
const login = useCallback(async (username: string, password: string) => {
setStatus('authenticating');
setError(null);
try {
const response = await loginRequest({ username, password });
setToken(response.accessToken);
setUser(response.user);
setStatus('authenticated');
writeToStorage(STORAGE_TOKEN_KEY, response.accessToken);
writeToStorage(STORAGE_USER_KEY, response.user);
return true;
} catch (err) {
setStatus('unauthenticated');
const message = err instanceof Error ? err.message : 'Authentication failed';
setError(message);
return false;
}
}, []);
const register = useCallback(
async (username: string, password: string, fullName?: string | null) => {
setStatus('authenticating');
setError(null);
try {
const response = await registerRequest({ username, password, fullName });
setToken(response.accessToken);
setUser(response.user);
setStatus('authenticated');
writeToStorage(STORAGE_TOKEN_KEY, response.accessToken);
writeToStorage(STORAGE_USER_KEY, response.user);
return true;
} catch (err) {
setStatus('unauthenticated');
const message = err instanceof Error ? err.message : 'Registration failed';
setError(message);
return false;
}
},
[]
);
const value = useMemo<AuthContextValue>(
() => ({ token, user, status, error, login, register, logout }),
[token, user, status, error, login, register, logout]
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}

View File

@@ -0,0 +1,235 @@
:root {
font-family:
'Inter',
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
line-height: 1.5;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
margin: 0;
min-height: 100vh;
background: radial-gradient(circle at top, #1e293b, #0f172a 55%, #020617 100%);
}
.app-shell {
display: flex;
flex-direction: column;
min-height: 100vh;
align-items: center;
padding: 3rem 1.5rem;
gap: 2rem;
}
.app-header {
text-align: center;
max-width: 720px;
}
.app-header h1 {
font-size: 3rem;
font-weight: 700;
margin-bottom: 1rem;
}
.app-header p {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.72);
}
.app-main {
width: min(900px, 100%);
}
.card {
background: rgba(15, 23, 42, 0.85);
border: 1px solid rgba(148, 163, 184, 0.2);
border-radius: 16px;
padding: 2rem;
box-shadow: 0 35px 60px -15px rgba(15, 23, 42, 0.65);
}
.card h2 {
font-size: 1.6rem;
margin-bottom: 0.75rem;
}
.card p {
color: rgba(226, 232, 240, 0.85);
font-size: 1rem;
}
.grid {
display: grid;
gap: 1.5rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
margin-top: 1.5rem;
}
.grid ul {
list-style: none;
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0;
}
.grid h3 {
margin-bottom: 0.5rem;
font-size: 1.1rem;
}
.snapshot-layout {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.map-wrapper {
height: 360px;
border-radius: 14px;
overflow: hidden;
border: 1px solid rgba(148, 163, 184, 0.25);
}
.network-map {
height: 100%;
width: 100%;
}
@media (min-width: 768px) {
.snapshot-layout {
gap: 2rem;
}
.map-wrapper {
height: 420px;
}
}
.error-text {
color: #f87171;
font-weight: 600;
}
.app-header {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
text-align: center;
}
.app-header h1 {
margin-bottom: 0.5rem;
}
.auth-meta {
display: flex;
flex-direction: column;
gap: 0.75rem;
font-size: 0.95rem;
color: rgba(226, 232, 240, 0.9);
}
@media (min-width: 640px) {
.app-header {
flex-direction: row;
justify-content: space-between;
text-align: left;
}
.auth-meta {
align-items: flex-end;
}
}
.auth-card {
display: grid;
gap: 1rem;
}
.auth-card__heading {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.auth-helper {
font-size: 0.95rem;
color: rgba(226, 232, 240, 0.75);
}
.auth-card input {
padding: 0.75rem 0.85rem;
border-radius: 10px;
border: 1px solid rgba(148, 163, 184, 0.35);
background: rgba(15, 23, 42, 0.7);
color: rgba(226, 232, 240, 0.95);
}
.auth-card button,
.ghost-button {
cursor: pointer;
border: none;
border-radius: 999px;
padding: 0.75rem 1.5rem;
font-weight: 600;
transition:
background-color 0.2s ease,
transform 0.2s ease;
}
.auth-card button {
background: linear-gradient(135deg, #38bdf8, #818cf8);
color: #0f172a;
}
.auth-card button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.auth-card button:not(:disabled):hover {
transform: translateY(-1px);
}
.ghost-button {
background: transparent;
color: rgba(226, 232, 240, 0.85);
border: 1px solid rgba(148, 163, 184, 0.35);
padding: 0.35rem 1.1rem;
}
.ghost-button:hover {
background: rgba(148, 163, 184, 0.15);
}
.link-button {
background: none;
border: none;
color: #93c5fd;
cursor: pointer;
font-weight: 600;
text-decoration: underline;
padding: 0;
}
.link-button:disabled {
color: rgba(148, 163, 184, 0.6);
cursor: not-allowed;
text-decoration: none;
}

View File

@@ -0,0 +1,19 @@
export interface LoginPayload {
readonly username: string;
readonly password: string;
}
export interface RegisterPayload extends LoginPayload {
readonly fullName?: string | null;
}
export interface AuthenticatedUser {
readonly username: string;
readonly fullName?: string | null;
}
export interface AuthResponse {
readonly accessToken: string;
readonly tokenType: string;
readonly user: AuthenticatedUser;
}

View File

@@ -0,0 +1,30 @@
export interface Timestamped {
readonly createdAt: string;
readonly updatedAt: string;
}
export interface Identified extends Timestamped {
readonly id: string;
}
export interface Station extends Identified {
readonly name: string;
readonly latitude: number;
readonly longitude: number;
}
export interface Track extends Identified {
readonly startStationId: string;
readonly endStationId: string;
readonly lengthMeters: number;
readonly maxSpeedKph: number;
}
export interface Train extends Identified {
readonly designation: string;
readonly capacity: number;
readonly maxSpeedKph: number;
readonly operatingTrackIds: readonly string[];
}
export type NetworkEntity = Station | Track | Train;

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

21
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"types": ["vite/client"]
},
"include": ["src"]
}

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"composite": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"lib": ["ESNext"],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"resolveJsonModule": true,
"isolatedModules": true,
"types": ["node"]
},
"include": ["vite.config.ts"]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/hooks/usenetworksnapshot.ts","./src/services/api.ts","./src/types/domain.ts"],"errors":true,"version":"5.9.3"}

10
frontend/vite.config.ts Normal file
View File

@@ -0,0 +1,10 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
open: true
}
});