|
/**
|
|
* @fileoverview Rant Feed Component for Rantii
|
|
* @author retoor <retoor@molodetz.nl>
|
|
* @description Infinite scrolling list of rants
|
|
* @keywords feed, list, rants, infinite, scroll
|
|
*/
|
|
|
|
import { BaseComponent } from './base-component.js';
|
|
|
|
class RantFeed extends BaseComponent {
|
|
static get observedAttributes() {
|
|
return ['sort', 'feed-type'];
|
|
}
|
|
|
|
init() {
|
|
this.rants = [];
|
|
this.skip = 0;
|
|
this.limit = 20;
|
|
this.isLoading = false;
|
|
this.hasMore = true;
|
|
this.sort = this.getAttr('sort') || 'recent';
|
|
this.feedType = this.getAttr('feed-type') || 'rants';
|
|
|
|
this.render();
|
|
this.bindEvents();
|
|
}
|
|
|
|
async load(reset = false) {
|
|
if (this.isLoading) return;
|
|
if (!reset && !this.hasMore) return;
|
|
|
|
if (reset) {
|
|
this.rants = [];
|
|
this.skip = 0;
|
|
this.hasMore = true;
|
|
}
|
|
|
|
this.isLoading = true;
|
|
this.updateLoadingState();
|
|
|
|
try {
|
|
let result;
|
|
const api = this.getApi();
|
|
|
|
switch (this.feedType) {
|
|
case 'weekly':
|
|
result = await api?.getWeeklyRants(this.sort, this.limit, this.skip);
|
|
break;
|
|
case 'collabs':
|
|
result = await api?.getCollabs(this.sort, this.limit, this.skip);
|
|
break;
|
|
case 'stories':
|
|
result = await api?.getStories(this.sort, this.limit, this.skip);
|
|
break;
|
|
case 'search':
|
|
break;
|
|
default:
|
|
result = await api?.getRants(this.sort, this.limit, this.skip);
|
|
}
|
|
|
|
if (result?.success) {
|
|
const newRants = result.rants || [];
|
|
this.rants = [...this.rants, ...newRants];
|
|
this.skip += newRants.length;
|
|
this.hasMore = newRants.length >= this.limit;
|
|
} else {
|
|
this.hasMore = false;
|
|
}
|
|
} catch (error) {
|
|
this.hasMore = false;
|
|
} finally {
|
|
this.isLoading = false;
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
async search(term) {
|
|
if (this.isLoading) return;
|
|
|
|
this.isLoading = true;
|
|
this.rants = [];
|
|
this.updateLoadingState();
|
|
|
|
try {
|
|
const result = await this.getApi()?.search(term);
|
|
if (result?.success) {
|
|
this.rants = result.rants || [];
|
|
}
|
|
this.hasMore = false;
|
|
} catch (error) {
|
|
this.rants = [];
|
|
} finally {
|
|
this.isLoading = false;
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
render() {
|
|
this.addClass('rant-feed');
|
|
|
|
if (this.rants.length === 0 && !this.isLoading) {
|
|
this.setHtml(`
|
|
<div class="feed-empty">
|
|
<p>No rants found</p>
|
|
</div>
|
|
`);
|
|
return;
|
|
}
|
|
|
|
this.setHtml(`
|
|
<div class="feed-controls">
|
|
<div class="sort-tabs">
|
|
<button class="sort-tab ${this.sort === 'recent' ? 'active' : ''}" data-sort="recent">Recent</button>
|
|
<button class="sort-tab ${this.sort === 'top' ? 'active' : ''}" data-sort="top">Top</button>
|
|
<button class="sort-tab ${this.sort === 'algo' ? 'active' : ''}" data-sort="algo">Algo</button>
|
|
</div>
|
|
</div>
|
|
<div class="feed-list">
|
|
${this.rants.map(rant => `
|
|
<rant-card rant-id="${rant.id}"></rant-card>
|
|
`).join('')}
|
|
</div>
|
|
${this.isLoading ? `
|
|
<div class="feed-loading">
|
|
<loading-spinner></loading-spinner>
|
|
</div>
|
|
` : ''}
|
|
${this.hasMore && !this.isLoading ? `
|
|
<div class="feed-loadmore">
|
|
<button class="btn btn-secondary load-more-btn">Load More</button>
|
|
</div>
|
|
` : ''}
|
|
`);
|
|
|
|
this.initRantCards();
|
|
}
|
|
|
|
initRantCards() {
|
|
const cards = this.$$('rant-card');
|
|
cards.forEach((card, index) => {
|
|
if (this.rants[index]) {
|
|
card.setRant(this.rants[index]);
|
|
}
|
|
});
|
|
}
|
|
|
|
updateLoadingState() {
|
|
const loadingEl = this.$('.feed-loading');
|
|
if (this.isLoading && !loadingEl && this.rants.length > 0) {
|
|
const loadMore = this.$('.feed-loadmore');
|
|
if (loadMore) {
|
|
loadMore.innerHTML = '<loading-spinner></loading-spinner>';
|
|
}
|
|
}
|
|
}
|
|
|
|
bindEvents() {
|
|
this.on(this, 'click', this.handleClick);
|
|
this.setupInfiniteScroll();
|
|
}
|
|
|
|
handleClick(e) {
|
|
const sortTab = e.target.closest('.sort-tab');
|
|
const loadMoreBtn = e.target.closest('.load-more-btn');
|
|
|
|
if (sortTab) {
|
|
const newSort = sortTab.dataset.sort;
|
|
if (newSort !== this.sort) {
|
|
this.sort = newSort;
|
|
this.setAttr('sort', newSort);
|
|
this.load(true);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (loadMoreBtn) {
|
|
this.load();
|
|
}
|
|
}
|
|
|
|
setupInfiniteScroll() {
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting && !this.isLoading && this.hasMore) {
|
|
this.load();
|
|
}
|
|
});
|
|
},
|
|
{ rootMargin: '200px' }
|
|
);
|
|
|
|
this.intersectionObserver = observer;
|
|
}
|
|
|
|
onConnected() {
|
|
this.load(true);
|
|
}
|
|
|
|
onDisconnected() {
|
|
if (this.intersectionObserver) {
|
|
this.intersectionObserver.disconnect();
|
|
}
|
|
this.isLoading = false;
|
|
this.hasMore = true;
|
|
}
|
|
|
|
onAttributeChanged(name, oldValue, newValue) {
|
|
if (name === 'sort' && oldValue !== newValue) {
|
|
this.sort = newValue;
|
|
this.load(true);
|
|
}
|
|
if (name === 'feed-type' && oldValue !== newValue) {
|
|
this.feedType = newValue;
|
|
this.load(true);
|
|
}
|
|
}
|
|
|
|
setSort(sort) {
|
|
this.sort = sort;
|
|
this.setAttr('sort', sort);
|
|
this.load(true);
|
|
}
|
|
|
|
setFeedType(type) {
|
|
this.feedType = type;
|
|
this.setAttr('feed-type', type);
|
|
this.load(true);
|
|
}
|
|
|
|
refresh() {
|
|
this.load(true);
|
|
}
|
|
|
|
getRants() {
|
|
return this.rants;
|
|
}
|
|
}
|
|
|
|
customElements.define('rant-feed', RantFeed);
|
|
|
|
export { RantFeed };
|