Compare commits

..

No commits in common. "b2ae712e29cf04133b5e7e52295fc8ff254d8c15" and "ce5363c9a27bb5601fc14763305a262b7307ff8e" have entirely different histories.

34 changed files with 19 additions and 1342 deletions

4
.gitignore vendored
View file

@ -1,4 +0,0 @@
raw_jpchx/
*.pyc
__pycache__/
*.swp

View file

@ -22,24 +22,7 @@ services:
max-size: "10m"
max-file: "3"
# 2. JPCHX Web App & Forum (Python Flask + SQLite)
jpchx-app:
build:
context: ./site
dockerfile: Dockerfile
container_name: jpchx-app
restart: always
volumes:
- ./site/data:/app/data
networks:
- shield_net
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# 3. Nginx Decoy: Шлюз сайта Клуба ЖПЧХ + скрытый шлюз подписок
# 2. Nginx Decoy: сайт клуба любителей пельменей + скрытый шлюз подписок
nginx-decoy:
image: nginx:alpine
container_name: nginx-decoy
@ -48,21 +31,19 @@ services:
- "80:80" # HTTP для ACME certbot и редиректа на HTTPS
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./site/static:/usr/share/nginx/html/static:ro
- ./nginx/site:/usr/share/nginx/html:ro
- ./certbot/www:/var/www/certbot:ro
- ./certbot/certs:/etc/letsencrypt:ro
networks:
- shield_net
depends_on:
- 3x-ui
- jpchx-app
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# 4. Certbot: автоматический выпуск и продление SSL Let's Encrypt
# 3. Certbot: автоматический выпуск и продление SSL Let's Encrypt
certbot:
image: certbot/certbot:latest
container_name: certbot-ssl

View file

@ -1,5 +1,5 @@
# ========================================================
# JPCHX Club Shield Nginx Configuration
# Pelmeni Shield Nginx Configuration
# ========================================================
# 1. Порт 80: Let's Encrypt ACME Challenge + Редирект на HTTPS
@ -16,22 +16,23 @@ server {
}
}
# 2. Порт 8080: Внутренний HTTP Fallback для Xray
# 2. Порт 8080: Внутренний Fallback для Xray (Сеть shield_net)
server {
listen 8080 default_server;
server_name _;
client_max_body_size 16M;
location /static/ {
alias /usr/share/nginx/html/static/;
expires 7d;
add_header Cache-Control "public, no-transform";
# Корневой сайт: Клуб любителей сибирских пельменей
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location ~ ^/api/v1/recipes/secret_([a-zA-Z0-9_]+)/(.*)$ {
resolver 127.0.0.11 valid=10s;
proxy_pass http://3x-ui:2096/sub/$2;
# Секретный шлюз выдачи подписок 3X-UI
# Снаружи выглядит как вызов API рецептов:
# URL: https://DOMAIN/api/v1/recipes/secret_TOKEN/SUB_ID
location ~ ^/api/v1/recipes/secret_([a-zA-Z0-9]+)/(.*)$ {
proxy_pass http://3x-ui:2053/sub/$2;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -40,54 +41,7 @@ server {
proxy_read_timeout 15s;
}
location / {
proxy_pass http://jpchx-app:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
}
# 3. Порт 8443: Защищенный HTTPS Fallback для Reality SNI
server {
listen 8443 ssl default_server;
server_name jpchx.matrixhasyou.ru;
ssl_certificate /etc/letsencrypt/live/jpchx.matrixhasyou.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/jpchx.matrixhasyou.ru/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
client_max_body_size 16M;
location /static/ {
alias /usr/share/nginx/html/static/;
expires 7d;
add_header Cache-Control "public, no-transform";
}
location ~ ^/api/v1/recipes/secret_([a-zA-Z0-9_]+)/(.*)$ {
resolver 127.0.0.11 valid=10s;
proxy_pass http://3x-ui:2096/sub/$2;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_connect_timeout 5s;
proxy_read_timeout 15s;
}
location / {
proxy_pass http://jpchx-app:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
}
# Защита от перебора: любая ошибка выдает главную страницу пельменей
error_page 404 /index.html;
error_page 500 502 503 504 /index.html;
}

View file

@ -48,7 +48,7 @@ echo "🔐 === 3. Проверка SSL-сертификата для ${DOMAIN} =
CERT_PATH="certbot/certs/live/${DOMAIN}/fullchain.pem"
if [[ ! -f "${CERT_PATH}" ]]; then
echo "🌐 Запуск первичного выпуска SSL-сертификата Let's Encrypt..."
docker run --rm --name certbot-init \
docker run -it --rm --name certbot-init \
-p 80:80 \
-v "$(pwd)/certbot/certs:/etc/letsencrypt" \
-v "$(pwd)/certbot/www:/var/www/certbot" \

View file

@ -1,17 +0,0 @@
FROM python:3.12-alpine
WORKDIR /app
RUN apk add --no-cache sqlite
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
ENV PYTHONUNBUFFERED=1
ENV FLASK_ENV=production
EXPOSE 8080
CMD ["python", "app.py"]

View file

@ -1,484 +0,0 @@
import os
import sqlite3
import hashlib
from datetime import datetime
from functools import wraps
from flask import Flask, render_template, request, redirect, url_for, session, flash, g
app = Flask(__name__)
app.secret_key = os.environ.get("SECRET_KEY", "jpchx_volga_secret_key_2012_matrixhasyou")
DATA_DIR = os.environ.get("DATA_DIR", os.path.join(os.path.dirname(__file__), "data"))
os.makedirs(DATA_DIR, exist_ok=True)
DB_PATH = os.path.join(DATA_DIR, "jpchx.db")
def get_db():
db = getattr(g, "_database", None)
if db is None:
db = g._database = sqlite3.connect(DB_PATH)
db.row_factory = sqlite3.Row
return db
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, "_database", None)
if db is not None:
db.close()
def hash_pw(password: str) -> str:
return hashlib.sha256(password.encode("utf-8")).hexdigest()
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if "user_id" not in session:
flash("Для выполнения этого действия необходимо войти в систему или зарегистрироваться.", "warning")
return redirect(url_for("login", next=request.url))
return f(*args, **kwargs)
return decorated_function
def init_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
role TEXT DEFAULT 'Участник',
avatar_text TEXT DEFAULT '📷',
created_at TEXT NOT NULL
);
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL
);
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS topics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL,
title TEXT NOT NULL,
author_id INTEGER NOT NULL,
views INTEGER DEFAULT 0,
created_at TEXT NOT NULL,
FOREIGN KEY (category_id) REFERENCES categories (id),
FOREIGN KEY (author_id) REFERENCES users (id)
);
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
topic_id INTEGER NOT NULL,
author_id INTEGER NOT NULL,
content TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (topic_id) REFERENCES topics (id),
FOREIGN KEY (author_id) REFERENCES users (id)
);
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS gallery (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
title TEXT NOT NULL,
location TEXT NOT NULL,
year TEXT NOT NULL,
description TEXT NOT NULL
);
""")
# Check if empty, populate initial content
cur.execute("SELECT count(*) FROM users;")
if cur.fetchone()[0] == 0:
print("Populating initial JPCHX club database...")
now = "2012-08-11 14:20:00"
# 1. Users
users_data = [
("Admin", hash_pw("admin_jpchx_2012"), "Главный Хранитель ЖПЧХ", "🎖️", "2012-01-10 12:00:00"),
("MatrixHasYou", hash_pw("matrix_volga_2012"), "Основатель / Мастер кадра", "👑", "2012-02-01 10:00:00"),
("PitKa", hash_pw("pitka_master_2012"), "Основатель / Теоретик контраста", "🎩", "2012-02-01 10:15:00"),
("LensMaster", hash_pw("lens_pass_2014"), "Оптик-испытатель", "🔍", "2013-05-12 16:30:00"),
("VolgaPhotographer", hash_pw("volga_pass_2014"), "Хроникёр Поволжья", "🌊", "2013-07-20 18:45:00"),
("BokehLover", hash_pw("bokeh_pass_2015"), "Любитель светосилы", "✨", "2014-03-15 11:20:00"),
("StreetHunter73", hash_pw("street_pass_2015"), "Уличный снайпер 73", "🎯", "2014-09-01 09:10:00"),
("WideAngleGuy", hash_pw("wide_pass_2016"), "Широкоугольный мастер", "📐", "2015-04-18 14:05:00"),
("WeddingCrasher", hash_pw("wedding_pass_2016"), "Банкетный летописец", "🍾", "2015-08-22 17:50:00"),
("ShutterBug", hash_pw("shutter_pass_2017"), "Плёночный энтузиаст", "🎞️", "2016-11-05 13:40:00"),
]
cur.executemany("INSERT INTO users (username, password_hash, role, avatar_text, created_at) VALUES (?, ?, ?, ?, ?)", users_data)
# 2. Categories
cats_data = [
(1, "Теория и философия ЖПЧХ", "Основы композиции, контраст планов, смысл жеста и манифест движения"),
(2, "Техника и оптика", "Камеры, объективы, глубина резкости, выдержка и угол захвата"),
(3, "Полевые отчеты и истории", "Хроники внедрения на свадьбах, выпускных, банкетах и пленэрах"),
(4, "Клубная жизнь и мастерская", "Членство, дипломы, этика участника и организация съемок"),
]
cur.executemany("INSERT INTO categories (id, title, description) VALUES (?, ?, ?)", cats_data)
# 3. Topics & Posts
# Topic 1
cur.execute("INSERT INTO topics (category_id, title, author_id, views, created_at) VALUES (?, ?, ?, ?, ?)",
(1, "Основы композиции ЖПЧХ: контраст первого плана против пафоса фона", 2, 428, "2013-04-15 10:20:00"))
t1_id = cur.lastrowid
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t1_id, 2, """Коллеги и единомышленники!
Часто новички спрашивают: «В чём глубинная суть ЖПЧХ? Это просто шутка или фото-арт?».
Отвечаю со всей основательностью. ЖПЧХ — это в первую очередь художественный приём абсолютного контраста.
На заднем плане кадра разворачивается нечто чрезмерно торжественное: чинные лица гостей в строгих костюмах, пафосный тост на банкете, церемония вручения дипломов у Мемцентра или свадебная фотосессия на волжском косогоре. Все напряжены, неестественны, играют социальные роли.
И тут на первом плане — в расфокусе или чётком контуре спиной к камере — появляется рука, уверенно и честно почёсывающая задницу.
Этот жест мгновенно приземляет напыщенность, возвращает кадру витальность и заставляет зрителя улыбнуться искренней правде жизни. Главное правило: почес должен быть уверенным, хватким и органичным. Не имитация, а честный физиологический импульс!""", "2013-04-15 10:20:00"))
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t1_id, 3, """Поддерживаю каждое слово MatrixHasYou!
От себя добавлю как сооснователь: важен тайминг. Нельзя чесаться непрерывно, как шелудивый пёс. Истинный мастер ЖПЧХ ждёт апогея торжества на заднем плане — момент поцелуя молодых, звон бокалов или речь ректора. Именно в эту секунду производится вход в кадр и фиксация.""", "2013-04-15 11:45:00"))
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t1_id, 4, """Великолепный манифест! Пробовал снимать на 50mm f/1.4 с открытой диафрагмой. Рука на первом плане слегка в боке, а на заднем плане чётко видны изумлённые лица свидетелей. Эффект потрясающий!""", "2013-04-15 14:10:00"))
# Topic 2
cur.execute("INSERT INTO topics (category_id, title, author_id, views, created_at) VALUES (?, ?, ?, ?, ?)",
(2, "Выбор оптики: сверхширокий угол 16-35mm против классического портретника 50mm", 3, 312, "2013-06-02 12:00:00"))
t2_id = cur.lastrowid
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t2_id, 3, """Провёл полевые испытания на двух объективах: Nikkor 16-35mm f/4 и 50mm f/1.8.
На 16mm масштаб задницы на переднем плане получается монументальным — она буквально доминирует над архитектурой города и Волгой. Но требует подойти к оператору вплотную (30-40 см).
На 50mm геометрия естественнее, легче остаться незамеченным охраной. Кто какую оптику предпочитает?""", "2013-06-02 12:00:00"))
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t2_id, 8, """Ширик рулит однозначно! 24mm — золотая середина. Влезает и весь банкетный зал, и монументальный почес.""", "2013-06-02 13:30:00"))
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t2_id, 2, """Согласен с PitKa насчёт 50mm для скрытой съёмки. Особенно когда свадьба на Венце или банкет в ресторане Волга. Подойти на 30 см с шириком не всегда дадут подружки невесты. А с полтинника со второго ряда — шедевр готов в одно нажатие!""", "2013-06-02 15:00:00"))
# Topic 3
cur.execute("INSERT INTO topics (category_id, title, author_id, views, created_at) VALUES (?, ?, ?, ?, ?)",
(3, "Хроники Венца 2014: как мы встревали в кадр официального свадебного кортежа", 2, 580, "2014-04-10 17:30:00"))
t3_id = cur.lastrowid
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t3_id, 2, """Апрель 2014 года. Венец, вид на Императорский мост через Волгу.
Белый лимузин, жених поднимает невесту на руки, фотограф с зонтом командует: «Смотрим в даль, изображаем романтику!».
Мы с PitKa прогуливаемся рядом в куртках. PitKa занимает позицию с камерой, я делаю непринуждённый шаг спиной к объективу, рука уверенно впивается в карман джинсов и производит канонический ЖПЧХ.
Фотограф свадьбы даже не понял, почему у него на заднем плане композиции возник наш монументальный шедевр. Кадр вошёл в золотой фонд Клуба (см. Галерею, экспонат №4)!""", "2014-04-10 17:30:00"))
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t3_id, 9, """Это же классика жанра! Невеста потом пересматривает свадебный альбом, а там на фоне вечной Волги — вечный человеческий почес! Искусство как оно есть.""", "2014-04-10 19:15:00"))
# Topic 4
cur.execute("INSERT INTO topics (category_id, title, author_id, views, created_at) VALUES (?, ?, ?, ?, ?)",
(4, "Кодекс и этика участника Клуба: границы допустимого и уважение к событию", 1, 290, "2014-05-18 09:00:00"))
t4_id = cur.lastrowid
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t4_id, 1, """Напоминание Администрации всем новым членам сообщества.
Клуб Любителей ЖПЧХ — это интеллектуальная уличная фотография, а не хулиганство.
Правила:
1. Никакого оголения! Почес производится строго поверх одежды (джинсы, брюки, шорты).
2. Никаких оскорблений и срыва мероприятий. Мы не мешаем людям, мы просто фиксируем контраст.
3. Лица участников на открытых ресурсах стилизуются графически (карандаш, гравюра) во избежание лишней мирской славы.
Соблюдайте чистоту жанра!""", "2014-05-18 09:00:00"))
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t4_id, 2, """Золотые слова. Именно поэтому в нашей официальной галерее все 16 архивных работ представлены в строгой графической обработке.""", "2014-05-18 10:30:00"))
# Topic 5
cur.execute("INSERT INTO topics (category_id, title, author_id, views, created_at) VALUES (?, ?, ?, ?, ?)",
(2, "Съемка со вспышкой в темных залах: синхронизация по второй шторке", 6, 215, "2014-11-20 20:00:00"))
t5_id = cur.lastrowid
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t5_id, 6, """На юбилеях и корпоративах в полутемных банкетных залах свет обычно отвратительный.
Если лупить вспышкой в лоб — убивается атмосфера.
Кто как настраивает синхронизацию по второй шторке (Rear Sync), чтобы смаз движения руки давал ощущение динамики почеса?""", "2014-11-20 20:00:00"))
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t5_id, 2, """Выдержка 1/15 секунды, ISO 800, пыха на -1.7 EV с рассеивателем в потолок по второй шторке. Рука оставляет лёгкий динамический шлейф, а в конце импульс фиксирует пальцы в кульминационной точке. Проверено на десятке банкетов!""", "2014-11-20 21:40:00"))
# Topic 6
cur.execute("INSERT INTO topics (category_id, title, author_id, views, created_at) VALUES (?, ?, ?, ?, ?)",
(3, "Парная работа: техника «Наводчик и Исполнитель»", 3, 340, "2015-02-14 15:20:00"))
t6_id = cur.lastrowid
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t6_id, 3, """В одиночку снимать ЖПЧХ на автоспуске или от бедра сложно.
Идеальная тактика — работа в паре:
- Фотограф делает вид, что увлечённо снимает памятник, панораму реки или фонтан.
- Исполнитель как бы невзначай переходит улицу или останавливается перед объективом, любуясь пейзажем.
- Кодовое слово: «Горизонт завален!», после чего исполнитель приступает к почесу, а фотограф жмёт спуск серией.""", "2015-02-14 15:20:00"))
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t6_id, 7, """Мы в Ульяновске у Дома Офицеров так три кадра сняли подряд. Ни одна собака не заподозрила! Паре зачет.""", "2015-02-14 16:50:00"))
# Topic 7
cur.execute("INSERT INTO topics (category_id, title, author_id, views, created_at) VALUES (?, ?, ?, ?, ?)",
(1, "История зарождения движения на берегах Волги (Симбирск, лето 2012)", 1, 620, "2015-08-11 14:00:00"))
t7_id = cur.lastrowid
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t7_id, 1, """Историческая справка для архива.
Клуб ЖПЧХ ведёт свою летопись с жаркого августа 2012 года. Именно тогда в славном волжском городе Симбирске (Ульяновске) MatrixHasYou и PitKa впервые сформулировали концепцию контр-глянцевой фотографии.
В эпоху повального увлечения постановочными фотосессиями с искусственными улыбками и вылизанными фильтрами Инстаграма, зародился протест: возвращение к корням, честности и несокрушимой самоиронии.
С тех пор движение обрело последователей далеко за пределами Волги!""", "2015-08-11 14:00:00"))
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t7_id, 2, """Помню тот жаркий день на волжском берегу как вчера. 11 августа 2012 года. Именно тогда был сделан легендарный кадр `20120811_141940-2` (экспонат №6 в Галерее). 14 лет прошло, а актуальность только растёт!""", "2015-08-11 15:30:00"))
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t7_id, 3, """Волга вообще располагает к глубоким философским выводам о бренности пафоса. Слава ЖПЧХ!""", "2015-08-11 16:15:00"))
# Topic 8
cur.execute("INSERT INTO topics (category_id, title, author_id, views, created_at) VALUES (?, ?, ?, ?, ?)",
(2, "Плёнка или матрица: передача зерна и текстуры денима в ч/б исполнении", 10, 185, "2016-03-01 11:10:00"))
t8_id = cur.lastrowid
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t8_id, 10, """Зарядил в старый Киев-19 плёнку Tasma Тип-42.
Контраст бешеный, фактура потертых джинсов при почесе выглядит как гравюра эпохи Возрождения. Цифра всё-таки слишком стерильна для такого жанра, что думаете?""", "2016-03-01 11:10:00"))
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t8_id, 4, """Поэтому мы и переводим все цифры в карандашный набросок и гравюру. Штрих придаёт вечность.""", "2016-03-01 13:00:00"))
# Topic 9
cur.execute("INSERT INTO topics (category_id, title, author_id, views, created_at) VALUES (?, ?, ?, ?, ?)",
(1, "Символизм позы: анатомия захвата и положение пальцев", 2, 275, "2016-06-19 18:00:00"))
t9_id = cur.lastrowid
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t9_id, 2, """Важная деталь, отличающая мастера от дилетанта.
Пальцы не должны быть расслаблены! Должна чувствоваться работа предплечья, естественное натяжение ткани кармана или шва.
Если рука просто вяло лежит на бедре — это не ЖПЧХ, это поза уставшего грузчика.
ЖПЧХ — это действие в апогее!""", "2016-06-19 18:00:00"))
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t9_id, 3, """Абсолютно. В кадре должно читаться намерение. Зритель должен почувствовать этот чес спинным мозгом.""", "2016-06-19 19:20:00"))
# Topic 10
cur.execute("INSERT INTO topics (category_id, title, author_id, views, created_at) VALUES (?, ?, ?, ?, ?)",
(4, "Клубные членские билеты и дипломы Мастера Первой Степени", 1, 390, "2017-01-25 14:15:00"))
t10_id = cur.lastrowid
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t10_id, 1, """По многочисленным просьбам участников:
В типографии отпечатаны номерные членские билеты Клуба Любителей ЖПЧХ на плотной крафтовой бумаге.
Условия получения:
1. Зарегистрироваться на форуме.
2. Опубликовать не менее одного подтверждённого полевого фото-шедевра.
3. Получить одобрение Основателей (MatrixHasYou / PitKa).""", "2017-01-25 14:15:00"))
cur.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(t10_id, 5, """Подал заявку с серией кадров с набережной Свияги! Жду вердикта комиссии!""", "2017-01-25 16:30:00"))
# 4. Gallery items
gallery_items = [
("jpchx_01.jpg", "Экспонат №1: Монументальный почес у парадной", "Симбирск, Центр", "2012", "Первые полевые испытания контрастного кадрирования."),
("jpchx_02.jpg", "Экспонат №2: Весеннее обострение классики", "Набережная реки Свияги", "2014", "Кадр с естественным освещением в закатный час."),
("jpchx_03.jpg", "Экспонат №3: Кульминация банкетного вечера", "Ресторанный зал", "2014", "На заднем плане разгар торжества, на переднем — суровая правда жизни."),
("jpchx_04.jpg", "Экспонат №4: Свадебный эскорт на Венце", "Бульвар Новый Венец", "2014", "Легендарный кадр с участием Основателей. Хрестоматийный контраст."),
("jpchx_05.jpg", "Экспонат №5: Зимняя выдержка на морозе", "Площадь 100-летия Ленина", "2012", "Почес через плотный зимний пуховик требует особого мастерства."),
("jpchx_06.jpg", "Экспонат №6: Августовский манифест на Волге", "Волжский косогор", "2012", "Исторический день рождения движения 11 августа 2012 года."),
("jpchx_07.jpg", "Экспонат №7: Диалог с архитектурой", "Мемориальный комплекс", "2013", "Геометрическая рифма вертикалей здания и руки мастера."),
("jpchx_08.jpg", "Экспонат №8: Праздничный фуршет", "Банкетный комплекс", "2013", "Внедрение в официальное протокольное мероприятие."),
("jpchx_09.jpg", "Экспонат №9: Летний полдень в тени акаций", "Городской сквер", "2013", "Классический ракурс со спины на среднем фокусном расстоянии."),
("jpchx_10.jpg", "Экспонат №10: Встречное солнце и силуэт", "Волжский пляж", "2013", "Контровой свет выявляет фактуру ткани и динамику жеста."),
("jpchx_11.jpg", "Экспонат №11: Инспекция горизонта", "Смотровая площадка", "2014", "Широкий угол охвата: волжские дали и уверенный почес."),
("jpchx_12.jpg", "Экспонат №12: Ночной дозор после юбилея", "Вход в клуб", "2014", "Съемка при свете неоновых фонарей с легким смазом."),
("jpchx_13.jpg", "Экспонат №13: Вертикаль и ритм ступеней", "Лестница к речному вокзалу", "2014", "Уличный этюд с акцентом на пластику движения."),
("jpchx_14.jpg", "Экспонат №14: Осенний сплин у парапета", "Парк Дружбы Народов", "2014", "Меланхоличный волжский пейзаж и бескомпромиссная поза."),
("jpchx_15.jpg", "Экспонат №15: Академический этюд", "Аллея Славы", "2015", "Выверенная композиция по правилу третей."),
("jpchx_16.jpg", "Экспонат №16: Финал юбилейной сессии", "Загородная усадьба", "2015", "Закрытие четвертого сезона полевых съемок Клуба."),
]
cur.executemany("INSERT INTO gallery (filename, title, location, year, description) VALUES (?, ?, ?, ?, ?)", gallery_items)
conn.commit()
conn.close()
# ----------------- ROUTES -----------------
@app.context_processor
def inject_user():
user = None
if "user_id" in session:
db = get_db()
user = db.execute("SELECT id, username, role, avatar_text FROM users WHERE id = ?", (session["user_id"],)).fetchone()
return dict(current_user=user)
@app.route("/")
def index():
db = get_db()
latest_topics = db.execute("""
SELECT t.id, t.title, t.views, t.created_at, u.username, u.avatar_text,
(SELECT count(*) FROM posts p WHERE p.topic_id = t.id) as posts_count
FROM topics t
JOIN users u ON t.author_id = u.id
ORDER BY t.created_at DESC LIMIT 5
""").fetchall()
gallery_sample = db.execute("SELECT id, filename, title, location, year FROM gallery ORDER BY id ASC LIMIT 4").fetchall()
stats = {
"users": db.execute("SELECT count(*) FROM users").fetchone()[0],
"topics": db.execute("SELECT count(*) FROM topics").fetchone()[0],
"posts": db.execute("SELECT count(*) FROM posts").fetchone()[0],
"photos": db.execute("SELECT count(*) FROM gallery").fetchone()[0],
}
return render_template("index.html", latest_topics=latest_topics, gallery_sample=gallery_sample, stats=stats)
@app.route("/manifest")
def manifest():
return render_template("manifest.html")
@app.route("/gallery")
def gallery():
db = get_db()
items = db.execute("SELECT id, filename, title, location, year, description FROM gallery ORDER BY id ASC").fetchall()
return render_template("gallery.html", items=items)
@app.route("/forum")
def forum():
db = get_db()
categories = db.execute("SELECT id, title, description FROM categories ORDER BY id ASC").fetchall()
topics_by_cat = {}
for cat in categories:
topics = db.execute("""
SELECT t.id, t.title, t.views, t.created_at, u.username, u.role, u.avatar_text,
(SELECT count(*) FROM posts p WHERE p.topic_id = t.id) as posts_count,
(SELECT p.created_at FROM posts p WHERE p.topic_id = t.id ORDER BY p.id DESC LIMIT 1) as last_post_time
FROM topics t
JOIN users u ON t.author_id = u.id
WHERE t.category_id = ?
ORDER BY t.created_at DESC
""", (cat["id"],)).fetchall()
topics_by_cat[cat["id"]] = topics
return render_template("forum.html", categories=categories, topics_by_cat=topics_by_cat)
@app.route("/forum/topic/<int:topic_id>")
def topic(topic_id):
db = get_db()
db.execute("UPDATE topics SET views = views + 1 WHERE id = ?", (topic_id,))
db.commit()
topic_row = db.execute("""
SELECT t.id, t.title, t.created_at, t.views, c.id as cat_id, c.title as cat_title, u.username, u.avatar_text, u.role
FROM topics t
JOIN categories c ON t.category_id = c.id
JOIN users u ON t.author_id = u.id
WHERE t.id = ?
""", (topic_id,)).fetchone()
if not topic_row:
flash("Тема не найдена.", "danger")
return redirect(url_for("forum"))
posts = db.execute("""
SELECT p.id, p.content, p.created_at, u.username, u.role, u.avatar_text,
(SELECT count(*) FROM posts p2 WHERE p2.author_id = u.id) as user_posts_count
FROM posts p
JOIN users u ON p.author_id = u.id
WHERE p.topic_id = ?
ORDER BY p.id ASC
""", (topic_id,)).fetchall()
return render_template("topic.html", topic=topic_row, posts=posts)
@app.route("/forum/new_topic", methods=["GET", "POST"])
@login_required
def new_topic():
db = get_db()
if request.method == "POST":
title = request.form.get("title", "").strip()
category_id = request.form.get("category_id")
content = request.form.get("content", "").strip()
if not title or not category_id or not content:
flash("Все поля обязательны для заполнения.", "warning")
return redirect(url_for("new_topic"))
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cur = db.execute("INSERT INTO topics (category_id, title, author_id, created_at) VALUES (?, ?, ?, ?)",
(category_id, title, session["user_id"], now))
topic_id = cur.lastrowid
db.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(topic_id, session["user_id"], content, now))
db.commit()
flash("Тема успешно создана!", "success")
return redirect(url_for("topic", topic_id=topic_id))
categories = db.execute("SELECT id, title FROM categories ORDER BY id ASC").fetchall()
return render_template("new_topic.html", categories=categories)
@app.route("/forum/reply/<int:topic_id>", methods=["POST"])
@login_required
def reply_topic(topic_id):
content = request.form.get("content", "").strip()
if not content:
flash("Сообщение не может быть пустым.", "warning")
return redirect(url_for("topic", topic_id=topic_id))
db = get_db()
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
db.execute("INSERT INTO posts (topic_id, author_id, content, created_at) VALUES (?, ?, ?, ?)",
(topic_id, session["user_id"], content, now))
db.commit()
flash("Ваш ответ опубликован!", "success")
return redirect(url_for("topic", topic_id=topic_id))
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form.get("username", "").strip()
password = request.form.get("password", "").strip()
db = get_db()
user = db.execute("SELECT id, username, password_hash FROM users WHERE username = ?", (username,)).fetchone()
if user and user["password_hash"] == hash_pw(password):
session["user_id"] = user["id"]
session["username"] = user["username"]
flash(f"Добро пожаловать, {username}!", "success")
next_url = request.args.get("next") or url_for("forum")
return redirect(next_url)
else:
flash("Неверное имя пользователя или пароль.", "danger")
return render_template("login.html")
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "POST":
username = request.form.get("username", "").strip()
password = request.form.get("password", "").strip()
password2 = request.form.get("password2", "").strip()
if not username or not password:
flash("Заполните все обязательные поля.", "warning")
return render_template("register.html")
if len(username) < 3:
flash("Имя пользователя должно быть не менее 3 символов.", "warning")
return render_template("register.html")
if password != password2:
flash("Введенные пароли не совпадают.", "warning")
return render_template("register.html")
db = get_db()
existing = db.execute("SELECT id FROM users WHERE username = ?", (username,)).fetchone()
if existing:
flash("Пользователь с таким именем уже зарегистрирован.", "warning")
return render_template("register.html")
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
cur = db.execute("INSERT INTO users (username, password_hash, role, avatar_text, created_at) VALUES (?, ?, ?, ?, ?)",
(username, hash_pw(password), "Новичок Клуба", "📸", now))
db.commit()
session["user_id"] = cur.lastrowid
session["username"] = username
flash(f"Регистрация успешна! Добро пожаловать в Клуб Любителей ЖПЧХ, {username}!", "success")
return redirect(url_for("forum"))
return render_template("register.html")
@app.route("/logout")
def logout():
session.clear()
flash("Вы успешно вышли из системы.", "info")
return redirect(url_for("index"))
@app.route("/healthz")
def healthz():
return "OK", 200
if __name__ == "__main__":
init_db()
port = int(os.environ.get("PORT", 8080))
app.run(host="0.0.0.0", port=port, debug=False)

Binary file not shown.

View file

@ -1,2 +0,0 @@
flask==3.0.3
werkzeug==3.0.3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 289 KiB

View file

@ -1,341 +0,0 @@
/* ========================================================
Клуб Любителей ЖПЧХ (Основан на Волге, 2012)
Канонический текстовый интерфейс
======================================================== */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #f4f4f4;
color: #222222;
font-family: "Courier New", Consolas, Monaco, "Lucida Console", monospace;
font-size: 14px;
line-height: 1.5;
padding: 10px;
}
.wrapper {
max-width: 1050px;
margin: 0 auto;
background: #ffffff;
border: 1px solid #707680;
box-shadow: none;
}
/* Header */
header {
background: #ece9d8;
border-bottom: 2px solid #707680;
padding: 12px 16px;
}
.header-top {
display: flex;
justify-content: space-between;
align-items: baseline;
flex-wrap: wrap;
gap: 10px;
}
.site-title {
font-size: 20px;
font-weight: bold;
color: #000000;
text-decoration: none;
letter-spacing: -0.5px;
}
.site-tagline {
font-size: 12px;
color: #555555;
margin-top: 4px;
}
.user-panel {
font-size: 13px;
background: #ffffff;
border: 1px solid #999999;
padding: 4px 8px;
}
/* Navigation bar */
nav {
background: #e0ded0;
border-bottom: 1px solid #707680;
padding: 6px 16px;
font-size: 13px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
nav a {
color: #0000cc;
text-decoration: none;
padding: 2px 6px;
}
nav a:hover {
background: #ffffff;
text-decoration: underline;
}
nav a.active {
font-weight: bold;
color: #000000;
background: #ffffff;
border: 1px solid #999999;
}
/* Content area */
main {
padding: 16px;
min-height: 500px;
}
/* Flash alerts */
.alert {
padding: 8px 12px;
margin-bottom: 14px;
border: 1px solid #707680;
font-size: 13px;
}
.alert-success { background: #e6f4ea; border-color: #5bb974; color: #137333; }
.alert-warning { background: #fef7e0; border-color: #f9ab00; color: #b06000; }
.alert-danger { background: #fce8e6; border-color: #ee675c; color: #c5221f; }
.alert-info { background: #e8f0fe; border-color: #669df6; color: #1a73e8; }
/* Typography */
h1 { font-size: 18px; border-bottom: 1px solid #cccccc; padding-bottom: 6px; margin-bottom: 14px; }
h2 { font-size: 16px; border-bottom: 1px solid #e0e0e0; padding-bottom: 4px; margin: 16px 0 10px 0; }
h3 { font-size: 14px; margin: 12px 0 6px 0; }
p { margin-bottom: 10px; text-align: justify; }
a { color: #0000cc; text-decoration: underline; }
a:hover { color: #cc0000; }
/* Tables */
table.table-forum {
width: 100%;
border-collapse: collapse;
border: 1px solid #707680;
margin: 12px 0 20px 0;
font-size: 13px;
}
table.table-forum th {
background: #ece9d8;
border: 1px solid #707680;
padding: 6px 8px;
text-align: left;
font-weight: bold;
}
table.table-forum td {
border: 1px solid #d0d0d0;
padding: 6px 8px;
vertical-align: top;
}
table.table-forum tr:hover td {
background: #fafafa;
}
.table-stats {
width: 100%;
border-collapse: collapse;
border: 1px solid #cccccc;
margin: 10px 0;
font-size: 12px;
}
.table-stats td {
padding: 4px 8px;
border: 1px solid #e0e0e0;
}
/* Topic & Posts view */
.post-box {
border: 1px solid #707680;
margin-bottom: 14px;
display: flex;
background: #ffffff;
}
.post-author {
width: 180px;
background: #f7f6f0;
border-right: 1px solid #707680;
padding: 10px;
font-size: 12px;
flex-shrink: 0;
}
.post-author .name {
font-weight: bold;
font-size: 14px;
margin-bottom: 4px;
color: #000066;
}
.post-author .role {
color: #555555;
margin-bottom: 6px;
}
.post-content {
flex-grow: 1;
padding: 10px 14px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.post-text {
font-size: 13px;
white-space: pre-wrap;
line-height: 1.6;
}
.post-meta {
font-size: 11px;
color: #777777;
border-top: 1px dashed #dddddd;
margin-top: 12px;
padding-top: 4px;
text-align: right;
}
/* Gallery Grid */
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
gap: 14px;
margin-top: 14px;
}
.gallery-card {
border: 1px solid #707680;
background: #ffffff;
padding: 8px;
display: flex;
flex-direction: column;
}
.gallery-img-wrap {
width: 100%;
height: 240px;
overflow: hidden;
background: #f0f0f0;
border: 1px solid #cccccc;
display: flex;
align-items: center;
justify-content: center;
}
.gallery-img-wrap img {
width: 100%;
height: 100%;
object-fit: cover;
filter: contrast(1.1);
}
.gallery-info {
margin-top: 8px;
font-size: 12px;
flex-grow: 1;
}
.gallery-title {
font-weight: bold;
font-size: 13px;
margin-bottom: 2px;
}
.gallery-location {
color: #666666;
font-size: 11px;
margin-bottom: 4px;
}
.gallery-desc {
color: #333333;
font-size: 11px;
line-height: 1.4;
}
/* Forms */
.form-group {
margin-bottom: 12px;
}
.form-group label {
display: block;
font-weight: bold;
margin-bottom: 4px;
font-size: 13px;
}
input[type="text"],
input[type="password"],
select,
textarea {
width: 100%;
padding: 6px 8px;
font-family: inherit;
font-size: 13px;
border: 1px solid #707680;
background: #ffffff;
}
textarea {
resize: vertical;
min-height: 140px;
}
button.btn, .btn {
display: inline-block;
background: #ece9d8;
color: #000000;
border: 1px solid #707680;
padding: 5px 14px;
font-family: inherit;
font-size: 13px;
cursor: pointer;
text-decoration: none;
}
button.btn:hover, .btn:hover {
background: #ffffff;
border-color: #000000;
}
.btn-primary {
background: #d8e2ec;
font-weight: bold;
}
/* Footer */
footer {
background: #ece9d8;
border-top: 1px solid #707680;
padding: 10px 16px;
font-size: 11px;
color: #555555;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 6px;
}
pre.ascii-box {
background: #f7f6f0;
border: 1px solid #cccccc;
padding: 10px;
font-size: 12px;
overflow-x: auto;
margin: 10px 0;
}

View file

@ -1,57 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Клуб Любителей ЖПЧХ{% endblock %} — Международное сообщество контрастной фотографии</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="wrapper">
<header>
<div class="header-top">
<div>
<a href="{{ url_for('index') }}" class="site-title">КЛУБ ЛЮБИТЕЛЕЙ ЖПЧХ</a>
<div class="site-tagline">Международное сообщество уличной и контрастной фотографии (Основано на Волге, 2012)</div>
</div>
<div class="user-panel">
{% if current_user %}
<span>Вы вошли как: <b>{{ current_user.username }}</b> ({{ current_user.role }})</span> |
<a href="{{ url_for('logout') }}">Выход</a>
{% else %}
<a href="{{ url_for('login') }}">Вход</a> |
<a href="{{ url_for('register') }}">Регистрация</a>
{% endif %}
</div>
</div>
</header>
<nav>
<a href="{{ url_for('index') }}" class="{% if request.endpoint == 'index' %}active{% endif %}">[ Главная ]</a>
<a href="{{ url_for('manifest') }}" class="{% if request.endpoint == 'manifest' %}active{% endif %}">[ Манифест ]</a>
<a href="{{ url_for('gallery') }}" class="{% if request.endpoint == 'gallery' %}active{% endif %}">[ Фотогалерея (16) ]</a>
<a href="{{ url_for('forum') }}" class="{% if request.endpoint in ['forum', 'topic', 'new_topic'] %}active{% endif %}">[ Форум ]</a>
{% if current_user %}
<a href="{{ url_for('new_topic') }}">[ + Создать тему ]</a>
{% endif %}
</nav>
<main>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer>
<div>&copy; 2012–2026 Клуб Любителей ЖПЧХ. Волжское отделение (Симбирск). Все права защищены.</div>
<div>Сервер: jpchx.matrixhasyou.ru | Портал контрастной уличной фотографии</div>
</footer>
</div>
</body>
</html>

View file

@ -1,54 +0,0 @@
{% extends "base.html" %}
{% block title %}Форум Клуба Любителей ЖПЧХ{% endblock %}
{% block content %}
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px;">
<h1>ОФИЦИАЛЬНЫЙ ФОРУМ КЛУБА ЖПЧХ</h1>
{% if current_user %}
<a href="{{ url_for('new_topic') }}" class="btn btn-primary">+ Создать новую тему</a>
{% endif %}
</div>
<p>
Здесь участники сообщества обсуждают технику съёмки, делятся опытом полевых вылазок, консультируются
с Основателями (<b>MatrixHasYou</b>, <b>PitKa</b>) и обсуждают клубные инициативы.
</p>
{% for cat in categories %}
<h2>{{ cat.title }}</h2>
<p style="font-size: 12px; color: #555555; margin-bottom: 6px;">{{ cat.description }}</p>
<table class="table-forum">
<thead>
<tr>
<th>Название темы</th>
<th style="width: 140px;">Автор темы</th>
<th style="width: 70px; text-align: center;">Ответов</th>
<th style="width: 80px; text-align: center;">Просмотров</th>
<th style="width: 160px;">Последняя активность</th>
</tr>
</thead>
<tbody>
{% set topics = topics_by_cat[cat.id] %}
{% if topics %}
{% for t in topics %}
<tr>
<td>
<a href="{{ url_for('topic', topic_id=t.id) }}"><b>{{ t.title }}</b></a>
</td>
<td>{{ t.username }}</td>
<td style="text-align: center;">{{ t.posts_count }}</td>
<td style="text-align: center;">{{ t.views }}</td>
<td style="font-size: 11px;">{{ t.last_post_time or t.created_at }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="5" style="text-align: center; color: #777777;">В этом разделе пока нет тем.</td>
</tr>
{% endif %}
</tbody>
</table>
{% endfor %}
{% endblock %}

View file

@ -1,34 +0,0 @@
{% extends "base.html" %}
{% block title %}Фотогалерея Клуба ЖПЧХ (16 экспонатов){% endblock %}
{% block content %}
<h1>АРХИВНАЯ ФОТОГАЛЕРЕЯ КЛУБА ЛЮБИТЕЛЕЙ ЖПЧХ</h1>
<p>
В экспозиции представлены 16 канонических полевых снимков 2012–2016 гг., прошедших авторскую графическую обработку
(перевод в классический карандашный набросок и гравюру).
Обработка подчеркивает пластику движений, складки ткани и контраст планов, сохраняя скромность авторов.
</p>
<div class="gallery-grid">
{% for item in items %}
<div class="gallery-card">
<div class="gallery-img-wrap">
<a href="{{ url_for('static', filename='gallery/' + item.filename) }}" target="_blank">
<img src="{{ url_for('static', filename='gallery/' + item.filename) }}" alt="{{ item.title }}" loading="lazy">
</a>
</div>
<div class="gallery-info">
<div class="gallery-title">{{ item.title }}</div>
<div class="gallery-location"><b>Локация:</b> {{ item.location }} ({{ item.year }})</div>
<div class="gallery-desc">{{ item.description }}</div>
</div>
</div>
{% endfor %}
</div>
<p style="margin-top: 20px; font-size: 12px; color: #555555;">
* Нажмите на изображение для просмотра в полном разрешении. Все снимки защищены архивным статусом Клуба.
</p>
{% endblock %}

View file

@ -1,81 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h1>ДОБРО ПОЖАЛОВАТЬ В КЛУБ ЛЮБИТЕЛЕЙ ЖПЧХ</h1>
<p>
<b>Клуб Любителей ЖПЧХ (ЖопоПоЧесХа)</b> — независимое международное фотографическое объединение,
зародившееся жарким летом 2012 года на берегах великой русской реки Волги (город Симбирск / Ульяновск).
</p>
<pre class="ascii-box">
========================================================================================
ФИЛОСОФИЯ КОНТРАСТА ЖПЧХ:
[Задний план]: Пафос, официальные лица, свадебные клятвы, выпускные ленты, банкеты.
[Передний план]: Человек спиной к камере. Честный, уверенный почес своей задницы.
[Итог]: Разрушение фальшивого глянца, возвращение к искренней витальности бытия.
========================================================================================
</pre>
<p>
В эпоху всеобщей фальшивости постановочных кадров, искусственных улыбок и пластиковых фильтров социальных сетей,
основатели Клуба (<b>MatrixHasYou</b> и <b>PitKa</b>) создали новое направление репортажной стри-фотографии.
Суть методики — встревать в любые важные фотокадры в любом месте: от протокольных свадебных кортежей на Венце
до торжественных юбилеев, выпускных у Мемцентра и корпоративных банкетов.
</p>
<h2>ИЗБРАННЫЕ ЭКСПОНАТЫ ГАЛЕРЕИ (ГРАФИЧЕСКИЙ АРХИВ)</h2>
<div class="gallery-grid">
{% for item in gallery_sample %}
<div class="gallery-card">
<div class="gallery-img-wrap">
<img src="{{ url_for('static', filename='gallery/' + item.filename) }}" alt="{{ item.title }}">
</div>
<div class="gallery-info">
<div class="gallery-title">{{ item.title }}</div>
<div class="gallery-location">{{ item.location }} ({{ item.year }})</div>
</div>
</div>
{% endfor %}
</div>
<p style="margin-top: 10px; text-align: right;">
<a href="{{ url_for('gallery') }}"><b>Перейти ко всем 16 экспонатам в Фотогалерее &rarr;</b></a>
</p>
<h2>СВЕЖИЕ ОБСУЖДЕНИЯ НА ФОРУМЕ КЛУБА</h2>
<table class="table-forum">
<thead>
<tr>
<th>Тема</th>
<th style="width: 140px;">Автор</th>
<th style="width: 80px; text-align: center;">Ответов</th>
<th style="width: 80px; text-align: center;">Просмотров</th>
<th style="width: 160px;">Дата создания</th>
</tr>
</thead>
<tbody>
{% for t in latest_topics %}
<tr>
<td><a href="{{ url_for('topic', topic_id=t.id) }}"><b>{{ t.title }}</b></a></td>
<td>{{ t.username }}</td>
<td style="text-align: center;">{{ t.posts_count }}</td>
<td style="text-align: center;">{{ t.views }}</td>
<td>{{ t.created_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p style="text-align: right;">
<a href="{{ url_for('forum') }}"><b>Смотреть все темы на Форуме &rarr;</b></a>
</p>
<h2>СТАТИСТИКА СООБЩЕСТВА</h2>
<table class="table-stats">
<tr>
<td><b>Участников:</b> {{ stats.users }}</td>
<td><b>В веток на форуме:</b> {{ stats.topics }}</td>
<td><b>Сообщений:</b> {{ stats.posts }}</td>
<td><b>Работ в архиве:</b> {{ stats.photos }}</td>
</tr>
</table>
{% endblock %}

View file

@ -1,26 +0,0 @@
{% extends "base.html" %}
{% block title %}Вход в Клуб Любителей ЖПЧХ{% endblock %}
{% block content %}
<h1>АВТОРИЗАЦИЯ УЧАСТНИКА</h1>
<form action="{{ url_for('login', next=request.args.get('next')) }}" method="POST" style="max-width: 400px; margin-top: 16px;">
<div class="form-group">
<label for="username">Имя пользователя (Никнейм):</label>
<input type="text" name="username" id="username" required autofocus>
</div>
<div class="form-group">
<label for="password">Пароль:</label>
<input type="password" name="password" id="password" required>
</div>
<button type="submit" class="btn btn-primary">Войти</button>
<a href="{{ url_for('register') }}" class="btn" style="margin-left: 8px;">Регистрация</a>
</form>
<p style="margin-top: 20px; font-size: 12px; color: #666666;">
* Если вы еще не состоите в Клубе, пройдите <a href="{{ url_for('register') }}">простую регистрацию</a>.
</p>
{% endblock %}

View file

@ -1,48 +0,0 @@
{% extends "base.html" %}
{% block title %}Манифест движения ЖПЧХ{% endblock %}
{% block content %}
<h1>МАНИФЕСТ ДВИЖЕНИЯ ЖПЧХ (СИМБИРСК, 2012)</h1>
<pre class="ascii-box">
«В мире, где каждый пытается казаться тем, кем не является,
честный почес задницы на фоне пафосного события —
единственный оплот подлинного реализма».
— MatrixHasYou & PitKa, лето 2012 г.
</pre>
<h2>1. ИСТОРИЧЕСКИЕ КОРНИ</h2>
<p>
Движение ЖПЧХ зародилось в городе Симбирске (Ульяновск) на крутом волжском венце.
Наблюдая за бесконечными одинаковыми свадебными фотосессиями у парапета с видом на Императорский мост,
где женихи в синтетических костюмах изображали античных героев, а невесты застывали в неестественных позах,
основатели почувствовали острую потребность в деконструкции жанра.
</p>
<p>
Так родился первый исторический кадр: человек в куртке стоит спиной к объективу, пока на заднем плане
разворачивается свадебный кортеж, и уверенным движением производит почес.
Контраст оказался настолько мощным и визуально убедительным, что положил начало целому направлению.
</p>
<h2>2. ТРИ ЗАКОНА КОМПОЗИЦИИ ЖПЧХ</h2>
<ol style="margin-left: 20px; line-height: 1.8;">
<li><b>Закон Первого Плана:</b> Почес должен доминировать в ближней зоне (от 0.5 до 2 метров от объектива), задавая масштаб кадра.</li>
<li><b>Закон Контраста События:</b> Чем торжественнее и напыщеннее задний план (государственные приемы, выпускные, свадьбы, награждения), тем выше художественная ценность снимка.</li>
<li><b>Закон Достоверности Жеста:</b> Рука обязана плотно фиксировать ткань, создавая натяжение складок денима. Никакой вялой бутафории — только честная кинематика почеса!</li>
</ol>
<h2>3. ЭТИЧЕСКИЙ КОДЕКС КЛУБА</h2>
<p>
Клуб Любителей ЖПЧХ категорически отвергает вульгарность и хулиганство:
</p>
<ul style="margin-left: 20px; line-height: 1.8;">
<li><b>Строго поверх одежды:</b> Почес осуществляется исключительно через штаны, джинсы или шорты. Обнажение запрещено уставом.</li>
<li><b>Невидимость для участников:</b> Настоящий мастер ЖПЧХ не срывает событие. Торжество идет своим чередом, люди радуются и празднуют, не подозревая, что их пафос только что был художественно заземлен.</li>
<li><b>Графическая анонимизация:</b> В открытых клубных альбомах лица участников переводятся в гравюрный и карандашный штрих. Герои скромны и служат искусству.</li>
</ul>
<p style="margin-top: 20px;">
Присоединиться к движению может любой желающий. Достаточно зарегистрироваться на форуме и представить свой первый отчет!
</p>
{% endblock %}

View file

@ -1,31 +0,0 @@
{% extends "base.html" %}
{% block title %}Создать новую тему{% endblock %}
{% block content %}
<h1>СОЗДАНИЕ НОВОЙ ТЕМЫ НА ФОРУМЕ</h1>
<form action="{{ url_for('new_topic') }}" method="POST" style="max-width: 700px;">
<div class="form-group">
<label for="category_id">Раздел форума:</label>
<select name="category_id" id="category_id" required>
{% for cat in categories %}
<option value="{{ cat.id }}">{{ cat.title }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label for="title">Заголовок темы:</label>
<input type="text" name="title" id="title" required placeholder="Краткое и ёмкое название темы...">
</div>
<div class="form-group">
<label for="content">Текст сообщения:</label>
<textarea name="content" id="content" required placeholder="Опишите ситуацию, вопрос по оптике, локацию или предложите идею съемки..."></textarea>
</div>
<button type="submit" class="btn btn-primary">Создать тему</button>
<a href="{{ url_for('forum') }}" class="btn" style="margin-left: 8px;">Отмена</a>
</form>
{% endblock %}

View file

@ -1,32 +0,0 @@
{% extends "base.html" %}
{% block title %}Регистрация в Клубе Любителей ЖПЧХ{% endblock %}
{% block content %}
<h1>РЕГИСТРАЦИЯ НОВОГО УЧАСТНИКА</h1>
<p>
Регистрация открыта для всех ценителей реализма и уличной контрастной фотографии.
После регистрации вы сможете создавать темы на форуме и отвечать в дискуссиях.
</p>
<form action="{{ url_for('register') }}" method="POST" style="max-width: 400px; margin-top: 16px;">
<div class="form-group">
<label for="username">Желаемый никнейм (от 3 символов):</label>
<input type="text" name="username" id="username" required minlength="3" autofocus>
</div>
<div class="form-group">
<label for="password">Пароль:</label>
<input type="password" name="password" id="password" required minlength="4">
</div>
<div class="form-group">
<label for="password2">Повторите пароль:</label>
<input type="password" name="password2" id="password2" required minlength="4">
</div>
<button type="submit" class="btn btn-primary">Зарегистрироваться</button>
<a href="{{ url_for('login') }}" class="btn" style="margin-left: 8px;">Уже есть аккаунт</a>
</form>
{% endblock %}

View file

@ -1,47 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ topic.title }}{% endblock %}
{% block content %}
<div style="font-size: 12px; margin-bottom: 10px;">
<a href="{{ url_for('forum') }}">&larr; Вернуться в список тем</a> /
<b>{{ topic.cat_title }}</b>
</div>
<h1>{{ topic.title }}</h1>
<div style="font-size: 11px; color: #666666; margin-bottom: 16px;">
Автор темы: <b>{{ topic.username }}</b> | Создано: {{ topic.created_at }} | Просмотров: {{ topic.views }}
</div>
<div class="posts-list">
{% for post in posts %}
<div class="post-box" id="post-{{ post.id }}">
<div class="post-author">
<div class="name">{{ post.username }}</div>
<div class="role">{{ post.role }}</div>
<div style="font-size: 11px; color: #777777;">Сообщений: {{ post.user_posts_count }}</div>
</div>
<div class="post-content">
<div class="post-text">{{ post.content }}</div>
<div class="post-meta">Опубликовано: {{ post.created_at }} #{{ loop.index }}</div>
</div>
</div>
{% endfor %}
</div>
<h2>ОТВЕТИТЬ В ТЕМУ</h2>
{% if current_user %}
<form action="{{ url_for('reply_topic', topic_id=topic.id) }}" method="POST">
<div class="form-group">
<label for="content">Ваше сообщение (от имени <b>{{ current_user.username }}</b>):</label>
<textarea name="content" id="content" required placeholder="Поделитесь опытом, задайте вопрос или прокомментируйте ракурс..."></textarea>
</div>
<button type="submit" class="btn btn-primary">Опубликовать ответ</button>
</form>
{% else %}
<p>
Чтобы написать ответ в эту тему, пожалуйста, <a href="{{ url_for('login', next=request.url) }}"><b>войдите</b></a>
или <a href="{{ url_for('register') }}"><b>зарегистрируйтесь</b></a>.
</p>
{% endif %}
{% endblock %}