23 Commits

Author SHA1 Message Date
Telegram Bot 5f99a0460c infra: deploy script + webhook server + S3 uploader (saguaro test) 2026-04-29 01:28:59 +03:00
Telegram Bot d857400988 Sveltia: S3 string widget + disqus field + archetypes templates 2026-04-29 00:59:54 +03:00
Telegram Bot 6731496f7a Sveltia: image widget + notes collection + field hints 2026-04-29 00:56:30 +03:00
PTP Admin 8eafab3f03 feat: Sveltia → Gitea backend (local, pkce auth) 2026-04-29 00:36:54 +03:00
PTP Admin b1202bc9af fix: CMS branch → mirror (не прод) 2026-04-29 00:04:00 +03:00
PTP Admin d4baad799e feat: Sveltia CMS admin panel (test-repo backend)
- static/admin/index.html — Sveltia CMS from CDN
- static/admin/config.yml — posts collection, TOML frontmatter
- branch cms only, не мержить в main/mirror
2026-04-29 00:02:28 +03:00
Telegram Bot 514f188f4a Посты: две колонки + единый формат фото 16:9 2026-04-16 01:33:13 +03:00
Telegram Bot edaa4a4b32 Посты одной колонкой + glassmorphism карточки поездок 2026-04-16 01:26:33 +03:00
Telegram Bot 350d0dbf64 Удален пост: Rrr 2026-04-16 01:16:41 +03:00
Telegram Bot 7f3fb5a84d Добавлен новый пост: Rrr
🤖 Создано через Telegram бота
2026-04-16 01:15:37 +03:00
Telegram Bot 066e324710 Cleanup smoke test data 2026-04-16 00:35:35 +03:00
Telegram Bot 406378f7d1 Smoke test 20260416-000049 2026-04-16 00:00:51 +03:00
Telegram Bot 88803ec438 Smoke test 20260415-235703 2026-04-15 23:57:05 +03:00
Telegram Bot 28aa4814dc Smoke test cleanup 2026-04-15 23:56:58 +03:00
Telegram Bot 5ff7d77a90 Smoke test 20260415-235416 2026-04-15 23:54:18 +03:00
Telegram Bot 4cc569d048 Добавлена новая поездка через бот (2026-04-15 23:10) 2026-04-15 23:10:24 +03:00
Telegram Bot 810f469879 Удалена поездка через бот (2026-04-15 22:59) 2026-04-15 22:59:05 +03:00
Telegram Bot eb32de261f Удален пост: Тест 2026-04-15 22:58:38 +03:00
Telegram Bot cee9453757 Добавлен новый пост: Тест
🤖 Создано через Telegram бота
2026-04-15 22:56:58 +03:00
Telegram Bot fa0296a65c Удалено фото из галереи: Gallery-20260415-1.jpg 2026-04-15 22:53:38 +03:00
Telegram Bot 26de2d6fcf Добавлено фото в галлерею 2026-04-15 22:53:07 +03:00
Telegram Bot 2842bffeaa Удалена поездка через бот (2026-04-15 22:52) 2026-04-15 22:52:46 +03:00
Telegram Bot 1f6d485f32 Добавлена новая поездка через бот (2026-04-15 22:51) 2026-04-15 22:52:00 +03:00
15 changed files with 696 additions and 166 deletions
+6
View File
@@ -0,0 +1,6 @@
+++
title = '{{ replace .Name "-" " " | title }}'
slug = '{{ .Name }}'
date = "{{ .Date | dateFormat "2006-01-02T00:00:00" }}"
description = ''
+++
+17
View File
@@ -0,0 +1,17 @@
+++
title = '{{ replace .Name "-" " " | title }}'
slug = '{{ .Name }}'
image = "https://s3.regru.cloud/sleeptrip-dev/images/{{ .Name }}_1.jpg"
date = "{{ .Date | dateFormat "2006-01-02T00:00:00" }}"
description = ''
disqus_identifier = 0
+++
Текст поездки.
![{{ .Name }}](https://s3.regru.cloud/sleeptrip-dev/images/{{ .Name }}_2.jpg)
![{{ .Name }}](https://s3.regru.cloud/sleeptrip-dev/images/{{ .Name }}_3.jpg)
{{< rawhtml >}}
{{< back-to-top >}}
{{< /rawhtml >}}
+1
View File
@@ -258,3 +258,4 @@ disableComments = true
</details> </details>
<p class="legal-note"><sup class="fn">2</sup> Отметка подтверждает, что участнику поездки исполнилось 18 лет.</p> <p class="legal-note"><sup class="fn">2</sup> Отметка подтверждает, что участнику поездки исполнилось 18 лет.</p>
{{< /rawhtml >}} {{< /rawhtml >}}
@@ -0,0 +1,30 @@
+++
title = 'Дома и стены помогают'
slug = 'doma-i-steny-pomogayut-20260213'
date = "2026-02-13T00:01:00"
description = 'открытка на снегу'
image = 'https://s3.regru.cloud/sleeptrip-dev/images/Serdtse-20260401-1.jpg'
+++
На мой взгляд, связь с родными местами, связь с близкими людьми является залогом счастья и умиротворения, даже если судьба (или тяга к путешествиям) забросит вас далеко от дома.
Я посвящаю эту открытку своей семье, они всегда ждут меня из поездок
## Фотографии
![Фото](https://s3.regru.cloud/sleeptrip-dev/images/Serdtse-20260401-2.jpg)
![Фото](https://s3.regru.cloud/sleeptrip-dev/images/Serdtse-20260401-3.jpg)
![Фото](https://s3.regru.cloud/sleeptrip-dev/images/Serdtse-20260401-4.jpg)
## Видео
{{< youtube f-Nsf5mZHi4 >}}
{{< rawhtml >}}
{{< back-to-top >}}
{{< /rawhtml >}}
@@ -0,0 +1,42 @@
+++
title = 'Дронослёт в феврале'
slug = 'dronoslyot-v-fevrale-20260228'
date = "2026-02-28T00:01:00"
description = 'у заброшенной церкви'
image = 'https://s3.regru.cloud/sleeptrip-dev/images/Savelevo-20260402-1.jpg'
+++
Закрытый дронослёт в феврале, до вступления в силу в марте 2026 новых требований по оборудованию дронов.
Базировались мы не в самом красивом месте, потому что подъехать прямо к церкви было невозможно из-за завалов снега.
Плюс рядом постоянно выгружали снег и буксовали грузовики.
А ещё на улице шёл дождь.
Но никакие помехи не помехи, если очень хочется летать.
Видео: https://youtu.be/WpTUA7keBqw
## Фотографии
![Фото](https://s3.regru.cloud/sleeptrip-dev/images/Savelevo-20260402-2.jpg)
![Фото](https://s3.regru.cloud/sleeptrip-dev/images/Savelevo-20260402-3.jpg)
![Фото](https://s3.regru.cloud/sleeptrip-dev/images/Savelevo-20260402-4.jpg)
![Фото](https://s3.regru.cloud/sleeptrip-dev/images/Savelevo-20260402-5.jpg)
![Фото](https://s3.regru.cloud/sleeptrip-dev/images/Savelevo-20260402-6.jpg)
![Фото](https://s3.regru.cloud/sleeptrip-dev/images/Savelevo-20260402-7.jpg)
![Фото](https://s3.regru.cloud/sleeptrip-dev/images/Savelevo-20260402-8.jpg)
## Видео
{{< youtube WpTUA7keBqw >}}
{{< rawhtml >}}
{{< back-to-top >}}
{{< /rawhtml >}}
+31
View File
@@ -0,0 +1,31 @@
# PTP Infrastructure Scripts
## deploy-ptp.sh
Deploy script: git pull mirror → hugo build → /var/www/html
Triggered by Gitea webhook on push to mirror/main.
Service: ptp-webhook (port 9000)
## webhook-server.py
HTTP server на порту 9000.
Принимает POST /deploy от Gitea (HMAC-SHA256 подпись).
Secret: ptp-deploy-2026
## uploader.py
Flask-приложение на порту 9001.
Drag-and-drop загрузка фото → S3 reg.ru (sleeptrip-dev/images/).
Защищено Basic Auth через nginx.
Поддерживает: jpg, png, gif, webp, heic, mp4, mov.
HEIC автоматически конвертируется в JPG.
## Systemd services
- ptp-webhook.service → webhook-server.py
- ptp-uploader.service → uploader.py
## Nginx snippets
- /etc/nginx/snippets/ptp-webhook.conf
- /etc/nginx/snippets/ptp-uploader.conf
## Gitea webhook
Repo: ptpadmin/ptp, hook id: 1
URL: http://127.0.0.1:9000/deploy
Branch filter: mirror
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
set -e
LOG=/var/log/ptp-deploy.log
echo "[2026-04-29 00:05:28] Deploy started" >> $LOG
cd /var/www/hugo-source
git checkout mirror >> $LOG 2>&1
git pull gitea mirror >> $LOG 2>&1
hugo -d /var/www/html --baseURL 'https://ptp.saguaro-cactus.ru' >> $LOG 2>&1
echo "[2026-04-29 00:05:28] Deploy done" >> $LOG
+228
View File
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
import os, tempfile
from flask import Flask, request, jsonify
import boto3
from botocore.client import Config
from botocore.exceptions import ClientError
import pillow_heif
from PIL import Image
pillow_heif.register_heif_opener()
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB
S3_CONFIG = dict(
endpoint_url='https://s3.regru.cloud',
aws_access_key_id='TXBN3MRO1YCC2Y593URD',
aws_secret_access_key='GIMA9lJetvXgMtl8GCO4dNSpJDGHvKgGBVfiA9zV',
region_name='ru-1',
config=Config(signature_version='s3v4')
)
BUCKET = 'sleeptrip-dev'
PREFIX = 'images/'
ALLOWED = {'jpg', 'jpeg', 'png', 'gif', 'webp', 'mp4', 'mov', 'heic', 'heif'}
MIME = {
'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png',
'gif': 'image/gif', 'webp': 'image/webp',
'mp4': 'video/mp4', 'mov': 'video/quicktime',
'heic': 'image/jpeg', 'heif': 'image/jpeg'
}
PAGE = """<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Upload to S3</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#0d1117;color:#e6edf3;font-family:-apple-system,BlinkMacSystemFont,monospace;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:24px}
.card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:32px;width:100%;max-width:520px}
h1{font-size:16px;font-weight:600;margin-bottom:4px}
.sub{font-size:12px;color:#8b949e;margin-bottom:24px}
.drop{border:2px dashed #30363d;border-radius:8px;padding:48px 24px;text-align:center;cursor:pointer;transition:all .2s}
.drop.over{border-color:#58a6ff;background:rgba(88,166,255,.05)}
.drop input{display:none}
.drop-icon{font-size:36px;margin-bottom:12px}
.drop-text{font-size:13px;color:#8b949e}
.drop-text b{color:#58a6ff;cursor:pointer}
.progress{margin-top:16px;display:none}
.bar-wrap{background:#21262d;border-radius:4px;height:6px;margin-top:8px}
.bar{background:#58a6ff;height:6px;border-radius:4px;width:0;transition:width .15s}
.status{font-size:12px;color:#8b949e;margin-top:6px}
.result{margin-top:20px;display:none}
.url-box{background:#0d1117;border:1px solid #3fb950;border-radius:6px;padding:12px 14px;font-size:12px;font-family:monospace;word-break:break-all;color:#3fb950;cursor:pointer}
.url-box:hover{background:rgba(63,185,80,.05)}
.copy-hint{font-size:11px;color:#8b949e;margin-top:6px}
.preview{margin-top:12px;max-width:100%;max-height:200px;border-radius:6px;display:none}
.err{color:#f85149;font-size:12px;margin-top:12px;display:none;padding:8px 12px;background:rgba(248,81,73,.08);border-radius:6px}
.hist{margin-top:24px;border-top:1px solid #21262d;padding-top:16px;display:none}
.hist h2{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#8b949e;margin-bottom:10px}
.hi{font-size:11px;font-family:monospace;color:#8b949e;padding:5px 0;border-bottom:1px solid #21262d;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hi:hover{color:#58a6ff}
</style>
</head>
<body>
<div class="card">
<h1>Upload &rarr; S3</h1>
<p class="sub">sleeptrip-dev &middot; s3.regru.cloud/images/</p>
<div class="drop" id="drop">
<input type="file" id="fi" accept="image/*,video/*" multiple>
<div class="drop-icon">&#8679;</div>
<div class="drop-text">Перетащи или <b id="btn">выбери файл</b></div>
</div>
<div class="progress" id="prog">
<div class="bar-wrap"><div class="bar" id="bar"></div></div>
<div class="status" id="stat">Загружаем...</div>
</div>
<div class="err" id="err"></div>
<div class="result" id="res">
<div class="url-box" id="url" onclick="copy(this)"></div>
<div class="copy-hint" id="hint">Нажми чтобы скопировать</div>
<img class="preview" id="prev">
</div>
<div class="hist" id="hist">
<h2>История</h2>
<div id="hl"></div>
</div>
</div>
<script>
var urls = [];
var drop = document.getElementById('drop');
var fi = document.getElementById('fi');
document.getElementById('btn').onclick = function(){ fi.click(); };
drop.addEventListener('dragover', function(e){ e.preventDefault(); drop.classList.add('over'); });
drop.addEventListener('dragleave', function(){ drop.classList.remove('over'); });
drop.addEventListener('drop', function(e){ e.preventDefault(); drop.classList.remove('over'); for(var i=0;i<e.dataTransfer.files.length;i++) doUpload(e.dataTransfer.files[i]); });
fi.addEventListener('change', function(){ for(var i=0;i<fi.files.length;i++) doUpload(fi.files[i]); });
function doUpload(file) {
var prog = document.getElementById('prog');
var res = document.getElementById('res');
var err = document.getElementById('err');
prog.style.display = 'block';
res.style.display = 'none';
err.style.display = 'none';
document.getElementById('bar').style.width = '0';
document.getElementById('stat').textContent = file.name + ' ...';
var fd = new FormData();
fd.append('file', file);
var xhr = new XMLHttpRequest();
xhr.upload.onprogress = function(e){ if(e.lengthComputable) document.getElementById('bar').style.width = (e.loaded/e.total*100)+'%'; };
xhr.onload = function(){
prog.style.display = 'none';
if(xhr.status === 200){
var r = JSON.parse(xhr.responseText);
showRes(r.url, file.type.indexOf('image')===0);
} else {
var msg = 'Ошибка '+xhr.status;
try{ msg = JSON.parse(xhr.responseText).error; }catch(e){}
showErr(msg);
}
};
xhr.onerror = function(){ prog.style.display='none'; showErr('Сетевая ошибка'); };
xhr.open('POST', '/upload/api');
xhr.withCredentials = true;
xhr.send(fd);
}
function showRes(url, isImg) {
var el = document.getElementById('url');
el.textContent = url;
document.getElementById('res').style.display = 'block';
document.getElementById('hint').textContent = 'Нажми чтобы скопировать';
var p = document.getElementById('prev');
if(isImg){ p.src=url; p.style.display='block'; } else { p.style.display='none'; }
urls.unshift(url);
updateHist();
}
function showErr(msg){ var e=document.getElementById('err'); e.textContent=msg; e.style.display='block'; }
function copy(el){
navigator.clipboard.writeText(el.textContent).then(function(){
document.getElementById('hint').textContent='Скопировано!';
setTimeout(function(){ document.getElementById('hint').textContent='Нажми чтобы скопировать'; }, 2000);
});
}
function updateHist(){
document.getElementById('hist').style.display='block';
var html = '';
for(var i=0;i<urls.length;i++){
html += '<div class="hi" data-url="'+urls[i]+'">'+urls[i]+'</div>';
}
document.getElementById('hl').innerHTML = html;
var items = document.getElementById('hl').querySelectorAll('.hi');
for(var j=0;j<items.length;j++){
items[j].onclick = (function(u){ return function(){ navigator.clipboard.writeText(u); this.textContent='Скопировано!'; }; })(items[j].getAttribute('data-url'));
}
}
</script>
</body>
</html>"""
@app.route('/upload/')
def index():
return PAGE
@app.route('/upload/api', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return jsonify({'error': 'Нет файла'}), 400
f = request.files['file']
if not f.filename:
return jsonify({'error': 'Пустое имя файла'}), 400
ext = f.filename.rsplit('.', 1)[-1].lower() if '.' in f.filename else ''
if ext not in ALLOWED:
return jsonify({'error': 'Формат .{} не поддерживается'.format(ext)}), 400
original_name = os.path.splitext(f.filename)[0]
# HEIC/HEIF → конвертируем в JPG
is_heic = ext in ('heic', 'heif')
out_ext = 'jpg' if is_heic else ext
s3_key = '{}{}.{}'.format(PREFIX, original_name, out_ext)
tmp_path = None
try:
with tempfile.NamedTemporaryFile(delete=False, suffix='.{}'.format(ext)) as tmp:
f.save(tmp.name)
tmp_path = tmp.name
if is_heic:
img = Image.open(tmp_path)
converted = tmp_path.replace('.{}'.format(ext), '.jpg')
img.convert('RGB').save(converted, 'JPEG', quality=92)
os.unlink(tmp_path)
tmp_path = converted
import urllib3
urllib3.disable_warnings()
s3 = boto3.client('s3', verify=False, **S3_CONFIG)
s3.upload_file(
tmp_path, BUCKET, s3_key,
ExtraArgs={
'ContentType': MIME.get(ext, 'application/octet-stream'),
'CacheControl': 'max-age=31536000'
}
)
url = 'https://s3.regru.cloud/{}/{}'.format(BUCKET, s3_key)
# hint: HEIC was converted to JPG
return jsonify({'url': url})
except Exception as e:
return jsonify({'error': str(e)}), 500
finally:
if tmp_path and os.path.exists(tmp_path):
os.unlink(tmp_path)
if __name__ == '__main__':
app.run(host='127.0.0.1', port=9001, debug=False)
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
import http.server, subprocess, hashlib, hmac, json, os, sys
SECRET = os.environ.get('WEBHOOK_SECRET', 'ptp-deploy-2026')
PORT = 9000
class Handler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
if self.path != '/deploy':
self.send_response(404); self.end_headers(); return
length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(length)
# Verify signature (Gitea sends X-Gitea-Signature)
sig = self.headers.get('X-Gitea-Signature', '')
expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
self.send_response(403); self.end_headers()
self.wfile.write(b'Bad signature'); return
try:
data = json.loads(body)
ref = data.get('ref', '')
if ref not in ('refs/heads/mirror', 'refs/heads/main'):
self.send_response(200); self.end_headers()
self.wfile.write(b'Skipped (not mirror/main)'); return
except: pass
subprocess.Popen(['/usr/local/bin/deploy-ptp.sh'])
self.send_response(200); self.end_headers()
self.wfile.write(b'Deploy triggered')
def log_message(self, fmt, *args):
pass # silent
if __name__ == '__main__':
httpd = http.server.HTTPServer(('127.0.0.1', PORT), Handler)
print(f'Webhook server on port {PORT}')
httpd.serve_forever()
+84
View File
@@ -0,0 +1,84 @@
backend:
name: gitea
repo: ptpadmin/ptp
branch: mirror
api_root: https://git.ptp.saguaro-cactus.ru/api/v1
server: https://git.ptp.saguaro-cactus.ru
auth_type: pkce
app_id: 6be1691d-ea91-448e-acfa-016f57c976d6
media_folder: static/images/uploads
public_folder: /images/uploads
locale: ru
collections:
- name: post
label: 🧳 Поездки
label_singular: Поездка
folder: content/post
create: true
format: toml-frontmatter
extension: md
summary: '{{title}} — {{date}}'
fields:
- label: Заголовок
name: title
widget: string
hint: 'Например: Коломна или Дюкинский заказник'
- label: Slug (URL)
name: slug
widget: string
hint: 'Латиница без пробелов. Пример: Kolomna или Dukyn2504'
- label: Дата
name: date
widget: datetime
format: 'YYYY-MM-DDTHH:mm:ss'
date_format: 'DD.MM.YYYY'
time_format: 'HH:mm'
default: ''
- label: Описание (анонс)
name: description
widget: string
hint: 'Короткая фраза. Пример: скалы, рассвет, однодневная поездка'
- label: Обложка (S3 URL)
name: image
widget: string
required: false
hint: 'https://s3.regru.cloud/sleeptrip-dev/images/ИМЯ_1.jpg'
pattern: ['^https?://', 'Должен быть полный URL']
- label: Disqus ID
name: disqus_identifier
widget: number
value_type: int
hint: 'Следующий свободный: 134. Каждый пост — уникальный номер.'
min: 1
- label: Контент
name: body
widget: markdown
- name: notes
label: 📝 Заметки
label_singular: Заметка
folder: content/notes
create: true
format: toml-frontmatter
extension: md
summary: '{{title}} — {{date}}'
fields:
- { label: Заголовок, name: title, widget: string }
- { label: Slug (URL), name: slug, widget: string, hint: 'Латиница без пробелов' }
- label: Дата
name: date
widget: datetime
format: 'YYYY-MM-DDTHH:mm:ss'
date_format: 'DD.MM.YYYY'
time_format: 'HH:mm'
default: ''
- { label: Контент, name: body, widget: markdown }
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html>
<head>
<meta charset=utf-8 />
<meta name=viewport content=width=device-width, initial-scale=1.0 />
<meta name=robots content=noindex />
<title>PTP Admin</title>
<script src="https://cdn.jsdelivr.net/npm/@sveltia/cms/dist/sveltia-cms.js"></script>
</head>
<body></body>
</html>
+27
View File
@@ -0,0 +1,27 @@
/* Посты на главной: две колонки, но фото крупнее и единого формата */
/* Все фото постов — единый aspect ratio 16:9 с cover */
#main > .posts > article .image.main img {
border-radius: 10px;
width: 100%;
height: auto;
aspect-ratio: 16/9;
object-fit: cover;
}
/* Featured пост сверху — чуть выделить */
article.post.featured {
border-bottom: 1px solid rgba(0,0,0,0.08);
padding-bottom: 3rem;
margin-bottom: 1rem;
}
article.post.featured .image.main img {
border-radius: 12px;
aspect-ratio: 16/9;
object-fit: cover;
}
article.post.featured header.major h2 {
font-size: 1.8em;
}
+68 -72
View File
@@ -1,6 +1,5 @@
/** /**
* Стили для календаря поездок (карточки из upcoming-trips.json) * Стили для карточек поездок glassmorphism
* Заменяет внешний Tockify виджет
*/ */
.trips-calendar { .trips-calendar {
@@ -11,24 +10,26 @@
.trips-calendar .trip-card { .trips-calendar .trip-card {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: #fff; background: rgba(255, 255, 255, 0.75);
border: 1px solid #e0e0e0; backdrop-filter: blur(16px);
border-radius: 12px; -webkit-backdrop-filter: blur(16px);
margin-bottom: 20px; border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 16px;
margin-bottom: 24px;
overflow: hidden; overflow: hidden;
transition: all 0.3s ease; transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
} }
.trips-calendar .trip-card:hover { .trips-calendar .trip-card:hover {
transform: translateY(-2px); transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); box-shadow: 0 12px 40px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.06);
} }
.trips-calendar .trip-image { .trips-calendar .trip-image {
position: relative; position: relative;
width: 100%; width: 100%;
height: 200px; height: 240px;
overflow: hidden; overflow: hidden;
} }
@@ -36,66 +37,78 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
transition: transform 0.3s ease; transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
} }
.trips-calendar .trip-card:hover .trip-image img { .trips-calendar .trip-card:hover .trip-image img {
transform: scale(1.05); transform: scale(1.06);
} }
/* Дата-оверлей — pill-стиль */
.trips-calendar .trip-overlay { .trips-calendar .trip-overlay {
position: absolute; position: absolute;
top: 16px; top: 16px;
right: 16px; right: 16px;
background: rgba(0, 0, 0, 0.8); background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
color: white; color: white;
padding: 8px 12px; padding: 8px 16px;
border-radius: 6px; border-radius: 24px;
font-size: 0.9em; font-size: 0.85em;
font-weight: 500; font-weight: 600;
letter-spacing: 0.02em;
} }
.trips-calendar .trip-content { .trips-calendar .trip-content {
padding: 20px; padding: 24px 24px 28px;
} }
.trips-calendar .trip-content h3 { .trips-calendar .trip-content h3 {
margin: 0 0 12px 0; margin: 0 0 12px 0;
font-size: 1.4em; font-size: 1.35em;
font-weight: 600; font-weight: 700;
color: #333; color: #1a202c;
line-height: 1.3; line-height: 1.35;
} }
.trips-calendar .trip-details p { .trips-calendar .trip-details p {
margin: 0 0 16px 0; margin: 0 0 20px 0;
color: #666; color: #4a5568;
line-height: 1.5; line-height: 1.6;
font-size: 0.95em; font-size: 0.95em;
} }
/* Мета-теги — accent-стиль */
.trips-calendar .trip-meta { .trips-calendar .trip-meta {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: 8px;
margin-top: 12px; margin-top: 16px;
} }
.trips-calendar .trip-meta span { .trips-calendar .trip-meta span {
background: #f0f4f8; background: linear-gradient(135deg, #667eea12, #764ba212);
color: #2d3748; color: #4a5568;
padding: 6px 12px; padding: 8px 16px;
border-radius: 20px; border-radius: 24px;
font-size: 0.85em; font-size: 0.82em;
border: 1px solid #e2e8f0; line-height: 1.4;
border: 1px solid rgba(102, 126, 234, 0.15);
transition: all 0.2s ease;
} }
/* Адаптивность */ .trips-calendar .trip-meta span:hover {
background: linear-gradient(135deg, #667eea22, #764ba222);
border-color: rgba(102, 126, 234, 0.3);
}
/* Grid на десктопе */
@media (min-width: 768px) { @media (min-width: 768px) {
.trips-calendar { .trips-calendar {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
gap: 24px; gap: 28px;
} }
.trips-calendar .trip-card { .trips-calendar .trip-card {
@@ -105,55 +118,38 @@
@media (min-width: 1024px) { @media (min-width: 1024px) {
.trips-calendar { .trips-calendar {
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
}
.trips-calendar .trip-image {
height: 280px;
} }
} }
/* Состояние загрузки */ /* Состояния */
.trips-calendar .loading { .trips-calendar .loading {
text-align: center; text-align: center;
padding: 40px 20px; padding: 48px 20px;
color: #666; color: #718096;
font-size: 1.1em; font-size: 1em;
} }
.trips-calendar .error { .trips-calendar .error {
text-align: center; text-align: center;
padding: 40px 20px; padding: 48px 20px;
color: #e53e3e; color: #c53030;
background: #fed7d7; background: rgba(254, 215, 215, 0.6);
border-radius: 8px; backdrop-filter: blur(8px);
border-radius: 12px;
margin: 20px 0; margin: 20px 0;
} }
.trips-calendar .no-trips { .trips-calendar .no-trips {
text-align: center; text-align: center;
padding: 40px 20px; padding: 48px 20px;
color: #666; color: #718096;
background: #f7fafc; background: rgba(247, 250, 252, 0.6);
border-radius: 8px; backdrop-filter: blur(8px);
border-radius: 12px;
border: 1px dashed #cbd5e0; border: 1px dashed #cbd5e0;
} }
/* Темная тема (если используется) */
@media (prefers-color-scheme: dark) {
.trips-calendar .trip-card {
background: #2d3748;
border-color: #4a5568;
color: #e2e8f0;
}
.trips-calendar .trip-content h3 {
color: #f7fafc;
}
.trips-calendar .trip-details p {
color: #a0aec0;
}
.trips-calendar .trip-meta span {
background: #4a5568;
color: #e2e8f0;
border-color: #718096;
}
}
+13 -7
View File
@@ -1,15 +1,21 @@
{ {
"trips": [ "trips": [
{ {
"id": "trip-20260322-234035", "id": "trip-20260415-231020",
"title": "Расписание ближайших дронослётов", "title": "Дронослёт в мае 2026",
"period": "После 9 апреля 2026 года", "period": "16-17 мая 2026",
"description": "В ближайшее время (март и начало апреля 2026 года) не планируется проведение открытых дронослётов (это для всех желающих по заявкам с сайта).\n\nВозможные даты проведения открытых дронослётов - не раньше 9 апреля 2026 года, следите за расписанием на сайте", "description": "Полёты выходного дня в Тверской области",
"image": "https://s3.regru.cloud/sleeptrip-dev/images/Elbrus-20230128-1.jpg", "image": "https://s3.regru.cloud/sleeptrip-dev/images/plan/trip-20260415-231010.jpg",
"meta": [], "meta": [
"Выезд из Москвы предположительно рано утром, чтобы избежать пробок.",
"Расстояние от МКАД - примерно 180 км, 2,5 часа в дороге.",
"Заявки принимаются не позднее, чем за 7 дней до даты поездки.",
"Ограничения: не более 1 дрона на 1 пилота в заявке, дрон до 5 кг с постановкой на учёт и страховкой.",
"Возможны уточнения по оснащению оборудованием, если будет получен отказ от ОрВД"
],
"active": true, "active": true,
"order": 1 "order": 1
} }
], ],
"last_updated": "2026-03-22T23:40:35.502230" "last_updated": "2026-04-15T23:10:20.404397"
} }
@@ -50,6 +50,7 @@
<link rel="stylesheet" href="/css/back-to-top.css"> <link rel="stylesheet" href="/css/back-to-top.css">
<link rel="stylesheet" href="/css/typography-improvements.css?v={{ now.Unix }}"> <link rel="stylesheet" href="/css/typography-improvements.css?v={{ now.Unix }}">
<link rel="stylesheet" href="/css/about-site.css"> <link rel="stylesheet" href="/css/about-site.css">
t <link rel="stylesheet" href="/css/posts-single-column.css?v={{ now.Unix }}">
<link rel="stylesheet" href="/css/copyright-visible.css"> <link rel="stylesheet" href="/css/copyright-visible.css">
<link rel="stylesheet" href="/css/nav-background.css"> <link rel="stylesheet" href="/css/nav-background.css">
<link rel="stylesheet" href="/css/pagination-info.css"> <link rel="stylesheet" href="/css/pagination-info.css">