Email Sorter Beta
Ich habe soweit automatisiert the Emails sortieren aber ich muss noch schauen was es fur bugs es gibt wenn die app online ist deswegen wurde ich mit diesen Commit die website veroffentlichen obwohjl es sein konnte das es noch nicht fertig ist und verkaufs bereit
This commit is contained in:
@@ -3,258 +3,699 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Email Sortierer</title>
|
||||
<title>EmailSorter API</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-dark: #0a0a0f;
|
||||
--bg-card: #12121a;
|
||||
--bg-hover: #1a1a25;
|
||||
--border: #2a2a3a;
|
||||
--text: #e4e4e7;
|
||||
--text-muted: #71717a;
|
||||
--accent: #6366f1;
|
||||
--accent-glow: rgba(99, 102, 241, 0.3);
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--error: #ef4444;
|
||||
--get: #22c55e;
|
||||
--post: #3b82f6;
|
||||
--delete: #ef4444;
|
||||
--patch: #f59e0b;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
background: var(--bg-dark);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Animated background */
|
||||
.bg-pattern {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background:
|
||||
radial-gradient(circle at 20% 20%, var(--accent-glow) 0%, transparent 40%),
|
||||
radial-gradient(circle at 80% 80%, rgba(16, 185, 129, 0.15) 0%, transparent 40%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
text-align: center;
|
||||
padding: 4rem 0 3rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: linear-gradient(135deg, var(--accent), #8b5cf6);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2rem;
|
||||
box-shadow: 0 0 40px var(--accent-glow);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--text), var(--accent));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--text-muted);
|
||||
font-size: 1.1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Status Cards */
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.status-card:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.status-card h3 {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-dot.healthy { background: var(--success); }
|
||||
.status-dot.warning { background: var(--warning); }
|
||||
.status-dot.error { background: var(--error); }
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* API Endpoints */
|
||||
.section-title {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.section-title::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 24px;
|
||||
background: var(--accent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.endpoint-group {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.endpoint-group-title {
|
||||
font-size: 1rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 1rem;
|
||||
border-left: 2px solid var(--border);
|
||||
}
|
||||
|
||||
.endpoint {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 0.75rem;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.endpoint:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.endpoint-header {
|
||||
padding: 1rem 1.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.method {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 6px;
|
||||
min-width: 70px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.method.get { background: rgba(34, 197, 94, 0.2); color: var(--get); }
|
||||
.method.post { background: rgba(59, 130, 246, 0.2); color: var(--post); }
|
||||
.method.delete { background: rgba(239, 68, 68, 0.2); color: var(--delete); }
|
||||
.method.patch { background: rgba(245, 158, 11, 0.2); color: var(--patch); }
|
||||
|
||||
.endpoint-path {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.9rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.endpoint-desc {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.endpoint-details {
|
||||
display: none;
|
||||
padding: 1rem 1.25rem;
|
||||
background: var(--bg-hover);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.endpoint.open .endpoint-details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.endpoint-details pre {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
background: var(--bg-dark);
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.endpoint-details h4 {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.endpoint-details h4:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Features */
|
||||
.features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 3rem 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Loading animation */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--border);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Try button */
|
||||
.try-btn {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.try-btn:hover {
|
||||
background: #4f46e5;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<h1>Email Sortierer</h1>
|
||||
<div id="form-container"></div>
|
||||
<div id="navigation">
|
||||
<button id="prev-btn" style="display:none;">Zurück</button>
|
||||
<button id="next-btn" style="display:none;">Weiter</button>
|
||||
<div class="bg-pattern"></div>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<div class="logo">
|
||||
<div class="logo-icon">📧</div>
|
||||
<div>
|
||||
<h1>EmailSorter API</h1>
|
||||
<p class="subtitle">KI-gestützte E-Mail-Sortierung</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Status Cards -->
|
||||
<div class="status-grid">
|
||||
<div class="status-card">
|
||||
<h3>Status</h3>
|
||||
<div class="status-value">
|
||||
<span class="status-dot healthy" id="status-dot"></span>
|
||||
<span id="status-text">Wird geladen...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<h3>Version</h3>
|
||||
<div class="status-value" id="version">-</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<h3>Uptime</h3>
|
||||
<div class="status-value" id="uptime">-</div>
|
||||
</div>
|
||||
<div class="status-card">
|
||||
<h3>Environment</h3>
|
||||
<div class="status-value" id="environment">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="summary" style="display:none;">
|
||||
<h2>Zusammenfassung</h2>
|
||||
<div id="summary-content"></div>
|
||||
<button id="buy-btn">Jetzt kaufen</button>
|
||||
|
||||
<!-- API Endpoints -->
|
||||
<h2 class="section-title">API Endpoints</h2>
|
||||
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-group-title">🔧 System</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<div class="endpoint-header" onclick="toggleEndpoint(this)">
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint-path">/api/health</span>
|
||||
<span class="endpoint-desc">Server Status prüfen</span>
|
||||
<button class="try-btn" onclick="event.stopPropagation(); tryEndpoint('/api/health')">Testen</button>
|
||||
</div>
|
||||
<div class="endpoint-details">
|
||||
<h4>Response</h4>
|
||||
<pre id="health-response">{ "success": true, "data": { "status": "healthy", ... } }</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<div class="endpoint-header" onclick="toggleEndpoint(this)">
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint-path">/api/config</span>
|
||||
<span class="endpoint-desc">Öffentliche Konfiguration</span>
|
||||
<button class="try-btn" onclick="event.stopPropagation(); tryEndpoint('/api/config')">Testen</button>
|
||||
</div>
|
||||
<div class="endpoint-details">
|
||||
<h4>Response</h4>
|
||||
<pre id="config-response">Klicke auf "Testen"</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-group-title">🔐 OAuth</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<div class="endpoint-header" onclick="toggleEndpoint(this)">
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint-path">/api/oauth/status</span>
|
||||
<span class="endpoint-desc">OAuth Provider Status</span>
|
||||
<button class="try-btn" onclick="event.stopPropagation(); tryEndpoint('/api/oauth/status')">Testen</button>
|
||||
</div>
|
||||
<div class="endpoint-details">
|
||||
<h4>Response</h4>
|
||||
<pre id="oauth-response">Klicke auf "Testen"</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<div class="endpoint-header" onclick="toggleEndpoint(this)">
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint-path">/api/oauth/gmail/connect</span>
|
||||
<span class="endpoint-desc">Gmail OAuth starten</span>
|
||||
</div>
|
||||
<div class="endpoint-details">
|
||||
<h4>Query Parameter</h4>
|
||||
<pre>userId: string (required)</pre>
|
||||
<h4>Response</h4>
|
||||
<pre>{ "success": true, "data": { "url": "https://accounts.google.com/..." } }</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<div class="endpoint-header" onclick="toggleEndpoint(this)">
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint-path">/api/oauth/outlook/connect</span>
|
||||
<span class="endpoint-desc">Outlook OAuth starten</span>
|
||||
</div>
|
||||
<div class="endpoint-details">
|
||||
<h4>Query Parameter</h4>
|
||||
<pre>userId: string (required)</pre>
|
||||
<h4>Response</h4>
|
||||
<pre>{ "success": true, "data": { "url": "https://login.microsoftonline.com/..." } }</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-group-title">📬 E-Mail</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<div class="endpoint-header" onclick="toggleEndpoint(this)">
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint-path">/api/email/accounts</span>
|
||||
<span class="endpoint-desc">Verbundene E-Mail-Konten</span>
|
||||
</div>
|
||||
<div class="endpoint-details">
|
||||
<h4>Query Parameter</h4>
|
||||
<pre>userId: string (required)</pre>
|
||||
<h4>Response</h4>
|
||||
<pre>{
|
||||
"success": true,
|
||||
"data": [
|
||||
{ "id": "...", "email": "user@gmail.com", "provider": "gmail", "connected": true }
|
||||
]
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<div class="endpoint-header" onclick="toggleEndpoint(this)">
|
||||
<span class="method post">POST</span>
|
||||
<span class="endpoint-path">/api/email/sort</span>
|
||||
<span class="endpoint-desc">E-Mails sortieren (KI)</span>
|
||||
</div>
|
||||
<div class="endpoint-details">
|
||||
<h4>Request Body</h4>
|
||||
<pre>{
|
||||
"userId": "string (required)",
|
||||
"accountId": "string (required)",
|
||||
"maxEmails": 50
|
||||
}</pre>
|
||||
<h4>Response</h4>
|
||||
<pre>{
|
||||
"success": true,
|
||||
"data": {
|
||||
"sorted": 42,
|
||||
"categories": { "vip": 5, "newsletters": 20, ... },
|
||||
"timeSaved": { "minutes": 10, "formatted": "10 Minuten" }
|
||||
}
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<div class="endpoint-header" onclick="toggleEndpoint(this)">
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint-path">/api/email/stats</span>
|
||||
<span class="endpoint-desc">Sortier-Statistiken</span>
|
||||
</div>
|
||||
<div class="endpoint-details">
|
||||
<h4>Query Parameter</h4>
|
||||
<pre>userId: string (required)</pre>
|
||||
<h4>Response</h4>
|
||||
<pre>{
|
||||
"success": true,
|
||||
"data": {
|
||||
"totalSorted": 1250,
|
||||
"todaySorted": 45,
|
||||
"weekSorted": 312,
|
||||
"timeSaved": 312
|
||||
}
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<div class="endpoint-header" onclick="toggleEndpoint(this)">
|
||||
<span class="method delete">DELETE</span>
|
||||
<span class="endpoint-path">/api/email/accounts/:accountId</span>
|
||||
<span class="endpoint-desc">E-Mail-Konto trennen</span>
|
||||
</div>
|
||||
<div class="endpoint-details">
|
||||
<h4>Query Parameter</h4>
|
||||
<pre>userId: string (required)</pre>
|
||||
<h4>Response</h4>
|
||||
<pre>{ "success": true, "message": "Konto erfolgreich getrennt" }</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint-group">
|
||||
<div class="endpoint-group-title">💳 Subscription</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<div class="endpoint-header" onclick="toggleEndpoint(this)">
|
||||
<span class="method post">POST</span>
|
||||
<span class="endpoint-path">/api/subscription/checkout</span>
|
||||
<span class="endpoint-desc">Checkout Session erstellen</span>
|
||||
</div>
|
||||
<div class="endpoint-details">
|
||||
<h4>Request Body</h4>
|
||||
<pre>{
|
||||
"userId": "string (required)",
|
||||
"plan": "basic | pro | business (required)",
|
||||
"email": "string (optional)"
|
||||
}</pre>
|
||||
<h4>Response</h4>
|
||||
<pre>{ "success": true, "data": { "url": "https://checkout.stripe.com/...", "sessionId": "..." } }</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<div class="endpoint-header" onclick="toggleEndpoint(this)">
|
||||
<span class="method get">GET</span>
|
||||
<span class="endpoint-path">/api/subscription/status</span>
|
||||
<span class="endpoint-desc">Subscription Status</span>
|
||||
</div>
|
||||
<div class="endpoint-details">
|
||||
<h4>Query Parameter</h4>
|
||||
<pre>userId: string (required)</pre>
|
||||
<h4>Response</h4>
|
||||
<pre>{
|
||||
"success": true,
|
||||
"data": {
|
||||
"status": "active",
|
||||
"plan": "pro",
|
||||
"features": { "emailAccounts": 3, ... },
|
||||
"currentPeriodEnd": "2026-02-15T00:00:00Z"
|
||||
}
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<div class="endpoint-header" onclick="toggleEndpoint(this)">
|
||||
<span class="method post">POST</span>
|
||||
<span class="endpoint-path">/api/subscription/portal</span>
|
||||
<span class="endpoint-desc">Stripe Kundenportal</span>
|
||||
</div>
|
||||
<div class="endpoint-details">
|
||||
<h4>Request Body</h4>
|
||||
<pre>{ "userId": "string (required)" }</pre>
|
||||
<h4>Response</h4>
|
||||
<pre>{ "success": true, "data": { "url": "https://billing.stripe.com/..." } }</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="endpoint">
|
||||
<div class="endpoint-header" onclick="toggleEndpoint(this)">
|
||||
<span class="method post">POST</span>
|
||||
<span class="endpoint-path">/api/subscription/cancel</span>
|
||||
<span class="endpoint-desc">Subscription kündigen</span>
|
||||
</div>
|
||||
<div class="endpoint-details">
|
||||
<h4>Request Body</h4>
|
||||
<pre>{ "userId": "string (required)" }</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
<h2 class="section-title">Features</h2>
|
||||
<div class="features">
|
||||
<div class="feature-card">
|
||||
<h3>🤖 Mistral AI</h3>
|
||||
<p>Intelligente E-Mail-Kategorisierung mit modernster KI-Technologie.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>📧 Multi-Provider</h3>
|
||||
<p>Unterstützt Gmail und Outlook mit OAuth 2.0 Authentifizierung.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>💳 Stripe Billing</h3>
|
||||
<p>Sichere Zahlungsabwicklung mit Subscriptions und Kundenportal.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>🔒 Rate Limiting</h3>
|
||||
<p>Schutz vor Missbrauch mit intelligenten Request-Limits.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>📊 Statistiken</h3>
|
||||
<p>Detaillierte Einblicke in sortierte E-Mails und gesparte Zeit.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>🌐 Webhooks</h3>
|
||||
<p>Real-time Updates für Gmail und Outlook Benachrichtigungen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>EmailSorter API v2.0.0 • <a href="/api/health">Health Check</a> • Powered by Node.js + Express</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let questions = [];
|
||||
let answers = {};
|
||||
let currentStep = 1;
|
||||
let submissionId = null;
|
||||
|
||||
async function loadQuestions() {
|
||||
// Load server status
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const response = await fetch('/api/questions?productSlug=email-sorter');
|
||||
questions = await response.json();
|
||||
renderStep();
|
||||
} catch (error) {
|
||||
console.error('Error loading questions:', error);
|
||||
const res = await fetch('/api/health');
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
document.getElementById('status-text').textContent = 'Online';
|
||||
document.getElementById('status-dot').className = 'status-dot healthy';
|
||||
document.getElementById('version').textContent = data.data.version;
|
||||
document.getElementById('uptime').textContent = formatUptime(data.data.uptime);
|
||||
document.getElementById('environment').textContent = data.data.environment;
|
||||
document.getElementById('health-response').textContent = JSON.stringify(data, null, 2);
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('status-text').textContent = 'Offline';
|
||||
document.getElementById('status-dot').className = 'status-dot error';
|
||||
}
|
||||
}
|
||||
|
||||
function renderStep() {
|
||||
const container = document.getElementById('form-container');
|
||||
const stepQuestions = questions.filter(q => q.step === currentStep);
|
||||
function formatUptime(seconds) {
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
|
||||
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
||||
return `${Math.floor(seconds / 86400)}d ${Math.floor((seconds % 86400) / 3600)}h`;
|
||||
}
|
||||
|
||||
function toggleEndpoint(header) {
|
||||
header.parentElement.classList.toggle('open');
|
||||
}
|
||||
|
||||
async function tryEndpoint(path) {
|
||||
const responseId = path.replace('/api/', '').replace('/', '-') + '-response';
|
||||
const pre = document.getElementById(responseId) || document.querySelector('.endpoint.open pre');
|
||||
|
||||
if (stepQuestions.length === 0) {
|
||||
showSummary();
|
||||
return;
|
||||
if (pre) {
|
||||
pre.textContent = 'Wird geladen...';
|
||||
}
|
||||
|
||||
container.innerHTML = `<h2>Schritt ${currentStep}</h2>`;
|
||||
|
||||
stepQuestions.forEach(question => {
|
||||
const div = document.createElement('div');
|
||||
div.style.marginBottom = '20px';
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.textContent = question.label + (question.required ? ' *' : '');
|
||||
label.style.display = 'block';
|
||||
label.style.marginBottom = '5px';
|
||||
div.appendChild(label);
|
||||
|
||||
if (question.helpText) {
|
||||
const help = document.createElement('small');
|
||||
help.textContent = question.helpText;
|
||||
help.style.display = 'block';
|
||||
help.style.marginBottom = '5px';
|
||||
div.appendChild(help);
|
||||
}
|
||||
|
||||
let input;
|
||||
switch (question.type) {
|
||||
case 'textarea':
|
||||
input = document.createElement('textarea');
|
||||
input.rows = 4;
|
||||
break;
|
||||
case 'select':
|
||||
input = document.createElement('select');
|
||||
let options = [];
|
||||
try {
|
||||
const parsed = JSON.parse(question.optionsJson || '[]');
|
||||
// Handle both array format and {options: [...]} format
|
||||
options = Array.isArray(parsed) ? parsed : (parsed.options || []);
|
||||
} catch (e) {
|
||||
console.error('Error parsing options:', e);
|
||||
options = [];
|
||||
}
|
||||
options.forEach(opt => {
|
||||
const option = document.createElement('option');
|
||||
// Handle both string and {value, label} format
|
||||
option.value = typeof opt === 'string' ? opt : opt.value;
|
||||
option.textContent = typeof opt === 'string' ? opt : opt.label;
|
||||
input.appendChild(option);
|
||||
});
|
||||
break;
|
||||
case 'multiselect':
|
||||
input = document.createElement('select');
|
||||
input.multiple = true;
|
||||
input.size = 5;
|
||||
let multiOptions = [];
|
||||
try {
|
||||
const parsed = JSON.parse(question.optionsJson || '[]');
|
||||
// Handle both array format and {options: [...]} format
|
||||
multiOptions = Array.isArray(parsed) ? parsed : (parsed.options || []);
|
||||
} catch (e) {
|
||||
console.error('Error parsing options:', e);
|
||||
multiOptions = [];
|
||||
}
|
||||
multiOptions.forEach(opt => {
|
||||
const option = document.createElement('option');
|
||||
// Handle both string and {value, label} format
|
||||
option.value = typeof opt === 'string' ? opt : opt.value;
|
||||
option.textContent = typeof opt === 'string' ? opt : opt.label;
|
||||
input.appendChild(option);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
input = document.createElement('input');
|
||||
input.type = question.type;
|
||||
}
|
||||
|
||||
input.id = question.key;
|
||||
input.name = question.key;
|
||||
input.required = question.required;
|
||||
|
||||
// Restore previous values
|
||||
if (question.type === 'multiselect' && Array.isArray(answers[question.key])) {
|
||||
// For multiselect, select all previously selected options
|
||||
Array.from(input.options).forEach(option => {
|
||||
if (answers[question.key].includes(option.value)) {
|
||||
option.selected = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
input.value = answers[question.key] || '';
|
||||
}
|
||||
|
||||
input.style.width = '100%';
|
||||
input.style.padding = '8px';
|
||||
|
||||
div.appendChild(input);
|
||||
container.appendChild(div);
|
||||
});
|
||||
|
||||
updateNavigation();
|
||||
}
|
||||
|
||||
function updateNavigation() {
|
||||
const prevBtn = document.getElementById('prev-btn');
|
||||
const nextBtn = document.getElementById('next-btn');
|
||||
|
||||
prevBtn.style.display = currentStep > 1 ? 'inline-block' : 'none';
|
||||
nextBtn.style.display = 'inline-block';
|
||||
nextBtn.textContent = hasMoreSteps() ? 'Weiter' : 'Zur Zusammenfassung';
|
||||
}
|
||||
|
||||
function hasMoreSteps() {
|
||||
const maxStep = Math.max(...questions.map(q => q.step));
|
||||
return currentStep < maxStep;
|
||||
}
|
||||
|
||||
function validateCurrentStep() {
|
||||
const stepQuestions = questions.filter(q => q.step === currentStep);
|
||||
|
||||
for (const question of stepQuestions) {
|
||||
const input = document.getElementById(question.key);
|
||||
if (question.required) {
|
||||
// For multiselect, check if at least one option is selected
|
||||
if (question.type === 'multiselect') {
|
||||
if (input.selectedOptions.length === 0) {
|
||||
alert(`Bitte wählen Sie mindestens eine Option für "${question.label}" aus.`);
|
||||
return false;
|
||||
}
|
||||
} else if (!input.value) {
|
||||
alert(`Bitte füllen Sie das Feld "${question.label}" aus.`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function saveCurrentStep() {
|
||||
const stepQuestions = questions.filter(q => q.step === currentStep);
|
||||
stepQuestions.forEach(question => {
|
||||
const input = document.getElementById(question.key);
|
||||
if (question.type === 'multiselect') {
|
||||
answers[question.key] = Array.from(input.selectedOptions).map(opt => opt.value);
|
||||
} else {
|
||||
answers[question.key] = input.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showSummary() {
|
||||
document.getElementById('form-container').style.display = 'none';
|
||||
document.getElementById('navigation').style.display = 'none';
|
||||
document.getElementById('summary').style.display = 'block';
|
||||
|
||||
const summaryContent = document.getElementById('summary-content');
|
||||
summaryContent.innerHTML = '';
|
||||
|
||||
questions.forEach(question => {
|
||||
const div = document.createElement('div');
|
||||
div.style.marginBottom = '10px';
|
||||
div.innerHTML = `<strong>${question.label}:</strong> ${formatAnswer(answers[question.key])}`;
|
||||
summaryContent.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function formatAnswer(answer) {
|
||||
if (Array.isArray(answer)) {
|
||||
return answer.join(', ');
|
||||
}
|
||||
return answer || '-';
|
||||
}
|
||||
|
||||
document.getElementById('prev-btn').addEventListener('click', () => {
|
||||
saveCurrentStep();
|
||||
currentStep--;
|
||||
renderStep();
|
||||
});
|
||||
|
||||
document.getElementById('next-btn').addEventListener('click', () => {
|
||||
if (!validateCurrentStep()) return;
|
||||
|
||||
saveCurrentStep();
|
||||
currentStep++;
|
||||
renderStep();
|
||||
});
|
||||
|
||||
document.getElementById('buy-btn').addEventListener('click', async () => {
|
||||
try {
|
||||
const submitResponse = await fetch('/api/submissions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
productSlug: 'email-sorter',
|
||||
answers: answers
|
||||
})
|
||||
});
|
||||
|
||||
const submitData = await submitResponse.json();
|
||||
submissionId = submitData.submissionId;
|
||||
|
||||
const checkoutResponse = await fetch('/api/checkout', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ submissionId })
|
||||
});
|
||||
|
||||
const checkoutData = await checkoutResponse.json();
|
||||
window.location.href = checkoutData.url;
|
||||
} catch (error) {
|
||||
console.error('Error during checkout:', error);
|
||||
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
|
||||
const res = await fetch(path);
|
||||
const data = await res.json();
|
||||
if (pre) {
|
||||
pre.textContent = JSON.stringify(data, null, 2);
|
||||
}
|
||||
} catch (e) {
|
||||
if (pre) {
|
||||
pre.textContent = `Error: ${e.message}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadQuestions();
|
||||
// Initial load
|
||||
loadStatus();
|
||||
|
||||
// Refresh every 30 seconds
|
||||
setInterval(loadStatus, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user