6290475ea3
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>
66 lines
2.7 KiB
React
66 lines
2.7 KiB
React
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>
|
|
);
|
|
}
|