// src/app/[username]/page.tsx 'use client' import { notFound } from 'next/navigation' import Image from 'next/image' 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 title: string url: string icon?: string icon_url?: string description?: string } interface Group { id: number name: string description?: string icon?: string icon_url?: string header_color?: string background_image?: string is_favorite: boolean links: LinkItem[] } interface UserGroupsData { username: string full_name?: string bio?: string avatar?: string cover?: string design_settings: PublicDesignSettings groups: Group[] } interface PublicDesignSettings { theme_color: string background_image?: string dashboard_layout: string groups_default_expanded: boolean show_group_icons: boolean show_link_icons: boolean dashboard_background_color: string font_family: string header_text_color?: string group_text_color?: string link_text_color?: string 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 cover_overlay_enabled?: boolean cover_overlay_color?: string cover_overlay_opacity?: number } export default function UserPage({ params, }: { params: Promise<{ username: string }> }) { const [username, setUsername] = useState('') const [data, setData] = useState(null) const [designSettings, setDesignSettings] = useState({ 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', header_text_color: '#000000', group_text_color: '#333333', link_text_color: '#666666' }) const [expandedGroups, setExpandedGroups] = useState>(new Set()) const [loading, setLoading] = useState(true) useEffect(() => { const loadData = async () => { const resolvedParams = await params const usernameValue = resolvedParams.username setUsername(usernameValue) // Определяем API URL в зависимости от окружения const API = typeof window !== 'undefined' ? (process.env.NEXT_PUBLIC_API_URL || 'https://links.shareon.kr') // клиент : 'http://web:8000' // сервер в Docker console.log('Loading data for user:', usernameValue) console.log('API URL:', API) console.log('Is client side:', typeof window !== 'undefined') try { // Загружаем только данные пользователя (они уже включают настройки дизайна) const userRes = await fetch(`${API}/api/users/${usernameValue}/public/`, { cache: 'no-store', }) console.log('User response status:', userRes.status) if (userRes.status === 404) { notFound() return } if (!userRes.ok) { throw new Error('Ошибка загрузки публичных данных') } const userData: UserGroupsData = await userRes.json() setData(userData) // Используем настройки дизайна из ответа пользователя if (userData.design_settings) { setDesignSettings(userData.design_settings) // Если группы должны быть развернуты по умолчанию if (userData.design_settings.groups_default_expanded) { setExpandedGroups(new Set(userData.groups.map(g => g.id))) } } } catch (error) { console.error('Error loading data:', error) } finally { setLoading(false) } } loadData() }, [params]) // Функция для переключения видимости группы (для сайдбар-макета) const toggleGroup = (groupId: number) => { setExpandedGroups(prev => { const newSet = new Set(prev) if (newSet.has(groupId)) { newSet.delete(groupId) } else { newSet.add(groupId) } return newSet }) } // Базовый список (по умолчанию) const renderListLayout = () => (
{(designSettings.show_groups_title !== false) && (
Группы ссылок
)}
{data!.groups.map((group) => { const isExpanded = expandedGroups.has(group.id) return (
toggleGroup(group.id)} > {group.icon_url && designSettings.show_group_icons && ( {group.name} )} {group.name} {group.links.length} {group.is_favorite && ( )}
{isExpanded && group.links.length > 0 && (
{group.links.map((link) => (
{designSettings.show_link_icons && link.icon_url && ( {link.title} )}
{link.title}
{link.description && ( {link.description} )}
))}
)}
) })}
) // Сетка групп const renderGridLayout = () => (
Группы ссылок
{data!.groups.map((group) => (
{group.icon_url && designSettings.show_group_icons && ( {group.name} )}
{group.name}
{group.is_favorite && ( )}
{group.links.length}
{designSettings.group_overlay_enabled && (
)}
{group.description && (

{group.description}

)}
({ id: link.id, title: link.title, url: link.url, description: link.description, image: designSettings.show_link_icons ? link.icon_url : undefined }))} layout="grid" initialShowCount={5} className="" linkClassName="btn btn-outline-primary btn-sm d-flex align-items-center justify-content-start" />
))}
) const renderCardsLayout = () => (
{(designSettings.show_groups_title !== false) && (
Группы ссылок
)}
{data!.groups.map((group) => (
{group.icon_url && designSettings.show_group_icons && ( {group.name} )}
{group.name}
{group.links.length} ссылок {group.is_favorite && ( )}
{designSettings.group_overlay_enabled && (
)}
{group.description && (

{group.description}

)}
{group.links.map((link) => (
{link.icon_url && designSettings.show_link_icons && ( {link.title} )}
{link.title}
{link.description && ( {link.description} )}
))}
))}
) // Компактный макет const renderCompactLayout = () => (
{data!.groups.map((group) => (
{designSettings.show_group_icons && group.icon_url && ( )}
{group.name}
{group.is_favorite && ( )}
({ id: link.id, title: link.title, url: link.url, description: link.description, image: designSettings.show_link_icons ? link.icon_url : undefined }))} layout="cards" initialShowCount={10} className="row" linkClassName="col-auto mb-1" />
))}
) // Боковая панель const renderSidebarLayout = () => (
Группы
{data!.groups.map((group) => ( ))}
{data!.groups.filter(g => expandedGroups.has(g.id)).map((group) => (
{group.name}
{group.description && ( {group.description} )}
({ id: link.id, title: link.title, url: link.url, description: link.description, image: designSettings.show_link_icons ? link.icon_url : undefined }))} layout="cards" initialShowCount={6} className="row" linkClassName="col-12 col-md-6 col-lg-4 mb-3" />
))}
) // Кладка макет const renderMasonryLayout = () => (
Группы ссылок
{data!.groups.map((group, index) => (
{group.icon_url && designSettings.show_group_icons && ( {group.name} )}
{group.name}
{group.is_favorite && ( )}
{group.links.length}
{group.description && (

{group.description}

)}
))}
) // Лента времени макет const renderTimelineLayout = () => (
{(designSettings.show_groups_title !== false) && (
Группы ссылок
)}
{data!.groups.map((group, index) => (
{group.icon_url && designSettings.show_group_icons && ( {group.name} )}
{group.name}
{group.links.length} ссылок {group.is_favorite && ( )}
{designSettings.group_overlay_enabled && (
)}
{group.description && (

{group.description}

)}
{/* footer intentionally left empty for public page, mirrors dashboard structure */}
))}
) // Журнальный макет const renderMagazineLayout = () => (
{(designSettings.show_groups_title !== false) && (
Группы ссылок
)}
{data!.groups.map((group, index) => (
{designSettings.group_overlay_enabled && group.background_image && (
)}
{!group.background_image && ( group.icon_url && designSettings.show_group_icons ? ( {group.name} ) : ( ) )} {group.is_favorite && (
)}
{group.name}

{group.description || `${group.links.length} ссылок в этой группе`}

))}
) // Тестовый список - все группы и ссылки в одном списке 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}

)} {/* Ссылки группы */}
))}
) // Основная функция рендеринга групп в зависимости от выбранного макета const renderGroupsLayout = () => { switch (designSettings.dashboard_layout) { case 'test-list': return renderTestListLayout() case 'list': return renderListLayout() case 'grid': return renderGridLayout() case 'cards': return renderCardsLayout() case 'compact': return renderCompactLayout() case 'sidebar': return renderSidebarLayout() case 'masonry': return renderMasonryLayout() case 'timeline': return renderTimelineLayout() case 'magazine': return renderMagazineLayout() default: return renderListLayout() } } if (loading) { return (
Загрузка...
) } if (!data) { return notFound() } // Стили для контейнера const containerStyle = { backgroundColor: designSettings.dashboard_background_color, fontFamily: designSettings.body_font_family || designSettings.font_family, backgroundImage: designSettings.background_image ? `url(${designSettings.background_image})` : 'none', backgroundSize: 'cover', backgroundPosition: 'center', backgroundAttachment: 'fixed', minHeight: '100vh', paddingTop: '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 ( <> {/* Динамическая загрузка шрифтов */}
{/* Обложка пользователя - растягиваем на всю ширину экрана */} {data.cover && (
Обложка {/* Cover overlay если включен */} {designSettings.cover_overlay_enabled && (
)}
)} {/* Если обложки нет, показываем плашку */} {!data.cover && (

Обложка

)}
{/* Профиль пользователя - полупрозрачный */}
{/* Аватар пользователя */} {data.avatar ? (
{data.username}
) : (
{(data.full_name || data.username).charAt(0).toUpperCase()}
)} {/* Имя пользователя */}

{data.full_name || data.username}

{/* Username если есть полное имя */} {data.full_name && (

@{data.username}

)} {/* Биография пользователя */} {data.bio && (

{data.bio}

)} {/* Статистика */}
{data.groups.length}
Групп
{data.groups.reduce((total, group) => total + group.links.length, 0)}
Ссылок
{/* Группы ссылок */}
{renderGroupsLayout()}
) }