Initial commit: site monorepo with API, web, and infra.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
влад
2026-07-16 10:11:54 +03:00
co-authored by Cursor
commit 016910ffb7
447 changed files with 73972 additions and 0 deletions
@@ -0,0 +1,285 @@
/**
* Toast-уведомления зоотех-страниц (нижний pill-toast, стиль Яндекс Музыки).
*/
(function (global) {
const HOST_ID = "wespZtToastHost";
const DEFAULT_DURATION_MS = 4000;
const EXIT_MS = 220;
const UM = () => global.WespUserMessages || {};
const userFacingMessage = (message, fallback) => {
const api = UM();
if (typeof api.userFacingMessage === "function") {
return api.userFacingMessage(message, fallback);
}
const text = message == null ? "" : String(message).trim();
return text || fallback || "Не удалось выполнить операцию. Попробуйте ещё раз.";
};
let hostEl = null;
let activeToast = null;
let dismissTimer = null;
function ensureHost() {
if (hostEl && hostEl.isConnected) return hostEl;
hostEl = document.getElementById(HOST_ID);
if (!hostEl) {
hostEl = document.createElement("div");
hostEl.id = HOST_ID;
hostEl.className = "wesp-zt-toast-host";
hostEl.setAttribute("aria-live", "polite");
hostEl.setAttribute("aria-atomic", "true");
document.body.appendChild(hostEl);
}
return hostEl;
}
function clearDismissTimer() {
if (dismissTimer) {
clearTimeout(dismissTimer);
dismissTimer = null;
}
}
function normalizeType(type) {
if (type === "error" || type === "warning") return type;
return "success";
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function normalizeDisplayMessage(message) {
if (message && typeof message === "object") {
if (Array.isArray(message.parts)) return message;
if (message.text != null) message = message.text;
}
const text = message == null ? "" : String(message).trim();
if (/^Сохранено\b/i.test(text)) {
return "Сохранено";
}
return text;
}
function renderMessageContent(message, opts) {
if (message && typeof message === "object") {
if (Array.isArray(message.parts)) {
return message.parts
.map((part) => {
const text = escapeHtml(part && part.text != null ? part.text : "");
return part && part.bold ? `<strong>${text}</strong>` : text;
})
.join("");
}
if (message.text != null) {
return escapeHtml(message.text);
}
}
const text = message == null ? "" : String(message);
if (opts && Array.isArray(opts.parts)) {
return opts.parts
.map((part) => {
const chunk = escapeHtml(part && part.text != null ? part.text : "");
return part && part.bold ? `<strong>${chunk}</strong>` : chunk;
})
.join("");
}
return escapeHtml(text);
}
function removeToast(toastEl, immediate) {
if (!toastEl || !toastEl.isConnected) return Promise.resolve();
clearDismissTimer();
if (immediate) {
toastEl.remove();
if (activeToast === toastEl) activeToast = null;
return Promise.resolve();
}
return new Promise((resolve) => {
toastEl.classList.add("is-exiting");
const onEnd = () => {
toastEl.removeEventListener("animationend", onEnd);
toastEl.remove();
if (activeToast === toastEl) activeToast = null;
resolve();
};
toastEl.addEventListener("animationend", onEnd);
setTimeout(onEnd, EXIT_MS + 40);
});
}
function dismissAll() {
clearDismissTimer();
if (!activeToast) return;
const toast = activeToast;
activeToast = null;
return removeToast(toast, false);
}
function plainTextFromMessage(message) {
const api = UM();
if (typeof api.plainText === "function") return api.plainText(message);
if (message && typeof message === "object") {
if (message instanceof Error) return "";
if (Array.isArray(message.parts)) {
return message.parts.map((p) => (p && p.text != null ? String(p.text) : "")).join("");
}
if (message.text != null) return String(message.text);
}
return message == null ? "" : String(message);
}
function persistNotification(type, message, opts) {
if (opts && opts.skipRecord) return;
const plain = plainTextFromMessage(
message && typeof message === "object" && Array.isArray(message.parts)
? message
: normalizeDisplayMessage(message)
);
if (!plain.trim() && !(opts && opts.record)) return;
const builder = global.WespZootechNotificationMessages;
const payload = builder
? builder.buildNotificationRecord(type, plain, opts)
: {
title: plain.slice(0, 200) || "Уведомление",
detail: plain,
kind: type,
category: "general",
page: "general",
};
fetch("/api/notifications", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
.then(() => {
global.WespZootechNotificationCenter?.refreshBadge?.();
})
.catch(() => {
/* fire-and-forget */
});
}
function show(type, message, opts) {
const normalizedType = normalizeType(type);
const duration =
opts && Number.isFinite(opts.duration) ? opts.duration : DEFAULT_DURATION_MS;
const displayMessage =
message && typeof message === "object" && Array.isArray(message.parts)
? message
: normalizeDisplayMessage(message);
const html = renderMessageContent(displayMessage, opts);
const plainText =
displayMessage && typeof displayMessage === "object"
? ""
: String(displayMessage || "");
const isMultiline = plainText.length > 42 || /\n/.test(plainText);
const run = async () => {
if (activeToast) {
await removeToast(activeToast, false);
}
const root = ensureHost();
const toast = document.createElement("div");
toast.className = `wesp-zt-toast wesp-zt-toast--${normalizedType}${
isMultiline ? " wesp-zt-toast--multiline" : ""
}`;
toast.setAttribute("role", "status");
const accent = document.createElement("span");
accent.className = "wesp-zt-toast__accent";
accent.setAttribute("aria-hidden", "true");
const messageEl = document.createElement("p");
messageEl.className = "wesp-zt-toast__message";
messageEl.innerHTML = html;
const closeBtn = document.createElement("button");
closeBtn.type = "button";
closeBtn.className = "wesp-zt-toast__close";
closeBtn.setAttribute("aria-label", "Закрыть уведомление");
closeBtn.textContent = "×";
closeBtn.addEventListener("click", () => {
dismissAll();
});
toast.append(accent, messageEl, closeBtn);
root.appendChild(toast);
activeToast = toast;
if (duration > 0) {
dismissTimer = setTimeout(() => {
dismissAll();
}, duration);
}
};
persistNotification(normalizedType, displayMessage, opts);
run();
}
function success(message, opts) {
show("success", message, opts);
}
function error(message, opts) {
const safe = userFacingMessage(message, opts && opts.fallback);
show("error", safe, opts);
}
function warning(message, opts) {
const safe = userFacingMessage(message, opts && opts.fallback);
show("warning", safe, opts);
}
function open(payload) {
const data = payload && typeof payload === "object" ? payload : {};
show(data.type || "success", data.message, data);
}
function createAdapter() {
return {
success,
error,
warning,
open,
dismissAll,
};
}
global.WespZootechNotify = {
success,
error,
warning,
open,
dismissAll,
createAdapter,
userFacingMessage,
messageFromResponseBody: (body, fallback) => {
const api = UM();
if (typeof api.messageFromResponseBody === "function") {
return api.messageFromResponseBody(body, fallback);
}
return userFacingMessage(null, fallback);
},
isTechnicalMessage: (message) => {
const api = UM();
if (typeof api.isTechnicalMessage === "function") return api.isTechnicalMessage(message);
return false;
},
DEFAULT_ERROR_MESSAGE: UM().DEFAULT_ERROR_MESSAGE || "Не удалось выполнить операцию. Попробуйте ещё раз.",
get instance() {
return createAdapter();
},
};
})(typeof window !== "undefined" ? window : globalThis);