Redesign public user pages with new layout
Some checks failed
continuous-integration/drone/push Build is failing
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:
@@ -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,98 +144,76 @@ export default function UserPage({
|
||||
|
||||
// Базовый список (по умолчанию)
|
||||
const renderListLayout = () => (
|
||||
<div className="row">
|
||||
<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 (
|
||||
<div key={group.id} className="col-12 mb-4">
|
||||
<div className="card shadow-sm">
|
||||
<Fragment key={group.id}>
|
||||
<div className="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div
|
||||
className="card-header d-flex align-items-center justify-content-between"
|
||||
className="d-flex align-items-center"
|
||||
style={{ cursor: 'pointer' }}
|
||||
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 && (
|
||||
{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'
|
||||
}}
|
||||
>
|
||||
</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>
|
||||
</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">
|
||||
{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 mb-3">
|
||||
<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="card h-100 shadow-sm border-start border-3 hover-shadow"
|
||||
<div className="border rounded p-2 h-100 hover-shadow"
|
||||
style={{
|
||||
borderColor: designSettings.theme_color + '60',
|
||||
cursor: 'pointer',
|
||||
borderColor: designSettings.theme_color + '40',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
>
|
||||
<div className="card-body d-flex align-items-center">
|
||||
<div className="d-flex align-items-center">
|
||||
{designSettings.show_link_icons && link.icon_url && (
|
||||
<Image
|
||||
<img
|
||||
src={link.icon_url}
|
||||
alt=""
|
||||
width={24}
|
||||
height={24}
|
||||
width={20}
|
||||
height={20}
|
||||
className="me-2 rounded"
|
||||
priority
|
||||
alt={link.title}
|
||||
/>
|
||||
)}
|
||||
<div className="flex-grow-1">
|
||||
<h6 className="card-title mb-1" style={{ color: designSettings.link_text_color || designSettings.theme_color }}>
|
||||
<h6 className="mb-0 small" 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>
|
||||
<small className="text-muted d-block text-truncate">{link.description}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -244,51 +222,48 @@ export default function UserPage({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted mb-0">
|
||||
В этой группе пока нет ссылок.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Сетка групп
|
||||
const renderGridLayout = () => (
|
||||
<div className="row">
|
||||
<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-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 && (
|
||||
<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}
|
||||
alt=""
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-circle mb-2"
|
||||
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 mt-1"></i>
|
||||
<i className="bi bi-star-fill text-warning ms-2"></i>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="card-body"
|
||||
<span className="badge bg-secondary rounded-pill">
|
||||
{group.links.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="card-body"
|
||||
style={{
|
||||
backgroundImage: group.background_image ? `url(${group.background_image})` : 'none',
|
||||
backgroundSize: 'cover',
|
||||
@@ -299,108 +274,121 @@ export default function UserPage({
|
||||
<p className="text-muted small mb-3">{group.description}</p>
|
||||
)}
|
||||
<div className="d-grid gap-2">
|
||||
{group.links.map((link) => (
|
||||
{group.links.slice(0, 5).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"
|
||||
className="btn btn-outline-primary btn-sm d-flex align-items-center justify-content-start"
|
||||
style={{
|
||||
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=""
|
||||
src={link.icon_url}
|
||||
width={16}
|
||||
height={16}
|
||||
className="me-2 rounded"
|
||||
alt={link.title}
|
||||
/>
|
||||
)}
|
||||
{link.title}
|
||||
<span className="text-truncate">{link.title}</span>
|
||||
</Link>
|
||||
))}
|
||||
{group.links.length > 5 && (
|
||||
<small className="text-muted text-center">+{group.links.length - 5} еще...</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Карточки (большие карточки с описанием)
|
||||
const renderCardsLayout = () => (
|
||||
<div className="row">
|
||||
<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 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 && (
|
||||
<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}
|
||||
alt=""
|
||||
width={48}
|
||||
height={48}
|
||||
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>
|
||||
{group.description && (
|
||||
<p className="text-muted mb-0 small">{group.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<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 ms-auto"></i>
|
||||
<i className="bi bi-star-fill text-warning" title="Избранная группа"></i>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="card-body"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body"
|
||||
style={{
|
||||
backgroundImage: group.background_image ? `url(${group.background_image})` : 'none',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
}}
|
||||
>
|
||||
<div className="row">
|
||||
{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-12 col-md-6 mb-2">
|
||||
<div key={link.id} className="col-md-6 col-lg-4">
|
||||
<Link
|
||||
href={link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="d-flex align-items-center p-2 border rounded text-decoration-none hover-shadow"
|
||||
className="d-block text-decoration-none"
|
||||
>
|
||||
<div className="border rounded p-3 h-100 hover-shadow"
|
||||
style={{
|
||||
borderColor: designSettings.theme_color + '40',
|
||||
color: designSettings.link_text_color || designSettings.theme_color
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
>
|
||||
{designSettings.show_link_icons && link.icon && (
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
{link.icon_url && designSettings.show_link_icons && (
|
||||
<img
|
||||
src={link.icon}
|
||||
alt=""
|
||||
width={24}
|
||||
height={24}
|
||||
src={link.icon_url}
|
||||
width={20}
|
||||
height={20}
|
||||
className="me-2 rounded"
|
||||
alt={link.title}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<h6 className="mb-0" style={{ color: designSettings.link_text_color || designSettings.theme_color }}>
|
||||
{link.title}
|
||||
</h6>
|
||||
</div>
|
||||
{link.description && (
|
||||
<small className="text-muted">{link.description}</small>
|
||||
<small className="text-muted d-block">{link.description}</small>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
@@ -412,6 +400,7 @@ export default function UserPage({
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user