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():
|
def get_backend_url():
|
||||||
"""Получить базовый URL backend из настроек"""
|
"""Получить базовый 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():
|
def get_media_base_url():
|
||||||
"""Получить базовый URL для медиа файлов"""
|
"""Получить базовый URL для медиа файлов"""
|
||||||
return getattr(settings, 'MEDIA_BASE_URL', 'http://localhost:8000')
|
return get_backend_url() # Используем тот же базовый URL
|
||||||
|
|
||||||
|
|
||||||
def build_absolute_url(path):
|
def build_absolute_url(path):
|
||||||
@@ -48,7 +50,8 @@ def normalize_file_url(file_url):
|
|||||||
'http://web:8000',
|
'http://web:8000',
|
||||||
'http://links-web-1:8000',
|
'http://links-web-1:8000',
|
||||||
'http://backend:8000',
|
'http://backend:8000',
|
||||||
'http://localhost:8000'
|
'http://localhost:8000',
|
||||||
|
'http://links.shareon.kr' # Заменяем HTTP на HTTPS
|
||||||
]
|
]
|
||||||
|
|
||||||
# Заменяем все внутренние URL на внешний
|
# Заменяем все внутренние URL на внешний
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export default function DashboardClient() {
|
|||||||
fetch('/api/auth/user', { headers: { Authorization: `Bearer ${token}` } }),
|
fetch('/api/auth/user', { headers: { Authorization: `Bearer ${token}` } }),
|
||||||
fetch('/api/groups', { headers: { Authorization: `Bearer ${token}` } }),
|
fetch('/api/groups', { headers: { Authorization: `Bearer ${token}` } }),
|
||||||
fetch('/api/links', { 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]) => {
|
.then(async ([uRes, gRes, lRes, dRes]) => {
|
||||||
if (!uRes.ok) throw new Error('Не удалось получить профиль')
|
if (!uRes.ok) throw new Error('Не удалось получить профиль')
|
||||||
|
|||||||
@@ -563,10 +563,274 @@ export default function UserPage({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
// Заглушки для остальных макетов
|
// Кладка макет
|
||||||
const renderMasonryLayout = () => renderGridLayout()
|
const renderMasonryLayout = () => (
|
||||||
const renderTimelineLayout = () => renderListLayout()
|
<div>
|
||||||
const renderMagazineLayout = () => renderCardsLayout()
|
<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 = () => {
|
const renderGroupsLayout = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user