Загрузка данных
import os
import uuid
import sqlite3
import requests
from flask import Flask, render_template_string, request, redirect, url_for, flash, session
app = Flask(__name__)
# Секретный ключ для защиты сессий Flask
app.secret_key = 'super_secret_session_key_999'
# ---------------------------------------------------------------
# СЕКРЕТНЫЙ КЛЮЧ / ПАРОЛЬ ДЛЯ ВХОДА В АДМИН-ПАНЕЛЬ
# Вы можете изменить его на любой свой пароль:
ADMIN_SECRET_KEY = "admin12345"
# ---------------------------------------------------------------
# Твой токен Яндекс.Диска
YANDEX_TOKEN = "0cb7e4fba386444fbe8007f2dceb223b"
# --- ИНИЦИАЛИЗАЦИЯ БАЗЫ ДАННЫХ ---
def init_db():
conn = sqlite3.connect('photographer_studio.db')
cursor = conn.cursor()
# Тексты и фото для Главной и Обо мне
cursor.execute('''
CREATE TABLE IF NOT EXISTS content (
key TEXT PRIMARY KEY,
value TEXT
)
''')
# Отзывы
cursor.execute('''
CREATE TABLE IF NOT EXISTS reviews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
author TEXT,
text TEXT,
photo_url TEXT
)
''')
# Подарочные сертификаты
cursor.execute('''
CREATE TABLE IF NOT EXISTS certificates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE,
client_name TEXT,
amount_or_desc TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Персональные галереи для клиентов
cursor.execute('''
CREATE TABLE IF NOT EXISTS client_galleries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
unique_id TEXT UNIQUE,
client_name TEXT,
template TEXT,
photos_json TEXT
)
''')
# Фотографии, загруженные из Яндекс.Диска
cursor.execute('''
CREATE TABLE IF NOT EXISTS yandex_photos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
public_url TEXT
)
''')
# Дефолтные данные, если база пустая
defaults = [
('main_title', 'Атмосферная & Эстетичная Фотография'),
('main_text', 'Я создаю кадры, которые сохраняют настоящие эмоции, свет и тепло ваших лучших моментов.'),
('main_photo', 'https://images.unsplash.com/photo-1492691527719-9d1e07e534b4?auto=format&fit=crop&w=1200&q=80'),
('about_title', 'Обо мне'),
('about_text', 'Привет! Я профессиональный фотограф с 7-летним стажем. Специализируюсь на портретах, свадьбах и индивидуальных съёмках. Мой подход — естественность и лаконичность.'),
('about_photo', 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=800&q=80')
]
for key, val in defaults:
cursor.execute('INSERT OR IGNORE INTO content (key, value) VALUES (?, ?)', (key, val))
conn.commit()
conn.close()
init_db()
# --- ОБЩИЙ HTML-СТИЛЬ В ПРИЯТНЫХ ПАСТЕЛЬНЫХ ТОНАХ ---
BASE_CSS = """
<style>
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Plus+Jakarta+Sans:wght@300;400;500;600&display=swap');
:root {
--bg-color: #FBF9F5;
--card-bg: #FFFFFF;
--text-color: #332E2B;
--accent-color: #8C7A6B;
--accent-hover: #6E5E51;
--light-accent: #EFECE6;
--border-color: #E2DDD5;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
line-height: 1.6;
}
h1, h2, h3, .serif {
font-family: 'Cormorant Garamond', serif;
font-weight: 600;
color: var(--text-color);
}
/* Навигация */
header {
background: rgba(251, 249, 245, 0.95);
backdrop-filter: blur(8px);
position: sticky;
top: 0;
z-index: 100;
border-bottom: 1px solid var(--border-color);
padding: 20px 40px;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo { font-size: 24px; font-weight: 600; letter-spacing: 1px; color: var(--accent-color); text-decoration: none; }
nav a { margin-left: 25px; text-decoration: none; color: var(--text-color); font-size: 15px; font-weight: 500; transition: 0.3s; }
nav a:hover { color: var(--accent-color); }
.admin-link { border: 1px solid var(--accent-color); padding: 6px 14px; border-radius: 20px; color: var(--accent-color); }
.admin-link:hover { background: var(--accent-color); color: #fff; }
/* Контейнеры */
.container { max-width: 1100px; margin: 40px auto; padding: 0 20px; }
.hero { text-align: center; padding: 60px 20px; }
.hero h1 { font-size: 48px; margin-bottom: 20px; }
.hero p { font-size: 18px; color: #666; max-width: 700px; margin: 0 auto 30px; }
.hero-img { width: 100%; height: 500px; object-fit: cover; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.05); }
/* Сетка фото и отзывов */
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 25px; margin-top: 30px; }
.card { background: var(--card-bg); border-radius: 10px; padding: 25px; border: 1px solid var(--border-color); }
.card img { width: 100%; height: 250px; object-fit: cover; border-radius: 6px; margin-bottom: 15px; }
/* Формы и кнопки */
.btn {
background: var(--accent-color); color: white; padding: 12px 24px; border: none;
border-radius: 6px; cursor: pointer; font-size: 15px; transition: 0.3s; text-decoration: none; display: inline-block;
}
.btn:hover { background: var(--accent-hover); }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 8px; font-weight: 500; font-size: 14px; }
input[type="text"], input[type="password"], textarea, select {
width: 100%; padding: 12px; border: 1px solid var(--border-color); border-radius: 6px;
background: #FAF8F5; font-family: inherit; font-size: 14px;
}
textarea { height: 120px; resize: vertical; }
/* Шаблоны галерей */
.gallery-masonry { columns: 3 250px; column-gap: 15px; }
.gallery-masonry img { width: 100%; margin-bottom: 15px; border-radius: 8px; display: block; }
.gallery-slideshow { display: flex; flex-direction: column; gap: 40px; align-items: center; }
.gallery-slideshow img { max-width: 900px; width: 100%; border-radius: 8px; }
/* Панель админа */
.admin-grid { display: grid; grid-template-columns: 240px 1fr; gap: 30px; margin-top: 30px; }
.sidebar { background: var(--card-bg); padding: 20px; border-radius: 8px; border: 1px solid var(--border-color); }
.sidebar a { display: block; padding: 10px; color: var(--text-color); text-decoration: none; border-radius: 4px; margin-bottom: 5px; }
.sidebar a:hover { background: var(--light-accent); color: var(--accent-color); }
footer { text-align: center; padding: 40px; border-top: 1px solid var(--border-color); margin-top: 80px; font-size: 14px; color: #888; }
</style>
"""
NAVBAR_HTML = """
<header>
<a href="/" class="logo serif">PORTFOLIO</a>
<nav>
<a href="/">Главная</a>
<a href="/about">Обо мне</a>
<a href="/reviews">Отзывы</a>
<a href="/certificates">Сертификаты</a>
<a href="/admin" class="admin-link">Админ-панель</a>
</nav>
</header>
"""
def get_db():
conn = sqlite3.connect('photographer_studio.db')
conn.row_factory = sqlite3.Row
return conn
# --- ПУБЛИЧНЫЕ СТРАНИЦЫ САЙТА ---
@app.route('/')
def index():
conn = get_db()
content = dict(conn.execute('SELECT * FROM content').fetchall())
photos = conn.execute('SELECT * FROM yandex_photos LIMIT 6').fetchall()
conn.close()
html = f"""
<!DOCTYPE html><html><head><title>Главная | Фотограф</title>{BASE_CSS}</head><body>
{NAVBAR_HTML}
<div class="container">
<section class="hero">
<h1>{content.get('main_title', '')}</h1>
<p>{content.get('main_text', '')}</p>
<img src="{content.get('main_photo', '')}" class="hero-img" alt="Main Photo">
</section>
<h2 style="text-align: center; margin-top: 50px;" class="serif">Избранные Работы</h2>
<div class="grid">
{"".join([f'<div class="card"><img src="{p["public_url"]}" alt="{p["name"]}"></div>' for p in photos]) if photos else '<p style="text-align:center; grid-column:1/-1;">Фотографии пока не синхронизированы с Яндекс.Диска.</p>'}
</div>
</div>
<footer>© 2026 Все права защищены. Создано с любовью.</footer>
</body></html>
"""
return render_template_string(html)
@app.route('/about')
def about():
conn = get_db()
content = dict(conn.execute('SELECT * FROM content').fetchall())
conn.close()
html = f"""
<!DOCTYPE html><html><head><title>Обо мне | Фотограф</title>{BASE_CSS}</head><body>
{NAVBAR_HTML}
<div class="container">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 40px; align-items: center; margin-top: 50px;">
<img src="{content.get('about_photo', '')}" style="width:100%; border-radius:12px;" alt="About">
<div>
<h1 style="font-size: 42px; margin-bottom: 20px;">{content.get('about_title', '')}</h1>
<p style="font-size: 16px; color: #555; white-space: pre-line;">{content.get('about_text', '')}</p>
</div>
</div>
</div>
<footer>© 2026 Все права защищены.</footer>
</body></html>
"""
return render_template_string(html)
@app.route('/reviews')
def reviews():
conn = get_db()
revs = conn.execute('SELECT * FROM reviews').fetchall()
conn.close()
rev_cards = ""
for r in revs:
photo_tag = f'<img src="{r["photo_url"]}">' if r["photo_url"] else ""
rev_cards += f"""
<div class="card">
{photo_tag}
<p style="font-style: italic; margin-bottom: 10px;">"{r['text']}"</p>
<h4 class="serif" style="text-align: right; color: var(--accent-color);">— {r['author']}</h4>
</div>
"""
html = f"""
<!DOCTYPE html><html><head><title>Отзывы | Фотограф</title>{BASE_CSS}</head><body>
{NAVBAR_HTML}
<div class="container">
<h1 style="text-align: center; margin: 40px 0;" class="serif">Отзывы Клиентов</h1>
<div class="grid">
{rev_cards if rev_cards else '<p style="text-align:center; grid-column:1/-1;">Отзывов пока нет.</p>'}
</div>
</div>
<footer>© 2026 Все права защищены.</footer>
</body></html>
"""
return render_template_string(html)
@app.route('/certificates', methods=['GET', 'POST'])
def certificates():
cert_info = None
if request.method == 'POST':
code = request.form.get('code', '').strip().upper()
conn = get_db()
cert_info = conn.execute('SELECT * FROM certificates WHERE code = ?', (code,)).fetchone()
conn.close()
html = f"""
<!DOCTYPE html><html><head><title>Сертификаты | Фотограф</title>{BASE_CSS}</head><body>
{NAVBAR_HTML}
<div class="container" style="max-width: 600px;">
<h1 style="text-align: center; margin-bottom: 20px;" class="serif">Подарочные Сертификаты</h1>
<p style="text-align: center; margin-bottom: 30px; color: #666;">Проверьте уникальный код вашего сертификата здесь:</p>
<div class="card">
<form method="POST">
<div class="form-group">
<label>Введите номер сертификата:</label>
<input type="text" name="code" placeholder="Например: CERT-A1B2C3" required style="text-transform: uppercase;">
</div>
<button type="submit" class="btn" style="width: 100%;">Проверить Сертификат</button>
</form>
{f'''
<div style="margin-top: 25px; padding: 20px; background: var(--light-accent); border-radius: 6px; text-align: center;">
{"<h3 class='serif' style='color: green;'>Сертификат Действителен!</h3><p><b>Кому:</b> " + cert_info['client_name'] + "</p><p><b>Услуга/Сумма:</b> " + cert_info['amount_or_desc'] + "</p>" if cert_info else "<h3 class='serif' style='color: red;'>Сертификат не найден</h3><p>Проверьте правильность введенного кода.</p>"}
</div>
''' if request.method == 'POST' else ''}
</div>
</div>
<footer>© 2026 Все права защищены.</footer>
</body></html>
"""
return render_template_string(html)
# --- ПЕРСОНАЛЬНАЯ КЛИЕНТСКАЯ ГАЛЕРЕЯ ---
@app.route('/gallery/<unique_id>')
def client_gallery(unique_id):
conn = get_db()
gallery = conn.execute('SELECT * FROM client_galleries WHERE unique_id = ?', (unique_id,)).fetchone()
conn.close()
if not gallery:
return "<h1>Галерея не найдена или ссылка устарела.</h1>", 404
photos = gallery['photos_json'].split(',') if gallery['photos_json'] else []
template = gallery['template']
if template == 'masonry':
gallery_html = '<div class="gallery-masonry">' + "".join([f'<img src="{url}">' for url in photos]) + '</div>'
elif template == 'slideshow':
gallery_html = '<div class="gallery-slideshow">' + "".join([f'<img src="{url}">' for url in photos]) + '</div>'
else:
gallery_html = '<div class="grid">' + "".join([f'<div class="card" style="padding:0; overflow:hidden;"><img src="{url}" style="height:100%; margin:0;"></div>' for url in photos]) + '</div>'
html = f"""
<!DOCTYPE html><html><head><title>Персональная Галерея | {gallery['client_name']}</title>{BASE_CSS}</head><body>
<div class="container">
<div style="text-align: center; margin: 40px 0;">
<p style="text-transform: uppercase; letter-spacing: 2px; color: var(--accent-color); font-size: 12px;">Персональная Галерея</p>
<h1 class="serif" style="font-size: 42px;">{gallery['client_name']}</h1>
</div>
{gallery_html}
</div>
<footer>© 2026 Ваш Персональный Фотограф.</footer>
</body></html>
"""
return render_template_string(html)
# --- ПАНЕЛЬ АДМИНИСТРАТОРА И АВТОРИЗАЦИЯ (СЕКРЕТНЫЙ КЛЮЧ) ---
@app.route('/admin/logout')
def admin_logout():
session.pop('is_admin', None)
return redirect('/admin')
@app.route('/admin', methods=['GET', 'POST'])
def admin_panel():
login_error = False
# Обработка ввода секретного ключа (пароля)
if request.method == 'POST' and request.form.get('action') == 'login':
secret_key_input = request.form.get('secret_key', '').strip()
if secret_key_input == ADMIN_SECRET_KEY:
session['is_admin'] = True
return redirect('/admin')
else:
login_error = True
# Если администратор НЕ авторизован — показываем форму ввода секретного ключа
if not session.get('is_admin'):
login_html = f"""
<!DOCTYPE html><html><head><title>Вход в Админ-панель</title>{BASE_CSS}</head><body>
{NAVBAR_HTML}
<div class="container" style="max-width: 450px; margin-top: 80px;">
<div class="card" style="text-align: center;">
<h2 class="serif" style="margin-bottom: 10px;">Защищенный Доступ</h2>
<p style="color: #666; font-size: 14px; margin-bottom: 20px;">Введите секретный ключ администратора</p>
{"<p style='color: red; font-size: 14px; margin-bottom: 15px;'>Неверный секретный ключ!</p>" if login_error else ""}
<form method="POST">
<input type="hidden" name="action" value="login">
<div class="form-group">
<input type="password" name="secret_key" placeholder="Введите Ваш секретный ключ..." required style="text-align: center;">
</div>
<button type="submit" class="btn" style="width: 100%;">Войти в Панель</button>
</form>
</div>
</div>
<footer>© 2026 Безопасная админ-панель</footer>
</body></html>
"""
return render_template_string(login_html)
# --- ЕСЛИ КЛЮЧ ВВЕДЕН ВЕРНО — ОТКРЫВАЕТСЯ АДМИНКА ---
conn = get_db()
tab = request.args.get('tab', 'main')
if request.method == 'POST' and tab in ['main', 'about']:
for key in request.form:
if key != 'action':
conn.execute('INSERT OR REPLACE INTO content (key, value) VALUES (?, ?)', (key, request.form[key]))
conn.commit()
elif request.method == 'POST' and tab == 'reviews':
author = request.form.get('author')
text = request.form.get('text')
photo_url = request.form.get('photo_url')
conn.execute('INSERT INTO reviews (author, text, photo_url) VALUES (?, ?, ?)', (author, text, photo_url))
conn.commit()
elif request.method == 'POST' and tab == 'certificates':
client_name = request.form.get('client_name')
desc = request.form.get('desc')
code = f"CERT-{str(uuid.uuid4())[:6].upper()}"
conn.execute('INSERT INTO certificates (code, client_name, amount_or_desc) VALUES (?, ?, ?)', (code, client_name, desc))
conn.commit()
elif request.method == 'POST' and tab == 'create_gallery':
client_name = request.form.get('client_name')
template = request.form.get('template')
selected_photos = request.form.getlist('photos')
unique_id = str(uuid.uuid4())[:12]
photos_str = ",".join(selected_photos)
conn.execute('INSERT INTO client_galleries (unique_id, client_name, template, photos_json) VALUES (?, ?, ?, ?)',
(unique_id, client_name, template, photos_str))
conn.commit()
content = dict(conn.execute('SELECT * FROM content').fetchall())
reviews_list = conn.execute('SELECT * FROM reviews').fetchall()
certs_list = conn.execute('SELECT * FROM certificates').fetchall()
yandex_photos = conn.execute('SELECT * FROM yandex_photos').fetchall()
galleries_list = conn.execute('SELECT * FROM client_galleries').fetchall()
conn.close()
tab_content = ""
if tab == 'main':
tab_content = f"""
<h2 class="serif">Редактирование Главной Страницы</h2><br>
<form method="POST">
<div class="form-group"><label>Заголовок:</label><input type="text" name="main_title" value="{content.get('main_title', '')}"></div>
<div class="form-group"><label>Текст на главной:</label><textarea name="main_text">{content.get('main_text', '')}</textarea></div>
<div class="form-group"><label>URL главного фото:</label><input type="text" name="main_photo" value="{content.get('main_photo', '')}"></div>
<button class="btn" type="submit">Сохранить</button>
</form>
"""
elif tab == 'about':
tab_content = f"""
<h2 class="serif">Редактирование 'Обо мне'</h2><br>
<form method="POST">
<div class="form-group"><label>Заголовок:</label><input type="text" name="about_title" value="{content.get('about_title', '')}"></div>
<div class="form-group"><label>Текст Обо мне:</label><textarea name="about_text">{content.get('about_text', '')}</textarea></div>
<div class="form-group"><label>URL Вашего Фото:</label><input type="text" name="about_photo" value="{content.get('about_photo', '')}"></div>
<button class="btn" type="submit">Сохранить</button>
</form>
"""
elif tab == 'reviews':
tab_content = f"""
<h2 class="serif">Управление Отзывами</h2><br>
<form method="POST">
<div class="form-group"><label>Имя клиента:</label><input type="text" name="author" required></div>
<div class="form-group"><label>Текст отзыва: