feat: implement core UI dashboard and backend service management for multi-PHP environment
This commit is contained in:
@@ -2,6 +2,8 @@ import { ipcMain, app } from 'electron'
|
||||
import { processManager } from '../services/ProcessManager'
|
||||
import { configService } from '../services/ConfigService'
|
||||
import { downloadService } from '../services/DownloadService'
|
||||
import { projectService } from '../services/ProjectService'
|
||||
import { phpManagerService } from '../services/PhpManagerService'
|
||||
import path from 'path'
|
||||
|
||||
export function registerIpcHandlers(): void {
|
||||
@@ -54,6 +56,33 @@ export function registerIpcHandlers(): void {
|
||||
})
|
||||
|
||||
// Binary management
|
||||
ipcMain.handle('binaries:list-php', async () => {
|
||||
return phpManagerService.getVersions()
|
||||
})
|
||||
|
||||
ipcMain.handle('binaries:download-php', async (event, id: string) => {
|
||||
const versions = await phpManagerService.getVersions()
|
||||
const version = versions.find(v => v.id === id)
|
||||
if (!version) return { success: false, message: 'Geçersiz sürüm.' }
|
||||
|
||||
try {
|
||||
// Signal immediate start
|
||||
phpManagerService.updateProgress(id, 0, 'downloading')
|
||||
event.sender.send('binaries:progress', { id, progress: 0 })
|
||||
|
||||
await downloadService.downloadAndExtract(version.url, id, (p) => {
|
||||
phpManagerService.updateProgress(id, p)
|
||||
event.sender.send('binaries:progress', { id, progress: p })
|
||||
})
|
||||
|
||||
phpManagerService.updateProgress(id, 100, 'installed')
|
||||
return { success: true, message: 'İndirme tamamlandı.' }
|
||||
} catch (error: any) {
|
||||
phpManagerService.updateProgress(id, 0, 'available')
|
||||
return { success: false, message: `Hata: ${error.message}` }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('binaries:download', async (_event, { name, url }) => {
|
||||
return downloadService.downloadAndExtract(url, name, (p) => {
|
||||
// TODO: Emit progress to renderer via webContents.send
|
||||
@@ -61,6 +90,19 @@ export function registerIpcHandlers(): void {
|
||||
})
|
||||
})
|
||||
|
||||
// Project management
|
||||
ipcMain.handle('projects:list', async () => {
|
||||
return projectService.getProjects()
|
||||
})
|
||||
|
||||
ipcMain.handle('projects:add', async (_event, project) => {
|
||||
return projectService.addProject(project)
|
||||
})
|
||||
|
||||
ipcMain.handle('projects:remove', async (_event, id) => {
|
||||
return projectService.removeProject(id)
|
||||
})
|
||||
|
||||
// Config management
|
||||
ipcMain.handle('config:get', async (_event, key: string) => {
|
||||
return null
|
||||
|
||||
@@ -2,13 +2,14 @@ import axios from 'axios'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { app } from 'electron'
|
||||
import * as decompress from 'decompress'
|
||||
const decompress = require('decompress')
|
||||
|
||||
export class DownloadService {
|
||||
private binDir: string
|
||||
|
||||
constructor() {
|
||||
this.binDir = path.join(app.getAppPath(), 'bin')
|
||||
// Use userData for binaries to avoid permission issues in Program Files
|
||||
this.binDir = path.join(app.getPath('userData'), 'bin')
|
||||
if (!fs.existsSync(this.binDir)) {
|
||||
fs.mkdirSync(this.binDir, { recursive: true })
|
||||
}
|
||||
@@ -17,42 +18,58 @@ export class DownloadService {
|
||||
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)
|
||||
const phpExe = path.join(targetDir, 'php.exe')
|
||||
|
||||
if (fs.existsSync(targetDir)) {
|
||||
// If already installed and working, skip
|
||||
if (fs.existsSync(phpExe)) {
|
||||
return targetDir
|
||||
}
|
||||
|
||||
// Cleanup failed attempts
|
||||
if (fs.existsSync(targetDir)) {
|
||||
fs.rmSync(targetDir, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
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)
|
||||
try {
|
||||
const response = await axios({
|
||||
url,
|
||||
method: 'GET',
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
||||
}
|
||||
})
|
||||
writer.on('error', reject)
|
||||
})
|
||||
|
||||
const totalLength = parseInt(response.headers['content-length'] || '0')
|
||||
let downloadedLength = 0
|
||||
|
||||
response.data.on('data', (chunk: any) => {
|
||||
downloadedLength += chunk.length
|
||||
if (onProgress && totalLength > 0) {
|
||||
onProgress(Math.round((downloadedLength / totalLength) * 100))
|
||||
}
|
||||
})
|
||||
|
||||
response.data.pipe(writer)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.on('finish', async () => {
|
||||
try {
|
||||
await decompress(tempFile, targetDir)
|
||||
if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile)
|
||||
resolve(targetDir)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
writer.on('error', reject)
|
||||
})
|
||||
} catch (error) {
|
||||
if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
getBinPath(service: string, executable: string): string {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { app } from 'electron'
|
||||
import axios from 'axios'
|
||||
|
||||
export interface PhpVersion {
|
||||
id: string
|
||||
version: string
|
||||
url: string
|
||||
status: 'available' | 'downloading' | 'installed'
|
||||
progress: number
|
||||
}
|
||||
|
||||
export class PhpManagerService {
|
||||
private versions: PhpVersion[] = []
|
||||
private binDir: string
|
||||
private winPhpUrl = 'https://windows.php.net/downloads/releases/'
|
||||
private winReleasesJsonUrl = 'https://windows.php.net/downloads/releases/releases.json'
|
||||
private mainReleasesJsonUrl = 'https://www.php.net/releases/index.php?json'
|
||||
|
||||
constructor() {
|
||||
this.binDir = path.join(app.getPath('userData'), 'bin')
|
||||
this.init()
|
||||
}
|
||||
|
||||
private async init() {
|
||||
if (!fs.existsSync(this.binDir)) {
|
||||
fs.mkdirSync(this.binDir, { recursive: true })
|
||||
}
|
||||
await this.fetchAvailableVersions()
|
||||
}
|
||||
|
||||
private async fetchAvailableVersions() {
|
||||
try {
|
||||
// Fetch from windows.php.net for actual paths
|
||||
const winResponse = await axios.get(this.winReleasesJsonUrl, {
|
||||
headers: { 'User-Agent': 'Mozilla/5.0' }
|
||||
})
|
||||
const winData = winResponse.data
|
||||
|
||||
// Fetch from php.net for supported versions list
|
||||
const mainResponse = await axios.get(this.mainReleasesJsonUrl, {
|
||||
headers: { 'User-Agent': 'Mozilla/5.0' }
|
||||
})
|
||||
const mainData = mainResponse.data
|
||||
|
||||
const dynamicVersions: PhpVersion[] = []
|
||||
|
||||
// Method 1: Versions explicitly available in windows releases.json
|
||||
for (const majorMinor in winData) {
|
||||
const release = winData[majorMinor]
|
||||
const version = release.version
|
||||
const tsBuild = release['ts-vs16-x64'] || release['ts-vs17-x64'] || release['ts-vc15-x64']
|
||||
|
||||
if (tsBuild && tsBuild.zip) {
|
||||
dynamicVersions.push({
|
||||
id: `php-${version}`,
|
||||
version: version,
|
||||
url: this.winPhpUrl + tsBuild.zip.path,
|
||||
status: 'available',
|
||||
progress: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: Check supported_versions from main php.net
|
||||
// This helps if windows releases.json doesn't group some newer versions yet
|
||||
if (mainData['8'] && mainData['8'].supported_versions) {
|
||||
for (const ver of mainData['8'].supported_versions) {
|
||||
// ver is "8.2", "8.3" etc.
|
||||
// If we already have a version for this branch, we are good.
|
||||
// Otherwise, we could try to guess or find more.
|
||||
}
|
||||
}
|
||||
|
||||
// De-duplicate by version string
|
||||
const uniqueVersionsMap = new Map<string, PhpVersion>()
|
||||
dynamicVersions.forEach(v => uniqueVersionsMap.set(v.version, v))
|
||||
|
||||
this.versions = Array.from(uniqueVersionsMap.values()).sort((a, b) => b.version.localeCompare(a.version))
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch PHP versions:', e)
|
||||
}
|
||||
}
|
||||
|
||||
private refreshInstalledStatus() {
|
||||
this.versions = this.versions.map(v => {
|
||||
const targetDir = path.join(this.binDir, v.id)
|
||||
const phpExe = path.join(targetDir, 'php.exe')
|
||||
if (fs.existsSync(phpExe)) {
|
||||
return { ...v, status: 'installed', progress: 100 }
|
||||
}
|
||||
return v
|
||||
})
|
||||
}
|
||||
|
||||
async getVersions(): Promise<PhpVersion[]> {
|
||||
if (this.versions.length === 0) {
|
||||
await this.fetchAvailableVersions()
|
||||
}
|
||||
this.refreshInstalledStatus()
|
||||
return this.versions
|
||||
}
|
||||
|
||||
updateProgress(id: string, progress: number, status: PhpVersion['status'] = 'downloading') {
|
||||
const version = this.versions.find(v => v.id === id)
|
||||
if (version) {
|
||||
version.progress = progress
|
||||
version.status = status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const phpManagerService = new PhpManagerService()
|
||||
@@ -0,0 +1,58 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { app } from 'electron'
|
||||
|
||||
export interface Project {
|
||||
id: string
|
||||
name: string
|
||||
path: string
|
||||
phpVersion: string
|
||||
host: string
|
||||
}
|
||||
|
||||
export class ProjectService {
|
||||
private projectsFile: string
|
||||
private projects: Project[] = []
|
||||
|
||||
constructor() {
|
||||
this.projectsFile = path.join(app.getPath('userData'), 'projects.json')
|
||||
this.loadProjects()
|
||||
}
|
||||
|
||||
private loadProjects() {
|
||||
if (fs.existsSync(this.projectsFile)) {
|
||||
try {
|
||||
const data = fs.readFileSync(this.projectsFile, 'utf-8')
|
||||
this.projects = JSON.parse(data)
|
||||
} catch (e) {
|
||||
console.error('Failed to load projects:', e)
|
||||
this.projects = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private saveProjects() {
|
||||
fs.writeFileSync(this.projectsFile, JSON.stringify(this.projects, null, 2))
|
||||
}
|
||||
|
||||
getProjects(): Project[] {
|
||||
return this.projects
|
||||
}
|
||||
|
||||
addProject(project: Omit<Project, 'id'>): Project {
|
||||
const newProject = {
|
||||
...project,
|
||||
id: Date.now().toString()
|
||||
}
|
||||
this.projects.push(newProject)
|
||||
this.saveProjects()
|
||||
return newProject
|
||||
}
|
||||
|
||||
removeProject(id: string) {
|
||||
this.projects = this.projects.filter(p => p.id !== id)
|
||||
this.saveProjects()
|
||||
}
|
||||
}
|
||||
|
||||
export const projectService = new ProjectService()
|
||||
@@ -7,7 +7,13 @@ const api = {
|
||||
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)
|
||||
saveConfig: (config: any) => electronAPI.ipcRenderer.invoke('config:save', config),
|
||||
getProjects: () => electronAPI.ipcRenderer.invoke('projects:list'),
|
||||
addProject: (project: any) => electronAPI.ipcRenderer.invoke('projects:add', project),
|
||||
removeProject: (id: string) => electronAPI.ipcRenderer.invoke('projects:remove', id),
|
||||
getPhpVersions: () => electronAPI.ipcRenderer.invoke('binaries:list-php'),
|
||||
downloadPhpVersion: (id: string) => electronAPI.ipcRenderer.invoke('binaries:download-php', id),
|
||||
onBinaryProgress: (callback: any) => electronAPI.ipcRenderer.on('binaries:progress', (_event, data) => callback(data))
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
+208
-14
@@ -19,7 +19,17 @@ import {
|
||||
Divider,
|
||||
Button,
|
||||
Snackbar,
|
||||
Alert
|
||||
Alert,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
LinearProgress
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Dashboard as DashboardIcon,
|
||||
@@ -31,9 +41,13 @@ import {
|
||||
Code as PhpIcon,
|
||||
PlayArrow as StartIcon,
|
||||
Stop as StopIcon,
|
||||
Refresh as RestartIcon
|
||||
Delete as DeleteIcon,
|
||||
Download as DownloadIcon,
|
||||
CheckCircle as InstalledIcon,
|
||||
CloudDownload as AvailableIcon
|
||||
} from '@mui/icons-material'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
api: any
|
||||
@@ -56,21 +70,58 @@ function App(): JSX.Element {
|
||||
mysql: 'stopped'
|
||||
})
|
||||
const [activeTab, setActiveTab] = useState('dashboard')
|
||||
const [projects, setProjects] = useState<any[]>([])
|
||||
const [phpVersions, setPhpVersions] = useState<any[]>([])
|
||||
const [isProjectDialogOpen, setIsProjectDialogOpen] = useState(false)
|
||||
const [newProject, setNewProject] = useState({ name: '', path: '', phpVersion: '8.2', host: 'localhost' })
|
||||
const [notification, setNotification] = useState<{ open: boolean, message: string, severity: 'success' | 'error' }>({
|
||||
open: false,
|
||||
message: '',
|
||||
severity: 'success'
|
||||
})
|
||||
|
||||
const fetchStatus = async () => {
|
||||
// @ts-ignore
|
||||
const currentStatus = await window.api.getServiceStatus()
|
||||
if (currentStatus) setStatus(currentStatus)
|
||||
}
|
||||
|
||||
const fetchProjects = async () => {
|
||||
// @ts-ignore
|
||||
const list = await window.api.getProjects()
|
||||
setProjects(list || [])
|
||||
}
|
||||
|
||||
const fetchPhpVersions = async () => {
|
||||
// @ts-ignore
|
||||
const list = await window.api.getPhpVersions()
|
||||
setPhpVersions(list || [])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStatus = async () => {
|
||||
// @ts-ignore
|
||||
const currentStatus = await window.api.getServiceStatus()
|
||||
if (currentStatus) setStatus(currentStatus)
|
||||
}
|
||||
if (!window.api) return
|
||||
|
||||
fetchStatus()
|
||||
const interval = setInterval(fetchStatus, 3000)
|
||||
return () => clearInterval(interval)
|
||||
fetchProjects()
|
||||
fetchPhpVersions()
|
||||
|
||||
// @ts-ignore
|
||||
const removeListener = window.api.onBinaryProgress((data) => {
|
||||
if (!data) return
|
||||
const { id, progress } = data
|
||||
setPhpVersions(prev => prev.map(v => v.id === id ? { ...v, progress, status: progress === 100 ? 'installed' : 'downloading' } : v))
|
||||
})
|
||||
|
||||
const interval = setInterval(() => {
|
||||
fetchStatus()
|
||||
fetchProjects()
|
||||
fetchPhpVersions()
|
||||
}, 3000)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
if (typeof removeListener === 'function') removeListener()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleToggleService = async (service: keyof ServiceStatus) => {
|
||||
@@ -118,6 +169,33 @@ function App(): JSX.Element {
|
||||
return <Chip label={s.toUpperCase()} color={color} size="small" sx={{ fontWeight: 'bold' }} />
|
||||
}
|
||||
|
||||
const handleAddProject = async () => {
|
||||
// @ts-ignore
|
||||
await window.api.addProject(newProject)
|
||||
setIsProjectDialogOpen(false)
|
||||
setNewProject({ name: '', path: '', phpVersion: '8.2', host: 'localhost' })
|
||||
setNotification({ open: true, message: 'Proje başarıyla eklendi.', severity: 'success' })
|
||||
fetchProjects()
|
||||
}
|
||||
|
||||
const handleRemoveProject = async (id: string) => {
|
||||
// @ts-ignore
|
||||
await window.api.removeProject(id)
|
||||
setNotification({ open: true, message: 'Proje silindi.', severity: 'success' })
|
||||
fetchProjects()
|
||||
}
|
||||
|
||||
const handleDownloadPhp = async (id: string) => {
|
||||
// @ts-ignore
|
||||
const result = await window.api.downloadPhpVersion(id)
|
||||
setNotification({
|
||||
open: true,
|
||||
message: result.message,
|
||||
severity: result.success ? 'success' : 'error'
|
||||
})
|
||||
fetchPhpVersions()
|
||||
}
|
||||
|
||||
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)' }}>
|
||||
@@ -175,6 +253,49 @@ function App(): JSX.Element {
|
||||
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
|
||||
<Toolbar />
|
||||
<Container maxWidth="md">
|
||||
{activeTab === 'settings' && (
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 'medium', mb: 3 }}>
|
||||
Genel Ayarlar
|
||||
</Typography>
|
||||
|
||||
<Paper sx={{ mb: 4, bgcolor: 'rgba(255, 255, 255, 0.03)', borderRadius: 2 }}>
|
||||
<Box sx={{ p: 2, borderBottom: '1px solid rgba(255, 255, 255, 0.05)' }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>PHP Sürümleri</Typography>
|
||||
<Typography variant="body2" color="text.secondary">Gereken PHP sürümlerini buradan indirebilir ve yönetebilirsiniz.</Typography>
|
||||
</Box>
|
||||
<List>
|
||||
{phpVersions.map((v) => (
|
||||
<ListItem key={v.id} sx={{ py: 2 }}>
|
||||
<ListItemIcon>
|
||||
{v.status === 'installed' ? <InstalledIcon color="success" /> : <AvailableIcon color="action" />}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={`PHP ${v.version}`}
|
||||
secondary={v.status === 'downloading' ? `İndiriliyor... %${Math.round(v.progress)}` : v.status === 'installed' ? 'Kurulu' : 'İndirilebilir'}
|
||||
sx={{ flexGrow: 1 }}
|
||||
/>
|
||||
<Box sx={{ minWidth: 120, textAlign: 'right' }}>
|
||||
{v.status === 'available' && (
|
||||
<Button variant="outlined" size="small" startIcon={<DownloadIcon />} onClick={() => handleDownloadPhp(v.id)}>
|
||||
İNDİR
|
||||
</Button>
|
||||
)}
|
||||
{v.status === 'downloading' && (
|
||||
<Box sx={{ width: '100%', mt: 1 }}>
|
||||
<LinearProgress variant="determinate" value={v.progress} sx={{ height: 8, borderRadius: 5 }} />
|
||||
</Box>
|
||||
)}
|
||||
{v.status === 'installed' && (
|
||||
<Chip label="HAZIR" color="success" variant="outlined" size="small" />
|
||||
)}
|
||||
</Box>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
</Box>
|
||||
)}
|
||||
{activeTab === 'dashboard' && (
|
||||
<>
|
||||
<Typography variant="h5" gutterBottom sx={{ mb: 3, fontWeight: 'medium' }}>
|
||||
@@ -223,11 +344,41 @@ function App(): JSX.Element {
|
||||
)}
|
||||
|
||||
{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>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 'medium' }}>
|
||||
Projelerim
|
||||
</Typography>
|
||||
<Button variant="contained" startIcon={<ProjectIcon />} onClick={() => setIsProjectDialogOpen(true)}>
|
||||
Yeni Proje Ekle
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{projects.length === 0 ? (
|
||||
<Paper sx={{ p: 4, textAlign: 'center', bgcolor: 'rgba(255, 255, 255, 0.03)' }}>
|
||||
<Typography variant="body1" color="text.secondary">Henüz bir proje eklenmemiş.</Typography>
|
||||
</Paper>
|
||||
) : (
|
||||
<List>
|
||||
{projects.map((project) => (
|
||||
<Paper key={project.id} sx={{ mb: 2, bgcolor: 'rgba(255, 255, 255, 0.05)' }}>
|
||||
<ListItem
|
||||
secondaryAction={
|
||||
<IconButton edge="end" aria-label="delete" onClick={() => handleRemoveProject(project.id)}>
|
||||
<DeleteIcon color="error" />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ListItemIcon><ProjectIcon color="primary" /></ListItemIcon>
|
||||
<ListItemText
|
||||
primary={project.name}
|
||||
secondary={`${project.path} - PHP: ${project.phpVersion} - Host: ${project.host}`}
|
||||
/>
|
||||
</ListItem>
|
||||
</Paper>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Container>
|
||||
@@ -242,6 +393,49 @@ function App(): JSX.Element {
|
||||
{notification.message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
<Dialog open={isProjectDialogOpen} onClose={() => setIsProjectDialogOpen(false)} fullWidth maxWidth="sm">
|
||||
<DialogTitle>Yeni Proje Ekle</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
label="Proje Adı"
|
||||
fullWidth
|
||||
value={newProject.name}
|
||||
onChange={(e) => setNewProject({ ...newProject, name: e.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
label="Dosya Yolu (Absolute path)"
|
||||
fullWidth
|
||||
value={newProject.path}
|
||||
onChange={(e) => setNewProject({ ...newProject, path: e.target.value })}
|
||||
/>
|
||||
<TextField
|
||||
label="Hostname (örn: local.test)"
|
||||
fullWidth
|
||||
value={newProject.host}
|
||||
onChange={(e) => setNewProject({ ...newProject, host: e.target.value })}
|
||||
/>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>PHP Sürümü</InputLabel>
|
||||
<Select
|
||||
value={newProject.phpVersion}
|
||||
label="PHP Sürümü"
|
||||
onChange={(e) => setNewProject({ ...newProject, phpVersion: e.target.value })}
|
||||
>
|
||||
<MenuItem value="8.0">PHP 8.0</MenuItem>
|
||||
<MenuItem value="8.1">PHP 8.1</MenuItem>
|
||||
<MenuItem value="8.2">PHP 8.2</MenuItem>
|
||||
<MenuItem value="8.3">PHP 8.3</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setIsProjectDialogOpen(false)}>İptal</Button>
|
||||
<Button onClick={handleAddProject} variant="contained" disabled={!newProject.name || !newProject.path}>Ekle</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,73 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './i18n'
|
||||
import i18n from './i18n'
|
||||
import { ThemeProvider, createTheme } from '@mui/material/styles'
|
||||
import CssBaseline from '@mui/material/CssBaseline'
|
||||
import { I18nextProvider } from 'react-i18next'
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
main: '#153E5E',
|
||||
},
|
||||
secondary: {
|
||||
main: '#4A7CA3',
|
||||
},
|
||||
mode: 'dark',
|
||||
secondary: {
|
||||
main: '#153E5E',
|
||||
},
|
||||
background: {
|
||||
default: '#121212',
|
||||
paper: '#1e1e1e',
|
||||
},
|
||||
text: {
|
||||
primary: '#ffffff',
|
||||
secondary: 'rgba(255, 255, 255, 0.7)',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundImage: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Error Boundary for better debugging
|
||||
class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean, error: any }> {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { hasError: false, error: null }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div style={{ padding: 20, color: 'white' }}>
|
||||
<h1>Görüntüleme Hatası Oluştu</h1>
|
||||
<pre style={{ whiteSpace: 'pre-wrap' }}>{this.state.error?.toString()}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
<ErrorBoundary>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</I18nextProvider>
|
||||
</ErrorBoundary>
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user