Add user auth, personal cabinet, admin panel and first admin bootstrap
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
<nav class="admin-nav">
|
||||
<a href="{{ url_for('admin.dashboard') }}" class="admin-nav__link {% if request.endpoint == 'admin.dashboard' %}admin-nav__link--active{% endif %}">Обзор</a>
|
||||
<a href="{{ url_for('admin.users') }}" class="admin-nav__link {% if request.endpoint == 'admin.users' %}admin-nav__link--active{% endif %}">Пользователи</a>
|
||||
<a href="{{ url_for('admin.photos') }}" class="admin-nav__link {% if request.endpoint == 'admin.photos' %}admin-nav__link--active{% endif %}">Фото</a>
|
||||
</nav>
|
||||
@@ -0,0 +1,80 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "macros.html" import format_size %}
|
||||
|
||||
{% block title %}Админка — PhotoHost{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-header page-header--admin">
|
||||
<div class="container">
|
||||
<h1 class="page-header__title">Панель администратора</h1>
|
||||
<p class="page-header__subtitle">Управление пользователями и контентом</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="admin-section">
|
||||
<div class="container">
|
||||
{% include "admin/_nav.html" %}
|
||||
{% include "partials/alerts.html" %}
|
||||
|
||||
<div class="admin-stats">
|
||||
<div class="stat-card stat-card--admin">
|
||||
<span class="stat-card__value">{{ stats.users }}</span>
|
||||
<span class="stat-card__label">пользователей</span>
|
||||
</div>
|
||||
<div class="stat-card stat-card--admin">
|
||||
<span class="stat-card__value">{{ stats.photos }}</span>
|
||||
<span class="stat-card__label">фотографий</span>
|
||||
</div>
|
||||
<div class="stat-card stat-card--admin">
|
||||
<span class="stat-card__value">{{ stats.admins }}</span>
|
||||
<span class="stat-card__label">администраторов</span>
|
||||
</div>
|
||||
<div class="stat-card stat-card--admin">
|
||||
<span class="stat-card__value">{{ format_size(stats.storage) }}</span>
|
||||
<span class="stat-card__label">хранилище</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-grid">
|
||||
<div class="admin-panel">
|
||||
<h2 class="admin-panel__title">Новые пользователи</h2>
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Логин</th>
|
||||
<th>Email</th>
|
||||
<th>Дата</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in recent_users %}
|
||||
<tr>
|
||||
<td>{{ user.username }}{% if user.is_admin %} <span class="badge badge--admin">admin</span>{% endif %}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.created_at.strftime('%d.%m.%Y') }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3">Нет пользователей</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="admin-panel">
|
||||
<h2 class="admin-panel__title">Последние фото</h2>
|
||||
<div class="admin-mini-gallery">
|
||||
{% for photo in recent_photos %}
|
||||
<a href="{{ photo.url }}" target="_blank" class="admin-mini-gallery__item">
|
||||
<img src="{{ photo.url }}" alt="{{ photo.original_name }}">
|
||||
</a>
|
||||
{% else %}
|
||||
<p class="admin-empty">Нет фотографий</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,22 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Фото — Админка{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-header page-header--admin">
|
||||
<div class="container">
|
||||
<h1 class="page-header__title">Все фотографии</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="admin-section">
|
||||
<div class="container">
|
||||
{% include "admin/_nav.html" %}
|
||||
{% include "partials/alerts.html" %}
|
||||
|
||||
{% with photos=photos, show_owner=true, delete_mode='admin', empty_title='Нет фотографий', empty_text='Пользователи ещё не загружали фото' %}
|
||||
{% include "partials/photo_gallery.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,79 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Пользователи — Админка{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-header page-header--admin">
|
||||
<div class="container">
|
||||
<h1 class="page-header__title">Пользователи</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="admin-section">
|
||||
<div class="container">
|
||||
{% include "admin/_nav.html" %}
|
||||
{% include "partials/alerts.html" %}
|
||||
|
||||
<div class="admin-table-wrap">
|
||||
<table class="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Логин</th>
|
||||
<th>Email</th>
|
||||
<th>Фото</th>
|
||||
<th>Роль</th>
|
||||
<th>Статус</th>
|
||||
<th>Дата</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ user.photo_count }}</td>
|
||||
<td>
|
||||
{% if user.is_admin %}
|
||||
<span class="badge badge--admin">Админ</span>
|
||||
{% else %}
|
||||
<span class="badge">User</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if user.is_active %}
|
||||
<span class="badge badge--success">Активен</span>
|
||||
{% else %}
|
||||
<span class="badge badge--danger">Заблокирован</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ user.created_at.strftime('%d.%m.%Y') }}</td>
|
||||
<td class="admin-actions">
|
||||
{% if user.id != current_user.id %}
|
||||
<form action="{{ url_for('admin.toggle_admin', user_id=user.id) }}" method="post">
|
||||
<button type="submit" class="btn btn--ghost btn--sm">
|
||||
{% if user.is_admin %}Снять admin{% else %}Сделать admin{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ url_for('admin.toggle_active', user_id=user.id) }}" method="post">
|
||||
<button type="submit" class="btn btn--ghost btn--sm">
|
||||
{% if user.is_active %}Блок{% else %}Разблок{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
<form action="{{ url_for('admin.delete_user', user_id=user.id) }}" method="post" onsubmit="return confirm('Удалить пользователя и все его фото?');">
|
||||
<button type="submit" class="btn btn--danger btn--sm">Удалить</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="text-muted">Вы</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Вход — PhotoHost{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="auth-section">
|
||||
<div class="container auth-container">
|
||||
<div class="auth-card">
|
||||
<h1 class="auth-card__title">Вход</h1>
|
||||
<p class="auth-card__subtitle">Войдите в аккаунт для загрузки фото</p>
|
||||
|
||||
{% include "partials/alerts.html" %}
|
||||
|
||||
<form method="post" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="login">Логин или email</label>
|
||||
<input type="text" id="login" name="login" required autocomplete="username" placeholder="username или email@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Пароль</label>
|
||||
<input type="password" id="password" name="password" required autocomplete="current-password" placeholder="••••••••">
|
||||
</div>
|
||||
<label class="form-checkbox">
|
||||
<input type="checkbox" name="remember">
|
||||
<span>Запомнить меня</span>
|
||||
</label>
|
||||
<button type="submit" class="btn btn--primary btn--full">Войти</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-card__footer">
|
||||
Нет аккаунта? <a href="{{ url_for('auth.register') }}">Зарегистрироваться</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Регистрация — PhotoHost{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="auth-section">
|
||||
<div class="container auth-container">
|
||||
<div class="auth-card">
|
||||
<h1 class="auth-card__title">Регистрация</h1>
|
||||
<p class="auth-card__subtitle">Создайте аккаунт для загрузки и управления фото</p>
|
||||
|
||||
{% include "partials/alerts.html" %}
|
||||
|
||||
<form method="post" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="username">Имя пользователя</label>
|
||||
<input type="text" id="username" name="username" required minlength="3" autocomplete="username" placeholder="min 3 символа">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" required autocomplete="email" placeholder="you@example.com">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Пароль</label>
|
||||
<input type="password" id="password" name="password" required minlength="6" autocomplete="new-password" placeholder="min 6 символов">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password2">Подтверждение пароля</label>
|
||||
<input type="password" id="password2" name="password2" required minlength="6" autocomplete="new-password" placeholder="повторите пароль">
|
||||
</div>
|
||||
<button type="submit" class="btn btn--primary btn--full">Создать аккаунт</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-card__footer">
|
||||
Уже есть аккаунт? <a href="{{ url_for('auth.login') }}">Войти</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
+12
-2
@@ -21,8 +21,18 @@
|
||||
<span class="logo__text">PhotoHost</span>
|
||||
</a>
|
||||
<nav class="nav">
|
||||
<a href="#upload" class="nav__link">Загрузить</a>
|
||||
<a href="#gallery" class="nav__link">Галерея</a>
|
||||
<a href="{{ url_for('main.index') }}" class="nav__link">Главная</a>
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('cabinet.index') }}" class="nav__link">Личный кабинет</a>
|
||||
{% if current_user.is_admin %}
|
||||
<a href="{{ url_for('admin.dashboard') }}" class="nav__link nav__link--admin">Админка</a>
|
||||
{% endif %}
|
||||
<span class="nav__user">{{ current_user.username }}</span>
|
||||
<a href="{{ url_for('auth.logout') }}" class="nav__link">Выйти</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('auth.login') }}" class="nav__link">Вход</a>
|
||||
<a href="{{ url_for('auth.register') }}" class="nav__link nav__link--accent">Регистрация</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "macros.html" import format_size %}
|
||||
|
||||
{% block title %}Личный кабинет — PhotoHost{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-header">
|
||||
<div class="container">
|
||||
<h1 class="page-header__title">Личный кабинет</h1>
|
||||
<p class="page-header__subtitle">Привет, {{ current_user.username }}! Управляйте своими фотографиями.</p>
|
||||
<div class="page-header__actions">
|
||||
<a href="{{ url_for('cabinet.profile') }}" class="btn btn--ghost">Настройки профиля</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% include "partials/alerts.html" %}
|
||||
|
||||
<section class="stats-bar">
|
||||
<div class="container stats">
|
||||
<div class="stat-card">
|
||||
<span class="stat-card__value">{{ total_photos }}</span>
|
||||
<span class="stat-card__label">ваших фото</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-card__value">{{ format_size(total_size) }}</span>
|
||||
<span class="stat-card__label">занято места</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-card__value">до {{ max_upload_mb }} МБ</span>
|
||||
<span class="stat-card__label">на файл</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="upload" class="upload-section">
|
||||
<div class="container">
|
||||
<h2 class="section-title">Загрузить фото</h2>
|
||||
<form action="{{ url_for('main.upload') }}" method="post" enctype="multipart/form-data" class="upload-form" id="uploadForm">
|
||||
<div class="dropzone" id="dropzone">
|
||||
<input type="file" name="photo" id="photoInput" accept="image/png,image/jpeg,image/gif,image/webp,image/bmp" hidden>
|
||||
<div class="dropzone__icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M12 16V4m0 0L8 8m4-4l4 4"/>
|
||||
<path d="M20 16.5v1a2.5 2.5 0 01-2.5 2.5h-11A2.5 2.5 0 014 17.5v-1"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="dropzone__title">Перетащите фото сюда</p>
|
||||
<p class="dropzone__hint">или нажмите для выбора файла</p>
|
||||
<div class="dropzone__preview" id="preview" hidden>
|
||||
<img id="previewImg" alt="Предпросмотр">
|
||||
<span id="previewName"></span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn--primary" id="submitBtn" disabled>Загрузить</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="gallery-section">
|
||||
<div class="container">
|
||||
<div class="gallery-header">
|
||||
<h2 class="section-title">Мои фото</h2>
|
||||
<span class="gallery-count">{{ total_photos }} фото</span>
|
||||
</div>
|
||||
{% with photos=photos, empty_title='У вас пока нет фото', empty_text='Загрузите первое изображение выше' %}
|
||||
{% include "partials/photo_gallery.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,59 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Профиль — PhotoHost{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-header">
|
||||
<div class="container">
|
||||
<h1 class="page-header__title">Настройки профиля</h1>
|
||||
<p class="page-header__subtitle">Измените email или пароль</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="auth-section">
|
||||
<div class="container auth-container">
|
||||
<div class="auth-card auth-card--wide">
|
||||
{% include "partials/alerts.html" %}
|
||||
|
||||
<div class="profile-info">
|
||||
<div class="profile-info__row">
|
||||
<span>Имя пользователя</span>
|
||||
<strong>{{ current_user.username }}</strong>
|
||||
</div>
|
||||
<div class="profile-info__row">
|
||||
<span>Роль</span>
|
||||
<strong>{% if current_user.is_admin %}Администратор{% else %}Пользователь{% endif %}</strong>
|
||||
</div>
|
||||
<div class="profile-info__row">
|
||||
<span>Дата регистрации</span>
|
||||
<strong>{{ current_user.created_at.strftime('%d.%m.%Y') }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" required value="{{ current_user.email }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="current_password">Текущий пароль</label>
|
||||
<input type="password" id="current_password" name="current_password" required placeholder="для подтверждения изменений">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new_password">Новый пароль (необязательно)</label>
|
||||
<input type="password" id="new_password" name="new_password" minlength="6" placeholder="оставьте пустым, если не меняете">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new_password2">Подтверждение нового пароля</label>
|
||||
<input type="password" id="new_password2" name="new_password2" minlength="6">
|
||||
</div>
|
||||
<button type="submit" class="btn btn--primary btn--full">Сохранить</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-card__footer">
|
||||
<a href="{{ url_for('cabinet.index') }}">← Вернуться в кабинет</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
+21
-66
@@ -1,29 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% macro format_size(bytes) %}
|
||||
{% set size = bytes|float %}
|
||||
{% if size < 1024 %}
|
||||
{{ size|int }} Б
|
||||
{% elif size < 1048576 %}
|
||||
{{ "%.1f"|format(size / 1024) }} КБ
|
||||
{% elif size < 1073741824 %}
|
||||
{{ "%.1f"|format(size / 1048576) }} МБ
|
||||
{% else %}
|
||||
{{ "%.1f"|format(size / 1073741824) }} ГБ
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
{% from "macros.html" import format_size %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero">
|
||||
<div class="container hero__inner">
|
||||
<div class="hero__badge">Бесплатно · Без регистрации</div>
|
||||
<div class="hero__badge">Регистрация · Личный кабинет · Админка</div>
|
||||
<h1 class="hero__title">
|
||||
Загружайте фото<br>
|
||||
<span class="hero__accent">мгновенно</span>
|
||||
</h1>
|
||||
<p class="hero__subtitle">
|
||||
Современный фото-хостинг на Python и PostgreSQL.
|
||||
Перетащите изображение — получите прямую ссылку за секунды.
|
||||
{% if current_user.is_authenticated %}
|
||||
Загружайте изображения в личном кабинете и делитесь ссылками.
|
||||
{% else %}
|
||||
Зарегистрируйтесь, чтобы загружать фото и управлять галереей.
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="stats">
|
||||
<div class="stat-card">
|
||||
@@ -39,19 +31,18 @@
|
||||
<span class="stat-card__label">на файл</span>
|
||||
</div>
|
||||
</div>
|
||||
{% if not current_user.is_authenticated %}
|
||||
<div class="hero__actions">
|
||||
<a href="{{ url_for('auth.register') }}" class="btn btn--primary">Создать аккаунт</a>
|
||||
<a href="{{ url_for('auth.login') }}" class="btn btn--ghost">Войти</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<section class="container alerts">
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert--{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% include "partials/alerts.html" %}
|
||||
|
||||
{% if current_user.is_authenticated %}
|
||||
<section id="upload" class="upload-section">
|
||||
<div class="container">
|
||||
<h2 class="section-title">Загрузить фото</h2>
|
||||
@@ -81,53 +72,17 @@
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section id="gallery" class="gallery-section">
|
||||
<div class="container">
|
||||
<div class="gallery-header">
|
||||
<h2 class="section-title">Галерея</h2>
|
||||
<span class="gallery-count">{{ total_photos }} {{ 'фото' if total_photos != 1 else 'фото' }}</span>
|
||||
<h2 class="section-title">Последние фото</h2>
|
||||
<span class="gallery-count">{{ total_photos }} фото</span>
|
||||
</div>
|
||||
|
||||
{% if photos %}
|
||||
<div class="gallery">
|
||||
{% for photo in photos %}
|
||||
<article class="photo-card" data-id="{{ photo.id }}">
|
||||
<div class="photo-card__image-wrap">
|
||||
<img
|
||||
src="{{ photo.url }}"
|
||||
alt="{{ photo.original_name }}"
|
||||
class="photo-card__image"
|
||||
loading="lazy"
|
||||
>
|
||||
<div class="photo-card__overlay">
|
||||
<button type="button" class="btn btn--ghost btn--sm copy-btn" data-url="{{ request.url_root.rstrip('/') }}{{ photo.url }}">
|
||||
Копировать ссылку
|
||||
</button>
|
||||
<a href="{{ photo.url }}" target="_blank" class="btn btn--ghost btn--sm">Открыть</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="photo-card__info">
|
||||
<span class="photo-card__name" title="{{ photo.original_name }}">{{ photo.original_name }}</span>
|
||||
<div class="photo-card__meta">
|
||||
<span>{{ photo.size_human }}</span>
|
||||
<span>{{ photo.created_at.strftime('%d.%m.%Y %H:%M') }}</span>
|
||||
</div>
|
||||
<form action="{{ url_for('main.delete_photo', photo_id=photo.id) }}" method="post" class="photo-card__delete" onsubmit="return confirm('Удалить это фото?');">
|
||||
<button type="submit" class="btn btn--danger btn--sm">Удалить</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state__icon">🖼️</div>
|
||||
<h3>Пока нет фотографий</h3>
|
||||
<p>Загрузите первое изображение — оно появится здесь</p>
|
||||
<a href="#upload" class="btn btn--primary">Загрузить фото</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% with photos=photos, show_owner=true, empty_title='Пока нет фотографий', empty_text='Будьте первым — зарегистрируйтесь и загрузите фото', empty_link=url_for('auth.register') if not current_user.is_authenticated else url_for('cabinet.index'), empty_link_text='Загрузить фото' %}
|
||||
{% include "partials/photo_gallery.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{% macro format_size(bytes) %}
|
||||
{% set size = bytes|float %}
|
||||
{% if size < 1024 %}
|
||||
{{ size|int }} Б
|
||||
{% elif size < 1048576 %}
|
||||
{{ "%.1f"|format(size / 1024) }} КБ
|
||||
{% elif size < 1073741824 %}
|
||||
{{ "%.1f"|format(size / 1048576) }} МБ
|
||||
{% else %}
|
||||
{{ "%.1f"|format(size / 1073741824) }} ГБ
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
@@ -0,0 +1,9 @@
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<section class="container alerts">
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert--{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
@@ -0,0 +1,50 @@
|
||||
{% if photos %}
|
||||
<div class="gallery">
|
||||
{% for photo in photos %}
|
||||
<article class="photo-card" data-id="{{ photo.id }}">
|
||||
<div class="photo-card__image-wrap">
|
||||
<img
|
||||
src="{{ photo.url }}"
|
||||
alt="{{ photo.original_name }}"
|
||||
class="photo-card__image"
|
||||
loading="lazy"
|
||||
>
|
||||
<div class="photo-card__overlay">
|
||||
<button type="button" class="btn btn--ghost btn--sm copy-btn" data-url="{{ request.url_root.rstrip('/') }}{{ photo.url }}">
|
||||
Копировать ссылку
|
||||
</button>
|
||||
<a href="{{ photo.url }}" target="_blank" class="btn btn--ghost btn--sm">Открыть</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="photo-card__info">
|
||||
<span class="photo-card__name" title="{{ photo.original_name }}">{{ photo.original_name }}</span>
|
||||
<div class="photo-card__meta">
|
||||
<span>{{ photo.size_human }}</span>
|
||||
<span>{{ photo.created_at.strftime('%d.%m.%Y %H:%M') }}</span>
|
||||
</div>
|
||||
{% if show_owner and photo.owner %}
|
||||
<div class="photo-card__owner">@{{ photo.owner.username }}</div>
|
||||
{% endif %}
|
||||
{% if delete_mode == 'admin' %}
|
||||
<form action="{{ url_for('admin.delete_photo', photo_id=photo.id) }}" method="post" class="photo-card__delete" onsubmit="return confirm('Удалить это фото?');">
|
||||
<button type="submit" class="btn btn--danger btn--sm">Удалить</button>
|
||||
</form>
|
||||
{% elif current_user.is_authenticated and (current_user.is_admin or photo.user_id == current_user.id) %}
|
||||
<form action="{{ url_for('main.delete_photo', photo_id=photo.id) }}" method="post" class="photo-card__delete" onsubmit="return confirm('Удалить это фото?');">
|
||||
<button type="submit" class="btn btn--danger btn--sm">Удалить</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-state__icon">🖼️</div>
|
||||
<h3>{{ empty_title or 'Пока нет фотографий' }}</h3>
|
||||
<p>{{ empty_text or 'Загрузите первое изображение — оно появится здесь' }}</p>
|
||||
{% if empty_link %}
|
||||
<a href="{{ empty_link }}" class="btn btn--primary">{{ empty_link_text or 'Загрузить фото' }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user