Files
links/frontend/linktree-frontend/src/app/[username]/page.tsx
Andrey K. Choi cefd884172
Some checks failed
continuous-integration/drone/push Build is failing
Fix: Implement proper link scrolling in groups using ExpandableGroup component
- Replace all hardcoded group.links.slice(0, 5) with ExpandableGroup component
- Updated renderGridLayout, renderCardsLayout, renderCompactLayout, renderSidebarLayout
- Fixed icon display in ExpandableGroup component
- Improved styling for button-style links
- All group layouts now support expandable link lists with 'Show X more links' functionality
2025-11-09 12:08:35 +09:00

1086 lines
42 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<string>('')
const [data, setData] = useState<UserGroupsData | null>(null)
const [designSettings, setDesignSettings] = useState<PublicDesignSettings>({
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<Set<number>>(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 = () => (
<div className="card">
{(designSettings.show_groups_title !== false) && (
<div className="card-header">
<h5 className="mb-0">Группы ссылок</h5>
</div>
)}
<div className="list-group list-group-flush">
{data!.groups.map((group) => {
const isExpanded = expandedGroups.has(group.id)
return (
<Fragment key={group.id}>
<div className="list-group-item d-flex justify-content-between align-items-center">
<div
className="d-flex align-items-center"
style={{ cursor: 'pointer' }}
onClick={() => toggleGroup(group.id)}
>
{group.icon_url && designSettings.show_group_icons && (
<img
src={group.icon_url}
width={32}
height={32}
className="me-2 rounded"
alt={group.name}
/>
)}
<strong className="me-2" style={{ color: designSettings.group_text_color || designSettings.theme_color }}>
{group.name}
</strong>
<span className="badge bg-secondary rounded-pill">
{group.links.length}
</span>
{group.is_favorite && (
<i className="bi bi-star-fill text-warning ms-2" title="Избранная группа"></i>
)}
</div>
<i className={`bi ${isExpanded ? 'bi-chevron-up' : 'bi-chevron-down'}`}></i>
</div>
{isExpanded && group.links.length > 0 && (
<div className="list-group-item bg-light">
<div className="row g-2">
{group.links.map((link) => (
<div key={link.id} className="col-12 col-md-6 col-lg-4">
<Link
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="d-block text-decoration-none"
>
<div className="border rounded p-2 h-100 hover-shadow"
style={{
borderColor: designSettings.theme_color + '40',
transition: 'all 0.2s ease'
}}
>
<div className="d-flex align-items-center">
{designSettings.show_link_icons && link.icon_url && (
<img
src={link.icon_url}
width={20}
height={20}
className="me-2 rounded"
alt={link.title}
/>
)}
<div className="flex-grow-1">
<h6 className="mb-0 small" style={{ color: designSettings.link_text_color || designSettings.theme_color }}>
{link.title}
</h6>
{link.description && (
<small className="text-muted d-block text-truncate">{link.description}</small>
)}
</div>
</div>
</div>
</Link>
</div>
))}
</div>
</div>
)}
</Fragment>
)
})}
</div>
</div>
)
// Сетка групп
const renderGridLayout = () => (
<div>
<div className="d-flex justify-content-between align-items-center mb-3">
<h5 className="mb-0">Группы ссылок</h5>
</div>
<div className="row g-3">
{data!.groups.map((group) => (
<div key={group.id} className="col-md-6 col-lg-4">
<div className="card h-100">
<div className="card-header d-flex justify-content-between align-items-center">
<div className="d-flex align-items-center">
{group.icon_url && designSettings.show_group_icons && (
<img
src={group.icon_url}
width={24}
height={24}
className="me-2 rounded"
alt={group.name}
/>
)}
<h6 className="mb-0" style={{ color: designSettings.group_text_color || designSettings.theme_color }}>
{group.name}
</h6>
{group.is_favorite && (
<i className="bi bi-star-fill text-warning ms-2"></i>
)}
</div>
<span className="badge bg-secondary rounded-pill">
{group.links.length}
</span>
</div>
<div
className="card-body position-relative"
style={{
backgroundImage: group.background_image ? `url(${group.background_image})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
{designSettings.group_overlay_enabled && (
<div
className="position-absolute top-0 start-0 w-100 h-100"
style={{
backgroundColor: designSettings.group_overlay_color || '#000000',
opacity: designSettings.group_overlay_opacity || 0.3,
zIndex: 1
}}
></div>
)}
<div className="position-relative" style={{ zIndex: 2 }}>
{group.description && (
<p
className="small mb-3"
style={{ color: designSettings.group_description_text_color || '#666666' }}
>
{group.description}
</p>
)}
<div className="d-grid gap-2">
<ExpandableGroup
links={group.links.map(link => ({
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"
/>
</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
)
const renderCardsLayout = () => (
<div>
<div className="d-flex justify-content-between align-items-center mb-4">
{(designSettings.show_groups_title !== false) && (
<h5 className="mb-0">Группы ссылок</h5>
)}
</div>
<div className="row g-4">
{data!.groups.map((group) => (
<div key={group.id} className="col-12">
<div className="card shadow-sm">
<div className="card-header bg-light">
<div className="row align-items-center">
<div className="col">
<div className="d-flex align-items-center">
{group.icon_url && designSettings.show_group_icons && (
<img
src={group.icon_url}
width={40}
height={40}
className="me-3 rounded"
alt={group.name}
/>
)}
<div>
<h5 className="mb-1" style={{ color: designSettings.group_text_color || designSettings.theme_color }}>
{group.name}
</h5>
<div className="d-flex align-items-center">
<small className="text-muted me-2">{group.links.length} ссылок</small>
{group.is_favorite && (
<i className="bi bi-star-fill text-warning" title="Избранная группа"></i>
)}
</div>
</div>
</div>
</div>
</div>
</div>
<div
className="card-body position-relative"
style={{
backgroundImage: group.background_image ? `url(${group.background_image})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
{designSettings.group_overlay_enabled && (
<div
className="position-absolute top-0 start-0 w-100 h-100"
style={{
backgroundColor: designSettings.group_overlay_color || '#000000',
opacity: designSettings.group_overlay_opacity || 0.3,
zIndex: 1
}}
></div>
)}
<div className="position-relative" style={{ zIndex: 2 }}>
{group.description && (
<p
className="mb-3"
style={{ color: designSettings.group_description_text_color || '#666666' }}
>
{group.description}
</p>
)}
<div className="row g-3">
{group.links.map((link) => (
<div key={link.id} className="col-md-6 col-lg-4">
<Link
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="d-block text-decoration-none"
>
<div className="border rounded p-3 h-100 hover-shadow"
style={{
borderColor: designSettings.theme_color + '40',
transition: 'all 0.2s ease'
}}
>
<div className="d-flex align-items-center mb-2">
{link.icon_url && designSettings.show_link_icons && (
<img
src={link.icon_url}
width={20}
height={20}
className="me-2 rounded"
alt={link.title}
/>
)}
<h6 className="mb-0" style={{ color: designSettings.link_text_color || designSettings.theme_color }}>
{link.title}
</h6>
</div>
{link.description && (
<small className="text-muted d-block">{link.description}</small>
)}
</div>
</Link>
</div>
))}
</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
)
// Компактный макет
const renderCompactLayout = () => (
<div className="row">
{data!.groups.map((group) => (
<div key={group.id} className="col-12 mb-3">
<div className="border-start border-4 ps-3" style={{ borderColor: group.header_color || designSettings.theme_color }}>
<div className="d-flex align-items-center mb-2">
{designSettings.show_group_icons && group.icon_url && (
<Image
src={group.icon_url}
alt=""
width={32}
height={32}
className="me-2 rounded"
priority
/>
)}
<h6 className="mb-0" style={{ color: designSettings.group_text_color || designSettings.theme_color }}>
{group.name}
</h6>
{group.is_favorite && (
<i className="bi bi-star-fill text-warning ms-2"></i>
)}
</div>
<ExpandableGroup
links={group.links.map(link => ({
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"
/>
</div>
</div>
))}
</div>
)
// Боковая панель
const renderSidebarLayout = () => (
<div className="row">
<div className="col-12 col-lg-3 mb-4">
<div className="card">
<div className="card-header">
<h6 className="mb-0">Группы</h6>
</div>
<div className="list-group list-group-flush">
{data!.groups.map((group) => (
<button
key={group.id}
className={`list-group-item list-group-item-action d-flex align-items-center ${expandedGroups.has(group.id) ? 'active' : ''}`}
onClick={() => toggleGroup(group.id)}
style={{
borderColor: expandedGroups.has(group.id) ? designSettings.theme_color : undefined
}}
>
{designSettings.show_group_icons && group.icon && (
<img
src={group.icon}
alt=""
width={24}
height={24}
className="me-2 rounded"
/>
)}
<span className="flex-grow-1">{group.name}</span>
{group.is_favorite && (
<i className="bi bi-star-fill text-warning ms-1"></i>
)}
</button>
))}
</div>
</div>
</div>
<div className="col-12 col-lg-9">
{data!.groups.filter(g => expandedGroups.has(g.id)).map((group) => (
<div key={group.id} className="card mb-4">
<div
className="card-header"
style={{
backgroundColor: group.header_color || designSettings.theme_color + '20',
borderColor: group.header_color || designSettings.theme_color
}}
>
<h6 className="mb-0" style={{ color: designSettings.group_text_color || designSettings.theme_color }}>
{group.name}
</h6>
{group.description && (
<small className="text-muted">{group.description}</small>
)}
</div>
<div
className="card-body"
style={{
backgroundImage: group.background_image ? `url(${group.background_image})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
<ExpandableGroup
links={group.links.map(link => ({
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"
/>
</div>
</div>
))}
</div>
</div>
)
// Кладка макет
const renderMasonryLayout = () => (
<div>
<div className="d-flex justify-content-between align-items-center mb-3">
<h5 className="mb-0">Группы ссылок</h5>
</div>
<div className="row g-3">
{data!.groups.map((group, index) => (
<div key={group.id} className={`col-md-6 ${index % 3 === 0 ? 'col-lg-4' : index % 3 === 1 ? 'col-lg-8' : 'col-lg-4'}`}>
<div className="card h-100">
<div className="card-header">
<div className="d-flex align-items-center justify-content-between">
<div className="d-flex align-items-center">
{group.icon_url && designSettings.show_group_icons && (
<img
src={group.icon_url}
width={32}
height={32}
className="me-2 rounded"
alt={group.name}
/>
)}
<h6 className="mb-0" style={{ color: designSettings.group_text_color || designSettings.theme_color }}>
{group.name}
</h6>
{group.is_favorite && (
<i className="bi bi-star-fill text-warning ms-2"></i>
)}
</div>
<span className="badge bg-secondary rounded-pill">
{group.links.length}
</span>
</div>
</div>
<div className="card-body"
style={{
backgroundImage: group.background_image ? `url(${group.background_image})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
{group.description && (
<p className="text-muted small mb-3">{group.description}</p>
)}
<ExpandableGroup
links={group.links}
layout="cards"
initialShowCount={8}
overlayColor={designSettings.group_overlay_enabled ? designSettings.group_overlay_color : undefined}
overlayOpacity={designSettings.group_overlay_enabled ? designSettings.group_overlay_opacity : undefined}
/>
</div>
</div>
</div>
))}
</div>
</div>
)
// Лента времени макет
const renderTimelineLayout = () => (
<div>
<div className="d-flex justify-content-between align-items-center mb-4">
{(designSettings.show_groups_title !== false) && (
<h5 className="mb-0">Группы ссылок</h5>
)}
</div>
<div className="timeline">
{data!.groups.map((group, index) => (
<div key={group.id} className={`timeline-item ${index % 2 === 0 ? 'left' : 'right'}`}>
<div className="timeline-content">
<div className="card">
<div className="card-header d-flex align-items-center">
{group.icon_url && designSettings.show_group_icons && (
<img
src={group.icon_url}
width={40}
height={40}
className="me-3 rounded"
alt={group.name}
/>
)}
<div>
<h6 className="mb-0" style={{ color: designSettings.group_text_color || designSettings.theme_color }}>
{group.name}
</h6>
<div className="d-flex align-items-center">
<small className="text-muted me-2">{group.links.length} ссылок</small>
{group.is_favorite && (
<i className="bi bi-star-fill text-warning"></i>
)}
</div>
</div>
</div>
<div
className="card-body position-relative"
style={{
backgroundImage: group.background_image ? `url(${group.background_image})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
{designSettings.group_overlay_enabled && (
<div
className="position-absolute top-0 start-0 w-100 h-100"
style={{
backgroundColor: designSettings.group_overlay_color || '#000000',
opacity: designSettings.group_overlay_opacity || 0.3,
zIndex: 1
}}
></div>
)}
<div className="position-relative" style={{ zIndex: 2 }}>
{group.description && (
<p
className="mb-3"
style={{ color: designSettings.group_description_text_color || '#666666' }}
>
{group.description}
</p>
)}
<ExpandableGroup
links={group.links}
layout="timeline"
initialShowCount={5}
overlayColor={designSettings.group_overlay_enabled ? designSettings.group_overlay_color : undefined}
overlayOpacity={designSettings.group_overlay_enabled ? designSettings.group_overlay_opacity : undefined}
/>
</div>
</div>
<div className="card-footer">
{/* footer intentionally left empty for public page, mirrors dashboard structure */}
</div>
</div>
</div>
</div>
))}
</div>
</div>
)
// Журнальный макет
const renderMagazineLayout = () => (
<div>
<div className="d-flex justify-content-between align-items-center mb-4">
{(designSettings.show_groups_title !== false) && (
<h5 className="mb-0">Группы ссылок</h5>
)}
</div>
<div className="magazine-layout">
{data!.groups.map((group, index) => (
<div key={group.id} className={`magazine-item ${index === 0 ? 'featured' : ''} mb-4`}>
<div className="card">
<div className="row g-0">
<div className={`${index === 0 ? 'col-md-6' : 'col-md-4'}`}>
<div
className="magazine-image d-flex align-items-center justify-content-center bg-light position-relative"
style={{
minHeight: index === 0 ? '300px' : '200px',
backgroundImage: group.background_image ? `url(${group.background_image})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
{designSettings.group_overlay_enabled && group.background_image && (
<div
className="position-absolute top-0 start-0 w-100 h-100"
style={{
backgroundColor: designSettings.group_overlay_color || '#000000',
opacity: designSettings.group_overlay_opacity || 0.3,
zIndex: 1
}}
></div>
)}
<div className="position-relative" style={{ zIndex: 2 }}>
{!group.background_image && (
group.icon_url && designSettings.show_group_icons ? (
<img
src={group.icon_url}
className="img-fluid"
alt={group.name}
style={{ maxHeight: '120px', objectFit: 'contain' }}
/>
) : (
<i className="bi bi-collection fs-1 text-muted"></i>
)
)}
{group.is_favorite && (
<div className="position-absolute top-0 end-0 p-2">
<i className="bi bi-star-fill text-warning"></i>
</div>
)}
</div>
</div>
</div>
<div className={`${index === 0 ? 'col-md-6' : 'col-md-8'}`}>
<div className="card-body">
<h5 className="card-title" style={{ color: designSettings.group_text_color || designSettings.theme_color }}>
{group.name}
</h5>
<p
className="card-text"
style={{ color: designSettings.group_description_text_color || '#666666' }}
>
{group.description || `${group.links.length} ссылок в этой группе`}
</p>
<div className="links-preview">
<ExpandableGroup
links={group.links}
layout="magazine"
initialShowCount={index === 0 ? 5 : 3}
overlayColor={designSettings.group_overlay_enabled ? designSettings.group_overlay_color : undefined}
overlayOpacity={designSettings.group_overlay_enabled ? designSettings.group_overlay_opacity : undefined}
/>
</div>
</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
)
// Тестовый список - все группы и ссылки в одном списке
const renderTestListLayout = () => (
<div className={styles.testListLayout}>
{(designSettings.show_groups_title !== false) && (
<h5 className="mb-4" style={{
color: designSettings.header_text_color || designSettings.theme_color,
fontFamily: designSettings.heading_font_family || designSettings.font_family
}}>
Все группы и ссылки
</h5>
)}
{data!.groups.map((group) => (
<div key={group.id} className={styles.linkGroup}>
{/* Заголовок группы */}
<div className={styles.groupHeader}>
{group.icon_url && designSettings.show_group_icons && (
<img
src={group.icon_url}
width={24}
height={24}
className={styles.linkIcon}
alt={group.name}
/>
)}
<span className={styles.linkTitle}>{group.name}</span>
{group.is_favorite && (
<i className="bi bi-star-fill text-warning ms-2"></i>
)}
</div>
{/* Описание группы если есть */}
{group.description && (
<p className={`${styles.linkDescription} mb-3 ps-2`}>
{group.description}
</p>
)}
{/* Ссылки группы */}
<div className={styles.groupLinks}>
{group.links.map((link) => (
<a
key={link.id}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className={styles.linkItem}
>
{link.icon_url && designSettings.show_link_icons && (
<img
src={link.icon_url}
width={20}
height={20}
className={styles.linkIcon}
alt={link.title}
/>
)}
<span className={styles.linkTitle}>{link.title}</span>
{link.description && (
<small className={styles.linkDescription}>{link.description}</small>
)}
</a>
))}
</div>
</div>
))}
</div>
)
// Основная функция рендеринга групп в зависимости от выбранного макета
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 (
<main className="pb-8">
<div className="container">
<div className="text-center py-5">
<div className="spinner-border" role="status">
<span className="visually-hidden">Загрузка...</span>
</div>
</div>
</div>
</main>
)
}
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 (
<>
{/* Динамическая загрузка шрифтов */}
<FontLoader
fontFamily={designSettings.font_family}
headingFontFamily={designSettings.heading_font_family}
bodyFontFamily={designSettings.body_font_family}
/>
<main style={containerStyle}>
<div className="container-fluid px-0">
{/* Обложка пользователя - растягиваем на всю ширину экрана */}
{data.cover && (
<div className="position-relative mb-4" style={{ height: '300px' }}>
<Image
src={data.cover}
alt="Обложка"
fill
className="object-cover"
style={{ objectFit: 'cover' }}
priority
/>
{/* Cover overlay если включен */}
{designSettings.cover_overlay_enabled && (
<div
className="position-absolute top-0 start-0 w-100 h-100"
style={{
backgroundColor: designSettings.cover_overlay_color || '#000000',
opacity: designSettings.cover_overlay_opacity || 0.3,
pointerEvents: 'none'
}}
></div>
)}
</div>
)}
{/* Если обложки нет, показываем плашку */}
{!data.cover && (
<div
className="position-relative mb-4 d-flex align-items-center justify-content-center text-white"
style={{
height: '300px',
backgroundColor: designSettings.theme_color || '#6c757d',
backgroundImage: 'linear-gradient(135deg, rgba(0,0,0,0.1) 0%, rgba(255,255,255,0.1) 100%)'
}}
>
<h2 className="mb-0 fw-bold opacity-75">Обложка</h2>
</div>
)}
</div>
<div className="container">
{/* Профиль пользователя - полупрозрачный */}
<div className="row justify-content-center" style={{ marginTop: '-100px', position: 'relative', zIndex: 10 }}>
<div className="col-12 col-md-8 col-lg-6">
<div className="card shadow-lg border-0" style={{
backgroundColor: 'rgba(255, 255, 255, 0.85)',
backdropFilter: 'blur(15px)',
borderRadius: '20px'
}}>
<div className="card-body text-center p-4">
{/* Аватар пользователя */}
{data.avatar ? (
<div className="mb-3 position-relative d-inline-block">
<Image
src={data.avatar}
alt={data.username}
width={120}
height={120}
className="rounded-circle border border-4 shadow-sm"
style={{
borderColor: designSettings.theme_color,
objectFit: 'cover'
}}
priority
/>
<div
className="position-absolute bottom-0 end-0 rounded-circle border border-2 border-white"
style={{
width: '24px',
height: '24px',
backgroundColor: designSettings.theme_color
}}
></div>
</div>
) : (
<div className="mb-3 position-relative d-inline-block">
<div
className="rounded-circle border border-4 shadow-sm d-flex align-items-center justify-content-center"
style={{
width: '120px',
height: '120px',
backgroundColor: designSettings.theme_color || '#6c757d',
borderColor: designSettings.theme_color,
color: 'white',
fontSize: '3rem'
}}
>
{(data.full_name || data.username).charAt(0).toUpperCase()}
</div>
<div
className="position-absolute bottom-0 end-0 rounded-circle border border-2 border-white"
style={{
width: '24px',
height: '24px',
backgroundColor: designSettings.theme_color
}}
></div>
</div>
)}
{/* Имя пользователя */}
<h1
className="mb-2 fw-bold"
style={{
color: designSettings.header_text_color || designSettings.theme_color,
fontFamily: designSettings.heading_font_family || designSettings.font_family
}}
>
{data.full_name || data.username}
</h1>
{/* Username если есть полное имя */}
{data.full_name && (
<p className="text-muted mb-2 fs-5">@{data.username}</p>
)}
{/* Биография пользователя */}
{data.bio && (
<p className="text-muted mb-3 fs-6 px-3">{data.bio}</p>
)}
{/* Статистика */}
<div className="row text-center mt-3">
<div className="col-6">
<div className="fw-bold fs-4" style={{ color: designSettings.theme_color }}>
{data.groups.length}
</div>
<small className="text-muted">Групп</small>
</div>
<div className="col-6">
<div className="fw-bold fs-4" style={{ color: designSettings.theme_color }}>
{data.groups.reduce((total, group) => total + group.links.length, 0)}
</div>
<small className="text-muted">Ссылок</small>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Группы ссылок */}
<div className="row justify-content-center mt-5">
<div className="col-12">
{renderGroupsLayout()}
</div>
</div>
</div>
</main>
</>
)
}