docs: refresh docs demo data
This commit is contained in:
@@ -1,37 +1,54 @@
|
|||||||
import { spawn } from 'node:child_process';
|
import { spawn } from "node:child_process";
|
||||||
import { access, copyFile, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
import {
|
||||||
import path from 'node:path';
|
access,
|
||||||
import { fileURLToPath } from 'node:url';
|
copyFile,
|
||||||
|
mkdir,
|
||||||
|
readdir,
|
||||||
|
readFile,
|
||||||
|
rm,
|
||||||
|
writeFile,
|
||||||
|
} from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const siteRoot = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
|
const siteRoot = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
|
||||||
const repoRoot = path.resolve(siteRoot, '..');
|
const repoRoot = path.resolve(siteRoot, "..");
|
||||||
const frontendRoot = path.join(repoRoot, 'frontend');
|
const frontendRoot = path.join(repoRoot, "frontend");
|
||||||
const runtimeDir = path.join(siteRoot, 'public', 'demo', 'runtime');
|
const runtimeDir = path.join(siteRoot, "public", "demo", "runtime");
|
||||||
const templatesDir = path.join(repoRoot, 'backend', 'bot', 'app', 'web', 'templates');
|
const templatesDir = path.join(
|
||||||
const themesDir = path.join(repoRoot, 'backend', 'bot', 'app', 'web', 'themes');
|
repoRoot,
|
||||||
const localesDir = path.join(repoRoot, 'locales');
|
"backend",
|
||||||
const runtimeBase = '/demo/runtime';
|
"bot",
|
||||||
|
"app",
|
||||||
|
"web",
|
||||||
|
"templates",
|
||||||
|
);
|
||||||
|
const themesDir = path.join(repoRoot, "backend", "bot", "app", "web", "themes");
|
||||||
|
const localesDir = path.join(repoRoot, "locales");
|
||||||
|
const runtimeBase = "/demo/runtime";
|
||||||
const installGuidesConfigUrl =
|
const installGuidesConfigUrl =
|
||||||
'https://raw.githubusercontent.com/legiz-ru/my-remnawave/main/sub-page/subpage-config/multiapp.json';
|
"https://raw.githubusercontent.com/legiz-ru/my-remnawave/main/sub-page/subpage-config/multiapp.json";
|
||||||
const installGuidesConfigRetries = 3;
|
const installGuidesConfigRetries = 3;
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === "win32";
|
||||||
const npmExecPath = process.env.npm_execpath || '';
|
const npmExecPath = process.env.npm_execpath || "";
|
||||||
|
|
||||||
function run(command, args, options = {}) {
|
function run(command, args, options = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const child = spawn(command, args, {
|
const child = spawn(command, args, {
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
stdio: 'inherit',
|
stdio: "inherit",
|
||||||
shell: false,
|
shell: false,
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
child.on('error', reject);
|
child.on("error", reject);
|
||||||
child.on('exit', (code) => {
|
child.on("exit", (code) => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve();
|
resolve();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
|
reject(
|
||||||
|
new Error(`${command} ${args.join(" ")} exited with code ${code}`),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -40,7 +57,7 @@ function runNpm(args) {
|
|||||||
if (npmExecPath) {
|
if (npmExecPath) {
|
||||||
return run(process.execPath, [npmExecPath, ...args]);
|
return run(process.execPath, [npmExecPath, ...args]);
|
||||||
}
|
}
|
||||||
return run(isWindows ? 'npm.cmd' : 'npm', args, { shell: isWindows });
|
return run(isWindows ? "npm.cmd" : "npm", args, { shell: isWindows });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pathExists(targetPath) {
|
async function pathExists(targetPath) {
|
||||||
@@ -54,10 +71,10 @@ async function pathExists(targetPath) {
|
|||||||
|
|
||||||
async function ensureFrontendDependencies() {
|
async function ensureFrontendDependencies() {
|
||||||
const viteBin = isWindows
|
const viteBin = isWindows
|
||||||
? path.join(frontendRoot, 'node_modules', '.bin', 'vite.cmd')
|
? path.join(frontendRoot, "node_modules", ".bin", "vite.cmd")
|
||||||
: path.join(frontendRoot, 'node_modules', '.bin', 'vite');
|
: path.join(frontendRoot, "node_modules", ".bin", "vite");
|
||||||
if (await pathExists(viteBin)) return;
|
if (await pathExists(viteBin)) return;
|
||||||
await runNpm(['--prefix', frontendRoot, 'ci']);
|
await runNpm(["--prefix", frontendRoot, "ci"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyDirectory(sourceDir, targetDir, transform = null) {
|
async function copyDirectory(sourceDir, targetDir, transform = null) {
|
||||||
@@ -81,11 +98,14 @@ async function copyDirectory(sourceDir, targetDir, transform = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function copyThemeFile(sourcePath, targetPath) {
|
async function copyThemeFile(sourcePath, targetPath) {
|
||||||
if (path.extname(sourcePath).toLowerCase() !== '.css') return false;
|
if (path.extname(sourcePath).toLowerCase() !== ".css") return false;
|
||||||
const css = await readFile(sourcePath, 'utf8');
|
const css = await readFile(sourcePath, "utf8");
|
||||||
const rewritten = css.replace(/\/webapp-theme-assets\//g, `${runtimeBase}/themes/`);
|
const rewritten = css.replace(
|
||||||
|
/\/webapp-theme-assets\//g,
|
||||||
|
`${runtimeBase}/themes/`,
|
||||||
|
);
|
||||||
await mkdir(path.dirname(targetPath), { recursive: true });
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
||||||
await writeFile(targetPath, rewritten, 'utf8');
|
await writeFile(targetPath, rewritten, "utf8");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,13 +118,13 @@ function wait(ms) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function jsonScriptPayload(value) {
|
function jsonScriptPayload(value) {
|
||||||
return JSON.stringify(value).replace(/</g, '\\u003c');
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function demoI18nPayload() {
|
async function demoI18nPayload() {
|
||||||
const [ru, en] = await Promise.all([
|
const [ru, en] = await Promise.all([
|
||||||
readFile(path.join(localesDir, 'ru.json'), 'utf8'),
|
readFile(path.join(localesDir, "ru.json"), "utf8"),
|
||||||
readFile(path.join(localesDir, 'en.json'), 'utf8'),
|
readFile(path.join(localesDir, "en.json"), "utf8"),
|
||||||
]);
|
]);
|
||||||
return jsonScriptPayload({ ru: JSON.parse(ru), en: JSON.parse(en) });
|
return jsonScriptPayload({ ru: JSON.parse(ru), en: JSON.parse(en) });
|
||||||
}
|
}
|
||||||
@@ -115,7 +135,7 @@ async function installGuidesConfigPayload() {
|
|||||||
for (let attempt = 1; attempt <= installGuidesConfigRetries; attempt += 1) {
|
for (let attempt = 1; attempt <= installGuidesConfigRetries; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(installGuidesConfigUrl, {
|
const response = await fetch(installGuidesConfigUrl, {
|
||||||
headers: { accept: 'application/json' },
|
headers: { accept: "application/json" },
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -146,7 +166,13 @@ async function appHtml() {
|
|||||||
/>
|
/>
|
||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
<meta name="theme-color" content="#03070b" />
|
<meta name="theme-color" content="#03070b" />
|
||||||
<title>Remnawave Minishop Demo</title>
|
<title>remnawave-minishop demo</title>
|
||||||
|
<link
|
||||||
|
id="app-favicon"
|
||||||
|
rel="icon"
|
||||||
|
href="${runtimeBase}/default-brand/favicons/19b2a242e5b7bc2d/icon-180.png"
|
||||||
|
sizes="180x180"
|
||||||
|
/>
|
||||||
<link rel="stylesheet" href="${runtimeBase}/subscription_webapp_docs_demo.css" />
|
<link rel="stylesheet" href="${runtimeBase}/subscription_webapp_docs_demo.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -161,7 +187,7 @@ async function appHtml() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await ensureFrontendDependencies();
|
await ensureFrontendDependencies();
|
||||||
await runNpm(['--prefix', frontendRoot, 'run', 'build:docs-demo']);
|
await runNpm(["--prefix", frontendRoot, "run", "build:docs-demo"]);
|
||||||
|
|
||||||
await rm(runtimeDir, { recursive: true, force: true });
|
await rm(runtimeDir, { recursive: true, force: true });
|
||||||
await mkdir(runtimeDir, { recursive: true });
|
await mkdir(runtimeDir, { recursive: true });
|
||||||
@@ -169,16 +195,25 @@ await mkdir(runtimeDir, { recursive: true });
|
|||||||
const html = await appHtml();
|
const html = await appHtml();
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
copyRuntimeAsset('subscription_webapp_docs_demo.js'),
|
copyRuntimeAsset("subscription_webapp_docs_demo.js"),
|
||||||
copyRuntimeAsset('subscription_webapp_docs_demo.css'),
|
copyRuntimeAsset("subscription_webapp_docs_demo.css"),
|
||||||
copyRuntimeAsset('subscription_webapp_admin.js'),
|
copyRuntimeAsset("subscription_webapp_admin.js"),
|
||||||
copyRuntimeAsset('subscription_webapp_admin.css'),
|
copyRuntimeAsset("subscription_webapp_admin.css"),
|
||||||
copyDirectory(path.join(templatesDir, 'default-brand'), path.join(runtimeDir, 'default-brand')),
|
copyDirectory(
|
||||||
copyDirectory(themesDir, path.join(runtimeDir, 'themes'), copyThemeFile),
|
path.join(templatesDir, "default-brand"),
|
||||||
writeFile(path.join(runtimeDir, 'app.html'), html, 'utf8'),
|
path.join(runtimeDir, "default-brand"),
|
||||||
|
),
|
||||||
|
copyDirectory(themesDir, path.join(runtimeDir, "themes"), copyThemeFile),
|
||||||
|
writeFile(path.join(runtimeDir, "app.html"), html, "utf8"),
|
||||||
installGuidesConfigPayload().then((payload) =>
|
installGuidesConfigPayload().then((payload) =>
|
||||||
writeFile(path.join(runtimeDir, 'subscription-guides-config.json'), payload, 'utf8'),
|
writeFile(
|
||||||
|
path.join(runtimeDir, "subscription-guides-config.json"),
|
||||||
|
payload,
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log(`Built static docs demo runtime at ${path.relative(repoRoot, runtimeDir)}`);
|
console.log(
|
||||||
|
`Built static docs demo runtime at ${path.relative(repoRoot, runtimeDir)}`,
|
||||||
|
);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const docsHref = '/getting-started/demo/';
|
|||||||
/>
|
/>
|
||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
<meta name="theme-color" content="#05080f" />
|
<meta name="theme-color" content="#05080f" />
|
||||||
<title>Remnawave Minishop Demo</title>
|
<title>remnawave-minishop demo</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
@@ -227,7 +227,7 @@ const docsHref = '/getting-started/demo/';
|
|||||||
</button>
|
</button>
|
||||||
<div class="demo-topbar__panel" id="demo-topbar-panel">
|
<div class="demo-topbar__panel" id="demo-topbar-panel">
|
||||||
<div class="demo-topbar__title">
|
<div class="demo-topbar__title">
|
||||||
<strong>Remnawave Minishop demo</strong>
|
<strong>remnawave-minishop demo</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="demo-topbar__actions">
|
<div class="demo-topbar__actions">
|
||||||
<a class="demo-topbar__back" href={docsHref}>К документации</a>
|
<a class="demo-topbar__back" href={docsHref}>К документации</a>
|
||||||
@@ -240,7 +240,7 @@ const docsHref = '/getting-started/demo/';
|
|||||||
<iframe
|
<iframe
|
||||||
id="demo-frame"
|
id="demo-frame"
|
||||||
class="demo-frame"
|
class="demo-frame"
|
||||||
title="Remnawave Minishop static demo"
|
title="remnawave-minishop static demo"
|
||||||
src={defaultDemoSrc}
|
src={defaultDemoSrc}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
></iframe>
|
></iframe>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,7 @@
|
|||||||
import { DEV_MOCK } from "./previewMock.js";
|
import { DEV_MOCK } from "./previewMock.js";
|
||||||
|
import { DEMO_DATASET } from "./demoDataset.js";
|
||||||
|
|
||||||
|
const DEMO_LANGUAGE_STORAGE_KEY = "rw_minishop_demo_language";
|
||||||
|
|
||||||
function defaultClone(value) {
|
function defaultClone(value) {
|
||||||
try {
|
try {
|
||||||
@@ -8,6 +11,616 @@ function defaultClone(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let demoPromosState = null;
|
||||||
|
let demoAdsState = null;
|
||||||
|
let demoSupportTicketsState = null;
|
||||||
|
let demoSupportMessagesState = null;
|
||||||
|
let demoTariffsState = null;
|
||||||
|
const demoSettingsChanges = new Map();
|
||||||
|
|
||||||
|
function demoPromos() {
|
||||||
|
if (!demoPromosState) demoPromosState = defaultClone(DEMO_DATASET.promos || []);
|
||||||
|
return demoPromosState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoAds() {
|
||||||
|
if (!demoAdsState) demoAdsState = defaultClone(DEMO_DATASET.ads || []);
|
||||||
|
return demoAdsState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoSupportTickets() {
|
||||||
|
if (!demoSupportTicketsState) {
|
||||||
|
demoSupportTicketsState = defaultClone(DEMO_DATASET.supportTickets || []);
|
||||||
|
}
|
||||||
|
return demoSupportTicketsState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoSupportMessages() {
|
||||||
|
if (!demoSupportMessagesState) {
|
||||||
|
demoSupportMessagesState = defaultClone(DEMO_DATASET.supportMessages || {});
|
||||||
|
}
|
||||||
|
return demoSupportMessagesState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoTariffs() {
|
||||||
|
if (!demoTariffsState) {
|
||||||
|
demoTariffsState = defaultClone(
|
||||||
|
DEMO_DATASET.tariffsCatalog || {
|
||||||
|
default_tariff: "",
|
||||||
|
topup_packages_default: { rub: [], stars: [] },
|
||||||
|
tariffs: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return demoTariffsState;
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryParams(path) {
|
||||||
|
return new URLSearchParams(String(path || "").split("?")[1] || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonBody(options) {
|
||||||
|
try {
|
||||||
|
return options?.body ? JSON.parse(String(options.body)) : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeDemoLanguage(language) {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
try {
|
||||||
|
window.localStorage?.setItem(DEMO_LANGUAGE_STORAGE_KEY, language);
|
||||||
|
} catch {
|
||||||
|
// Demo storage can be unavailable in private contexts; the in-memory mock still updates.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function paged(items, params, fallbackSize = 25) {
|
||||||
|
const total = items.length;
|
||||||
|
if (params.has("limit") || params.has("offset")) {
|
||||||
|
const limit = Math.max(1, Number(params.get("limit") || fallbackSize));
|
||||||
|
const offset = Math.max(0, Number(params.get("offset") || 0));
|
||||||
|
return {
|
||||||
|
items: items.slice(offset, offset + limit),
|
||||||
|
total,
|
||||||
|
page: Math.floor(offset / limit),
|
||||||
|
pageSize: limit,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const page = Math.max(0, Number(params.get("page") || 0));
|
||||||
|
const pageSize = Math.max(1, Number(params.get("page_size") || fallbackSize));
|
||||||
|
const start = page * pageSize;
|
||||||
|
return { items: items.slice(start, start + pageSize), total, page, pageSize };
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringDate(value) {
|
||||||
|
const time = Date.parse(value || "");
|
||||||
|
return Number.isFinite(time) ? time : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function userName(user) {
|
||||||
|
return (
|
||||||
|
[user?.first_name, user?.last_name].filter(Boolean).join(" ").trim() ||
|
||||||
|
user?.username ||
|
||||||
|
user?.email ||
|
||||||
|
String(user?.user_id || "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterDemoUsers(params) {
|
||||||
|
let out = [...(DEMO_DATASET.adminUsers || [])];
|
||||||
|
const q = (params.get("q") || params.get("search") || "").trim().toLowerCase();
|
||||||
|
if (q) {
|
||||||
|
out = out.filter((user) =>
|
||||||
|
[
|
||||||
|
user.user_id,
|
||||||
|
user.telegram_id,
|
||||||
|
user.username,
|
||||||
|
user.first_name,
|
||||||
|
user.last_name,
|
||||||
|
user.email,
|
||||||
|
user.panel_user_uuid,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.some((value) => String(value).toLowerCase().includes(q))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filter = params.get("filter") || "all";
|
||||||
|
if (filter === "active") out = out.filter((user) => !user.is_banned);
|
||||||
|
else if (filter === "banned") out = out.filter((user) => user.is_banned);
|
||||||
|
else if (filter === "tg_linked") out = out.filter((user) => user.telegram_linked);
|
||||||
|
else if (filter === "no_tg") out = out.filter((user) => !user.telegram_linked);
|
||||||
|
else if (filter === "email_linked") out = out.filter((user) => Boolean(user.email));
|
||||||
|
else if (filter === "no_email") out = out.filter((user) => !user.email);
|
||||||
|
else if (filter === "panel_linked") out = out.filter((user) => Boolean(user.panel_user_uuid));
|
||||||
|
|
||||||
|
const panelStatus = params.get("panel_status") || "all";
|
||||||
|
if (panelStatus !== "all") out = out.filter((user) => user.panel_status === panelStatus);
|
||||||
|
|
||||||
|
const premiumTraffic = params.get("premium_traffic") || "all";
|
||||||
|
if (premiumTraffic !== "all") {
|
||||||
|
out = out.filter((user) => (user.premium_traffic?.state || "none") === premiumTraffic);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sort = params.get("sort") || "registered_desc";
|
||||||
|
out.sort((a, b) => {
|
||||||
|
if (sort === "registered_asc")
|
||||||
|
return stringDate(a.registration_date) - stringDate(b.registration_date);
|
||||||
|
if (sort === "name_asc") return userName(a).localeCompare(userName(b));
|
||||||
|
if (sort === "name_desc") return userName(b).localeCompare(userName(a));
|
||||||
|
if (sort === "id_asc") return Number(a.user_id || 0) - Number(b.user_id || 0);
|
||||||
|
if (sort === "id_desc") return Number(b.user_id || 0) - Number(a.user_id || 0);
|
||||||
|
if (sort === "premium_ratio_asc")
|
||||||
|
return Number(a.premium_traffic?.percent ?? -1) - Number(b.premium_traffic?.percent ?? -1);
|
||||||
|
if (sort === "premium_ratio_desc")
|
||||||
|
return Number(b.premium_traffic?.percent ?? -1) - Number(a.premium_traffic?.percent ?? -1);
|
||||||
|
return stringDate(b.registration_date) - stringDate(a.registration_date);
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoSupportCounts(items = demoSupportTickets()) {
|
||||||
|
const byStatus = { open: 0, awaiting_admin: 0, awaiting_user: 0, resolved: 0, closed: 0 };
|
||||||
|
for (const item of items) byStatus[item.status] = (byStatus[item.status] || 0) + 1;
|
||||||
|
const closed = (byStatus.closed || 0) + (byStatus.resolved || 0);
|
||||||
|
return {
|
||||||
|
...byStatus,
|
||||||
|
active: items.length - closed,
|
||||||
|
closed,
|
||||||
|
total: items.length,
|
||||||
|
total_unread_admin: items.reduce((sum, item) => sum + Number(item.unread_admin_count || 0), 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterDemoSupportTickets(items, params) {
|
||||||
|
let out = [...items];
|
||||||
|
const status = params.get("status");
|
||||||
|
if (status === "active")
|
||||||
|
out = out.filter((item) => !["closed", "resolved"].includes(item.status));
|
||||||
|
else if (status === "closed")
|
||||||
|
out = out.filter((item) => ["closed", "resolved"].includes(item.status));
|
||||||
|
else if (status) out = out.filter((item) => item.status === status);
|
||||||
|
|
||||||
|
const priority = params.get("priority");
|
||||||
|
if (priority) out = out.filter((item) => item.priority === priority);
|
||||||
|
const category = params.get("category");
|
||||||
|
if (category) out = out.filter((item) => item.category === category);
|
||||||
|
const search = (params.get("search") || "").trim().toLowerCase();
|
||||||
|
if (search) {
|
||||||
|
out = out.filter((item) =>
|
||||||
|
[item.subject, item.user?.username, item.user?.email, item.ticket_id]
|
||||||
|
.filter(Boolean)
|
||||||
|
.some((value) => String(value).toLowerCase().includes(search))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const priorityRank = { urgent: 4, high: 3, normal: 2, low: 1 };
|
||||||
|
const sort = params.get("sort") || "updated_desc";
|
||||||
|
out.sort((a, b) => {
|
||||||
|
if (sort === "importance_desc") {
|
||||||
|
return (
|
||||||
|
(priorityRank[b.priority] || 0) - (priorityRank[a.priority] || 0) ||
|
||||||
|
stringDate(b.last_message_at || b.created_at) -
|
||||||
|
stringDate(a.last_message_at || a.created_at)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (sort === "updated_asc") {
|
||||||
|
return (
|
||||||
|
stringDate(a.last_message_at || a.created_at) -
|
||||||
|
stringDate(b.last_message_at || b.created_at)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (sort === "created_desc") return stringDate(b.created_at) - stringDate(a.created_at);
|
||||||
|
if (sort === "created_asc") return stringDate(a.created_at) - stringDate(b.created_at);
|
||||||
|
return (
|
||||||
|
stringDate(b.last_message_at || b.created_at) - stringDate(a.last_message_at || a.created_at)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoSettingsSections(clone) {
|
||||||
|
const sections = clone(DEMO_DATASET.settingsSections || []);
|
||||||
|
for (const section of sections) {
|
||||||
|
for (const field of section.fields || []) {
|
||||||
|
if (!demoSettingsChanges.has(field.key)) continue;
|
||||||
|
const change = demoSettingsChanges.get(field.key);
|
||||||
|
if (change.deleted) {
|
||||||
|
field.value = field.default ?? "";
|
||||||
|
field.overridden = false;
|
||||||
|
} else {
|
||||||
|
field.value = change.value;
|
||||||
|
field.overridden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyDemoSettingToMock(key, value) {
|
||||||
|
if (key === "WEBAPP_TITLE") DEV_MOCK.config.title = value || "";
|
||||||
|
if (key === "WEBAPP_LOGO_URL") DEV_MOCK.config.logoUrl = value || "";
|
||||||
|
if (key === "WEBAPP_LOGO_USE_EMOJI") DEV_MOCK.config.logoUseEmoji = Boolean(value);
|
||||||
|
if (key === "WEBAPP_LOGO_EMOJI") DEV_MOCK.config.logoEmoji = value || "";
|
||||||
|
if (key === "WEBAPP_LOGO_EMOJI_FONT") DEV_MOCK.config.logoEmojiFont = value || "system";
|
||||||
|
if (key === "WEBAPP_FAVICON_URL" || key === "WEBAPP_LOGO_FAVICON_URL") {
|
||||||
|
DEV_MOCK.config.faviconUrl = value || DEV_MOCK.config.faviconUrl || "";
|
||||||
|
}
|
||||||
|
if (key === "WEBAPP_FAVICON_USE_CUSTOM") DEV_MOCK.config.faviconUseCustom = Boolean(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function userSnapshotForTicket(ticket) {
|
||||||
|
const detail = DEMO_DATASET.adminUserDetails?.[String(ticket?.user_id)] || {};
|
||||||
|
const user = detail.user || ticket?.user || {};
|
||||||
|
const sub = detail.active_subscription || {};
|
||||||
|
return {
|
||||||
|
user_id: user.user_id,
|
||||||
|
name: userName(user) || user.username || user.email || String(user.user_id || ""),
|
||||||
|
username: user.username || "",
|
||||||
|
email: user.email || "",
|
||||||
|
tariff: sub.tariff_name || sub.tariff_key || "Demo",
|
||||||
|
panel_status: user.panel_status || sub.status_from_panel || "",
|
||||||
|
remaining: sub.end_date || "",
|
||||||
|
regular_traffic: `${sub.traffic_used_bytes || 0} / ${sub.traffic_limit_bytes || 0}`,
|
||||||
|
premium_traffic: `${sub.premium_used_bytes || 0} / ${sub.premium_limit_bytes || 0}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function demoApiResponse(path, cleanPath, options, context) {
|
||||||
|
const { clone, currentLang = "ru", normalizeLangCode = (value) => value || "ru" } = context;
|
||||||
|
const method = String(options.method || "GET").toUpperCase();
|
||||||
|
const params = queryParams(path);
|
||||||
|
|
||||||
|
if (cleanPath === "/admin/stats") return clone(DEMO_DATASET.stats);
|
||||||
|
if (cleanPath === "/admin/sync") return { ok: true, status: "queued" };
|
||||||
|
|
||||||
|
if (cleanPath === "/admin/payments") {
|
||||||
|
const page = paged(DEMO_DATASET.adminPayments || [], params, 25);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
payments: clone(page.items),
|
||||||
|
total: page.total,
|
||||||
|
page: page.page,
|
||||||
|
page_size: page.pageSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (/^\/admin\/payments\/\d+$/.test(cleanPath)) {
|
||||||
|
const id = Number(cleanPath.split("/").pop());
|
||||||
|
const payment = (DEMO_DATASET.adminPayments || []).find((item) => item.payment_id === id);
|
||||||
|
return payment ? { ok: true, payment: clone(payment) } : { ok: false, error: "not_found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanPath === "/admin/users") {
|
||||||
|
const filtered = filterDemoUsers(params);
|
||||||
|
const page = paged(filtered, params, 25);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
users: clone(page.items),
|
||||||
|
total: page.total,
|
||||||
|
page: page.page,
|
||||||
|
page_size: page.pageSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (cleanPath.startsWith("/admin/users/")) {
|
||||||
|
const parts = cleanPath.split("/");
|
||||||
|
const id = Number(parts[3]);
|
||||||
|
const detail = DEMO_DATASET.adminUserDetails?.[String(id)];
|
||||||
|
if (!detail) return { ok: false, error: "not_found" };
|
||||||
|
if (parts[4]) {
|
||||||
|
if (parts[4] === "telegram-profile-link") {
|
||||||
|
return { ok: true, url: `https://t.me/${detail.user?.username || "demo_user"}` };
|
||||||
|
}
|
||||||
|
if (parts[4] === "message" && parts[5] === "preview") {
|
||||||
|
return { ok: true, text: "Demo broadcast preview for the selected account." };
|
||||||
|
}
|
||||||
|
return { ok: true, user: clone(detail.user), detail: clone(detail) };
|
||||||
|
}
|
||||||
|
return clone(detail);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanPath === "/admin/logs") {
|
||||||
|
let logs = [...(DEMO_DATASET.adminLogs || [])];
|
||||||
|
const userId = params.get("user_id");
|
||||||
|
if (userId) {
|
||||||
|
logs = logs.filter(
|
||||||
|
(item) =>
|
||||||
|
String(item.user_id || "") === userId || String(item.target_user_id || "") === userId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const page = paged(logs, params, 50);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
logs: clone(page.items),
|
||||||
|
total: page.total,
|
||||||
|
page: page.page,
|
||||||
|
page_size: page.pageSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanPath === "/admin/promos") {
|
||||||
|
if (method === "POST") {
|
||||||
|
const body = jsonBody(options);
|
||||||
|
demoPromos().unshift({
|
||||||
|
id: 3900 + demoPromos().length + 1,
|
||||||
|
code: body.code || "DEMO",
|
||||||
|
bonus_days: Number(body.bonus_days || 7),
|
||||||
|
max_activations: Number(body.max_activations || 1),
|
||||||
|
current_activations: 0,
|
||||||
|
is_active: true,
|
||||||
|
valid_until: new Date(Date.now() + Number(body.valid_days || 30) * 86400000).toISOString(),
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
created_by_admin_id: DEV_MOCK.data.user?.id || DEV_MOCK.data.user?.user_id,
|
||||||
|
});
|
||||||
|
return { ok: true, promo: clone(demoPromos()[0]) };
|
||||||
|
}
|
||||||
|
const page = paged(demoPromos(), params, 25);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
promos: clone(page.items),
|
||||||
|
total: page.total,
|
||||||
|
page: page.page,
|
||||||
|
page_size: page.pageSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (cleanPath.startsWith("/admin/promos/")) {
|
||||||
|
const id = Number(cleanPath.split("/").pop());
|
||||||
|
const promo = demoPromos().find((item) => item.id === id);
|
||||||
|
if (!promo) return { ok: false, error: "not_found" };
|
||||||
|
if (method === "DELETE") {
|
||||||
|
demoPromosState = demoPromos().filter((item) => item.id !== id);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
Object.assign(promo, jsonBody(options));
|
||||||
|
return { ok: true, promo: clone(promo) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanPath === "/admin/ads") {
|
||||||
|
if (method === "POST") {
|
||||||
|
const body = jsonBody(options);
|
||||||
|
demoAds().unshift({
|
||||||
|
id: 900 + demoAds().length + 1,
|
||||||
|
source: body.source || "demo",
|
||||||
|
start_param: body.start_param || "demo_campaign",
|
||||||
|
cost: Number(body.cost || 0),
|
||||||
|
is_active: true,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
stats: { users: 0, trial_activations: 0, payments: 0, revenue: 0 },
|
||||||
|
});
|
||||||
|
return { ok: true, campaign: clone(demoAds()[0]) };
|
||||||
|
}
|
||||||
|
return { ok: true, campaigns: clone(demoAds()), totals: clone(DEMO_DATASET.adsTotals || {}) };
|
||||||
|
}
|
||||||
|
if (cleanPath.startsWith("/admin/ads/")) {
|
||||||
|
const parts = cleanPath.split("/");
|
||||||
|
const id = Number(parts[3]);
|
||||||
|
const campaign = demoAds().find((item) => item.id === id);
|
||||||
|
if (!campaign) return { ok: false, error: "not_found" };
|
||||||
|
if (parts[4] === "toggle") {
|
||||||
|
campaign.is_active = !campaign.is_active;
|
||||||
|
return { ok: true, campaign: clone(campaign) };
|
||||||
|
}
|
||||||
|
if (method === "DELETE") {
|
||||||
|
demoAdsState = demoAds().filter((item) => item.id !== id);
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
return { ok: true, campaign: clone(campaign) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanPath === "/admin/backups") return clone(DEMO_DATASET.backups);
|
||||||
|
if (cleanPath === "/admin/backups/create") {
|
||||||
|
const archive = clone(DEMO_DATASET.backups?.archives?.[0] || {});
|
||||||
|
archive.name = `minishop-demo-${Date.now()}.zip`;
|
||||||
|
archive.created_at = new Date().toISOString();
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
archive,
|
||||||
|
result: { archive_name: archive.name, completed_at: archive.created_at, warnings: [] },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (cleanPath === "/admin/backups/upload") {
|
||||||
|
return { ok: true, archive: clone(DEMO_DATASET.backups?.archives?.[0] || {}) };
|
||||||
|
}
|
||||||
|
if (cleanPath === "/admin/backups/restore") {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
result: {
|
||||||
|
archive_name: DEMO_DATASET.backups?.archives?.[0]?.name || "demo.zip",
|
||||||
|
database_restored: true,
|
||||||
|
warnings: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanPath === "/admin/settings" && method === "PATCH") {
|
||||||
|
const body = jsonBody(options);
|
||||||
|
for (const key of body.deletes || []) demoSettingsChanges.set(key, { deleted: true });
|
||||||
|
for (const [key, value] of Object.entries(body.updates || {})) {
|
||||||
|
demoSettingsChanges.set(key, { value, deleted: false });
|
||||||
|
applyDemoSettingToMock(key, value);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
applied: Object.keys(body.updates || {}).length,
|
||||||
|
reverted: (body.deletes || []).length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (cleanPath === "/admin/settings") return { ok: true, sections: demoSettingsSections(clone) };
|
||||||
|
|
||||||
|
if (cleanPath === "/admin/tariffs") {
|
||||||
|
if (method === "PUT") {
|
||||||
|
const body = jsonBody(options);
|
||||||
|
const catalog = body.catalog || body;
|
||||||
|
demoTariffsState = defaultClone(catalog);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
path: "data/tariffs.json",
|
||||||
|
catalog: clone(demoTariffs()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanPath === "/admin/panel/internal-squads") {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
squads: clone(
|
||||||
|
DEMO_DATASET.panelSquads || [
|
||||||
|
{ uuid: "db786ee8-816b-4760-80aa-1fc7a3669ff2", name: "Base RU" },
|
||||||
|
{ uuid: "5f29045a-5e8b-4b06-a7b1-29abf0ad3a54", name: "Base EU" },
|
||||||
|
{ uuid: "2f2f6e0a-1f2d-4e80-a33b-0ebf3a409012", name: "Premium EU" },
|
||||||
|
]
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanPath === "/admin/translations" && method === "PATCH") {
|
||||||
|
return { ok: true, applied: 1, reverted: 0, file_written: false };
|
||||||
|
}
|
||||||
|
if (cleanPath === "/admin/translations") {
|
||||||
|
return { ok: true, ...clone(DEMO_DATASET.translations) };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanPath === "/admin/support/stats") return { ok: true, stats: demoSupportCounts() };
|
||||||
|
if (cleanPath === "/admin/support/tickets") {
|
||||||
|
const tickets = filterDemoSupportTickets(demoSupportTickets(), params);
|
||||||
|
const page = paged(tickets, params, 50);
|
||||||
|
return { ok: true, tickets: clone(page.items), total: page.total };
|
||||||
|
}
|
||||||
|
if (cleanPath.startsWith("/admin/support/tickets/")) {
|
||||||
|
const parts = cleanPath.split("/");
|
||||||
|
const ticketId = Number(parts[4]);
|
||||||
|
const ticket = demoSupportTickets().find((item) => item.ticket_id === ticketId);
|
||||||
|
if (!ticket) return { ok: false, error: "not_found" };
|
||||||
|
const messages = demoSupportMessages()[String(ticketId)] || [];
|
||||||
|
if (parts[5] === "read") {
|
||||||
|
ticket.unread_admin_count = 0;
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
if (parts[5] === "messages") {
|
||||||
|
const body = jsonBody(options);
|
||||||
|
const message = {
|
||||||
|
message_id: Date.now(),
|
||||||
|
ticket_id: ticketId,
|
||||||
|
author_role: "admin",
|
||||||
|
author_user_id: DEV_MOCK.data.user?.id || DEV_MOCK.data.user?.user_id,
|
||||||
|
author_name: "Поддержка",
|
||||||
|
body: body.body || "",
|
||||||
|
is_internal_note: Boolean(body.is_internal_note),
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
messages.push(message);
|
||||||
|
demoSupportMessages()[String(ticketId)] = messages;
|
||||||
|
ticket.last_message_at = message.created_at;
|
||||||
|
ticket.last_message_role = "admin";
|
||||||
|
ticket.status = "awaiting_user";
|
||||||
|
return { ok: true, ticket: clone(ticket), message: clone(message) };
|
||||||
|
}
|
||||||
|
if (method === "PATCH") {
|
||||||
|
Object.assign(ticket, jsonBody(options));
|
||||||
|
return { ok: true, ticket: clone(ticket) };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
ticket: clone(ticket),
|
||||||
|
messages: clone(messages),
|
||||||
|
user_snapshot: userSnapshotForTicket(ticket),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanPath === "/support/tickets" && method === "POST") {
|
||||||
|
const body = jsonBody(options);
|
||||||
|
const user = DEV_MOCK.data.user || {};
|
||||||
|
const ticket = {
|
||||||
|
ticket_id: 4900 + demoSupportTickets().length + 1,
|
||||||
|
user_id: user.user_id || user.id,
|
||||||
|
subject: body.subject || "Новое обращение в поддержку",
|
||||||
|
category: body.category || "other",
|
||||||
|
priority: body.priority || "normal",
|
||||||
|
status: "awaiting_admin",
|
||||||
|
unread_user_count: 0,
|
||||||
|
unread_admin_count: 1,
|
||||||
|
last_message_at: new Date().toISOString(),
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
demoSupportTickets().unshift(ticket);
|
||||||
|
demoSupportMessages()[String(ticket.ticket_id)] = [
|
||||||
|
{
|
||||||
|
message_id: Date.now(),
|
||||||
|
ticket_id: ticket.ticket_id,
|
||||||
|
author_role: "user",
|
||||||
|
author_user_id: ticket.user_id,
|
||||||
|
author_name: userName(user) || user.username || "Демо-пользователь",
|
||||||
|
body: body.body || "",
|
||||||
|
created_at: ticket.created_at,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return { ok: true, ticket: clone(ticket) };
|
||||||
|
}
|
||||||
|
if (cleanPath === "/support/tickets") {
|
||||||
|
const tickets = filterDemoSupportTickets(demoSupportTickets(), params);
|
||||||
|
const page = paged(tickets, params, 50);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
tickets: clone(page.items),
|
||||||
|
total: page.total,
|
||||||
|
counts: demoSupportCounts(demoSupportTickets()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (cleanPath.startsWith("/support/tickets/")) {
|
||||||
|
const parts = cleanPath.split("/");
|
||||||
|
const ticketId = Number(parts[3]);
|
||||||
|
const ticket = demoSupportTickets().find((item) => item.ticket_id === ticketId);
|
||||||
|
if (!ticket) return { ok: false, error: "not_found" };
|
||||||
|
const messages = demoSupportMessages()[String(ticketId)] || [];
|
||||||
|
if (parts[4] === "read") {
|
||||||
|
ticket.unread_user_count = 0;
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
if (parts[4] === "messages") {
|
||||||
|
const body = jsonBody(options);
|
||||||
|
const user = DEV_MOCK.data.user || {};
|
||||||
|
const message = {
|
||||||
|
message_id: Date.now(),
|
||||||
|
ticket_id: ticketId,
|
||||||
|
author_role: "user",
|
||||||
|
author_user_id: user.user_id || user.id,
|
||||||
|
author_name: userName(user) || user.username || "Демо-пользователь",
|
||||||
|
body: body.body || "",
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
messages.push(message);
|
||||||
|
demoSupportMessages()[String(ticketId)] = messages;
|
||||||
|
ticket.last_message_at = message.created_at;
|
||||||
|
ticket.last_message_role = "user";
|
||||||
|
ticket.status = "awaiting_admin";
|
||||||
|
return { ok: true, ticket: clone(ticket), message: clone(message) };
|
||||||
|
}
|
||||||
|
return { ok: true, ticket: clone(ticket), messages: clone(messages) };
|
||||||
|
}
|
||||||
|
if (cleanPath === "/support/unread") {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
unread: demoSupportTickets().reduce(
|
||||||
|
(sum, item) => sum + Number(item.unread_user_count || 0),
|
||||||
|
0
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanPath === "/account/language" && method === "POST") {
|
||||||
|
const language = normalizeLangCode(jsonBody(options).language || currentLang);
|
||||||
|
DEV_MOCK.data.user.language_code = language;
|
||||||
|
DEV_MOCK.config.language = language;
|
||||||
|
writeDemoLanguage(language);
|
||||||
|
return { ok: true, language };
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export async function mockApi(path, options = {}, context = {}) {
|
export async function mockApi(path, options = {}, context = {}) {
|
||||||
const {
|
const {
|
||||||
currentLang = "ru",
|
currentLang = "ru",
|
||||||
@@ -16,6 +629,12 @@ export async function mockApi(path, options = {}, context = {}) {
|
|||||||
} = context;
|
} = context;
|
||||||
await new Promise((resolve) => window.setTimeout(resolve, 120));
|
await new Promise((resolve) => window.setTimeout(resolve, 120));
|
||||||
const cleanPath = String(path || "").split("?")[0];
|
const cleanPath = String(path || "").split("?")[0];
|
||||||
|
const demoResponse = demoApiResponse(path, cleanPath, options, {
|
||||||
|
clone,
|
||||||
|
currentLang,
|
||||||
|
normalizeLangCode,
|
||||||
|
});
|
||||||
|
if (demoResponse !== undefined) return demoResponse;
|
||||||
const adminUsers = [
|
const adminUsers = [
|
||||||
{
|
{
|
||||||
user_id: 100200300,
|
user_id: 100200300,
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
import { DEMO_DATASET } from "./demoDataset.js";
|
||||||
|
|
||||||
|
const DEMO_LANGUAGE_STORAGE_KEY = "rw_minishop_demo_language";
|
||||||
|
|
||||||
|
function readStoredDemoLanguage() {
|
||||||
|
if (typeof window === "undefined") return "";
|
||||||
|
try {
|
||||||
|
return window.localStorage?.getItem(DEMO_LANGUAGE_STORAGE_KEY) || "";
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const WINDOWS_95_THEME = {
|
const WINDOWS_95_THEME = {
|
||||||
key: "windows95",
|
key: "windows95",
|
||||||
names: { ru: "Windows 95", en: "Windows 95" },
|
names: { ru: "Windows 95", en: "Windows 95" },
|
||||||
@@ -426,6 +439,55 @@ export const DEV_MOCK = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function applyDemoDataset() {
|
||||||
|
const storedLanguage = readStoredDemoLanguage();
|
||||||
|
const demoUser = {
|
||||||
|
...(DEMO_DATASET.currentUser || {}),
|
||||||
|
id: DEMO_DATASET.currentUser?.id ?? DEMO_DATASET.currentUser?.user_id,
|
||||||
|
language_code: storedLanguage || DEMO_DATASET.currentUser?.language_code || "ru",
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(DEV_MOCK.config, DEMO_DATASET.config || {});
|
||||||
|
DEV_MOCK.config.language = demoUser.language_code || "ru";
|
||||||
|
Object.assign(DEV_MOCK.data, {
|
||||||
|
user: demoUser,
|
||||||
|
subscription: DEMO_DATASET.currentSubscription || DEV_MOCK.data.subscription,
|
||||||
|
devices: DEMO_DATASET.devices || DEV_MOCK.data.devices,
|
||||||
|
plans: DEMO_DATASET.plans || DEV_MOCK.data.plans,
|
||||||
|
payment_methods: DEMO_DATASET.paymentMethods || DEV_MOCK.data.payment_methods,
|
||||||
|
referral: DEMO_DATASET.referral || DEV_MOCK.data.referral,
|
||||||
|
tariff_change_options:
|
||||||
|
DEMO_DATASET.tariff_change_options || DEV_MOCK.data.tariff_change_options,
|
||||||
|
topup_options: DEMO_DATASET.topup_options || DEV_MOCK.data.topup_options,
|
||||||
|
device_topup_options: DEMO_DATASET.device_topup_options || DEV_MOCK.data.device_topup_options,
|
||||||
|
settings: {
|
||||||
|
...DEV_MOCK.data.settings,
|
||||||
|
...(DEMO_DATASET.webappSettings || {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
applyDemoDataset();
|
||||||
|
|
||||||
|
function applyDemoTariffScenario(subscriptionPatch = {}) {
|
||||||
|
DEV_MOCK.data.subscription = {
|
||||||
|
...DEV_MOCK.data.subscription,
|
||||||
|
...(DEMO_DATASET.currentSubscription || {}),
|
||||||
|
...subscriptionPatch,
|
||||||
|
traffic_limit_strategy:
|
||||||
|
subscriptionPatch.traffic_limit_strategy ||
|
||||||
|
DEMO_DATASET.currentSubscription?.traffic_limit_strategy ||
|
||||||
|
DEV_MOCK.data.subscription.traffic_limit_strategy ||
|
||||||
|
"MONTH",
|
||||||
|
};
|
||||||
|
DEV_MOCK.data.plans = DEMO_DATASET.plans;
|
||||||
|
DEV_MOCK.data.tariff_change_options =
|
||||||
|
DEMO_DATASET.tariff_change_options || DEV_MOCK.data.tariff_change_options;
|
||||||
|
DEV_MOCK.data.topup_options = DEMO_DATASET.topup_options || DEV_MOCK.data.topup_options;
|
||||||
|
DEV_MOCK.data.device_topup_options =
|
||||||
|
DEMO_DATASET.device_topup_options || DEV_MOCK.data.device_topup_options;
|
||||||
|
}
|
||||||
|
|
||||||
export function applyPreviewMock(kind) {
|
export function applyPreviewMock(kind) {
|
||||||
const mode = String(kind || "")
|
const mode = String(kind || "")
|
||||||
.trim()
|
.trim()
|
||||||
@@ -506,6 +568,10 @@ export function applyPreviewMock(kind) {
|
|||||||
];
|
];
|
||||||
} else if (mode === "tariffs") {
|
} else if (mode === "tariffs") {
|
||||||
DEV_MOCK.data.settings.traffic_mode = false;
|
DEV_MOCK.data.settings.traffic_mode = false;
|
||||||
|
if (DEMO_DATASET.plans?.length) {
|
||||||
|
applyDemoTariffScenario();
|
||||||
|
return;
|
||||||
|
}
|
||||||
DEV_MOCK.data.subscription = {
|
DEV_MOCK.data.subscription = {
|
||||||
...DEV_MOCK.data.subscription,
|
...DEV_MOCK.data.subscription,
|
||||||
tariff_key: "standard",
|
tariff_key: "standard",
|
||||||
@@ -519,57 +585,40 @@ export function applyPreviewMock(kind) {
|
|||||||
id: "standard:period:1",
|
id: "standard:period:1",
|
||||||
tariff_key: "standard",
|
tariff_key: "standard",
|
||||||
tariff_name: "Стандарт",
|
tariff_name: "Стандарт",
|
||||||
|
tariff_description: "100 GB каждый месяц",
|
||||||
billing_model: "period",
|
billing_model: "period",
|
||||||
sale_mode: "subscription",
|
|
||||||
months: 1,
|
months: 1,
|
||||||
price: 150,
|
price: 150,
|
||||||
currency: "RUB",
|
currency: "RUB",
|
||||||
title: "Стандарт",
|
title: "Стандарт",
|
||||||
subtitle: "1 месяц",
|
subtitle: "1 месяц",
|
||||||
description: "100 GB каждый месяц",
|
sale_mode: "subscription",
|
||||||
monthly_gb: 100,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "standard:period:3",
|
id: "standard:period:3",
|
||||||
tariff_key: "standard",
|
tariff_key: "standard",
|
||||||
tariff_name: "Стандарт",
|
tariff_name: "Стандарт",
|
||||||
|
tariff_description: "100 GB каждый месяц",
|
||||||
billing_model: "period",
|
billing_model: "period",
|
||||||
sale_mode: "subscription",
|
|
||||||
months: 3,
|
months: 3,
|
||||||
price: 400,
|
price: 400,
|
||||||
currency: "RUB",
|
currency: "RUB",
|
||||||
title: "Стандарт",
|
title: "Стандарт",
|
||||||
subtitle: "3 месяца",
|
subtitle: "3 месяца",
|
||||||
description: "100 GB каждый месяц",
|
sale_mode: "subscription",
|
||||||
monthly_gb: 100,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "business:period:1",
|
id: "business:period:1",
|
||||||
tariff_key: "business",
|
tariff_key: "business",
|
||||||
tariff_name: "Бизнес",
|
tariff_name: "Business",
|
||||||
|
tariff_description: "500 GB и premium-серверы",
|
||||||
billing_model: "period",
|
billing_model: "period",
|
||||||
sale_mode: "subscription",
|
|
||||||
months: 1,
|
months: 1,
|
||||||
price: 350,
|
price: 690,
|
||||||
currency: "RUB",
|
currency: "RUB",
|
||||||
title: "Бизнес",
|
title: "Business",
|
||||||
subtitle: "1 месяц",
|
subtitle: "1 месяц",
|
||||||
description: "300 GB и приоритетные серверы",
|
sale_mode: "subscription",
|
||||||
monthly_gb: 300,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "traffic:traffic:50",
|
|
||||||
tariff_key: "traffic",
|
|
||||||
tariff_name: "Трафик",
|
|
||||||
billing_model: "traffic",
|
|
||||||
sale_mode: "traffic_package",
|
|
||||||
months: 50,
|
|
||||||
traffic_gb: 50,
|
|
||||||
price: 799,
|
|
||||||
currency: "RUB",
|
|
||||||
title: "Трафик",
|
|
||||||
subtitle: "50 GB",
|
|
||||||
description: "Пакет без срока действия",
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
DEV_MOCK.data.tariff_change_options = {
|
DEV_MOCK.data.tariff_change_options = {
|
||||||
@@ -579,44 +628,31 @@ export function applyPreviewMock(kind) {
|
|||||||
title: "Стандарт",
|
title: "Стандарт",
|
||||||
description: "100 GB каждый месяц",
|
description: "100 GB каждый месяц",
|
||||||
billing_model: "period",
|
billing_model: "period",
|
||||||
|
monthly_gb: 100,
|
||||||
|
expires_at: "31.05.2026",
|
||||||
},
|
},
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
tariff_key: "business",
|
tariff_key: "business",
|
||||||
title: "Бизнес",
|
title: "Business",
|
||||||
description: "300 GB и приоритетные серверы",
|
description: "500 GB и premium-серверы",
|
||||||
billing_model: "period",
|
billing_model: "period",
|
||||||
monthly_gb: 300,
|
monthly_gb: 500,
|
||||||
|
price: 690,
|
||||||
|
currency: "RUB",
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
mode: "recalc_days",
|
mode: "recalc_days",
|
||||||
kind: "free",
|
kind: "free",
|
||||||
title: "recalc_days",
|
title: "Пересчитать дни",
|
||||||
days_after: 10,
|
days_after: 12,
|
||||||
remaining_days: 25,
|
remaining_days: 28,
|
||||||
},
|
|
||||||
{ mode: "paid_diff", kind: "payment", title: "paid_diff", price: 190, currency: "RUB" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tariff_key: "traffic",
|
|
||||||
title: "Трафик",
|
|
||||||
description: "Пакеты без срока действия",
|
|
||||||
billing_model: "traffic",
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
mode: "convert_days_to_gb",
|
|
||||||
kind: "free",
|
|
||||||
title: "convert_days_to_gb",
|
|
||||||
converted_gb: 18,
|
|
||||||
remaining_days: 25,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
mode: "buy_package",
|
mode: "paid_diff",
|
||||||
kind: "payment",
|
kind: "payment",
|
||||||
title: "+50 GB",
|
title: "Доплатить разницу",
|
||||||
traffic_gb: 50,
|
price: 240,
|
||||||
price: 799,
|
|
||||||
currency: "RUB",
|
currency: "RUB",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -625,10 +661,29 @@ export function applyPreviewMock(kind) {
|
|||||||
};
|
};
|
||||||
DEV_MOCK.data.topup_options = {
|
DEV_MOCK.data.topup_options = {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
topup_kind: "regular",
|
||||||
|
traffic_mode: false,
|
||||||
tariff_key: "standard",
|
tariff_key: "standard",
|
||||||
tariff_name: "Стандарт",
|
regular: {
|
||||||
traffic_percent: 86,
|
can_topup: true,
|
||||||
warning_levels: [85, 90, 95],
|
monthly_limit_gb: 100,
|
||||||
|
used_gb: 86,
|
||||||
|
available_gb: 14,
|
||||||
|
packages: [
|
||||||
|
{ gb: 10, price: 99, currency: "RUB" },
|
||||||
|
{ gb: 50, price: 399, currency: "RUB" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
premium: {
|
||||||
|
can_topup: true,
|
||||||
|
monthly_limit_gb: 25,
|
||||||
|
used_gb: 24,
|
||||||
|
available_gb: 1,
|
||||||
|
packages: [
|
||||||
|
{ gb: 10, price: 190, currency: "RUB" },
|
||||||
|
{ gb: 25, price: 390, currency: "RUB" },
|
||||||
|
],
|
||||||
|
},
|
||||||
plans: [
|
plans: [
|
||||||
{
|
{
|
||||||
id: "standard:topup:10",
|
id: "standard:topup:10",
|
||||||
@@ -643,67 +698,53 @@ export function applyPreviewMock(kind) {
|
|||||||
subtitle: "Стандарт",
|
subtitle: "Стандарт",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "standard:topup:50",
|
id: "standard:premium_topup:10",
|
||||||
tariff_key: "standard",
|
tariff_key: "standard",
|
||||||
tariff_name: "Стандарт",
|
tariff_name: "Стандарт",
|
||||||
sale_mode: "topup",
|
sale_mode: "premium_topup",
|
||||||
traffic_gb: 50,
|
traffic_gb: 10,
|
||||||
months: 50,
|
months: 10,
|
||||||
price: 399,
|
price: 190,
|
||||||
currency: "RUB",
|
currency: "RUB",
|
||||||
title: "50 GB",
|
title: "Premium 10 GB",
|
||||||
subtitle: "Стандарт",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "standard:topup:200",
|
|
||||||
tariff_key: "standard",
|
|
||||||
tariff_name: "Стандарт",
|
|
||||||
sale_mode: "topup",
|
|
||||||
traffic_gb: 200,
|
|
||||||
months: 200,
|
|
||||||
price: 1299,
|
|
||||||
currency: "RUB",
|
|
||||||
title: "200 GB",
|
|
||||||
subtitle: "Стандарт",
|
subtitle: "Стандарт",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
DEV_MOCK.data.device_topup_options = {
|
DEV_MOCK.data.device_topup_options = {
|
||||||
ok: true,
|
ok: true,
|
||||||
tariff_key: "standard",
|
current_devices: 1,
|
||||||
tariff_name: "Стандарт",
|
max_devices: 2,
|
||||||
current_limit: 5,
|
available_extra_devices: 3,
|
||||||
extra_hwid_devices: 2,
|
packages: [
|
||||||
extra_hwid_devices_valid_until_text: "01.06.2026 12:00",
|
{ count: 1, price: 120, currency: "RUB" },
|
||||||
renewal_available: false,
|
{ count: 3, price: 290, currency: "RUB" },
|
||||||
renewal_recommended_count: 0,
|
],
|
||||||
plans: [
|
plans: [
|
||||||
{
|
{
|
||||||
id: "standard:hwid:1",
|
id: "standard:hwid:1",
|
||||||
tariff_key: "standard",
|
tariff_key: "standard",
|
||||||
tariff_name: "Стандарт",
|
tariff_name: "Стандарт",
|
||||||
sale_mode: "hwid_devices",
|
sale_mode: "hwid_device",
|
||||||
device_count: 1,
|
purchased_hwid_devices: 1,
|
||||||
months: 1,
|
price: 120,
|
||||||
price: 99,
|
|
||||||
currency: "RUB",
|
currency: "RUB",
|
||||||
title: "+1",
|
title: "+1 устройство",
|
||||||
subtitle: "Стандарт",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "standard:hwid:3",
|
|
||||||
tariff_key: "standard",
|
|
||||||
tariff_name: "Стандарт",
|
|
||||||
sale_mode: "hwid_devices",
|
|
||||||
device_count: 3,
|
|
||||||
months: 3,
|
|
||||||
price: 249,
|
|
||||||
currency: "RUB",
|
|
||||||
title: "+3",
|
|
||||||
subtitle: "Стандарт",
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
} else if (mode === "depleted") {
|
||||||
|
DEV_MOCK.data.settings.traffic_mode = false;
|
||||||
|
DEV_MOCK.data.settings.trial_available = false;
|
||||||
|
if (DEMO_DATASET.plans?.length) {
|
||||||
|
const limitBytes = Number(DEMO_DATASET.currentSubscription?.traffic_limit_bytes || 0);
|
||||||
|
applyDemoTariffScenario({
|
||||||
|
traffic_used: DEMO_DATASET.currentSubscription?.traffic_limit || "150 GB",
|
||||||
|
traffic_used_bytes: limitBytes,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return;
|
||||||
} else if (mode === "devices") {
|
} else if (mode === "devices") {
|
||||||
DEV_MOCK.data.settings.my_devices_enabled = true;
|
DEV_MOCK.data.settings.my_devices_enabled = true;
|
||||||
DEV_MOCK.data.subscription = {
|
DEV_MOCK.data.subscription = {
|
||||||
|
|||||||
Reference in New Issue
Block a user