Initial: RAPPORT-HOST Iteration 1 (proprietär)
Kommerzielle Hosting-/Abo-Plattform für Rapport-Instanzen. - React-Frontend (Vite/JSX): Landing, Register, Login, Plans, Dashboard - Node/Express-Backend: Auth (bcrypt+JWT), Stripe-Billing, Provisioning - HOST-Postgres-Schema: accounts, subscriptions, instances - Provisioning-Interface + Modell-A-Adapter (Studio im geteilten Stack) - MOCK-Modus: voller End-to-End-Flow ohne Stripe/Rapport-Stack testbar - Idempotentes Fulfillment (Upsert auf stripe_subscription_id) - docker-compose für lokale host-db; identisch auf Hetzner deploybar E2E lokal verifiziert: Register -> Checkout(mock) -> Instanz -> Idempotenz. Lizenz: proprietär (kein AGPL-Code eingebunden, nur Netzwerk-API zur Familie). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { api, auth } from "../api.js";
|
||||
|
||||
export default function Dashboard({ navigate }) {
|
||||
const [data, setData] = useState(null);
|
||||
const [err, setErr] = useState("");
|
||||
const justProvisioned = new URLSearchParams(window.location.search).get("provisioned") === "1";
|
||||
|
||||
useEffect(() => {
|
||||
api.me().then(setData).catch((e) => setErr(e.message));
|
||||
}, []);
|
||||
|
||||
const logout = () => { auth.clear(); navigate("/"); };
|
||||
|
||||
if (err) return <div className="center"><div className="card card-sm"><div className="err">{err}</div><button className="primary" onClick={logout}>Abmelden</button></div></div>;
|
||||
if (!data) return <div className="center"><div className="muted">Lädt…</div></div>;
|
||||
|
||||
const { account, subscription, instance } = data;
|
||||
|
||||
return (
|
||||
<div className="wrap">
|
||||
<div className="nav">
|
||||
<div className="brand" style={{ cursor: "pointer" }} onClick={() => navigate("/")}>RAPPORT</div>
|
||||
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
|
||||
<span className="muted" style={{ fontSize: 12 }}>{account.email}</span>
|
||||
<button className="ghost" onClick={logout}>Abmelden</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{justProvisioned && <div className="ok">Zahlung erfolgreich — Ihre Instanz wird bereitgestellt.</div>}
|
||||
|
||||
<h1 style={{ fontSize: 28 }}>Dashboard</h1>
|
||||
|
||||
<div className="card" style={{ marginTop: 16 }}>
|
||||
<label>Abo</label>
|
||||
{subscription ? (
|
||||
<div style={{ fontSize: 14 }}>
|
||||
Plan <b>{subscription.plan}</b> · Status <b>{subscription.status}</b>
|
||||
{subscription.current_period_end && (
|
||||
<span className="muted"> · läuft bis {new Date(subscription.current_period_end).toLocaleDateString("de-CH")}</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p className="muted" style={{ fontSize: 13 }}>Noch kein aktives Abo.</p>
|
||||
<button className="primary" style={{ width: "auto", padding: "10px 22px" }} onClick={() => navigate("/plans")}>Abo wählen</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{instance && (
|
||||
<div className="card" style={{ marginTop: 16 }}>
|
||||
<label>Ihre Instanz</label>
|
||||
<div style={{ fontSize: 14, marginBottom: 12 }}>
|
||||
<span className="muted">Status:</span> <b>{instance.status}</b>
|
||||
</div>
|
||||
<a className="primary" style={{ display: "inline-block", width: "auto", padding: "11px 24px", textDecoration: "none", textAlign: "center" }} href={instance.instance_url} target="_blank" rel="noreferrer">
|
||||
Rapport öffnen →
|
||||
</a>
|
||||
<div className="muted" style={{ fontSize: 11, marginTop: 10 }}>{instance.instance_url}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user