refactor: support prefixed webapp routes

This commit is contained in:
3252a8
2026-05-28 14:59:34 +03:00
parent 92f0277dad
commit 613a9860a0
9 changed files with 224 additions and 103 deletions
@@ -1,9 +1,11 @@
import { writable } from "svelte/store";
import { withRoutePrefix } from "../../webapp/routes.js";
export function createPaymentsStore({
api,
onToast = () => {},
at = (key, _params, fallback) => fallback || key,
routePrefix = "",
}) {
const state = writable({
payments: [],
@@ -25,7 +27,10 @@ export function createPaymentsStore({
function pushPaymentPath(paymentId) {
if (typeof window === "undefined" || window.location.protocol === "file:") return;
if (active !== "payments") return;
const target = paymentId ? `/admin/payments/${paymentId}` : "/admin/payments";
const target = withRoutePrefix(
paymentId ? `/admin/payments/${paymentId}` : "/admin/payments",
routePrefix
);
if (window.location.pathname === target) return;
window.history.pushState(null, "", `${target}${window.location.search}${window.location.hash}`);
}
@@ -1,6 +1,7 @@
import { writable } from "svelte/store";
import { withRoutePrefix } from "../../webapp/routes.js";
export function createAdminSupportStore({ api, onToast, at }) {
export function createAdminSupportStore({ api, onToast, at, routePrefix = "" }) {
const OPEN_TICKET_POLL_MS = 3_000;
const STATS_POLL_MS = 30_000;
const HIDDEN_POLL_MS = 300_000;
@@ -58,7 +59,10 @@ export function createAdminSupportStore({ api, onToast, at }) {
function pushTicketPath(ticketId) {
if (typeof window === "undefined" || window.location.protocol === "file:") return;
if (active !== "support") return;
const target = ticketId ? `/admin/support/${ticketId}` : "/admin/support";
const target = withRoutePrefix(
ticketId ? `/admin/support/${ticketId}` : "/admin/support",
routePrefix
);
if (window.location.pathname !== target) {
window.history.pushState(
null,
+3 -1
View File
@@ -1,6 +1,7 @@
import { writable } from "svelte/store";
import { withRoutePrefix } from "../../webapp/routes.js";
export function createUsersStore({ api, onToast, at }) {
export function createUsersStore({ api, onToast, at, routePrefix = "" }) {
const USERS_PAGE_SIZE = 25;
const USER_LOGS_PAGE_SIZE = 20;
@@ -70,6 +71,7 @@ export function createUsersStore({ api, onToast, at }) {
target = userId ? `/admin/payments/users/${userId}` : `/admin/payments`;
}
if (!target) return;
target = withRoutePrefix(target, routePrefix);
if (window.location.pathname === target) return;
window.history.pushState(null, "", `${target}${window.location.search}${window.location.hash}`);
}
@@ -5,6 +5,7 @@
export let ticket;
export let snapshot = {};
export let at = (key) => key;
export let onOpenUser = () => {};
$: user = ticket?.user || {};
$: displayName = snapshot?.name || user.username || user.email || user.user_id || "-";
@@ -65,7 +66,7 @@
variant="ghost"
size="icon"
disabled={!canOpenUser}
onclick={() => (window.location.href = `/admin/users/${user.user_id}`)}
onclick={() => onOpenUser(user.user_id)}
aria-label={at("support_open_user", {}, "Карточка")}
title={at("support_open_user", {}, "Карточка")}
>
+64 -36
View File
@@ -25,15 +25,41 @@ export function normalizeAdminSection(value) {
return ADMIN_SECTIONS.has(section) ? section : "stats";
}
export function sectionFromPath(pathname) {
function normalizePathname(pathname) {
const normalized = String(pathname || "")
.trim()
.replace(/\/+$/, "");
return normalized || "/";
}
export function stripRoutePrefix(pathname, routePrefix = "") {
const path = normalizePathname(pathname);
const prefix = normalizePathname(routePrefix);
if (prefix === "/") return path;
if (path.toLowerCase() === prefix.toLowerCase()) return "/";
if (path.toLowerCase().startsWith(`${prefix.toLowerCase()}/`)) {
return path.slice(prefix.length) || "/";
}
return path;
}
export function withRoutePrefix(pathname, routePrefix = "") {
const path = normalizePathname(pathname);
const prefix = normalizePathname(routePrefix);
if (prefix === "/") return path;
if (path === "/") return prefix;
return `${prefix}${path}`;
}
export function sectionFromPath(pathname, routePrefix = "") {
const normalizedPath = String(pathname || "")
.trim()
.toLowerCase()
.replace(/\/+$/, "");
if (!normalizedPath || normalizedPath === "/") return "home";
if (normalizedPath === "/admin" || normalizedPath.startsWith("/admin/")) return "admin";
if (normalizedPath === "/support" || normalizedPath.startsWith("/support/")) return "support";
const section = normalizedPath.startsWith("/") ? normalizedPath.slice(1) : normalizedPath;
const routePath = stripRoutePrefix(normalizedPath, routePrefix).toLowerCase().replace(/\/+$/, "");
if (!routePath || routePath === "/") return "home";
if (routePath === "/admin" || routePath.startsWith("/admin/")) return "admin";
if (routePath === "/support" || routePath.startsWith("/support/")) return "support";
const section = routePath.startsWith("/") ? routePath.slice(1) : routePath;
return normalizeSection(section);
}
@@ -45,67 +71,68 @@ export function publicInstallTokenFromPath(pathname) {
return match ? match[1].toLowerCase() : "";
}
export function adminSectionFromPath(pathname) {
const normalized = String(pathname || "")
.toLowerCase()
.replace(/\/+$/, "");
export function adminSectionFromPath(pathname, routePrefix = "") {
const normalized = stripRoutePrefix(pathname, routePrefix).toLowerCase().replace(/\/+$/, "");
const m = normalized.match(/^\/admin\/([a-z0-9_-]+)(?:\/.*)?$/);
return normalizeAdminSection(m ? m[1] : "");
}
export function adminUserIdFromPath(pathname) {
const normalized = String(pathname || "")
.toLowerCase()
.replace(/\/+$/, "");
export function adminUserIdFromPath(pathname, routePrefix = "") {
const normalized = stripRoutePrefix(pathname, routePrefix).toLowerCase().replace(/\/+$/, "");
const m = normalized.match(/^\/admin\/users\/(-?\d+)$/);
return m ? Number(m[1]) : null;
}
export function adminPaymentIdFromPath(pathname) {
const normalized = String(pathname || "")
.toLowerCase()
.replace(/\/+$/, "");
export function adminPaymentIdFromPath(pathname, routePrefix = "") {
const normalized = stripRoutePrefix(pathname, routePrefix).toLowerCase().replace(/\/+$/, "");
const m = normalized.match(/^\/admin\/payments\/(\d+)$/);
return m ? Number(m[1]) : null;
}
export function adminPaymentsUserIdFromPath(pathname) {
const normalized = String(pathname || "")
.toLowerCase()
.replace(/\/+$/, "");
export function adminPaymentsUserIdFromPath(pathname, routePrefix = "") {
const normalized = stripRoutePrefix(pathname, routePrefix).toLowerCase().replace(/\/+$/, "");
const m = normalized.match(/^\/admin\/payments\/users\/(-?\d+)$/);
return m ? Number(m[1]) : null;
}
export function supportTicketIdFromPath(pathname) {
const normalized = String(pathname || "")
.toLowerCase()
.replace(/\/+$/, "");
export function supportTicketIdFromPath(pathname, routePrefix = "") {
const normalized = stripRoutePrefix(pathname, routePrefix).toLowerCase().replace(/\/+$/, "");
const m = normalized.match(/^\/support\/(\d+)$/);
return m ? Number(m[1]) : null;
}
export function adminSupportTicketIdFromPath(pathname) {
const normalized = String(pathname || "")
.toLowerCase()
.replace(/\/+$/, "");
export function adminSupportTicketIdFromPath(pathname, routePrefix = "") {
const normalized = stripRoutePrefix(pathname, routePrefix).toLowerCase().replace(/\/+$/, "");
const m = normalized.match(/^\/admin\/support\/(\d+)$/);
return m ? Number(m[1]) : null;
}
export function syncSectionPath(section, replace = false, adminSection = null, adminUserId = null) {
export function syncSectionPath(
section,
replace = false,
adminSection = null,
adminUserId = null,
routePrefix = ""
) {
if (window.location.protocol === "file:") return;
const normalized = normalizeSection(section);
let targetPath = APP_SECTION_PATHS[normalized] || APP_SECTION_PATHS.home;
if (normalized === "admin") {
const adm = adminSection || adminSectionFromPath(window.location.pathname) || "stats";
const adm =
adminSection || adminSectionFromPath(window.location.pathname, routePrefix) || "stats";
const uid =
adminUserId ?? (adm === "users" ? adminUserIdFromPath(window.location.pathname) : null);
adminUserId ??
(adm === "users" ? adminUserIdFromPath(window.location.pathname, routePrefix) : null);
const supportTicketId =
adm === "support" ? adminSupportTicketIdFromPath(window.location.pathname) : null;
const paymentId = adm === "payments" ? adminPaymentIdFromPath(window.location.pathname) : null;
adm === "support"
? adminSupportTicketIdFromPath(window.location.pathname, routePrefix)
: null;
const paymentId =
adm === "payments" ? adminPaymentIdFromPath(window.location.pathname, routePrefix) : null;
const paymentUserId =
adm === "payments" ? adminPaymentsUserIdFromPath(window.location.pathname) : null;
adm === "payments"
? adminPaymentsUserIdFromPath(window.location.pathname, routePrefix)
: null;
if (adm === "users" && uid) targetPath = `/admin/users/${uid}`;
else if (adm === "support" && supportTicketId) targetPath = `/admin/support/${supportTicketId}`;
else if (adm === "payments" && paymentUserId)
@@ -113,6 +140,7 @@ export function syncSectionPath(section, replace = false, adminSection = null, a
else if (adm === "payments" && paymentId) targetPath = `/admin/payments/${paymentId}`;
else targetPath = `/admin/${adm}`;
}
targetPath = withRoutePrefix(targetPath, routePrefix);
if (window.location.pathname === targetPath) return;
const nextUrl = `${targetPath}${window.location.search}${window.location.hash}`;
window.history[replace ? "replaceState" : "pushState"](null, "", nextUrl);
@@ -1,6 +1,7 @@
import { writable } from "svelte/store";
import { withRoutePrefix } from "../routes.js";
export function createSupportStore({ api, t, showToast }) {
export function createSupportStore({ api, t, showToast, routePrefix = "" }) {
const OPEN_TICKET_POLL_MS = 3_000;
const ACTIVE_POLL_MS = 8_000;
const BACKGROUND_POLL_MS = 45_000;
@@ -198,7 +199,7 @@ export function createSupportStore({ api, t, showToast }) {
detailLoading: true,
}));
if (!opts.skipPush && typeof window !== "undefined" && window.location.protocol !== "file:") {
const target = `/support/${id}`;
const target = withRoutePrefix(`/support/${id}`, routePrefix);
if (window.location.pathname !== target) {
window.history.pushState(
null,
@@ -232,11 +233,12 @@ export function createSupportStore({ api, t, showToast }) {
function closeTicketView(opts = {}) {
state.update((s) => ({ ...s, openedTicketId: null, openedTicket: null, messages: [] }));
if (!opts.skipPush && typeof window !== "undefined" && window.location.protocol !== "file:") {
if (window.location.pathname.startsWith("/support/")) {
const supportPath = withRoutePrefix("/support", routePrefix);
if (window.location.pathname.startsWith(`${supportPath}/`)) {
window.history.pushState(
null,
"",
`/support${window.location.search}${window.location.hash}`
`${supportPath}${window.location.search}${window.location.hash}`
);
}
}