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:
-15
@@ -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>
|
|
||||||
Generated
-1719
File diff suppressed because it is too large
Load Diff
-53
@@ -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
@@ -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"),
|
|
||||||
};
|
|
||||||
@@ -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 />);
|
|
||||||
@@ -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; }
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user