Fix public page layouts and avatar display
Some checks failed
continuous-integration/drone/push Build is failing

🎨 Layout improvements:
- Add full implementations for timeline, masonry, and magazine layouts
- Replace layout stubs with proper React components matching dashboard
- Add timeline with alternating left/right positioning and central line
- Implement masonry layout with dynamic column sizing
- Create magazine layout with featured first item and image previews

🐛 Avatar & URL fixes:
- Fix avatar URL protocol (http → https) in normalize_file_url
- Add http://links.shareon.kr to internal URLs replacement list
- Update get_backend_url to use HTTPS and proper domain from env vars
- Fix CustomizationPanel API URL consistency in DashboardClient

 Visual enhancements:
- Proper hover states and transitions for all layout types
- Timeline dots with theme color coordination
- Magazine layout with responsive image handling
- Masonry cards with dynamic content sizing
- Consistent group/link styling across all layouts

🔧 Technical:
- Environment-driven URL generation for production
- Consistent API endpoint usage across components
- Better responsive design for mobile devices
- Improved accessibility with proper alt text and ARIA labels
This commit is contained in:
2025-11-08 20:24:42 +09:00
parent 95d6137713
commit 9c1c5b4b62
3 changed files with 275 additions and 8 deletions

View File

@@ -7,12 +7,14 @@ from django.conf import settings
def get_backend_url():
"""Получить базовый URL backend из настроек"""
return getattr(settings, 'BACKEND_URL', 'http://localhost:8000')
protocol = os.getenv('DJANGO_BACKEND_PROTOCOL', 'https')
domain = os.getenv('DJANGO_BACKEND_DOMAIN', 'links.shareon.kr')
return f"{protocol}://{domain}"
def get_media_base_url():
"""Получить базовый URL для медиа файлов"""
return getattr(settings, 'MEDIA_BASE_URL', 'http://localhost:8000')
return get_backend_url() # Используем тот же базовый URL
def build_absolute_url(path):
@@ -48,7 +50,8 @@ def normalize_file_url(file_url):
'http://web:8000',
'http://links-web-1:8000',
'http://backend:8000',
'http://localhost:8000'
'http://localhost:8000',
'http://links.shareon.kr' # Заменяем HTTP на HTTPS
]
# Заменяем все внутренние URL на внешний

View File

@@ -210,7 +210,7 @@ export default function DashboardClient() {
fetch('/api/auth/user', { headers: { Authorization: `Bearer ${token}` } }),
fetch('/api/groups', { headers: { Authorization: `Bearer ${token}` } }),
fetch('/api/links', { headers: { Authorization: `Bearer ${token}` } }),
fetch('/api/customization/settings', { headers: { Authorization: `Bearer ${token}` } }),
fetch('/api/customization/settings/', { headers: { Authorization: `Bearer ${token}` } }),
])
.then(async ([uRes, gRes, lRes, dRes]) => {
if (!uRes.ok) throw new Error('Не удалось получить профиль')

View File

@@ -563,10 +563,274 @@ export default function UserPage({
</div>
)
// Заглушки для остальных макетов
const renderMasonryLayout = () => renderGridLayout()
const renderTimelineLayout = () => renderListLayout()
const renderMagazineLayout = () => renderCardsLayout()
// Кладка макет
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>
)}
{group.links.map(link => (
<Link
key={link.id}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="d-block text-decoration-none mb-2"
>
<div className="p-2 border rounded hover-shadow"
style={{
borderColor: designSettings.theme_color + '40',
transition: 'all 0.2s ease'
}}
>
<div className="d-flex align-items-center">
{link.icon_url && designSettings.show_link_icons && (
<img
src={link.icon_url}
width={16}
height={16}
className="me-2 rounded"
alt={link.title}
/>
)}
<small className="text-truncate" style={{ color: designSettings.link_text_color || designSettings.theme_color }}>
{link.title}
</small>
</div>
</div>
</Link>
))}
</div>
</div>
</div>
))}
</div>
</div>
)
// Лента времени макет
const renderTimelineLayout = () => (
<div>
<div className="d-flex justify-content-between align-items-center mb-4">
<h5 className="mb-0">Группы ссылок</h5>
</div>
<div className="timeline position-relative">
{/* Центральная линия */}
<div className="position-absolute start-50 translate-middle-x bg-secondary"
style={{ width: '2px', height: '100%', top: '0' }}></div>
{data!.groups.map((group, index) => (
<div key={group.id} className={`timeline-item row mb-4 ${index % 2 === 0 ? '' : 'flex-row-reverse'}`}>
<div className="col-md-6">
<div className={`card shadow-sm ${index % 2 === 0 ? 'me-3' : 'ms-3'}`}>
<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"
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.slice(0, 5).map(link => (
<Link
key={link.id}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="d-block text-decoration-none mb-2"
>
<div className="d-flex align-items-center">
{link.icon_url && designSettings.show_link_icons && (
<img
src={link.icon_url}
width={16}
height={16}
className="me-2 rounded"
alt={link.title}
/>
)}
<small style={{ color: designSettings.link_text_color || designSettings.theme_color }}>
{link.title}
</small>
</div>
</Link>
))}
{group.links.length > 5 && (
<small className="text-muted">+{group.links.length - 5} еще...</small>
)}
</div>
{/* Точка на временной линии */}
<div className={`position-absolute top-50 translate-middle-y ${index % 2 === 0 ? 'end-0' : 'start-0'}`}
style={{
[index % 2 === 0 ? 'right' : 'left']: '-20px',
width: '16px',
height: '16px',
backgroundColor: designSettings.theme_color,
borderRadius: '50%',
border: '3px solid white',
boxShadow: '0 0 0 2px ' + (designSettings.theme_color || '#007bff')
}}></div>
</div>
</div>
</div>
))}
</div>
</div>
)
// Журнальный макет
const renderMagazineLayout = () => (
<div>
<div className="d-flex justify-content-between align-items-center mb-4">
<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'
}}
>
{!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 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 text-muted">
{group.description || `${group.links.length} ссылок в этой группе`}
</p>
<div className="links-preview">
{group.links.slice(0, index === 0 ? 5 : 3).map(link => (
<Link
key={link.id}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="d-block text-decoration-none mb-2"
>
<div className="d-flex align-items-center p-2 border rounded hover-shadow"
style={{
borderColor: designSettings.theme_color + '40',
transition: 'all 0.2s ease'
}}
>
{link.icon_url && designSettings.show_link_icons && (
<img
src={link.icon_url}
width={16}
height={16}
className="me-2 rounded"
alt={link.title}
/>
)}
<small style={{ color: designSettings.link_text_color || designSettings.theme_color }}>
{link.title}
</small>
</div>
</Link>
))}
{group.links.length > (index === 0 ? 5 : 3) && (
<small className="text-muted">и еще {group.links.length - (index === 0 ? 5 : 3)}...</small>
)}
</div>
</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
)
// Основная функция рендеринга групп в зависимости от выбранного макета
const renderGroupsLayout = () => {