feat: enrich docs demo mock data

This commit is contained in:
3252a8
2026-05-28 14:59:43 +03:00
parent 613a9860a0
commit 7e0e8ff319
4 changed files with 5118 additions and 4877 deletions
+56
View File
@@ -0,0 +1,56 @@
const DEMO_ADMIN_EMAIL = "3252a8@proton.me";
const DEMO_ADMIN_GRAVATAR_HASH = "e06e9ae24816fb1d6ed86b58e6fd00d3abe7860b8d51128b8a3e848f208d5e92";
const DEMO_ADMIN_GRAVATAR_URL = `https://www.gravatar.com/avatar/${DEMO_ADMIN_GRAVATAR_HASH}?d=mp&s=160`;
function normalizedEmail(value) {
return String(value || "")
.trim()
.toLowerCase();
}
function hasLinkedTelegram(user) {
if (!user || user.telegram_linked === false) return false;
return Boolean(user.telegram_linked || Number(user.telegram_id || 0) > 0);
}
function avatarSeed(user) {
return encodeURIComponent(
String(
user?.telegram_id || user?.user_id || user?.id || user?.username || user?.email || "user"
)
);
}
export function demoAvatarUrl(user, size = 96) {
if (!user) return "";
if (normalizedEmail(user.email) === DEMO_ADMIN_EMAIL) return DEMO_ADMIN_GRAVATAR_URL;
if (!hasLinkedTelegram(user)) return "";
return `https://i.pravatar.cc/${size}?u=remnawave-minishop-demo-${avatarSeed(user)}`;
}
export function withDemoAvatar(user, size = 96) {
if (!user || typeof user !== "object") return user;
const avatarUrl = demoAvatarUrl(user, size);
if (!avatarUrl) return user;
return {
...user,
avatar_url: avatarUrl,
telegram_photo_url: avatarUrl,
};
}
export function withDemoAvatarDetail(detail, size = 96) {
if (!detail || typeof detail !== "object") return detail;
return {
...detail,
user: withDemoAvatar(detail.user, size),
};
}
export function withDemoAvatarTicket(ticket, size = 96) {
if (!ticket || typeof ticket !== "object") return ticket;
return {
...ticket,
user: withDemoAvatar(ticket.user, size),
};
}
File diff suppressed because it is too large Load Diff
+23 -13
View File
@@ -1,5 +1,6 @@
import { DEV_MOCK } from "./previewMock.js"; import { DEV_MOCK } from "./previewMock.js";
import { DEMO_DATASET } from "./demoDataset.js"; import { DEMO_DATASET } from "./demoDataset.js";
import { withDemoAvatar, withDemoAvatarDetail, withDemoAvatarTicket } from "./demoAvatars.js";
const DEMO_LANGUAGE_STORAGE_KEY = "rw_minishop_demo_language"; const DEMO_LANGUAGE_STORAGE_KEY = "rw_minishop_demo_language";
@@ -108,6 +109,14 @@ function userName(user) {
); );
} }
function withDemoAvatars(users, size = 96) {
return (users || []).map((user) => withDemoAvatar(user, size));
}
function withDemoAvatarTickets(tickets, size = 96) {
return (tickets || []).map((ticket) => withDemoAvatarTicket(ticket, size));
}
function filterDemoUsers(params) { function filterDemoUsers(params) {
let out = [...(DEMO_DATASET.adminUsers || [])]; let out = [...(DEMO_DATASET.adminUsers || [])];
const q = (params.get("q") || params.get("search") || "").trim().toLowerCase(); const q = (params.get("q") || params.get("search") || "").trim().toLowerCase();
@@ -298,7 +307,7 @@ function demoApiResponse(path, cleanPath, options, context) {
const page = paged(filtered, params, 25); const page = paged(filtered, params, 25);
return { return {
ok: true, ok: true,
users: clone(page.items), users: clone(withDemoAvatars(page.items)),
total: page.total, total: page.total,
page: page.page, page: page.page,
page_size: page.pageSize, page_size: page.pageSize,
@@ -309,6 +318,7 @@ function demoApiResponse(path, cleanPath, options, context) {
const id = Number(parts[3]); const id = Number(parts[3]);
const detail = DEMO_DATASET.adminUserDetails?.[String(id)]; const detail = DEMO_DATASET.adminUserDetails?.[String(id)];
if (!detail) return { ok: false, error: "not_found" }; if (!detail) return { ok: false, error: "not_found" };
const decoratedDetail = withDemoAvatarDetail(detail);
if (parts[4]) { if (parts[4]) {
if (parts[4] === "telegram-profile-link") { if (parts[4] === "telegram-profile-link") {
return { ok: true, url: `https://t.me/${detail.user?.username || "demo_user"}` }; return { ok: true, url: `https://t.me/${detail.user?.username || "demo_user"}` };
@@ -316,9 +326,9 @@ function demoApiResponse(path, cleanPath, options, context) {
if (parts[4] === "message" && parts[5] === "preview") { if (parts[4] === "message" && parts[5] === "preview") {
return { ok: true, text: "Demo broadcast preview for the selected account." }; return { ok: true, text: "Demo broadcast preview for the selected account." };
} }
return { ok: true, user: clone(detail.user), detail: clone(detail) }; return { ok: true, user: clone(decoratedDetail.user), detail: clone(decoratedDetail) };
} }
return clone(detail); return clone(decoratedDetail);
} }
if (cleanPath === "/admin/logs") { if (cleanPath === "/admin/logs") {
@@ -486,7 +496,7 @@ function demoApiResponse(path, cleanPath, options, context) {
if (cleanPath === "/admin/support/tickets") { if (cleanPath === "/admin/support/tickets") {
const tickets = filterDemoSupportTickets(demoSupportTickets(), params); const tickets = filterDemoSupportTickets(demoSupportTickets(), params);
const page = paged(tickets, params, 50); const page = paged(tickets, params, 50);
return { ok: true, tickets: clone(page.items), total: page.total }; return { ok: true, tickets: clone(withDemoAvatarTickets(page.items)), total: page.total };
} }
if (cleanPath.startsWith("/admin/support/tickets/")) { if (cleanPath.startsWith("/admin/support/tickets/")) {
const parts = cleanPath.split("/"); const parts = cleanPath.split("/");
@@ -515,17 +525,17 @@ function demoApiResponse(path, cleanPath, options, context) {
ticket.last_message_at = message.created_at; ticket.last_message_at = message.created_at;
ticket.last_message_role = "admin"; ticket.last_message_role = "admin";
ticket.status = "awaiting_user"; ticket.status = "awaiting_user";
return { ok: true, ticket: clone(ticket), message: clone(message) }; return { ok: true, ticket: clone(withDemoAvatarTicket(ticket)), message: clone(message) };
} }
if (method === "PATCH") { if (method === "PATCH") {
Object.assign(ticket, jsonBody(options)); Object.assign(ticket, jsonBody(options));
return { ok: true, ticket: clone(ticket) }; return { ok: true, ticket: clone(withDemoAvatarTicket(ticket)) };
} }
return { return {
ok: true, ok: true,
ticket: clone(ticket), ticket: clone(withDemoAvatarTicket(ticket)),
messages: clone(messages), messages: clone(messages),
user_snapshot: userSnapshotForTicket(ticket), user_snapshot: userSnapshotForTicket(withDemoAvatarTicket(ticket)),
}; };
} }
@@ -564,7 +574,7 @@ function demoApiResponse(path, cleanPath, options, context) {
const page = paged(tickets, params, 50); const page = paged(tickets, params, 50);
return { return {
ok: true, ok: true,
tickets: clone(page.items), tickets: clone(withDemoAvatarTickets(page.items)),
total: page.total, total: page.total,
counts: demoSupportCounts(demoSupportTickets()), counts: demoSupportCounts(demoSupportTickets()),
}; };
@@ -596,9 +606,9 @@ function demoApiResponse(path, cleanPath, options, context) {
ticket.last_message_at = message.created_at; ticket.last_message_at = message.created_at;
ticket.last_message_role = "user"; ticket.last_message_role = "user";
ticket.status = "awaiting_admin"; ticket.status = "awaiting_admin";
return { ok: true, ticket: clone(ticket), message: clone(message) }; return { ok: true, ticket: clone(withDemoAvatarTicket(ticket)), message: clone(message) };
} }
return { ok: true, ticket: clone(ticket), messages: clone(messages) }; return { ok: true, ticket: clone(withDemoAvatarTicket(ticket)), messages: clone(messages) };
} }
if (cleanPath === "/support/unread") { if (cleanPath === "/support/unread") {
return { return {
@@ -635,7 +645,7 @@ export async function mockApi(path, options = {}, context = {}) {
normalizeLangCode, normalizeLangCode,
}); });
if (demoResponse !== undefined) return demoResponse; if (demoResponse !== undefined) return demoResponse;
const adminUsers = [ const adminUsers = withDemoAvatars([
{ {
user_id: 100200300, user_id: 100200300,
telegram_id: 100200300, telegram_id: 100200300,
@@ -684,7 +694,7 @@ export async function mockApi(path, options = {}, context = {}) {
is_banned: true, is_banned: true,
premium_traffic: { state: "none" }, premium_traffic: { state: "none" },
}, },
]; ]);
const supportTickets = [ const supportTickets = [
{ {
ticket_id: 42, ticket_id: 42,
+61 -23
View File
@@ -1,4 +1,5 @@
import { DEMO_DATASET } from "./demoDataset.js"; import { DEMO_DATASET } from "./demoDataset.js";
import { withDemoAvatar } from "./demoAvatars.js";
const DEMO_LANGUAGE_STORAGE_KEY = "rw_minishop_demo_language"; const DEMO_LANGUAGE_STORAGE_KEY = "rw_minishop_demo_language";
@@ -441,11 +442,14 @@ export const DEV_MOCK = {
function applyDemoDataset() { function applyDemoDataset() {
const storedLanguage = readStoredDemoLanguage(); const storedLanguage = readStoredDemoLanguage();
const demoUser = { const demoUser = withDemoAvatar(
...(DEMO_DATASET.currentUser || {}), {
id: DEMO_DATASET.currentUser?.id ?? DEMO_DATASET.currentUser?.user_id, ...(DEMO_DATASET.currentUser || {}),
language_code: storedLanguage || DEMO_DATASET.currentUser?.language_code || "ru", id: DEMO_DATASET.currentUser?.id ?? DEMO_DATASET.currentUser?.user_id,
}; language_code: storedLanguage || DEMO_DATASET.currentUser?.language_code || "ru",
},
160
);
Object.assign(DEV_MOCK.config, DEMO_DATASET.config || {}); Object.assign(DEV_MOCK.config, DEMO_DATASET.config || {});
DEV_MOCK.config.language = demoUser.language_code || "ru"; DEV_MOCK.config.language = demoUser.language_code || "ru";
@@ -488,6 +492,37 @@ function applyDemoTariffScenario(subscriptionPatch = {}) {
DEMO_DATASET.device_topup_options || DEV_MOCK.data.device_topup_options; DEMO_DATASET.device_topup_options || DEV_MOCK.data.device_topup_options;
} }
function applyInactiveSubscriptionScenario({ trialAvailable = false } = {}) {
DEV_MOCK.data.settings.traffic_mode = false;
DEV_MOCK.data.settings.trial_enabled = true;
DEV_MOCK.data.settings.trial_available = Boolean(trialAvailable);
DEV_MOCK.data.settings.trial_duration_days = 5;
DEV_MOCK.data.settings.trial_traffic_limit_gb = 10;
DEV_MOCK.data.subscription = {
...DEV_MOCK.data.subscription,
active: false,
status: "INACTIVE",
remaining_text: "Подписка не активна",
end_date_text: "",
days_left: 0,
config_link: null,
connect_url: null,
panel_short_uuid: "",
install_share_token: "",
install_share_url: "",
traffic_used: "0 B",
traffic_limit: "0 GB",
traffic_used_bytes: 0,
traffic_limit_bytes: 0,
premium_used_bytes: 0,
premium_limit_bytes: 0,
premium_is_limited: false,
};
DEV_MOCK.data.plans = DEMO_DATASET.plans || DEV_MOCK.data.plans;
DEV_MOCK.data.tariff_change_options =
DEMO_DATASET.tariff_change_options || DEV_MOCK.data.tariff_change_options;
}
export function applyPreviewMock(kind) { export function applyPreviewMock(kind) {
const mode = String(kind || "") const mode = String(kind || "")
.trim() .trim()
@@ -745,6 +780,26 @@ export function applyPreviewMock(kind) {
return; return;
} }
return; return;
} else if (mode === "no-subscription" || mode === "inactive") {
applyInactiveSubscriptionScenario();
} else if (mode === "expiring" || mode === "ending-soon") {
DEV_MOCK.data.settings.traffic_mode = false;
DEV_MOCK.data.settings.trial_available = false;
if (DEMO_DATASET.plans?.length) {
applyDemoTariffScenario({
remaining_text: "2 д.",
end_date_text: "30.05.2026",
days_left: 2,
});
return;
}
DEV_MOCK.data.subscription = {
...DEV_MOCK.data.subscription,
active: true,
remaining_text: "2 д.",
end_date_text: "30.05.2026",
days_left: 2,
};
} 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 = {
@@ -755,23 +810,6 @@ export function applyPreviewMock(kind) {
extra_hwid_devices_valid_until_text: "01.06.2026 12:00", extra_hwid_devices_valid_until_text: "01.06.2026 12:00",
}; };
} else if (mode === "trial") { } else if (mode === "trial") {
DEV_MOCK.data.settings.traffic_mode = false; applyInactiveSubscriptionScenario({ trialAvailable: true });
DEV_MOCK.data.settings.trial_enabled = true;
DEV_MOCK.data.settings.trial_available = true;
DEV_MOCK.data.settings.trial_duration_days = 5;
DEV_MOCK.data.settings.trial_traffic_limit_gb = 10;
DEV_MOCK.data.subscription = {
active: false,
status: "INACTIVE",
remaining_text: "Подписка не активна",
end_date_text: "",
days_left: 0,
config_link: null,
connect_url: null,
traffic_used: "0 B",
traffic_limit: "10 GB",
traffic_used_bytes: 0,
traffic_limit_bytes: 10737418240,
};
} }
} }