first commit

This commit is contained in:
2025-11-26 18:57:18 +03:00
commit 16c21a4e49
41 changed files with 1075 additions and 0 deletions

4
frontend/.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
dist
npm-debug.log
.env

1
frontend/.env.example Normal file
View File

@@ -0,0 +1 @@
VITE_API_URL=http://localhost:4000

12
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json .
RUN npm install
COPY tsconfig.json tsconfig.node.json vite.config.ts tailwind.config.js postcss.config.js index.html .
COPY src ./src
EXPOSE 5173
CMD ["npm", "run", "dev"]

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Wisecolt Starter</title>
</head>
<body class="bg-background text-foreground">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

35
frontend/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "frontend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host --port 5173",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-slot": "^1.0.2",
"axios": "^1.5.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-react": "^0.292.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.0",
"socket.io-client": "^4.7.2",
"sonner": "^1.4.0",
"tailwind-merge": "^1.14.0"
},
"devDependencies": {
"@types/node": "^20.9.0",
"@types/react": "^18.2.28",
"@types/react-dom": "^18.2.12",
"@vitejs/plugin-react": "^4.1.1",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2",
"vite": "^4.4.9"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

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

@@ -0,0 +1,18 @@
import { Navigate, Route, Routes } from "react-router-dom";
import { LoginPage } from "./pages/LoginPage";
import { AdminPage } from "./pages/AdminPage";
import { ProtectedRoute } from "./components/ProtectedRoute";
function App() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route element={<ProtectedRoute />}>
<Route path="/admin" element={<AdminPage />} />
</Route>
<Route path="*" element={<Navigate to="/admin" replace />} />
</Routes>
);
}
export default App;

View File

@@ -0,0 +1,27 @@
import axios from "axios";
const apiBaseURL = import.meta.env.VITE_API_URL || "http://localhost:4000";
export const apiClient = axios.create({
baseURL: apiBaseURL
});
export function setAuthToken(token?: string) {
if (token) {
apiClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
localStorage.setItem("token", token);
} else {
delete apiClient.defaults.headers.common["Authorization"];
localStorage.removeItem("token");
}
}
export async function loginRequest(username: string, password: string) {
const { data } = await apiClient.post("/auth/login", { username, password });
return data as { token: string; username: string };
}
export async function fetchMe() {
const { data } = await apiClient.get("/auth/me");
return data as { username: string };
}

View File

@@ -0,0 +1,21 @@
import React from "react";
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../providers/auth-provider";
export function ProtectedRoute() {
const { token, loading } = useAuth();
if (loading) {
return (
<div className="flex h-screen items-center justify-center text-sm text-muted-foreground">
Yükleniyor...
</div>
);
}
if (!token) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
}

View File

@@ -0,0 +1,13 @@
import { Moon, Sun } from "lucide-react";
import { Button } from "./ui/button";
import { useTheme } from "../providers/theme-provider";
export function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<Button variant="outline" size="icon" aria-label="Tema değiştir" onClick={toggleTheme}>
{theme === "light" ? <Moon className="h-5 w-5" /> : <Sun className="h-5 w-5" />}
</Button>
);
}

View File

@@ -0,0 +1,51 @@
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline"
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10"
}
},
defaultVariants: {
variant: "default",
size: "default"
}
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = "Button";
export { Button, buttonVariants };

View File

@@ -0,0 +1,51 @@
import * as React from "react";
import { cn } from "../../lib/utils";
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
{...props}
/>
)
);
Card.displayName = "Card";
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
)
);
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn("text-2xl font-semibold leading-none tracking-tight", className)} {...props} />
)
);
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
)
);
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
)
);
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import { cn } from "../../lib/utils";
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
);
});
Input.displayName = "Input";
export { Input };

View File

@@ -0,0 +1,16 @@
import * as React from "react";
import { cn } from "../../lib/utils";
const Label = React.forwardRef<
HTMLLabelElement,
React.LabelHTMLAttributes<HTMLLabelElement>
>(({ className, ...props }, ref) => (
<label
ref={ref}
className={cn("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", className)}
{...props}
/>
));
Label.displayName = "Label";
export { Label };

View File

@@ -0,0 +1,24 @@
import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { cn } from "../../lib/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-10 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className="pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };

View File

@@ -0,0 +1,16 @@
import { Toaster as SonnerToaster } from "sonner";
export function Toaster() {
return (
<SonnerToaster
position="top-right"
toastOptions={{
style: {
background: "hsl(var(--card))",
color: "hsl(var(--card-foreground))",
border: "1px solid hsl(var(--border))"
}
}}
/>
);
}

57
frontend/src/index.css Normal file
View File

@@ -0,0 +1,57 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: 0 0% 100%;
--foreground: 0 0% 4%;
--card: 0 0% 100%;
--card-foreground: 0 0% 4%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 4%;
--primary: 0 0% 4%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 4%;
--radius: 0.5rem;
}
.dark {
--background: 0 0% 5%;
--foreground: 0 0% 95%;
--card: 0 0% 9%;
--card-foreground: 0 0% 95%;
--popover: 0 0% 9%;
--popover-foreground: 0 0% 95%;
--primary: 0 0% 95%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--radius: 0.5rem;
}
body {
@apply bg-background text-foreground antialiased;
}
.card-shadow {
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.04);
}

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

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

@@ -0,0 +1,21 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import "./index.css";
import { ThemeProvider } from "./providers/theme-provider";
import { AuthProvider } from "./providers/auth-provider";
import { Toaster } from "./components/ui/toaster";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ThemeProvider>
<AuthProvider>
<BrowserRouter>
<App />
<Toaster />
</BrowserRouter>
</AuthProvider>
</ThemeProvider>
</React.StrictMode>
);

View File

@@ -0,0 +1,113 @@
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { io, Socket } from "socket.io-client";
import { toast } from "sonner";
import { Button } from "../components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
import { ThemeToggle } from "../components/ThemeToggle";
import { useAuth } from "../providers/auth-provider";
import { apiClient } from "../api/client";
export function AdminPage() {
const { user, token, logout } = useAuth();
const navigate = useNavigate();
const [socket, setSocket] = useState<Socket | null>(null);
const [messages, setMessages] = useState<string[]>([]);
const socketUrl = useMemo(() => apiClient.defaults.baseURL || window.location.origin, []);
useEffect(() => {
if (!token) return;
const newSocket = io(socketUrl, {
auth: { token },
transports: ["websocket", "polling"]
});
newSocket.on("connect", () => setMessages((prev) => [...prev, "Socket bağlandı"]));
newSocket.on("hello", (msg: string) => setMessages((prev) => [...prev, msg]));
newSocket.on("pong", (msg: string) => setMessages((prev) => [...prev, msg]));
newSocket.on("connect_error", (err) => {
setMessages((prev) => [...prev, `Hata: ${err.message}`]);
toast.error("Socket bağlantısı başarısız");
});
setSocket(newSocket);
return () => {
newSocket.disconnect();
};
}, [token, socketUrl]);
const handleLogout = () => {
logout();
navigate("/login", { replace: true });
};
const sendPing = () => {
if (!socket) return;
socket.emit("ping");
};
return (
<div className="min-h-screen bg-background text-foreground">
<header className="flex items-center justify-between border-b border-border px-6 py-4">
<div className="flex items-center gap-3">
<div className="text-lg font-semibold">Yönetim Paneli</div>
{user?.username && <span className="text-sm text-muted-foreground">Hoş geldin, {user.username}</span>}
</div>
<div className="flex items-center gap-3">
<ThemeToggle />
<Button variant="outline" onClick={handleLogout}>
Çıkış Yap
</Button>
</div>
</header>
<main className="mx-auto grid max-w-4xl gap-6 px-6 py-10 md:grid-cols-2">
<Card className="border-border card-shadow">
<CardHeader>
<CardTitle>Bağlantı</CardTitle>
<CardDescription>Backend'e bağlanıp basit ping/pong testi yapın.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Button onClick={sendPing} disabled={!socket}>
Ping Gönder
</Button>
<div className="rounded-md border border-border bg-muted/40 p-3 text-sm text-muted-foreground">
<div className="mb-2 text-xs uppercase tracking-wide text-foreground">Socket Mesajları</div>
<div className="space-y-1">
{messages.length === 0 && <div>Mesaj yok.</div>}
{messages.map((msg, idx) => (
<div key={idx} className="text-foreground/80">
{msg}
</div>
))}
</div>
</div>
</CardContent>
</Card>
<Card className="border-border card-shadow">
<CardHeader>
<CardTitle>Durum</CardTitle>
<CardDescription>Auth & bağlantı bilgileri.</CardDescription>
</CardHeader>
<CardContent className="space-y-3 text-sm text-muted-foreground">
<div className="flex items-center justify-between">
<span>API URL</span>
<span className="font-medium text-foreground">{socketUrl}</span>
</div>
<div className="flex items-center justify-between">
<span>Kullanıcı</span>
<span className="font-medium text-foreground">{user?.username ?? "-"}</span>
</div>
<div className="flex items-center justify-between">
<span>Token</span>
<span className="truncate font-mono text-foreground/80">{token?.slice(0, 24) ?? "-"}...</span>
</div>
</CardContent>
</Card>
</main>
</div>
);
}

View File

@@ -0,0 +1,72 @@
import { FormEvent, useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
import { useAuth } from "../providers/auth-provider";
import { ThemeToggle } from "../components/ThemeToggle";
export function LoginPage() {
const { login } = useAuth();
const navigate = useNavigate();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setLoading(true);
try {
await login(username, password);
navigate("/admin", { replace: true });
} catch (err) {
toast.error("Giriş başarısız. Bilgileri kontrol edin.");
} finally {
setLoading(false);
}
};
return (
<div className="flex min-h-screen items-center justify-center bg-background px-4">
<div className="absolute right-6 top-6">
<ThemeToggle />
</div>
<Card className="w-full max-w-md border-border card-shadow">
<CardHeader>
<CardTitle>Yönetici Girişi</CardTitle>
<CardDescription>Panel erişimi için bilgilerinizi girin.</CardDescription>
</CardHeader>
<CardContent>
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2">
<Label htmlFor="username">Kullanıcı adı</Label>
<Input
id="username"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="admin"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Şifre</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? "Giriş yapılıyor..." : "Giriş Yap"}
</Button>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,58 @@
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
import { fetchMe, loginRequest, setAuthToken } from "../api/client";
interface AuthContextProps {
user: { username: string } | null;
token: string | null;
login: (username: string, password: string) => Promise<void>;
logout: () => void;
loading: boolean;
}
const AuthContext = createContext<AuthContextProps | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [user, setUser] = useState<{ username: string } | null>(null);
const [token, setToken] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const stored = localStorage.getItem("token");
if (stored) {
setAuthToken(stored);
setToken(stored);
fetchMe()
.then((data) => setUser({ username: data.username }))
.catch(() => setAuthToken(undefined))
.finally(() => setLoading(false));
} else {
setLoading(false);
}
}, []);
const login = async (username: string, password: string) => {
const { token: newToken, username: returnedUsername } = await loginRequest(username, password);
setAuthToken(newToken);
setToken(newToken);
setUser({ username: returnedUsername });
};
const logout = () => {
setAuthToken(undefined);
setToken(null);
setUser(null);
};
const value = useMemo(
() => ({ user, token, login, logout, loading }),
[user, token, loading]
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth AuthProvider içinde kullanılmalı");
return ctx;
}

View File

@@ -0,0 +1,37 @@
import React, { createContext, useContext, useEffect, useState } from "react";
type Theme = "light" | "dark";
interface ThemeContextProps {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);
const storageKey = "theme";
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<Theme>(() => {
const stored = localStorage.getItem(storageKey) as Theme | null;
if (stored === "light" || stored === "dark") return stored;
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
});
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove(theme === "light" ? "dark" : "light");
root.classList.add(theme);
localStorage.setItem(storageKey, theme);
}, [theme]);
const toggleTheme = () => setTheme((prev) => (prev === "light" ? "dark" : "light"));
return <ThemeContext.Provider value={{ theme, toggleTheme }}>{children}</ThemeContext.Provider>;
};
export const useTheme = () => {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme ThemeProvider içinde kullanılmalı");
return ctx;
};

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

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

View File

@@ -0,0 +1,53 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: "class",
content: [
"./index.html",
"./src/**/*.{ts,tsx}"
],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))"
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))"
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))"
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))"
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))"
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))"
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))"
}
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)"
}
}
},
plugins: []
};

8
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.node.json",
"compilerOptions": {
"jsx": "react-jsx",
"types": ["vite/client"]
},
"include": ["src"]
}

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"noEmit": true,
"lib": ["ES2020", "DOM"],
"types": ["node"]
},
"include": ["vite.config.ts"]
}

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,
host: true
}
});