Добавлен веб-личный кабинет: посты, контент-план, медиа-библиотека и генерация картинок
This commit is contained in:
1 parent
d8c086d3b4
commit
81625b2e6f
35 files changed
+2220
-64
No files matched your search
@@ -0,0 +1,114 @@
|
||||
<!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="/static/style.css?v=2">
|
||||
<script>
|
||||
(function () {
|
||||
function isDark() {
|
||||
return document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
}
|
||||
function setIcon(btn) {
|
||||
if (btn) btn.textContent = isDark() ? '☀️' : '🌙';
|
||||
}
|
||||
try {
|
||||
var saved = localStorage.getItem('theme');
|
||||
// Тёмная тема по умолчанию: dark включается всегда, кроме явного выбора 'light'
|
||||
if (saved !== 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
}
|
||||
} catch (e) {}
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('[data-theme-toggle]');
|
||||
if (!btn) return;
|
||||
if (isDark()) {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
try { localStorage.setItem('theme', 'light'); } catch (e) {}
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
try { localStorage.setItem('theme', 'dark'); } catch (e) {}
|
||||
}
|
||||
setIcon(btn);
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
setIcon(document.querySelector('[data-theme-toggle]'));
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="topbar">
|
||||
<a class="brand" href="{{ '/dashboard' if user else '/login' }}">🗂 Личный кабинет</a>
|
||||
<nav>
|
||||
<button type="button" class="theme-toggle" data-theme-toggle aria-label="Переключить тему">🌙</button>
|
||||
{% if user %}
|
||||
<a href="/dashboard">Главная</a>
|
||||
<a href="/generate">Пост</a>
|
||||
<a href="/plan">Контент-план</a>
|
||||
<a href="/history">История</a>
|
||||
<a href="/images">Картинки</a>
|
||||
<a href="/stats">Статистика</a>
|
||||
<a href="/profile/edit">Профиль</a>
|
||||
<form action="/logout" method="post" class="inline-form">
|
||||
<button type="submit" class="link-button">Выйти</button>
|
||||
</form>
|
||||
</nav>
|
||||
{% else %}
|
||||
<a href="/login">Вход</a>
|
||||
<a href="/register">Регистрация</a>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
{% if flash_message %}
|
||||
<div class="flash">{{ flash_message }}</div>
|
||||
{% endif %}
|
||||
{% if errors %}
|
||||
<div class="errors">
|
||||
<ul>
|
||||
{% for error in errors %}
|
||||
<li>{{ error }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<script>
|
||||
function copyText(text, btn) {
|
||||
function done(ok) {
|
||||
if (!btn) return;
|
||||
var old = btn.textContent;
|
||||
btn.textContent = ok ? '✅ Скопировано' : '❌ Ошибка';
|
||||
setTimeout(function () { btn.textContent = old; }, 1500);
|
||||
}
|
||||
function fallback() {
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed';
|
||||
ta.style.opacity = '0';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
var ok = false;
|
||||
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
|
||||
document.body.removeChild(ta);
|
||||
done(ok);
|
||||
}
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(function () { done(true); }, fallback);
|
||||
} else {
|
||||
fallback();
|
||||
}
|
||||
}
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('[data-copy-target]');
|
||||
if (!btn) return;
|
||||
var el = document.getElementById(btn.getAttribute('data-copy-target'));
|
||||
if (el) copyText(el.textContent, btn);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Главная — Личный кабинет{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Привет, {{ user.name }}!</h1>
|
||||
<div class="card">
|
||||
<p><strong>Телефон:</strong> {{ user.phone }}</p>
|
||||
<p><strong>Вид бизнеса:</strong> {{ business_name }}</p>
|
||||
<p><strong>Описание:</strong> {{ user.description }}</p>
|
||||
<p><strong>В кабинете с:</strong> {{ user.created_at }}</p>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<a class="tile" href="/generate">💡 Сгенерировать пост</a>
|
||||
<a class="tile" href="/plan">📅 Контент-план</a>
|
||||
<a class="tile" href="/history">📜 История постов</a>
|
||||
<a class="tile" href="/images">🖼 Медиа-библиотека</a>
|
||||
<a class="tile" href="/stats">📈 Статистика</a>
|
||||
<a class="tile" href="/profile/edit">✏️ Редактировать профиль</a>
|
||||
<a class="tile tile-danger" href="/delete-account">🗑 Удалить аккаунт</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Удаление аккаунта — Личный кабинет{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Удалить аккаунт?</h1>
|
||||
<p>Это безвозвратно удалит твой профиль и всю историю постов.</p>
|
||||
<form method="post" action="/delete-account" class="form">
|
||||
<input type="hidden" name="confirm" value="yes">
|
||||
<button type="submit" class="button-danger">Да, удалить аккаунт</button>
|
||||
</form>
|
||||
<p><a href="/dashboard">Отмена, вернуться в кабинет</a></p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Сгенерировать пост — Личный кабинет{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Пост в своём стиле</h1>
|
||||
<form method="post" action="/generate" class="form">
|
||||
<label>Платформа
|
||||
<select name="platform" required>
|
||||
{% for key, label in platforms %}
|
||||
<option value="{{ key }}" {% if form and form.platform == key %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>Стиль (например: дружелюбный, с юмором, экспертный)
|
||||
<input type="text" name="style" value="{{ form.style if form else '' }}" required>
|
||||
</label>
|
||||
<label>Пожелания к посту
|
||||
<textarea name="wishes" rows="3">{{ form.wishes if form else '' }}</textarea>
|
||||
</label>
|
||||
<button type="submit">Сгенерировать</button>
|
||||
</form>
|
||||
{% if result %}
|
||||
<h2>Готовый пост</h2>
|
||||
<div class="card result">{{ result }}</div>
|
||||
{% endif %}
|
||||
<p><a href="/dashboard">← Назад в кабинет</a></p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}История постов — Личный кабинет{% endblock %}
|
||||
{% block content %}
|
||||
<h1>История постов</h1>
|
||||
{% if posts %}
|
||||
<ul class="post-list">
|
||||
{% for post in posts %}
|
||||
<li class="card">
|
||||
<div class="post-meta">{{ platform_label(post.platform) }} · {{ post.created_at }}</div>
|
||||
<div class="post-content" id="post-content-{{ post.id }}">{{ post.content }}</div>
|
||||
<div class="post-actions">
|
||||
<button type="button" class="copy-btn" data-copy-target="post-content-{{ post.id }}">📋 Копировать</button>
|
||||
<a class="button-link" href="/posts/{{ post.id }}">👁 Открыть</a>
|
||||
<a class="button-link" href="/posts/{{ post.id }}/edit">✏️ Изменить</a>
|
||||
<form method="post" action="/posts/{{ post.id }}/delete" class="inline-form"
|
||||
onsubmit="return confirm('Удалить этот пост? Действие необратимо.');">
|
||||
<button type="submit" class="button-link button-link-danger">🗑 Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="card">
|
||||
<p>Постов пока нет.</p>
|
||||
<p><a href="/generate">💡 Сгенерировать первый пост</a></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<p><a href="/dashboard">← Назад в кабинет</a></p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Сгенерировать картинку — Личный кабинет{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Генерация картинки</h1>
|
||||
<form method="post" action="/images/generate" class="form">
|
||||
<label>Что нарисовать?
|
||||
<textarea name="prompt" rows="4" required placeholder="Например: современный кофейный стакан на белом фоне, логотип для кофейни">{{ form.prompt if form else '' }}</textarea>
|
||||
</label>
|
||||
<label>Размер
|
||||
<select name="size" required>
|
||||
{% for key, (value, label) in sizes.items() %}
|
||||
<option value="{{ key }}" {% if form and form.size == key %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>Стиль
|
||||
<select name="style">
|
||||
{% for key, (label, suffix) in styles.items() %}
|
||||
<option value="{{ key }}" {% if form and form.style == key %}selected{% endif %}>{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">🎨 Сгенерировать</button>
|
||||
<p class="form-hint">Генерация занимает 10–60 секунд. Картинка сохранится в медиа-библиотеку.</p>
|
||||
</form>
|
||||
<p><a href="/images">← Назад в библиотеку</a></p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Картинки — Личный кабинет{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Медиа-библиотека</h1>
|
||||
<p><a class="copy-btn" href="/images/generate">🎨 Сгенерировать картинку</a></p>
|
||||
{% if images %}
|
||||
<div class="media-grid">
|
||||
{% for img in images %}
|
||||
<div class="media-card">
|
||||
<a href="/media/{{ img.filename }}" target="_blank" title="Открыть в полном размере">
|
||||
<img class="media-thumb" src="/media/{{ img.filename }}" alt="{{ img.prompt }}" loading="lazy">
|
||||
</a>
|
||||
<div class="media-body">
|
||||
<div class="media-prompt" title="{{ img.prompt }}">{{ img.prompt }}</div>
|
||||
<div class="post-meta">{{ img.created_at }}</div>
|
||||
<div class="post-actions">
|
||||
<a class="copy-btn" href="/media/{{ img.filename }}" download>⬇ Скачать</a>
|
||||
<form method="post" action="/images/{{ img.id }}/delete" class="inline-form"
|
||||
onsubmit="return confirm('Удалить эту картинку?');">
|
||||
<button type="submit" class="button-link button-link-danger">🗑 Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card">
|
||||
<p>Картинок пока нет.</p>
|
||||
<p><a href="/images/generate">🎨 Сгенерировать первую картинку</a></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<p><a href="/dashboard">← Назад в кабинет</a></p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Вход — Личный кабинет{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Вход</h1>
|
||||
<form method="post" action="/login" class="form">
|
||||
<label>Email
|
||||
<input type="email" name="email" value="{{ form.email if form else '' }}" required>
|
||||
</label>
|
||||
<label>Пароль
|
||||
<input type="password" name="password" required>
|
||||
</label>
|
||||
<button type="submit">Войти</button>
|
||||
</form>
|
||||
<p>Ещё нет аккаунта? <a href="/register">Зарегистрироваться</a></p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,58 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Контент-план — Личный кабинет{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Контент-план</h1>
|
||||
|
||||
{% if plan %}
|
||||
<div class="chips">
|
||||
<span class="chip chip-planned">📝 В плане: {{ status_counts.get('planned', 0) }}</span>
|
||||
<span class="chip chip-published">✅ Опубликовано: {{ status_counts.get('published', 0) }}</span>
|
||||
<span class="chip chip-postponed">🕓 Отложено: {{ status_counts.get('postponed', 0) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="plan-list">
|
||||
{% for item in plan %}
|
||||
<div class="card plan-item status-{{ item.status }}">
|
||||
<div class="plan-item-head">
|
||||
<span class="plan-day">День {{ item.day }}</span>
|
||||
<span class="plan-platform">{{ platform_label(item.platform) }}</span>
|
||||
<span class="status-badge status-{{ item.status }}">
|
||||
{% if item.status == 'published' %}✅ Опубликовано
|
||||
{% elif item.status == 'postponed' %}🕓 Отложено
|
||||
{% else %}📝 В плане{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="plan-idea">{{ item.idea }}</div>
|
||||
<div class="post-actions">
|
||||
<form method="post" action="/plan/{{ item.id }}/status" class="inline-form">
|
||||
<select name="status" class="status-select">
|
||||
<option value="planned" {% if item.status == 'planned' %}selected{% endif %}>📝 В плане</option>
|
||||
<option value="published" {% if item.status == 'published' %}selected{% endif %}>✅ Опубликовано</option>
|
||||
<option value="postponed" {% if item.status == 'postponed' %}selected{% endif %}>🕓 Отложено</option>
|
||||
</select>
|
||||
<button type="submit" class="copy-btn">Сохранить</button>
|
||||
</form>
|
||||
<form method="post" action="/plan/{{ item.id }}/delete" class="inline-form"
|
||||
onsubmit="return confirm('Удалить этот пункт плана?');">
|
||||
<button type="submit" class="button-link button-link-danger">🗑 Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card">
|
||||
<p>Контент-плана пока нет. Составь его на 1–14 дней — он будет сохранён, и ты сможешь отмечать опубликованные и отложенные посты.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h2>{% if plan %}Пересоставить план{% else %}Составить план{% endif %}</h2>
|
||||
<form method="post" action="/plan" class="form"
|
||||
onsubmit="{% if plan %}return confirm('Новый план заменит текущий (отметки статусов сбросятся). Продолжить?');{% endif %}">
|
||||
<label>На сколько дней (1-14)
|
||||
<input type="number" name="days" min="1" max="14" value="7" required>
|
||||
</label>
|
||||
<button type="submit">📅 Составить план</button>
|
||||
</form>
|
||||
<p><a href="/dashboard">← Назад в кабинет</a></p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Пост — Личный кабинет{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Пост</h1>
|
||||
<div class="card">
|
||||
<div class="post-meta">
|
||||
{{ platform_label(post.platform) }} · {{ post.created_at }}
|
||||
{% if post.style %} · Стиль: {{ post.style }}{% endif %}
|
||||
</div>
|
||||
<div class="post-content" id="post-content-{{ post.id }}">{{ post.content }}</div>
|
||||
{% if post.wishes %}
|
||||
<div class="post-notes"><strong>Пожелания:</strong> {{ post.wishes }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="post-actions">
|
||||
<button type="button" class="copy-btn" data-copy-target="post-content-{{ post.id }}">📋 Копировать</button>
|
||||
<a class="button-link" href="/posts/{{ post.id }}/edit">✏️ Изменить</a>
|
||||
<form method="post" action="/posts/{{ post.id }}/delete" class="inline-form"
|
||||
onsubmit="return confirm('Удалить этот пост? Действие необратимо.');">
|
||||
<button type="submit" class="button-link button-link-danger">🗑 Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
<p><a href="/history">← Назад к истории</a></p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Изменить пост — Личный кабинет{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Изменить пост</h1>
|
||||
<div class="card">
|
||||
<p class="post-meta">{{ platform_label(post.platform) }} · {{ post.created_at }}</p>
|
||||
<form method="post" action="/posts/{{ post.id }}/edit" class="form">
|
||||
<label>Текст поста
|
||||
<textarea name="content" rows="12" required>{{ post.content }}</textarea>
|
||||
</label>
|
||||
<div class="form-actions">
|
||||
<button type="submit">💾 Сохранить</button>
|
||||
<a class="button-link" href="/posts/{{ post.id }}">Отмена</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<p><a href="/history">← Назад к истории</a></p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Профиль — Личный кабинет{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Редактировать профиль</h1>
|
||||
<form method="post" action="/profile/edit" class="form">
|
||||
<label>Имя
|
||||
<input type="text" name="name" value="{{ user.name }}" required>
|
||||
</label>
|
||||
<label>Телефон
|
||||
<input type="text" name="phone" value="{{ user.phone }}" required>
|
||||
</label>
|
||||
<label>Вид бизнеса
|
||||
<select name="business_type" required>
|
||||
{% for key, value in business_types.items() %}
|
||||
<option value="{{ key }}" {% if key == user.business_type|string %}selected{% endif %}>{{ value.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>Описание бизнеса
|
||||
<textarea name="description" rows="4" required>{{ user.description }}</textarea>
|
||||
</label>
|
||||
<button type="submit">Сохранить</button>
|
||||
</form>
|
||||
<p><a href="/dashboard">← Назад в кабинет</a></p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Регистрация — Личный кабинет{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Регистрация</h1>
|
||||
<form method="post" action="/register" class="form">
|
||||
<label>Имя
|
||||
<input type="text" name="name" value="{{ form.name if form else '' }}" required>
|
||||
</label>
|
||||
<label>Телефон
|
||||
<input type="text" name="phone" value="{{ form.phone if form else '' }}" required>
|
||||
</label>
|
||||
<label>Вид бизнеса
|
||||
<select name="business_type" required>
|
||||
<option value="" disabled {% if not form %}selected{% endif %}>Выбери вид бизнеса</option>
|
||||
{% for key, value in business_types.items() %}
|
||||
<option value="{{ key }}" {% if form and form.business_type == key %}selected{% endif %}>{{ value.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label>Описание бизнеса
|
||||
<textarea name="description" rows="4" required>{{ form.description if form else '' }}</textarea>
|
||||
</label>
|
||||
<label>Email
|
||||
<input type="email" name="email" value="{{ form.email if form else '' }}" required>
|
||||
</label>
|
||||
<label>Пароль
|
||||
<input type="password" name="password" minlength="8" required>
|
||||
</label>
|
||||
<label>Повтори пароль
|
||||
<input type="password" name="password_confirm" minlength="8" required>
|
||||
</label>
|
||||
<button type="submit">Зарегистрироваться</button>
|
||||
</form>
|
||||
<p>Уже есть аккаунт? <a href="/login">Войти</a></p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Статистика — Личный кабинет{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Статистика использования</h1>
|
||||
<div class="card">
|
||||
<p><strong>В кабинете с:</strong> {{ stats.created_at }}</p>
|
||||
<p><strong>Всего постов:</strong> {{ stats.total_posts }}</p>
|
||||
</div>
|
||||
<h2>По платформам</h2>
|
||||
{% if stats.by_platform %}
|
||||
<ul>
|
||||
{% for platform, count in stats.by_platform.items() %}
|
||||
<li>{{ platform }}: {{ count }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>Пока нет постов.</p>
|
||||
{% endif %}
|
||||
<p><a href="/dashboard">← Назад в кабинет</a></p>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user