cleanup: obsolete Frontend-Reste entfernt (src/, index.html, vite.config.js, stale lock)

git rm im vorigen Commit brach am fehlenden marketing/ ab, daher waren die
React-Dateien noch im Repo. RAPPORT-HOST ist jetzt wirklich reines Backend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-30 16:45:09 +02:00
parent 13173dddc5
commit 2755b3b293
12 changed files with 0 additions and 2178 deletions
-15
View File
@@ -1,15 +0,0 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RAPPORT — Hosting für Architekturbüros</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
-1719
View File
File diff suppressed because it is too large Load Diff
-53
View File
@@ -1,53 +0,0 @@
import React, { useState, useEffect, useCallback } from "react";
import { auth } from "./api.js";
import Register from "./views/Register.jsx";
import Login from "./views/Login.jsx";
import Plans from "./views/Plans.jsx";
import Dashboard from "./views/Dashboard.jsx";
// Die App wird unter /app/ ausgeliefert (Hugo-Marketing liegt unter /).
// Der Mini-Router rechnet intern in App-relativen Pfaden ("/login"),
// schreibt aber immer mit BASE-Präfix in die URL ("/app/login").
const BASE = "/app";
// Interne App-Routen. Alles andere, was navigate() bekommt (z.B. "/preise/"),
// ist ein Marketing-Link und wird als echter Seitenwechsel behandelt.
const ROUTES = ["/login", "/register", "/plans", "/dashboard"];
function toRel(pathname) {
const p = pathname.startsWith(BASE) ? pathname.slice(BASE.length) : pathname;
const clean = (p.replace(/\/$/, "") || "/");
return ROUTES.includes(clean) ? clean : "/";
}
function useRoute() {
const [route, setRoute] = useState(toRel(window.location.pathname));
useEffect(() => {
const onPop = () => setRoute(toRel(window.location.pathname));
window.addEventListener("popstate", onPop);
return () => window.removeEventListener("popstate", onPop);
}, []);
const navigate = useCallback((to) => {
if (!ROUTES.includes(to)) { window.location.href = to; return; } // Marketing/extern
window.history.pushState({}, "", BASE + to);
setRoute(to);
window.scrollTo(0, 0);
}, []);
return [route, navigate];
}
export default function App() {
const [route, navigate] = useRoute();
const loggedIn = auth.isLoggedIn;
// Geschützte/öffentliche Weiterleitungen. "/" (App-Einstieg) → je nach Login.
useEffect(() => {
if (route === "/dashboard" && !loggedIn) navigate("/login");
else if ((route === "/login" || route === "/register" || route === "/") && loggedIn) navigate("/dashboard");
else if (route === "/" && !loggedIn) navigate("/login");
}, [route, loggedIn, navigate]);
if (route === "/register") return <Register navigate={navigate} />;
if (route === "/plans") return <Plans navigate={navigate} />;
if (route === "/dashboard") return <Dashboard navigate={navigate} />;
return <Login navigate={navigate} />;
}
-33
View File
@@ -1,33 +0,0 @@
// Schmaler API-Client. Token im localStorage; /api wird im Dev geproxt.
const TOKEN_KEY = "rapport_host_token";
export const auth = {
get: () => localStorage.getItem(TOKEN_KEY),
set: (t) => localStorage.setItem(TOKEN_KEY, t),
clear: () => localStorage.removeItem(TOKEN_KEY),
get isLoggedIn() {
return !!localStorage.getItem(TOKEN_KEY);
},
};
async function req(method, path, body) {
const headers = { "Content-Type": "application/json" };
const token = auth.get();
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(`/api${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || `Fehler ${res.status}`);
return data;
}
export const api = {
register: (email, password) => req("POST", "/auth/register", { email, password }),
login: (email, password) => req("POST", "/auth/login", { email, password }),
plans: () => req("GET", "/billing/plans"),
checkout: (planId) => req("POST", "/billing/checkout", { planId }),
me: () => req("GET", "/account/me"),
};
-6
View File
@@ -1,6 +0,0 @@
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./styles.css";
createRoot(document.getElementById("root")).render(<App />);
-94
View File
@@ -1,94 +0,0 @@
/* Design-Tokens 1:1 aus RAPPORT-WEBSITE (assets/css/custom.css):
warmes Beige-Grau als Grund, Krungthep-Brand, Inter-Body, Braun-Akzent. */
@font-face {
font-family: "Krungthep";
src: url("/fonts/Krungthep.ttf") format("truetype");
font-weight: normal;
font-style: normal;
font-display: swap;
}
:root {
--bg: #ece9e3; /* warmes Beige-Grau (RAPPORT-WEBSITE) */
--card: #f0ede8; /* Karten / leicht heller */
--card-soft: #f5f2ee;
--ink: #2d2926; /* warmes Near-Black */
--muted: #6b645c;
--faint: #9b938a;
--line: #d9d4cc; /* Border */
--line-soft: #e4dfd7;
--accent: #b07848; /* Braun-Akzent */
--accent-hover: #9a6539;
--accent-ink: #ffffff;
--font-sans: Inter, -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
--brand-font: Krungthep, "Archivo Black", sans-serif;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: var(--font-sans);
font-size: 16px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
.wrap { max-width: 1040px; margin: 0 auto; padding: 0 24px; }
.center { min-height: 100vh; display: flex; align-items: center; justify-content: center; }
.brand { font-family: var(--brand-font); font-size: 28px; letter-spacing: -0.02em; color: var(--ink); }
.card {
background: var(--card);
border: 1px solid var(--line);
border-radius: 14px;
padding: 32px;
box-shadow: 0 1px 2px rgba(45,41,38,0.04), 0 8px 28px rgba(45,41,38,0.06);
}
.card-sm { width: 100%; max-width: 400px; }
label { display: block; font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); margin-bottom: 7px; }
input {
width: 100%; background: #fdfcfa; border: 1.5px solid var(--line);
border-radius: 9px; padding: 11px 14px; font-family: var(--font-sans); font-size: 15px;
color: var(--ink); outline: none; margin-bottom: 16px; transition: border-color .15s, box-shadow .15s;
}
input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(176,120,72,0.15); }
button.primary {
width: 100%; padding: 12px; background: var(--accent); color: var(--accent-ink);
border: none; border-radius: 9px; font-family: var(--font-sans); font-size: 15px;
font-weight: 600; cursor: pointer; transition: background .15s;
}
button.primary:hover { background: var(--accent-hover); }
button.dark { background: var(--ink); }
button.dark:hover { background: #1f1c19; }
button.ghost {
background: none; border: none; color: var(--muted);
font-family: var(--font-sans); font-size: 14px; cursor: pointer;
}
button.ghost:hover { color: var(--ink); }
.err { background: #f8ece4; border: 1px solid #e0b896; color: #9a4a1e;
padding: 10px 14px; border-radius: 8px; font-size: 14px; margin-bottom: 16px; }
.ok { background: #ecefe6; border: 1px solid #c2cdb0; color: #4a5a32;
padding: 10px 14px; border-radius: 8px; font-size: 14px; margin-bottom: 16px; }
.nav { display: flex; justify-content: space-between; align-items: center; padding: 22px 0; border-bottom: 1px solid var(--line-soft); }
.muted { color: var(--muted); }
.plan-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 18px; }
.plan { position: relative; display: flex; flex-direction: column; }
.plan.rec { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), 0 8px 28px rgba(176,120,72,0.14); }
.plan .badge { position: absolute; top: -11px; right: 18px; background: var(--accent);
color: #fff; font-size: 10px; font-weight: 700; padding: 4px 10px; border-radius: 6px; letter-spacing: 0.06em; text-transform: uppercase; }
.plan .price { font-size: 32px; font-weight: 700; margin: 10px 0; letter-spacing: -0.02em; }
.plan ul { list-style: none; padding: 0; margin: 8px 0 0; font-size: 14px; color: var(--muted); flex: 1; }
.plan li { padding: 7px 0; border-bottom: 1px solid var(--line-soft); }
.plan li:last-child { border-bottom: none; }
/* Hero — angelehnt an die Hextra-Home der RAPPORT-WEBSITE */
.hero { padding: 88px 0 72px; max-width: 680px; }
.hero h1 { font-family: var(--brand-font); font-size: 52px; line-height: 1.05; letter-spacing: -0.02em; margin: 0; color: var(--ink); }
.hero p { font-size: 17px; line-height: 1.6; color: var(--muted); margin-top: 22px; }
-65
View File
@@ -1,65 +0,0 @@
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>
);
}
-41
View File
@@ -1,41 +0,0 @@
import React from "react";
import { auth } from "../api.js";
export default function Landing({ navigate }) {
return (
<div className="wrap">
<div className="nav">
<div className="brand">RAPPORT</div>
<div style={{ display: "flex", gap: 16, alignItems: "center" }}>
<button className="ghost" onClick={() => navigate("/plans")}>Preise</button>
{auth.isLoggedIn ? (
<button className="primary" style={{ width: "auto", padding: "8px 18px" }} onClick={() => navigate("/dashboard")}>
Dashboard
</button>
) : (
<button className="primary" style={{ width: "auto", padding: "8px 18px" }} onClick={() => navigate("/login")}>
Anmelden
</button>
)}
</div>
</div>
<div className="hero">
<h1>Ihre eigene Rapport-Instanz.<br />In Minuten startklar.</h1>
<p>
Studio-Management für Architekturbüros gehostet, gewartet und
gesichert. Registrieren, Abo wählen, loslegen. Ihre Daten in der
Schweiz.
</p>
<div style={{ display: "flex", gap: 12, marginTop: 30 }}>
<button className="primary" style={{ width: "auto", padding: "13px 28px" }} onClick={() => navigate("/register")}>
Jetzt starten
</button>
<button className="primary dark" style={{ width: "auto", padding: "13px 28px", background: "transparent", color: "var(--ink)", border: "1px solid var(--line)" }} onClick={() => navigate("/plans")}>
Preise ansehen
</button>
</div>
</div>
</div>
);
}
-41
View File
@@ -1,41 +0,0 @@
import React, { useState } from "react";
import { api, auth } from "../api.js";
export default function Login({ navigate }) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [err, setErr] = useState("");
const [busy, setBusy] = useState(false);
const submit = async (e) => {
e.preventDefault();
setErr(""); setBusy(true);
try {
const { token } = await api.login(email.trim(), password);
auth.set(token);
navigate("/dashboard");
} catch (e) {
setErr(e.message);
} finally {
setBusy(false);
}
};
return (
<div className="center">
<form className="card card-sm" onSubmit={submit}>
<div className="brand" style={{ textAlign: "center", marginBottom: 24 }}>RAPPORT</div>
<div className="muted" style={{ fontSize: 11, textAlign: "center", marginBottom: 24 }}>Anmelden</div>
{err && <div className="err">{err}</div>}
<label>Email</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="name@studio.ch" autoFocus />
<label>Passwort</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="••••••" />
<button className="primary" disabled={busy}>{busy ? "…" : "Anmelden"}</button>
<div style={{ textAlign: "center", marginTop: 16, fontSize: 11 }}>
<button type="button" className="ghost" onClick={() => navigate("/register")}>Noch kein Konto? Registrieren</button>
</div>
</form>
</div>
);
}
-52
View File
@@ -1,52 +0,0 @@
import React, { useState, useEffect } from "react";
import { api, auth } from "../api.js";
export default function Plans({ navigate }) {
const [plans, setPlans] = useState([]);
const [err, setErr] = useState("");
const [busy, setBusy] = useState("");
useEffect(() => {
api.plans().then((d) => setPlans(d.plans)).catch((e) => setErr(e.message));
}, []);
const choose = async (planId) => {
if (!auth.isLoggedIn) { navigate("/register"); return; }
setErr(""); setBusy(planId);
try {
const { url } = await api.checkout(planId);
window.location.href = url; // Stripe-Checkout oder Mock-Erfolgs-URL
} catch (e) {
setErr(e.message); setBusy("");
}
};
return (
<div className="wrap">
<div className="nav">
<div className="brand" style={{ cursor: "pointer" }} onClick={() => navigate("/")}>RAPPORT</div>
<button className="ghost" onClick={() => navigate(auth.isLoggedIn ? "/dashboard" : "/login")}>
{auth.isLoggedIn ? "Dashboard" : "Anmelden"}
</button>
</div>
<h1 style={{ fontSize: 32 }}>Abo wählen</h1>
<p className="muted" style={{ marginBottom: 28 }}>Jederzeit kündbar. Preise in CHF, pro Monat, exkl. MwSt.</p>
{err && <div className="err">{err}</div>}
<div className="plan-grid">
{plans.map((p) => (
<div key={p.id} className={`card plan${p.recommended ? " rec" : ""}`}>
{p.recommended && <div className="badge">EMPFOHLEN</div>}
<div style={{ fontSize: 14, letterSpacing: "0.1em", textTransform: "uppercase" }}>{p.name}</div>
<div className="price">CHF {p.priceChf}<span className="muted" style={{ fontSize: 13 }}>/{p.interval}</span></div>
<ul>{p.features.map((f, i) => <li key={i}>{f}</li>)}</ul>
<button className="primary" style={{ marginTop: 16 }} disabled={busy === p.id} onClick={() => choose(p.id)}>
{busy === p.id ? "…" : "Wählen"}
</button>
</div>
))}
</div>
</div>
);
}
-43
View File
@@ -1,43 +0,0 @@
import React, { useState } from "react";
import { api, auth } from "../api.js";
export default function Register({ navigate }) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [err, setErr] = useState("");
const [busy, setBusy] = useState(false);
const submit = async (e) => {
e.preventDefault();
setErr(""); setBusy(true);
try {
const { token } = await api.register(email.trim(), password);
auth.set(token);
navigate("/plans");
} catch (e) {
setErr(e.message);
} finally {
setBusy(false);
}
};
return (
<div className="center">
<form className="card card-sm" onSubmit={submit}>
<div className="brand" style={{ textAlign: "center", marginBottom: 24 }}>RAPPORT</div>
<div className="muted" style={{ fontSize: 11, textAlign: "center", marginBottom: 24 }}>
Konto erstellen
</div>
{err && <div className="err">{err}</div>}
<label>Email</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="name@studio.ch" autoFocus />
<label>Passwort</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="min. 8 Zeichen" />
<button className="primary" disabled={busy}>{busy ? "…" : "Konto erstellen"}</button>
<div style={{ textAlign: "center", marginTop: 16, fontSize: 11 }}>
<button type="button" className="ghost" onClick={() => navigate("/login")}>Schon ein Konto? Anmelden</button>
</div>
</form>
</div>
);
}
-16
View File
@@ -1,16 +0,0 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// Die React-App wird in Produktion unter /app/ ausgeliefert (base), die
// Hugo-Marketing-Site liegt unter /. Im Dev läuft Vite auf :5273 und proxt
// /api → Backend, damit kein CORS nötig ist und die URLs identisch bleiben.
export default defineConfig({
base: "/app/",
plugins: [react()],
server: {
port: 5273,
proxy: {
"/api": "http://localhost:8787",
},
},
});