diff --git a/build.ipynb b/build.ipynb index 47abcc6..7b48c77 100644 --- a/build.ipynb +++ b/build.ipynb @@ -775,9 +775,10 @@ "\n", "[Service]\n", "WorkingDirectory=/app/production\n", - "ExecStart=/app/production/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 30081 --workers 8\n", + "ExecStart=/app/production/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 30081 --workers 3\n", "Restart=always\n", "EnvironmentFile=/app/production/env\n", + "CPUWeight=60\n", "\n", "[Install]\n", "WantedBy=default.target\n", @@ -798,23 +799,23 @@ }, "outputs": [], "source": [ - "%%bash\n", - "ssh kultmost 'tee /etc/systemd/system/fastapi.service >/dev/null' <<'EOF'\n", - "[Unit]\n", - "Description=FastAPI prod service\n", - "After=network.target\n", + "# %%bash\n", + "# ssh kultmost 'tee /etc/systemd/system/fastapi.service >/dev/null' <<'EOF'\n", + "# [Unit]\n", + "# Description=FastAPI prod service\n", + "# After=network.target\n", "\n", - "[Service]\n", - "WorkingDirectory=/app/production\n", - "ExecStart=/app/production/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 30080 --workers 8\n", - "Restart=always\n", - "EnvironmentFile=/app/production/env\n", + "# [Service]\n", + "# WorkingDirectory=/app/production\n", + "# ExecStart=/app/production/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 30080 --workers 8\n", + "# Restart=always\n", + "# EnvironmentFile=/app/production/env\n", "\n", - "[Install]\n", - "WantedBy=default.target\n", - "EOF\n", + "# [Install]\n", + "# WantedBy=default.target\n", + "# EOF\n", "\n", - "ssh kultmost 'systemctl daemon-reload && systemctl enable --now fastapi.service && systemctl status --no-pager -l fastapi.service'\n" + "# ssh kultmost 'systemctl daemon-reload && systemctl enable --now fastapi.service && systemctl status --no-pager -l fastapi.service'\n" ], "display_name": ".venv" }, @@ -837,23 +838,23 @@ }, "outputs": [], "source": [ - "%%bash\n", - "ssh kultmost 'tee /etc/systemd/system/staging.service >/dev/null' <<'EOF'\n", - "[Unit]\n", - "Description=FastAPI stag service\n", - "After=network.target\n", + "# %%bash\n", + "# ssh kultmost 'tee /etc/systemd/system/staging.service >/dev/null' <<'EOF'\n", + "# [Unit]\n", + "# Description=FastAPI stag service\n", + "# After=network.target\n", "\n", - "[Service]\n", - "WorkingDirectory=/app/staging\n", - "ExecStart=/app/staging/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 30082 --reload\n", - "Restart=always\n", - "EnvironmentFile=/app/staging/env\n", + "# [Service]\n", + "# WorkingDirectory=/app/staging\n", + "# ExecStart=/app/staging/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 30082 --reload\n", + "# Restart=always\n", + "# EnvironmentFile=/app/staging/env\n", "\n", - "[Install]\n", - "WantedBy=default.target\n", - "EOF\n", + "# [Install]\n", + "# WantedBy=default.target\n", + "# EOF\n", "\n", - "ssh kultmost 'systemctl daemon-reload && systemctl enable --now staging.service && systemctl status --no-pager -l staging.service'\n" + "# ssh kultmost 'systemctl daemon-reload && systemctl enable --now staging.service && systemctl status --no-pager -l staging.service'\n" ], "display_name": ".venv" }, @@ -876,21 +877,21 @@ }, "outputs": [], "source": [ - "%%bash\n", - "'tee /etc/systemd/system/staging.service >/dev/null' <<'EOF'\n", - "[Unit]\n", - "Description=FastAPI stag service\n", - "After=network.target\n", + "# %%bash\n", + "# 'tee /etc/systemd/system/staging.service >/dev/null' <<'EOF'\n", + "# [Unit]\n", + "# Description=FastAPI stag service\n", + "# After=network.target\n", "\n", - "[Service]\n", - "WorkingDirectory=$pwd\n", - "ExecStart=$pwd/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 30082 --reload\n", - "Restart=always\n", - "EnvironmentFile=$pwd/env\n", + "# [Service]\n", + "# WorkingDirectory=$pwd\n", + "# ExecStart=$pwd/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 30082 --reload\n", + "# Restart=always\n", + "# EnvironmentFile=$pwd/env\n", "\n", - "[Install]\n", - "WantedBy=default.target\n", - "EOF" + "# [Install]\n", + "# WantedBy=default.target\n", + "# EOF" ], "display_name": ".venv" }, @@ -905,23 +906,23 @@ }, "outputs": [], "source": [ - "%%bash\n", - "ssh kultmost 'tee /etc/systemd/system/staging.service >/dev/null' <<'EOF'\n", - "[Unit]\n", - "Description=FastAPI stag service\n", - "After=network.target\n", + "# %%bash\n", + "# ssh kultmost 'tee /etc/systemd/system/staging.service >/dev/null' <<'EOF'\n", + "# [Unit]\n", + "# Description=FastAPI stag service\n", + "# After=network.target\n", "\n", - "[Service]\n", - "WorkingDirectory=/app/staging\n", - "ExecStart=/app/staging/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 30082 --reload\n", - "Restart=always\n", - "EnvironmentFile=/app/staging/env\n", + "# [Service]\n", + "# WorkingDirectory=/app/staging\n", + "# ExecStart=/app/staging/.venv/bin/uvicorn main:app --host 127.0.0.1 --port 30082 --reload\n", + "# Restart=always\n", + "# EnvironmentFile=/app/staging/env\n", "\n", - "[Install]\n", - "WantedBy=default.target\n", - "EOF\n", + "# [Install]\n", + "# WantedBy=default.target\n", + "# EOF\n", "\n", - "ssh kultmost 'systemctl daemon-reload && systemctl enable --now staging.service && systemctl status --no-pager -l staging.service'\n" + "# ssh kultmost 'systemctl daemon-reload && systemctl enable --now staging.service && systemctl status --no-pager -l staging.service'\n" ], "display_name": ".venv" }, @@ -974,7 +975,7 @@ }, "outputs": [], "source": [ - "!ssh kultmost 'systemctl restart fastapi.service'" + "# !ssh kultmost 'systemctl restart fastapi.service'" ], "display_name": ".venv" }, @@ -989,7 +990,7 @@ }, "outputs": [], "source": [ - "!ssh kultmost 'systemctl status fastapi.service'" + "# !ssh kultmost 'systemctl status fastapi.service'" ], "display_name": ".venv" }, diff --git a/main.ipynb b/main.ipynb index 4568785..ad328d4 100644 --- a/main.ipynb +++ b/main.ipynb @@ -80,7 +80,6 @@ "import uuid\n", "import bcrypt\n", "from typing import List, Any, Dict, Optional, Union\n", - "from datetime import timedelta\n", "from urllib.parse import urlencode\n", "from bson import ObjectId\n", "from PIL import Image\n", @@ -473,6 +472,24 @@ ], "display_name": ".venv" }, + { + "cell_type": "code", + "execution_count": null, + "id": "edb712bd", + "metadata": {}, + "outputs": [], + "source": [ + "@app.get(\"/cron/08\", response_model=dict)\n", + "async def cron_08(background_tasks: BackgroundTasks):\n", + " async for doc in posts_collection.find({\n", + " '_id': {'$gte': ObjectId.from_datetime(datetime.now() - timedelta(days=1))},\n", + " '$or':[{'vk':[]},{'tg':[]},{'fb':[]},{'inst':[]},{'max':[]}]\n", + " }):\n", + " background_tasks.add_task(publish_social_post, bson_clear(doc))\n", + " return {'ok': True}\n" + ], + "display_name": ".venv" + }, { "cell_type": "code", "execution_count": null, diff --git a/main.py b/main.py index e0c93cb..cd32f21 100644 --- a/main.py +++ b/main.py @@ -19,7 +19,6 @@ from starlette.middleware.gzip import GZipMiddleware import uuid import bcrypt from typing import List, Any, Dict, Optional, Union -from datetime import timedelta from urllib.parse import urlencode from bson import ObjectId from PIL import Image @@ -303,6 +302,16 @@ async def posts_temp(user,request): request=request, name="profile/posts.html", context={ "user": user, 'cards': data} ) +@app.get("/cron/08", response_model=dict) +async def cron_08(background_tasks: BackgroundTasks): + async for doc in posts_collection.find({ + '_id': {'$gte': ObjectId.from_datetime(datetime.now() - timedelta(days=1))}, + '$or':[{'vk':[]},{'tg':[]},{'fb':[]},{'inst':[]},{'max':[]}] + }): + background_tasks.add_task(publish_social_post, bson_clear(doc)) + return {'ok': True} + + @app.get("/cron/day") async def cron_every_day(token: str = Security(get_token)): res = await competitions_col.update_many( diff --git a/static/IMG_20260904_063446.jpg b/static/IMG_20260904_063446.jpg new file mode 100644 index 0000000..81fe34b Binary files /dev/null and b/static/IMG_20260904_063446.jpg differ diff --git a/static/script/select.js b/static/script/select.js new file mode 100644 index 0000000..78d44d5 --- /dev/null +++ b/static/script/select.js @@ -0,0 +1,270 @@ +(function() { + 'use strict'; + + const root = document.getElementById('dd-modal-root') || document.body; + + // Утилиты парсинга + function parseList(attr) { + if (!attr) return []; + try { + const fixed = attr.replace(/'/g, '"'); + return JSON.parse(fixed); + } catch (e) { + return String(attr).split(',').map(s => s.trim()).filter(Boolean); + } + } + + function parseSavedValue(el) { + const v = el.getAttribute('data-value'); + if (!v) return []; + try { + return JSON.parse(v); + } catch (e) { + return v.includes(',') + ? v.split(',').map(s => s.trim()).filter(Boolean) + : [v]; + } + } + + async function openModalForInput(triggerEl) { + const listRaw = triggerEl.getAttribute('data-list') || '[]'; + const options = parseList(listRaw); + const isRadio = triggerEl.getAttribute('data-radio') === '1' || + triggerEl.getAttribute('data-radio') === 'true'; + const takeAll = triggerEl.getAttribute('data-takeall') === '1' || + triggerEl.getAttribute('data-takeall') === 'true'; + const allowCustom = triggerEl.getAttribute('data-custom') === '1' || + triggerEl.getAttribute('data-custom') === 'true'; + + const title = triggerEl.getAttribute('data-title') || 'Выберите'; + const saveLabel = triggerEl.getAttribute('data-label') || 'Сохранить'; + const customPlaceholder = triggerEl.getAttribute('data-custom_placeholder') || 'Добавить...'; + + const initial = parseSavedValue(triggerEl); + + return new Promise(resolve => { + const backdrop = document.createElement('div'); + backdrop.className = 'dd-modal-backdrop'; + + const modal = document.createElement('div'); + modal.className = 'dd-modal'; + modal.innerHTML = ` +
+ ${title} + +
+
+
+
+ +
+ `; + + backdrop.appendChild(modal); + root.appendChild(backdrop); + + const body = modal.querySelector('.dd-body'); + const customContainer = modal.querySelector('.dd-custom-container'); + const saveBtn = modal.querySelector('.dd-save'); + const closeBtn = modal.querySelector('.dd-close'); + + const groupName = 'dd-group-' + Date.now() + Math.random().toString(36).slice(2); + + let currentOptions = options.slice(); + let selected = initial.slice(); + + function renderOptions() { + body.innerHTML = ''; + + // "Выбрать все" + if (takeAll) { + const wrapper = createOption('Выбрать все', true, groupName, isRadio); + const takeAllInput = wrapper.querySelector('input'); + takeAllInput.id = 'takeAll'; + takeAllInput.checked = selected.length === 0 || selected.includes('Выбрать все'); + body.appendChild(wrapper); + } + + currentOptions.forEach(opt => { + const label = typeof opt === 'object' ? opt.name : String(opt); + const tooltip = typeof opt === 'object' ? opt.tooltip : null; + const count = typeof opt === 'object' ? opt.count : null; + + + const wrapper = createOption(label, false, groupName, isRadio, tooltip, count); + const input = wrapper.querySelector('input'); + input.checked = selected.includes(label); + body.appendChild(wrapper); + }); + } + + function createOption(label, isTakeAll, name, isRadio, tooltip = null, count = null) { + const wrapper = document.createElement('label'); + wrapper.className = 'dd-option'; + + const input = document.createElement('input'); + input.type = isRadio ? 'radio' : 'checkbox'; + input.name = name; + input.value = label; + input.className = 'offer-checkbox'; + + const span = document.createElement('span'); + span.textContent = label; + + const div = document.createElement('div'); + div.className = 'some-class'; + + // count + if (count !== undefined && count !== null) { + const countNode = document.createElement('span'); + countNode.textContent = count; + div.appendChild(countNode); + } + // tooltip + if (tooltip) { + const img = document.createElement('img'); + img.className = 'tooltip-trigger'; + img.dataset.tooltip = tooltip; + img.src = '/static/help.svg'; + img.alt = 'Подсказка'; + div.appendChild(img); + } + span.appendChild(div); + wrapper.append(input, span); + return wrapper; + } + + if (allowCustom) { + customContainer.innerHTML = ` +
+ + +
+ `; + + const customInput = customContainer.querySelector('.dd-custom-input'); + const customAdd = customContainer.querySelector('.dd-custom-add'); + + const addCustom = (val) => { + val = String(val).trim(); + if (!val) return; + + if (!currentOptions.some(o => String(o) === val && typeof o !== 'object')) { + currentOptions.push(val); + triggerEl.setAttribute('data-list', JSON.stringify(currentOptions)); + } + + if (isRadio) { + selected = [val]; + } else if (!selected.includes(val)) { + selected.push(val); + } + + renderOptions(); + updateCheckedState(); + }; + + customAdd.addEventListener('click', () => { + addCustom(customInput.value); + customInput.value = ''; + customInput.focus(); + }); + + customInput.addEventListener('keydown', e => { + if (e.key === 'Enter') { + e.preventDefault(); + addCustom(customInput.value); + customInput.value = ''; + } + }); + } + + function updateCheckedState() { + Array.from(body.querySelectorAll('input')).forEach(inp => { + inp.checked = selected.includes(inp.value); + }); + } + + function collectChecked() { + return Array.from(body.querySelectorAll('input:checked')).map(i => i.value); + } + + function closeModal() { + const result = collectChecked(); + triggerEl.setAttribute('data-value', isRadio ? (result[0] ?? '') : JSON.stringify(result)); + + if (triggerEl.type && triggerEl.type !== 'hidden') { + triggerEl.value = isRadio ? (result[0] ?? '') : result.join(', '); + } + + backdrop.remove(); + resolve(result); + } + + // Обработчики закрытия + closeBtn.addEventListener('click', closeModal); + saveBtn.addEventListener('click', closeModal); + backdrop.addEventListener('click', e => { + if (e.target === backdrop) closeModal(); + }); + + // Логика "Выбрать все" + function setupTakeAll() { + const takeAllInput = document.getElementById('takeAll'); + if (!takeAllInput) return; + + const allInputs = () => Array.from(body.querySelectorAll('input.offer-checkbox')); + + takeAllInput.addEventListener('change', () => { + if (takeAllInput.checked) { + selected = []; + allInputs().forEach(inp => { + if (inp !== takeAllInput) inp.checked = false; + }); + closeModal(); + } + }); + + // Сброс "Выбрать все" при выборе других + body.addEventListener('change', e => { + const inp = e.target; + if (inp.type !== 'checkbox' || inp === takeAllInput) return; + + if (inp.checked) { + takeAllInput.checked = false; + } else { + const anyChecked = allInputs().some(x => x !== takeAllInput && x.checked); + takeAllInput.checked = !anyChecked; + } + }); + } + + renderOptions(); + setupTakeAll(); + updateCheckedState(); + }); + } + + // Главный обработчик + document.addEventListener('click', async function(e) { + const trigger = e.target.closest('[data-select="1"]'); + if (!trigger) return; + + const input = trigger.querySelector('input'); + if (input) { + input.readOnly = true; + input.blur(); + } + + e.preventDefault(); + + const result = await openModalForInput(trigger); + if (result && result.length) { + trigger.dispatchEvent(new Event('change', { bubbles: true })); + } + }, true); + +})(); \ No newline at end of file diff --git a/static/script/share.js b/static/script/share.js new file mode 100644 index 0000000..6e7b5da --- /dev/null +++ b/static/script/share.js @@ -0,0 +1,138 @@ +async function imageWithTextBlob(imageUrl, text, options = {}) { + const response = await fetch(imageUrl, { + mode: 'cors', + credentials: 'same-origin', + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const imageBlob = await response.blob(); + const imageBitmap = await createImageBitmap(imageBlob); + + // OffscreenCanvas (works in modern browsers; fallback to normal canvas if needed) + const canvas = typeof OffscreenCanvas !== 'undefined' + ? new OffscreenCanvas( + options.width ?? imageBitmap.width, + options.height ?? imageBitmap.height + ) + : document.createElement('canvas'); + + const width = canvas.width ?? (options.width ?? imageBitmap.width); + const height = canvas.height ?? (options.height ?? imageBitmap.height); + + if (!(canvas instanceof OffscreenCanvas)) { + canvas.width = width; + canvas.height = height; + } + + const ctx = canvas.getContext('2d', { alpha: true }); + + // Draw image + ctx.drawImage(imageBitmap, 0, 0, width, height); + + // No text: return original bitmap as blob + if (!text?.trim()) { + if (canvas.convertToBlob) { + return canvas.convertToBlob({ + type: options.type ?? 'image/png', + quality: options.quality ?? 0.92, + }); + } + return await new Promise((resolve) => + canvas.toBlob((b) => resolve(b), options.type ?? 'image/png', options.quality ?? 0.92) + ); + } + + // === Text rendering === + const basePadding = options.basePadding ?? 8; // internal left/right/top padding + const fontSize = options.fontSize ?? Math.round(width * 0.05); + ctx.font = `${fontSize}px ${options.fontFamily ?? 'sans-serif'}`; + ctx.textBaseline = 'bottom'; + ctx.textAlign = 'center'; + + const textColor = options.textColor ?? '#5F1798'; + const bgColor = options.bgColor ?? '#fff'; + const strokeColor = options.strokeColor ?? 'rgba(0,0,0,0.35)'; + const shadowBlur = options.shadowBlur ?? 6; + const maxWidth = width - basePadding * 2 - (options.extraPadding ?? 0); + + // Wrap text into lines + const words = text.split(' '); + let line = ''; + const lines = []; + for (const word of words) { + const testLine = line ? line + ' ' + word : word; + if (ctx.measureText(testLine).width > maxWidth && line) { + lines.push(line); + line = word; + } else { + line = testLine; + } + } + if (line) lines.push(line); + + const lineHeight = fontSize + (options.lineGap ?? 6); + + // Extra internal bottom padding inside the rounded box + const internalBottomPadding = options.internalBottomPadding ?? Math.round(fontSize * 0.35); + const boxHeight = lines.length * lineHeight + basePadding + internalBottomPadding; + + const maxLineWidth = Math.max(...lines.map((l) => ctx.measureText(l).width)); + const boxWidth = maxLineWidth + basePadding * 2; + + // Distance from the very bottom of the image to the bottom of the text box + const bottomPadding = options.bottomPadding ?? Math.round(fontSize * 0.9); + + const boxX = (width - boxWidth) / 2; + const boxY = height - bottomPadding - boxHeight; + const radius = options.radius ?? 8; + + // Background rounded rect + ctx.save(); + ctx.fillStyle = bgColor; + ctx.shadowColor = strokeColor; + ctx.shadowBlur = shadowBlur; + roundRect(ctx, boxX, boxY, boxWidth, boxHeight, radius, true, false); + ctx.restore(); + + // Text + ctx.fillStyle = textColor; + ctx.shadowColor = 'transparent'; + + for (let i = 0; i < lines.length; i++) { + const lineY = boxY + basePadding + (i + 1) * lineHeight - (lineHeight - fontSize) / 2; + const lineX = width / 2; + + if (options.outline) { + ctx.lineWidth = options.outlineWidth ?? 2; + ctx.strokeStyle = options.outlineColor ?? 'rgba(255,255,255,0.8)'; + ctx.strokeText(lines[i], lineX, lineY); + } + ctx.fillText(lines[i], lineX, lineY); + } + + // Convert to blob + if (canvas.convertToBlob) { + return await canvas.convertToBlob({ + type: options.type ?? 'image/png', + quality: options.quality ?? 0.92, + }); + } + return await new Promise((resolve) => + canvas.toBlob((b) => resolve(b), options.type ?? 'image/png', options.quality ?? 0.92) + ); +} +function roundRect(ctx, x, y, w, h, r, fill, stroke) { + if (typeof r === 'number') r = { tl: r, tr: r, br: r, bl: r }; + ctx.beginPath(); + ctx.moveTo(x + r.tl, y); + ctx.lineTo(x + w - r.tr, y); + ctx.quadraticCurveTo(x + w, y, x + w, y + r.tr); + ctx.lineTo(x + w, y + h - r.br); + ctx.quadraticCurveTo(x + w, y + h, x + w - r.br, y + h); + ctx.lineTo(x + r.bl, y + h); + ctx.quadraticCurveTo(x, y + h, x, y + h - r.bl); + ctx.lineTo(x, y + r.tl); + ctx.quadraticCurveTo(x, y, x + r.tl, y); + ctx.closePath(); + if (fill) ctx.fill(); + if (stroke) ctx.stroke(); +} \ No newline at end of file diff --git a/static/service-worker.js b/static/service-worker.js index 958a026..67505a1 100644 --- a/static/service-worker.js +++ b/static/service-worker.js @@ -2,7 +2,7 @@ importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox workbox.setConfig({ debug: false }); -const CACHE_NAME = 'app-cache-v2.1.5'; +const CACHE_NAME = 'app-cache-v2.1.6'; const MIN_VALID_SIZE = 500; const ASSETS_TO_CACHE = [ @@ -19,6 +19,8 @@ const ASSETS_TO_CACHE = [ '/static/event3.jpg', '/static/event4.jpg', '/static/event5.jpg', + '/static/script/share.js', + '/static/script/select.js', '/static/event6.jpg', '/static/event7.webp', '/partner', diff --git a/templates/base.html b/templates/base.html index ab85e47..b5d4d6a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -97,8 +97,10 @@ picture{cursor: zoom-in;} {% block content %}{% endblock %} - - +
@@ -868,6 +818,7 @@ shareBtns.forEach(btn => { diff --git a/templates/competition/edit.html b/templates/competition/edit.html index 8b0f687..5099334 100644 --- a/templates/competition/edit.html +++ b/templates/competition/edit.html @@ -207,6 +207,7 @@ {%- endmacro %} diff --git a/templates/competition/form.html b/templates/competition/form.html index 609d8a0..e299948 100644 --- a/templates/competition/form.html +++ b/templates/competition/form.html @@ -3,6 +3,7 @@ {% macro section(user) -%}
@@ -348,10 +350,13 @@ function bindLinkValidation({ }); +// @license-end {% if user.send_address %} {% endif %} {%- endmacro %} diff --git a/templates/competition/index.html b/templates/competition/index.html index 9e7094c..6ed25ef 100644 --- a/templates/competition/index.html +++ b/templates/competition/index.html @@ -153,6 +153,7 @@ diff --git a/templates/components/contacts.html b/templates/components/contacts.html new file mode 100644 index 0000000..2c91a83 --- /dev/null +++ b/templates/components/contacts.html @@ -0,0 +1,52 @@ +{% macro details(user) -%} +
+ Показать контакты + +
+{%- endmacro %} + +{% macro style() -%} + +{%- endmacro %} \ No newline at end of file diff --git a/templates/components/header.html b/templates/components/header.html index 49b57af..2a306ee 100644 --- a/templates/components/header.html +++ b/templates/components/header.html @@ -1,5 +1,6 @@ {% macro section(user) -%} {% if user %} {% if user.notify %} + // @license-end + {% endif %} {% endif %}
@@ -198,6 +202,7 @@ document.addEventListener('DOMContentLoaded', () => { {% if user and 'back' in user.get('scopes',[]) %} Переключиться на Админа + // @license-end + {% endif %} {% endmacro %} \ No newline at end of file diff --git a/templates/exhibition/index.html b/templates/exhibition/index.html index f369b30..1c7f930 100644 --- a/templates/exhibition/index.html +++ b/templates/exhibition/index.html @@ -2,6 +2,7 @@ {% import "/components/header.html" as header %} {% import "/components/footer.html" as footer %} {% import "components/share.html" as share %} +{% import "/components/contacts.html" as contacts %} {% block title %}{{exhibition.title}} | Картины{% endblock %} @@ -42,6 +43,14 @@

{{exhibition.title}}

+
+ +
+ Организатор + {{exhibition.user.username}} +
+
+ {{ contacts.details(exhibition.user) }} {% if exhibition.get('tizer_file_id') or exhibition.get('tizer_link') %} Релиз выставки @@ -118,6 +127,7 @@ @@ -195,6 +208,7 @@ {{ header.style() }} +{{ contacts.style() }} - {{ footer.style() }} {% endblock %} \ No newline at end of file diff --git a/templates/profile/form.html b/templates/profile/form.html index 0a69008..c2faf6d 100644 --- a/templates/profile/form.html +++ b/templates/profile/form.html @@ -217,6 +217,7 @@ {{ footer.section(user) }} {% endblock %} diff --git a/templates/profile/invitation.html b/templates/profile/invitation.html index 94197d9..eb87f53 100644 --- a/templates/profile/invitation.html +++ b/templates/profile/invitation.html @@ -3,6 +3,7 @@ {% import "/components/footer.html" as footer %} {% import "components/profile_card.html" as profile_card %} {% import "components/profile_nav.html" as profile_nav %} +{% import "/components/contacts.html" as contacts %} {% block title %}Приглашения{% endblock %} @@ -199,6 +200,7 @@ {%endif%} {% if doc.get('type_')=='paint' %} @@ -253,6 +256,7 @@ {# Sticky bottom action bar #} {% endif %} @@ -568,6 +579,7 @@ document.getElementById('delivery_type')?.addEventListener('change', toggleTrack {{ footer.section(user) }} {% endblock %} @@ -742,45 +755,7 @@ document.getElementById('delivery_type')?.addEventListener('change', toggleTrack {% endif %} -
- Показать контакты - -
+{{ contacts.details(user) }} {% endmacro %} {% macro render_links(doc) %} {% if doc.get('exhibition')%} @@ -793,6 +768,7 @@ document.getElementById('delivery_type')?.addEventListener('change', toggleTrack {% block head %} {{ header.style() }} {{ profile_nav.style() }} +{{ contacts.style() }}