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