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 { DEMO_DATASET } from "./demoDataset.js";
import { withDemoAvatar, withDemoAvatarDetail, withDemoAvatarTicket } from "./demoAvatars.js";
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) {
let out = [...(DEMO_DATASET.adminUsers || [])];
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);
return {
ok: true,
users: clone(page.items),
users: clone(withDemoAvatars(page.items)),
total: page.total,
page: page.page,
page_size: page.pageSize,
@@ -309,6 +318,7 @@ function demoApiResponse(path, cleanPath, options, context) {
const id = Number(parts[3]);
const detail = DEMO_DATASET.adminUserDetails?.[String(id)];
if (!detail) return { ok: false, error: "not_found" };
const decoratedDetail = withDemoAvatarDetail(detail);
if (parts[4]) {
if (parts[4] === "telegram-profile-link") {
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") {
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") {
@@ -486,7 +496,7 @@ function demoApiResponse(path, cleanPath, options, context) {
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 };
return { ok: true, tickets: clone(withDemoAvatarTickets(page.items)), total: page.total };
}
if (cleanPath.startsWith("/admin/support/tickets/")) {
const parts = cleanPath.split("/");
@@ -515,17 +525,17 @@ function demoApiResponse(path, cleanPath, options, context) {
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) };
return { ok: true, ticket: clone(withDemoAvatarTicket(ticket)), message: clone(message) };
}
if (method === "PATCH") {
Object.assign(ticket, jsonBody(options));
return { ok: true, ticket: clone(ticket) };
return { ok: true, ticket: clone(withDemoAvatarTicket(ticket)) };
}
return {
ok: true,
ticket: clone(ticket),
ticket: clone(withDemoAvatarTicket(ticket)),
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);
return {
ok: true,
tickets: clone(page.items),
tickets: clone(withDemoAvatarTickets(page.items)),
total: page.total,
counts: demoSupportCounts(demoSupportTickets()),
};
@@ -596,9 +606,9 @@ function demoApiResponse(path, cleanPath, options, context) {
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(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") {
return {
@@ -635,7 +645,7 @@ export async function mockApi(path, options = {}, context = {}) {
normalizeLangCode,
});
if (demoResponse !== undefined) return demoResponse;
const adminUsers = [
const adminUsers = withDemoAvatars([
{
user_id: 100200300,
telegram_id: 100200300,
@@ -684,7 +694,7 @@ export async function mockApi(path, options = {}, context = {}) {
is_banned: true,
premium_traffic: { state: "none" },
},
];
]);
const supportTickets = [
{
ticket_id: 42,
+61 -23
View File
@@ -1,4 +1,5 @@
import { DEMO_DATASET } from "./demoDataset.js";
import { withDemoAvatar } from "./demoAvatars.js";
const DEMO_LANGUAGE_STORAGE_KEY = "rw_minishop_demo_language";
@@ -441,11 +442,14 @@ 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",
};
const demoUser = withDemoAvatar(
{
...(DEMO_DATASET.currentUser || {}),
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 || {});
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;
}
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) {
const mode = String(kind || "")
.trim()
@@ -745,6 +780,26 @@ export function applyPreviewMock(kind) {
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") {
DEV_MOCK.data.settings.my_devices_enabled = true;
DEV_MOCK.data.subscription = {
@@ -755,23 +810,6 @@ export function applyPreviewMock(kind) {
extra_hwid_devices_valid_until_text: "01.06.2026 12:00",
};
} else if (mode === "trial") {
DEV_MOCK.data.settings.traffic_mode = false;
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,
};
applyInactiveSubscriptionScenario({ trialAvailable: true });
}
}