feat: initialize Multi-PHP server project with Electron, React, and service management infrastructure
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import { app, shell, BrowserWindow, Tray, Menu, nativeImage } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||
import { registerIpcHandlers } from './ipc'
|
||||
|
||||
let tray: Tray | null = null
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
|
||||
function createTray(): void {
|
||||
// Use a simple colored pixel if no icon is available for now
|
||||
const icon = nativeImage.createFromPath(join(__dirname, '../../resources/icon.png'))
|
||||
tray = new Tray(icon.isEmpty() ? nativeImage.createEmpty() : icon)
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: 'Trunçgil Multi-PHP', enabled: false },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Göster/Gizle', click: () => {
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isVisible()) mainWindow.hide()
|
||||
else mainWindow.show()
|
||||
}
|
||||
}
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ label: 'Başlat (Tüm Servisler)', click: () => { /* TODO: Start services */ } },
|
||||
{ label: 'Durdur (Tüm Servisler)', click: () => { /* TODO: Stop services */ } },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Çıkış', click: () => {
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
tray.setToolTip('Trunçgil Multi-PHP Server')
|
||||
tray.setContextMenu(contextMenu)
|
||||
|
||||
tray.on('double-click', () => {
|
||||
if (mainWindow) mainWindow.show()
|
||||
})
|
||||
}
|
||||
|
||||
function createWindow(): void {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 750,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
if (mainWindow) mainWindow.show()
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
electronApp.setAppUserModelId('com.electron')
|
||||
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
|
||||
createWindow()
|
||||
createTray()
|
||||
registerIpcHandlers()
|
||||
|
||||
app.on('activate', function () {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
// Overriding default behavior: stay in tray if all windows closed
|
||||
// unless on certain platforms where we really want to quit.
|
||||
// For this server app, we often want it to stay alive in Tray.
|
||||
if (process.platform === 'darwin') {
|
||||
// macOS behavior
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,72 @@
|
||||
import { ipcMain, app } from 'electron'
|
||||
import { processManager } from '../services/ProcessManager'
|
||||
import { configService } from '../services/ConfigService'
|
||||
import { downloadService } from '../services/DownloadService'
|
||||
import path from 'path'
|
||||
|
||||
export function registerIpcHandlers(): void {
|
||||
// Service management
|
||||
ipcMain.handle('services:start', async (_event, serviceName: string) => {
|
||||
try {
|
||||
if (serviceName === 'nginx') {
|
||||
const rootPath = path.join(app.getAppPath(), 'www')
|
||||
const configPath = await configService.generateConfig('nginx.conf.template', 'nginx.conf', {
|
||||
PORT: '80',
|
||||
ROOT: rootPath
|
||||
})
|
||||
const binPath = downloadService.getBinPath('nginx', 'nginx.exe')
|
||||
const success = await processManager.startService('nginx', binPath, ['-c', configPath])
|
||||
return { success, message: success ? 'Nginx başlatıldı.' : 'Nginx başlatılamadı.' }
|
||||
}
|
||||
|
||||
if (serviceName === 'php') {
|
||||
const configPath = await configService.generateConfig('php.ini.template', 'php.ini', {})
|
||||
const binPath = downloadService.getBinPath('php8.2', 'php-cgi.exe')
|
||||
const success = await processManager.startService('php', binPath, ['-b', '127.0.0.1:9001', '-c', configPath])
|
||||
return { success, message: success ? 'PHP başlatıldı.' : 'PHP başlatılamadı.' }
|
||||
}
|
||||
|
||||
if (serviceName === 'mysql') {
|
||||
const dataDir = path.join(app.getPath('userData'), 'mysql_data')
|
||||
const configPath = await configService.generateConfig('my.ini.template', 'my.ini', {
|
||||
DATADIR: dataDir,
|
||||
SOCKET: '/tmp/mysql.sock'
|
||||
})
|
||||
const binPath = downloadService.getBinPath('mysql', 'bin/mysqld.exe')
|
||||
const success = await processManager.startService('mysql', binPath, ['--defaults-file=' + configPath])
|
||||
return { success, message: success ? 'MySQL başlatıldı.' : 'MySQL başlatılamadı.' }
|
||||
}
|
||||
|
||||
return { success: false, message: 'Bilinmeyen servis.' }
|
||||
} catch (error: any) {
|
||||
console.error('Service start error:', error)
|
||||
return { success: false, message: `Hata: ${error.message}` }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('services:stop', async (_event, serviceName: string) => {
|
||||
const success = await processManager.stopService(serviceName)
|
||||
return { success, message: success ? `${serviceName} durduruldu.` : `${serviceName} zaten durmuş.` }
|
||||
})
|
||||
|
||||
ipcMain.handle('services:status', async () => {
|
||||
return processManager.getStatuses()
|
||||
})
|
||||
|
||||
// Binary management
|
||||
ipcMain.handle('binaries:download', async (_event, { name, url }) => {
|
||||
return downloadService.downloadAndExtract(url, name, (p) => {
|
||||
// TODO: Emit progress to renderer via webContents.send
|
||||
console.log(`Download progress for ${name}: ${p}%`)
|
||||
})
|
||||
})
|
||||
|
||||
// Config management
|
||||
ipcMain.handle('config:get', async (_event, key: string) => {
|
||||
return null
|
||||
})
|
||||
|
||||
ipcMain.handle('config:save', async (_event, config: any) => {
|
||||
return { success: true }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { app } from 'electron'
|
||||
|
||||
export class ConfigService {
|
||||
private configDir: string
|
||||
private templateDir: string
|
||||
|
||||
constructor() {
|
||||
this.templateDir = path.join(app.getAppPath(), 'config')
|
||||
this.configDir = path.join(app.getPath('userData'), 'generated_configs')
|
||||
|
||||
if (!fs.existsSync(this.configDir)) {
|
||||
fs.mkdirSync(this.configDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
async generateConfig(templateName: string, targetName: string, variables: Record<string, string>): Promise<string> {
|
||||
const templatePath = path.join(this.templateDir, templateName)
|
||||
const targetPath = path.join(this.configDir, targetName)
|
||||
|
||||
if (!fs.existsSync(templatePath)) {
|
||||
throw new Error(`Template not found: ${templatePath}`)
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(templatePath, 'utf-8')
|
||||
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
const escapedValue = value.replace(/\\/g, '/') // Use forward slashes for cross-platform compatibility in configs
|
||||
content = content.replace(new RegExp(`{{${key}}}`, 'g'), escapedValue)
|
||||
}
|
||||
|
||||
fs.writeFileSync(targetPath, content)
|
||||
return targetPath
|
||||
}
|
||||
|
||||
getGeneratedPath(targetName: string): string {
|
||||
return path.join(this.configDir, targetName)
|
||||
}
|
||||
}
|
||||
|
||||
export const configService = new ConfigService()
|
||||
@@ -0,0 +1,63 @@
|
||||
import axios from 'axios'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { app } from 'electron'
|
||||
import * as decompress from 'decompress'
|
||||
|
||||
export class DownloadService {
|
||||
private binDir: string
|
||||
|
||||
constructor() {
|
||||
this.binDir = path.join(app.getAppPath(), 'bin')
|
||||
if (!fs.existsSync(this.binDir)) {
|
||||
fs.mkdirSync(this.binDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
async downloadAndExtract(url: string, targetName: string, onProgress?: (p: number) => void): Promise<string> {
|
||||
const tempFile = path.join(app.getPath('temp'), `${targetName}.zip`)
|
||||
const targetDir = path.join(this.binDir, targetName)
|
||||
|
||||
if (fs.existsSync(targetDir)) {
|
||||
return targetDir
|
||||
}
|
||||
|
||||
const writer = fs.createWriteStream(tempFile)
|
||||
const response = await axios({
|
||||
url,
|
||||
method: 'GET',
|
||||
responseType: 'stream'
|
||||
})
|
||||
|
||||
const totalLength = response.headers['content-length']
|
||||
let downloadedLength = 0
|
||||
|
||||
response.data.on('data', (chunk: any) => {
|
||||
downloadedLength += chunk.length
|
||||
if (onProgress && totalLength) {
|
||||
onProgress(Math.round((downloadedLength / totalLength) * 100))
|
||||
}
|
||||
})
|
||||
|
||||
response.data.pipe(writer)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', async () => {
|
||||
try {
|
||||
await decompress(tempFile, targetDir)
|
||||
fs.unlinkSync(tempFile)
|
||||
resolve(targetDir)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
writer.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
getBinPath(service: string, executable: string): string {
|
||||
return path.join(this.binDir, service, executable)
|
||||
}
|
||||
}
|
||||
|
||||
export const downloadService = new DownloadService()
|
||||
@@ -0,0 +1,43 @@
|
||||
import { exec } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
export interface OSOperations {
|
||||
addHostEntry(ip: string, host: string): Promise<boolean>
|
||||
isElevated(): Promise<boolean>
|
||||
runElevated(command: string): Promise<{ stdout: string, stderr: string }>
|
||||
}
|
||||
|
||||
export class WindowsOperations implements OSOperations {
|
||||
async addHostEntry(ip: string, host: string): Promise<boolean> {
|
||||
const entry = `${ip} ${host}`
|
||||
const hostsPath = 'C:\\Windows\\System32\\drivers\\etc\\hosts'
|
||||
try {
|
||||
// Use powershell to append to hosts file if not exists
|
||||
const command = `powershell -Command "if (!(Get-Content ${hostsPath} | Select-String '${host}')) { Add-Content ${hostsPath} '${entry}' }"`
|
||||
await this.runElevated(command)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to add host entry:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async isElevated(): Promise<boolean> {
|
||||
try {
|
||||
await execAsync('net session')
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async runElevated(command: string): Promise<{ stdout: string, stderr: string }> {
|
||||
// In a real app, we would use sudo-prompt or a similar library.
|
||||
// For now, we assume the app might be run as admin or we use a basic exec.
|
||||
return execAsync(command)
|
||||
}
|
||||
}
|
||||
|
||||
export const osOps: OSOperations = new WindowsOperations()
|
||||
@@ -0,0 +1,86 @@
|
||||
import { spawn, ChildProcess } from 'child_process'
|
||||
import { join } from 'path'
|
||||
import { app } from 'electron'
|
||||
import fs from 'fs'
|
||||
|
||||
export class ProcessManager {
|
||||
private processes: Map<string, ChildProcess> = new Map()
|
||||
|
||||
constructor() {
|
||||
this.processes = new Map()
|
||||
}
|
||||
|
||||
async startService(name: string, binPath: string, args: string[]): Promise<boolean> {
|
||||
if (this.processes.has(name)) {
|
||||
console.log(`Service ${name} is already running.`)
|
||||
return true
|
||||
}
|
||||
|
||||
if (!fs.existsSync(binPath)) {
|
||||
console.warn(`[DEV] Executable not found: ${binPath}. Mocking service ${name}.`)
|
||||
const mockChild = {
|
||||
kill: () => { },
|
||||
on: () => { },
|
||||
unref: () => { }
|
||||
} as unknown as ChildProcess
|
||||
this.processes.set(name, mockChild)
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
const child = spawn(binPath, args, {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
})
|
||||
|
||||
child.on('error', (err) => {
|
||||
console.error(`Failed to start service ${name}:`, err)
|
||||
this.processes.delete(name)
|
||||
})
|
||||
|
||||
child.on('exit', (code) => {
|
||||
console.log(`Service ${name} exited with code ${code}`)
|
||||
this.processes.delete(name)
|
||||
})
|
||||
|
||||
this.processes.set(name, child)
|
||||
child.unref() // Allow the parent to exit independently
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`Error starting service ${name}:`, error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async stopService(name: string): Promise<boolean> {
|
||||
const child = this.processes.get(name)
|
||||
if (child) {
|
||||
child.kill()
|
||||
this.processes.delete(name)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
isServiceRunning(name: string): boolean {
|
||||
return this.processes.has(name)
|
||||
}
|
||||
|
||||
getStatuses(): Record<string, string> {
|
||||
return {
|
||||
nginx: this.isServiceRunning('nginx') ? 'running' : 'stopped',
|
||||
php: this.isServiceRunning('php') ? 'running' : 'stopped',
|
||||
mysql: this.isServiceRunning('mysql') ? 'running' : 'stopped'
|
||||
}
|
||||
}
|
||||
|
||||
stopAll(): void {
|
||||
for (const name of this.processes.keys()) {
|
||||
this.stopService(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const processManager = new ProcessManager()
|
||||
app.on('before-quit', () => processManager.stopAll())
|
||||
@@ -0,0 +1,28 @@
|
||||
import { contextBridge } from 'electron'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
startService: (name: string) => electronAPI.ipcRenderer.invoke('services:start', name),
|
||||
stopService: (name: string) => electronAPI.ipcRenderer.invoke('services:stop', name),
|
||||
getServiceStatus: () => electronAPI.ipcRenderer.invoke('services:status'),
|
||||
getConfig: (key: string) => electronAPI.ipcRenderer.invoke('config:get', key),
|
||||
saveConfig: (config: any) => electronAPI.ipcRenderer.invoke('config:save', config)
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
// renderer only if context isolation is enabled, otherwise
|
||||
// just add to the DOM global.
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore (define in dts)
|
||||
window.electron = electronAPI
|
||||
// @ts-ignore (define in dts)
|
||||
window.api = api
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Trunçgil Multi-PHP & MySQL Server</title>
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,249 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Drawer,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Container,
|
||||
Paper,
|
||||
Chip,
|
||||
Switch,
|
||||
Divider,
|
||||
Button,
|
||||
Snackbar,
|
||||
Alert
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Dashboard as DashboardIcon,
|
||||
Folder as ProjectIcon,
|
||||
Settings as SettingsIcon,
|
||||
Language as LanguageIcon,
|
||||
Storage as DbIcon,
|
||||
Dns as ServerIcon,
|
||||
Code as PhpIcon,
|
||||
PlayArrow as StartIcon,
|
||||
Stop as StopIcon,
|
||||
Refresh as RestartIcon
|
||||
} from '@mui/icons-material'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
declare global {
|
||||
interface Window {
|
||||
api: any
|
||||
}
|
||||
}
|
||||
|
||||
const drawerWidth = 240
|
||||
|
||||
interface ServiceStatus {
|
||||
nginx: string
|
||||
php: string
|
||||
mysql: string
|
||||
}
|
||||
|
||||
function App(): JSX.Element {
|
||||
const { t, i18n } = useTranslation()
|
||||
const [status, setStatus] = useState<ServiceStatus>({
|
||||
nginx: 'stopped',
|
||||
php: 'stopped',
|
||||
mysql: 'stopped'
|
||||
})
|
||||
const [activeTab, setActiveTab] = useState('dashboard')
|
||||
const [notification, setNotification] = useState<{ open: boolean, message: string, severity: 'success' | 'error' }>({
|
||||
open: false,
|
||||
message: '',
|
||||
severity: 'success'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStatus = async () => {
|
||||
// @ts-ignore
|
||||
const currentStatus = await window.api.getServiceStatus()
|
||||
if (currentStatus) setStatus(currentStatus)
|
||||
}
|
||||
fetchStatus()
|
||||
const interval = setInterval(fetchStatus, 3000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const handleToggleService = async (service: keyof ServiceStatus) => {
|
||||
// @ts-ignore
|
||||
const result = status[service] === 'running'
|
||||
? await window.api.stopService(service)
|
||||
: await window.api.startService(service)
|
||||
|
||||
setNotification({
|
||||
open: true,
|
||||
message: result.message,
|
||||
severity: result.success ? 'success' : 'error'
|
||||
})
|
||||
}
|
||||
|
||||
const toggleLanguage = () => {
|
||||
const newLang = i18n.language === 'tr' ? 'en' : i18n.language === 'en' ? 'de' : 'tr'
|
||||
i18n.changeLanguage(newLang)
|
||||
}
|
||||
|
||||
const handleStartAll = async () => {
|
||||
const services: (keyof ServiceStatus)[] = ['nginx', 'php', 'mysql']
|
||||
for (const s of services) {
|
||||
if (status[s] !== 'running') {
|
||||
// @ts-ignore
|
||||
await window.api.startService(s)
|
||||
}
|
||||
}
|
||||
setNotification({ open: true, message: 'Tüm servisler başlatıldı.', severity: 'success' })
|
||||
}
|
||||
|
||||
const handleStopAll = async () => {
|
||||
const services: (keyof ServiceStatus)[] = ['nginx', 'php', 'mysql']
|
||||
for (const s of services) {
|
||||
if (status[s] === 'running') {
|
||||
// @ts-ignore
|
||||
await window.api.stopService(s)
|
||||
}
|
||||
}
|
||||
setNotification({ open: true, message: 'Tüm servisler durduruldu.', severity: 'success' })
|
||||
}
|
||||
|
||||
const getStatusChip = (s: string) => {
|
||||
const color = s === 'running' ? 'success' : s === 'stopped' ? 'error' : 'warning'
|
||||
return <Chip label={s.toUpperCase()} color={color} size="small" sx={{ fontWeight: 'bold' }} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', minHeight: '100vh', backgroundImage: 'radial-gradient(circle at 50% 0%, #153E5E 0%, #121212 100%)', bgcolor: 'transparent' }}>
|
||||
<AppBar position="fixed" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1, bgcolor: 'rgba(21, 62, 94, 0.85)', backdropFilter: 'blur(10px)' }}>
|
||||
<Toolbar>
|
||||
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1, fontWeight: 'bold' }}>
|
||||
Trunçgil Multi-PHP Server
|
||||
</Typography>
|
||||
<Tooltip title="Dil Değiştir">
|
||||
<IconButton onClick={toggleLanguage} color="inherit">
|
||||
<LanguageIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton color="inherit">
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
<Drawer
|
||||
variant="permanent"
|
||||
sx={{
|
||||
width: drawerWidth,
|
||||
flexShrink: 0,
|
||||
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box', bgcolor: 'rgba(30, 30, 30, 0.95)', borderRight: '1px solid rgba(255, 255, 255, 0.05)' },
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
<Box sx={{ overflow: 'auto' }}>
|
||||
<List>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton selected={activeTab === 'dashboard'} onClick={() => setActiveTab('dashboard')}>
|
||||
<ListItemIcon><DashboardIcon color={activeTab === 'dashboard' ? 'primary' : 'inherit'} /></ListItemIcon>
|
||||
<ListItemText primary="Dashboard" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton selected={activeTab === 'projects'} onClick={() => setActiveTab('projects')}>
|
||||
<ListItemIcon><ProjectIcon color={activeTab === 'projects' ? 'primary' : 'inherit'} /></ListItemIcon>
|
||||
<ListItemText primary="Projeler" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
<Divider />
|
||||
<List>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton selected={activeTab === 'settings'} onClick={() => setActiveTab('settings')}>
|
||||
<ListItemIcon><SettingsIcon color={activeTab === 'settings' ? 'primary' : 'inherit'} /></ListItemIcon>
|
||||
<ListItemText primary="Genel Ayarlar" />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Box>
|
||||
</Drawer>
|
||||
|
||||
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
||||
<Toolbar />
|
||||
<Container maxWidth="md">
|
||||
{activeTab === 'dashboard' && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom sx={{ mb: 3, fontWeight: 'medium' }}>
|
||||
Hızlı Bakış
|
||||
</Typography>
|
||||
<Paper elevation={4} sx={{ mb: 4, borderRadius: 3, overflow: 'hidden', bgcolor: 'rgba(255, 255, 255, 0.03)', backdropFilter: 'blur(10px)', border: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
||||
<List dense>
|
||||
<ListItem sx={{ py: 1.5 }}>
|
||||
<ListItemIcon><ServerIcon color="primary" /></ListItemIcon>
|
||||
<ListItemText primary="Nginx Web Sunucusu" secondary="Port: 80, 443" />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{getStatusChip(status.nginx)}
|
||||
<Switch checked={status.nginx === 'running'} onChange={() => handleToggleService('nginx')} />
|
||||
</Box>
|
||||
</ListItem>
|
||||
<Divider variant="inset" />
|
||||
<ListItem sx={{ py: 1.5 }}>
|
||||
<ListItemIcon><PhpIcon color="primary" /></ListItemIcon>
|
||||
<ListItemText primary="PHP FastCGI" secondary="Sürüm: Seçilmedi" />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{getStatusChip(status.php)}
|
||||
<Switch checked={status.php === 'running'} onChange={() => handleToggleService('php')} />
|
||||
</Box>
|
||||
</ListItem>
|
||||
<Divider variant="inset" />
|
||||
<ListItem sx={{ py: 1.5 }}>
|
||||
<ListItemIcon><DbIcon color="primary" /></ListItemIcon>
|
||||
<ListItemText primary="MySQL / MariaDB" secondary="Port: 3306" />
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
{getStatusChip(status.mysql)}
|
||||
<Switch checked={status.mysql === 'running'} onChange={() => handleToggleService('mysql')} />
|
||||
</Box>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Paper>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Button variant="contained" fullWidth startIcon={<StartIcon />} size="large" onClick={handleStartAll} sx={{ borderRadius: 2, padding: '12px', background: 'linear-gradient(45deg, #153E5E 30%, #4A7CA3 90%)', boxShadow: '0 3px 5px 2px rgba(21, 62, 94, .3)' }}>
|
||||
{t('common.start')}
|
||||
</Button>
|
||||
<Button variant="outlined" fullWidth startIcon={<StopIcon />} size="large" color="error" onClick={handleStopAll} sx={{ borderRadius: 2, padding: '12px', borderWidth: 2, '&:hover': { borderWidth: 2 } }}>
|
||||
{t('common.stop')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === 'projects' && (
|
||||
<Box sx={{ textAlign: 'center', mt: 4 }}>
|
||||
<Typography variant="h6">Henüz bir proje eklenmemiş.</Typography>
|
||||
<Button variant="contained" startIcon={<ProjectIcon />} sx={{ mt: 2 }}>
|
||||
Yeni Proje Ekle
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
<Snackbar
|
||||
open={notification.open}
|
||||
autoHideDuration={4000}
|
||||
onClose={() => setNotification({ ...notification, open: false })}
|
||||
>
|
||||
<Alert severity={notification.severity} variant="filled" sx={{ width: '100%' }}>
|
||||
{notification.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,22 @@
|
||||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import tr from '@locales/tr.json'
|
||||
import en from '@locales/en.json'
|
||||
import de from '@locales/de.json'
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
tr: { translation: tr },
|
||||
en: { translation: en },
|
||||
de: { translation: de }
|
||||
},
|
||||
lng: 'tr',
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
}
|
||||
})
|
||||
|
||||
export default i18n
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './i18n'
|
||||
import { ThemeProvider, createTheme } from '@mui/material/styles'
|
||||
import CssBaseline from '@mui/material/CssBaseline'
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#153E5E',
|
||||
},
|
||||
secondary: {
|
||||
main: '#4A7CA3',
|
||||
},
|
||||
mode: 'dark',
|
||||
},
|
||||
})
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>
|
||||
)
|
||||
Reference in New Issue
Block a user