Build EnvHelper desktop app
Some checks failed
Build Windows App / build-windows (push) Has been cancelled

This commit is contained in:
MrSphay
2026-05-01 12:54:29 +02:00
commit 0d4c6e9c82
15 changed files with 978 additions and 0 deletions

150
src/App.tsx Normal file
View File

@@ -0,0 +1,150 @@
import { Check, Clipboard, FileDown, FileInput, RefreshCcw, ShieldCheck, Sparkles } from "lucide-react";
import { useMemo, useState } from "react";
import { transformEnv } from "./env";
const sampleEnv = `APP_IMAGE=git.wilkensxl.de/mrsphay/hilden-directory-gateway:latest
APP_PORT=3000
NODE_ENV=production
PUBLIC_BASE_URL=https://gateway.example.local
TRUSTED_IFRAME_ANCESTORS=https://www.hilden.de
DATABASE_URL=postgresql://hilden_app:CHANGE_ME_POSTGRES_PASSWORD@postgres:5432/hilden_directory
POSTGRES_PASSWORD=CHANGE_ME_POSTGRES_PASSWORD
SESSION_SECRET=CHANGE_ME_AT_LEAST_32_RANDOM_CHARACTERS
ENCRYPTION_KEY_BASE64=CHANGE_ME_32_RANDOM_BYTES_AS_BASE64
BOOTSTRAP_ADMIN_EMAIL=admin@example.local
BOOTSTRAP_ADMIN_PASSWORD=CHANGE_ME_LONG_INITIAL_ADMIN_PASSWORD`;
export default function App() {
const [input, setInput] = useState(sampleEnv);
const [loadedPath, setLoadedPath] = useState<string | null>(null);
const [copyLabel, setCopyLabel] = useState("Kopieren");
const [generation, setGeneration] = useState(0);
const result = useMemo(() => transformEnv(input), [input, generation]);
async function openFile() {
const file = await window.envHelper?.openFile();
if (file) {
setInput(file.content);
setLoadedPath(file.path);
}
}
async function saveFile() {
await window.envHelper?.saveFile(result.output);
}
async function copyOutput() {
await navigator.clipboard.writeText(result.output);
setCopyLabel("Kopiert");
window.setTimeout(() => setCopyLabel("Kopieren"), 1400);
}
function regenerate() {
setGeneration((current) => current + 1);
}
return (
<main className="shell">
<section className="hero">
<div>
<p className="eyebrow"><Sparkles size={16} /> EnvHelper</p>
<h1>.env Platzhalter sauber ersetzen</h1>
<p className="subline">
Erkennt CHANGE_ME Werte, erzeugt passende Secrets im richtigen Format und hält gleiche Platzhalter synchron.
</p>
</div>
<div className="heroActions">
<button className="secondary" onClick={openFile} type="button">
<FileInput size={18} /> Datei laden
</button>
<button className="secondary" onClick={saveFile} type="button" disabled={!result.output}>
<FileDown size={18} /> .env speichern
</button>
</div>
</section>
<section className="stats">
<div>
<span>{result.changedCount}</span>
<p>Fundstellen</p>
</div>
<div>
<span>{result.replacements.length}</span>
<p>eindeutige Werte</p>
</div>
<div>
<span>{loadedPath ? "Datei" : "Text"}</span>
<p>Quelle</p>
</div>
</section>
<section className="workspace">
<div className="panel">
<div className="panelHeader">
<div>
<h2>Input</h2>
<p>{loadedPath ?? "Direkt einfügen oder Beispieldaten ersetzen"}</p>
</div>
<button className="iconButton" onClick={() => setInput(sampleEnv)} title="Beispiel laden" type="button">
<RefreshCcw size={18} />
</button>
</div>
<textarea
spellCheck={false}
value={input}
onChange={(event) => setInput(event.target.value)}
aria-label="Env input"
/>
</div>
<div className="panel">
<div className="panelHeader">
<div>
<h2>Output</h2>
<p>Bereit für Deployment oder Secret Store</p>
</div>
<div className="buttonRow">
<button className="iconButton" onClick={regenerate} title="Neu erzeugen" type="button">
<RefreshCcw size={18} />
</button>
<button className="primary" onClick={copyOutput} type="button">
{copyLabel === "Kopiert" ? <Check size={18} /> : <Clipboard size={18} />} {copyLabel}
</button>
</div>
</div>
<textarea spellCheck={false} value={result.output} readOnly aria-label="Env output" />
</div>
</section>
<section className="replacementPanel">
<div className="panelHeader compact">
<div>
<h2>Erkannte Anforderungen</h2>
<p>Formate werden aus Variablennamen und Platzhaltertext abgeleitet.</p>
</div>
<ShieldCheck size={22} />
</div>
{result.replacements.length === 0 ? (
<div className="empty">Keine CHANGE_ME Platzhalter gefunden.</div>
) : (
<div className="replacementGrid">
{result.replacements.map((replacement) => (
<article className="replacement" key={replacement.placeholder}>
<div>
<strong>{replacement.placeholder}</strong>
<p>{replacement.keys.join(", ")}</p>
</div>
<span>{replacement.format}</span>
</article>
))}
</div>
)}
</section>
</main>
);
}