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 = `
+
+
+
+
+
+
+ `;
+
+ 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 %}
@@ -111,6 +113,7 @@ var userAgent = navigator.userAgent;var isFirefox = userAgent.indexOf("Firefox")
@@ -172,14 +178,13 @@ function showToast(e={}){let{message:t="",status:a="info",duration:l=4e3}=e,n=do
-
-
+
@@ -868,6 +818,7 @@ shareBtns.forEach(btn => {