diff --git a/backend/customization/migrations/0008_add_template_id_and_test_list_layout.py b/backend/customization/migrations/0008_add_template_id_and_test_list_layout.py new file mode 100644 index 0000000..cc3559f --- /dev/null +++ b/backend/customization/migrations/0008_add_template_id_and_test_list_layout.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.8 on 2025-11-09 02:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('customization', '0007_designsettings_body_font_family_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='designsettings', + name='template_id', + field=models.CharField(blank=True, help_text='ID выбранного дизайн-шаблона', max_length=50, null=True), + ), + migrations.AlterField( + model_name='designsettings', + name='dashboard_layout', + field=models.CharField(choices=[('sidebar', 'Боковая панель'), ('grid', 'Сетка'), ('list', 'Список'), ('cards', 'Карточки'), ('compact', 'Компактный'), ('masonry', 'Кладка'), ('timeline', 'Временная линия'), ('magazine', 'Журнальный'), ('test-list', 'Тестовый список')], default='list', help_text='Стиль отображения дашборда', max_length=20), + ), + ] diff --git a/backend/customization/models.py b/backend/customization/models.py index bb58ba8..9e79a1d 100644 --- a/backend/customization/models.py +++ b/backend/customization/models.py @@ -38,6 +38,7 @@ class DesignSettings(models.Model): ('masonry', 'Кладка'), ('timeline', 'Временная линия'), ('magazine', 'Журнальный'), + ('test-list', 'Тестовый список'), ], default='list', help_text='Стиль отображения дашборда' @@ -140,6 +141,14 @@ class DesignSettings(models.Model): help_text='Шрифт для заголовков' ) + # ID выбранного шаблона + template_id = models.CharField( + max_length=50, + blank=True, + null=True, + help_text='ID выбранного дизайн-шаблона' + ) + updated_at = models.DateTimeField( auto_now=True, help_text='Дата и время последнего изменения' diff --git a/backend/customization/serializers.py b/backend/customization/serializers.py index a7db70c..e7437a9 100644 --- a/backend/customization/serializers.py +++ b/backend/customization/serializers.py @@ -12,6 +12,7 @@ class DesignSettingsSerializer(serializers.ModelSerializer): model = DesignSettings fields = [ 'id', + 'template_id', 'theme_color', 'background_image', 'background_image_url', @@ -120,7 +121,7 @@ class DesignSettingsSerializer(serializers.ModelSerializer): """ Валидация типа макета дашборда """ - valid_layouts = ['sidebar', 'grid', 'list', 'cards', 'compact', 'masonry', 'timeline', 'magazine'] + valid_layouts = ['sidebar', 'grid', 'list', 'cards', 'compact', 'masonry', 'timeline', 'magazine', 'test-list'] if value not in valid_layouts: raise serializers.ValidationError(f'Макет должен быть одним из: {", ".join(valid_layouts)}') return value @@ -252,6 +253,14 @@ class DesignSettingsSerializer(serializers.ModelSerializer): raise serializers.ValidationError('Название шрифта слишком длинное') return value + def validate_template_id(self, value): + """ + Валидация ID шаблона + """ + if value and len(value) > 50: + raise serializers.ValidationError('ID шаблона слишком длинный') + return value + class PublicDesignSettingsSerializer(serializers.ModelSerializer): """ diff --git a/frontend/linktree-frontend/src/app/[username]/TestListLayout.module.css b/frontend/linktree-frontend/src/app/[username]/TestListLayout.module.css new file mode 100644 index 0000000..b537fd5 --- /dev/null +++ b/frontend/linktree-frontend/src/app/[username]/TestListLayout.module.css @@ -0,0 +1,133 @@ +/* Стили для макета "Тестовый список" */ +.testListLayout { + max-width: 800px; + margin: 0 auto; + /* Применяем пользовательские шрифты через CSS переменные */ + font-family: var(--user-font-family, 'Inter', sans-serif); +} + +.testListLayout h5 { + font-family: var(--user-heading-font-family, var(--user-font-family, 'Inter', sans-serif)); +} + +.linkGroup { + margin-bottom: 2rem; + border: none; + background: transparent; +} + +.groupHeader { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: white; + padding: 1rem; + border-radius: 8px; + margin-bottom: 0.75rem; + font-weight: 600; + font-size: 1.125rem; + display: flex; + align-items: center; + font-family: var(--user-heading-font-family, var(--user-font-family, 'Inter', sans-serif)); +} + +.groupLinks { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0; + background: transparent; +} + +.linkItem { + background: white; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 0.75rem 1rem; + transition: all 0.2s ease; + display: flex; + align-items: center; + text-decoration: none; + color: #475569; + font-family: var(--user-body-font-family, var(--user-font-family, 'Inter', sans-serif)); +} + +.linkItem:hover { + background: #f1f5f9; + border-color: #6366f1; + transform: translateX(4px); + color: inherit; + text-decoration: none; +} + +.linkIcon { + margin-right: 0.75rem; + width: 20px; + height: 20px; + flex-shrink: 0; + border-radius: 4px; + object-fit: cover; +} + +.linkTitle { + font-weight: 500; + color: #334155; + flex-grow: 1; + font-family: inherit; +} + +.linkDescription { + color: #6b7280; + font-size: 0.875rem; + margin-left: auto; + opacity: 0.8; + font-family: inherit; +} + +/* Для групп с иконками в заголовке */ +.groupHeader .linkIcon { + margin-right: 0.5rem; + width: 24px; + height: 24px; +} + +/* Адаптивность */ +@media (max-width: 768px) { + .testListLayout { + max-width: 100%; + padding: 0 1rem; + } + + .groupHeader { + padding: 0.75rem; + font-size: 1rem; + } + + .linkItem { + padding: 0.5rem 0.75rem; + } + + .linkDescription { + display: none; + } +} + +/* Темная тема поддержка */ +@media (prefers-color-scheme: dark) { + .linkItem { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.2); + color: #e2e8f0; + } + + .linkItem:hover { + background: rgba(255, 255, 255, 0.2); + border-color: #8b5cf6; + } + + .linkTitle { + color: #f1f5f9; + } + + .linkDescription { + color: #cbd5e1; + } +} \ No newline at end of file diff --git a/frontend/linktree-frontend/src/app/[username]/page.tsx b/frontend/linktree-frontend/src/app/[username]/page.tsx index a6ac660..d540f67 100644 --- a/frontend/linktree-frontend/src/app/[username]/page.tsx +++ b/frontend/linktree-frontend/src/app/[username]/page.tsx @@ -7,6 +7,7 @@ import Link from 'next/link' import { useEffect, useState, Fragment } from 'react' import FontLoader from '../components/FontLoader' import ExpandableGroup from '../components/ExpandableGroup' +import styles from './TestListLayout.module.css' interface LinkItem { id: number @@ -836,9 +837,79 @@ export default function UserPage({ ) + // Тестовый список - все группы и ссылки в одном списке + const renderTestListLayout = () => ( +
+ {(designSettings.show_groups_title !== false) && ( +
+ Все группы и ссылки +
+ )} + {data!.groups.map((group) => ( +
+ {/* Заголовок группы */} +
+ {group.icon_url && designSettings.show_group_icons && ( + {group.name} + )} + {group.name} + {group.is_favorite && ( + + )} +
+ + {/* Описание группы если есть */} + {group.description && ( +

+ {group.description} +

+ )} + + {/* Ссылки группы */} +
+ {group.links.map((link) => ( + + {link.icon_url && designSettings.show_link_icons && ( + {link.title} + )} + {link.title} + {link.description && ( + {link.description} + )} + + ))} +
+
+ ))} +
+ ) + // Основная функция рендеринга групп в зависимости от выбранного макета const renderGroupsLayout = () => { switch (designSettings.dashboard_layout) { + case 'test-list': + return renderTestListLayout() case 'list': return renderListLayout() case 'grid': @@ -888,8 +959,17 @@ export default function UserPage({ backgroundAttachment: 'fixed', minHeight: '100vh', paddingTop: '2rem', // отступ сверху для рамки фона - paddingBottom: '2rem' // отступ снизу для рамки фона - } + paddingBottom: '2rem', // отступ снизу для рамки фона + // CSS переменные для использования в стилях + '--user-font-family': designSettings.font_family, + '--user-heading-font-family': designSettings.heading_font_family || designSettings.font_family, + '--user-body-font-family': designSettings.body_font_family || designSettings.font_family, + '--user-theme-color': designSettings.theme_color, + '--user-header-text-color': designSettings.header_text_color || designSettings.theme_color, + '--user-group-text-color': designSettings.group_text_color || '#333333', + '--user-link-text-color': designSettings.link_text_color || '#666666', + '--user-group-description-text-color': designSettings.group_description_text_color || '#666666' + } as React.CSSProperties return ( <> diff --git a/frontend/linktree-frontend/src/app/components/CustomizationPanel.tsx b/frontend/linktree-frontend/src/app/components/CustomizationPanel.tsx index 851020c..f591d19 100644 --- a/frontend/linktree-frontend/src/app/components/CustomizationPanel.tsx +++ b/frontend/linktree-frontend/src/app/components/CustomizationPanel.tsx @@ -6,6 +6,7 @@ import { designTemplates, DesignTemplate } from '../constants/designTemplates' interface DesignSettings { id?: number + template_id?: string theme_color: string background_image?: string background_image_url?: string @@ -92,6 +93,7 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate }: Custom 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()) @@ -135,6 +137,7 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate }: Custom } 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, @@ -144,7 +147,6 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate }: Custom font_family: settings.font_family, custom_css: settings.custom_css, header_text_color: settings.header_text_color || '#000000', - 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, @@ -195,10 +197,31 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate }: Custom setSettings(prev => ({ ...prev, ...template.settings, - id: prev.id // Сохраняем оригинальный ID + 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 ( @@ -328,6 +351,12 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate }: Custom label: 'Журнальный', icon: 'bi-newspaper', description: 'Стиль журнала с крупными изображениями' + }, + { + value: 'test-list', + label: 'Тестовый список', + icon: 'bi-list-check', + description: 'Полный несворачиваемый список всех групп и ссылок' } ].map((layout) => (
@@ -683,7 +712,7 @@ export function CustomizationPanel({ isOpen, onClose, onSettingsUpdate }: Custom
)} diff --git a/frontend/linktree-frontend/src/app/components/TemplatesSelector.tsx b/frontend/linktree-frontend/src/app/components/TemplatesSelector.tsx index dc50892..c5e8c52 100644 --- a/frontend/linktree-frontend/src/app/components/TemplatesSelector.tsx +++ b/frontend/linktree-frontend/src/app/components/TemplatesSelector.tsx @@ -2,7 +2,6 @@ import React from 'react' import { designTemplates, DesignTemplate } from '../constants/designTemplates' -import styles from './TemplatesSelector.module.css' interface TemplatesSelectorProps { onTemplateSelect: (template: DesignTemplate) => void @@ -10,13 +9,16 @@ interface TemplatesSelectorProps { } export function TemplatesSelector({ onTemplateSelect, currentTemplate }: TemplatesSelectorProps) { - const handleTemplateClick = (template: DesignTemplate) => { + const handleTemplateClick = (template: DesignTemplate, event: React.MouseEvent) => { + event.preventDefault() + event.stopPropagation() console.log('Template clicked:', template.name) + console.log('onTemplateSelect function:', onTemplateSelect) onTemplateSelect(template) } return ( -
+
Готовые шаблоны @@ -24,90 +26,72 @@ export function TemplatesSelector({ onTemplateSelect, currentTemplate }: Templat
{designTemplates.map((template) => (
-
handleTemplateClick(template)} +
))}
diff --git a/frontend/linktree-frontend/src/app/constants/designTemplates.ts b/frontend/linktree-frontend/src/app/constants/designTemplates.ts index 015b3a5..de7f314 100644 --- a/frontend/linktree-frontend/src/app/constants/designTemplates.ts +++ b/frontend/linktree-frontend/src/app/constants/designTemplates.ts @@ -358,5 +358,79 @@ export const designTemplates: DesignTemplate[] = [ } ` } + }, + { + id: 'test-list', + name: 'Тестовый список', + description: 'Полный несворачиваемый список всех групп и ссылок', + preview: '/templates/test-list.jpg', + settings: { + theme_color: '#6366f1', + background_color: '#f8fafc', + font_family: "'Inter', sans-serif", + heading_font_family: "'Inter', sans-serif", + body_font_family: "'Inter', sans-serif", + header_text_color: '#1e293b', + group_text_color: '#334155', + link_text_color: '#475569', + group_description_text_color: '#64748b', + dashboard_layout: 'list', + group_overlay_enabled: false, + show_groups_title: true, + custom_css: ` + .test-list-layout .link-group { + margin-bottom: 2rem; + border: none; + background: transparent; + } + .test-list-layout .group-header { + background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); + color: white; + padding: 1rem; + border-radius: 8px; + margin-bottom: 0.75rem; + font-weight: 600; + font-size: 1.125rem; + } + .test-list-layout .group-links { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0; + background: transparent; + } + .test-list-layout .link-item { + background: white; + border: 1px solid #e2e8f0; + border-radius: 8px; + padding: 0.75rem 1rem; + transition: all 0.2s ease; + display: flex; + align-items: center; + text-decoration: none; + color: #475569; + } + .test-list-layout .link-item:hover { + background: #f1f5f9; + border-color: #6366f1; + transform: translateX(4px); + } + .test-list-layout .link-icon { + margin-right: 0.75rem; + width: 20px; + height: 20px; + flex-shrink: 0; + } + .test-list-layout .link-title { + font-weight: 500; + color: #334155; + } + .test-list-layout .expandable-group { + /* Принудительно отключаем сворачивание */ + .show-more-button { display: none !important; } + .group-links { max-height: none !important; overflow: visible !important; } + } + ` + } } ] \ No newline at end of file