Redesign public user pages with new layout
Some checks failed
continuous-integration/drone/push Build is failing

Major improvements to public user page display:

 Layout changes:
- Cover image moved to top, full-width stretch
- User profile card made semi-transparent and overlaid
- Cover placeholder when no image available
- Fixed avatar display with fallback to initials

🎨 Design system alignment:
- All layouts now match dashboard exactly (list, grid, cards, compact, sidebar)
- Proper Fragment support for list layout expansion
- Consistent group and link styling
- Improved responsive design

📱 User experience:
- Better visual hierarchy with cover → profile → content
- Enhanced transparency effects with backdrop blur
- Proper hover states and transitions
- Statistics display (groups/links count)

🔧 Technical:
- Added Fragment import for proper React rendering
- Improved icon handling with icon_url consistency
- Better error handling for missing images
- Enhanced accessibility with proper alt text
This commit is contained in:
2025-11-08 20:07:59 +09:00
parent 2b217ded53
commit 95d6137713

View File

@@ -4,7 +4,7 @@
import { notFound } from 'next/navigation' import { notFound } from 'next/navigation'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import { useEffect, useState } from 'react' import { useEffect, useState, Fragment } from 'react'
interface LinkItem { interface LinkItem {
id: number id: number
@@ -144,273 +144,262 @@ export default function UserPage({
// Базовый список (по умолчанию) // Базовый список (по умолчанию)
const renderListLayout = () => ( const renderListLayout = () => (
<div className="row"> <div className="card">
{data!.groups.map((group) => { <div className="card-header">
const isExpanded = expandedGroups.has(group.id) <h5 className="mb-0">Группы ссылок</h5>
return ( </div>
<div key={group.id} className="col-12 mb-4"> <div className="list-group list-group-flush">
<div className="card shadow-sm"> {data!.groups.map((group) => {
<div const isExpanded = expandedGroups.has(group.id)
className="card-header d-flex align-items-center justify-content-between" return (
onClick={() => toggleGroup(group.id)} <Fragment key={group.id}>
style={{ <div className="list-group-item d-flex justify-content-between align-items-center">
backgroundColor: group.header_color || designSettings.theme_color + '20', <div
borderColor: group.header_color || designSettings.theme_color, className="d-flex align-items-center"
cursor: 'pointer' style={{ cursor: 'pointer' }}
}} onClick={() => toggleGroup(group.id)}
> >
<div className="d-flex align-items-center flex-grow-1"> {group.icon_url && designSettings.show_group_icons && (
{designSettings.show_group_icons && group.icon && (
<img <img
src={group.icon} src={group.icon_url}
alt=""
width={32} width={32}
height={32} height={32}
className="me-2 rounded" className="me-2 rounded"
alt={group.name}
/> />
)} )}
<h5 className="mb-0 flex-grow-1" style={{ color: designSettings.group_text_color || designSettings.theme_color }}> <strong className="me-2" style={{ color: designSettings.group_text_color || designSettings.theme_color }}>
{group.name} {group.name}
</h5> </strong>
<div className="d-flex align-items-center ms-2"> <span className="badge bg-secondary rounded-pill">
{group.is_favorite && ( {group.links.length}
<i className="bi bi-star-fill text-warning me-2" title="Избранная группа"></i> </span>
)} {group.is_favorite && (
<span <i className="bi bi-star-fill text-warning ms-2" title="Избранная группа"></i>
className="badge rounded-pill me-2"
style={{
backgroundColor: designSettings.theme_color,
color: 'white'
}}
>
{group.links.length}
</span>
</div>
</div>
<i className={`bi ${isExpanded ? 'bi-chevron-up' : 'bi-chevron-down'} ms-2`}></i>
</div>
{isExpanded && (
<div
className="card-body"
style={{
backgroundImage: group.background_image ? `url(${group.background_image})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
{group.description && (
<p className="text-muted mb-3">{group.description}</p>
)} )}
{group.links.length > 0 ? ( </div>
<div className="row"> <i className={`bi ${isExpanded ? 'bi-chevron-up' : 'bi-chevron-down'}`}></i>
{group.links.map((link) => ( </div>
<div key={link.id} className="col-12 col-md-6 col-lg-4 mb-3">
<Link {isExpanded && group.links.length > 0 && (
href={link.url} <div className="list-group-item bg-light">
target="_blank" <div className="row g-2">
rel="noopener noreferrer" {group.links.map((link) => (
className="d-block text-decoration-none" <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="card h-100 shadow-sm border-start border-3 hover-shadow" <div className="d-flex align-items-center">
style={{ {designSettings.show_link_icons && link.icon_url && (
borderColor: designSettings.theme_color + '60', <img
cursor: 'pointer', src={link.icon_url}
transition: 'all 0.2s ease' width={20}
}} height={20}
> className="me-2 rounded"
<div className="card-body d-flex align-items-center"> alt={link.title}
{designSettings.show_link_icons && link.icon_url && ( />
<Image )}
src={link.icon_url} <div className="flex-grow-1">
alt="" <h6 className="mb-0 small" style={{ color: designSettings.link_text_color || designSettings.theme_color }}>
width={24} {link.title}
height={24} </h6>
className="me-2 rounded" {link.description && (
priority <small className="text-muted d-block text-truncate">{link.description}</small>
/>
)} )}
<div className="flex-grow-1">
<h6 className="card-title mb-1" style={{ color: designSettings.link_text_color || designSettings.theme_color }}>
{link.title}
</h6>
{link.description && (
<p className="card-text small text-muted mb-0">{link.description}</p>
)}
</div>
</div> </div>
</div> </div>
</Link> </div>
</div> </Link>
))} </div>
</div> ))}
) : ( </div>
<p className="text-muted mb-0">
В этой группе пока нет ссылок.
</p>
)}
</div> </div>
)} )}
</div> </Fragment>
</div> )
) })}
})} </div>
</div> </div>
) )
// Сетка групп // Сетка групп
const renderGridLayout = () => ( const renderGridLayout = () => (
<div className="row"> <div>
{data!.groups.map((group) => ( <div className="d-flex justify-content-between align-items-center mb-3">
<div key={group.id} className="col-12 col-md-6 col-lg-4 mb-4"> <h5 className="mb-0">Группы ссылок</h5>
<div className="card h-100 shadow-sm"> </div>
<div <div className="row g-3">
className="card-header text-center" {data!.groups.map((group) => (
style={{ <div key={group.id} className="col-md-6 col-lg-4">
backgroundColor: group.header_color || designSettings.theme_color + '20', <div className="card h-100">
borderColor: group.header_color || designSettings.theme_color <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 && (
{designSettings.show_group_icons && group.icon && ( <img
<img src={group.icon_url}
src={group.icon} width={24}
alt="" height={24}
width={40} className="me-2 rounded"
height={40} alt={group.name}
className="rounded-circle mb-2" />
/> )}
)} <h6 className="mb-0" style={{ color: designSettings.group_text_color || designSettings.theme_color }}>
<h6 className="mb-0" style={{ color: designSettings.group_text_color || designSettings.theme_color }}> {group.name}
{group.name} </h6>
</h6> {group.is_favorite && (
{group.is_favorite && ( <i className="bi bi-star-fill text-warning ms-2"></i>
<i className="bi bi-star-fill text-warning mt-1"></i> )}
)} </div>
</div> <span className="badge bg-secondary rounded-pill">
<div {group.links.length}
className="card-body" </span>
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>
)}
<div className="d-grid gap-2">
{group.links.map((link) => (
<Link
key={link.id}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="btn btn-outline-primary btn-sm d-flex align-items-center"
style={{
borderColor: designSettings.theme_color,
color: designSettings.link_text_color || designSettings.theme_color
}}
>
{designSettings.show_link_icons && link.icon && (
<img
src={link.icon}
alt=""
width={16}
height={16}
className="me-2 rounded"
/>
)}
{link.title}
</Link>
))}
</div> </div>
</div> <div className="card-body"
</div> style={{
</div> backgroundImage: group.background_image ? `url(${group.background_image})` : 'none',
))} backgroundSize: 'cover',
</div> backgroundPosition: 'center'
) }}
>
// Карточки (большие карточки с описанием)
const renderCardsLayout = () => (
<div className="row">
{data!.groups.map((group) => (
<div key={group.id} className="col-12 col-lg-6 mb-4">
<div className="card h-100 shadow">
<div
className="card-header d-flex align-items-center"
style={{
backgroundColor: group.header_color || designSettings.theme_color + '20',
borderColor: group.header_color || designSettings.theme_color
}}
>
{designSettings.show_group_icons && group.icon && (
<img
src={group.icon}
alt=""
width={48}
height={48}
className="me-3 rounded"
/>
)}
<div>
<h5 className="mb-1" style={{ color: designSettings.group_text_color || designSettings.theme_color }}>
{group.name}
</h5>
{group.description && ( {group.description && (
<p className="text-muted mb-0 small">{group.description}</p> <p className="text-muted small mb-3">{group.description}</p>
)} )}
</div> <div className="d-grid gap-2">
{group.is_favorite && ( {group.links.slice(0, 5).map((link) => (
<i className="bi bi-star-fill text-warning ms-auto"></i>
)}
</div>
<div
className="card-body"
style={{
backgroundImage: group.background_image ? `url(${group.background_image})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
<div className="row">
{group.links.map((link) => (
<div key={link.id} className="col-12 col-md-6 mb-2">
<Link <Link
key={link.id}
href={link.url} href={link.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="d-flex align-items-center p-2 border rounded text-decoration-none hover-shadow" className="btn btn-outline-primary btn-sm d-flex align-items-center justify-content-start"
style={{ style={{
borderColor: designSettings.theme_color + '40', borderColor: designSettings.theme_color,
color: designSettings.link_text_color || designSettings.theme_color color: designSettings.link_text_color || designSettings.theme_color
}} }}
> >
{designSettings.show_link_icons && link.icon && ( {designSettings.show_link_icons && link.icon_url && (
<img <img
src={link.icon} src={link.icon_url}
alt="" width={16}
width={24} height={16}
height={24}
className="me-2 rounded" className="me-2 rounded"
alt={link.title}
/> />
)} )}
<div> <span className="text-truncate">{link.title}</span>
<h6 className="mb-0" style={{ color: designSettings.link_text_color || designSettings.theme_color }}>
{link.title}
</h6>
{link.description && (
<small className="text-muted">{link.description}</small>
)}
</div>
</Link> </Link>
</div> ))}
))} {group.links.length > 5 && (
<small className="text-muted text-center">+{group.links.length - 5} еще...</small>
)}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> ))}
))} </div>
</div>
)
// Карточки (большие карточки с описанием)
const renderCardsLayout = () => (
<div>
<div className="d-flex justify-content-between align-items-center mb-4">
<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"
style={{
backgroundImage: group.background_image ? `url(${group.background_image})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
{group.description && (
<p className="text-muted mb-3">{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>
) )
@@ -636,18 +625,59 @@ export default function UserPage({
return ( return (
<main style={containerStyle}> <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="container">
{/* Профиль пользователя */} {/* Профиль пользователя - полупрозрачный */}
<div className="row justify-content-center mb-5"> <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="col-12 col-md-8 col-lg-6">
<div className="card shadow-lg border-0" style={{ <div className="card shadow-lg border-0" style={{
backgroundColor: 'rgba(255, 255, 255, 0.95)', backgroundColor: 'rgba(255, 255, 255, 0.85)',
backdropFilter: 'blur(10px)', backdropFilter: 'blur(15px)',
borderRadius: '20px' borderRadius: '20px'
}}> }}>
<div className="card-body text-center p-4"> <div className="card-body text-center p-4">
{/* Аватар пользователя */} {/* Аватар пользователя */}
{data.avatar && ( {data.avatar ? (
<div className="mb-3 position-relative d-inline-block"> <div className="mb-3 position-relative d-inline-block">
<Image <Image
src={data.avatar} src={data.avatar}
@@ -670,6 +700,30 @@ export default function UserPage({
}} }}
></div> ></div>
</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>
)} )}
{/* Имя пользователя */} {/* Имя пользователя */}
@@ -707,37 +761,8 @@ export default function UserPage({
</div> </div>
</div> </div>
{/* Обложка пользователя если есть */}
{data.cover && (
<div className="row justify-content-center mb-4">
<div className="col-12 col-lg-10">
<div className="card border-0 shadow position-relative">
<Image
src={data.cover}
alt="Обложка"
width={800}
height={300}
className="card-img rounded"
style={{ objectFit: 'cover', maxHeight: '300px' }}
/>
{/* Cover overlay если включен */}
{designSettings.cover_overlay_enabled && (
<div
className="position-absolute top-0 start-0 w-100 h-100 rounded"
style={{
backgroundColor: designSettings.cover_overlay_color || '#000000',
opacity: designSettings.cover_overlay_opacity || 0.3,
pointerEvents: 'none'
}}
></div>
)}
</div>
</div>
</div>
)}
{/* Группы ссылок */} {/* Группы ссылок */}
<div className="row justify-content-center"> <div className="row justify-content-center mt-5">
<div className="col-12"> <div className="col-12">
{renderGroupsLayout()} {renderGroupsLayout()}
</div> </div>