try this shi
This commit is contained in:
23
frontend/Dockerfile
Normal file
23
frontend/Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM node:20-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:1.27-alpine
|
||||
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
17
frontend/index.html
Normal file
17
frontend/index.html
Normal file
@@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/minecraft-icon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
|
||||
<title>Minecraft Server Dashboard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
25
frontend/nginx.conf
Normal file
25
frontend/nginx.conf
Normal file
@@ -0,0 +1,25 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://mc-dashboard-backend:3001;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
}
|
||||
|
||||
27
frontend/package.json
Normal file
27
frontend/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "mc-dashboard-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"axios": "^1.6.0",
|
||||
"lucide-react": "^0.294.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"postcss": "^8.4.32",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"vite": "^5.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
7
frontend/postcss.config.js
Normal file
7
frontend/postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
9
frontend/public/minecraft-icon.svg
Normal file
9
frontend/public/minecraft-icon.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<rect width="100" height="100" fill="#8B4513"/>
|
||||
<rect x="10" y="10" width="30" height="30" fill="#654321"/>
|
||||
<rect x="60" y="10" width="30" height="30" fill="#654321"/>
|
||||
<rect x="10" y="60" width="30" height="30" fill="#654321"/>
|
||||
<rect x="60" y="60" width="30" height="30" fill="#654321"/>
|
||||
<rect x="35" y="35" width="30" height="30" fill="#A0522D"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 431 B |
2
frontend/src/App.css
Normal file
2
frontend/src/App.css
Normal file
@@ -0,0 +1,2 @@
|
||||
/* Additional custom styles if needed */
|
||||
|
||||
234
frontend/src/App.jsx
Normal file
234
frontend/src/App.jsx
Normal file
@@ -0,0 +1,234 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
Play,
|
||||
Square,
|
||||
Users,
|
||||
Activity,
|
||||
Server,
|
||||
Clock,
|
||||
Zap,
|
||||
RefreshCw
|
||||
} from 'lucide-react'
|
||||
import './App.css'
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL || '/api'
|
||||
|
||||
function App() {
|
||||
const [serverStatus, setServerStatus] = useState(null)
|
||||
const [containerStatus, setContainerStatus] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [actionLoading, setActionLoading] = useState(false)
|
||||
const [lastUpdate, setLastUpdate] = useState(null)
|
||||
|
||||
const fetchStatus = async () => {
|
||||
try {
|
||||
const [serverRes, containerRes] = await Promise.all([
|
||||
axios.get(`${API_URL}/status`),
|
||||
axios.get(`${API_URL}/container/status`)
|
||||
])
|
||||
setServerStatus(serverRes.data)
|
||||
setContainerStatus(containerRes.data)
|
||||
setLastUpdate(new Date())
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
console.error('Error fetching status:', error)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatus()
|
||||
const interval = setInterval(fetchStatus, 5000) // Update every 5 seconds
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const handleStart = async () => {
|
||||
setActionLoading(true)
|
||||
try {
|
||||
await axios.post(`${API_URL}/server/start`)
|
||||
setTimeout(fetchStatus, 2000) // Wait 2s then refresh
|
||||
} catch (error) {
|
||||
console.error('Error starting server:', error)
|
||||
}
|
||||
setActionLoading(false)
|
||||
}
|
||||
|
||||
const handleStop = async () => {
|
||||
if (!window.confirm('Are you sure you want to stop the server?')) {
|
||||
return
|
||||
}
|
||||
setActionLoading(true)
|
||||
try {
|
||||
await axios.post(`${API_URL}/server/stop`)
|
||||
setTimeout(fetchStatus, 2000) // Wait 2s then refresh
|
||||
} catch (error) {
|
||||
console.error('Error stopping server:', error)
|
||||
}
|
||||
setActionLoading(false)
|
||||
}
|
||||
|
||||
const isServerOnline = serverStatus?.online && containerStatus?.running
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-white text-2xl flex items-center gap-3">
|
||||
<RefreshCw className="animate-spin" size={32} />
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen p-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-5xl font-bold text-white mb-4 drop-shadow-lg">
|
||||
⛏️ Minecraft Server Dashboard
|
||||
</h1>
|
||||
<p className="text-white/80 text-lg">
|
||||
Monitor and control your Minecraft server
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Status Overview Card */}
|
||||
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 mb-8 border-4 border-white/20">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-4 h-4 rounded-full ${isServerOnline ? 'bg-green-400 pulse-glow' : 'bg-red-400'}`}></div>
|
||||
<h2 className="text-3xl font-bold text-white">
|
||||
{isServerOnline ? 'Server Online' : 'Server Offline'}
|
||||
</h2>
|
||||
</div>
|
||||
{lastUpdate && (
|
||||
<div className="text-white/60 text-sm flex items-center gap-2">
|
||||
<Clock size={16} />
|
||||
Updated {lastUpdate.toLocaleTimeString()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Control Buttons */}
|
||||
<div className="flex gap-4 mb-8">
|
||||
<button
|
||||
onClick={handleStart}
|
||||
disabled={actionLoading || (containerStatus?.running && isServerOnline)}
|
||||
className="minecraft-button flex-1 bg-green-500 hover:bg-green-600 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-bold py-4 px-8 rounded-lg flex items-center justify-center gap-3 text-lg"
|
||||
>
|
||||
<Play size={24} />
|
||||
Start Server
|
||||
</button>
|
||||
<button
|
||||
onClick={handleStop}
|
||||
disabled={actionLoading || !containerStatus?.running}
|
||||
className="minecraft-button flex-1 bg-red-500 hover:bg-red-600 disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-bold py-4 px-8 rounded-lg flex items-center justify-center gap-3 text-lg"
|
||||
>
|
||||
<Square size={24} />
|
||||
Stop Server
|
||||
</button>
|
||||
<button
|
||||
onClick={fetchStatus}
|
||||
disabled={actionLoading}
|
||||
className="minecraft-button bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white font-bold py-4 px-8 rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<RefreshCw size={24} className={actionLoading ? 'animate-spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stats Grid */}
|
||||
{isServerOnline && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{/* Players Online */}
|
||||
<div className="bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl p-6 text-white">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Users size={24} />
|
||||
<h3 className="font-semibold text-lg">Players</h3>
|
||||
</div>
|
||||
<p className="text-4xl font-bold">
|
||||
{serverStatus.players.online} / {serverStatus.players.max}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Server Version */}
|
||||
<div className="bg-gradient-to-br from-purple-500 to-purple-600 rounded-xl p-6 text-white">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Server size={24} />
|
||||
<h3 className="font-semibold text-lg">Version</h3>
|
||||
</div>
|
||||
<p className="text-2xl font-bold truncate">
|
||||
{serverStatus.version}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Latency */}
|
||||
<div className="bg-gradient-to-br from-green-500 to-green-600 rounded-xl p-6 text-white">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Activity size={24} />
|
||||
<h3 className="font-semibold text-lg">Latency</h3>
|
||||
</div>
|
||||
<p className="text-4xl font-bold">
|
||||
{serverStatus.latency}ms
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* TPS */}
|
||||
<div className="bg-gradient-to-br from-yellow-500 to-yellow-600 rounded-xl p-6 text-white">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Zap size={24} />
|
||||
<h3 className="font-semibold text-lg">TPS</h3>
|
||||
</div>
|
||||
<p className="text-4xl font-bold">
|
||||
{serverStatus.tps || '20.0'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Player List */}
|
||||
{isServerOnline && serverStatus.players.online > 0 && (
|
||||
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 border-4 border-white/20">
|
||||
<h2 className="text-2xl font-bold text-white mb-4 flex items-center gap-3">
|
||||
<Users size={28} />
|
||||
Online Players
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{serverStatus.players.list.map((player, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white/20 rounded-lg p-4 text-white font-semibold flex items-center gap-2"
|
||||
>
|
||||
<div className="w-8 h-8 bg-gradient-to-br from-green-400 to-blue-500 rounded-md flex items-center justify-center text-xs">
|
||||
{player[0].toUpperCase()}
|
||||
</div>
|
||||
{player}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* MOTD */}
|
||||
{isServerOnline && serverStatus.motd && (
|
||||
<div className="bg-white/10 backdrop-blur-lg rounded-2xl p-8 mt-8 border-4 border-white/20">
|
||||
<h2 className="text-2xl font-bold text-white mb-4">Message of the Day</h2>
|
||||
<p className="text-white/90 text-lg font-mono">
|
||||
{serverStatus.motd}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-center mt-12 text-white/60">
|
||||
<p>Made with ❤️ for Minecraft</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
56
frontend/src/index.css
Normal file
56
frontend/src/index.css
Normal file
@@ -0,0 +1,56 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.pixel-border {
|
||||
box-shadow:
|
||||
0 -4px 0 0 rgba(0,0,0,0.3),
|
||||
0 4px 0 0 rgba(255,255,255,0.3),
|
||||
-4px 0 0 0 rgba(0,0,0,0.3),
|
||||
4px 0 0 0 rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.minecraft-button {
|
||||
position: relative;
|
||||
transition: all 0.1s;
|
||||
box-shadow:
|
||||
0 4px 0 0 rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.minecraft-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 6px 0 0 rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.minecraft-button:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow:
|
||||
0 2px 0 0 rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.pulse-glow {
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 20px rgba(93, 205, 227, 0.5);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 40px rgba(93, 205, 227, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
11
frontend/src/main.jsx
Normal file
11
frontend/src/main.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
||||
27
frontend/tailwind.config.js
Normal file
27
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
minecraft: {
|
||||
grass: '#7cbd56',
|
||||
dirt: '#8b5a2b',
|
||||
stone: '#7a7a7a',
|
||||
diamond: '#5dcde3',
|
||||
emerald: '#50c878',
|
||||
gold: '#fcba03',
|
||||
redstone: '#dc143c',
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
minecraft: ['"Press Start 2P"', 'cursive'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
17
frontend/vite.config.js
Normal file
17
frontend/vite.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: process.env.VITE_API_URL || 'http://localhost:3001',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user