Add section components

This commit is contained in:
Daniel Supernault 2023-06-11 15:35:06 -06:00
parent 9817025578
commit bffd8f0771
No known key found for this signature in database
GPG key ID: 0DEF1C662C9033F7
2 changed files with 788 additions and 0 deletions

View file

@ -0,0 +1,206 @@
<template>
<div class="discover-feed-component">
<section class="mt-3 mb-5 section-explore">
<b-breadcrumb class="font-default" :items="breadcrumbItems"></b-breadcrumb>
<div class="profile-timeline">
<div class="row p-0 mt-5">
<div class="col-12 mb-4 d-flex justify-content-between align-items-center">
<p class="d-block d-md-none h1 font-weight-bold mb-0 font-default">Trending</p>
<p class="d-none d-md-block display-4 font-weight-bold mb-0 font-default">Trending</p>
<div>
<div class="btn-group trending-range">
<button @click="rangeToggle('daily')" :class="range == 'daily' ? 'btn py-1 font-weight-bold px-3 text-uppercase btn-sm btn-danger':'btn py-1 font-weight-bold px-3 text-uppercase btn-sm btn-outline-danger'">Today</button>
<button @click="rangeToggle('monthly')" :class="range == 'monthly' ? 'btn py-1 font-weight-bold px-3 text-uppercase btn-sm btn-danger':'btn py-1 font-weight-bold px-3 text-uppercase btn-sm btn-outline-danger'">This month</button>
<button @click="rangeToggle('yearly')" :class="range == 'yearly' ? 'btn py-1 font-weight-bold px-3 text-uppercase btn-sm btn-danger':'btn py-1 font-weight-bold px-3 text-uppercase btn-sm btn-outline-danger'">This year</button>
</div>
</div>
</div>
</div>
<div v-if="!loading" class="row p-0 px-lg-3">
<div v-if="trending.length" v-for="(s, index) in trending" class="col-6 col-lg-4 col-xl-3 p-1">
<a class="card info-overlay card-md-border-0" :href="s.url" @click.prevent="goToPost(s)">
<div class="square square-next">
<div v-if="s.sensitive" class="square-content">
<div class="info-overlay-text-label">
<h5 class="text-white m-auto font-weight-bold">
<span>
<span class="far fa-eye-slash fa-lg p-2 d-flex-inline"></span>
</span>
</h5>
</div>
<blur-hash-canvas
width="32"
height="32"
:hash="s.media_attachments[0].blurhash"
/>
</div>
<div v-else class="square-content">
<blur-hash-image
width="32"
height="32"
:hash="s.media_attachments[0].blurhash"
:src="s.media_attachments[0].preview_url"
/>
</div>
<div class="info-overlay-text">
<div class="text-white m-auto">
<p class="info-overlay-text-field font-weight-bold">
<span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{formatCount(s.favourites_count)}}</span>
</p>
<p class="info-overlay-text-field font-weight-bold">
<span class="far fa-comment fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{formatCount(s.reply_count)}}</span>
</p>
<p class="mb-0 info-overlay-text-field font-weight-bold">
<span class="far fa-sync fa-lg p-2 d-flex-inline"></span>
<span class="d-flex-inline">{{formatCount(s.reblogs_count)}}</span>
</p>
</div>
</div>
</div>
</a>
</div>
<div v-else class="col-12 d-flex align-items-center justify-content-center bg-light border" style="min-height: 40vh;">
<div class="h2">No posts found :(</div>
</div>
</div>
<div v-else class="row p-0 px-lg-3">
<div class="col-12 d-flex align-items-center justify-content-center" style="min-height: 40vh;">
<b-spinner size="lg" />
</div>
</div>
</div>
</section>
</div>
</template>
<script type="text/javascript">
export default {
props: {
profile: {
type: Object
}
},
data() {
return {
loading: true,
trending: [],
range: 'daily',
breadcrumbItems: [
{
text: 'Discover',
href: '/i/web/discover'
},
{
text: 'Trending',
active: true
}
]
}
},
mounted() {
this.loadTrending();
},
methods: {
fetchData() {
axios.get('/api/pixelfed/v2/discover/posts')
.then((res) => {
this.posts = res.data.posts.filter(r => r != null);
this.recommendedLoading = false;
});
},
loadTrending() {
this.loading = true;
axios.get('/api/pixelfed/v2/discover/posts/trending', {
params: {
range: this.range
}
})
.then(res => {
let data = res.data.filter(r => {
return r !== null;
});
this.trending = data.filter(t => t.sensitive == false);
if(this.range == 'daily' && data.length == 0) {
this.range = 'yearly';
this.loadTrending();
}
this.loading = false;
});
},
formatCount(s) {
return App.util.format.count(s);
},
goToPost(status) {
this.$router.push({
name: 'post',
params: {
id: status.id,
cachedStatus: status,
cachedProfile: this.profile
}
})
},
rangeToggle(range) {
event.currentTarget.blur();
this.range = range;
this.loadTrending();
}
}
}
</script>
<style lang="scss">
.discover-feed-component {
.font-default {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
letter-spacing: -0.7px;
}
.info-overlay {
border-radius: 15px !important;
}
.square-next {
img,
.info-overlay-text {
border-radius: 15px !important;
}
}
.trending-range {
.btn {
&:hover:not(.btn-danger) {
background-color: #fca5a5
}
}
}
.info-overlay-text-field {
font-size: 13.5px;
margin-bottom: 2px;
@media (min-width: 768px) {
font-size: 20px;
margin-bottom: 15px;
}
}
}
</style>

View file

@ -0,0 +1,582 @@
<template>
<div class="timeline-section-component">
<div v-if="!isLoaded">
<status-placeholder />
<status-placeholder />
<status-placeholder />
<status-placeholder />
</div>
<div v-else>
<status
v-for="(status, index) in feed"
:key="'pf_feed:' + status.id + ':idx:' + index + ':fui:' + forceUpdateIdx"
:status="status"
:profile="profile"
v-on:like="likeStatus(index)"
v-on:unlike="unlikeStatus(index)"
v-on:share="shareStatus(index)"
v-on:unshare="unshareStatus(index)"
v-on:menu="openContextMenu(index)"
v-on:counter-change="counterChange(index, $event)"
v-on:likes-modal="openLikesModal(index)"
v-on:shares-modal="openSharesModal(index)"
v-on:follow="follow(index)"
v-on:unfollow="unfollow(index)"
v-on:comment-likes-modal="openCommentLikesModal"
v-on:handle-report="handleReport"
v-on:bookmark="handleBookmark(index)"
v-on:mod-tools="handleModTools(index)"
/>
<div v-if="showLoadMore" class="text-center">
<button
class="btn btn-primary rounded-pill font-weight-bold"
@click="tryToLoadMore">
Load more
</button>
</div>
<div v-if="canLoadMore">
<intersect @enter="enterIntersect">
<status-placeholder style="margin-bottom: 10rem;"/>
</intersect>
</div>
<div v-if="!isLoaded && feed.length && endFeedReached" style="margin-bottom: 50vh">
<div class="card card-body shadow-sm mb-3" style="border-radius: 15px;">
<p class="display-4 text-center"></p>
<p class="lead mb-0 text-center">You have reached the end of this feed</p>
</div>
</div>
<timeline-onboarding
v-if="scope == 'home' && !feed.length"
:profile="profile"
v-on:update-profile="updateProfile" />
<empty-timeline v-if="isLoaded && scope !== 'home' && !feed.length" />
</div>
<context-menu
v-if="showMenu"
ref="contextMenu"
:status="feed[postIndex]"
:profile="profile"
v-on:moderate="commitModeration"
v-on:delete="deletePost"
v-on:report-modal="handleReport"
v-on:edit="handleEdit"
/>
<likes-modal
v-if="showLikesModal"
ref="likesModal"
:status="likesModalPost"
:profile="profile"
/>
<shares-modal
v-if="showSharesModal"
ref="sharesModal"
:status="sharesModalPost"
:profile="profile"
/>
<report-modal
ref="reportModal"
:key="reportedStatusId"
:status="reportedStatus"
/>
<post-edit-modal
ref="editModal"
v-on:update="mergeUpdatedPost"
/>
</div>
</template>
<script type="text/javascript">
import StatusPlaceholder from './../partials/StatusPlaceholder.vue';
import Status from './../partials/TimelineStatus.vue';
import Intersect from 'vue-intersect';
import ContextMenu from './../partials/post/ContextMenu.vue';
import LikesModal from './../partials/post/LikeModal.vue';
import SharesModal from './../partials/post/ShareModal.vue';
import ReportModal from './../partials/modal/ReportPost.vue';
import EmptyTimeline from './../partials/placeholders/EmptyTimeline.vue'
import TimelineOnboarding from './../partials/placeholders/TimelineOnboarding.vue'
import PostEditModal from './../partials/post/PostEditModal.vue';
export default {
props: {
scope: {
type: String,
default: "home"
},
profile: {
type: Object
},
refresh: {
type: Boolean,
default: false
}
},
components: {
"intersect": Intersect,
"status-placeholder": StatusPlaceholder,
"status": Status,
"context-menu": ContextMenu,
"likes-modal": LikesModal,
"shares-modal": SharesModal,
"report-modal": ReportModal,
"empty-timeline": EmptyTimeline,
"timeline-onboarding": TimelineOnboarding,
"post-edit-modal": PostEditModal
},
data() {
return {
isLoaded: false,
feed: [],
ids: [],
max_id: 0,
canLoadMore: true,
showLoadMore: false,
loadMoreTimeout: undefined,
loadMoreAttempts: 0,
isFetchingMore: false,
endFeedReached: false,
postIndex: 0,
showMenu: false,
showLikesModal: false,
likesModalPost: {},
showReportModal: false,
reportedStatus: {},
reportedStatusId: 0,
showSharesModal: false,
sharesModalPost: {},
forceUpdateIdx: 0
}
},
mounted() {
if(window.App.config.features.hasOwnProperty('timelines')) {
if(this.scope == 'local' && !window.App.config.features.timelines.local) {
swal('Error', 'Cannot load this timeline', 'error');
return;
};
if(this.scope == 'network' && !window.App.config.features.timelines.network) {
swal('Error', 'Cannot load this timeline', 'error');
return;
};
}
this.fetchTimeline();
},
methods: {
getScope() {
switch(this.scope) {
case 'local':
return 'public'
break;
case 'global':
return 'network'
break;
default:
return 'home';
break;
}
},
fetchTimeline(scrollToTop = false) {
let url = `/api/pixelfed/v1/timelines/${this.getScope()}`;
axios.get(url, {
params: {
max_id: this.max_id,
limit: 6
}
}).then(res => {
let ids = res.data.map(p => {
if(p && p.hasOwnProperty('relationship')) {
this.$store.commit('updateRelationship', [p.relationship]);
}
return p.id
});
this.isLoaded = true;
if(res.data.length == 0) {
return;
}
this.ids = ids;
this.max_id = Math.min(...ids);
this.feed = res.data;
if(res.data.length !== 6) {
this.canLoadMore = false;
this.showLoadMore = true;
}
})
.then(() => {
if(scrollToTop) {
this.$nextTick(() => {
window.scrollTo({
top: 0,
left: 0,
behavior: 'smooth'
});
this.$emit('refreshed');
});
}
})
},
enterIntersect() {
if(this.isFetchingMore) {
return;
}
this.isFetchingMore = true;
let url = `/api/pixelfed/v1/timelines/${this.getScope()}`;
axios.get(url, {
params: {
max_id: this.max_id,
limit: 6
}
}).then(res => {
if(!res.data.length) {
this.endFeedReached = true;
this.canLoadMore = false;
this.isFetchingMore = false;
}
setTimeout(() => {
res.data.forEach(p => {
if(this.ids.indexOf(p.id) == -1) {
if(this.max_id > p.id) {
this.max_id = p.id;
}
this.ids.push(p.id);
this.feed.push(p);
if(p && p.hasOwnProperty('relationship')) {
this.$store.commit('updateRelationship', [p.relationship]);
}
}
});
this.isFetchingMore = false;
}, 100);
});
},
tryToLoadMore() {
this.loadMoreAttempts++;
if(this.loadMoreAttempts >= 3) {
this.showLoadMore = false;
}
this.showLoadMore = false;
this.canLoadMore = true;
this.loadMoreTimeout = setTimeout(() => {
this.canLoadMore = false;
this.showLoadMore = true;
}, 5000);
},
likeStatus(index) {
let status = this.feed[index];
let state = status.favourited;
let count = status.favourites_count;
this.feed[index].favourites_count = count + 1;
this.feed[index].favourited = !status.favourited;
axios.post('/api/v1/statuses/' + status.id + '/favourite')
.then(res => {
//
}).catch(err => {
this.feed[index].favourites_count = count;
this.feed[index].favourited = false;
let el = document.createElement('p');
el.classList.add('text-left');
el.classList.add('mb-0');
el.innerHTML = '<span class="lead">We limit certain interactions to keep our community healthy and it appears that you have reached that limit. <span class="font-weight-bold">Please try again later.</span></span>';
let wrapper = document.createElement('div');
wrapper.appendChild(el);
if(err.response.status === 429) {
swal({
title: 'Too many requests',
content: wrapper,
icon: 'warning',
buttons: {
// moreInfo: {
// text: "Contact a human",
// visible: true,
// value: "more",
// className: "text-lighter bg-transparent border"
// },
confirm: {
text: "OK",
value: false,
visible: true,
className: "bg-transparent primary",
closeModal: true
}
}
})
.then((val) => {
if(val == 'more') {
location.href = '/site/contact'
}
return;
});
}
})
},
unlikeStatus(index) {
let status = this.feed[index];
let state = status.favourited;
let count = status.favourites_count;
this.feed[index].favourites_count = count - 1;
this.feed[index].favourited = !status.favourited;
axios.post('/api/v1/statuses/' + status.id + '/unfavourite')
.then(res => {
//
}).catch(err => {
this.feed[index].favourites_count = count;
this.feed[index].favourited = false;
})
},
openContextMenu(idx) {
this.postIndex = idx;
this.showMenu = true;
this.$nextTick(() => {
this.$refs.contextMenu.open();
});
},
handleModTools(idx) {
this.postIndex = idx;
this.showMenu = true;
this.$nextTick(() => {
this.$refs.contextMenu.openModMenu();
});
},
openLikesModal(idx) {
this.postIndex = idx;
this.likesModalPost = this.feed[this.postIndex];
this.showLikesModal = true;
this.$nextTick(() => {
this.$refs.likesModal.open();
});
},
openSharesModal(idx) {
this.postIndex = idx;
this.sharesModalPost = this.feed[this.postIndex];
this.showSharesModal = true;
this.$nextTick(() => {
this.$refs.sharesModal.open();
});
},
commitModeration(type) {
let idx = this.postIndex;
switch(type) {
case 'addcw':
this.feed[idx].sensitive = true;
break;
case 'remcw':
this.feed[idx].sensitive = false;
break;
case 'unlist':
this.feed.splice(idx, 1);
break;
case 'spammer':
let id = this.feed[idx].account.id;
this.feed = this.feed.filter(post => {
return post.account.id != id;
});
break;
}
},
deletePost() {
this.feed.splice(this.postIndex, 1);
},
counterChange(index, type) {
switch(type) {
case 'comment-increment':
this.feed[index].reply_count = this.feed[index].reply_count + 1;
break;
case 'comment-decrement':
this.feed[index].reply_count = this.feed[index].reply_count - 1;
break;
}
},
openCommentLikesModal(post) {
this.likesModalPost = post;
this.showLikesModal = true;
this.$nextTick(() => {
this.$refs.likesModal.open();
});
},
shareStatus(index) {
let status = this.feed[index];
let state = status.reblogged;
let count = status.reblogs_count;
this.feed[index].reblogs_count = count + 1;
this.feed[index].reblogged = !status.reblogged;
axios.post('/api/v1/statuses/' + status.id + '/reblog')
.then(res => {
//
}).catch(err => {
this.feed[index].reblogs_count = count;
this.feed[index].reblogged = false;
})
},
unshareStatus(index) {
let status = this.feed[index];
let state = status.reblogged;
let count = status.reblogs_count;
this.feed[index].reblogs_count = count - 1;
this.feed[index].reblogged = !status.reblogged;
axios.post('/api/v1/statuses/' + status.id + '/unreblog')
.then(res => {
//
}).catch(err => {
this.feed[index].reblogs_count = count;
this.feed[index].reblogged = false;
})
},
handleReport(post) {
this.reportedStatusId = post.id;
this.$nextTick(() => {
this.reportedStatus = post;
this.$refs.reportModal.open();
});
},
handleBookmark(index) {
let p = this.feed[index];
axios.post('/i/bookmark', {
item: p.id
})
.then(res => {
this.feed[index].bookmarked = !p.bookmarked;
})
.catch(err => {
// this.feed[index].bookmarked = false;
this.$bvToast.toast('Cannot bookmark post at this time.', {
title: 'Bookmark Error',
variant: 'danger',
autoHideDelay: 5000
});
});
},
follow(index) {
// this.feed[index].relationship.following = true;
axios.post('/api/v1/accounts/' + this.feed[index].account.id + '/follow')
.then(res => {
this.$store.commit('updateRelationship', [res.data]);
this.updateProfile({ following_count: this.profile.following_count + 1 });
this.feed[index].account.followers_count = this.feed[index].account.followers_count + 1;
}).catch(err => {
swal('Oops!', 'An error occured when attempting to follow this account.', 'error');
this.feed[index].relationship.following = false;
});
},
unfollow(index) {
// this.feed[index].relationship.following = false;
axios.post('/api/v1/accounts/' + this.feed[index].account.id + '/unfollow')
.then(res => {
this.$store.commit('updateRelationship', [res.data]);
this.updateProfile({ following_count: this.profile.following_count - 1 });
this.feed[index].account.followers_count = this.feed[index].account.followers_count - 1;
}).catch(err => {
swal('Oops!', 'An error occured when attempting to unfollow this account.', 'error');
this.feed[index].relationship.following = true;
});
},
updateProfile(delta) {
this.$emit('update-profile', delta);
},
handleRefresh() {
this.isLoaded = false;
this.feed = [];
this.ids = [];
this.max_id = 0;
this.canLoadMore = true;
this.showLoadMore = false;
this.loadMoreTimeout = undefined;
this.loadMoreAttempts = 0;
this.isFetchingMore = false;
this.endFeedReached = false;
this.postIndex = 0;
this.showMenu = false;
this.showLikesModal = false;
this.likesModalPost = {};
this.showReportModal = false;
this.reportedStatus = {};
this.reportedStatusId = 0;
this.showSharesModal = false;
this.sharesModalPost = {};
this.$nextTick(() => {
this.fetchTimeline(true);
});
},
handleEdit(status) {
this.$refs.editModal.show(status);
},
mergeUpdatedPost(post) {
this.feed = this.feed.map(p => {
if(p.id == post.id) {
p = post;
}
return p;
});
this.$nextTick(() => {
this.forceUpdateIdx++;
});
}
},
watch: {
'refresh': 'handleRefresh'
},
beforeDestroy() {
clearTimeout(this.loadMoreTimeout);
}
}
</script>