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.is_favorite && (
+
+ )}
+
+
+ {/* Описание группы если есть */}
+ {group.description && (
+
+ {group.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)}
+