philippurbschat.de/_template/index.php
Philipp Urbschat f393beb7ff
feat: inkubator-standard & modul-autodiscovery
- Dynamische Modulerkennung via module.json in ModuleService.php
- Kaskadierender Env-Loader fuer zentrale API-Keys (GEMINI_API_KEY)
- Graceful Auth fuer alle Subprojekte (dinos, plants, storymachine, test)
- Wiederverwendbares Starter-Template in _template/
- test-Modul auf philippurbschat.de Dark-Design modernisiert
2026-09-13 00:23:16 +02:00

152 lines
7.5 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
require_once __DIR__ . '/bootstrap.php';
// Modul-Metadaten laden
$moduleDescriptor = file_exists(__DIR__ . '/module.json')
? json_decode(file_get_contents(__DIR__ . '/module.json'), true)
: [];
$moduleName = $moduleDescriptor['name'] ?? 'Neues Projekt';
$moduleBadge = $moduleDescriptor['badge'] ?? 'Lab';
$moduleDesc = $moduleDescriptor['description'] ?? 'Experimentelles Web-Modul';
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($moduleName) ?> philippurbschat.de</title>
<!-- Zentrale Styles von philippurbschat.de -->
<link rel="stylesheet" href="/css/app.css">
<link rel="stylesheet" href="/css/style.css">
<!-- Standalone-Fallback: Falls außerhalb von philippurbschat.de gehostet -->
<script>
window.addEventListener('error', function(e) {
if (e.target && e.target.tagName === 'LINK' && e.target.href.includes('/css/app.css')) {
const cdn = document.createElement('script');
cdn.src = 'https://cdn.tailwindcss.com';
document.head.appendChild(cdn);
}
}, true);
</script>
</head>
<body class="bg-slate-950 text-slate-200 min-h-screen font-sans flex flex-col antialiased selection:bg-fuchsia-500 selection:text-white">
<!-- Top Navigation Bar -->
<header class="border-b border-slate-800/80 bg-slate-900/50 backdrop-blur-md sticky top-0 z-50">
<div class="max-w-5xl mx-auto px-4 sm:px-6 py-3.5 flex items-center justify-between">
<div class="flex items-center gap-3">
<a href="/" class="text-xs font-mono text-slate-400 hover:text-cyan-400 transition-colors flex items-center gap-1.5 group">
<span class="group-hover:-translate-x-0.5 transition-transform">←</span> philippurbschat.de
</a>
<span class="text-slate-700">/</span>
<span class="text-xs font-mono text-slate-300 font-medium"><?= htmlspecialchars(basename(__DIR__)) ?></span>
</div>
<div class="flex items-center gap-3">
<span class="text-[11px] font-mono px-2 py-0.5 rounded-full border border-cyan-500/30 bg-cyan-500/10 text-cyan-400 uppercase tracking-wider">
<?= htmlspecialchars($moduleBadge) ?>
</span>
<span class="text-[11px] font-mono text-slate-500 hidden sm:inline-block">
<?= htmlspecialchars($currentUser) ?>
</span>
</div>
</div>
</header>
<!-- Main Content Container -->
<main class="grow max-w-5xl mx-auto px-4 sm:px-6 py-10 w-full">
<!-- Header Section -->
<div class="mb-10">
<div class="inline-flex items-center gap-2 mb-3">
<span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
<span class="text-xs font-mono uppercase tracking-[0.2em] text-emerald-400">System Ready</span>
</div>
<h1 class="text-3xl sm:text-4xl font-bold font-mono tracking-tight text-white mb-2">
<?= htmlspecialchars($moduleName) ?>
</h1>
<p class="text-slate-400 text-sm max-w-2xl">
<?= htmlspecialchars($moduleDesc) ?>
</p>
</div>
<!-- App Sandbox Card -->
<div class="bg-slate-900/40 border border-slate-800 rounded-2xl p-6 sm:p-8 backdrop-blur shadow-2xl relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-cyan-500/30 to-transparent"></div>
<div class="space-y-6">
<div>
<h2 class="text-xs font-mono uppercase tracking-widest text-slate-400 mb-2">Modul Workspace</h2>
<p class="text-sm text-slate-300 leading-relaxed">
Hier startet dein neues Mini-Projekt. Du hast vollen Zugriff auf Tailwind CSS, den zentralen Gemini API-Key und das Dark-Design der Hauptseite.
</p>
</div>
<!-- Interaktiver AI-Testbereich -->
<div class="pt-4 border-t border-slate-800/80">
<label for="promptInput" class="block text-xs font-mono uppercase text-slate-400 mb-2">
Schnelltest: Gemini API Relay
</label>
<div class="flex flex-col sm:flex-row gap-3">
<input type="text" id="promptInput" placeholder="Stelle eine kurze Frage..."
value="Erkläre Quantencomputer in einem prägnanten Satz."
class="grow bg-slate-950/80 border border-slate-700/80 rounded-xl px-4 py-3 text-sm text-slate-200 placeholder:text-slate-600 focus:outline-none focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500 transition-all font-mono">
<button id="sendBtn" class="bg-cyan-500/10 hover:bg-cyan-500/20 text-cyan-400 border border-cyan-500/30 hover:border-cyan-500/60 font-mono text-xs uppercase tracking-wider px-6 py-3 rounded-xl transition-all duration-200 flex items-center justify-center gap-2 cursor-pointer active:scale-95">
<span>Ausführen</span> →
</button>
</div>
<!-- Output Area -->
<div id="outputContainer" class="hidden mt-4 p-4 rounded-xl bg-slate-950/90 border border-slate-800/90 font-mono text-xs text-slate-300 whitespace-pre-wrap leading-relaxed"></div>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="border-t border-slate-900 py-6 text-center text-xs font-mono text-slate-600">
<?= htmlspecialchars($moduleName) ?> &bull; Philipp Urbschat Lab
</footer>
<script>
const sendBtn = document.getElementById('sendBtn');
const promptInput = document.getElementById('promptInput');
const outputContainer = document.getElementById('outputContainer');
sendBtn.addEventListener('click', async () => {
const prompt = promptInput.value.trim();
if (!prompt) return;
sendBtn.disabled = true;
sendBtn.innerHTML = 'Verarbeite...';
outputContainer.classList.remove('hidden');
outputContainer.textContent = 'Verbinde mit Gemini API...';
try {
const res = await fetch('api.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: prompt })
});
const data = await res.json();
if (data.error) {
outputContainer.textContent = 'Fehler: ' + data.error;
outputContainer.classList.add('text-rose-400');
} else {
outputContainer.classList.remove('text-rose-400');
const reply = data.candidates?.[0]?.content?.parts?.[0]?.text || JSON.stringify(data, null, 2);
outputContainer.textContent = reply;
}
} catch (err) {
outputContainer.textContent = 'Netzwerkfehler: ' + err.message;
outputContainer.classList.add('text-rose-400');
} finally {
sendBtn.disabled = false;
sendBtn.innerHTML = '<span>Ausführen</span> →';
}
});
</script>
</body>
</html>