mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-12-23 13:33:18 +00:00
507 lines
17 KiB
Vue
507 lines
17 KiB
Vue
<template>
|
|
<div class="container">
|
|
<div v-if="loading" class="pt-5 text-center">
|
|
<div class="spinner-border" role="status">
|
|
<span class="sr-only">Loading…</span>
|
|
</div>
|
|
</div>
|
|
<div v-if="networkError" class="pt-5 text-center">
|
|
<p class="lead font-weight-lighter">An error occured, results could not be loaded.<br> Please try again later.</p>
|
|
</div>
|
|
|
|
<div v-if="!loading && !networkError" class="mt-5">
|
|
<div v-if="analysis == 'all'" class="row">
|
|
<div class="col-12 mb-5">
|
|
<p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
|
|
<hr>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="mb-4">
|
|
<p class="text-secondary small font-weight-bold">HASHTAGS <span class="pl-1 text-lighter">({{results.hashtags.length}})</span></p>
|
|
</div>
|
|
<div v-if="results.hashtags.length">
|
|
<a v-for="(hashtag, index) in results.hashtags" class="mb-2 result-card" :href="buildUrl('hashtag', hashtag)">
|
|
<div class="pb-3">
|
|
<div class="media align-items-center py-2 pr-3">
|
|
<span class="d-inline-flex align-items-center justify-content-center border rounded-circle mr-3" style="width: 50px;height: 50px;">
|
|
<i class="fas fa-hashtag text-muted"></i>
|
|
</span>
|
|
<div class="media-body text-truncate">
|
|
<p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
|
|
#{{hashtag.value}}
|
|
</p>
|
|
<p v-if="hashtag.count > 2" class="mb-0 small font-weight-bold text-muted text-uppercase">
|
|
{{hashtag.count}} posts
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
<div v-else>
|
|
<div class="border py-3 text-center font-weight-bold">No results found</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-5">
|
|
<div class="mb-4">
|
|
<p class="text-secondary small font-weight-bold">PROFILES <span class="pl-1 text-lighter">({{results.profiles.length}})</span></p>
|
|
</div>
|
|
<div v-if="results.profiles.length">
|
|
<a v-for="(profile, index) in results.profiles" class="mb-2 result-card" :href="buildUrl('profile', profile)">
|
|
<div class="pb-3">
|
|
<div class="media align-items-center py-2 pr-3">
|
|
<img class="mr-3 rounded-circle border" :src="profile.avatar" width="50px" height="50px">
|
|
<div class="media-body">
|
|
<p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value">
|
|
{{profile.value}}
|
|
</p>
|
|
<p class="mb-0 small font-weight-bold text-muted text-uppercase">
|
|
{{profile.entity.post_count}} Posts
|
|
</p>
|
|
</div>
|
|
<div class="ml-3">
|
|
<a v-if="profile.entity.following" class="btn btn-primary btn-sm font-weight-bold text-uppercase py-0" :href="buildUrl('profile', profile)">Following</a>
|
|
<a v-else class="btn btn-outline-primary btn-sm font-weight-bold text-uppercase py-0" :href="buildUrl('profile', profile)">View</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
<div v-else>
|
|
<div class="border py-3 text-center font-weight-bold">No results found</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="mb-4">
|
|
<p class="text-secondary small font-weight-bold">STATUSES <span class="pl-1 text-lighter">({{results.statuses.length}})</span></p>
|
|
</div>
|
|
<div v-if="results.statuses.length">
|
|
<a v-for="(status, index) in results.statuses" class="mr-2 result-card" :href="buildUrl('status', status)">
|
|
<img :src="status.thumb" width="90px" height="90px" class="mb-2">
|
|
</a>
|
|
</div>
|
|
<div v-else>
|
|
<div class="border py-3 text-center font-weight-bold">No results found</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="analysis == 'hashtag'" class="row">
|
|
<div class="col-12 mb-5">
|
|
<p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
|
|
<hr>
|
|
</div>
|
|
<div class="col-md-6 offset-md-3">
|
|
<div class="mb-4">
|
|
<p class="text-secondary small font-weight-bold">HASHTAGS <span class="pl-1 text-lighter">({{results.hashtags.length}})</span></p>
|
|
</div>
|
|
<div v-if="results.hashtags.length">
|
|
<a v-for="(hashtag, index) in results.hashtags" class="mb-2 result-card" :href="buildUrl('hashtag', hashtag)">
|
|
<div class="pb-3">
|
|
<div class="media align-items-center py-2 pr-3">
|
|
<span class="d-inline-flex align-items-center justify-content-center border rounded-circle mr-3" style="width: 50px;height: 50px;">
|
|
<i class="fas fa-hashtag text-muted"></i>
|
|
</span>
|
|
<div class="media-body">
|
|
<p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="hashtag.value">
|
|
#{{hashtag.value}}
|
|
</p>
|
|
<p v-if="hashtag.count > 2" class="mb-0 small font-weight-bold text-muted text-uppercase">
|
|
{{hashtag.count}} posts
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
<div v-else>
|
|
<div class="border py-3 text-center font-weight-bold">No results found</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="analysis == 'profile'" class="row">
|
|
<div class="col-12 mb-5">
|
|
<p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
|
|
<hr>
|
|
</div>
|
|
<div class="col-md-6 offset-md-3">
|
|
<div class="mb-4">
|
|
<p class="text-secondary small font-weight-bold">PROFILES <span class="pl-1 text-lighter">({{results.profiles.length}})</span></p>
|
|
</div>
|
|
<div v-if="results.profiles.length">
|
|
<div v-for="(profile, index) in results.profiles" class="card mb-4">
|
|
<div class="card-header p-0 m-0">
|
|
<div style="width: 100%;height: 140px;background: #0070b7"></div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="text-center mt-n5 mb-4">
|
|
<img class="rounded-circle p-1 border mt-n4 bg-white shadow" :src="profile.entity.thumb" width="90px" height="90px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
|
|
</div>
|
|
<p class="text-center lead font-weight-bold mb-1">{{profile.value}}</p>
|
|
<p class="text-center text-muted small text-uppercase mb-4"><!-- 2 followers --></p>
|
|
<div class="d-flex justify-content-center">
|
|
<button v-if="profile.entity.following" type="button" class="btn btn-outline-secondary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3" style="font-weight: 500">Following</button>
|
|
<a class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold" :href="buildUrl('profile',profile)" style="font-weight: 500">View Profile</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else>
|
|
<div class="border py-3 text-center font-weight-bold">No results found</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="analysis == 'webfinger'" class="row">
|
|
<div class="col-12 mb-5">
|
|
<p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
|
|
<hr>
|
|
<div class="col-md-6 offset-md-3">
|
|
<div v-for="(profile, index) in results.profiles" class="card mb-2">
|
|
<div class="card-header p-0 m-0">
|
|
<div style="width: 100%;height: 140px;background: #0070b7"></div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="text-center mt-n5 mb-4">
|
|
<img class="rounded-circle p-1 border mt-n4 bg-white shadow" :src="profile.entity.thumb" width="90px" height="90px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
|
|
</div>
|
|
<p class="text-center lead font-weight-bold mb-1">{{profile.value}}</p>
|
|
<p class="text-center text-muted small text-uppercase mb-4"><!-- 2 followers --></p>
|
|
<div class="d-flex justify-content-center">
|
|
<!-- <button v-if="profile.entity.following" type="button" class="btn btn-outline-secondary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3" style="font-weight: 500">Unfollow</button> -->
|
|
<!-- <button v-else type="button" class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold mr-3" style="font-weight: 500">Follow</button> -->
|
|
<a class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold" :href="'/i/web/profile/_/' + profile.entity.id" style="font-weight: 500">View Profile</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="analysis == 'remote'" class="row">
|
|
<div class="col-12 mb-5">
|
|
<p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
|
|
<hr>
|
|
</div>
|
|
<div v-if="results.profiles.length" class="col-md-6 offset-3">
|
|
<a v-for="(profile, index) in results.profiles" class="mb-2 result-card" :href="buildUrl('profile', profile)">
|
|
<div class="pb-3">
|
|
<div class="media align-items-center py-2 pr-3">
|
|
<img class="mr-3 rounded-circle border" :src="profile.entity.thumb" width="50px" height="50px" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
|
|
<div class="media-body">
|
|
<p class="mb-0 text-truncate text-dark font-weight-bold" data-toggle="tooltip" :title="profile.value">
|
|
{{profile.value}}
|
|
</p>
|
|
<p class="mb-0 small font-weight-bold text-muted text-uppercase">
|
|
{{profile.entity.post_count}} Posts
|
|
</p>
|
|
</div>
|
|
<div class="ml-3">
|
|
<a v-if="profile.entity.following" class="btn btn-primary btn-sm font-weight-bold text-uppercase py-0" :href="buildUrl('profile', profile)">Following</a>
|
|
<a v-else class="btn btn-outline-primary btn-sm font-weight-bold text-uppercase py-0" :href="buildUrl('profile', profile)">View</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
<div v-if="results.statuses.length" class="col-md-6 offset-3">
|
|
<a v-for="(status, index) in results.statuses" class="mr-2 result-card" :href="buildUrl('status', status)">
|
|
<img :src="status.thumb" width="90px" height="90px" class="mb-2" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div v-else-if="analysis == 'remotePost'" class="row">
|
|
<div class="col-12 mb-5">
|
|
<p class="h5 font-weight-bold text-dark">Showing results for <i>{{query}}</i></p>
|
|
<hr>
|
|
</div>
|
|
<div class="col-md-6 offset-md-3">
|
|
<div v-if="results.statuses.length">
|
|
<div v-for="(status, index) in results.statuses" class="card mb-4 shadow-none border">
|
|
<div class="card-header p-0 m-0">
|
|
<div style="width: 100%;height: 200px;background: #fff">
|
|
<div class="pt-4 text-center">
|
|
<img :src="status.thumb" class="img-fluid border" style="max-height: 140px;">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mt-n4 mb-2">
|
|
<div class="media">
|
|
|
|
<img class="rounded-circle p-1 mr-2 border mt-n3 bg-white shadow" src="/storage/avatars/default.png" width="70px" height="70px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png';">
|
|
<div class="media-body pt-3">
|
|
<p class="font-weight-bold mb-0">{{status.username}}</p>
|
|
</div>
|
|
<div class="float-right pt-3">
|
|
<p class="small mb-0 text-muted">{{status.timestamp}}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="text-center mb-3 lead" v-html="status.caption"></p>
|
|
<!-- <p class="text-center text-muted small text-uppercase mb-4">2 likes</p> -->
|
|
<!-- <div class="d-flex justify-content-center">
|
|
<a class="btn btn-primary btn-sm py-1 px-4 text-uppercase font-weight-bold" :href="status.url" style="font-weight: 500">View Post</a>
|
|
</div> -->
|
|
</div>
|
|
<div class="card-footer">
|
|
<a class="btn btn-primary btn-block font-weight-bold rounded-0" :href="status.url">View Post</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else>
|
|
<div class="border py-3 text-center font-weight-bold">No results found</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="col-12">
|
|
<p class="text-center text-muted lead font-weight-bold">No results found</p>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<style type="text/css" scoped>
|
|
.result-card {
|
|
text-decoration: none;
|
|
}
|
|
.result-card .media:hover {
|
|
background: #EDF2F7;
|
|
}
|
|
@media (min-width: 1200px) {
|
|
.container {
|
|
max-width: 995px;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script type="text/javascript">
|
|
export default {
|
|
props: ['query', 'profileId'],
|
|
|
|
data() {
|
|
return {
|
|
loading: true,
|
|
networkError: false,
|
|
results: {
|
|
hashtags: [],
|
|
profiles: [],
|
|
statuses: []
|
|
},
|
|
filters: {
|
|
hashtags: true,
|
|
profiles: true,
|
|
statuses: true
|
|
},
|
|
analysis: 'profile',
|
|
}
|
|
},
|
|
beforeMount() {
|
|
this.bootSearch();
|
|
},
|
|
mounted() {
|
|
$('.search-bar input').val(this.query);
|
|
},
|
|
methods: {
|
|
bootSearch() {
|
|
let lexer = this.searchLexer();
|
|
this.analysis = lexer;
|
|
this.fetchSearchResults();
|
|
},
|
|
|
|
fetchSearchResults() {
|
|
if(this.analysis == 'remote') {
|
|
let term = this.query;
|
|
let parsed = new URL(term);
|
|
if(parsed.host === window.location.host) {
|
|
window.location.href = term;
|
|
return;
|
|
}
|
|
}
|
|
this.searchContext(this.analysis);
|
|
},
|
|
|
|
followProfile(profile, index) {
|
|
this.loading = true;
|
|
axios.post('/i/follow', {
|
|
item: profile.entity.id
|
|
}).then(res => {
|
|
if(profile.entity.local == true) {
|
|
this.fetchSearchResults();
|
|
return;
|
|
} else {
|
|
this.loading = false;
|
|
this.results.profiles[index].entity.follow_request = true;
|
|
return;
|
|
}
|
|
}).catch(err => {
|
|
if(err.response.data.message) {
|
|
swal('Error', err.response.data.message, 'error');
|
|
}
|
|
});
|
|
},
|
|
|
|
searchLexer() {
|
|
let q = this.query;
|
|
|
|
if(q.startsWith('#')) {
|
|
return 'hashtag';
|
|
}
|
|
|
|
if((q.match(/@/g) || []).length == 2) {
|
|
return 'webfinger';
|
|
}
|
|
|
|
if(q.startsWith('@')) {
|
|
return 'profile';
|
|
}
|
|
|
|
if(q.startsWith('https://')) {
|
|
return 'remote';
|
|
}
|
|
|
|
return 'all';
|
|
},
|
|
|
|
buildUrl(type = 'hashtag', obj) {
|
|
switch(type) {
|
|
case 'hashtag':
|
|
return obj.url + '?src=search';
|
|
break;
|
|
|
|
case 'profile':
|
|
if(obj.entity.local == true) {
|
|
return obj.url;
|
|
}
|
|
return '/i/web/profile/_/' + obj.entity.id;
|
|
break;
|
|
|
|
default:
|
|
return obj.url + '?src=search';
|
|
break;
|
|
|
|
}
|
|
},
|
|
|
|
searchContext(type) {
|
|
switch(type) {
|
|
case 'all':
|
|
axios.get('/api/search', {
|
|
params: {
|
|
'q': this.query,
|
|
'src': 'metro',
|
|
'v': 1,
|
|
'scope': 'all'
|
|
}
|
|
}).then(res => {
|
|
let results = res.data;
|
|
this.results.hashtags = results.hashtags ? results.hashtags : [];
|
|
this.results.profiles = results.profiles ? results.profiles : [];
|
|
this.results.statuses = results.posts ? results.posts : [];
|
|
this.loading = false;
|
|
}).catch(err => {
|
|
this.loading = false;
|
|
console.log(err);
|
|
this.networkError = true;
|
|
});
|
|
break;
|
|
|
|
case 'remote':
|
|
axios.get('/api/search', {
|
|
params: {
|
|
'q': this.query,
|
|
'src': 'metro',
|
|
'v': 1,
|
|
'scope': 'remote'
|
|
}
|
|
}).then(res => {
|
|
let results = res.data;
|
|
this.results.hashtags = results.hashtags ? results.hashtags : [];
|
|
this.results.profiles = results.profiles ? results.profiles : [];
|
|
this.results.statuses = results.posts ? results.posts : [];
|
|
|
|
if(this.results.profiles.length) {
|
|
this.analysis = 'profile';
|
|
}
|
|
if(this.results.statuses.length) {
|
|
this.analysis = 'remotePost';
|
|
}
|
|
this.loading = false;
|
|
}).catch(err => {
|
|
this.loading = false;
|
|
console.log(err);
|
|
this.networkError = true;
|
|
});
|
|
break;
|
|
|
|
case 'hashtag':
|
|
axios.get('/api/search', {
|
|
params: {
|
|
'q': this.query.slice(1),
|
|
'src': 'metro',
|
|
'v': 1,
|
|
'scope': 'hashtag'
|
|
}
|
|
}).then(res => {
|
|
let results = res.data;
|
|
this.results.hashtags = results.hashtags ? results.hashtags : [];
|
|
this.results.profiles = results.profiles ? results.profiles : [];
|
|
this.results.statuses = results.posts ? results.posts : [];
|
|
this.loading = false;
|
|
}).catch(err => {
|
|
this.loading = false;
|
|
console.log(err);
|
|
this.networkError = true;
|
|
});
|
|
break;
|
|
|
|
case 'profile':
|
|
axios.get('/api/search', {
|
|
params: {
|
|
'q': this.query,
|
|
'src': 'metro',
|
|
'v': 1,
|
|
'scope': 'profile'
|
|
}
|
|
}).then(res => {
|
|
let results = res.data;
|
|
this.results.hashtags = results.hashtags ? results.hashtags : [];
|
|
this.results.profiles = results.profiles ? results.profiles : [];
|
|
this.results.statuses = results.posts ? results.posts : [];
|
|
this.loading = false;
|
|
}).catch(err => {
|
|
this.loading = false;
|
|
console.log(err);
|
|
this.networkError = true;
|
|
});
|
|
break;
|
|
|
|
case 'webfinger':
|
|
axios.get('/api/search', {
|
|
params: {
|
|
'q': this.query,
|
|
'src': 'metro',
|
|
'v': 1,
|
|
'scope': 'webfinger'
|
|
}
|
|
}).then(res => {
|
|
let results = res.data;
|
|
this.results.hashtags = [];
|
|
this.results.profiles = results.profiles;
|
|
this.results.statuses = [];
|
|
this.loading = false;
|
|
}).catch(err => {
|
|
this.loading = false;
|
|
console.log(err);
|
|
this.networkError = true;
|
|
});
|
|
break;
|
|
|
|
default:
|
|
this.loading = false;
|
|
this.networkError = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
</script>
|