225 lines
7.9 KiB
JavaScript
225 lines
7.9 KiB
JavaScript
|
|
/**
|
||
|
|
* @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, '&')
|
||
|
|
.replace(/"/g, '"')
|
||
|
|
.replace(/'/g, ''')
|
||
|
|
.replace(/</g, '<')
|
||
|
|
.replace(/>/g, '>');
|
||
|
|
}
|
||
|
|
|
||
|
|
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 };
|