avtopost.kultmost.ru/templates/profile/partner/exhibition catalog.html
2026-09-10 14:44:50 -04:00

595 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% import "/components/header.html" as header %}
{% import "/components/footer.html" as footer %}
{% import "components/profile_card.html" as profile_card %}
{% import "components/profile_nav.html" as profile_nav %}
{% import "competition/list.html" as competition %}
{% import "components/label.html" as label %}
{% block title %}Каталог картин{% endblock %}
{% block content %}
{{ header.section(user) }}
<section class="c_dg3k4">
<div class="c_gtoe9">
{{ profile_card.profile_card(user) }}
<div class="c_ru432">
{{ profile_nav.profile_nav(user, 'exhibitions') }}
<div class="c_3ihvi" id="main">
<a class="c_ctfod b_289i1 ba" href="/profile/partner/exhibition/{{exhibition._id}}#main">
<span class="c_d2l0b b_al8e7">Отобранные картины</span>
</a>
</div>
<div class="c_tihtk">
<div class="c_9fgdh b_ax8vz" id="filters" style="padding: 10px;">
<h2 style="margin-bottom: 1.5rem;">Фильтры</h2>
<div class="c_aihm6 floating-group">
<input readonly name="filter-genre" id="filter-genre" value="Выбрать все" class="c_xhd5k" data-select="1" data-radio="0" data-takeall="1" data-label="Сохранить" data-custom_placeholder="" data-custom="0" data-title="Выберите жанр из списка">
<label for="filter-genre" class="c_z3ybu floating-label">По жанру</label>
</div>
<div class="c_aihm6 floating-group">
<input readonly name="filter-city" id="filter-city" value="Выбрать все" class="c_xhd5k" data-select="1" data-radio="0" data-takeall="1" data-label="Сохранить" data-custom_placeholder="" data-custom="0" data-title="Выберите город из списка">
<label for="filter-city" class="c_z3ybu floating-label">По городу</label>
</div>
<div class="c_aihm6 floating-group">
<input readonly name="filter-grade" id="filter-grade" value="Выбрать все" class="c_xhd5k" data-select="1" data-radio="0" data-takeall="1"
data-list="['Без звания', 'Заслуженный художник Российской федерации', 'Народный художник Российской федерации', 'Академик Российской академии художеств', 'Почетный член Российской академии художеств', 'Действительный член Российской Академии художеств', 'Член-корреспондент Российской академии художеств', 'Член Союза художников России', 'Член творческого союза художников России', 'Член Московского союза художников', 'Член Подмосковного союза художников', 'Член Союза русских художников', 'Творческий союз профессиональных художников', 'Евразийский союз художников', 'Международный художественный фонд', 'Творческое объединение женщин-художников']"
data-label="Сохранить" data-custom_placeholder="" data-custom="0" data-title="Выберите звание из списка">
<label for="filter-grade" class="c_z3ybu floating-label">По званию</label>
</div>
<div class="c_aihm6 floating-group">
<input readonly name="filter-galerists" id="filter-galerists" value="Выбрать все" class="c_xhd5k" data-select="1" data-radio="0" data-takeall="1" data-label="Сохранить" data-custom_placeholder="" data-custom="0" data-title="Выберите жанр из списка">
<label for="filter-galerists" class="c_z3ybu floating-label">Галерея</label>
</div>
<div class="c_aihm6 floating-group">
<input type="text" id="filter-name" name="filter-name" class="c_xhd5k">
<label for="filter-name" class="c_z3ybu floating-label">Название картины</label>
</div>
<div class="c_aihm6 floating-group">
<input type="text" id="filter-author" name="filter-author" class="c_xhd5k">
<label for="filter-author" class="c_z3ybu floating-label">Автор (ФИО)</label>
</div>
<button id="apply-filters" class="btn b ba">Применить фильтры</button>
</div>
<article class="c_9fgdh b_ax8vz">
<!-- Poster -->
<div class="b_ornr9">
<picture>
<img src="/b/exhibition/{{exhibition.poster_file_id}}" id="preview_poster"
data-src="/b/exhibition/{{exhibition.poster_orig_id}}" crossorigin="anonymous" fetchpriority="high"
loading="eager" alt="Выставка {{exhibition.title}}" class="b_ornr9">
</picture>
</div>
<div class="c_z3dpx b_73p10">
<div class="c_c4wam b_vabj9">
{% if now > exhibition.date_finish %}
<span class="c_95csi b_0jst1">Закончилась</span>
{% elif now > exhibition.datetime_start %}
<span class="c_95csi b_0jst1">Началась</span>
{% else %}
<span class="c_95csi b_0jst1">Формируется</span>
{% endif %}
</div>
<header class="c_m876y b_9xtz9">
<h3 class="c_hysjg b_8f3uh" id="preview_title">Выставка {{exhibition.title}}</h3>
</header>
<p class="c_2fr39 b_tfnz2" id="preview_topic">{{exhibition.topic}}</p>
<p>Место проведения: {{ exhibition.get('place') }}</p>
<p>Отобрано картин: <span id="lenPosts">{{ exhibition.posts | length }}</span></p>
<div class="b_otttb">
<span class="c_2fr39 b_tfnz2">С {{ ( exhibition.datetime_start.strftime('%d.%m.%Y')) if exhibition.datetime_start else ''}}</span>
<span class="c_2fr39 b_tfnz2">По {{ ( exhibition.date_finish.strftime('%d.%m.%Y')) if exhibition.date_finish else ''}}</span>
</div>
</div>
</article>
</div>
<button id="show-more-btn" class="btn" type="button">Показать ещё</button>
</div>
</div>
</section>
<script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
const galerists = [
{% for p in galerists %}
{
name: "{{ p._id|string }}",
count: {{ p.count|tojson }},
}{% if not loop.last %},{% endif %}
{% endfor %}
];
document.getElementById('filter-galerists').dataset.list=JSON.stringify(galerists)
// @license-end
</script>
<script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
const living_places = [
{% for p in living_places %}
{
name: "{{ p._id|string }}",
count: {{ p.count|tojson }},
}{% if not loop.last %},{% endif %}
{% endfor %}
];
document.getElementById('filter-city').dataset.list=JSON.stringify(living_places)
// @license-end
</script>
<script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
document.addEventListener('DOMContentLoaded', () => {
const genres_count = {{genres|tojson}};
let genres = [
{
"name": "Абстрактное искусство",
"tooltip": "В нём не используются реальные объекты или изображения, а создаются абстрактные формы и композиции."
},
{
"name": "Авангардизм",
"tooltip": "Включает в себя экспрессионизм, абстракционизм и другие разновидности модернистского искусства."
},
{
"name": "Анималистика",
"tooltip": "Посвящена изображению зверей и птиц."
},
{
"name": "Барокко",
"tooltip": "Торжественность и вычурность, яркие краски и динамичные сюжеты. Сочетает иллюзию и реальность, духовное и материальное."
},
{
"name": "Батальная живопись",
"tooltip": "Изображение сцен сражений и военных действий"
},
{
"name": "Бытовая живопись",
"tooltip": "Посвящён повседневной жизни людей."
},
{
"name": "ДПИ – декоративно прикладное искусство",
"tooltip": "Вид художественного искусства, который охватывает многие разновидности профессиональной и самодеятельной творческой деятельности, которые люди используют в повседневной жизни."
},
{
"name": "Импрессионизм",
"tooltip": "Работа на пленэрах. Запечатление реального мира в его мимолётности и воздушности. художественное эмоциональное состояние автора."
},
{
"name": "Интерьерная живопись",
"tooltip": "Изображение внутренних помещений"
},
{
"name": "Историческая живопись",
"tooltip": "Изображение событий прошлого, исторических личностей и мифологических сцен."
},
{
"name": "Классицизм",
"tooltip": "Строгость и гармония. Обращение к античности. Основными элементами являются линия и светотень."
},
{
"name": "Концептуальное искусство",
"tooltip": "Для создания произведений используются концепции и идеи."
},
{
"name": "Кубизм",
"tooltip": "декомпозиция объектов на геометрические формы, такие как кубы, призмы и конусы, а затем их абстрактное пересложение."
},
{
"name": "Марина",
"tooltip": "Изображение морских пейзажей и кораблей"
},
{
"name": "Минимализм",
"tooltip": "Стремится к простоте и минимализму в своих произведениях."
},
{
"name": "Модернизм",
"tooltip": "Отрыв от реализма. Показ эмоций и переживаний автора, общее название для художественных направлений ХХ века, отвергающих традиционные формы."
},
{
"name": "Монументальное искусство",
"tooltip": "Живопись на архитектурных сооружениях и других стационарных основаниях, архитектурные и скульптурные композиции, выражающие значимые идеи. Включает в себя живопись (фрески, мозаики, витражи и т д)"
},
{
"name": "Натюрморт",
"tooltip": "Изображение неодушевленных предметов. Часто скомпонованных в декоративные композиции"
},
{
"name": "Ню",
"tooltip": "Показывает красоту обнажённого человеческого тела."
},
{
"name": "Оп-арт",
"tooltip": "Создание оптических иллюзий с помощью различных геометрических узоров, сочетания цветов и форм."
},
{
"name": "Пейзаж",
"tooltip": "Изображение природы отражающее ее красоту и разнообразие"
},
{
"name": "Поп-арт",
"tooltip": "Яркие краски, броские сюжеты, плакатность."
},
{
"name": "Портрет",
"tooltip": "Показывает индивидуальные черты конкретного человека"
},
{
"name": "Реализм",
"tooltip": "Изображение жизни как она есть. натуралистичность, правдивость образов."
},
{
"name": "Романтизм",
"tooltip": "Направление, подчеркивающее эмоциональность, индивидуальность и красоту"
},
{
"name": "Скульптура",
"tooltip": "Вид изобразительного искусства, произведения которого имеют объемную форму и выполняются из твёрдых материалов."
},
{
"name": "Соцреализм",
"tooltip": "Отражает социальную реальность через произведения искусства."
},
{
"name": "Сюрреализм",
"tooltip": "Искажает реальность и создаёт необычные образы."
},
{
"name": "Цифровое искусство",
"tooltip": "Использует компьютерные технологии для создания произведений."
}
]
const countById = new Map( genres_count.map(x => [String(x._id).trim(), x.count]));
genre = genres.map(g => {
const key = String(g.name).trim(); // or g.id if you have an id field
return {
...g,
count: countById.get(key) ?? ''
};
});
document.getElementById('filter-genre').dataset.list=JSON.stringify(genre)
})
// @license-end
</script>
<script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
const initialPostsData = [
{% for p in posts %}
{
_id: "{{ p._id|string }}",
title: {{ p.title|tojson }},
size: {{ p.size|tojson }},
material: {{ p.material|tojson }},
genre: {{ p.genre|tojson }},
status: {{ p.status|tojson }},
medias: {{ (p.medias or [])|tojson }},
medias_original: {{ (p.medias_original or [])|tojson }},
{% if p.user %}
user: { _id: "{{ p.user._id|string }}", username: {{ p.user.username|tojson }} }
{% else %}
user: null
{% endif %}
}{% if not loop.last %},{% endif %}
{% endfor %}
];
const exhibition = {
{% if exhibition %}
title: {{ exhibition.title|tojson }},
posts: {{ (exhibition.get('posts') or []) | map('string') | list | tojson }}
{% else %}
title: "", posts: []
{% endif %}
};
// @license-end
</script>
<script>
// @license magnet:?xt=urn:btih:c80d50af7d3db9be66a4d0a86db0286e4fd33292&dn=bsd-3-clause.txt BSD-3-Clause
const selectedPostIds = new Set((exhibition?.posts || []).map(String));
const container = document.querySelector('.c_tihtk');
const showMoreBtn = document.getElementById('show-more-btn');
let currentQuery = {
per_page: 16,
page: 1,
};
let hasMore = true;
function escapeHtml(str) {
return String(str ?? "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#039;");
}
function renderPost(post) {
const media0 = post?.medias?.[0] ?? post?.photos?.[0] ?? "";
const userId = post?.user?._id ?? "";
const userUsername = post?.user?.username ?? "";
const checked = selectedPostIds.has(String(post?._id));
return `
<article class="c_9fgdh b_ax8vz">
<div class="b_ornr9">
<picture><img src="/b/posts/${escapeHtml(media0)}" data-src="/b/posts/${escapeHtml(media0)}"
crossorigin="anonymous" fetchpriority="high" loading="eager" alt="${escapeHtml(post?.title)}" class="b_ornr9"></picture>
</div>
<div class="c_z3dpx b_73p10">
<header class="c_m876y b_9xtz9"><h3 class="c_hysjg b_8f3uh">${escapeHtml(post?.title)}</h3></header>
<div class="l_green"><span>${escapeHtml(post?.status)}</span></div>
<p class="c_2fr39 b_tfnz2">Размер: ${escapeHtml(post?.size)}</p>
<p class="c_2fr39 b_tfnz2">Материал: ${escapeHtml(post?.material)}</p>
<p class="c_2fr39 b_tfnz2">Жанр: ${escapeHtml(post?.genre)}</p>
${post?.user ? `
<div class="c_dbbe4 b_otttb">
<img src="/a/${escapeHtml(userId)}" class="c_pm80w b_a68fq" alt="${escapeHtml(userUsername)}">
<div class="c_4402p b_ntazj"><p class="c_kvah4 b_ghosy">${escapeHtml(userUsername)}</p></div>
</div>` : ''}
<label class="offer-label">
<input type="checkbox" class="offer-checkbox" onchange="selectPost(this, '${escapeHtml(post?._id)}')" ${checked ? "checked" : ""}>
<span class="offer-text">${escapeHtml(exhibition?.title ?? "")}</span>
</label>
</div>
</article>`;
}
function clearPostsAfterFirst() {
const articles = container.querySelectorAll('article');
for (let i = 1; i < articles.length; i++) {
articles[i].remove();
}
}
function renderPosts(posts) {
if (!posts?.length) return;
container.insertAdjacentHTML('beforeend', posts.map(renderPost).join(''));
}
function getFiltersQuery() {
const genre = document.getElementById('filter-genre')?.value || '';
const grade = document.getElementById('filter-grade')?.value || '';
const galerist = document.getElementById('filter-galerists')?.value || '';
const city = document.getElementById('filter-city')?.value || '';
const name = document.getElementById('filter-name')?.value || '';
const author = document.getElementById('filter-author')?.value || '';
const and = [];
if (name) and.push({field:"name", op:"like", value:name});
if (author) and.push({field:"author_name", op:"like", value:author});
if (genre) {
const arr = genre
.split(',')
.map(s => s.trim())
.filter(v => v && v !== 'Выбрать все');
if (arr.length) {
and.push({ field:"genre", op:"like_in", value: arr });
}
}
if (grade) {
const arr = grade
.split(',')
.map(s => s.trim())
.filter(v => v && v !== 'Выбрать все');
if (arr.length) {
and.push({ field:"grade", op:"like_in", value: arr });
}
}
if (galerist) {
const arr = galerist
.split(',')
.map(s => s.trim())
.filter(v => v && v !== 'Выбрать все');
if (arr.length) {
and.push({ field:"user.username", op:"like_in", value: arr });
}
}
if (city) {
const arr = city
.split(',')
.map(s => s.trim())
.filter(v => v && v !== 'Выбрать все')
.map(v => (v === 'Без города' ? null : v));
if (arr.length) {
and.push({field: "city", op: "like_in", value: arr });
}
}
return {
...currentQuery,
filter: and.length ? {and} : {},
page: currentQuery.page || 1
};
}
async function fetchPosts(query) {
try {
const params = new URLSearchParams();
Object.keys(query).forEach(k => {
if (k === 'filter' && query.filter?.and?.length) params.append('filter', JSON.stringify(query.filter));
else if (Array.isArray(query[k])) query[k].forEach(v => params.append(k+'[]', v));
else if (query[k] != null) params.append(k, query[k]);
});
const res = await fetch(`/api/v1/generic/post?${params}`);
const json = await res.json();
return json.data || [];
} catch (e) {
console.error(e);
return [];
}
}
async function loadMore() {
if (!hasMore) return;
const query = getFiltersQuery();
const posts = await fetchPosts(query);
if (posts.length > 0) {
renderPosts(posts);
currentQuery.page = (currentQuery.page || 1) + 1;
} else {
hasMore = false;
showMoreBtn.textContent = "Больше нет";
showMoreBtn.disabled = true;
}
}
async function applyFilters() {
currentQuery.page = 1;
hasMore = true;
showMoreBtn.textContent = "Показать ещё";
showMoreBtn.disabled = false;
clearPostsAfterFirst();
const filteredPosts = await fetchPosts(getFiltersQuery());
renderPosts(filteredPosts);
currentQuery.page = 2; // после initial + filtered
}
async function selectPost(el, postId) {
const form = new FormData();
form.set('post_id', postId);
form.set('exhibition_id', '{{exhibition._id}}');
const isAdding = el.checked;
try {
const res = await fetch('/api/exhibition/post/select', {
method: 'POST',
body: form
});
if (res.ok) {
if (isAdding) {
selectedPostIds.add(String(postId));
if (!exhibition.posts.includes(String(postId))) {
exhibition.posts.push(String(postId));
}
} else {
selectedPostIds.delete(String(postId));
exhibition.posts = exhibition.posts.filter(id => String(id) !== String(postId));
}
// Обновляем счётчик
const lenEl = document.getElementById('lenPosts');
if (lenEl) lenEl.textContent = exhibition.posts.length;
} else {
el.checked = !isAdding;
if (typeof showToast === 'function') {
showToast({ message: 'Ошибка сохранения', status: "error", duration: 2500 });
}
}
} catch (e) {
console.error(e);
el.checked = !isAdding;
if (typeof showToast === 'function') {
showToast({ message: 'Ошибка соединения', status: "error", duration: 2500 });
}
}
}
// Initial setup — page = 3 только здесь
document.addEventListener('DOMContentLoaded', () => {
renderPosts(initialPostsData);
currentQuery.page = 3; // ← только один раз при старте
document.getElementById('apply-filters').addEventListener('click', applyFilters);
showMoreBtn.addEventListener('click', loadMore);
});
// @license-end
</script>
{{ footer.section(user) }}
{% endblock %}
{% block head %}
{{ header.style() }}
{{ profile_nav.style() }}
{{ competition.style() }}
<style>
{{label.green()}}
{{label.purple()}}
@media (min-width: 901px) {
.c_aihm6 { display: flex; width: 284px; justify-content: center; align-items: center; border-radius: 5px; background: #FFF; }
.c_xhd5k { display: flex; height: 42px; padding: 5px 10px; align-items: center; flex: 1 0 0; }
.c_z3ybu { position: absolute; left: 13px; top: 12px; color: #000; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 400; line-height: normal; }
}
@media (min-width: 700px) and (max-width: 900px) {
.c_aihm6 { display: flex; width: 284px; justify-content: center; align-items: center; border-radius: 5px; background: #FFF; }
.c_xhd5k { display: flex; height: 42px; padding: 5px 10px; align-items: center; flex: 1 0 0; }
.c_z3ybu { position: absolute; left: 13px; top: 12px; color: #000; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 400; line-height: normal; }
}
@media (max-width: 699px) {
.c_aihm6 { display: flex; justify-content: center; align-items: center; align-self: stretch; border-radius: 5px; background: #FFF; }
.c_xhd5k { display: flex; height: 42px; padding: 5px 10px; align-items: center; flex: 1 0 0; }
.c_z3ybu { position: absolute; left: 13px; top: 12px; color: #000; font-family: Inter; font-size: 15px; font-style: normal; font-weight: 400; line-height: normal; }
}
.c_aihm6:hover input, .c_nb6gy:hover textarea, .poster-group:hover .file-name-display {border-color: #5F1798;}
.poster-group .floating-label {position: absolute;left: 16px;top: 16px;font-size: 16px;color: #666;background: #fff;padding: 0 6px;z-index: 3;transition: all 0.3s ease;pointer-events: none;}
/* Когда лейбл должен подняться */
.poster-group input:focus + .floating-label,
.poster-group.has-file .floating-label {top: -8px;font-size: 13px;color: #5F1798;}
/* Иконка справа */
.file-icon {position: absolute;right: 16px;top: 50%;transform: translateY(-50%);z-index: 3;pointer-events: none;}
/* Красная ошибка */
.poster-group.error .file-name-display {border-color: #e74c3c !important;box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.15);}
.poster-group.error .floating-label {color: #e74c3c !important;}
/* ====================== FLOATING LABELS ====================== */
.floating-group {position: relative;margin-bottom: 1.5rem;}
.floating-label {position: absolute;left: 16px;top: 16px;font-size: 16px;color: #666;transition: all 0.3s ease;pointer-events: none;background: #fff;padding: 0 6px;z-index: 2;}
/* Плавающий лейбл */
.floating-group input:focus + .floating-label, .floating-group textarea:focus + .floating-label,
.floating-group input:not(:placeholder-shown) + .floating-label, .floating-group textarea:not(:placeholder-shown) + .floating-label {top: -8px;font-size: 15px;color: #5F1798;}
/* Красная подсветка — ТОЛЬКО после попытки отправки */
.floating-group.error input, .floating-group.error textarea,
.floating-group.error input[type="file"] {border-color: #e74c3c !important;box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.38);}
.floating-group.error .floating-label {color: #e74c3c !important;}
/* Основные стили полей */
.c_xhd5k, .c_4ykpn {width: 100%;border: 2px solid #ddd;border-radius: 8px;font-size: 16px;transition: all 0.3s ease;background: #fff;}
.c_xhd5k:focus, .c_4ykpn:focus {border-color: #5F1798;outline: none;box-shadow: 0 0 0 3px rgba(95, 23, 152, 0.15);}
/* Файловые поля */
.file-group input[type="file"] {height: 43px;}
.file-group .floating-label {top: 12px;}
.offer-label{ width: 100%; padding: 8px 0; }
body{background: #F6EBFF;}
.c_3ihvi{ position: sticky; top: 0; z-index: 3; background: white; }
.b_289i1{ width: 100%; padding: 15px; }
/* Mobile */
@media (max-width: 699px) {
.c_mrl59 { align-items: center; background: #5F1798; border-radius: 25px; bottom: -15px; display: flex; height: 50px; justify-content: center; position: absolute; right: -15px; width: 50px; }
}
/* Tablet */
@media (min-width: 700px) and (max-width: 900px) {
.c_iiehk { align-items: center; border-radius: 5px; display: flex; flex-direction: column; gap: 20px; width: 335px; }
.c_nnsh6>a>img{display: none;}
}
/* Desktop */
@media (min-width: 901px) {
.c_iiehk { align-items: flex-end; border-radius: 5px; display: flex; flex-direction: column; gap: 20px; width: 310px; }
.c_nnsh6>a>img{display: none;}
}
#preview_poster{min-height: 456px;}
</style>
{{ footer.style() }}
{% endblock %}