function assert(predicate, ...args) { if (!predicate) { console.error(...args) throw new Error("fatal") } } const props = new Set([ "autoplay", "checked", "checked", "contentEditable", "controls", "default", "hidden", "loop", "selected", "spellcheck", "value", "id", "title", "accessKey", "dir", "dropzone", "lang", "src", "alt", "preload", "poster", "kind", "label", "srclang", "sandbox", "srcdoc", "type", "value", "accept", "placeholder", "acceptCharset", "action", "autocomplete", "enctype", "method", "name", "pattern", "htmlFor", "max", "min", "step", "wrap", "useMap", "shape", "coords", "align", "cite", "href", "target", "download", "download", "hreflang", "ping", "start", "headers", "scope", "span", ]) function setProperty(prop, value, el) { if (props.has(prop)) { el[prop] = value } else { el.setAttribute(prop, value) } } function listener(event) { const el = event.currentTarget const handler = el._ui.listeners[event.type] const enqueue = el._ui.enqueue assert(typeof enqueue == "function", "Invalid enqueue") const msg = handler(event) if (msg !== undefined) { enqueue(msg) } } function setListener(el, event, handle) { assert( typeof handle == "function", "Event listener is not a function for event:", event, ) if (el._ui.listeners[event] === undefined) { el.addEventListener(event, listener) } el._ui.listeners[event] = handle } function eventName(str) { if (str.indexOf("on") == 0) { return str.slice(2).toLowerCase() } return null } // diff two virtual nodes function diffOne(l, r) { assert( r instanceof VirtualNode, "Expected an instance of VirtualNode, found", r, ) const isText = l.text !== undefined if (isText) { return l.text !== r.text ? { replace: r } : { noop: true } } if (l.tag !== r.tag) { return { replace: r } } const remove = [] const set = {} for (const prop in l.properties) { if (r.properties[prop] === undefined) { remove.push(prop) } } for (const prop in r.properties) { if (r.properties[prop] !== l.properties[prop]) { set[prop] = r.properties[prop] } } const children = diffList(l.children, r.children) const noChildrenChange = children.every((e) => e.noop) const noPropertyChange = remove.length === 0 && Array.from(Object.keys(set)).length == 0 return noChildrenChange && noPropertyChange ? { noop: true } : { modify: { remove, set, children } } } function diffList(ls, rs) { assert(rs instanceof Array, "Expected an array, found", rs) const length = Math.max(ls.length, rs.length) return Array.from({ length }).map((_, i) => ls[i] === undefined ? { create: rs[i] } : rs[i] == undefined ? { remove: true } : diffOne(ls[i], rs[i]), ) } function create(enqueue, vnode) { assert( vnode instanceof VirtualNode, "Expected an instance of VirtualNode, found", vnode, ) if (vnode.text !== undefined) { const el = document.createTextNode(vnode.text) return el } const el = document.createElement(vnode.tag) el._ui = { listeners: {}, enqueue } for (const prop in vnode.properties) { const event = eventName(prop) const value = vnode.properties[prop] event === null ? setProperty(prop, value, el) : setListener(el, event, value) } for (const childVNode of vnode.children) { const child = create(enqueue, childVNode) el.appendChild(child) } return el } function modify(el, enqueue, diff) { for (const prop of diff.remove) { const event = eventName(prop) if (event === null) { el.removeAttribute(prop) } else { el._ui.listeners[event] = undefined el.removeEventListener(event, listener) } } for (const prop in diff.set) { const value = diff.set[prop] const event = eventName(prop) event === null ? setProperty(prop, value, el) : setListener(el, event, value) } assert( diff.children.length >= el.childNodes.length, "unmatched children lengths", ) apply(el, enqueue, diff.children) } function apply(el, enqueue, childrenDiff) { const children = Array.from(el.childNodes) childrenDiff.forEach((diff, i) => { const action = Object.keys(diff)[0] switch (action) { case "remove": children[i].remove() break case "modify": modify(children[i], enqueue, diff.modify) break case "create": { assert( i >= children.length, "adding to the middle of children", i, children.length, ) const child = create(enqueue, diff.create) el.appendChild(child) break } case "replace": { const child = create(enqueue, diff.replace) children[i].replaceWith(child) break } case "noop": break default: throw new Error("Unexpected diff option: " + Object.keys(diff)) } }) } class VirtualNode { constructor(any) { Object.assign(this, any) } } // Create an HTML element description (a virtual node) function h(tag, properties, children) { assert(typeof tag === "string", "Invalid tag value:", tag) assert( typeof properties === "object", "Expected properties object. Found:", properties, ) assert(Array.isArray(children), "Expected children array. Found:", children) return new VirtualNode({ tag, properties, children }) } // Create a text element description (a virtual text node) function text(content) { return new VirtualNode({ text: content }) } // Start managing the contents of an HTML element. function init(root, initialState, update, view, postDraw) { let state = initialState // client application state let nodes = [] // virtual DOM nodes let queue = [] // msg queue function enqueue(msg) { queue.push(msg) } // draws the current state function draw() { let newNodes = view(state) apply(root, enqueue, diffList(nodes, newNodes)) nodes = newNodes state = postDraw(state) } function updateState() { if (queue.length > 0) { let msgs = queue queue = [] msgs.forEach((msg) => { try { state = update(state, msg, enqueue) } catch (e) { console.error(e) } }) draw() } window.requestAnimationFrame(updateState) } draw() updateState() return { enqueue } } let Css = { Stylesheet: undefined, ClassPrefix: "s", StyleId: 0, } function CssAppend(css) { if (!Css.Stylesheet) { Css.Stylesheet = document.createElement("style") document.head.appendChild(Css.Stylesheet) } Css.Stylesheet.textContent += css + "\n\n" } function CssApply(className, table) { let styles = "" for (let key in table) { let value = table[key] styles = styles.concat("\t", key, ": ", value, ";", "\n") } CssAppend(`${className} {\n${styles}}`) return className } function CssNew(table) { Css.StyleId += 1 let className = "s" + Css.StyleId let styles = "" for (let key in table) { let value = table[key] styles = styles.concat("\t", key, ": ", value, ";", "\n") } CssAppend(`.${className} {\n${styles}}`) return className } function CssPseudo(className, pseudo, table) { let styles = "" for (let key in table) { let value = table[key] styles = styles.concat("\t", key, ": ", value, ";", "\n") } CssAppend(`.${className}:${pseudo} {\n${styles}}`) return className } function CssApplyPseudo(className, pseudo, table) { let styles = "" for (let key in table) { let value = table[key] styles = styles.concat("\t", key, ": ", value, ";", "\n") } CssAppend(`${className}:${pseudo} {\n${styles}}`) return className } function CssJoin() { return Array.from(arguments).join(" ") } const Button = "button" const Canvas = "canvas" const Div = "div" const Fieldset = "fieldset" const Form = "form" const Img = "img" const Input = "input" const Label = "label" const Legend = "legend" const Span = "span" const Picture = "picture" let StyleLoadingContainer = CssNew({ "-webkit-transform": "translate(-50%, -50%)", "left": "50%", "position": "absolute", "top": "50%", "transform": "translate(-50%, -50%)", }) let StyleLoader = CssNew({ "width": "4px", "color": "var(--link-color)", "aspect-ratio": 1, "box-shadow": ` 19px -19px 0 0px, 38px -19px 0 0px, 57px -19px 0 0px, 19px 0 0 5px, 38px 0 0 5px, 57px 0 0 5px, 19px 19px 0 0px, 38px 19px 0 0px, 57px 19px 0 0px`, "transform": "translateX(-38px)", "animation": "l26 2s infinite ease", }) let NoImageSvg = `url('data:image/svg+xml,')` CssApply("body", { "height": "100%", "width": "100%", "margin": "0", "padding": "0", "box-sizing": "border-box", "font-family": '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"', "font-size": "13px", "font-weight": "400", "line-height": "16px", "background-color": "var(--tg-theme-secondary-bg-color)", "color-scheme": "var(--tg-color-scheme)", "color": "var(--tg-theme-text-color)", "cursor": "default", "user-select": "none", "-webkit-user-select": "none", }) CssApply("*", { "-webkit-tap-highlight-color": "transparent", }) CssApply("button", { "cursor": "pointer", }) CssAppend(` .loader { --c:no-repeat linear-gradient(var(--button-color) 0 0); background: var(--c) 0 0, var(--c) 0 50%, var(--c) 0 100%, var(--c) 50% 0, var(--c) 50% 50%, var(--c) 50% 100%, var(--c) 100% 0, var(--c) 100% 50%, var(--c) 100% 100%; background-size: 16px 16px; animation: l31 .5s infinite alternate; } @keyframes l31 { 0%,20% {width:45px;height: 45px} 90%,100%{width:65px;height: 65px} } `) let StyleChartButtonBlock = CssNew({ "display": "flex", "align-items": "center", "justify-content": "space-between", }) let StyleChartButton = CssNew({ "box-sizing": "border-box", "width": "95%", "background-color": "var(--button-color)", "border": "0", "outline": "border", "padding": "0.5rem", "font-size": "1rem", "box-shadow": "0px 5px 10px rgba(darken(dodgerblue, 40%))", "transition": "all .3s", "cursor": "pointer", "border-radius": "0px", "border-bottom": "4px solid lighten(gray, 70%)", "transform": "scale(0.95)", }) function ChartLoadData(enqueue) { enqueue({ "Preloaded": true }) fetch( "/api/chart?" + new URLSearchParams({ UserId: LoginData.UserId, Token: Base64UrlEncodeArray(LoginData.Token), StoreId: LoginData.StoreId, }), ) .then((response) => response.json()) .then((data) => { enqueue({ Loaded: { ChartData: data } }) }) .catch(() => {}) } let ChartState = { Object: undefined, ChartData: undefined, PreparedChartData: undefined, Position: Number.MAX_SAFE_INTEGER, TimePeriod: "monthly", MaxPerPage: 8, } function ChartUpdate(s, msg) { if ("Preloaded" in msg) { } if ("Loaded" in msg) { let chartData = msg.Loaded.ChartData s.ChartData = chartData s.Position = Number.MAX_SAFE_INTEGER s.TimePeriod = "monthly" } if ("Monthly" in msg) { s.Position = Number.MAX_SAFE_INTEGER s.TimePeriod = "monthly" } if ("Weekly" in msg) { s.Position = Number.MAX_SAFE_INTEGER s.TimePeriod = "weekly" } if ("Daily" in msg) { s.Position = Number.MAX_SAFE_INTEGER s.TimePeriod = "daily" } if ("Prev" in msg) { s.Position -= 1 } if ("Next" in msg) { s.Position += 1 } let data if (s.ChartData) { if (s.TimePeriod == "monthly") { data = s.ChartData.Monthly } if (s.TimePeriod == "weekly") { data = s.ChartData.Weekly } if (s.TimePeriod == "daily") { data = s.ChartData.Daily } } if (!data) { data = [] } let dataLength = Object.keys(data).length let keys = Object.keys(data) let values = Object.values(data) let newKeys = keys.map((key) => { let date = new Date(Number(key) / 1_000_000) if (s.TimePeriod == "monthly") { return date.toLocaleString("ru-RU", { year: "2-digit", month: "long", }) } return date.toLocaleDateString("ru-RU") }) let newValues = values.map((value) => value / 100) let maxPosition = Math.floor(Math.max(dataLength - 1, 0) / s.MaxPerPage) function clamp(n, min, max) { return Math.max(min, Math.min(max, n)) } s.Position = clamp(s.Position, 0, maxPosition) let start = s.MaxPerPage * (s.Position + 0) let end = s.MaxPerPage * (s.Position + 1) start = Math.max(0, start) end = Math.max(0, end) start = Math.min(newKeys.length, start) end = Math.min(newKeys.length, end) newKeys = newKeys.slice(start, end) newValues = newValues.slice(start, end) s.PreparedChartData = { labels: newKeys, datasets: [ { label: "Turkish lira", data: newValues, }, ], } return s } function ChartRender(s) { if (s.ChartData) { return [ h(Div, {}, [ h(Canvas, { id: "chart" }, []), h(Div, { class: StyleChartButtonBlock }, [ h( Button, { class: StyleChartButton, onclick: () => ({ Prev: true }), }, [text("Назад")], ), h( Button, { class: StyleChartButton, onclick: () => ({ Next: true }), }, [text("Вперёд")], ), ]), h(Div, { class: StyleChartButtonBlock }, [ h( Button, { class: StyleChartButton, onclick: () => ({ Daily: true }), }, [text("День")], ), h( Button, { class: StyleChartButton, onclick: () => ({ Weekly: true }), }, [text("Неделя")], ), h( Button, { class: StyleChartButton, onclick: () => ({ Monthly: true }), }, [text("Месяц")], ), ]), ]), ] } else { return [h(Div, { class: "loader" }, [])] } } function ChartPostRender(s) { let chartCtx = document.getElementById("chart") if (chartCtx && !s.Object) { s.Object = new Chart(chartCtx, { type: "bar", options: { plugins: { legend: { display: false, }, }, scales: { y: { suggestedMin: 0, }, }, }, }) } if (s.Object) { Chart.defaults.backgroundColor = Telegram.WebApp.themeParams.button_color Chart.defaults.borderColor = Telegram.WebApp.themeParams.hint_color Chart.defaults.color = Telegram.WebApp.themeParams.text_color s.Object.data = s.PreparedChartData s.Object.update() } return s } function FormatPrice(s, price) { return Math.trunc(price / 100) + " " + s.StoreData.CurrencySign } function StoreLoadData(enqueue) { enqueue({ "Preloaded": true }) fetch( "/api/store_info?" + new URLSearchParams({ UserId: LoginData.UserId, Token: Base64UrlEncodeArray(LoginData.Token), StoreId: LoginData.StoreId, }), ) .then((response) => response.json()) .then((data) => { enqueue({ Loaded: { StoreData: data } }) }) .catch(() => {}) } let ScreenId = { Unknown: 0, Category: 1, Item: 2, Checkout: 3, } function GetCurrentScreen(session) { if (session) { let length = session.ScreenStack.length if (length > 0) { return session.ScreenStack[length - 1] } } return { Id: ScreenId.Unknown } } let StoreState = { StoreData: undefined, Session: undefined, } function StoreUpdate(s, msg) { if ("Preloaded" in msg) { try { let jsonData = localStorage.getItem("session") s.Session = JSON.parse(jsonData) } catch (error) {} } s.Session = s.Session || {} s.Session.ScreenStack = s.Session.ScreenStack || [] s.Session.ShoppingCard = s.Session.ShoppingCard || [] if ("Loaded" in msg) { let storeData = msg.Loaded.StoreData s.StoreData = storeData } if ("OpenCategory" in msg) { let categoryId = msg.OpenCategory.CategoryId s.Session.ScreenStack.push({ Id: ScreenId.Category, CategoryId: categoryId, }) window.scrollTo({ top: 0, behavior: "instant" }) } if ("GoBack" in msg) { s.Session.ScreenStack.pop() window.scrollTo({ top: 0, behavior: "instant" }) } if ("AddItem" in msg) { let itemId = msg.AddItem.ItemId if (s.StoreData.Items[itemId].OptionsGroupIds) { s.Session.ScreenStack.push({ Id: ScreenId.Item, ItemId: itemId }) window.scrollTo({ top: 0, behavior: "instant" }) } else { s.Session.ShoppingCard.push(itemId) } } if (s.StoreData) { let shoppingCardTotal = 0 for (let itemId of s.Session.ShoppingCard) { shoppingCardTotal += s.StoreData.Items[itemId].Price } Telegram.WebApp.isClosingConfirmationEnabled = s.Session.ShoppingCard.length > 0 Telegram.WebApp.MainButton.setParams({ text: "Оплатить " + FormatPrice(s, shoppingCardTotal), has_shine_effect: true, is_visible: s.Session.ShoppingCard.length > 0, }) Telegram.WebApp.BackButton.isVisible = s.Session.ScreenStack.length > 0 } let jsonData = JSON.stringify(s.Session) localStorage.setItem("session", jsonData) return s } let StyleStoreItems = CssNew({ "align-content": "flex-start", "display": "flex", "flex-wrap": "wrap", "justify-content": "space-between", "padding": "0.5rem", "max-width": "480px", "overflow": "hidden", "row-gap": "0.5rem", "transition": "max-height var(--page-animation), opacity var(--page-animation)", }) function StoreRender(s) { let currentScreen = GetCurrentScreen(s.Session) if (!s.StoreData) { return [ h(Div, { class: StyleLoadingContainer }, [ h(Div, { class: StyleLoader }, []), ]), ] } if ( currentScreen.Id == ScreenId.Unknown || currentScreen.Id == ScreenId.Category ) { let categories = [] let currentCategoryId = currentScreen.CategoryId for (let [_categoryId, category] of Object.entries( s.StoreData.Categories, )) { let categoryId = Number(_categoryId) if (category.ParentCategoryId != currentCategoryId) { continue } categories.push(StoreCategory(s, categoryId, category)) } let items = [] for (let [_itemId, item] of Object.entries(s.StoreData.Items)) { let itemId = Number(_itemId) if (item.CategoryId != currentCategoryId) { continue } items.push(StoreItem(s, itemId, item)) } let all = [].concat(categories, items) return [h(Div, { class: StyleStoreItems }, all)] } if (currentScreen.Id == ScreenId.Item) { return StoreItemOptions(s) } if (currentScreen.Id == ScreenId.Checkout) { return StoreItemList(s) } } function StorePostRender(s) { return s } function StoreCashier() { return h( "div.cafe-order-overview", h(".cafe-block"), h( ".cafe-switch-block", h(".cafe-switch-label", "Наличные"), h( "label.checkcontainer", h("input", { type: "radio", name: "radio" }), h("span.radiobtn"), ), ), h( ".cafe-switch-block", h(".cafe-switch-label", "Безналичные"), h( "label.checkcontainer", h("input", { type: "radio", name: "radio" }), h("span.radiobtn"), ), ), h( ".cafe-switch-block", h(".cafe-switch-label", "Баланс"), h( "label.checkcontainer", h("input", { type: "radio", name: "radio" }), h("span.radiobtn"), ), ), h( ".cafe-switch-block", h(".cafe-switch-label", "Скидка"), h("input", { class: StyleNumberInput }, { type: "text" }), ), h( ".cafe-text-field-wrap", h("textarea.cafe-text-field cafe-block", { rows: 1, placeholder: "Добавить комментарий…", style: "overflow: hidden visible; overflow-wrap: break-word;", oninput: function (_vnode) { autosize(this) }, }), ), ) } CssApplyPseudo(Img, "before", { "background-color": "var(--tg-theme-link-color)", "content": `""`, "display": "inline-block", "height": "100%", "mask-image": NoImageSvg, "mask-repeat": "no-repeat", "mask-size": "100% 100%", "opacity": "0.25", "width": "75%", }) let StyleStoreCategory = CssNew({ "background-color": "var(--tg-theme-section-bg-color)", "border-radius": "7px", "box-sizing": "border-box", "height": "182px", "overflow": "hidden", "padding": "8px", "position": "relative", "text-align": "center", "transition": "all 0.1s", "width": "calc(100% / 2.05)", }) let StyleSelected = CssNew({ "color": "#ff0000", }) let StyleStoreCategorySelected = CssJoin(StyleStoreCategory, StyleSelected) CssPseudo(StyleStoreCategory, "hover", { "transform": "scale(1.05)", }) CssPseudo(StyleStoreCategory, "active", { "transform": "scale(1.05)", }) function IsSelected(categoryId) { if (categoryId % 2 == 0) { return StyleStoreCategory } else { return StyleStoreCategorySelected } } function StoreCategory(s, categoryId, category) { return h( Div, { class: IsSelected(categoryId), onclick: () => ({ OpenCategory: { CategoryId: categoryId }, }), }, [ h(Div, { class: StyleStoreItemPhoto }, [ h( Img, { class: StyleStoreItemImage, src: s.StoreData.Categories[categoryId].FileUrl, }, [], ), ]), h(Div, { class: StyleStoreItemLabel }, [ h(Span, { class: StyleStoreItemTitle }, [text(category.Name)]), ]), ], ) } function StoreDelivery() { return h( "div.cafe-order-overview", h(".cafe-block"), h( ".cafe-switch-block", h(".cafe-switch-label", "Забрать на кассе"), h( ".cafe-switch", h( "label.switch", h("input", { type: "checkbox" }), h("span.slider"), ), ), ), h( ".cafe-switch-block", h(".cafe-switch-label", "Номер столика"), h("input", { class: StyleNumberInput }, { type: "text" }), ), h( ".cafe-text-field-wrap", h("textarea.cafe-text-field cafe-block", { rows: 1, placeholder: "Добавить комментарий…", style: "overflow: hidden visible; overflow-wrap: break-word;", oninput: function (_vnode) { autosize(this) }, }), ), ) } let StyleStoreItem = CssNew({ "background-color": "var(--tg-theme-section-bg-color)", "border-radius": "7px", "box-sizing": "border-box", "overflow": "hidden", "padding": "8px", "position": "relative", "text-align": "center", "width": "calc(100% / 2.05)", }) let StyleStoreItemLabel = CssNew({ "align-items": "flex-start", "display": "flex", "flex-direction": "column", "font-size": "15px", "gap": "6px", "margin": "0 1px", "overflow": "hidden", "text-overflow": "ellipsis", "white-space": "nowrap", }) let StyleStoreItemImage = CssNew({ "bottom": "0", "display": "inline-block", "height": "100%", "left": "0", "margin": "0 auto", "position": "relative", "right": "0", "top": "0", "vertical-align": "top", "width": "100%", // 'position': 'absolute', // 'width': '146px', }) let StyleStoreItemBadge = CssNew({ "border-radius": "4px", "border": "none", "box-sizing": "border-box", "color": "#fff", "display": "inline-block", "font-family": "var(--default-font)", "font-size": "13px", "font-weight": "500", "left": "5px", "line-height": "14px", "outline": "none", "padding": "2px 4px", "position": "absolute", "text-transform": "uppercase", "top": "5px", "transform": "rotate(-12deg)", }) let StyleStoreItemPhoto = CssNew({ "height": "150px", "position": "relative", }) let StyleStoreItemTitle = CssNew({}) let StyleStoreItemPrice = CssNew({ "font-weight": "700", "white-space": "nowrap", }) let StyleStoreItemCounter = CssNew({ "animation": "var(--animation) both", "font-size": "20px", "height": "32px", "line-height": "32px", "margin": "12px", "min-width": "32px", "pointer-events": "none", "position": "absolute", "right": "0", "text-align": "center", "top": "0", "transform": "scale3d(0, 0, 1)", "vertical-align": "middle", "z-index": "3", }) let StyleItemCounter = CssNew({ "display": "inline-block", "font-family": "var(--default-font)", "font-weight": "700", "font-size": "13px", "line-height": "18px", "height": "30px", "border-radius": "7px", "box-sizing": "border-box", "background-color": "var(--link-color)", "color": "#fff", "outline": "none", "border": "none", "white-space": "nowrap", "text-overflow": "ellipsis", "overflow": "hidden", "display": "flex", "justify-content": "center", "align-items": "center", }) function StoreItem(s, itemId) { return h( Div, { class: StyleStoreItem, onclick: () => ({ AddItem: { ItemId: itemId } }), }, [ h(Div, { class: StyleStoreItemPhoto }, [ h( Img, { class: StyleStoreItemImage, src: s.StoreData.Items[itemId].FileUrl, }, [], ), ]), h(Div, { class: StyleStoreItemLabel }, [ h(Span, { class: StyleStoreItemPrice }, [ text(FormatPrice(s, s.StoreData.Items[itemId].Price)), ]), h(Span, { class: StyleItemTitle }, [ text(s.StoreData.Items[itemId].Name), ]), ]), h(Div, { class: StyleStoreItemButtons }, [ h( Button, { class: "cafe-item-decr-button button-item", onclick: function () { ShoppingCard.pop() }, }, [h(Span, { class: "gg-math-minus" }, [])], ), h(Button, { class: "cafe-item-incr-button button-item" }, [ h(Span, { class: "button-item-icon gg-math-plus" }, []), h(Span, { class: StyleButtonItemLabel }, [ text("В корзину"), ]), ]), ]), ], ) } let StyleItemPrice = CssNew({ "font-size": "16px", "line-height": "17px", "font-weight": "500", "padding": "4px 0", }) let StyleStoreItemButtons = CssNew({ "display": "flex", "margin": "8px auto 0", "position": "relative", "transition": "all var(--animation)", }) let StyleStoreOrderItem = CssNew({ "display": "flex", "padding": "5px 20px 5px 14px", }) let StyleNumberInput = CssNew({ "border-radius": "0", "border": "none", "box-sizing": "border-box", "color": "var(--text-color)", "cursor": "auto", "display": "block", "font-family": "var(--default-font)", "font-size": "17px", "height": "32px", "line-height": "21px", "outline": "none", "padding": "0px 10px", "resize": "none", "user-select": "auto", "width": "48px", }) let StyleItemTitle = CssNew({ "font-size": "18px", "font-weight": "700", "line-height": "18px", "overflow": "hidden", "padding": "3px 0", "white-space": "nowrap", // "width": "200px", }) let StyleStatusWrap = CssNew({ "left": "0", "position": "fixed", "right": "0", "top": "0", "transform": "translateY(var(--tg-viewport-height, 100vh))", "z-index": "1", }) let StyleButtonItemLabel = CssNew({ "display": "inline-block", "max-width": "100%", "overflow": "hidden", "text-overflow": "ellipsis", "vertical-align": "top", "position": "relative", "z-index": 1, }) CssPseudo(StyleButtonItemLabel, { "transition": "transform var(--animation)", }) let StyleButtonItemIcon = CssNew({ "display": "none", }) function StoreItemList(s) { let items = [] for (let itemId of s.Session.ShoppingCard) { items.push(StoreItemLine(s, itemId)) } return items } function StoreItemLine(s, id) { return h(Div, { class: StyleStoreOrderItem }, [ h(Div, { class: "cafe-order-item-photo" }, [ h( // what is this tag??? Picture, { class: "cafe-item-lottie" }, [ h( Img, { src: s.StoreData.Items[id].FileUrl, }, [], ), ], ), ]), h(Div, { class: "cafe-order-item-label" }, [ h(Div, { class: StyleItemTitle }, [ text(s.StoreData.Items[id].Name), text(" "), h(Span, { class: "cafe-order-item-counter" }, [ text(`${s.ShoppingCard} шт`), ]), ]), h(Div, { class: "cafe-order-item-description" }, [text("test")]), ]), h(Div, { class: "cafe-order-item-price" }, [ text(FormatPrice(s, s.StoreData.Items[id].Price)), ]), ]) } let StyleStoreItemOptionsLegend = CssNew({ "font-weight": "bold", "padding": "0 1rem", }) let StyleStoreItemOptionsFieldset = CssNew({ "display": "flex", "flex-direction": "column", "gap": "0.5rem", "border": "0.1rem solid var(--tg-theme-section-separator-color)", "padding": "1rem", "margin-bottom": "1rem", "background-color": "var(--tg-theme-section-bg-color)", }) let StyleStoreItemOptionsRadio = CssNew({ "display": "flex", "align-items": "center", "gap": "0.5rem", }) let StyleStoreItemOptionsRadioInput = CssNew({ "width": "1.5rem", "height": "1.5rem", "accent-color": "var(--button-color)", }) let StyleStoreItemOptionsLabel = CssNew({ // "font": "2rem", // "height": "2rem", // "accent-color": "var(--button-color)", }) function combineId(optionsGroupsId, optionId) { return `${optionsGroupsId}-${optionId}` } function StoreItemOptions(s) { let optionsGroupsElements = [] for (let [_optionsGroupsId, optionsGroups] of Object.entries( s.StoreData.OptionsGroups, )) { let optionsGroupsId = Number(_optionsGroupsId) let options = [] options.push( h(Legend, { class: StyleStoreItemOptionsLegend }, [ text(optionsGroups.Name), ]), ) for (let [_optionId, option] of Object.entries(optionsGroups.Options)) { let optionId = Number(_optionId) options.push( h(Div, { class: StyleStoreItemOptionsRadio }, [ h( Input, { "type": "radio", "id": combineId(optionsGroupsId, optionId), "name": optionsGroupsId, "value": combineId(optionsGroupsId, optionId), "class": StyleStoreItemOptionsRadioInput, }, [], ), h( Label, { "for": combineId(optionsGroupsId, optionId), class: StyleStoreItemOptionsLabel, }, [text(option.Name)], ), ]), ) } optionsGroupsElements.push( h(Form, {}, [ h(Fieldset, { class: StyleStoreItemOptionsFieldset }, options), ]), ) } return optionsGroupsElements } function StoreTakeout() { return h( Div, { class: "cafe-order-overview" }, h( Div, { class: ".cafe-block" }, h( { class: ".cafe-order-items" }, Object.entries(Data.general.items).map(function (d) { shopItemId = d[0] return h(StoreItemLine, { id: shopItemId }) }), ), ), h( ".cafe-switch-block", h(".cafe-switch-label", "Забрать на кассе"), h( ".cafe-switch", h( "label.switch", h("input", { type: "checkbox" }), h("span.slider"), ), ), ), h( ".cafe-switch-block", h(".cafe-switch-label", "Номер столика"), h("input", { class: StyleNumberInput }, { type: "text" }), ), h( ".cafe-text-field-wrap", h("textarea.cafe-text-field cafe-block", { rows: 1, placeholder: "Добавить комментарий…", style: "overflow: hidden visible; overflow-wrap: break-word;", oninput: function (_vnode) { autosize(this) }, }), ), ) } let Utf8Encoder = new TextEncoder() let Utf8Decoder = new TextDecoder("utf-8") function Base64UrlEncodeString(s) { return Base64UrlEncodeArray(Utf8Encoder.encode(s)) } function Base64UrlDecodeString(s) { return Utf8Decoder.decode(Base64UrlDecodeArray(s)) } function Base64UrlEncodeArray(buffer) { return btoa( Array.from(new Uint8Array(buffer), (b) => String.fromCharCode(b)).join( "", ), ) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, "") } function Base64UrlDecodeArray(value) { let m = value.length % 4 return Uint8Array.from( atob( value .replace(/-/g, "+") .replace(/_/g, "/") .padEnd(value.length + (m === 0 ? 0 : 4 - m), "="), ), (c) => c.charCodeAt(0), ).buffer } let LoginData = {} function LoadLoginData() { let base64Decoded = window.location.search base64Decoded = base64Decoded.substring(1) let jsonDecoded try { jsonDecoded = Base64UrlDecodeString(base64Decoded) LoginData = JSON.parse(jsonDecoded) } catch (error) { return } } let Page = { Store: 1, Chart: 2, } function Init() { Telegram.WebApp.MainButton.setParams({ text_color: "#fff", has_shine_effect: true, }).onClick(function () {}) Telegram.WebApp.ready() Telegram.WebApp.expand() LoadLoginData() } function Render() { let startTime = performance.now() let root = document.getElementById("app") if (!LoginData.Page) { LoginData.Page = Page.Store } if (LoginData.Page == Page.Store) { let { enqueue } = init( root, StoreState, StoreUpdate, StoreRender, StorePostRender, ) StoreLoadData(enqueue) Telegram.WebApp.BackButton.onClick(function () { enqueue({ GoBack: true }) }) } if (LoginData.Page == Page.Chart) { let { enqueue } = init( root, ChartState, ChartUpdate, ChartRender, ChartPostRender, ) ChartLoadData(enqueue) } let endTime = performance.now() let t = Math.round((endTime - startTime) * 1000) console.log(`redraw took ${t} microseconds`) } Init() Render()