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