mirror of
https://github.com/LOBSTERVOVA/Tennis-Site.git
synced 2026-04-17 17:40:49 +03:00
добавил управление главными фото страницы
This commit is contained in:
@@ -51,7 +51,7 @@ public class SecurityConfig {
|
|||||||
.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
||||||
//.addFilterAfter(errorHandlingFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
//.addFilterAfter(errorHandlingFilter, SecurityWebFiltersOrder.AUTHENTICATION)
|
||||||
.authorizeExchange(exchange -> exchange
|
.authorizeExchange(exchange -> exchange
|
||||||
.pathMatchers("/account/login","/error","/error/**", "/account/logout").permitAll()
|
.pathMatchers("/account/login","/error","/error/**", "/account/logout", "/api/admin/site-settings/public").permitAll()
|
||||||
.pathMatchers("/account/**", "/admin", "/api/admin/**").authenticated()
|
.pathMatchers("/account/**", "/admin", "/api/admin/**").authenticated()
|
||||||
.anyExchange().permitAll()
|
.anyExchange().permitAll()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ import com.example.dateplanner.dto.PageResponse;
|
|||||||
import com.example.dateplanner.models.entities.AppUser;
|
import com.example.dateplanner.models.entities.AppUser;
|
||||||
import com.example.dateplanner.models.entities.Court;
|
import com.example.dateplanner.models.entities.Court;
|
||||||
import com.example.dateplanner.models.entities.EventApplication;
|
import com.example.dateplanner.models.entities.EventApplication;
|
||||||
|
import com.example.dateplanner.models.entities.SiteSettings;
|
||||||
import com.example.dateplanner.models.entities.TennisClub;
|
import com.example.dateplanner.models.entities.TennisClub;
|
||||||
import com.example.dateplanner.models.entities.TennisEvent;
|
import com.example.dateplanner.models.entities.TennisEvent;
|
||||||
import com.example.dateplanner.repositories.CourtRepository;
|
import com.example.dateplanner.repositories.CourtRepository;
|
||||||
import com.example.dateplanner.repositories.EventApplicationRepository;
|
import com.example.dateplanner.repositories.EventApplicationRepository;
|
||||||
|
import com.example.dateplanner.repositories.SiteSettingsRepository;
|
||||||
import com.example.dateplanner.repositories.TennisClubRepository;
|
import com.example.dateplanner.repositories.TennisClubRepository;
|
||||||
import com.example.dateplanner.repositories.TennisEventRepository;
|
import com.example.dateplanner.repositories.TennisEventRepository;
|
||||||
import com.example.dateplanner.services.ApplicationService;
|
import com.example.dateplanner.services.ApplicationService;
|
||||||
@@ -41,6 +43,7 @@ public class ApiAdminController {
|
|||||||
private final EventApplicationRepository applicationRepository;
|
private final EventApplicationRepository applicationRepository;
|
||||||
private final ApplicationService applicationService;
|
private final ApplicationService applicationService;
|
||||||
private final MinioService minioService;
|
private final MinioService minioService;
|
||||||
|
private final SiteSettingsRepository siteSettingsRepository;
|
||||||
|
|
||||||
// ── Загрузка изображений ───────────────────────────────────────────────────
|
// ── Загрузка изображений ───────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -450,4 +453,49 @@ public class ApiAdminController {
|
|||||||
dto.setCreatedAt(app.getCreatedAt());
|
dto.setCreatedAt(app.getCreatedAt());
|
||||||
return dto;
|
return dto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Настройки сайта ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@GetMapping("/site-settings")
|
||||||
|
public Mono<SiteSettings> getSiteSettings(@AuthenticationPrincipal AppUser user) {
|
||||||
|
return siteSettingsRepository.findFirstByOrderByUpdatedAtDesc()
|
||||||
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
|
SiteSettings settings = new SiteSettings();
|
||||||
|
settings.setHeroImageUrl(
|
||||||
|
"https://avatars.mds.yandex.net/get-altay/1880524/2a0000016ef11d02f18395e01a44f0ddbdb7/XXL_height");
|
||||||
|
settings.setBackgroundImageUrl(
|
||||||
|
"https://avatars.mds.yandex.net/get-altay/1880524/2a0000016ef11d02f18395e01a44f0ddbdb7/XXL_height");
|
||||||
|
return siteSettingsRepository.save(settings);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/site-settings")
|
||||||
|
public Mono<SiteSettings> updateSiteSettings(@RequestBody SiteSettings body,
|
||||||
|
@AuthenticationPrincipal AppUser user) {
|
||||||
|
return siteSettingsRepository.findFirstByOrderByUpdatedAtDesc()
|
||||||
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
|
SiteSettings settings = new SiteSettings();
|
||||||
|
return siteSettingsRepository.save(settings);
|
||||||
|
}))
|
||||||
|
.flatMap(existing -> {
|
||||||
|
existing.setHeroImageUrl(body.getHeroImageUrl());
|
||||||
|
existing.setBackgroundImageUrl(body.getBackgroundImageUrl());
|
||||||
|
existing.setUpdatedAt(LocalDateTime.now());
|
||||||
|
return siteSettingsRepository.save(existing);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Публичный endpoint для главной страницы
|
||||||
|
@GetMapping("/site-settings/public")
|
||||||
|
public Mono<SiteSettings> getPublicSiteSettings() {
|
||||||
|
return siteSettingsRepository.findFirstByOrderByUpdatedAtDesc()
|
||||||
|
.switchIfEmpty(Mono.defer(() -> {
|
||||||
|
SiteSettings settings = new SiteSettings();
|
||||||
|
settings.setHeroImageUrl(
|
||||||
|
"https://avatars.mds.yandex.net/get-altay/1880524/2a0000016ef11d02f18395e01a44f0ddbdb7/XXL_height");
|
||||||
|
settings.setBackgroundImageUrl(
|
||||||
|
"https://avatars.mds.yandex.net/get-altay/1880524/2a0000016ef11d02f18395e01a44f0ddbdb7/XXL_height");
|
||||||
|
return siteSettingsRepository.save(settings);
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.example.dateplanner.controllers.web;
|
package com.example.dateplanner.controllers.web;
|
||||||
|
|
||||||
import com.example.dateplanner.models.entities.AppUser;
|
import com.example.dateplanner.models.entities.AppUser;
|
||||||
|
import com.example.dateplanner.models.entities.SiteSettings;
|
||||||
|
import com.example.dateplanner.repositories.SiteSettingsRepository;
|
||||||
import com.example.dateplanner.services.UserService;
|
import com.example.dateplanner.services.UserService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -22,15 +24,29 @@ import java.util.Map;
|
|||||||
public class AccountController extends BaseWebController {
|
public class AccountController extends BaseWebController {
|
||||||
|
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
private final SiteSettingsRepository siteSettingsRepository;
|
||||||
|
|
||||||
|
private static final String DEFAULT_BG_IMAGE =
|
||||||
|
"https://avatars.mds.yandex.net/get-altay/1880524/2a0000016ef11d02f18395e01a44f0ddbdb7/XXL_height";
|
||||||
|
|
||||||
@GetMapping("/login")
|
@GetMapping("/login")
|
||||||
public Mono<Rendering> loginPage() {
|
public Mono<Rendering> loginPage() {
|
||||||
Map<String, Object> model = new HashMap<>();
|
Map<String, Object> model = new HashMap<>();
|
||||||
model.put("title", "Login");
|
model.put("title", "Login");
|
||||||
model.put("index", "login");
|
model.put("index", "login");
|
||||||
return addAuthToModel(model).map(m ->
|
|
||||||
Rendering.view("template").model(m).build()
|
return siteSettingsRepository.findFirstByOrderByUpdatedAtDesc()
|
||||||
);
|
.map(settings -> {
|
||||||
|
model.put("backgroundImageUrl",
|
||||||
|
settings.getBackgroundImageUrl() != null ? settings.getBackgroundImageUrl() : DEFAULT_BG_IMAGE);
|
||||||
|
return model;
|
||||||
|
})
|
||||||
|
.switchIfEmpty(Mono.fromCallable(() -> {
|
||||||
|
model.put("backgroundImageUrl", DEFAULT_BG_IMAGE);
|
||||||
|
return model;
|
||||||
|
}))
|
||||||
|
.flatMap(this::addAuthToModel)
|
||||||
|
.map(m -> Rendering.view("template").model(m).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/profile")
|
@GetMapping("/profile")
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.example.dateplanner.controllers.web;
|
package com.example.dateplanner.controllers.web;
|
||||||
|
|
||||||
|
import com.example.dateplanner.models.entities.SiteSettings;
|
||||||
|
import com.example.dateplanner.repositories.SiteSettingsRepository;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
@@ -17,14 +19,30 @@ import java.util.UUID;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class HomeController extends BaseWebController {
|
public class HomeController extends BaseWebController {
|
||||||
|
|
||||||
|
private final SiteSettingsRepository siteSettingsRepository;
|
||||||
|
|
||||||
|
private static final String DEFAULT_HERO_IMAGE =
|
||||||
|
"https://avatars.mds.yandex.net/get-altay/1880524/2a0000016ef11d02f18395e01a44f0ddbdb7/XXL_height";
|
||||||
|
|
||||||
@GetMapping("/")
|
@GetMapping("/")
|
||||||
public Mono<Rendering> home() {
|
public Mono<Rendering> home() {
|
||||||
Map<String, Object> model = new HashMap<>();
|
Map<String, Object> model = new HashMap<>();
|
||||||
model.put("title", "Home");
|
model.put("title", "Home");
|
||||||
model.put("index", "home");
|
model.put("index", "home");
|
||||||
return addAuthToModel(model).map(m ->
|
|
||||||
Rendering.view("template").model(m).build()
|
return siteSettingsRepository.findFirstByOrderByUpdatedAtDesc()
|
||||||
);
|
.map(settings -> {
|
||||||
|
model.put("heroImageUrl", settings.getHeroImageUrl() != null ? settings.getHeroImageUrl() : DEFAULT_HERO_IMAGE);
|
||||||
|
model.put("backgroundImageUrl", settings.getBackgroundImageUrl() != null ? settings.getBackgroundImageUrl() : DEFAULT_HERO_IMAGE);
|
||||||
|
return model;
|
||||||
|
})
|
||||||
|
.switchIfEmpty(Mono.fromCallable(() -> {
|
||||||
|
model.put("heroImageUrl", DEFAULT_HERO_IMAGE);
|
||||||
|
model.put("backgroundImageUrl", DEFAULT_HERO_IMAGE);
|
||||||
|
return model;
|
||||||
|
}))
|
||||||
|
.flatMap(this::addAuthToModel)
|
||||||
|
.map(m -> Rendering.view("template").model(m).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/clubs/{id}")
|
@GetMapping("/clubs/{id}")
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.example.dateplanner.models.entities;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.relational.core.mapping.Column;
|
||||||
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Table(name = "site_settings")
|
||||||
|
public class SiteSettings {
|
||||||
|
@Id
|
||||||
|
@Column("uuid")
|
||||||
|
private UUID uuid;
|
||||||
|
|
||||||
|
@Column("hero_image_url")
|
||||||
|
private String heroImageUrl;
|
||||||
|
|
||||||
|
@Column("background_image_url")
|
||||||
|
private String backgroundImageUrl;
|
||||||
|
|
||||||
|
@Column("updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.example.dateplanner.repositories;
|
||||||
|
|
||||||
|
import com.example.dateplanner.models.entities.SiteSettings;
|
||||||
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface SiteSettingsRepository extends R2dbcRepository<SiteSettings, UUID> {
|
||||||
|
Mono<SiteSettings> findFirstByOrderByUpdatedAtDesc();
|
||||||
|
}
|
||||||
16
src/main/resources/db/migration/V1_0_6__site_settings.sql
Normal file
16
src/main/resources/db/migration/V1_0_6__site_settings.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
-- Site settings for managing hero and background images
|
||||||
|
CREATE TABLE IF NOT EXISTS site_settings (
|
||||||
|
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
hero_image_url TEXT,
|
||||||
|
background_image_url TEXT,
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert default settings with the previously hardcoded URLs
|
||||||
|
INSERT INTO site_settings (uuid, hero_image_url, background_image_url, updated_at)
|
||||||
|
VALUES (
|
||||||
|
gen_random_uuid(),
|
||||||
|
'https://avatars.mds.yandex.net/get-altay/1880524/2a0000016ef11d02f18395e01a44f0ddbdb7/XXL_height',
|
||||||
|
'https://avatars.mds.yandex.net/get-altay/1880524/2a0000016ef11d02f18395e01a44f0ddbdb7/XXL_height',
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
) ON CONFLICT DO NOTHING;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { initAdminClubs } from './clubsSection.js';
|
import { initAdminClubs } from './clubsSection.js';
|
||||||
import { initAdminEvents } from './eventsSection.js';
|
import { initAdminEvents } from './eventsSection.js';
|
||||||
import { initApplicationsSection } from './applicationsSection.js';
|
import { initApplicationsSection } from './applicationsSection.js';
|
||||||
|
import { initSiteImages } from './siteImagesSection.js';
|
||||||
|
|
||||||
function onStats(stats) {
|
function onStats(stats) {
|
||||||
if (stats.clubs != null) $('#stat-clubs').text(stats.clubs);
|
if (stats.clubs != null) $('#stat-clubs').text(stats.clubs);
|
||||||
@@ -12,3 +13,4 @@ function onStats(stats) {
|
|||||||
initAdminClubs($('#admin-clubs-container'), onStats);
|
initAdminClubs($('#admin-clubs-container'), onStats);
|
||||||
initAdminEvents($('#admin-events-container'), onStats);
|
initAdminEvents($('#admin-events-container'), onStats);
|
||||||
initApplicationsSection($('#admin-applications-container'));
|
initApplicationsSection($('#admin-applications-container'));
|
||||||
|
initSiteImages();
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
import { getCsrfToken } from '../../utils/helpers.js';
|
||||||
|
|
||||||
|
const API = '/api/admin';
|
||||||
|
|
||||||
|
async function uploadImage(file) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
const res = await fetch(`${API}/upload/image`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-XSRF-TOKEN': getCsrfToken() },
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error('Ошибка загрузки изображения');
|
||||||
|
const data = await res.json();
|
||||||
|
return data.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initImageUploader(zoneId) {
|
||||||
|
const zone = document.getElementById(zoneId);
|
||||||
|
if (!zone) return { getUrl: () => null, setUrl: () => {}, clear: () => {} };
|
||||||
|
|
||||||
|
let currentUrl = null;
|
||||||
|
|
||||||
|
zone.addEventListener('click', () => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = 'image/*';
|
||||||
|
input.onchange = async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
zone.classList.add('uploading');
|
||||||
|
zone.innerHTML = '<div class="img-uploader__loading"><div class="spinner-border text-light"></div></div>';
|
||||||
|
try {
|
||||||
|
currentUrl = await uploadImage(file);
|
||||||
|
const cdn = window.cdn || '';
|
||||||
|
zone.innerHTML = `<img src="${cdn}${currentUrl}" class="img-uploader__preview" alt="preview">
|
||||||
|
<button class="img-uploader__remove" type="button"><i class="bi bi-x-lg"></i></button>`;
|
||||||
|
zone.querySelector('.img-uploader__remove').addEventListener('click', (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
currentUrl = null;
|
||||||
|
zone.innerHTML = `<div class="img-uploader__placeholder">
|
||||||
|
<i class="bi bi-cloud-upload"></i>
|
||||||
|
<span>Нажмите или перетащите изображение</span>
|
||||||
|
</div>`;
|
||||||
|
if (window.onSiteImagesChange) window.onSiteImagesChange();
|
||||||
|
});
|
||||||
|
if (window.onSiteImagesChange) window.onSiteImagesChange();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
zone.innerHTML = `<div class="img-uploader__placeholder">
|
||||||
|
<i class="bi bi-exclamation-triangle text-danger"></i>
|
||||||
|
<span class="text-danger">Ошибка загрузки</span>
|
||||||
|
</div>`;
|
||||||
|
setTimeout(() => {
|
||||||
|
zone.innerHTML = `<div class="img-uploader__placeholder">
|
||||||
|
<i class="bi bi-cloud-upload"></i>
|
||||||
|
<span>Нажмите или перетащите изображение</span>
|
||||||
|
</div>`;
|
||||||
|
}, 2000);
|
||||||
|
} finally {
|
||||||
|
zone.classList.remove('uploading');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
zone.addEventListener('dragover', (e) => { e.preventDefault(); zone.classList.add('dragover'); });
|
||||||
|
zone.addEventListener('dragleave', () => zone.classList.remove('dragover'));
|
||||||
|
zone.addEventListener('drop', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
zone.classList.remove('dragover');
|
||||||
|
const file = e.dataTransfer.files[0];
|
||||||
|
if (!file || !file.type.startsWith('image/')) return;
|
||||||
|
zone.classList.add('uploading');
|
||||||
|
zone.innerHTML = '<div class="img-uploader__loading"><div class="spinner-border text-light"></div></div>';
|
||||||
|
try {
|
||||||
|
currentUrl = await uploadImage(file);
|
||||||
|
const cdn = window.cdn || '';
|
||||||
|
zone.innerHTML = `<img src="${cdn}${currentUrl}" class="img-uploader__preview" alt="preview">
|
||||||
|
<button class="img-uploader__remove" type="button"><i class="bi bi-x-lg"></i></button>`;
|
||||||
|
zone.querySelector('.img-uploader__remove').addEventListener('click', (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
currentUrl = null;
|
||||||
|
zone.innerHTML = `<div class="img-uploader__placeholder">
|
||||||
|
<i class="bi bi-cloud-upload"></i>
|
||||||
|
<span>Нажмите или перетащите изображение</span>
|
||||||
|
</div>`;
|
||||||
|
if (window.onSiteImagesChange) window.onSiteImagesChange();
|
||||||
|
});
|
||||||
|
if (window.onSiteImagesChange) window.onSiteImagesChange();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
zone.innerHTML = `<div class="img-uploader__placeholder">
|
||||||
|
<i class="bi bi-exclamation-triangle text-danger"></i>
|
||||||
|
<span class="text-danger">Ошибка загрузки</span>
|
||||||
|
</div>`;
|
||||||
|
setTimeout(() => {
|
||||||
|
zone.innerHTML = `<div class="img-uploader__placeholder">
|
||||||
|
<i class="bi bi-cloud-upload"></i>
|
||||||
|
<span>Нажмите или перетащите изображение</span>
|
||||||
|
</div>`;
|
||||||
|
}, 2000);
|
||||||
|
} finally {
|
||||||
|
zone.classList.remove('uploading');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
getUrl: () => currentUrl,
|
||||||
|
setUrl: (url) => {
|
||||||
|
currentUrl = url;
|
||||||
|
if (url) {
|
||||||
|
const cdn = window.cdn || '';
|
||||||
|
zone.innerHTML = `<img src="${cdn}${url}" class="img-uploader__preview" alt="preview">
|
||||||
|
<button class="img-uploader__remove" type="button"><i class="bi bi-x-lg"></i></button>`;
|
||||||
|
zone.querySelector('.img-uploader__remove').addEventListener('click', (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
currentUrl = null;
|
||||||
|
zone.innerHTML = `<div class="img-uploader__placeholder">
|
||||||
|
<i class="bi bi-cloud-upload"></i>
|
||||||
|
<span>Нажмите или перетащите изображение</span>
|
||||||
|
</div>`;
|
||||||
|
if (window.onSiteImagesChange) window.onSiteImagesChange();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clear: () => {
|
||||||
|
currentUrl = null;
|
||||||
|
zone.innerHTML = `<div class="img-uploader__placeholder">
|
||||||
|
<i class="bi bi-cloud-upload"></i>
|
||||||
|
<span>Нажмите или перетащите изображение</span>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initSiteImages() {
|
||||||
|
const heroUploader = initImageUploader('hero-image-zone');
|
||||||
|
const bgUploader = initImageUploader('bg-image-zone');
|
||||||
|
const saveBtn = document.getElementById('save-site-images-btn');
|
||||||
|
|
||||||
|
if (!saveBtn) return;
|
||||||
|
|
||||||
|
window.onSiteImagesChange = () => {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Загрузка текущих настроек
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API}/site-settings`, {
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'X-XSRF-TOKEN': getCsrfToken()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
const settings = await res.json();
|
||||||
|
if (settings.heroImageUrl) heroUploader.setUrl(settings.heroImageUrl);
|
||||||
|
if (settings.backgroundImageUrl) bgUploader.setUrl(settings.backgroundImageUrl);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка загрузки настроек изображений:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveBtn.addEventListener('click', async () => {
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
saveBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Сохранение...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = {
|
||||||
|
heroImageUrl: heroUploader.getUrl(),
|
||||||
|
backgroundImageUrl: bgUploader.getUrl()
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch(`${API}/site-settings`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-XSRF-TOKEN': getCsrfToken()
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error('Ошибка сохранения настроек');
|
||||||
|
|
||||||
|
saveBtn.innerHTML = '<i class="bi bi-check-circle me-1"></i>Сохранено!';
|
||||||
|
setTimeout(() => {
|
||||||
|
saveBtn.innerHTML = '<i class="bi bi-check-circle me-1"></i>Сохранить изображения';
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
saveBtn.innerHTML = '<i class="bi bi-x-circle me-1"></i>Ошибка сохранения';
|
||||||
|
setTimeout(() => {
|
||||||
|
saveBtn.innerHTML = '<i class="bi bi-check-circle me-1"></i>Сохранить изображения';
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -689,6 +689,45 @@
|
|||||||
<div id="admin-applications-container"></div>
|
<div id="admin-applications-container"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Site Images section ── -->
|
||||||
|
<div class="section-card" id="site-images-section">
|
||||||
|
<div class="section-title mb-3">
|
||||||
|
<span><i class="bi bi-image me-2"></i>Изображения главной страницы</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-muted mb-4" style="font-size:0.9rem;">Настройте изображения для hero-секции и фона главной страницы</p>
|
||||||
|
<div class="row g-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="fw-bold mb-3"><i class="bi bi-card-image me-2"></i>Hero изображение</h6>
|
||||||
|
<p class="text-muted mb-2" style="font-size:0.8rem;">Основное изображение справа в hero-секции</p>
|
||||||
|
<div id="hero-image-uploader" class="img-uploader">
|
||||||
|
<div class="img-uploader__zone" id="hero-image-zone">
|
||||||
|
<div class="img-uploader__placeholder">
|
||||||
|
<i class="bi bi-cloud-upload"></i>
|
||||||
|
<span>Нажмите или перетащите изображение</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="fw-bold mb-3"><i class="bi bi-aspect-ratio me-2"></i>Фоновое изображение</h6>
|
||||||
|
<p class="text-muted mb-2" style="font-size:0.8rem;">Фон hero-секции и страницы входа</p>
|
||||||
|
<div id="bg-image-uploader" class="img-uploader">
|
||||||
|
<div class="img-uploader__zone" id="bg-image-zone">
|
||||||
|
<div class="img-uploader__placeholder">
|
||||||
|
<i class="bi bi-cloud-upload"></i>
|
||||||
|
<span>Нажмите или перетащите изображение</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4">
|
||||||
|
<button id="save-site-images-btn" class="btn-admin" disabled>
|
||||||
|
<i class="bi bi-check-circle me-1"></i>Сохранить изображения
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/js/site/pages/admin/admin.js"></script>
|
<script type="module" src="/js/site/pages/admin/admin.js"></script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<style>
|
<style th:inline="css">
|
||||||
.tennis-bg {
|
.tennis-bg {
|
||||||
background: linear-gradient(rgba(44,62,80,0.9), rgba(44,62,80,0.9)),
|
background: linear-gradient(rgba(44,62,80,0.9), rgba(44,62,80,0.9)),
|
||||||
url('https://avatars.mds.yandex.net/get-altay/1880524/2a0000016ef11d02f18395e01a44f0ddbdb7/XXL_height');
|
url([(${cdn != null ? cdn + backgroundImageUrl : backgroundImageUrl})]);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-6 hero-img mt-4 mt-lg-0">
|
<div class="col-lg-6 hero-img mt-4 mt-lg-0">
|
||||||
<img src="https://avatars.mds.yandex.net/get-altay/1880524/2a0000016ef11d02f18395e01a44f0ddbdb7/XXL_height"
|
<img th:src="${cdn != null ? cdn + heroImageUrl : heroImageUrl}"
|
||||||
class="img-fluid rounded-3 shadow-lg" alt="Теннисный корт">
|
class="img-fluid rounded-3 shadow-lg" alt="Теннисный корт">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
<style>
|
<style th:inline="css">
|
||||||
:root {
|
|
||||||
--tennis-green: #2ecc71;
|
|
||||||
--tennis-dark: #2c3e50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-bg {
|
.login-bg {
|
||||||
background: linear-gradient(rgba(44,62,80,0.85), rgba(44,62,80,0.85)),
|
background: linear-gradient(rgba(44,62,80,0.85), rgba(44,62,80,0.85)),
|
||||||
url('https://avatars.mds.yandex.net/get-altay/1880524/2a0000016ef11d02f18395e01a44f0ddbdb7/XXL_height');
|
url([(${cdn != null ? cdn + backgroundImageUrl : backgroundImageUrl})]);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user