1266 lines
62 KiB
TypeScript
1266 lines
62 KiB
TypeScript
'use client'
|
||
|
||
import React, { useState, useEffect } from 'react'
|
||
import { TemplatesSelector } from './TemplatesSelector'
|
||
import { ExportDataModal } from './ExportDataModal'
|
||
import { ImportDataModal } from './ImportDataModal'
|
||
import { designTemplates, DesignTemplate } from '../constants/designTemplates'
|
||
import { useLocale } from '../contexts/LocaleContext'
|
||
|
||
interface DesignSettings {
|
||
id?: number
|
||
template_id?: string
|
||
theme_color: string
|
||
background_image?: string
|
||
background_image_url?: string
|
||
dashboard_layout: 'sidebar' | 'grid' | 'list' | 'cards' | 'compact' | 'masonry' | 'timeline' | 'magazine'
|
||
groups_default_expanded: boolean
|
||
show_group_icons: boolean
|
||
show_link_icons: boolean
|
||
dashboard_background_color: string
|
||
font_family: string
|
||
custom_css: string
|
||
group_text_color?: string
|
||
link_text_color?: string
|
||
header_text_color?: string
|
||
cover_overlay_enabled?: boolean
|
||
cover_overlay_color?: string
|
||
cover_overlay_opacity?: number
|
||
// Новые опции кастомизации
|
||
group_overlay_enabled?: boolean
|
||
group_overlay_color?: string
|
||
group_overlay_opacity?: number
|
||
show_groups_title?: boolean
|
||
group_description_text_color?: string
|
||
body_font_family?: string
|
||
heading_font_family?: string
|
||
// Новые поля для цветового оверлея кнопок ссылок
|
||
link_overlay_enabled?: boolean
|
||
link_overlay_color?: string
|
||
link_overlay_opacity?: number
|
||
}
|
||
|
||
interface UserProfile {
|
||
id: number
|
||
username: string
|
||
email: string
|
||
full_name: string
|
||
bio?: string
|
||
avatar_url?: string
|
||
}
|
||
|
||
interface LinkItem {
|
||
id: number
|
||
title: string
|
||
url: string
|
||
icon_url?: string
|
||
group: number
|
||
}
|
||
|
||
interface Group {
|
||
id: number
|
||
name: string
|
||
description?: string
|
||
icon_url?: string
|
||
background_image_url?: string
|
||
is_public?: boolean
|
||
is_favorite?: boolean
|
||
links: LinkItem[]
|
||
}
|
||
|
||
interface CustomizationPanelProps {
|
||
isOpen: boolean
|
||
onClose: () => void
|
||
onSettingsUpdate: (settings: DesignSettings) => void
|
||
user?: UserProfile | null
|
||
groups?: Group[]
|
||
onDataUpdate?: () => void
|
||
}
|
||
|
||
export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate, user, groups = [], onDataUpdate }: CustomizationPanelProps) {
|
||
const { t } = useLocale()
|
||
const [settings, setSettings] = useState<DesignSettings>({
|
||
theme_color: '#ffffff',
|
||
dashboard_layout: 'list',
|
||
groups_default_expanded: true,
|
||
show_group_icons: true,
|
||
show_link_icons: true,
|
||
dashboard_background_color: '#f8f9fa',
|
||
font_family: 'sans-serif',
|
||
custom_css: '',
|
||
group_text_color: '#333333',
|
||
link_text_color: '#666666',
|
||
header_text_color: '#000000'
|
||
})
|
||
const [loading, setLoading] = useState(false)
|
||
const [activeTab, setActiveTab] = useState<'layout' | 'colors' | 'groups' | 'templates' | 'advanced' | 'data'>('templates')
|
||
const [backgroundImageFile, setBackgroundImageFile] = useState<File | null>(null)
|
||
|
||
// Состояния для модалов экспорта/импорта
|
||
const [showExportModal, setShowExportModal] = useState(false)
|
||
const [showImportModal, setShowImportModal] = useState(false)
|
||
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
loadSettings()
|
||
}
|
||
}, [isOpen])
|
||
|
||
const loadSettings = async () => {
|
||
try {
|
||
const token = localStorage.getItem('token')
|
||
const API = process.env.NEXT_PUBLIC_API_URL || 'https://links.shareon.kr'
|
||
const response = await fetch(`${API}/api/customization/settings/`, {
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
}
|
||
})
|
||
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setSettings(data)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading settings:', error)
|
||
}
|
||
}
|
||
|
||
const saveSettings = async () => {
|
||
setLoading(true)
|
||
try {
|
||
const token = localStorage.getItem('token')
|
||
const API = process.env.NEXT_PUBLIC_API_URL || 'https://links.shareon.kr'
|
||
|
||
// Если есть новый файл фоновой картинки, отправляем через FormData
|
||
if (backgroundImageFile) {
|
||
const formData = new FormData()
|
||
|
||
// Добавляем все настройки
|
||
formData.append('template_id', settings.template_id || '')
|
||
formData.append('theme_color', settings.theme_color)
|
||
formData.append('dashboard_layout', settings.dashboard_layout)
|
||
formData.append('groups_default_expanded', settings.groups_default_expanded.toString())
|
||
formData.append('show_group_icons', settings.show_group_icons.toString())
|
||
formData.append('show_link_icons', settings.show_link_icons.toString())
|
||
formData.append('dashboard_background_color', settings.dashboard_background_color)
|
||
formData.append('font_family', settings.font_family)
|
||
formData.append('custom_css', settings.custom_css)
|
||
formData.append('header_text_color', settings.header_text_color || '#000000')
|
||
formData.append('group_text_color', settings.group_text_color || '#333333')
|
||
formData.append('link_text_color', settings.link_text_color || '#666666')
|
||
formData.append('cover_overlay_enabled', (settings.cover_overlay_enabled || false).toString())
|
||
formData.append('cover_overlay_color', settings.cover_overlay_color || '#000000')
|
||
formData.append('cover_overlay_opacity', (settings.cover_overlay_opacity || 0.3).toString())
|
||
formData.append('group_overlay_enabled', (settings.group_overlay_enabled || false).toString())
|
||
formData.append('group_overlay_color', settings.group_overlay_color || '#000000')
|
||
formData.append('group_overlay_opacity', (settings.group_overlay_opacity || 0.3).toString())
|
||
formData.append('show_groups_title', (settings.show_groups_title !== false).toString())
|
||
formData.append('group_description_text_color', settings.group_description_text_color || '#666666')
|
||
formData.append('body_font_family', settings.body_font_family || 'sans-serif')
|
||
formData.append('heading_font_family', settings.heading_font_family || 'sans-serif')
|
||
formData.append('link_overlay_enabled', (settings.link_overlay_enabled || false).toString())
|
||
formData.append('link_overlay_color', settings.link_overlay_color || '#000000')
|
||
formData.append('link_overlay_opacity', (settings.link_overlay_opacity || 0.2).toString())
|
||
formData.append('background_image', backgroundImageFile)
|
||
|
||
const response = await fetch(`${API}/api/customization/settings/`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: formData
|
||
})
|
||
|
||
if (response.ok) {
|
||
const updatedSettings = await response.json()
|
||
onSettingsUpdate(updatedSettings)
|
||
setBackgroundImageFile(null) // Сбрасываем выбранный файл
|
||
onClose()
|
||
} else {
|
||
const errorData = await response.json()
|
||
console.error('Server error:', errorData)
|
||
}
|
||
} else {
|
||
// Если файл не выбран, отправляем только JSON настройки (картинка остается прежней)
|
||
const editableSettings = {
|
||
template_id: settings.template_id,
|
||
theme_color: settings.theme_color,
|
||
dashboard_layout: settings.dashboard_layout,
|
||
groups_default_expanded: settings.groups_default_expanded,
|
||
show_group_icons: settings.show_group_icons,
|
||
show_link_icons: settings.show_link_icons,
|
||
dashboard_background_color: settings.dashboard_background_color,
|
||
font_family: settings.font_family,
|
||
custom_css: settings.custom_css,
|
||
header_text_color: settings.header_text_color || '#000000',
|
||
group_text_color: settings.group_text_color || '#333333',
|
||
link_text_color: settings.link_text_color || '#666666',
|
||
cover_overlay_enabled: settings.cover_overlay_enabled || false,
|
||
cover_overlay_color: settings.cover_overlay_color || '#000000',
|
||
cover_overlay_opacity: settings.cover_overlay_opacity || 0.3,
|
||
group_overlay_enabled: settings.group_overlay_enabled || false,
|
||
group_overlay_color: settings.group_overlay_color || '#000000',
|
||
group_overlay_opacity: settings.group_overlay_opacity || 0.3,
|
||
show_groups_title: settings.show_groups_title !== false,
|
||
group_description_text_color: settings.group_description_text_color || '#666666',
|
||
body_font_family: settings.body_font_family || 'sans-serif',
|
||
heading_font_family: settings.heading_font_family || 'sans-serif',
|
||
link_overlay_enabled: settings.link_overlay_enabled || false,
|
||
link_overlay_color: settings.link_overlay_color || '#000000',
|
||
link_overlay_opacity: settings.link_overlay_opacity || 0.2
|
||
}
|
||
|
||
const response = await fetch(`${API}/api/customization/settings/`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
body: JSON.stringify(editableSettings)
|
||
})
|
||
|
||
if (response.ok) {
|
||
const updatedSettings = await response.json()
|
||
onSettingsUpdate(updatedSettings)
|
||
onClose()
|
||
} else {
|
||
const errorData = await response.json()
|
||
console.error('Server error:', errorData)
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error saving settings:', error)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleChange = (field: keyof DesignSettings, value: any) => {
|
||
setSettings(prev => ({
|
||
...prev,
|
||
[field]: value
|
||
}))
|
||
}
|
||
|
||
const handleTemplateSelect = (template: DesignTemplate) => {
|
||
setSettings(prev => ({
|
||
...prev,
|
||
...template.settings,
|
||
id: prev.id, // Сохраняем оригинальный ID
|
||
template_id: template.id // Добавляем ID шаблона для отслеживания
|
||
}))
|
||
}
|
||
|
||
// Определяем текущий шаблон
|
||
const getCurrentTemplateId = () => {
|
||
// Если есть сохраненный template_id
|
||
if ((settings as any).template_id) {
|
||
return (settings as any).template_id
|
||
}
|
||
|
||
// Или пытаемся определить по совпадению настроек
|
||
for (const template of designTemplates) {
|
||
if (
|
||
template.settings.theme_color === settings.theme_color &&
|
||
template.settings.background_color === settings.dashboard_background_color &&
|
||
template.settings.dashboard_layout === settings.dashboard_layout
|
||
) {
|
||
return template.id
|
||
}
|
||
}
|
||
return undefined
|
||
}
|
||
|
||
if (!isOpen) return null
|
||
|
||
return (
|
||
<div className="modal fade show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
||
<div className="modal-dialog modal-lg">
|
||
<div className="modal-content">
|
||
<div className="modal-header">
|
||
<h5 className="modal-title">
|
||
<i className="bi bi-palette me-2"></i>
|
||
{t('customization.title')}
|
||
</h5>
|
||
<button
|
||
type="button"
|
||
className="btn-close"
|
||
onClick={onClose}
|
||
></button>
|
||
</div>
|
||
|
||
<div className="modal-body">
|
||
{/* Вкладки */}
|
||
<ul className="nav nav-tabs mb-3">
|
||
<li className="nav-item">
|
||
<button
|
||
className={`nav-link ${activeTab === 'layout' ? 'active' : ''}`}
|
||
onClick={() => setActiveTab('layout')}
|
||
>
|
||
<i className="bi bi-layout-sidebar me-1"></i>
|
||
{t('customization.layout')}
|
||
</button>
|
||
</li>
|
||
<li className="nav-item">
|
||
<button
|
||
className={`nav-link ${activeTab === 'colors' ? 'active' : ''}`}
|
||
onClick={() => setActiveTab('colors')}
|
||
>
|
||
<i className="bi bi-palette-fill me-1"></i>
|
||
{t('customization.colors')}
|
||
</button>
|
||
</li>
|
||
<li className="nav-item">
|
||
<button
|
||
className={`nav-link ${activeTab === 'groups' ? 'active' : ''}`}
|
||
onClick={() => setActiveTab('groups')}
|
||
>
|
||
<i className="bi bi-collection me-1"></i>
|
||
{t('customization.groups')}
|
||
</button>
|
||
</li>
|
||
<li className="nav-item">
|
||
<button
|
||
className={`nav-link ${activeTab === 'templates' ? 'active' : ''}`}
|
||
onClick={() => setActiveTab('templates')}
|
||
>
|
||
<i className="bi bi-palette me-1"></i>
|
||
{t('customization.templates')}
|
||
</button>
|
||
</li>
|
||
<li className="nav-item">
|
||
<button
|
||
className={`nav-link ${activeTab === 'advanced' ? 'active' : ''}`}
|
||
onClick={() => setActiveTab('advanced')}
|
||
>
|
||
<i className="bi bi-gear me-1"></i>
|
||
{t('customization.advanced')}
|
||
</button>
|
||
</li>
|
||
<li className="nav-item">
|
||
<button
|
||
className={`nav-link ${activeTab === 'data' ? 'active' : ''}`}
|
||
onClick={() => setActiveTab('data')}
|
||
>
|
||
<i className="bi bi-database me-1"></i>
|
||
{t('customization.data')}
|
||
</button>
|
||
</li>
|
||
</ul>
|
||
|
||
{/* Содержимое вкладок */}
|
||
<div className="tab-content">
|
||
|
||
{/* Вкладка: Макет */}
|
||
{activeTab === 'layout' && (
|
||
<div className="tab-pane fade show active">
|
||
<div className="row">
|
||
<div className="col-12 mb-4">
|
||
<label className="form-label fs-5 mb-3">
|
||
<i className="bi bi-layout-text-window-reverse me-2"></i>
|
||
{t('customization.layout.style')}
|
||
</label>
|
||
<div className="row g-3">
|
||
{[
|
||
{
|
||
value: 'list',
|
||
labelKey: 'customization.layout.list',
|
||
icon: 'bi-list-ul',
|
||
descriptionKey: 'customization.layout.listDescription'
|
||
},
|
||
{
|
||
value: 'grid',
|
||
labelKey: 'customization.layout.grid',
|
||
icon: 'bi-grid-3x3',
|
||
descriptionKey: 'customization.layout.gridDescription'
|
||
},
|
||
{
|
||
value: 'cards',
|
||
labelKey: 'customization.layout.cards',
|
||
icon: 'bi-card-heading',
|
||
descriptionKey: 'customization.layout.cardsDescription'
|
||
},
|
||
{
|
||
value: 'compact',
|
||
labelKey: 'customization.layout.compact',
|
||
icon: 'bi-layout-text-sidebar',
|
||
descriptionKey: 'customization.layout.compactDescription'
|
||
},
|
||
{
|
||
value: 'sidebar',
|
||
labelKey: 'customization.layout.sidebar',
|
||
icon: 'bi-layout-sidebar',
|
||
descriptionKey: 'customization.layout.sidebarDescription'
|
||
},
|
||
{
|
||
value: 'masonry',
|
||
labelKey: 'customization.layout.masonry',
|
||
icon: 'bi-bricks',
|
||
descriptionKey: 'customization.layout.masonryDescription'
|
||
},
|
||
{
|
||
value: 'timeline',
|
||
labelKey: 'customization.layout.timeline',
|
||
icon: 'bi-clock-history',
|
||
descriptionKey: 'customization.layout.timelineDescription'
|
||
},
|
||
{
|
||
value: 'magazine',
|
||
labelKey: 'customization.layout.magazine',
|
||
icon: 'bi-newspaper',
|
||
descriptionKey: 'customization.layout.magazineDescription'
|
||
},
|
||
{
|
||
value: 'test-list',
|
||
labelKey: 'customization.layout.testList',
|
||
icon: 'bi-list-check',
|
||
descriptionKey: 'customization.layout.testListDescription'
|
||
}
|
||
].map((layout) => (
|
||
<div key={layout.value} className="col-md-6 col-lg-4">
|
||
<div
|
||
className={`card text-center h-100 layout-option ${settings.dashboard_layout === layout.value ? 'border-primary bg-primary bg-opacity-10 selected' : 'border-secondary'}`}
|
||
style={{ cursor: 'pointer', transition: 'all 0.2s ease' }}
|
||
onClick={() => handleChange('dashboard_layout', layout.value)}
|
||
>
|
||
<div className="card-body d-flex flex-column">
|
||
<i className={`${layout.icon} fs-1 mb-3 text-primary`}></i>
|
||
<h6 className="card-title mb-2">{t(layout.labelKey)}</h6>
|
||
<p className="card-text small text-muted flex-grow-1">{t(layout.descriptionKey)}</p>
|
||
{settings.dashboard_layout === layout.value && (
|
||
<div className="mt-2">
|
||
<span className="badge bg-primary">
|
||
<i className="bi bi-check-lg me-1"></i>
|
||
Выбрано
|
||
</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="col-12 mb-3">
|
||
<div className="alert alert-info">
|
||
<i className="bi bi-info-circle me-2"></i>
|
||
<strong>Совет:</strong> Попробуйте разные макеты, чтобы найти наиболее подходящий для вашего контента.
|
||
Каждый стиль имеет свои преимущества в зависимости от количества ссылок и их типа.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Вкладка: Цвета */}
|
||
{activeTab === 'colors' && (
|
||
<div className="tab-pane fade show active">
|
||
<div className="row">
|
||
<div className="col-md-6 mb-3">
|
||
<label className="form-label">{t('customization.colors.theme')}</label>
|
||
<div className="input-group">
|
||
<input
|
||
type="color"
|
||
className="form-control form-control-color"
|
||
value={settings.theme_color}
|
||
onChange={(e) => handleChange('theme_color', e.target.value)}
|
||
/>
|
||
<input
|
||
type="text"
|
||
className="form-control"
|
||
value={settings.theme_color}
|
||
onChange={(e) => handleChange('theme_color', e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="col-md-6 mb-3">
|
||
<label className="form-label">{t('customization.colors.background')}</label>
|
||
<div className="input-group">
|
||
<input
|
||
type="color"
|
||
className="form-control form-control-color"
|
||
value={settings.dashboard_background_color}
|
||
onChange={(e) => handleChange('dashboard_background_color', e.target.value)}
|
||
/>
|
||
<input
|
||
type="text"
|
||
className="form-control"
|
||
value={settings.dashboard_background_color}
|
||
onChange={(e) => handleChange('dashboard_background_color', e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="col-12 mb-3">
|
||
<label className="form-label">{t('customization.colors.backgroundImage')}</label>
|
||
<div className="mb-2">
|
||
<input
|
||
type="file"
|
||
className="form-control"
|
||
accept="image/*"
|
||
onChange={(e) => {
|
||
const file = e.target.files?.[0]
|
||
if (file) {
|
||
setBackgroundImageFile(file)
|
||
}
|
||
}}
|
||
/>
|
||
<div className="form-text">
|
||
{t('customization.colors.backgroundImageHelp')}
|
||
</div>
|
||
</div>
|
||
{settings.background_image_url && (
|
||
<div className="mb-2">
|
||
<label className="form-label small">{t('customization.colors.currentImage')}</label>
|
||
<div className="d-flex align-items-center gap-2">
|
||
<img
|
||
src={settings.background_image_url}
|
||
alt="Текущий фон"
|
||
className="img-thumbnail"
|
||
style={{ maxWidth: '200px', maxHeight: '100px', objectFit: 'cover' }}
|
||
/>
|
||
<button
|
||
type="button"
|
||
className="btn btn-outline-danger btn-sm"
|
||
onClick={() => {
|
||
handleChange('background_image_url', '')
|
||
}}
|
||
title={t('customization.colors.removeBackground')}
|
||
>
|
||
<i className="bi bi-trash"></i> {t('customization.colors.removeBackground')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{backgroundImageFile && (
|
||
<div className="mb-2">
|
||
<label className="form-label small">{t('customization.colors.newImage')}</label>
|
||
<div className="text-success">
|
||
<i className="bi bi-file-earmark-image me-1"></i>
|
||
{backgroundImageFile.name}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="col-md-4 mb-3">
|
||
<label className="form-label">{t('customization.colors.header')}</label>
|
||
<div className="input-group">
|
||
<input
|
||
type="color"
|
||
className="form-control form-control-color"
|
||
value={settings.header_text_color || '#000000'}
|
||
onChange={(e) => handleChange('header_text_color', e.target.value)}
|
||
/>
|
||
<input
|
||
type="text"
|
||
className="form-control"
|
||
value={settings.header_text_color || '#000000'}
|
||
onChange={(e) => handleChange('header_text_color', e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="col-md-4 mb-3">
|
||
<label className="form-label">{t('customization.colors.group')}</label>
|
||
<div className="input-group">
|
||
<input
|
||
type="color"
|
||
className="form-control form-control-color"
|
||
value={settings.group_text_color || '#333333'}
|
||
onChange={(e) => handleChange('group_text_color', e.target.value)}
|
||
/>
|
||
<input
|
||
type="text"
|
||
className="form-control"
|
||
value={settings.group_text_color || '#333333'}
|
||
onChange={(e) => handleChange('group_text_color', e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="col-md-4 mb-3">
|
||
<label className="form-label">{t('customization.colors.link')}</label>
|
||
<div className="input-group">
|
||
<input
|
||
type="color"
|
||
className="form-control form-control-color"
|
||
value={settings.link_text_color || '#666666'}
|
||
onChange={(e) => handleChange('link_text_color', e.target.value)}
|
||
/>
|
||
<input
|
||
type="text"
|
||
className="form-control"
|
||
value={settings.link_text_color || '#666666'}
|
||
onChange={(e) => handleChange('link_text_color', e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Вкладка: Группы */}
|
||
{activeTab === 'groups' && (
|
||
<div className="tab-pane fade show active">
|
||
<div className="row">
|
||
<div className="col-12 mb-3">
|
||
<h6 className="text-muted">{t('customization.groups.displaySettings')}</h6>
|
||
</div>
|
||
<div className="col-12 mb-3">
|
||
<div className="form-check form-switch">
|
||
<input
|
||
className="form-check-input"
|
||
type="checkbox"
|
||
checked={settings.groups_default_expanded}
|
||
onChange={(e) => handleChange('groups_default_expanded', e.target.checked)}
|
||
/>
|
||
<label className="form-check-label">
|
||
Развернуть группы по умолчанию
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div className="col-12 mb-3">
|
||
<div className="form-check form-switch">
|
||
<input
|
||
className="form-check-input"
|
||
type="checkbox"
|
||
checked={settings.show_group_icons}
|
||
onChange={(e) => handleChange('show_group_icons', e.target.checked)}
|
||
/>
|
||
<label className="form-check-label">
|
||
Показывать иконки групп
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div className="col-12 mb-3">
|
||
<div className="form-check form-switch">
|
||
<input
|
||
className="form-check-input"
|
||
type="checkbox"
|
||
checked={settings.show_link_icons}
|
||
onChange={(e) => handleChange('show_link_icons', e.target.checked)}
|
||
/>
|
||
<label className="form-check-label">
|
||
Показывать иконки ссылок
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Новые настройки */}
|
||
<div className="col-12 mb-3">
|
||
<div className="form-check form-switch">
|
||
<input
|
||
className="form-check-input"
|
||
type="checkbox"
|
||
checked={settings.show_groups_title !== false}
|
||
onChange={(e) => handleChange('show_groups_title', e.target.checked)}
|
||
/>
|
||
<label className="form-check-label">
|
||
Показывать заголовок "Группы ссылок"
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="col-md-6 mb-3">
|
||
<label className="form-label">Цвет описаний групп</label>
|
||
<div className="input-group">
|
||
<input
|
||
type="color"
|
||
className="form-control form-control-color"
|
||
value={settings.group_description_text_color || '#666666'}
|
||
onChange={(e) => handleChange('group_description_text_color', e.target.value)}
|
||
/>
|
||
<input
|
||
type="text"
|
||
className="form-control"
|
||
value={settings.group_description_text_color || '#666666'}
|
||
onChange={(e) => handleChange('group_description_text_color', e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Перекрытие групп цветом */}
|
||
<div className="col-12 mb-3">
|
||
<div className="card">
|
||
<div className="card-header">
|
||
<h6 className="mb-0">Цветовое перекрытие групп</h6>
|
||
</div>
|
||
<div className="card-body">
|
||
<div className="form-check mb-3">
|
||
<input
|
||
className="form-check-input"
|
||
type="checkbox"
|
||
id="groupOverlayEnabled"
|
||
checked={settings.group_overlay_enabled || false}
|
||
onChange={(e) => handleChange('group_overlay_enabled', e.target.checked)}
|
||
/>
|
||
<label className="form-check-label" htmlFor="groupOverlayEnabled">
|
||
Включить цветовое перекрытие групп
|
||
</label>
|
||
</div>
|
||
|
||
{settings.group_overlay_enabled && (
|
||
<>
|
||
<div className="row">
|
||
<div className="col-6 mb-3">
|
||
<label className="form-label">Цвет перекрытия</label>
|
||
<div className="d-flex gap-2">
|
||
<input
|
||
type="color"
|
||
className="form-control form-control-color"
|
||
value={settings.group_overlay_color || '#000000'}
|
||
onChange={(e) => handleChange('group_overlay_color', e.target.value)}
|
||
title="Выберите цвет перекрытия"
|
||
/>
|
||
<input
|
||
type="text"
|
||
className="form-control"
|
||
value={settings.group_overlay_color || '#000000'}
|
||
onChange={(e) => handleChange('group_overlay_color', e.target.value)}
|
||
placeholder="#000000"
|
||
title="Hex код цвета"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="col-6 mb-3">
|
||
<label className="form-label">
|
||
Прозрачность ({Math.round((settings.group_overlay_opacity || 0.3) * 100)}%)
|
||
</label>
|
||
<input
|
||
type="range"
|
||
className="form-range"
|
||
min="0"
|
||
max="1"
|
||
step="0.1"
|
||
value={settings.group_overlay_opacity || 0.3}
|
||
onChange={(e) => handleChange('group_overlay_opacity', parseFloat(e.target.value))}
|
||
title="Настройка прозрачности перекрытия"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Preview */}
|
||
<div className="mb-3">
|
||
<label className="form-label">Предварительный просмотр</label>
|
||
<div className="position-relative rounded" style={{ height: '80px', border: '1px solid #dee2e6', overflow: 'hidden' }}>
|
||
<div
|
||
className="w-100 h-100 d-flex align-items-center justify-content-center text-white fw-bold"
|
||
style={{
|
||
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
|
||
}}
|
||
>
|
||
Пример группы
|
||
</div>
|
||
<div
|
||
className="position-absolute top-0 start-0 w-100 h-100"
|
||
style={{
|
||
backgroundColor: settings.group_overlay_color || '#000000',
|
||
opacity: settings.group_overlay_opacity || 0.3
|
||
}}
|
||
></div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="col-12">
|
||
<div className="alert alert-info">
|
||
<i className="bi bi-info-circle me-2"></i>
|
||
<strong>Настройки отдельных групп</strong><br/>
|
||
Чтобы настроить конкретную группу (публичность, избранное, разворачивание), используйте кнопку редактирования рядом с названием группы в основном списке.
|
||
</div>
|
||
</div>
|
||
|
||
{/* Секция цветового оверлея кнопок ссылок */}
|
||
<div className="col-12 mt-4">
|
||
<div className="border rounded p-3">
|
||
<div className="d-flex align-items-center justify-content-between mb-3">
|
||
<h6 className="mb-0">Цветовое перекрытие кнопок ссылок</h6>
|
||
<div className="form-check form-switch">
|
||
<input
|
||
className="form-check-input"
|
||
type="checkbox"
|
||
id="linkOverlay"
|
||
checked={settings.link_overlay_enabled || false}
|
||
onChange={(e) => {
|
||
console.log('Link overlay enabled:', e.target.checked)
|
||
handleChange('link_overlay_enabled', e.target.checked)
|
||
}}
|
||
/>
|
||
<label className="form-check-label" htmlFor="linkOverlay">
|
||
Включить цветовое перекрытие кнопок ссылок
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="row">
|
||
{settings.link_overlay_enabled && (
|
||
<>
|
||
<div className="col-md-6 mb-3">
|
||
<label className="form-label">Цвет перекрытия</label>
|
||
<input
|
||
type="color"
|
||
className="form-control form-control-color"
|
||
value={settings.link_overlay_color || '#000000'}
|
||
onChange={(e) => {
|
||
console.log('Link overlay color:', e.target.value)
|
||
handleChange('link_overlay_color', e.target.value)
|
||
}}
|
||
/>
|
||
</div>
|
||
<div className="col-md-6 mb-3">
|
||
<label className="form-label">
|
||
Прозрачность ({Math.round((settings.link_overlay_opacity || 0.2) * 100)}%)
|
||
</label>
|
||
<input
|
||
type="range"
|
||
className="form-range"
|
||
min="0"
|
||
max="1"
|
||
step="0.1"
|
||
value={settings.link_overlay_opacity || 0.2}
|
||
onChange={(e) => {
|
||
const value = parseFloat(e.target.value)
|
||
console.log('Link overlay opacity:', value)
|
||
handleChange('link_overlay_opacity', value)
|
||
}}
|
||
/>
|
||
</div>
|
||
<div className="col-12">
|
||
<label className="form-label">Предварительный просмотр</label>
|
||
<div className="position-relative rounded" style={{ height: '60px', border: '1px solid #dee2e6', overflow: 'hidden' }}>
|
||
<div
|
||
className="position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center"
|
||
style={{
|
||
backgroundColor: '#f8f9fa',
|
||
color: '#333',
|
||
fontSize: '14px',
|
||
fontWeight: '500'
|
||
}}
|
||
>
|
||
Кнопка ссылки
|
||
</div>
|
||
<div
|
||
className="position-absolute top-0 start-0 w-100 h-100"
|
||
style={{
|
||
backgroundColor: settings.link_overlay_color || '#000000',
|
||
opacity: settings.link_overlay_opacity || 0.2,
|
||
}}
|
||
></div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Вкладка: Шаблоны */}
|
||
{activeTab === 'templates' && (
|
||
<div className="tab-pane fade show active">
|
||
<TemplatesSelector
|
||
onTemplateSelect={handleTemplateSelect}
|
||
currentTemplate={getCurrentTemplateId()}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* Вкладка: Дополнительно */}
|
||
{activeTab === 'advanced' && (
|
||
<div className="tab-pane fade show active">
|
||
<div className="row">
|
||
<div className="col-12 mb-4">
|
||
<h6 className="text-muted">{t('customization.advanced.fontSettings')}</h6>
|
||
</div>
|
||
<div className="col-md-6 mb-3">
|
||
<label className="form-label">{t('customization.advanced.mainFont')}</label>
|
||
<select
|
||
className="form-select"
|
||
value={settings.font_family}
|
||
onChange={(e) => handleChange('font_family', e.target.value)}
|
||
>
|
||
<option value="sans-serif">Системный Sans Serif</option>
|
||
<option value="serif">Системный Serif</option>
|
||
<option value="monospace">Monospace</option>
|
||
<option value="'PT Sans', sans-serif">PT Sans</option>
|
||
<option value="'PT Serif', serif">PT Serif</option>
|
||
<option value="'Roboto', sans-serif">Roboto</option>
|
||
<option value="'Open Sans', sans-serif">Open Sans</option>
|
||
<option value="'Source Sans Pro', sans-serif">Source Sans Pro</option>
|
||
<option value="'Fira Sans', sans-serif">Fira Sans</option>
|
||
<option value="'Ubuntu', sans-serif">Ubuntu</option>
|
||
<option value="'Yandex Sans Text', sans-serif">Yandex Sans Text</option>
|
||
<option value="'Inter', sans-serif">Inter</option>
|
||
<option value="'Manrope', sans-serif">Manrope</option>
|
||
<option value="'Nunito Sans', sans-serif">Nunito Sans</option>
|
||
</select>
|
||
</div>
|
||
<div className="col-md-6 mb-3">
|
||
<label className="form-label">{t('customization.advanced.headingFont')}</label>
|
||
<select
|
||
className="form-select"
|
||
value={settings.heading_font_family || settings.font_family}
|
||
onChange={(e) => handleChange('heading_font_family', e.target.value)}
|
||
>
|
||
<option value="">Как основной</option>
|
||
<option value="'PT Sans', sans-serif">PT Sans</option>
|
||
<option value="'PT Serif', serif">PT Serif</option>
|
||
<option value="'Roboto', sans-serif">Roboto</option>
|
||
<option value="'Open Sans', sans-serif">Open Sans</option>
|
||
<option value="'Source Sans Pro', sans-serif">Source Sans Pro</option>
|
||
<option value="'Fira Sans', sans-serif">Fira Sans</option>
|
||
<option value="'Ubuntu', sans-serif">Ubuntu</option>
|
||
<option value="'Yandex Sans Text', sans-serif">Yandex Sans Text</option>
|
||
<option value="'Inter', sans-serif">Inter</option>
|
||
<option value="'Manrope', sans-serif">Manrope</option>
|
||
<option value="'Montserrat', sans-serif">Montserrat</option>
|
||
<option value="'Playfair Display', serif">Playfair Display</option>
|
||
<option value="'Merriweather', serif">Merriweather</option>
|
||
<option value="'Oswald', sans-serif">Oswald</option>
|
||
<option value="'Russo One', sans-serif">Russo One</option>
|
||
<option value="'Comfortaa', cursive">Comfortaa</option>
|
||
<option value="'Philosopher', sans-serif">Philosopher</option>
|
||
<option value="'Cormorant Garamond', serif">Cormorant Garamond</option>
|
||
<option value="'Marck Script', cursive">Marck Script</option>
|
||
</select>
|
||
</div>
|
||
<div className="col-md-6 mb-3">
|
||
<label className="form-label">{t('customization.advanced.bodyFont')}</label>
|
||
<select
|
||
className="form-select"
|
||
value={settings.body_font_family || settings.font_family}
|
||
onChange={(e) => handleChange('body_font_family', e.target.value)}
|
||
>
|
||
<option value="">Как основной</option>
|
||
<option value="'PT Sans', sans-serif">PT Sans</option>
|
||
<option value="'PT Serif', serif">PT Serif</option>
|
||
<option value="'Roboto', sans-serif">Roboto</option>
|
||
<option value="'Open Sans', sans-serif">Open Sans</option>
|
||
<option value="'Source Sans Pro', sans-serif">Source Sans Pro</option>
|
||
<option value="'Fira Sans', sans-serif">Fira Sans</option>
|
||
<option value="'Ubuntu', sans-serif">Ubuntu</option>
|
||
<option value="'Yandex Sans Text', sans-serif">Yandex Sans Text</option>
|
||
<option value="'Inter', sans-serif">Inter</option>
|
||
<option value="'Manrope', sans-serif">Manrope</option>
|
||
<option value="'Nunito Sans', sans-serif">Nunito Sans</option>
|
||
<option value="'Lato', sans-serif">Lato</option>
|
||
<option value="'Source Serif Pro', serif">Source Serif Pro</option>
|
||
<option value="'Crimson Text', serif">Crimson Text</option>
|
||
<option value="Inter, sans-serif">Inter</option>
|
||
<option value="Roboto, sans-serif">Roboto</option>
|
||
<option value="Open Sans, sans-serif">Open Sans</option>
|
||
<option value="Source Sans Pro, sans-serif">Source Sans Pro</option>
|
||
<option value="Lato, sans-serif">Lato</option>
|
||
<option value="Nunito, sans-serif">Nunito</option>
|
||
<option value="Georgia, serif">Georgia</option>
|
||
<option value="Times New Roman, serif">Times New Roman</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div className="col-12 mb-4">
|
||
<hr />
|
||
<h6 className="text-muted">{t('customization.advanced.additionalSettings')}</h6>
|
||
</div>
|
||
|
||
<div className="col-12 mb-3">
|
||
<label className="form-label">{t('customization.advanced.customCSS')}</label>
|
||
<textarea
|
||
className="form-control font-monospace"
|
||
rows={6}
|
||
value={settings.custom_css}
|
||
onChange={(e) => handleChange('custom_css', e.target.value)}
|
||
placeholder="/* Ваш дополнительный CSS */ .my-custom-class { color: #333; }"
|
||
/>
|
||
</div>
|
||
|
||
{/* Cover Overlay Section */}
|
||
<div className="col-12 mb-3">
|
||
<div className="card">
|
||
<div className="card-header">
|
||
<h6 className="mb-0">Перекрытие обложки</h6>
|
||
</div>
|
||
<div className="card-body">
|
||
<div className="form-check mb-3">
|
||
<input
|
||
className="form-check-input"
|
||
type="checkbox"
|
||
id="coverOverlayEnabled"
|
||
checked={settings.cover_overlay_enabled || false}
|
||
onChange={(e) => handleChange('cover_overlay_enabled', e.target.checked)}
|
||
/>
|
||
<label className="form-check-label" htmlFor="coverOverlayEnabled">
|
||
Включить цветовое перекрытие обложки
|
||
</label>
|
||
</div>
|
||
|
||
{settings.cover_overlay_enabled && (
|
||
<>
|
||
<div className="row">
|
||
<div className="col-6 mb-3">
|
||
<label className="form-label">Цвет перекрытия</label>
|
||
<div className="d-flex gap-2">
|
||
<input
|
||
type="color"
|
||
className="form-control form-control-color"
|
||
value={settings.cover_overlay_color || '#000000'}
|
||
onChange={(e) => handleChange('cover_overlay_color', e.target.value)}
|
||
title="Выберите цвет перекрытия"
|
||
/>
|
||
<input
|
||
type="text"
|
||
className="form-control"
|
||
value={settings.cover_overlay_color || '#000000'}
|
||
onChange={(e) => handleChange('cover_overlay_color', e.target.value)}
|
||
placeholder="#000000"
|
||
title="Hex код цвета"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div className="col-6 mb-3">
|
||
<label className="form-label">
|
||
Прозрачность ({Math.round((settings.cover_overlay_opacity || 0.3) * 100)}%)
|
||
</label>
|
||
<input
|
||
type="range"
|
||
className="form-range"
|
||
min="0"
|
||
max="1"
|
||
step="0.1"
|
||
value={settings.cover_overlay_opacity || 0.3}
|
||
onChange={(e) => handleChange('cover_overlay_opacity', parseFloat(e.target.value))}
|
||
title="Настройка прозрачности перекрытия"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Preview */}
|
||
<div className="mb-3">
|
||
<label className="form-label">Предварительный просмотр</label>
|
||
<div className="position-relative" style={{ height: '100px', border: '1px solid #dee2e6', borderRadius: '0.375rem', overflow: 'hidden' }}>
|
||
<div
|
||
className="w-100 h-100"
|
||
style={{
|
||
background: 'linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(-45deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(-45deg, transparent 75%, #ccc 75%)',
|
||
backgroundSize: '20px 20px',
|
||
backgroundPosition: '0 0, 0 10px, 10px -10px, -10px 0px'
|
||
}}
|
||
></div>
|
||
<div
|
||
className="position-absolute top-0 start-0 w-100 h-100"
|
||
style={{
|
||
backgroundColor: settings.cover_overlay_color || '#000000',
|
||
opacity: settings.cover_overlay_opacity || 0.3
|
||
}}
|
||
></div>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Вкладка: Данные */}
|
||
{activeTab === 'data' && (
|
||
<div className="tab-pane fade show active">
|
||
<div className="row">
|
||
<div className="col-12 mb-4">
|
||
<h6 className="text-muted">
|
||
<i className="bi bi-database me-2"></i>
|
||
Экспорт и импорт данных профиля
|
||
</h6>
|
||
<p className="text-muted small">
|
||
Создавайте резервные копии данных профиля или восстанавливайте их из архива
|
||
</p>
|
||
</div>
|
||
|
||
{/* Экспорт данных */}
|
||
<div className="col-12 mb-4">
|
||
<div className="card">
|
||
<div className="card-header">
|
||
<h6 className="card-title mb-0">
|
||
<i className="bi bi-upload me-2"></i>
|
||
Экспорт данных
|
||
</h6>
|
||
</div>
|
||
<div className="card-body">
|
||
<p className="text-muted small mb-3">
|
||
Создать архив с данными профиля для резервного копирования или переноса
|
||
</p>
|
||
|
||
<button
|
||
type="button"
|
||
className="btn btn-outline-primary"
|
||
onClick={() => setShowExportModal(true)}
|
||
>
|
||
<i className="bi bi-download me-2"></i>
|
||
Создать экспорт
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Импорт данных */}
|
||
<div className="col-12 mb-4">
|
||
<div className="card">
|
||
<div className="card-header">
|
||
<h6 className="card-title mb-0">
|
||
<i className="bi bi-upload me-2"></i>
|
||
Импорт данных
|
||
</h6>
|
||
</div>
|
||
<div className="card-body">
|
||
<p className="text-muted small mb-3">
|
||
Загрузить и восстановить данные из архива экспорта
|
||
</p>
|
||
|
||
<div className="mb-3">
|
||
<label className="form-label">Выберите файл архива (.zip)</label>
|
||
<input
|
||
type="file"
|
||
className="form-control"
|
||
accept=".zip"
|
||
onChange={(e) => {
|
||
// TODO: Обработать загрузку файла и показать превью
|
||
const file = e.target.files?.[0]
|
||
if (file) {
|
||
console.log('Файл выбран:', file.name)
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
<button
|
||
type="button"
|
||
className="btn btn-outline-success"
|
||
onClick={() => setShowImportModal(true)}
|
||
>
|
||
<i className="bi bi-upload me-2"></i>
|
||
Открыть мастер импорта
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* История операций */}
|
||
<div className="col-12">
|
||
<div className="card">
|
||
<div className="card-header">
|
||
<h6 className="card-title mb-0">
|
||
<i className="bi bi-clock-history me-2"></i>
|
||
История операций
|
||
</h6>
|
||
</div>
|
||
<div className="card-body">
|
||
<p className="text-muted">
|
||
Здесь будет отображаться история экспортов и импортов
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="modal-footer">
|
||
<button
|
||
type="button"
|
||
className="btn btn-secondary"
|
||
onClick={onClose}
|
||
>
|
||
Отмена
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="btn btn-outline-warning"
|
||
onClick={() => {
|
||
if (confirm(t('customization.advanced.resetConfirm'))) {
|
||
// Сброс к дефолтным настройкам
|
||
const defaultSettings = {
|
||
theme_color: '#007bff',
|
||
background_image_url: '',
|
||
dashboard_layout: 'list' as const,
|
||
groups_default_expanded: true,
|
||
show_group_icons: true,
|
||
show_link_icons: true,
|
||
dashboard_background_color: '#ffffff',
|
||
font_family: 'Inter, sans-serif',
|
||
custom_css: '',
|
||
group_text_color: '',
|
||
link_text_color: '',
|
||
header_text_color: '',
|
||
cover_overlay_enabled: false,
|
||
cover_overlay_color: '#000000',
|
||
cover_overlay_opacity: 0.3,
|
||
group_overlay_enabled: false,
|
||
group_overlay_color: '#000000',
|
||
group_overlay_opacity: 0.3,
|
||
show_groups_title: true,
|
||
group_description_text_color: '',
|
||
body_font_family: 'Inter, sans-serif',
|
||
heading_font_family: 'Inter, sans-serif',
|
||
link_overlay_enabled: false,
|
||
link_overlay_color: '#000000',
|
||
link_overlay_opacity: 0.3
|
||
}
|
||
setSettings(defaultSettings)
|
||
onSettingsUpdate(defaultSettings)
|
||
}
|
||
}}
|
||
disabled={loading}
|
||
title={t('customization.resetSettings')}
|
||
>
|
||
<i className="bi bi-arrow-counterclockwise me-2"></i>
|
||
{t('customization.resetSettings')}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="btn btn-primary"
|
||
onClick={saveSettings}
|
||
disabled={loading}
|
||
>
|
||
{loading ? (
|
||
<>
|
||
<span className="spinner-border spinner-border-sm me-2"></span>
|
||
{t('common.saving')}
|
||
</>
|
||
) : (
|
||
<>
|
||
<i className="bi bi-check-lg me-2"></i>
|
||
{t('common.save')}
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Модалы экспорта и импорта */}
|
||
<ExportDataModal
|
||
isOpen={showExportModal}
|
||
onClose={() => setShowExportModal(false)}
|
||
user={user || null}
|
||
groups={groups}
|
||
/>
|
||
|
||
<ImportDataModal
|
||
isOpen={showImportModal}
|
||
onClose={() => setShowImportModal(false)}
|
||
onImportComplete={() => {
|
||
if (onDataUpdate) {
|
||
onDataUpdate()
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
)
|
||
} |