9 Commits

14 changed files with 574 additions and 402 deletions

View File

@@ -9,6 +9,11 @@ SECRET_KEY='change_me_in_production'
# Разрешённые хосты. В ПРОДАКШЕНЕ: Установите реальные домены, с которых будет доступно ваше приложение. # Разрешённые хосты. В ПРОДАКШЕНЕ: Установите реальные домены, с которых будет доступно ваше приложение.
ALLOWED_HOSTS=127.0.0.1,localhost ALLOWED_HOSTS=127.0.0.1,localhost
# CSRF Trusted Origins (ВАЖНО для Docker/Nginx/SSL).
# Перечислите здесь URL, по которым вы заходите на сайт, включая схему (http/https).
# Если этого не сделать, при попытке залогиниться в админку вы получите ошибку CSRF.
CSRF_TRUSTED_ORIGINS=https://dq.cube2.ru,http://127.0.0.1:8010,http://localhost:8010
# Email администратора (для получения уведомлений о критических ошибках и других важных сообщений от Django). # Email администратора (для получения уведомлений о критических ошибках и других важных сообщений от Django).
ADMIN_EMAIL=admin@example.com ADMIN_EMAIL=admin@example.com

132
README.md
View File

@@ -1,16 +1,130 @@
# DicQuo (цитаты и высказывания) # DicQuo (Цитаты, Афоризмы и Факты)
Пет-проект ротации цитат и высказываний. Развернут на [dq.cube2.ru](https://dq.cube2.ru). **Dicquo** — это коллекция отобранных вручную цитат, оформленных с уважением к типографике. Место для вдумчивого чтения, переосмысления и поиска вдохновения. Проект создан как пространство, где типографика встречается со смыслом, а технологии помогают контенту выглядеть безупречно.
Изначальная цель: Основные цели проекта:
* Испытывать различные типографы (библиотеки и API), разрабатывать и тестировать свои * **Типографика как искусство:** Разработка и тестирование собственных алгоритмов типографирования (висячая пунктуация, неразрывные пробелы, правильные тире) из библиотеки `etpgrf` (доступен в [GitHub](https://github.com/erjemin/etpgrf), [GitVerse](https://gitverse.ru/erjemin/etpgrf) и self-hosted [Cube2](https://git.cube2.ru/erjemin/2025-etpgrf), онлайн версия развёрнута на [typograph.cube2.ru](https://typograph.cube2.ru/)).
типографы и правила типографирования. * **SEO-эксперименты:** Исследование влияния микроразметки, мета-тегов и семантической верстки на индексацию поисковыми системами.
* Испытать, как содержание отдельных атрибуты (meta, keywords, description и т.п.) * **Технологический стек:** Современный Django, Docker, CI/CD и автоматизация деплоя.
влияют на поисковую выдачу.
[Инструкция по развертыванию на хостинге c CGI Passenger](deploy_to_dreamhost.md) Развернут на [dq.cube2.ru](https://dq.cube2.ru).
## ToDo? ---
## Структура файлов на сервере (Production)
После правильного развертывания, папка проекта на сервере (например, `~/docker-apps/dicquo/`) должна выглядеть так:
```text
dicquo/
├── docker-compose.yml # Переименованный docker-compose.prod.yml из этого репозитория (запускает контейнеры)
├── .env # Файл с переменными окружения и секретами (не пушить в git!)
├── database/ # Папка для базы данных (Persistent Volume)
│ ├── db.sqlite3 # Фактический файл базы данных
├── media/ # Папка с медиа-файлами (Persistent Volume)
│ ├── img2/ # Картинки для цитат (доступны через Nginx)
│ └── errors/ # Картинки для страниц ошибок (404, 500)
└── config/ # Папка для конфигураций (Генерируется контейнером)
└── nginx/
├── dq-app--external-nginx.conf # Конфиг для внешнего Nginx (скопируется из контейнера при первом запуске)
└── nginx_dq.conf.example # Образец конфига для внешнего Nginx (не используется в продакшене)
```
---
## Развёртывание (Deployment)
Проект полностью упакован в Docker и разворачивается с помощью `docker compose`. Ниже приведена инструкция для развертывания на чистом Linux-сервере (Ubuntu/Debian, архитектуры AMD/ARM).
### 1. Подготовка структуры
Создайте директорию для проекта (например, в домашней папке пользователя) и необходимые подпапки для persistent-данных:
```bash
mkdir -p ~/docker-apps/dicquo
cd ~/docker-apps/dicquo
mkdir -p database media config
```
### 2. Файлы конфигурации
Вам понадобятся два файла из репозитория (или их содержимое):
1. **`docker-compose.prod.yml`** -> сохраните его на сервере как `docker-compose.yml`.
2. **`.env`** -> создайте на основе `.env.sample`, заполнив секретами для продакшена.
**Важные переменные в `.env`:**
* `HOST_PROJECT_PATH`: Полный путь к папке проекта на хосте (например, `/home/username/docker-apps/dicquo`). Используется для корректной генерации конфига Nginx.
* `DJANGO_ALLOWED_HOSTS`: Список доменов через запятую (например, `dq.cube2.ru,127.0.0.1`).
### 3. Перенос данных (Опционально)
Если вы мигрируете с dev-окружения или другого сервера, можно просто скопировать файлы базы данных и медиа.
Скопируйте файл базы `database/db.sqlite3` и содержимое папки `media/` в соответсвующие папки на сервере.
### 4. Настройка прав доступа (Permissions) ⚠️
Это **критически важный этап**.
1. Docker-контейнер (с нашим бэкендом Django и Gunicorn) должн иметь доступ к примонтированным папкам.
2. Внешний Nginx (на хосте) должен иметь доступ к статике и медиа.
**Права на папки проекта:**
```bash
# Разрешаем запись в базу и медиа для всех (самый простой способ избежать проблем с UID внутри Docker)
sudo chmod 777 database
sudo chmod 666 database/db.sqlite3
sudo chmod -R 755 media
```
**Права на родительские директории (Pass-through):**
Если проект лежит в домашней папке пользователя (`/home/username/...`), то Nginx (пользователь `www-data`) по умолчанию **не сможет** туда попасть. Нужно разрешить "проход" (execute) для всех пользователей по пути к проекту:
```bash
# Разрешаем "проход" через домашнюю папку (чтение файлов при этом остается закрытым, только доступ к известным путям)
chmod o+x /home/username
chmod o+x /home/username/docker-apps
chmod o+x /home/username/docker-apps/dicquo
```
> *Без этого шага Nginx будет выдавать 403 Forbidden на картинки и статику.*
### 5. Запуск
```bash
docker compose up -d
```
При первом запуске контейнер автоматически:
* Применит миграции.
* Соберет статику.
* Сгенерирует конфиг для Nginx в папке `config/nginx/`.
### 6. Настройка Nginx и SSL
1. **Подключение конфига:**
Создайте симлинк на сгенерированный конфиг:
```bash
sudo ln -s /home/username/docker-apps/dicquo/config/nginx/dq-app--external-nginx.conf /etc/nginx/sites-enabled/dq-app.conf
```
2. **Проверка и релоад:**
```bash
sudo nginx -t
sudo systemctl reload nginx
```
3. **Получение SSL сертификата (Certbot):**
```bash
sudo certbot --nginx -d dq.cube2.ru
```
---
## Разработка (Dev)
Для локального запуска используется `docker-compose.yml` (он же dev-версия).
```bash
docker compose up --build
```
Проект будет доступен по адресу: http://127.0.0.1:8008
## ToDo
* В будущем, возможно, сделать API для предоставления цитат вешним потребителям * В будущем, возможно, сделать API для предоставления цитат вешним потребителям
(по темам, авторам и т.п.). (по темам, авторам и т.п.).

View File

@@ -21,15 +21,18 @@ env = environ.Env(
# If BASE_DIR is .../dicquo, then .env is at BASE_DIR.parent/.env # If BASE_DIR is .../dicquo, then .env is at BASE_DIR.parent/.env
environ.Env.read_env(os.path.join(BASE_DIR.parent, '.env')) environ.Env.read_env(os.path.join(BASE_DIR.parent, '.env'))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY') SECRET_KEY = env('SECRET_KEY')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG') DEBUG = env('DEBUG')
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[]) ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['127.0.0.1', 'localhost'])
CSRF_TRUSTED_ORIGINS = env.list('CSRF_TRUSTED_ORIGINS', default=['http://127.0.0.1', 'http://localhost'])
# Custom Admin URL from .env
ADMIN_URL = env('ADMIN_URL', default='admin/')
######################################### #########################################
# Настройки сообщений об ошибках когда все упало и т.п. # Настройки сообщений об ошибках когда все упало и т.п.

View File

@@ -19,7 +19,7 @@ from django.urls import path, re_path
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap from django.contrib.sitemaps.views import sitemap
from django.views.generic import TemplateView from django.views.generic import TemplateView
from dicquo import settings from django.conf import settings
from web import views from web import views
from web.sitemaps import DictumSitemap from web.sitemaps import DictumSitemap
@@ -28,7 +28,7 @@ sitemaps = {
} }
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), re_path(f'^{settings.ADMIN_URL}', admin.site.urls),
re_path(r'^$', views.IndexView.as_view()), re_path(r'^$', views.IndexView.as_view()),
re_path(r'^(?P<dq_id>\d{1,12})_\S*$', views.DictumDetailView.as_view()), re_path(r'^(?P<dq_id>\d{1,12})_\S*$', views.DictumDetailView.as_view()),

View File

@@ -1,58 +1,34 @@
<!DOCTYPE html> <!DOCTYPE html>{% load static %}
{% load static %}<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1"/>
{# SEO & Meta #}<title>{% block Title %}{% endblock %}</title>
<!-- SEO & Meta --> <meta name="description" content="{% block Description %}{% endblock %}"/>
<title>{% block Title %}{% endblock %}</title> <meta name="keywords" content="{% block Keywords %}{% endblock %}"/>
<meta name="description" content="{% block Description %}{% endblock %}" /> <meta name="copyright" content="Sergei Erjemin (дизайн){% block CopyrightAuthor4Meta %}{% endblock %}."/>
<meta name="keywords" content="{% block Keywords %}{% endblock %}" /> <meta name="robots" content="index,follow"/>
<meta name="copyright" content="Sergei Erjemin (дизайн){% block CopyrightAuthor4Meta %}{% endblock %}." /> {# Open Graph / Social Media #}<meta property="og:type" content="article"/>
<meta name="robots" content="index,follow" /> <meta property="og:title" content="{% block OgTitle %}{{ DQ.szContent|truncatechars:85 }}{% endblock %}"/>
<meta property="og:description" content="{% block OgDescription %}{{ DQ.szIntro|default:'' }} {{ DQ.szContent }} {{ AUTHOR.szAuthor|default:'' }}{% endblock %}"/>
<!-- Open Graph / Social Media --> <meta property="og:url" content="{{ request.build_absolute_uri }}"/>
<meta property="og:type" content="article" /> <meta property="og:site_name" content="DicQuo"/>
<meta property="og:title" content="{% block OgTitle %}{{ DQ.szContent|truncatechars:60 }}{% endblock %}" /> {% if IMAGE %}<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ IMAGE.url }}"/>{% endif %}
<meta property="og:description" content="{% block OgDescription %}{{ DQ.szIntro|default:'' }} {{ DQ.szContent }} {{ AUTHOR.szAuthor|default:'' }}{% endblock %}" /> {# Шутка #}<meta name="generator" content="Microsoft FrontPage 1.0"/>
<meta property="og:url" content="{{ request.build_absolute_uri }}" /> {# Canonical #}<link rel="canonical" href="{{ request.build_absolute_uri }}"/>
<meta property="og:site_name" content="DicQuo" /> {# Favicons #}<link rel="shortcut icon" type="image/x-icon" href="{% static 'img/favicon.ico' %}"/>
{% if IMAGE %}<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ IMAGE.url }}" />{% endif %} <link rel="icon" type="image/png" href="{% static 'img/favicon.png' %}"/>
{# Technical Meta #}<meta http-equiv="Last-Modified" content="{% block Last4Meta %}{% endblock %}"/>
<!-- Technical Meta --> {# CSS #}<link rel="stylesheet" href="{% static 'css/dicquo.css' %}"/>
<meta http-equiv="Last-Modified" content="{% block Last4Meta %}{% endblock %}" /> <noscript><style>body { opacity: 1; }</style></noscript>{# Показать все если JS не поддерживатся #}
<meta name="generator" content="Django" /> {% block ExtraHead %}{# Если нужно что=то добавить в `<head>` #}{% endblock %}
<!-- Favicons -->
<link rel="shortcut icon" type="image/x-icon" href="{% static 'img/favicon.ico' %}" />
<link rel="icon" type="image/png" href="{% static 'img/favicon.png' %}" />
<!-- Styles -->
<link rel="stylesheet" href="{% static 'css/dicquo.css' %}" />
<style>
body {
margin: 0;
min-height: 100vh;
background-color: #111; /* Изначально темный фон */
opacity: 0; /* Скрываем контент до расчета цвета */
transition: opacity 0.9s ease-in-out; /* Очень плавное появление */
}
</style>
<noscript>
<style>body { opacity: 1; }</style>
</noscript>
{% block ExtraHead %}{% endblock %}
</head> </head>
<body> <body>{% if DQ %}
{% if DQ %} {# Этот блок для передачи JavaScript-скрипту `bg-generator.js` текст цитаты ({{ DQ.szContent }}) и скрипт на основе него делает уникальный, но постоянный для этой цитаты фон (градиент) #}<span id="dq-content-raw" style="display:none;">{{ DQ.szContent }} ({% if AUTHOR %}{{ AUTHOR.szAuthorHTML|default:AUTHOR.szAuthor }}{% endif %})</span>{% endif %}
<span id="dq-content-raw" style="display:none;">{{ DQ.szContent }}</span> {% include "blocks/header_nav.html" %}
{% endif %} {% block CONTENT %}{% endblock %}{% if not cookie_accept %}
{% include "blocks/cookie_warning.html" %}{% endif %}
{% block CONTENT %}{% endblock %}
{% include "blocks/counters.html" %}
<script src="{% static 'js/bg-generator.js' %}"></script> <script src="{% static 'js/bg-generator.js' %}"></script>
{% include "blocks/counters.html" %}
</body> </body>
</html> </html>

View File

@@ -1,11 +1,6 @@
<!-- ПОДВАЛ: НАЧАЛО -- соглашение о сборе технической информации --> <footer data-nosnippet>
<div name="cookies_accept"> <!--noindex-->
<small>Тут используют cookie и&nbsp;ведут сбор технических данных о&nbsp;посещениях, потому как без этого <nobr>интернет-сайты</nobr> вообще почти <nobr>не&nbsp;работают&hellip;</small> <small>Следы на песке смывает волна, но цифровые следы (cookie) помогают нам помнить вас, пока вы здесь.</small>
<button onclick="CookieAcceptDate = new Date(); <button>Осознаю</button>
CookieAcceptDate.setTime(CookieAcceptDate.getTime() + 7948800000); <!--/noindex-->
document.cookie = 'cookie_accept=yes;expires=' + CookieAcceptDate; </footer>
document.getElementsByName('cookies_accept')[0].remove();">
Я согласен!
</button></nobr>
</div>
<!-- ПОДВАЛ: КОНЕЦ -->

View File

@@ -1,4 +1,2 @@
<!-- Rating Mail.ru counter --> {% load static %}<script src="{% static 'js/counters.js' %}"></script>
<script type="text/javascript">var _tmr = window._tmr || (window._tmr = []);_tmr.push({id:"1603042",type:"pageView",start:(new Date()).getTime()});(function(d,w,id){if(d.getElementById(id)) return;var ts=d.createElement("script");ts.type="text/javascript";ts.async=true;ts.id=id;ts.src="https://top-fwz1.mail.ru/js/code.js";var f=function(){var s=d.getElementsByTagName("script")[0];s.parentNode.insertBefore(ts,s);};if(w.opera == "[object Opera]"){ d.addEventListener("DOMContentLoaded",f,false);}else{f();}})(document,window,"topmailru-code");</script><noscript><div><img src="https://top-fwz1.mail.ru/counter?id=1603042;js=na" style="border:0;position:absolute;left:-9999px;" alt="Top.Mail.Ru" /></div></noscript> <noscript><div><img src="https://top-fwz1.mail.ru/counter?id=3744288;js=na" class="counter-pixel" alt="Top.Mail.Ru"/></div></noscript>
<!-- //Rating Mail.ru counter -->

View File

@@ -1,25 +1,15 @@
{% load static %} {% load static %}{# ШАПКА #}<header>
<!-- ШАПКА: НАЧАЛО --> <a href="/" id="logo">
<div class="container"> <img src='{% static "svgs/dq-logo.svg" %}' alt="Dictum & Quotes" title="Dictum & Quotes"/>
<header> </a>
<a href="/" id="logo"> <nav>
<img src='{% static "svgs/dq-logo.svg" %}' alt="Dictum & Quotes" title="Dictum & Quotes" <span id="stats-menu">
width="50" height="46"/> {# Манифест проекта #}<p>
</a> DicQuo&nbsp;— это коллекция отобранных вручную цитат, оформленных с&nbsp;уважением к&nbsp;типографике.<br/>
<div> Место для&nbsp;вдумчивого&nbsp;чтения.
<span id="stats-menu" style="display: none; color: silver; font-size: 0.9em; margin-right: 15px; text-align: right;"> </p>
<!-- Манифест проекта --> {# МЕНЮ #}{% if ticks %}<i class="stats-icon icon-time" title="Время генерации"></i>{{ ticks|floatformat:3|default:"1,023" }} ms{% endif %}{% if DQ %} <b>|</b> <i class="stats-icon icon-views" title="Просмотры"></i> {{ DQ.iViewCounter }}{% endif %} <a href="/add_quote/" title="Добавить цитату"></a> &nbsp;
<div style="margin-bottom: 5px; color: #aaa; font-style: italic; max-width: 300px; display: inline-block;"> </span>
Dicquo&nbsp;— это коллекция отобранных вручную цитат, оформленных с&nbsp;уважением к&nbsp;типографике. Место для&nbsp;вдумчивого чтения. {# БУРГЕР #}<a href="#" onclick="var m=document.getElementById('stats-menu'); m.style.display = (m.style.display === 'none' ? 'inline-block' : 'none'); return false;" title="О проекте"></a>
</div> </nav>
<br/>
<!-- Статистика -->
{% if ticks %}<i class="stats-icon icon-time" title="Время генерации"></i>{{ ticks|floatformat:1 }}ms{% endif %}
{% if DQ %} &nbsp;|&nbsp; <i class="stats-icon icon-views" title="Просмотры"></i>{{ DQ.iViewCounter }}{% endif %}
&nbsp;|&nbsp; <a href="/add_quote/" style="color: silver; text-decoration: none;" title="Добавить цитату"><i class="stats-icon icon-add"></i></a> &nbsp;|&nbsp;
</span>
<a href="#" onclick="var m=document.getElementById('stats-menu'); m.style.display = (m.style.display === 'none' ? 'inline-block' : 'none'); return false;" style="color: silver; text-decoration: none; font-size: 1.2em;"></a>
</div>
</header> </header>
</div>
<!-- ШАПКА: КОНЕЦ -->

View File

@@ -1,6 +0,0 @@
<!-- ТЕХНИЧЕСКАЯ ИНФОРМАЦИЯ: НАЧАЛО -->
<div style="bottom:0;" class="position-sticky float-right fixed-bottom">
<small style="background:#674376;color: white;font-size: xx-small;"
class="x">&ensp;🕗&nbsp;{{ ticks|stringformat:".6f" }}&thinsp;s&thinsp;<nobr>({% now 'c' %})</nobr>&ensp;</small>
</div>
<!-- ТЕХНИЧЕСКАЯ ИНФОРМАЦИЯ: КОНЕЦ -->

View File

@@ -13,84 +13,53 @@
<!--- ТИТУЛ ---> <!--- ТИТУЛ --->
{% block Title %}{% if AUTHOR %}{{ AUTHOR.szAuthor }} — {% endif %}{{ DQ.szContent|truncatewords:7 }} | Dicquo{% endblock %} {% block Title %}{% if AUTHOR %}{{ AUTHOR.szAuthor }} — {% endif %}{{ DQ.szContent|truncatewords:7 }} | Dicquo{% endblock %}
{% block ExtraHead %} {% block ExtraHead %}<script type="application/ld+json">
<script type="application/ld+json"> {
{ "@context": "https://schema.org",
"@context": "https://schema.org", "@type": "Quotation",
"@type": "Quotation", "name": "Цитата #{{ DQ.id }}",
"name": "Цитата #{{ DQ.id }}", "text": "{{ DQ.szContent|escapejs }}",
"text": "{{ DQ.szContent|escapejs }}", "creator": {
"creator": { "@type": "Person",
"@type": "Person", "name": "{% if AUTHOR %}{{ AUTHOR.szAuthor|escapejs }}{% else %}Неизвестный автор{% endif %}"
"name": "{% if AUTHOR %}{{ AUTHOR.szAuthor|escapejs }}{% else %}Неизвестный автор{% endif %}" },
}, "url": "{{ request.build_absolute_uri }}",
"url": "{{ request.build_absolute_uri }}", {% if IMAGE %}"image": "{{ request.scheme }}://{{ request.get_host }}{{ IMAGE.url }}",{% endif %}
{% if IMAGE %}"image": "{{ request.scheme }}://{{ request.get_host }}{{ IMAGE.url }}",{% endif %} "keywords": "афоризмы, цитаты, {% for i in TAGS %}{{ i.name|escapejs }}{% if not forloop.last %}, {% endif %}{% endfor %}",
"keywords": "афоризмы, цитаты, {% for i in TAGS %}{{ i.name|escapejs }}{% if not forloop.last %}, {% endif %}{% endfor %}", "inLanguage": "ru",
"inLanguage": "ru", "isPartOf": {
"isPartOf": { "@type": "WebSite",
"@type": "WebSite", "name": "Dicquo",
"name": "Dicquo", "url": "{{ request.scheme }}://{{ request.get_host }}"
"url": "{{ request.scheme }}://{{ request.get_host }}" },
}, "dateCreated": "{{ DQ.dtCreated|date:'Y-m-d' }}",
"dateCreated": "{{ DQ.dtCreated|date:'Y-m-d' }}", "dateModified": "{{ DQ.dtEdited|date:'Y-m-d' }}"
"dateModified": "{{ DQ.dtEdited|date:'Y-m-d' }}" }
} </script>{% endblock %}
</script>
{% endblock %}
{% block CONTENT %}{% include "blocks/header_nav.html" %} {% block CONTENT %}<main>{# Основной контент: Текст + Картинка #}
<div class="container main-content"> <article>{# Текстовая ряб. Задает высоту двум колонкам: текст и картина #}
<!-- Основной контент: Текст + Картинка --> <figure>{# КОЛОНКА С ТЕКСТОМ #}{% if DQ.szIntroHTML %}
<div class="content-row"> {# Интро/Вступление (например "Вася Пупкин как-то сказа". Может отсутствовать #}<p>{{ DQ.szIntroHTML|safe }}</p>{% endif %}
<blockquote>{{ DQ.szContentHTML|safe }}</blockquote>{% if AUTHOR %}
<!-- Текстовая колонка --> <cite>{# Автор #}{{ AUTHOR.szAuthorHTML|default:AUTHOR.szAuthor|safe }}</cite>{% endif %}
<div class="text-col"> </figure>{% if IMAGE %}
<!-- Интро/Вступление --> <div>{# КОЛОНКА С КАРТИНКОЙ #}
{% if DQ.szIntroHTML %} <div style="background:rgba(87,0,0,0.7);">
<div id="info">{{ DQ.szIntroHTML|safe }}</div> <div>
{% endif %} <img src="{{IMAGE.url}}" alt="{% if AUTHOR %}{{ AUTHOR.szAuthor }}{% else %}Dictum & Quotes{% endif %}" title="{% if AUTHOR %}{{ AUTHOR.szAuthor }}{% else %}Dictum & Quotes{% endif %}" />
</div>
<!-- Цитата: Семантический blockquote -->
<blockquote id="bb" style="border:none; margin:0; padding:0;">
{{ DQ.szContentHTML|safe }}
</blockquote>
<!-- Автор: Семантический cite -->
<div id="author">
<cite>
{% if AUTHOR %}
{{ AUTHOR.szAuthorHTML|default:AUTHOR.szAuthor|safe }}
{% endif %}
</cite>
</div> </div>
</div> </div>{% endif %}
</article>
<!-- Колонка с картинкой (если есть) --> </main>
{% if IMAGE %} <nav>
<div class="image-col" id="image"> <div>{# ТЕГИ #}{% for i in TAGS %}
<center> <a href="/?tag={{ i.slug }}">{{ i.name|safe }}</a> {% endfor %}
<div style="background:rgba(87,0,0,0.7);">
<div><img src="{{IMAGE.url}}" alt="{% if AUTHOR %}{{ AUTHOR.szAuthor }}{% else %}Dictum & Quotes{% endif %}" title="{% if AUTHOR %}{{ AUTHOR.szAuthor }}{% else %}Dictum & Quotes{% endif %}" /></div>
</div>
</center>
</div>
{% endif %}
</div>
<!-- Блок тегов и навигации -->
<div class="tags">
{% for i in TAGS %}<a href="/?tag={{ i.slug }}">{{ i.name|safe }}</a> {% endfor %}
<div id="next"><a href="/{{ NEXT }}_{{ NEXT_TXT }}{% if CURRENT_TAG %}?tag={{ CURRENT_TAG }}{% endif %}">&rightarrow;</a></div> <div id="next"><a href="/{{ NEXT }}_{{ NEXT_TXT }}{% if CURRENT_TAG %}?tag={{ CURRENT_TAG }}{% endif %}">&rightarrow;</a></div>
</div> </div>
</div> </nav>
<noscript>
<meta http-equiv="refresh" content="15; url=/{{ NEXT}}_{{ NEXT_TXT }}{% if CURRENT_TAG %}?tag={{ CURRENT_TAG }}{% endif %}">
<noscript> </noscript>{% endblock %}
<meta http-equiv="refresh" content="15; url=/{{ NEXT}}_{{ NEXT_TXT }}{% if CURRENT_TAG %}?tag={{ CURRENT_TAG }}{% endif %}">
</noscript>
{% if not cookie_accept %}{% include "blocks/cookie_warning.html" %}{% endif %}
{% endblock %}

View File

@@ -15,9 +15,9 @@ services:
# 1. ОБРАЗ # 1. ОБРАЗ
# В продакшене мы используем готовый, собранный образ из реестра (Gitea) # В продакшене мы используем готовый, собранный образ из реестра (Gitea)
# image: git.cube2.ru/e-serg/dicquo:latest image: git.cube2.ru/erjemin/2020-dq:latest
# Но пока, для первого деплоя или если реестра нет, можно собирать локально: # Если образа в gitae нет, то перенести весь код в прод и можно собирать локально:
build: . # build: .
restart: always restart: always
@@ -50,7 +50,8 @@ services:
# 4. Проброс портов (Внешний Nginx -> localhost:8010) # 4. Проброс портов (Внешний Nginx -> localhost:8010)
ports: ports:
- "8010:8000" # Слушаем только на localhost хоста, чтобы закрыть прямой доступ из интернета к Gunicorn
- "127.0.0.1:8010:8000"
# 5. Тома (Volumes) # 5. Тома (Volumes)
volumes: volumes:
@@ -106,7 +107,7 @@ services:
- WATCHTOWER_SCOPE=dq-scope - WATCHTOWER_SCOPE=dq-scope
- WATCHTOWER_CLEANUP=true # Удалять старые образы после обновления - WATCHTOWER_CLEANUP=true # Удалять старые образы после обновления
- WATCHTOWER_POLL_INTERVAL=1800 # Проверять каждые 30 минут - WATCHTOWER_POLL_INTERVAL=1800 # Проверять каждые 30 минут
command: --scope dq-scope - DOCKER_API_VERSION=1.44
logging: logging:
driver: "json-file" driver: "json-file"
options: options:

View File

@@ -1,143 +1,11 @@
@charset "utf-8"; @charset "utf-8";
body {
.tags{ margin: 0;
color: silver; min-height: 100vh;
font-size:1.5vh; min-width: 100vw;
line-height:1.9vh; background-color: #111; /* Изначально темный фон */
padding-top: 7vh; opacity: 0; /* Скрываем контент до расчета цвета */
} transition: opacity 0.9s ease-in-out; /* Очень плавное появление */
/*****************************************************************
* Настройки для анимирования цвета ссылок:
* рецепт взят из: https://habr.com/ru/company/ruvds/blog/491702/
*****************************************************************/
.tags a {
text-decoration: none;
position: relative;
padding: 0 0.5ex;
display: inline-block;
color: white;
border-bottom: dotted 1px silver;
/* градиент для цвета ссылки */
background: linear-gradient(to right, rgba(255,255,255,0.9) 40%, slategray, silver, lightyellow 50%, rgba(255,255,255,0.4));
/* обрезка градиента */
background-clip: initial;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-size: 250% 100%;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)";
background-position: 100%;
/* плавное позиионирование градиента */
transition: background-position 0.65s ease;
margin-right: 2vh;
}
.tags a:hover {
color: white;
background-position: 0 100%;
border-bottom: solid 1px white;
}
div[name="cookies_accept"] {
font-family:'Roboto', 'Lucida Grande', Verdana, Arial, sans-serif;
position:fixed;
bottom: 0; left: 0;
width: 100%;
padding: 2vh 2vw;
color: black;
background-color: gray;
text-align: center;
}
div[name="cookies_accept"] button {
padding:0.5vh 0.5vw;
background: silver;
color: black;
margin-left: 2vw;
cursor: pointer;
}
#logo {
margin-top:1vh;
filter: alpha(Opacity=75); /* Полупрозрачность для IE */
opacity: 0.75;
float: left;
}
#logo a {
border: none;
text-decoration: none;
}
table { width: 80%; }
#menu {
display: none;
color: silver;
}
#mm {
text-decoration: none;
color: silver;
}
#image {
width: 30vw;
text-align: center;
vertical-align: center;
}
#image > center > div {
width: 22vw;
height: 22vw;
padding:0.5vw;
border-radius: 50%;
}
#image > center > div > div {
border-radius:50%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
#image > center > div > div > img {
width: auto;
height: 22vw;
}
#author {
color: silver;
font-size: 3.5vh;
line-height: 4vh;
text-align: right;
padding-top: 4vh;
font-style: italic;
}
#info {
color: silver;
font-size: 2.5vh;
line-height: 3vh;
padding-bottom: 2vh;
}
#bb {
color: whitesmoke;
font-size: 4.5vh;
line-height: 5vh
}
#next { float: right; }
#next a { border-bottom: none; }
/* --- NEW STYLES for FLEXBOX LAYOUT --- */
.container {
width: 90%;
max-width: 1200px;
margin: 0 auto;
} }
/* Header */ /* Header */
@@ -145,46 +13,101 @@ header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 1vh 0; padding: 1vh 4vw;
} }
/* Main Content Area */ header > #logo {
.main-content { margin-top: 1vh;
display: flex; float: left;
flex-direction: column;
justify-content: center;
min-height: 80vh;
} }
.content-row { header > #logo a {
display: flex; border: none;
align-items: center; text-decoration: none;
justify-content: center;
gap: 2vw;
} }
.text-col { header > #logo a > img {
flex: 1; width:50px;
height:46px;
} }
.image-col { header > nav {
flex: 0 0 30vw; border: #555555;
display: flex; min-height: 50px;
justify-content: center;
} }
/* --- Icons for Header Stats (SVG in Base64) --- */ header > nav > a { /* бургер */
.stats-icon { color: silver;
text-decoration: none;
font-size: 1.2em;
padding: 0 0.5em;
margin-right: -0.5em;
border: solid 1px transparent;
transition: border-color 0.8s ease, color 0.8s ease;
vertical-align: top;
}
header > nav > a:hover {
color: white;
border: solid 1px silver;
transition: border-color 0.8s ease, color 0.8s ease;
}
header > nav > #stats-menu {
display: none;
color: silver;
font-size: 0.9em;
margin-right: 15px;
text-align: right;
vertical-align: top;
}
header > nav > #stats-menu > b {
font-weight: normal;
margin: 0 1ex;
}
header > nav > #stats-menu > p {
font-style: italic; display: inline-block;
margin: 0 1vw;
padding-right: 1vw;
border-right: 1px dotted silver;
}
header > nav > #stats-menu > i.stats-icon {
display: inline-block; display: inline-block;
width: 0.9em; width: 0.9em;
height: 0.9em; height: 0.9em;
vertical-align: middle; vertical-align: middle;
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
margin-right: 0.2em; margin-right: .2em;
opacity: 0.7; /* Slight transparency for subtle look */ opacity: 0.7; /* Slight transparency for subtle look */
} }
header > nav > #stats-menu > i.stats-icon.icon-views {
margin-left: .2em;
}
header > nav > #stats-menu > a {
color: silver;
text-decoration: none;
border: solid 1px gray;
border-radius: 2em;
padding: 1.5px 0.2em 0 0.2em;
margin-left: 1em;
transition: background-color 0.3s ease, color 0.3s ease;
}
header > nav > #stats-menu > a:hover {
background-color: tan;
color: black;
border: solid 1px white;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* --- Icons for Header Stats (SVG in Base64) --- */
/* Clock Icon (Time) */ /* Clock Icon (Time) */
.icon-time { .icon-time {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke='silver' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke='silver' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");
@@ -195,30 +118,194 @@ header {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke='silver' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z'%3E%3C/path%3E%3Ccircle cx='12' cy='12' r='3'%3E%3C/circle%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke='silver' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z'%3E%3C/path%3E%3Ccircle cx='12' cy='12' r='3'%3E%3C/circle%3E%3C/svg%3E");
} }
/* Responsive: on mobile stack columns */ /* MAIN ARTICLE CONTENT */
main {
/*justify-content: space-between;*/
/*align-items: center;*/
padding: 1vh 8vw;
display: flex;
flex-direction: column;
justify-content: center;
min-height: 60vh;
/*width: 90%;*/
/*max-width: 1200px;*/
/*margin: 0 auto;*/
}
main > article {
display: flex;
align-items: center;
justify-content: center;
gap: 2vw;
}
main > article > figure {
flex: 1;
}
main > article > figure > p { /* Интро/Вступление */
color: silver;
font-size: 3vmin;
line-height: 3.5vmin;
padding-bottom: 2vmin;
font-style: italic;
}
main > article > figure > blockquote { /* Цитата */
color: whitesmoke;
font-size: 4.5vmin;
line-height: 5vmin;
border:none;
margin:0;
padding:0;
}
main > article > figure > cite { /* Автор цитаты */
color: silver;
font-size: 3.5vmin;
line-height: 4vmin;
text-align: right;
padding-top: 4vmin;
font-style: italic;
}
main > article > div {
flex: 0 0 30vw;
display: flex;
justify-content: center;
width: 30vw;
text-align: right;
margin-bottom: 10vh;
}
main > article > div > div {
width: 26vmax;
height: 26vmax;
padding: 0.5vw;
border-radius: 50%;
}
main > article > div > div > div {
border-radius: 50%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
}
main > article > div > div > div > img {
width: auto;
height: 26vmax;
}
/* НАВИГАЦИЯ (ТЕГИ И ДАЛЕЕ) В КОНЦЕ */
nav {
padding: 1vh 4vw;
}
nav > div {
color: silver;
font-size: 1.5vmin;
line-height: 1.9vmin;
padding-top: 7vh;
}
nav > div a {
text-decoration: none;
position: relative;
padding: 0 0.5ex;
display: inline-block;
color: white;
border-bottom: dotted 1px silver;
/* градиент для цвета ссылки */
background: linear-gradient(to right, rgba(255, 255, 255, 0.9) 40%, slategray, silver, lightyellow 50%, rgba(255, 255, 255, 0.4));
/* обрезка градиента */
background-clip: initial;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-size: 250% 100%;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)";
background-position: 100%;
/* плавное позиионирование градиента */
transition: background-position 0.65s ease;
margin-right: 2vmin;
}
nav > div a:hover {
color: white;
background-position: 0 100%;
border-bottom: solid 1px white;
}
nav > div > div {
float: right;
}
nav > div > div a {
border-bottom: none;
}
/* --- ПОДВАЛ-КУКИ (ДЗЕН-СТИЛЬ) --- */
footer {
font-family: 'Roboto', sans-serif;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
padding: 2vh 4vw;
color: silver; /* Мягкий серый цвет текста */
background-color: rgba(30, 30, 30, 0.8); /* Темный полупрозрачный фон */
backdrop-filter: blur(5px); /* Эффект матового стекла (современно и медитативно) */
text-align: center;
border-top: 1px solid #444; /* Тонкая грань */
font-size: 0.9em;
z-index: 1000; /* Чтобы точно было поверх всего */
}
footer small {
display: inline-block;
margin-right: 2vw;
letter-spacing: 0.05em; /* Немного воздуха в тексте */
}
footer button {
padding: 0.5vh 1.5vw;
background: transparent;
color: silver;
border: 1px solid silver;
border-radius: 2em; /* Округлые, мягкие формы */
cursor: pointer;
font-family: inherit;
font-size: 0.9em;
transition: all 0.4s ease;
}
footer button:hover {
background: silver;
color: #111;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.2); /* Легкое свечение при наведении */
}
/* Отзывчивость: для мобильных устройств колонки свапаются */
@media (max-width: 768px) { @media (max-width: 768px) {
.content-row { main > article {
flex-direction: column-reverse; flex-direction: column-reverse;
} }
.image-col {
main > article > div {
flex: 0 0 auto; flex: 0 0 auto;
margin-bottom: 2vh; margin-bottom: 2vh;
} }
} }
/* --- ВЫРАВНИВАНИЕ СИМВОЛОВ ВИСЯЧЕЙ ПУНКТУАЦИИ (Hanging Punctuation) ТИПОГРАФА ETPGRF --- */ /* --- ВЫРАВНИВАНИЕ СИМВОЛОВ ВИСЯЧЕЙ ПУНКТУАЦИИ (Hanging Punctuation) ТИПОГРАФА ETPGRF --- */
/* --- ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по левому краю) --- */ /* --- В ПРОЕКТЕ ТОЛЬКО ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по левому краю) --- */
.etp-laquo { margin-left: -0.44em; } /* « */ .etp-laquo {margin-left: -0.44em;} /* « */
.etp-ldquo, .etp-bdquo { margin-left: -0.4em; } /* “ „ */ .etp-ldquo, .etp-bdquo { margin-left: -0.4em;} /* “ „ */
.etp-lsquo { margin-left: -0.22em; } /* */ .etp-lsquo {margin-left: -0.22em;} /* */
.etp-lpar, .etp-lsqb, .etp-lcub { margin-left: -0.25em; } /* ( [ { */ .etp-lpar, .etp-lsqb, .etp-lcub {margin-left: -0.25em;}/* ( [ { */
/* --- ПРАВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по правому краю) --- */ /* --- СЧЕТЧИКИ (СКРЫТЫЙ ПИКСЕЛЬ) --- */
/* Общая механика: "вырываем" символ из потока для идеального выравнивания текста */ .counter-pixel {
[class^="etp-r"], [class*=" etp-r"] { position: absolute; } border: 0;
/* Точечная настройка смещения для каждого символа */ position: absolute;
.etp-raquo { right: -0.44em; } /* » */ left: -9999px;
.etp-rdquo { right: -0.4em; } /* ” */ }
.etp-rsquo { right: -0.22em; } /* */
.etp-rpar, .etp-rsqb, .etp-rcub { right: -0.25em; } /* ) ] } */
.etp-r-dot, .etp-r-comma, .etp-r-colon { right: -0.15em; } /* . , : */

View File

@@ -1,25 +1,29 @@
// bg-generator.js: Generates a unique gradient background based on text content // bg-generator.js:
// - Генерирует уникальный градиентный фон на основе текстового содержимого
// - Реализует плавное появление и исчезновение при навигации
// - Авто-редирект через 15 секунд для создания "медитативного" слайд-шоу эффекта
// - Обрабатывает принятие Cookie
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
// 1. Get the text to hash (from hidden span in base.html) // 1. Получаем текст для хеширования (из скрытого span в base.html)
const rawSpan = document.getElementById('dq-content-raw'); const rawSpan = document.getElementById('dq-content-raw');
let text = rawSpan ? rawSpan.innerText.trim() : ""; let text = rawSpan ? rawSpan.innerText.trim() : "";
if (!text) { if (!text) {
text = "DictumAndQuotesDefault" + Math.random(); // Fallback random if no text text = "DictumAndQuotesDefault" + Math.random(); // Случайный вариант, если текста нет
} }
// 2. Hash function (DJB2) // 2. Хеш-функция (DJB2)
let hash = 5381; let hash = 5381;
for (let i = 0; i < text.length; i++) { for (let i = 0; i < text.length; i++) {
// Force 32-bit integer arithmetic // Принудительная 32-битная целочисленная арифметика
hash = ((hash << 5) + hash) + text.charCodeAt(i); hash = ((hash << 5) + hash) + text.charCodeAt(i);
hash = hash & hash; // Convert to 32bit integer hash = hash & hash; // Преобразование в 32-битное целое
} }
// 3. Generate 6 color components deterministically from the hash // 3. Детерминированная генерация 6 цветовых компонентов из хеша
// We need 6 numbers between 0 and 255. // Нам нужно 6 чисел от 0 до 255.
// Let's use pseudo-random generator seeded by hash // Используем генератор псевдослучайных чисел с затравкой из хеша
function Mulberry32(a) { function Mulberry32(a) {
return function() { return function() {
@@ -30,53 +34,53 @@ document.addEventListener("DOMContentLoaded", function() {
} }
} }
const rand = Mulberry32(hash); // Seeded random generator const rand = Mulberry32(hash); // Генератор случайных чисел с seed
// Generate 6 color components with darker range for "meditative" feel // Генерация 6 цветовых компонентов в темном диапазоне для "медитативного" ощущения
let colors = []; let colors = [];
for(let i=0; i<6; i++) { for(let i=0; i<6; i++) {
// Generate number between 10 and 80 (dark colors) // Генерируем число от 10 до 80 (темные цвета)
colors.push(Math.floor(rand() * 70) + 10); colors.push(Math.floor(rand() * 70) + 10);
} }
// Shuffle slightly based on random to allow variation on refresh (optional) // Немного перетасовываем на основе случайности, чтобы позволить вариации при обновлении (опционально)
// colors.sort(() => Math.random() - 0.5); colors.sort(() => Math.random() - 0.5);
const rgb1 = `rgb(${colors[0]}, ${colors[1]}, ${colors[2]})`; const rgb1 = `rgb(${colors[0]}, ${colors[1]}, ${colors[2]})`;
const rgb2 = `rgb(${colors[3]}, ${colors[4]}, ${colors[5]})`; const rgb2 = `rgb(${colors[3]}, ${colors[4]}, ${colors[5]})`;
console.log("DQ BG Generator:", text.substring(0, 20) + "...", hash, rgb1, rgb2); console.log("DQ BG Generator:", text.substring(0, 20) + "...", hash, rgb1, rgb2);
// 4. Apply to body // 4. Применяем к body.
// Using linear-gradient to right with standard syntax // Используем линейный градиент вправо со стандартным синтаксисом
const bgString = `linear-gradient(90deg, ${rgb1} 0%, ${rgb2} 100%)`; const bgString = `linear-gradient(90deg, ${rgb1} 0%, ${rgb2} 100%)`;
document.body.style.background = bgString; document.body.style.background = bgString;
// 5. Apply to image background container (if exists on index page) // 5. Применяем к контейнеру фона изображения (если он есть на главной странице)
const imgBgContainer = document.querySelector('.image-col center > div'); const imgBgContainer = document.querySelector('.image-col center > div');
if (imgBgContainer) { if (imgBgContainer) {
// Use the first color of the gradient with opacity 0.7 // Используем первый цвет градиента с прозрачностью 0.7
imgBgContainer.style.background = `rgba(${colors[0]}, ${colors[1]}, ${colors[2]}, 0.7)`; imgBgContainer.style.background = `rgba(${colors[0]}, ${colors[1]}, ${colors[2]}, 0.7)`;
} }
// 6. Reveal content (Fade In effect) // 6. Показываем контент (эффект плавного появления - Fade In)
setTimeout(() => { setTimeout(() => {
document.body.style.opacity = 1; document.body.style.opacity = 1;
}, 50); }, 50);
// 7. Handle Fade Out on link clicks // 7. Обработка плавного исчезновения (Fade Out) при клике по ссылкам
document.body.addEventListener('click', function(e) { document.body.addEventListener('click', function(e) {
// Find if a link was clicked (bubble up) // Ищем, была ли нажата ссылка (всплытие)
const link = e.target.closest('a'); const link = e.target.closest('a');
if (link && link.href && link.target !== '_blank') { if (link && link.href && link.target !== '_blank') {
const hrefAttr = link.getAttribute('href'); const hrefAttr = link.getAttribute('href');
if (hrefAttr && !hrefAttr.startsWith('#') && !link.href.includes('javascript:')) { if (hrefAttr && !hrefAttr.startsWith('#') && !link.href.includes('javascript:')) {
// Check if it is an internal link (same domain) // Проверяем, является ли ссылка внутренней (тот же домен)
if (new URL(link.href).origin === window.location.origin) { if (new URL(link.href).origin === window.location.origin) {
e.preventDefault(); // Stop immediate navigation e.preventDefault(); // Останавливаем немедленный переход
document.body.style.opacity = 0; // Start Fade Out document.body.style.opacity = 0; // Запускаем Fade Out
// Wait for transition (matches CSS transition time 1.5s) // Ждем завершения перехода (соответствует времени CSS transition 0.9s (было 1.5s))
setTimeout(() => { setTimeout(() => {
window.location.href = link.href; window.location.href = link.href;
}, 900); }, 900);
@@ -85,14 +89,28 @@ document.addEventListener("DOMContentLoaded", function() {
} }
}); });
// 8. Auto-redirect ("meditative" slideshow) // 8. Авто-редирект ("медитативное" слайд-шоу)
// Find the NEXT link and simulate a click on it after 15 seconds // Ищем ссылку "ДАЛЕЕ" и симулируем клик по ней через 15 секунд
const nextLink = document.querySelector('#next a'); const nextLink = document.querySelector('#next a');
if (nextLink) { if (nextLink) {
setTimeout(() => { setTimeout(() => {
// Trigger the click event on the link so our handler above (step 7) catches it // Вызываем событие клика по ссылке, чтобы наш обработчик выше (шаг 7) поймал его
// and performs the smooth fade out animation. // и выполнил анимацию плавного исчезновения.
nextLink.click(); nextLink.click();
}, 15000); }, 15000);
} }
// 9. Логика принятия Cookie
const cookieBanner = document.querySelector('footer');
if (cookieBanner) {
const acceptButton = cookieBanner.querySelector('button');
if (acceptButton) {
acceptButton.addEventListener('click', function() {
const date = new Date();
date.setTime(date.getTime() + (92 * 24 * 60 * 60 * 1000)); // ~3 месяца (7948800000ms)
document.cookie = "cookie_accept=1; expires=" + date.toUTCString() + "; path=/; SameSite=Lax";
cookieBanner.remove();
});
}
}
}); });

View File

@@ -0,0 +1,22 @@
// Rating Mail.ru counter
var _tmr = window._tmr || (window._tmr = []);
_tmr.push({id: "3744288", type: "pageView", start: (new Date()).getTime()});
(function (d, w, id) {
if (d.getElementById(id)) return;
var ts = d.createElement("script");
ts.type = "text/javascript";
ts.async = true;
ts.id = id;
ts.src = "https://top-fwz1.mail.ru/js/code.js";
var f = function () {
var s = d.getElementsByTagName("script")[0];
s.parentNode.insertBefore(ts, s);
};
if (w.opera == "[object Opera]") {
d.addEventListener("DOMContentLoaded", f, false);
} else {
f();
}
})(document, window, "tmr-code");
// //Rating Mail.ru counter