Some checks failed
continuous-integration/drone/push Build is failing
- 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
1086 lines
42 KiB
TypeScript
1086 lines
42 KiB
TypeScript
// 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>
|
||
</>
|
||
)
|
||
}
|