Fix public page layouts and avatar display
Some checks failed
continuous-integration/drone/push Build is failing
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:
@@ -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 на внешний
|
||||
|
||||
@@ -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('Не удалось получить профиль')
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
Reference in New Issue
Block a user