UNPKG

denwa-web-shared

Version:

A shared library for Next.js App Router projects containing reusable components, hooks, schemas, and utilities.

1,774 lines • 63.7 kB
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { useEffect, useRef, useState } from "react";
import { useIntersection, useMedia, useWindowScroll } from "react-use";
import { useInView } from "motion/react";
import { create } from "zustand";
import { jsx, jsxs } from "react/jsx-runtime";
import queryString from "query-string";
import axios from "axios";
//#region src/shared/constants/index.ts
var TIME = {
	seconds: { minutes10: 600 },
	milliseconds: {
		milliseconds100: 100,
		milliseconds200: 200,
		milliseconds500: 500,
		seconds1: 1e3,
		seconds2: 2e3,
		seconds5: 5e3,
		minutes1: 6e4
	}
};
var THEME = {
	VIEW_PORT: {
		EXTRA_SMALL: 280,
		SMALL: 320,
		SMALL_MOBILE: 380,
		MOBILE: 450,
		MEDIUM: 500,
		EXTRA_MEDIUM: 600,
		TABLET: 768,
		LAPTOP: 1024,
		LAPTOP_BIG: 1200,
		BIG: 1440,
		VERY_BIG: 1920
	},
	OFFSET: {
		1: 8,
		2: 16,
		3: 24,
		4: 32,
		5: 40,
		6: 48,
		7: 56,
		8: 64
	}
};
var CITY_MASK = {
	CITY: "{{CITY}}",
	CITY_DECL: "{{CITY_DECL}}",
	CITY_REGION: "{{CITY_REGION}}",
	CITY_REGION_DECL: "{{CITY_REGION_DECL}}"
};
//#endregion
//#region src/shared/lib/css.ts
function cn(...inputs) {
	return twMerge(clsx(inputs));
}
//#endregion
//#region src/client/hooks/use-disable-scroll.ts
var useDisableScroll = (isDisableScroll) => {
	useEffect(() => {
		if (!isDisableScroll || typeof document === "undefined") return;
		const originalOverflow = document.body.style.overflow;
		const originalPaddingRight = document.body.style.paddingRight;
		const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;
		if (scrollBarWidth > 0) document.body.style.paddingRight = `${scrollBarWidth}px`;
		document.body.style.overflow = "hidden";
		return () => {
			document.body.style.overflow = originalOverflow;
			document.body.style.paddingRight = originalPaddingRight;
		};
	}, [isDisableScroll]);
};
//#endregion
//#region src/client/hooks/use-is-client.ts
var useIsClient = () => {
	const [isClient, setIsClient] = useState(false);
	useEffect(() => {
		setIsClient(true);
	}, []);
	return isClient;
};
//#endregion
//#region src/client/hooks/use-view-port.ts
var useLaptopBigViewPort = () => {
	return {
		isLaptopBigMinWidth: useMedia(`(min-width: ${THEME.VIEW_PORT.LAPTOP_BIG}px)`),
		isLaptopBigMaxWidth: useMedia(`(max-width: ${THEME.VIEW_PORT.LAPTOP_BIG}px)`)
	};
};
var useLaptopViewPort = () => {
	return {
		isLaptopMinWidth: useMedia(`(min-width: ${THEME.VIEW_PORT.LAPTOP}px)`),
		isLaptopMaxWidth: useMedia(`(max-width: ${THEME.VIEW_PORT.LAPTOP}px)`)
	};
};
var useTabletViewPort = () => {
	return {
		isTabletMinWidth: useMedia(`(min-width: ${THEME.VIEW_PORT.TABLET}px)`),
		isTabletMaxWidth: useMedia(`(max-width: ${THEME.VIEW_PORT.TABLET}px)`)
	};
};
var useExtraMediumViewPort = () => {
	return {
		isExtraMediumMinWidth: useMedia(`(min-width: ${THEME.VIEW_PORT.EXTRA_MEDIUM}px)`),
		isExtraMediumMaxWidth: useMedia(`(max-width: ${THEME.VIEW_PORT.EXTRA_MEDIUM}px)`)
	};
};
var useMobileViewPort = () => {
	return {
		isMobileMinWidth: useMedia(`(min-width: ${THEME.VIEW_PORT.MOBILE}px)`),
		isMobileMaxWidth: useMedia(`(max-width: ${THEME.VIEW_PORT.MOBILE}px)`)
	};
};
var useSmallViewPort = () => {
	return {
		isSmallMinWidth: useMedia(`(min-width: ${THEME.VIEW_PORT.SMALL}px)`),
		isSmallMaxWidth: useMedia(`(max-width: ${THEME.VIEW_PORT.SMALL}px)`)
	};
};
//#endregion
//#region src/client/hooks/use-appear-animation.ts
var useAppearAnimation = ({ once = true, margin = "-100px", delay = .1, direction = "up", animationDistance = 50, duration = .8, ease = "easeOut" } = {}) => {
	const ref = useRef(null);
	const isInView = useInView(ref, {
		once,
		margin
	});
	const getInitialState = () => {
		switch (direction) {
			case "up": return {
				opacity: 0,
				y: animationDistance
			};
			case "down": return {
				opacity: 0,
				y: -animationDistance
			};
			case "left": return {
				opacity: 0,
				x: animationDistance
			};
			case "right": return {
				opacity: 0,
				x: -animationDistance
			};
			case "scale": return {
				opacity: 0,
				scale: .8
			};
			case "fade": return { opacity: 0 };
			default: return {
				opacity: 0,
				y: animationDistance
			};
		}
	};
	const getAnimateState = () => {
		switch (direction) {
			case "up":
			case "down": return {
				opacity: 1,
				y: 0
			};
			case "left":
			case "right": return {
				opacity: 1,
				x: 0
			};
			case "scale": return {
				opacity: 1,
				scale: 1
			};
			case "fade": return { opacity: 1 };
			default: return {
				opacity: 1,
				y: 0
			};
		}
	};
	return {
		ref,
		isInView,
		motionProps: {
			initial: getInitialState(),
			animate: isInView ? getAnimateState() : once ? void 0 : getInitialState(),
			transition: {
				duration,
				ease,
				delay
			}
		}
	};
};
//#endregion
//#region src/shared/lib/schema.ts
/**
* Минимальная замена zod для внутренних схем пакета.
*
* Почему не zod: обе точки входа пакета (`/server` и `/client`) импортируют его
* статически, а Vite собирает каждую точку входа в один файл. Поэтому у
* потребителя tree-shaking не может выбросить неиспользуемые схемы, и zod
* (~390 КБ в несжатом виде) попадал в бандл каждой страницы любого проекта,
* где из пакета берут хоть что-нибудь — например, `cn`.
*
* Здесь реализован только тот срез API, который нужен схемам пакета:
* `parse` и `safeParse`. Семантика повторяет zod:
* - неизвестные ключи отбрасываются;
* - значения по умолчанию подставляются (`default`);
* - ключ с неопределённым значением в результат не попадает;
* - при несоответствии `parse` бросает `ValidationError`, а `safeParse`
*   возвращает `{ success: false, error }`.
*
* Чего нет по сравнению с zod: `z.infer`, `.refine()`, трансформаций и
* разбора строк. Если понадобится что-то из этого — лучше вернуть zod
* отдельной точкой входа, а не растить этот файл.
*/
/** Ошибка валидации. Заменяет `ZodError`. */
var ValidationError = class extends Error {
	constructor(message) {
		super(message);
		this.name = "ValidationError";
	}
};
var TYPE_OBJECT = "объект";
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
var isString = (value) => typeof value === "string";
/** `z.number()` в zod не пропускает NaN — повторяем. */
var isNumber = (value) => typeof value === "number" && !Number.isNaN(value);
/** Прерывает валидацию с сообщением, указывающим путь до поля. */
var fail = (path, expected) => {
	throw new ValidationError(`${path || "value"}: ожидалось ${expected}`);
};
/** `z.string()` */
var str = (value, path) => isString(value) ? value : fail(path, "строка");
/** `z.number()` */
var num = (value, path) => isNumber(value) ? value : fail(path, "число");
/** `z.any()` — пропускает всё, включая undefined. */
var anyField = (value) => value;
/** `z.string().array()` */
var arr = (field) => (value, path) => Array.isArray(value) ? value.map((item, index) => field(item, `${path}[${index}]`)) : fail(path, "массив");
/** `z.object({ ... })` — собирает объект только из объявленных ключей. */
var obj = (shape) => (value, path) => {
	if (!isObject(value)) return fail(path, TYPE_OBJECT);
	const entries = Object.entries(shape);
	const result = {};
	for (const [key, field] of entries) {
		const parsed = field(value[key], path ? `${path}.${key}` : key);
		if (parsed !== void 0) result[key] = parsed;
	}
	return result;
};
/** `.optional()` */
var optional = (field) => (value, path) => value === void 0 ? void 0 : field(value, path);
/** `.nullable()` */
var nullable = (field) => (value, path) => value === null ? null : field(value, path);
/** `.default(...)` — подставляет значение только вместо `undefined`. */
var withDefault = (field, fallback) => (value, path) => value === void 0 ? fallback() : field(value, path);
/** Оборачивает проверку в объект с `parse` / `safeParse`. */
var createSchema = (field) => ({
	parse: (value) => field(value, ""),
	safeParse: (value) => {
		try {
			return {
				success: true,
				data: field(value, "")
			};
		} catch (error) {
			return {
				success: false,
				error: error instanceof ValidationError ? error : new ValidationError(String(error))
			};
		}
	}
});
//#endregion
//#region src/client/session/session-schema.ts
var tokenField = obj({
	token: withDefault(optional(str), () => ""),
	exp: optional(num)
});
var tokensField = obj({
	accessToken: optional(tokenField),
	refreshToken: optional(tokenField)
});
var sessionTokensSchema = createSchema(tokensField);
var sessionUserSchema = createSchema(obj({
	id: str,
	profileId: str,
	phone: optional(nullable(str)),
	email: optional(nullable(str)),
	name: optional(nullable(str)),
	surname: optional(nullable(str)),
	roles: withDefault(optional(arr(str)), () => []),
	maxRolePriority: optional(nullable(num)),
	tokens: optional(tokensField)
}));
/**
* Парсит сырое значение сессионной куки (JSON) и валидирует схемой.
* Возвращает null при отсутствии значения или невалидных данных.
*/
var parseSessionCookie = (rawValue) => {
	if (!rawValue) return null;
	try {
		return sessionUserSchema.parse(JSON.parse(rawValue));
	} catch {
		return null;
	}
};
/**
* Инициалы пользователя: ФамилияИмя → «ИФ», при отсутствии — первая буква email.
*/
var getUserInitials = (user) => {
	if (!user) return "";
	const surnameLetter = user.surname?.trim()?.[0]?.toUpperCase() ?? "";
	const nameLetter = user.name?.trim()?.[0]?.toUpperCase() ?? "";
	if (surnameLetter && nameLetter) return `${surnameLetter}${nameLetter}`;
	if (surnameLetter) return surnameLetter;
	if (nameLetter) return nameLetter;
	if (user.email?.trim()) return user.email.trim()[0].toUpperCase();
	return "";
};
/**
* Отображаемое имя: «Фамилия Имя», при отсутствии — email или телефон.
*/
var getUserDisplayName = (user) => {
	if (!user) return "";
	const parts = [user.surname?.trim(), user.name?.trim()].filter(Boolean);
	if (parts.length > 0) return parts.join(" ");
	return user.email || user.phone || "";
};
//#endregion
//#region src/client/session/token-manager.ts
var sessionConfig = {
	refreshUrl: "",
	cookieDomain: void 0,
	isNotSecure: false,
	sessionCookieName: "session",
	sessionTempCookieName: "session-temp",
	accessTokenCookieName: "access_token",
	refreshTokenCookieName: "refresh_token"
};
/**
* Конфигурация сессии. Вызывается один раз при инициализации приложения:
* configureSession({ refreshUrl: '/api-rest/auth/v1/refresh' })
*/
var configureSession = (config) => {
	sessionConfig = {
		...sessionConfig,
		...config,
		sessionCookieName: config.sessionCookieName || sessionConfig.sessionCookieName,
		sessionTempCookieName: config.sessionTempCookieName || sessionConfig.sessionTempCookieName,
		accessTokenCookieName: config.accessTokenCookieName || sessionConfig.accessTokenCookieName,
		refreshTokenCookieName: config.refreshTokenCookieName || sessionConfig.refreshTokenCookieName
	};
};
var getCookieValue = (name) => {
	if (typeof document === "undefined") return null;
	const cookies = document.cookie.split(";");
	for (const cookie of cookies) {
		const trimmed = cookie.trim();
		const separatorIndex = trimmed.indexOf("=");
		if (separatorIndex === -1) continue;
		if (trimmed.slice(0, separatorIndex) === name) return decodeURIComponent(trimmed.slice(separatorIndex + 1));
	}
	return null;
};
var setCookieValue = (name, value, options = {}) => {
	if (typeof document === "undefined") return;
	const maxAge = options.maxAgeSeconds ?? 720 * 60 * 60;
	const isHttps = typeof window !== "undefined" && window.location.protocol === "https:" && !options.isNotSecure && !sessionConfig.isNotSecure;
	const domain = options.domain ?? sessionConfig.cookieDomain;
	const domainPart = domain ? `; Domain=${domain}` : "";
	document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=${maxAge}; SameSite=Lax${isHttps ? "; Secure" : ""}${domainPart}`;
};
var deleteCookieValue = (name, options = {}) => {
	setCookieValue(name, "", {
		...options,
		maxAgeSeconds: -1
	});
	if (options.domain || sessionConfig.cookieDomain) setCookieValue(name, "", {
		...options,
		domain: void 0,
		maxAgeSeconds: -1
	});
};
/**
* Удаляет все cookies сессии и токенов и оповещает слушателей о логауте.
*/
var clearSessionCookies = (options = {}) => {
	if (typeof document === "undefined") return;
	const opts = {
		domain: options.domain ?? sessionConfig.cookieDomain,
		isNotSecure: options.isNotSecure ?? sessionConfig.isNotSecure
	};
	deleteCookieValue(sessionConfig.sessionCookieName, opts);
	deleteCookieValue(sessionConfig.sessionTempCookieName, opts);
	deleteCookieValue(sessionConfig.accessTokenCookieName, opts);
	deleteCookieValue(sessionConfig.refreshTokenCookieName, opts);
	if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent("session:updated", { detail: null }));
};
/**
* Полная сессия из куки с указанием имени куки (session или session-temp).
*/
var getFullSessionFromCookie = () => {
	if (typeof document === "undefined") return null;
	const sessionStr = getCookieValue(sessionConfig.sessionCookieName);
	if (sessionStr) {
		const data = parseSessionCookie(sessionStr);
		if (data) return {
			cookieName: sessionConfig.sessionCookieName,
			data
		};
	}
	const sessionTempStr = getCookieValue(sessionConfig.sessionTempCookieName);
	if (sessionTempStr) {
		const data = parseSessionCookie(sessionTempStr);
		if (data) return {
			cookieName: sessionConfig.sessionTempCookieName,
			data
		};
	}
	return null;
};
/**
* Пользователь сессии из куки (без токенов-фолбэков).
*/
var getSessionFromCookie = () => {
	if (typeof document === "undefined") return null;
	return parseSessionCookie(getCookieValue(sessionConfig.sessionCookieName)) || parseSessionCookie(getCookieValue(sessionConfig.sessionTempCookieName));
};
var parseJwtExp = (token) => {
	if (!token) return null;
	try {
		const parts = token.split(".");
		if (parts.length !== 3) return null;
		const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
		const payload = JSON.parse(atob(base64.padEnd(Math.ceil(base64.length / 4) * 4, "=")));
		return typeof payload.exp === "number" ? payload.exp * 1e3 : null;
	} catch {
		return null;
	}
};
var isTokenExpired = (exp) => {
	if (!exp) return true;
	return exp - Date.now() <= 3e4;
};
var refreshPromise = null;
var getMaxAgeSeconds = (refreshExp, isTemp = false) => {
	if (refreshExp) {
		const secondsLeft = Math.floor((refreshExp - Date.now()) / 1e3);
		if (secondsLeft > 0) return secondsLeft;
	}
	return isTemp ? 1440 * 60 : 720 * 60 * 60;
};
/**
* Обновление пары токенов. Single-flight: параллельные вызовы разделяют один запрос.
*/
var refreshSessionTokens = async () => {
	if (typeof window === "undefined") return null;
	if (refreshPromise) return refreshPromise;
	refreshPromise = (async () => {
		try {
			const current = getFullSessionFromCookie();
			const directRefreshToken = getCookieValue(sessionConfig.refreshTokenCookieName);
			const refreshToken = current?.data?.tokens?.refreshToken?.token || directRefreshToken;
			if (!refreshToken || !sessionConfig.refreshUrl) return null;
			const refreshExp = current?.data?.tokens?.refreshToken?.exp || parseJwtExp(refreshToken || void 0);
			if (refreshExp && refreshExp <= Date.now()) {
				clearSessionCookies();
				return null;
			}
			const response = await fetch(sessionConfig.refreshUrl, {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
					Authorization: `Bearer ${refreshToken}`
				}
			});
			if (response.status === 401 || response.status === 403) {
				clearSessionCookies();
				return null;
			}
			if (!response.ok) return null;
			const payload = (await response.json())?.data;
			if (!payload) return null;
			const newAccessToken = payload.tokens?.accessToken?.token;
			const newRefreshToken = payload.tokens?.refreshToken?.token;
			const newAccessExp = payload.tokens?.accessToken?.exp;
			const newRefreshExp = payload.tokens?.refreshToken?.exp;
			if (!newAccessToken) return null;
			const cookieName = current?.cookieName || sessionConfig.sessionCookieName;
			const updatedSessionData = {
				id: payload.id || current?.data?.id || "",
				profileId: payload.profileId || current?.data?.profileId || "",
				phone: payload.phone ?? current?.data?.phone,
				email: payload.email ?? current?.data?.email,
				name: payload.name ?? current?.data?.name,
				surname: payload.surname ?? current?.data?.surname,
				roles: payload.roles ?? current?.data?.roles ?? [],
				maxRolePriority: payload.maxRolePriority ?? current?.data?.maxRolePriority,
				tokens: {
					accessToken: {
						token: newAccessToken,
						exp: newAccessExp
					},
					refreshToken: {
						token: newRefreshToken || refreshToken,
						exp: newRefreshExp
					}
				}
			};
			const maxAgeSeconds = getMaxAgeSeconds(newRefreshExp, cookieName === sessionConfig.sessionTempCookieName);
			setCookieValue(cookieName, JSON.stringify(updatedSessionData), { maxAgeSeconds });
			if (newAccessToken) {
				const accessMaxAge = newAccessExp ? Math.max(0, Math.floor((newAccessExp - Date.now()) / 1e3)) : 900;
				setCookieValue(sessionConfig.accessTokenCookieName, newAccessToken, { maxAgeSeconds: accessMaxAge });
			}
			if (newRefreshToken) setCookieValue(sessionConfig.refreshTokenCookieName, newRefreshToken, { maxAgeSeconds });
			window.dispatchEvent(new CustomEvent("session:updated", { detail: updatedSessionData }));
			return newAccessToken;
		} catch {
			return null;
		} finally {
			refreshPromise = null;
		}
	})();
	return refreshPromise;
};
/**
* Возвращает валидный access-токен: актуальный — сразу, протухший — через refresh.
*/
var getValidAccessToken = async () => {
	if (typeof window === "undefined") return null;
	const current = getFullSessionFromCookie();
	const directAccessToken = getCookieValue(sessionConfig.accessTokenCookieName);
	const directRefreshToken = getCookieValue(sessionConfig.refreshTokenCookieName);
	const accessToken = current?.data?.tokens?.accessToken?.token || directAccessToken;
	const refreshToken = current?.data?.tokens?.refreshToken?.token || directRefreshToken;
	if (!accessToken && !refreshToken) return null;
	const refreshExp = current?.data?.tokens?.refreshToken?.exp || parseJwtExp(refreshToken || void 0);
	if (refreshExp && refreshExp <= Date.now()) {
		clearSessionCookies();
		return null;
	}
	const exp = current?.data?.tokens?.accessToken?.exp || parseJwtExp(accessToken || void 0);
	if (accessToken && !isTokenExpired(exp)) return accessToken;
	if (refreshToken) return refreshSessionTokens();
	return null;
};
//#endregion
//#region src/client/session/use-session.ts
var useSession = create((set) => {
	if (typeof window !== "undefined") window.addEventListener("session:updated", (event) => {
		set({
			user: event.detail ?? null,
			isLoaded: true
		});
	});
	return {
		user: null,
		isLoaded: false,
		initSession: () => {
			set({
				user: getSessionFromCookie(),
				isLoaded: true
			});
		},
		setUser: (user) => set({
			user,
			isLoaded: true
		})
	};
});
//#endregion
//#region src/client/ui/header-wrapper.tsx
var HeaderWrapper = ({ children, className, hideClassName, scrollPosition = 100, hideBackgroundOnScroll = true }) => {
	const { y } = useWindowScroll();
	const isClient = useIsClient();
	const isHiddenBackground = hideBackgroundOnScroll && y > scrollPosition && isClient;
	return /* @__PURE__ */ jsx("header", {
		className: cn("fixed z-30 w-full transition bg-background", className, { "bg-transparent": isHiddenBackground }, isHiddenBackground && hideClassName),
		children
	});
};
//#endregion
//#region src/client/ui/infinity-list.tsx
var InfinityList = ({ children, intersectionElementClassName, isNext, intersectionElement, onIntersection }) => {
	const intersectionRef = useRef(null);
	const intersection = useIntersection(intersectionRef, {
		root: null,
		rootMargin: "0px",
		threshold: 1
	});
	useEffect(() => {
		if (!intersection?.isIntersecting || !isNext) return;
		onIntersection();
	}, [intersection?.isIntersecting, isNext]);
	return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", { children }), /* @__PURE__ */ jsx("div", {
		className: cn(intersectionElementClassName, { hidden: !isNext }),
		ref: intersectionRef,
		children: typeof intersectionElement === "function" ? intersectionElement() : intersectionElement
	})] });
};
//#endregion
//#region src/server/lib/utils.ts
/**
* @description Хелпер для генерации путей к адаптивным статичным изображениям (PictureData)
* @param basePath - Базовый путь к директории (например, '/compressed/cta')
* @param filename - Имя файла без расширения (например, 'cta-arrow')
* @param ext - Расширение исходного fallback-файла (по умолчанию 'jpg')
* @return Объект PictureData со всеми нужными форматами
*/
var getImageData = (basePath, filename, ext) => ({
	image1x: `${basePath}/${filename}.${ext}`,
	image2x: `${basePath}/${filename}_2x.${ext}`,
	image1xWebp: `${basePath}/${filename}.webp`,
	image2xWebp: `${basePath}/${filename}_2x.webp`,
	image1xAvif: `${basePath}/${filename}.avif`,
	image2xAvif: `${basePath}/${filename}_2x.avif`,
	mobileImage1x: `${basePath}/${filename}_mobile.${ext}`,
	mobileImage2x: `${basePath}/${filename}_mobile_2x.${ext}`,
	mobileImage1xWebp: `${basePath}/${filename}_mobile.webp`,
	mobileImage2xWebp: `${basePath}/${filename}_mobile_2x.webp`,
	mobileImage1xAvif: `${basePath}/${filename}_mobile.avif`,
	mobileImage2xAvif: `${basePath}/${filename}_mobile_2x.avif`
});
/**
* @description Получить Intl для нужного языка
* @param locale - Локаль
* @param options - Опции
*/
var getNumberFormatter = (locale, options) => {
	return new Intl.NumberFormat(locale, options);
};
/**
* @description Получить Intl даты для нужного языка
* @param locale - Локаль
* @param options - Опции
*/
var getDateFormatter = (locale, options) => {
	return new Intl.DateTimeFormat(locale, options || {
		year: "numeric",
		month: "long",
		day: "numeric"
	});
};
/**
* @description Получить значение по ключу
* @param obj - Объект
* @param key - Ключ
*/
var getByKey = (obj, key) => {
	return obj[key];
};
/**
* @description Преобразует value у инпута в поле маски телефона
* @param eventValue - исходное значение инпута
* @response Возвращает либо обработанную строку, либо пустую строку
* @example
* 79881234567 -> +79881234567
* 89881234567 -> +79881234567
* 19881234567 -> +19881234567
* 8 (988) 505-42-19 -> +79885054219
*/
var convertPhoneMask = (eventValue) => {
	let digits = eventValue.replace(/\D/g, "");
	if (digits.length === 0) return "";
	if (digits.length === 11 && digits.startsWith("8")) digits = `7${digits.slice(1)}`;
	return `+${digits.slice(0, 15)}`;
};
/**
* @description Получить корректный поддомен из адреса хоста
* @param host - url хоста
* @param SUB_DOMAIN - Объект ключ-значение со списком поддоменов
*/
var getSubdomain = (host, SUB_DOMAIN) => {
	if (!host) return SUB_DOMAIN.main;
	const item = SUB_DOMAIN[host.split(".")[0]];
	if (!item) return SUB_DOMAIN.main;
	return item;
};
/**
* @description Заменяет текст по маске {{}}
* @param text - Исходный текст
* @param subdomain - Значение города, которое подставится
* @param SUBDOMAIN_NAME - Объект ключ-значение со списком поддоменов
* @param SUBDOMAIN_MASK - Объект ключ-значение со списком масок поддоменов
* @return Обновленная строка
*/
var updateTextByTemplate = ({ text, subdomain, SUBDOMAIN_NAME, SUBDOMAIN_MASK }) => {
	if (!subdomain || !SUBDOMAIN_NAME || !SUBDOMAIN_MASK) return text;
	const key = SUBDOMAIN_NAME[subdomain];
	if (!subdomain || !key) return text;
	const subdomainData = getByKey(SUBDOMAIN_NAME, subdomain);
	if (!subdomainData) return text;
	let result = text;
	const { name, declination, region, regionDeclination } = subdomainData;
	const replaceAllSafe = (str, search, replacement) => str.split(search).join(replacement);
	result = replaceAllSafe(result, SUBDOMAIN_MASK.CITY, name);
	result = replaceAllSafe(result, SUBDOMAIN_MASK.CITY_DECL, declination);
	result = replaceAllSafe(result, SUBDOMAIN_MASK.CITY_REGION, region);
	result = replaceAllSafe(result, SUBDOMAIN_MASK.CITY_REGION_DECL, regionDeclination);
	return result;
};
/**
* @description Заменят мета текст по маске, переданный напрямую
* @param subdomain - Поддомен
* @param metaData - Объект с title, descrption, keywords
* @param host - Хост сайта
* @param lang - Объект для локализации
* @param isSubdomain - Нужны ли поддомены в адресе хоста
* @param SUBDOMAIN_NAME - Объект ключ-значение со списком поддоменов
* @param SUBDOMAIN_MASK - Объект ключ-значение со списком масок поддоменов
* @param DEFAULT_SEO_TEXT - Объект ключ-значение со списком масок поддоменов
* @return Объект с мета тегами
*/
var prepareLocalMetaData = ({ subdomain, metaData, host, lang, isSubdomain = true, SUBDOMAIN_NAME, SUBDOMAIN_MASK, DEFAULT_SEO_TEXT }) => {
	const { title, description, keywords, canonical } = metaData;
	let correctHost = subdomain === "main" ? host : `${subdomain}.${host}`;
	if (!isSubdomain) correctHost = host;
	let currentTitle;
	let currentDescription;
	let currentKeywords;
	const alternates = {};
	if (title) currentTitle = updateTextByTemplate({
		text: title,
		subdomain,
		SUBDOMAIN_MASK,
		SUBDOMAIN_NAME
	});
	else currentTitle = updateTextByTemplate({
		text: DEFAULT_SEO_TEXT.title,
		subdomain,
		SUBDOMAIN_MASK,
		SUBDOMAIN_NAME
	});
	if (description) currentDescription = updateTextByTemplate({
		text: description,
		subdomain,
		SUBDOMAIN_MASK,
		SUBDOMAIN_NAME
	});
	else currentDescription = updateTextByTemplate({
		text: DEFAULT_SEO_TEXT.description,
		subdomain,
		SUBDOMAIN_MASK,
		SUBDOMAIN_NAME
	});
	if (keywords) currentKeywords = updateTextByTemplate({
		text: keywords,
		subdomain,
		SUBDOMAIN_MASK,
		SUBDOMAIN_NAME
	});
	else currentKeywords = updateTextByTemplate({
		text: DEFAULT_SEO_TEXT.keywords,
		subdomain,
		SUBDOMAIN_MASK,
		SUBDOMAIN_NAME
	});
	if (canonical !== void 0) alternates.canonical = `https://${correctHost}${canonical}`;
	if (lang) {
		const langs = lang.locales.filter((item) => item !== lang.locale);
		const languages = {};
		langs.forEach((item) => {
			let url = `https://${correctHost}/${item}`;
			if (lang.city) url += `/${lang.city}`;
			url += lang.route[0] === "/" ? lang.route : `/${lang.route}`;
			languages[item] = url;
		});
		let url = ``;
		if (lang.locale !== lang.defaultLocale) url += `/${lang.locale}`;
		if (lang.city) url += `/${lang.city}`;
		url += lang.route[0] === "/" ? lang.route : `/${lang.route}`;
		alternates.canonical = `https://${correctHost}${url}`;
		alternates.languages = languages;
	}
	return {
		title: currentTitle,
		description: currentDescription,
		keywords: currentKeywords,
		alternates: Object.keys(alternates).length ? alternates : void 0
	};
};
/**
* @description Превращает строку формата /catalog/price-from__1000--price-to__5000 в { price-from: '1000', price-to: '5000' }
* @param input - изначальная строка
* @return Объект с парами ключ-значение
*/
var parseStringToKeyValue = (input) => {
	const keyValuePairs = decodeURIComponent(input).replace(/^\/[^\/]*\//, "").split("--");
	const result = {};
	keyValuePairs.forEach((pair) => {
		const [key, value] = pair.split("__");
		if (key && value) result[key] = value.replace(/_/g, " ");
	});
	return result;
};
/**
* @description Конвентирует цвет из enum в строку
* @param color1 - цвет 1
* @param color2 - цвет 2
* @param notFoundText - текст при отсутствии цвета
* @param COLORS_NAMES - Объект ключ-значение со списком названий цветов
* @return Строка с названием цветов
*/
var prepareColor = ({ color1, color2, notFoundText, COLORS_NAMES }) => {
	const name1 = COLORS_NAMES[color1];
	const name2 = color2 ? COLORS_NAMES[color2] : null;
	if (name1 && name2) return `${name1}/${name2}`;
	else if (name1) return name1;
	else return notFoundText;
};
/**
* @description Подготавливает серверную пагинацию для клиента
* @param pagination - Серверная пагинация
* @param baseUrl - Url страницы
* @param initialParams - Параметры url
*/
var generatePaginationArray = (pagination, baseUrl, initialParams) => {
	if (!pagination) return { buttons: [] };
	const { page, pages } = pagination;
	if (!page || !pages) return { buttons: [] };
	const [cleanBaseUrl, queryFromUrl] = baseUrl.split("?");
	const baseParams = queryFromUrl ? Object.fromEntries(new URLSearchParams(queryFromUrl)) : {};
	const params = new URLSearchParams({
		...baseParams,
		...initialParams
	});
	const paginationArray = [];
	const maxVisibleButtons = 8;
	const halfMaxVisibleButtons = Math.floor(maxVisibleButtons / 2);
	let startPage = Math.max(1, page - halfMaxVisibleButtons);
	let endPage = Math.min(pages, page + halfMaxVisibleButtons);
	if (endPage - startPage + 1 < maxVisibleButtons) {
		startPage = Math.max(1, endPage - maxVisibleButtons + 1);
		endPage = Math.min(pages, startPage + maxVisibleButtons - 1);
	}
	for (let i = startPage; i <= endPage; i++) {
		const currentParams = new URLSearchParams(params);
		currentParams.set("page", i.toString());
		const qs = currentParams.toString();
		paginationArray.push({
			label: i.toString(),
			href: qs ? `${cleanBaseUrl}?${qs}` : cleanBaseUrl,
			isActive: i === page
		});
	}
	const containsFirstPage = paginationArray.some((btn) => btn.label === "1");
	const firstParams = new URLSearchParams(params);
	firstParams.set("page", "1");
	const firstPage = containsFirstPage ? void 0 : {
		label: "1",
		href: `${cleanBaseUrl}?${firstParams.toString()}`,
		isActive: page === 1
	};
	const containsLastPage = paginationArray.some((btn) => btn.label === pages.toString());
	const lastParams = new URLSearchParams(params);
	lastParams.set("page", pages.toString());
	return {
		buttons: paginationArray,
		firstPage,
		lastPage: containsLastPage ? void 0 : {
			label: pages.toString(),
			href: `${cleanBaseUrl}?${lastParams.toString()}`,
			isActive: page === pages
		}
	};
};
/**
* @description Превращение массива в объект ключ-значение
* @param arr - Массив
*/
var arrayToKeyValueObject = (arr) => {
	return arr.reduce((acc, item) => {
		acc[item] = item;
		return acc;
	}, {});
};
/**
* @description Подготовка номера страницы, полученной из query параметров
* @param page - Номер страницы
*/
var preparePageParam = (page) => {
	if (!page) return 1;
	if (typeof page === "number") {
		if (page < 1) return 1;
		return page;
	}
	if (typeof page === "string") {
		const parsed = parseInt(page);
		if (Number.isNaN(parsed)) return 1;
		return parsed;
	}
	return 1;
};
/**
* @description Проверка html на пустоту
* @param html - Html разметка редактора
*/
var isNotEmptyHtml = (html) => {
	return !!html && html !== "<p class=\"content-paragraph\" ></p>";
};
/**
* @description Подстановка города в путь ссылки
* @param city - Город
* @param href - Ссылка
*/
var createCityLink = (city, href) => {
	return `/${city}${href}`;
};
/**
* @description Получить текстовое поле по локали
* @param locale - Локаль. Пример ru
* @param field - Пример text1
* @param data - Объект с полями. К примеру text1RU и text1EN
* @result - Текст поля
*/
var getLocaleField = ({ locale, field, data }) => {
	if (!data) return "";
	const result = data[`${field}${locale.toUpperCase()}`] ?? "";
	if (typeof result !== "string") return "";
	return result;
};
/**
* @description Проверка user agent на бота
* @param userAgent - user agent
* @result - boolean
*/
var isBotUserAgent = (userAgent) => {
	return [
		"bot",
		"crawler",
		"spider",
		"scraper",
		"googlebot",
		"bingbot",
		"yandexbot",
		"baiduspider",
		"facebookexternalhit",
		"twitterbot",
		"whatsapp",
		"pinterest",
		"duckduckbot",
		"slurp",
		"archive.org",
		"headless",
		"phantomjs",
		"selenium"
	].some((pattern) => userAgent.toLowerCase().includes(pattern));
};
/**
* Генерирует приятный цвет для заглушек фотографий карточек
* @param seed - Необязательное число или строка для предсказуемой генерации цвета
* @returns Цвет в формате HEX (#RRGGBB)
*/
var generatePlaceholderColor = (seed) => {
	const stringToHash = (str) => {
		let hash = 0;
		for (let i = 0; i < str.length; i++) {
			const char = str.charCodeAt(i);
			hash = (hash << 5) - hash + char;
			hash = hash & hash;
		}
		return Math.abs(hash);
	};
	let numericSeed;
	if (seed !== void 0) if (typeof seed === "string") numericSeed = stringToHash(seed);
	else numericSeed = seed;
	const getRandom = numericSeed !== void 0 ? (() => {
		let val = (numericSeed * 9301 + 49297) % 233280;
		return () => {
			val = (val * 9301 + 49297) % 233280;
			return val / 233280;
		};
	})() : Math.random;
	const randomValue = getRandom();
	let hue;
	if (randomValue < .5) hue = Math.floor(randomValue * 2 * 90);
	else hue = 160 + Math.floor((randomValue - .5) * 2 * 200);
	const saturation = 40 + getRandom() * 40;
	const lightness = 50 + getRandom() * 30;
	return hslToHex(hue, saturation, lightness);
};
/**
* Конвертирует цвет из формата HSL в HEX
* @param h - Оттенок (0-360)
* @param s - Насыщенность (0-100)
* @param l - Яркость (0-100)
* @returns Цвет в формате HEX (#RRGGBB)
*/
var hslToHex = (h, s, l) => {
	const normalizedH = h / 360;
	const normalizedS = s / 100;
	const normalizedL = l / 100;
	let r, g, b;
	if (normalizedS === 0) r = g = b = normalizedL;
	else {
		const hue2rgb = (p, q, t) => {
			if (t < 0) t += 1;
			if (t > 1) t -= 1;
			if (t < 1 / 6) return p + (q - p) * 6 * t;
			if (t < 1 / 2) return q;
			if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
			return p;
		};
		const q = normalizedL < .5 ? normalizedL * (1 + normalizedS) : normalizedL + normalizedS - normalizedL * normalizedS;
		const p = 2 * normalizedL - q;
		r = hue2rgb(p, q, normalizedH + 1 / 3);
		g = hue2rgb(p, q, normalizedH);
		b = hue2rgb(p, q, normalizedH - 1 / 3);
	}
	const toHex = (x) => {
		const hex = Math.round(x * 255).toString(16);
		return hex.length === 1 ? "0" + hex : hex;
	};
	return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};
/**
* Преобразует UUID в короткий ID
* @param {string} uuid - UUID v4 строка (с дефисами или без)
* @param {number} length - нужная длина (по умолчанию 18)
* @returns {string} короткий ID без дефисов
*/
var uuidToStringId = (uuid, length = 18) => {
	return uuid.replace(/-/g, "").slice(-length);
};
/**
* Преобразует UUID в числовой ID
* @param {string} uuid - UUID v4 строка (с дефисами или без)
* @param {number} maxDigits - максимальное количество цифр в результате (по умолчанию 18)
* @returns {string} числовой ID в виде строки
*/
var uuidToNumericId = (uuid, maxDigits = 18) => {
	const cleanUuid = uuid.replace(/-/g, "");
	let hash = 0;
	for (let i = 0; i < cleanUuid.length; i++) {
		const char = cleanUuid.charCodeAt(i);
		hash = (hash << 5) - hash + char;
		hash = hash & hash;
	}
	let result = Math.abs(hash).toString();
	if (result.length < maxDigits) {
		const segments = cleanUuid.match(/.{1,4}/g) || [];
		for (const segment of segments) {
			result += Math.abs(parseInt(segment, 16)).toString();
			if (result.length >= maxDigits) break;
		}
	}
	return result.slice(0, maxDigits);
};
/**
* @description Экранирует XML-специальные символы в строке.
* @param str - Строка
*/
var escapeXml = (str) => {
	if (!str) return "";
	return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
};
/**
* @description Генерирует XML-фид для яндекс товаров.
* @param shopName - Название магазина
* @param shopCompany - Компания
* @param delivery - Доставка
* @param categoriesData - Категории
* @param offersData - Товары
* @param host - Хост
* @response Готовый xml фид товаров
*/
var generateYandexFeedXML = ({ shopName, shopCompany, delivery, categoriesData, offersData, host }) => {
	const url = `https://${host.replace(/^https?:\/\//, "").replace(/\/$/, "")}`;
	const offersXml = offersData.map((item) => {
		const id = typeof item.id === "number" ? item.id : uuidToNumericId(item.id);
		const categoryId = typeof item.categoryId === "number" ? item.categoryId : uuidToNumericId(item.categoryId);
		return `
     <offer id="${id}" available="true">
       <url>${escapeXml(`${url}${item.href ? item.href.startsWith("/") ? item.href : `/${item.href}` : `/product/${id}`}`)}</url>
       <price>${item.price}</price>
       <currencyId>RUR</currencyId>
       <categoryId>${categoryId}</categoryId>
       ${item.images.length ? item.images.map((img) => `<picture>${escapeXml(img.image1x)}</picture>`).join("") : ""}
       <name>${escapeXml(item.name)}</name>
       <vendor>${escapeXml(item.vendor)}</vendor>
       <description>${escapeXml(item.description)}</description>
       ${item.vendorCode ? `<vendorCode>${escapeXml(item.vendorCode)}</vendorCode>` : ""}
       <pickup>${item.pickup}</pickup>
       ${item.isPopular ? "<param name=\"Популярный\">Да</param>" : ""}
       ${item.params?.map((param) => {
			return `<param name="${escapeXml(param.name)}">${escapeXml(param.value)}</param>`;
		}).join("")}      
     </offer>
   `;
	}).join("");
	const categoriesXml = categoriesData.map((item) => {
		return `<category id="${typeof item.id === "number" ? item.id : uuidToNumericId(item.id)}"${item.parentId ? ` parentId="${typeof item.parentId === "number" ? item.parentId : uuidToNumericId(item.parentId)}"` : ""}>${escapeXml(item.name)}</category>`;
	}).join("");
	const now = /* @__PURE__ */ new Date();
	const pad = (n) => String(n).padStart(2, "0");
	return `<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE yml_catalog SYSTEM "shops.dtd">
 <yml_catalog date="${`${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`}">
   <shop>
     <name>${escapeXml(shopName)}</name>
     <company>${escapeXml(shopCompany)}</company>
     <url>${url}</url>
     <delivery>${delivery}</delivery>
     <currencies>
       <currency id="RUR" rate="1"/>
     </currencies>
     <categories>
       ${categoriesXml}
     </categories>
     <offers>
       ${offersXml}
     </offers>
   </shop>
 </yml_catalog>`;
};
//#endregion
//#region src/server/schemas/index.ts
var responseSchema = createSchema(obj({
	statusCode: optional(num),
	message: optional(nullable(str)),
	messages: optional(nullable(arr(str))),
	data: optional(nullable(anyField)),
	error: optional(nullable(obj({
		statusCode: num,
		message: optional(nullable(str)),
		messages: optional(nullable(arr(str)))
	}))),
	response: optional(nullable(anyField))
}));
var serverImageSchema = createSchema(obj({
	altRU: optional(nullable(str)),
	altEN: optional(nullable(str)),
	altAR: optional(nullable(str)),
	name: str,
	originalFileExtension: str,
	entityId: optional(str),
	fullPathExample: optional(str),
	fileExtensions: optional(arr(str)),
	prefixes: optional(arr(str)),
	folder: optional(str)
}));
var serverFileSchema = createSchema(obj({
	name: str,
	fullName: str,
	extension: str,
	entityId: str,
	fullPathExample: str,
	folder: optional(str)
}));
//#endregion
//#region src/server/lib/files.ts
/**
* @description Создает url картинки
* @param {string} name - название картинки
* @param {string} extension - расширение картинки
* @param {string} entityId - id сущности
* @param {string} bucketName - название бакета картинки
* @param {string} prefixe - префикс файла
* @param {string} uploadUrl - url бакета
* @param {string | undefined} folder - подпапка
* @param bucketFolder - enum с папкой бакета
* @response Возвращает url
*/
var getUploadImageUrl = ({ name, extension, entityId, prefixe, bucketFolder, folder, uploadUrl }) => {
	return `${uploadUrl}/${bucketFolder}/${entityId}${folder ? `/${folder}` : ""}/${name}-${prefixe}.${extension}`;
};
/**
* @description Создает url файла
* @param { string } fullName - название файла
* @param { string } entityId - id сущности
* @param { string | undefined } folder - название подпапки
* @param { string } uploadUrl - url бакета
* @param bucketFolder - enum с папкой бакета
* @response Возвращает url
*/
var getUploadFileUrl = ({ fullName, entityId, folder, bucketFolder, uploadUrl }) => {
	return `${uploadUrl}/${bucketFolder}/${entityId}${folder ? `/${folder}` : ""}/${fullName}`;
};
/**
* @description Проверяет валидность объекта картинки по схеме
* @param {object} object - объект с информацией о картинке
* @response Возвращает true/false
*/
var checkCorrectImageObject = (object, getError) => {
	try {
		serverImageSchema.parse(object);
		return true;
	} catch (error) {
		getError({ error });
		return false;
	}
};
/**
* @description Проверяет валидность объекта файла по схеме
* @param {object} object - объект с информацией о файле
* @response Возвращает true/false
*/
var checkCorrectFileObject = (object, getError) => {
	try {
		serverFileSchema.parse(object);
		return true;
	} catch (error) {
		getError({ error });
		return false;
	}
};
/**
* @description Преобразует файлы с сервера в формат для работы
* @param {string | undefined | null} files - json stringify строка с информацией
* @param bucketFolder - название папки в бакете
* @param uploadUrl - url бакета
* @param getError - функция обработки ошибок
* @response Возвращает массив с подготовленными файлами
*/
var prepareServerFiles = ({ files, bucketFolder, uploadUrl, getError }) => {
	if (!files) return {
		serverFiles: [],
		serverFilesUrls: []
	};
	try {
		const filesArray = JSON.parse(files);
		if (!Array.isArray(filesArray)) return {
			serverFiles: [],
			serverFilesUrls: []
		};
		const serverFiles = filesArray.map((item) => {
			if (!checkCorrectFileObject(item, getError)) return null;
			return item;
		}).filter(Boolean);
		return {
			serverFiles,
			serverFilesUrls: serverFiles.map((item) => {
				return getUploadFileUrl({
					fullName: item.fullName,
					entityId: item.entityId,
					folder: item.folder,
					bucketFolder,
					uploadUrl
				});
			})
		};
	} catch (error) {
		getError({ error });
		return {
			serverFiles: [],
			serverFilesUrls: []
		};
	}
};
/**
* @description Преобразует фотографии с сервера в формат для работы
* @param {string | undefined | null} images - json stringify строка с информацией
* @param bucketFolder - название папки в бакете
* @param uploadUrl - url бакета
* @param getError - функция обработки ошибок
* @response Возвращает массив с подготовленными картинками
*/
var prepareServerImages = ({ images, bucketFolder, uploadUrl, getError }) => {
	if (!images) return [];
	try {
		const imagesArray = JSON.parse(images);
		if (!Array.isArray(imagesArray)) return [];
		return imagesArray.map((item) => {
			if (!checkCorrectImageObject(item, getError)) return null;
			const type = item.originalFileExtension === "png" ? "image/png" : "image/jpeg";
			const avifExtension = item.fileExtensions?.find((extension) => extension === "avif") ?? "";
			const webpExtension = item.fileExtensions?.find((extension) => extension === "webp") ?? "";
			const extension = item.fileExtensions?.find((extension) => extension === "jpg" || extension === "png") ?? item.originalFileExtension;
			const image1xPrefix = getImagePrefix(item.prefixes ?? [], "1hd");
			const image2xPrefix = getImagePrefix(item.prefixes ?? [], "2hd");
			const mobileImage1xPrefix = getImagePrefix(item.prefixes ?? [], "0.5hd");
			const mobileImage2xPrefix = getImagePrefix(item.prefixes ?? [], "1hd");
			const image1x = extension ? getUploadImageUrl({
				name: item.name,
				extension,
				folder: item.folder,
				entityId: item.entityId,
				prefixe: image1xPrefix,
				bucketFolder,
				uploadUrl
			}) : "";
			const image1xWebp = webpExtension ? getUploadImageUrl({
				name: item.name,
				extension: webpExtension,
				folder: item.folder,
				entityId: item.entityId,
				prefixe: image1xPrefix,
				bucketFolder,
				uploadUrl
			}) : "";
			const image1xAvif = avifExtension ? getUploadImageUrl({
				name: item.name,
				extension: avifExtension,
				folder: item.folder,
				entityId: item.entityId,
				prefixe: image1xPrefix,
				bucketFolder,
				uploadUrl
			}) : "";
			const image2x = extension ? getUploadImageUrl({
				name: item.name,
				extension,
				entityId: item.entityId,
				folder: item.folder,
				prefixe: image2xPrefix,
				bucketFolder,
				uploadUrl
			}) : "";
			const image2xWebp = webpExtension ? getUploadImageUrl({
				name: item.name,
				extension: webpExtension,
				folder: item.folder,
				entityId: item.entityId,
				prefixe: image2xPrefix,
				bucketFolder,
				uploadUrl
			}) : "";
			const image2xAvif = avifExtension ? getUploadImageUrl({
				name: item.name,
				extension: avifExtension,
				folder: item.folder,
				entityId: item.entityId,
				prefixe: image2xPrefix,
				bucketFolder,
				uploadUrl
			}) : "";
			const mobileImage1x = extension ? getUploadImageUrl({
				name: item.name,
				extension,
				entityId: item.entityId,
				folder: item.folder,
				prefixe: mobileImage1xPrefix,
				bucketFolder,
				uploadUrl
			}) : "";
			const mobileImage1xWebp = webpExtension ? getUploadImageUrl({
				name: item.name,
				extension: webpExtension,
				folder: item.folder,
				entityId: item.entityId,
				prefixe: mobileImage1xPrefix,
				bucketFolder,
				uploadUrl
			}) : "";
			const mobileImage1xAvif = avifExtension ? getUploadImageUrl({
				name: item.name,
				extension: avifExtension,
				folder: item.folder,
				entityId: item.entityId,
				prefixe: mobileImage1xPrefix,
				bucketFolder,
				uploadUrl
			}) : "";
			return {
				image1x,
				image2x,
				image1xWebp,
				image2xWebp,
				image1xAvif,
				image2xAvif,
				mobileImage1x,
				mobileImage2x: extension ? getUploadImageUrl({
					name: item.name,
					extension,
					entityId: item.entityId,
					folder: item.folder,
					prefixe: mobileImage2xPrefix,
					bucketFolder,
					uploadUrl
				}) : "",
				mobileImage1xWebp,
				mobileImage2xWebp: webpExtension ? getUploadImageUrl({
					name: item.name,
					extension: webpExtension,
					entityId: item.entityId,
					folder: item.folder,
					prefixe: mobileImage2xPrefix,
					bucketFolder,
					uploadUrl
				}) : "",
				mobileImage1xAvif,
				mobileImage2xAvif: avifExtension ? getUploadImageUrl({
					name: item.name,
					extension: avifExtension,
					entityId: item.entityId,
					folder: item.folder,
					prefixe: mobileImage2xPrefix,
					bucketFolder,
					uploadUrl
				}) : "",
				altRU: item.altRU,
				altEN: item.altEN,
				type
			};
		}).filter((item) => !!item);
	} catch (error) {
		getError({ error });
		return [];
	}
};
var getImagePrefix = (prefixes, type) => {
	let item;
	switch (type) {
		case "original":
			item = prefixes[0];
			break;
		case "0.25hd":
			item = prefixes[1];
			break;
		case "0.5hd":
			item = prefixes[2];
			break;
		case "1hd":
			item = prefixes[3];
			break;
		case "2hd":
			item = prefixes[4];
			break;
		case "4hd":
			item = prefixes[5];
			break;
	}
	return item ?? "original";
};
//#endregion
//#region src/server/lib/openapi-axios/types/serializer.ts
/**
* @description Enumeration for different styles of query parameter serialization
*/
var QuerySerializerStyle = /* @__PURE__ */ function(QuerySerializerStyle) {
	QuerySerializerStyle["Form"] = "form";
	QuerySerializerStyle["SpaceDelimited"] = "spaceDelimited";
	QuerySerializerStyle["PipeDelimited"] = "pipeDelimited";
	QuerySerializerStyle["DeepObject"] = "deepObject";
	return QuerySerializerStyle;
}({});
//#endregion
//#region src/server/lib/openapi-axios/const/defaultOptions.ts
/**
* @description Default options for OpenAPI requests using Axios.
*/
var defaultOptions = {
	validStatus: "axios",
	querySerializationParams: {
		style: QuerySerializerStyle.Form,
		explode: true
	}
};
//#endregion
//#region src/server/lib/openapi-axios/interpolate-params.ts
var paramRegex = /\{([a-zA-Z0-9_-]+)\}/g;
/**
* @description Default mapping function used during parameter interpolation.
* It simply returns the value provided without modification.
*
* @param _param - The parameter name (unused).
* @param value - The value to map.
* @returns The mapped value.
*/
function defaultMap(_param, value) {
	return value;
}
/**
* @description Replaces placeholders in a pattern string with corresponding values from the `params` object.
*
* @param pattern - The string pattern containing placeholders in the format `{paramName}`.
* @param params - An object containing parameter values keyed by their names.
* @param map - An optional mapping function to transform values before interpolation.
* @returns The interpolated string with placeholders replaced by their corresponding values.
*/
function interpolateParams(pattern, params, map) {
	const actualMap = map || defaultMap;
	return pattern.replace(paramRegex, function(paramWithBrackets, param) {
		if (param in params) {
			const value = actualMap(param, params[param]);
			if (value === null || value === void 0) return "";
			return String(value);
		}
		return paramWithBrackets;
	});
}
//#endregion
//#region src/server/lib/openapi-axios/utils/querySerializer.ts
function getQuerySerializer({ style, explode }) {
	return (params) => {
		switch (style) {
			case QuerySerializerStyle.Form: if (explode) return queryString.stringify(flatObjects(params), { encode: true });
			else return queryString.stringify(ObjectsToArray(params), {
				arrayFormat: "comma",
				encode: true
			});
			case QuerySerializerStyle.SpaceDelimited: if (explode) return queryString.stringify(flatObjects(params), { encode: true });
			else return queryString.stringify(params, {
				encode: true,
				arrayFormat: "separator",
				arrayFormatSeparator: " "
			});
			case QuerySerializerStyle.PipeDelimited: if (explode) return queryString.stringify(flatObjects(params), { encode: true });
			else return queryString.stringify(params, {
				encode: true,
				arrayFormat: "separator",
				arrayFormatSeparator: "|"
			});
			case QuerySerializerStyle.DeepObject: return queryString.stringify(toDeepObject(params), { encode: true });
		}
	};
}
function flatObjects(params) {
	return Object.fromEntries(Object.entries(params).map(([key, value]) => {
		if (typeof value === "object" && !Array.isArray(value) && value !== null) return Object.entries(value);
		return [[key, value]];
	}).flat());
}
function ObjectsToArray(params) {
	return Object.fromEntries(Object.entries(params).map(([key, value]) => {
		if (typeof value === "object" && !Array.isArray(value) && value !== null) return [key, Object.entries(value).map(([innerKey, innerValue]) => `${innerKey},${innerValue}`).join(",")];
		return [key, value];
	}));
}
function toDeepObject(params, prevKeys = []) {
	return Object.fromEntries(Object.entries(params).map(([key, value]) => {
		if (typeof value === "object" && !Array.isArray(value) && value !== null) return Object.entries(toDeepObject(value, [...prevKeys, key]));
		const [firstKey, ...restKeys] = [...prevKeys, key];
		return [[`${firstKey}${restKeys.map((k) => `[${k}]`).join("")}`, value]];
	}).flat());
}
//#endregion
//#region src/server/lib/openapi-axios/utils/response-converters/convertToAll.ts
async function convertToAll(response) {
	return response.then((response) => ({
		response,
		error: null,
		status: response.status,
		data: response.data
	})).catch((error) => {
		if (!axios.isAxiosError(error)) return {
			error,
			data: void 0,
			response: void 0,
			status: void 0
		};
		return {
			error,
			status: Number(error.response?.status) || void 0,
			data: error.response?.data || void 0,
			response: error.response
		};
	});
}
//#endregion
//#region src/server/lib/openapi-axios/utils/response-converters/convertToAxios.ts
async function convertToAxios(response) {
	return response.then((response) => ({
		response,
		status: response.status,
		data: response.data
	}));
}
//#endregion
//#region src/server/lib/openapi-axios/utils/response-converters/convertToFetch.ts
async function convertToFetch(response) {
	return response.then((response) => ({
		response,
		error: void 0,
		status: response.status,
		data: response.data
	})).catch((error) => {
		if (!axios.isAxiosError(error) || !error.response?.status) throw error;
		return {
			error,
			status: +error.response.status,
			data: error.response.data,
			response: error.response
		};
	});
}
//#endregion
//#region src/server/lib/openapi-axios/index.ts
/**
* @description OpenApiAxios class is a wrapper around Axios that provides methods to handle API requests
* and responses based on OpenAPI specifications.
*
* @template Schema - The OpenAPI schema type.
* @template ClassValidStatus - The type that configures the error handling strategy.
*/
var OpenApiAxios = class {
	axios;
	opt;
	/**
	* @description Creates an instance of OpenApiAxios.
	*
	* @param axios - The Axios instance to use for requests.
	* @param options - Configuration options for the OpenApiAxios instance.
	*/
	constructor(axios, options) {
		this.axios = axios;
		this.opt = Object.assign({}, defaultOptions, options);
	}
	get = this.factoryWithoutBody("get");
	head = this.factoryWithoutBody("head");
	delete = this.factoryWithoutBody("delete");
	options = this.factoryWithoutBody("options");
	put = this.factoryWithBody("put");
	post = this.factoryWithBody("post");
	patch = this.factoryWithBody("patch");
	/**
	* @description Generates a URI for a given API endpoint, including query parameters.
	*
	* @param method - The HTTP method to use.
	* @param path - The API route.
	* @param options - Additional options for the request.
	* @returns The generated URI as a string.
	*/
	async getUri(method, path, options) {
		const { urlString, newOptions } = this.prepareOptions(path, options);
		const { paramsSerializer, params, ...axios } = this.optionsToAxiosOptions(newOptions);
		const queryUri = Object.keys(params || {}).length > 0 ? `?${paramsSerializer(params)}` : "";
		return this.axios.getUri({
			url: `${urlString}${queryUri}`,
			method,
			...axios
		});
	}
	/**
	* @description Prepares the options and URL for an API request.
	*
	* @param path - The API route.
	* @param options - Additional options for the request.
	* @returns An object containing the prepared URL string and new options.
	*/
	prepareOptions(path, options) {
		let urlString = path;
		const newOptions = Object.assign({}, options, { validStatus: this.opt.validStatus });
		const optionsWithParams = newOptions;
		if (optionsWithParams?.params) urlString = interpolateParams(urlString, optionsWithParams.params);
		const querySerializationParams = options?.querySerializationParams || this.opt.querySerializationParams;
		if (!options?.axios?.paramsSerializer && querySerializationParams) newOptions.axios = Object.assign({}, newOptions.axios, { paramsSerializer: getQuerySerializer(querySerializationParams) });
		return {
			urlString,
			newOptions
		};
	}
	/**
	* @description Prepares the API response based on the valid status type.
	*
	* @param response - The Axios response promise.
	* @param options - Options for the request.
	* @returns A promise that resolves to the processed API response.
	*/
	async prepareResponse(response, options) {
		switch (options.validStatus) {
			case "all": return convertToAll(response);
			case "fetch": return convertToFetch(response);
			default: return convertToAxios(response);
		}
	}
	/**
	* @description Converts OpenAPI options to Axios request options.
	*
	* @param options - The options to convert.
	* @returns The converted Axios request configuration.
	*/
	optionsToAxiosOptions(options) {
		return {
			params: options?.query,
			...options.axios
		};
	}
	/**
	* @description Creates a fetcher method for HTTP methods that do not have a request body (e.g., GET, DELETE).
	*
	* @param method - The HTTP method to create a fetcher for.
	* @returns A function that performs the API request.
	*/
	factoryWithoutBody(method) {
		return (...args) => {
			const [path, options] = args;
			const { urlString, newOptions } = this.prepareOptions(path, options);
			return this.prepareResponse(this.axios[method](urlString, this.optionsToAxiosOptions(newOptions)), newOptions);
		};
	}
	/**
	* @description Creates a fetcher method for HTTP methods that have a request body (e.g., POST, PUT).
	*
	* @param method - The HTTP method to create a fetcher for.
	* @returns A function that performs the API request.
	*/
	factoryWithBody(method) {
		return (...args) => {
			const [path, body, options] = args;
			const { urlString, newOptions } = this.prepareOptions(path, options);
			return this.prepareResponse(this.axios[method](urlString, body, this.optionsToAxiosOptions(newOptions)), newOptions);
		};
	}
};
//#endregion
//#region src/server/ui/image.tsx
var BasePicture = ({ imgProps, data, type, alt, mobileMaxWidth = 450, loading = "lazy", ...pictureProps }) => {
	return /* @__PURE__ */ jsxs("picture", {
		...pictureProps,
		children: [
			!!data?.mobileImage1xAvif && /* @__PURE__ */ jsx("source", {
				srcSet: `${data?.mobileImage1xAvif} 1x${data?.mobileImage2xAvif ? `, ${data?.mobileImage2xAvif} 2x` : ""}`,
				media: `(max-width: ${mobileMaxWidth}px)`,
				type: "image/avif"
			}),
			!!data?.mobileImage1xWebp && /* @__PURE__ */ jsx("source", {
				srcSet: `${data?.mobileImage1xWebp} 1x${data?.mobileImage2xWebp ? `, ${data?.mobileImage2xWebp} 2x` : ""}`,
				media: `(max-width: ${mobileMaxWidth}px)`,
				type: "image/webp"
			}),
			!!data?.mobileImage1x && /* @__PURE__ */ jsx("source", {
				srcSet: `${data?.mobileImage1x} 1x${data?.mobileImage2x ? `, ${data?.mobileImage2x} 2x` : ""}`,
				media: `(max-width: ${mobileMaxWidth}px)`,
				type
			}),
			!!data?.image1xAvif && /* @__PURE__ */ jsx("source", {
				srcSet: `${data?.image1xAvif} 1x${data?.image2xAvif ? `, ${data?.image2xAvif} 2x` : ""}`,
				type: "image/avif"
			}),
			!!data?.image1xWebp && /* @__PURE__ */ jsx("source", {
				srcSet: `${data?.image1xWebp} 1x${data?.image2xWebp ? `, ${data?.image2xWebp} 2x` : ""}`,
				type: "image/webp"
			}),
			!!data?.image1x && /* @__PURE__ */ jsx("source", {
				srcSet: `${data?.image1x} 1x${data?.image2x ? `, ${data?.image2x} 2x` : ""}`,
				type
			}),
			/* @__PURE__ */ jsx("img", {
				...imgProps,
				src: data?.image1x,
				alt,
				loading,
				decoding: loading === "eager" ? "sync" : "async",
				fetchPriority: loading === "eager" ? "high" : "low"
			})
		]
	});
};
//#endregion
//#region src/server/ui/video.tsx
var Video = ({ className, preload = "auto", loop = true, autoPlay = true, muted = true, playsInline = true, controls = false, ...props }) => {
	return /* @__PURE__ */ jsx("video", {
		...props,
		className: cn("object-cover", className),
		preload,
		loop,
		autoPlay,
		muted,
		playsInline,
		controls
	});
};
//#endregion
//#region src/server/ui/container.tsx
var Container = ({ children, className, variant = "default", ...props }) => {
	const containerClasses = cn("my-0 mx-auto py-0 px-4", {
		"max-w-[1440px]": variant === "default",
		"max-w-[1920px]": variant === "large"
	}, className);
	return /* @__PURE__ */ jsx("div", {
		...props,
		className: containerClasses,
		children
	});
};
//#endregion
export { BasePicture, CITY_MASK, Container, HeaderWrapper, InfinityList, OpenApiAxios, THEME, TIME, ValidationError, Video, arrayToKeyValueObject, checkCorrectFileObject, checkCorrectImageObject, clearSessionCookies, cn, configureSession, convertPhoneMask, createCityLink, deleteCookieValue, escapeXml, generatePaginationArray, generatePlaceholderColor, generateYandexFeedXML, getByKey, getCookieValue, getDateFormatter, getFullSessionFromCookie, getImageData, getImagePrefix, getLocaleField, getNumberFormatter, getSessionFromCookie, getSubdomain, getUploadFileUrl, getUploadImageUrl, getUserDisplayName, getUserInitials, getValidAccessToken, hslToHex, isBotUserAgent, isNotEmptyHtml, parseSessionCookie, parseStringToKeyValue, prepareColor, prepareLocalMetaData, preparePageParam, prepareServerFiles, prepareServerImages, refreshSessionTokens, responseSchema, serverFileSchema, serverImageSchema, sessionTokensSchema, sessionUserSchema, setCookieValue, updateTextByTemplate, useAppearAnimation, useDisableScroll, useExtraMediumViewPort, useIsClient, useLaptopBigViewPort, useLaptopViewPort, useMobileViewPort, useSession, useSmallViewPort, useTabletViewPort, uuidToNumericId, uuidToStringId };