225 lines
7.9 KiB
JavaScript
Raw Normal View History

2025-12-04 20:29:35 +01:00
/**
* @fileoverview User Profile Component for Rantii
* @author retoor <retoor@molodetz.nl>
* @description Complete user profile display with stats and content
* @keywords user, profile, stats, about, content
*/
import { BaseComponent } from './base-component.js';
import { formatDate } from '../utils/date.js';
class UserProfile extends BaseComponent {
static get observedAttributes() {
return ['username', 'user-id'];
}
init() {
this.profileData = null;
this.isLoading = false;
this.activeTab = 'rants';
this.render();
this.bindEvents();
}
async load(username) {
if (!username) return;
this.isLoading = true;
this.setAttr('username', username);
this.render();
try {
const result = await this.getApi()?.getProfileByUsername(username);
if (result?.success) {
this.profileData = result.profile;
}
} catch (error) {
this.profileData = null;
} finally {
this.isLoading = false;
this.render();
}
}
render() {
if (this.isLoading) {
this.setHtml(`
<div class="profile-loading">
<loading-spinner text="Loading profile..."></loading-spinner>
</div>
`);
return;
}
if (!this.profileData) {
this.setHtml(`
<div class="profile-error">
<p>User not found</p>
<button class="btn btn-primary back-btn">Go Back</button>
</div>
`);
return;
}
const profile = this.profileData;
const rants = profile.content?.content?.rants || [];
const comments = profile.content?.content?.comments || [];
this.addClass('user-profile');
this.setHtml(`
<header class="profile-header">
<button class="back-btn" aria-label="Go back">
<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/>
</svg>
</button>
<div class="profile-avatar">
<user-avatar
avatar='${JSON.stringify(profile.avatar)}'
username="${profile.username}"
size="large">
</user-avatar>
</div>
<div class="profile-info">
<h1 class="profile-username">${profile.username}</h1>
<div class="profile-score">+${profile.score}</div>
</div>
</header>
<section class="profile-details">
${profile.about ? `
<div class="detail-item">
<span class="detail-label">About</span>
<p class="detail-value">${profile.about}</p>
</div>
` : ''}
${profile.location ? `
<div class="detail-item">
<span class="detail-label">Location</span>
<span class="detail-value">${profile.location}</span>
</div>
` : ''}
${profile.skills ? `
<div class="detail-item">
<span class="detail-label">Skills</span>
<span class="detail-value">${profile.skills}</span>
</div>
` : ''}
${profile.github ? `
<div class="detail-item">
<span class="detail-label">GitHub</span>
<a class="detail-value detail-link" href="https://github.com/${profile.github}" target="_blank" rel="noopener">${profile.github}</a>
</div>
` : ''}
${profile.website ? `
<div class="detail-item">
<span class="detail-label">Website</span>
<a class="detail-value detail-link" href="${profile.website}" target="_blank" rel="noopener">${profile.website}</a>
</div>
` : ''}
<div class="detail-item">
<span class="detail-label">Joined</span>
<span class="detail-value">${formatDate(profile.created_time)}</span>
</div>
</section>
<section class="profile-content">
<div class="content-tabs">
<button class="tab ${this.activeTab === 'rants' ? 'active' : ''}" data-tab="rants">
Rants (${rants.length})
</button>
<button class="tab ${this.activeTab === 'comments' ? 'active' : ''}" data-tab="comments">
Comments (${comments.length})
</button>
</div>
<div class="content-panel">
${this.activeTab === 'rants' ? `
<div class="rants-list">
${rants.length > 0 ? rants.map(rant => `
<rant-card rant-id="${rant.id}"></rant-card>
`).join('') : '<p class="empty-message">No rants yet</p>'}
</div>
` : ''}
${this.activeTab === 'comments' ? `
<div class="comments-list">
${comments.length > 0 ? comments.map(comment => `
<div class="profile-comment" data-rant-id="${comment.rant_id}">
<rant-content text="${this.escapeAttr(comment.body)}"></rant-content>
<div class="comment-meta">
<span class="comment-score">+${comment.score}</span>
</div>
</div>
`).join('') : '<p class="empty-message">No comments yet</p>'}
</div>
` : ''}
</div>
</section>
`);
this.initRantCards(rants);
}
escapeAttr(str) {
if (!str) return '';
return str
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
initRantCards(rants) {
const cards = this.$$('rant-card');
cards.forEach((card, index) => {
if (rants[index]) {
card.setRant(rants[index]);
}
});
}
bindEvents() {
this.on(this, 'click', this.handleClick);
}
handleClick(e) {
const backBtn = e.target.closest('.back-btn');
const tab = e.target.closest('.tab');
const profileComment = e.target.closest('.profile-comment');
if (backBtn) {
e.preventDefault();
window.history.back();
return;
}
if (tab) {
this.activeTab = tab.dataset.tab;
this.render();
const rants = this.profileData?.content?.content?.rants || [];
this.initRantCards(rants);
return;
}
if (profileComment) {
const rantId = profileComment.dataset.rantId;
if (rantId) {
this.getRouter()?.goToRant(rantId);
}
}
}
onAttributeChanged(name, oldValue, newValue) {
if (name === 'username' && newValue && oldValue !== newValue) {
this.load(newValue);
}
}
getUsername() {
return this.profileData?.username || this.getAttr('username');
}
}
customElements.define('user-profile', UserProfile);
export { UserProfile };