158 lines
4.9 KiB
TypeScript
158 lines
4.9 KiB
TypeScript
import { useState } from 'react';
|
|
import { File } from 'lucide-react';
|
|
import { Env } from './models/Env';
|
|
import { Environment } from './componets/env/Environment';
|
|
import { Content } from './componets/content/Content';
|
|
import { FileChooser } from './componets/FileChooser';
|
|
import { Config } from './models/Config';
|
|
|
|
class AppState {
|
|
private constructor(
|
|
public config: Config = new Config(),
|
|
) {}
|
|
|
|
static readonly Instance = new AppState();
|
|
|
|
public loadConfig(cfg: Config) {
|
|
this.config = cfg;
|
|
}
|
|
|
|
public async saveEnv(env: Env): Promise<number> {
|
|
// Simulate async save with 1 second delay
|
|
return await new Promise<number>((resolve) => {
|
|
setTimeout(() => {
|
|
console.log('Saved env:', env.name);
|
|
resolve(0);
|
|
}, 1000);
|
|
});
|
|
}
|
|
}
|
|
|
|
function App() {
|
|
const [config, setConfig] = useState(() => AppState.Instance.config);
|
|
const [envs, setEnvs] = useState(() => AppState.Instance.config.envs);
|
|
const [selectedEnv, setSelectedEnv] = useState(0);
|
|
|
|
// Ensure selectedEnv is always valid
|
|
const validSelectedEnv = Math.min(selectedEnv, Math.max(0, envs.length - 1));
|
|
const currentEnv = envs[validSelectedEnv];
|
|
|
|
async function handleEnvChanged(env: Env) {
|
|
// Optimistic update - update React state immediately
|
|
setEnvs((prevEnvs) => {
|
|
const newEnvs = [...prevEnvs];
|
|
const idx = newEnvs.findIndex((x) => x.id === env.id);
|
|
if (idx > -1) {
|
|
newEnvs[idx] = env;
|
|
}
|
|
return newEnvs;
|
|
});
|
|
|
|
// Also update config.envs and template to keep them in sync
|
|
setConfig((prevConfig) => {
|
|
const newConfig = new Config();
|
|
newConfig.envs = prevConfig.envs.map((e) => (e.id === env.id ? env : e));
|
|
newConfig.template = prevConfig.template;
|
|
newConfig.updateTemplateFromEnv(env);
|
|
return newConfig;
|
|
});
|
|
|
|
// Fire off async save in background (no need to wait)
|
|
AppState.Instance.saveEnv(env);
|
|
}
|
|
|
|
function handleEnvSelected(idx: number) {
|
|
setSelectedEnv(idx);
|
|
}
|
|
|
|
function handleEnvAdded(env: Env): number {
|
|
const newIdx = envs.length;
|
|
setEnvs((prevEnvs) => [...prevEnvs, env]);
|
|
setConfig((prevConfig) => {
|
|
const newConfig = new Config();
|
|
newConfig.envs = [...prevConfig.envs, env];
|
|
newConfig.template = prevConfig.template;
|
|
return newConfig;
|
|
});
|
|
return newIdx;
|
|
}
|
|
|
|
function handleEnvRemoved(envId: number) {
|
|
setEnvs((prevEnvs) => prevEnvs.filter((e) => e.id !== envId));
|
|
setConfig((prevConfig) => {
|
|
const newConfig = new Config();
|
|
newConfig.envs = prevConfig.envs.filter((e) => e.id !== envId);
|
|
newConfig.template = prevConfig.template;
|
|
return newConfig;
|
|
});
|
|
}
|
|
|
|
function handleTemplateSaved(newContent: string) {
|
|
setConfig((prevConfig) => {
|
|
const newConfig = new Config();
|
|
newConfig.envs = prevConfig.envs;
|
|
newConfig.setTemplate(newContent);
|
|
return newConfig;
|
|
});
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
|
|
<main className="container mx-auto px-4 py-6 max-w-[1920px]">
|
|
{/* Header */}
|
|
<div className="mb-6">
|
|
<FileChooser
|
|
onSelected={(x) => {
|
|
AppState.Instance.loadConfig(x);
|
|
setEnvs(x.envs);
|
|
setConfig(x);
|
|
}}
|
|
config={config}
|
|
/>
|
|
</div>
|
|
|
|
{envs.length > 0 ? (
|
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
|
{/* Environment Panel - Fixed 4/12 on all large screens */}
|
|
<section className="lg:col-span-4 xl:col-span-4 2xl:col-span-3">
|
|
<div className="sticky top-6">
|
|
<Environment
|
|
envs={envs}
|
|
onChanged={async (e) => await handleEnvChanged(e)}
|
|
onSelected={handleEnvSelected}
|
|
onAdd={handleEnvAdded}
|
|
onRemove={handleEnvRemoved}
|
|
/>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Content Panel - Maximum width (8/12 → 9/12 on 2xl) */}
|
|
<section className="lg:col-span-8 xl:col-span-8 2xl:col-span-9">
|
|
<Content
|
|
env={currentEnv}
|
|
config={config}
|
|
onTemplateSaved={handleTemplateSaved}
|
|
/>
|
|
</section>
|
|
</div>
|
|
) : (
|
|
/* Empty State */
|
|
<div className="flex flex-col items-center justify-center py-20">
|
|
<div className="w-24 h-24 bg-gradient-to-br from-blue-400 to-blue-600 rounded-2xl flex items-center justify-center mb-6 shadow-lg">
|
|
<File className="w-12 h-12 text-white opacity-80" />
|
|
</div>
|
|
<h2 className="text-2xl font-bold text-slate-700 mb-2">
|
|
No Configuration Loaded
|
|
</h2>
|
|
<p className="text-slate-500 text-center max-w-md">
|
|
Create a new configuration or upload an existing XML file to get started
|
|
</p>
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|