mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-25 15:55:22 +00:00
Add Year in Review feature (mysql only)
This commit is contained in:
parent
b00e2b0868
commit
f32072a396
4 changed files with 469 additions and 9 deletions
|
@ -4,17 +4,235 @@ namespace App\Http\Controllers;
|
|||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth;
|
||||
use App\AccountLog;
|
||||
use App\Follower;
|
||||
use App\Like;
|
||||
use App\Status;
|
||||
use App\StatusHashtag;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class SeasonalController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function yearInReview()
|
||||
{
|
||||
$profile = Auth::user()->profile;
|
||||
return view('account.yir', compact('profile'));
|
||||
}
|
||||
public function yearInReview()
|
||||
{
|
||||
abort_if(now()->gt('2021-03-01 00:00:00'), 404);
|
||||
abort_if(config('database.default') != 'mysql', 404);
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
return view('account.yir', compact('profile'));
|
||||
}
|
||||
|
||||
public function getData(Request $request)
|
||||
{
|
||||
abort_if(now()->gt('2021-03-01 00:00:00'), 404);
|
||||
abort_if(config('database.default') != 'mysql', 404);
|
||||
|
||||
$uid = $request->user()->id;
|
||||
$pid = $request->user()->profile_id;
|
||||
$epoch = '2020-01-01 00:00:00';
|
||||
$epochStart = '2020-01-01 00:00:00';
|
||||
$epochEnd = '2020-12-31 23:59:59';
|
||||
|
||||
$siteKey = 'seasonal:my2020:shared';
|
||||
$siteTtl = now()->addMonths(3);
|
||||
$userKey = 'seasonal:my2020:user:' . $uid;
|
||||
$userTtl = now()->addMonths(3);
|
||||
|
||||
$shared = Cache::remember($siteKey, $siteTtl, function() use($epochStart, $epochEnd) {
|
||||
return [
|
||||
'average' => [
|
||||
'posts' => round(Status::selectRaw('*, count(profile_id) as count')
|
||||
->whereNull('uri')
|
||||
->whereIn('type', ['photo','photo:album','video','video:album','photo:video:album'])
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->groupBy('profile_id')
|
||||
->pluck('count')
|
||||
->avg()),
|
||||
|
||||
'likes' => round(Like::selectRaw('*, count(profile_id) as count')
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->groupBy('profile_id')
|
||||
->pluck('count')
|
||||
->avg()),
|
||||
],
|
||||
|
||||
'popular' => [
|
||||
|
||||
'hashtag' => StatusHashtag::selectRaw('*,count(hashtag_id) as count')
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->groupBy('hashtag_id')
|
||||
->orderByDesc('count')
|
||||
->take(1)
|
||||
->get()
|
||||
->map(function($sh) {
|
||||
return [
|
||||
'name' => $sh->hashtag->name,
|
||||
'count' => $sh->count
|
||||
];
|
||||
})
|
||||
->first(),
|
||||
|
||||
'post' => Status::whereScope('public')
|
||||
->where('likes_count', '>', 1)
|
||||
->whereIsNsfw(false)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->orderByDesc('likes_count')
|
||||
->take(1)
|
||||
->get()
|
||||
->map(function($status) {
|
||||
return [
|
||||
'id' => (string) $status->id,
|
||||
'username' => (string) $status->profile->username,
|
||||
'created_at' => $status->created_at->format('M d, Y'),
|
||||
'type' => $status->type,
|
||||
'url' => $status->url(),
|
||||
'thumb' => $status->thumb(),
|
||||
'likes_count' => $status->likes_count,
|
||||
'reblogs_count' => $status->reblogs_count,
|
||||
'reply_count' => $status->reply_count ?? 0,
|
||||
];
|
||||
})
|
||||
->first(),
|
||||
|
||||
'places' => Status::selectRaw('*, count(place_id) as count')
|
||||
->whereNotNull('place_id')
|
||||
->having('count', '>', 1)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->groupBy('place_id')
|
||||
->orderByDesc('count')
|
||||
->take(1)
|
||||
->get()
|
||||
->map(function($sh) {
|
||||
return [
|
||||
'name' => $sh->place->getName(),
|
||||
'url' => $sh->place->url(),
|
||||
'count' => $sh->count
|
||||
];
|
||||
})
|
||||
->first()
|
||||
],
|
||||
|
||||
];
|
||||
});
|
||||
|
||||
$res = Cache::remember($userKey, $userTtl, function() use($uid, $pid, $epochStart, $epochEnd, $request) {
|
||||
return [
|
||||
'account' => [
|
||||
'user_id' => $request->user()->id,
|
||||
'created_at' => $request->user()->created_at->format('M d, Y'),
|
||||
'created_this_year' => $request->user()->created_at->gt('2020-01-01 00:00:00'),
|
||||
'created_months_ago' => $request->user()->created_at->diffInMonths(now()),
|
||||
'followers_this_year' => Follower::whereFollowingId($pid)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->count(),
|
||||
'followed_this_year' => Follower::whereProfileId($pid)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->count(),
|
||||
'most_popular' => Status::whereProfileId($pid)
|
||||
->where('likes_count', '>', 1)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->orderByDesc('likes_count')
|
||||
->take(1)
|
||||
->get()
|
||||
->map(function($status) {
|
||||
return [
|
||||
'id' => (string) $status->id,
|
||||
'username' => (string) $status->profile->username,
|
||||
'created_at' => $status->created_at->format('M d, Y'),
|
||||
'type' => $status->type,
|
||||
'url' => $status->url(),
|
||||
'thumb' => $status->thumb(),
|
||||
'likes_count' => $status->likes_count,
|
||||
'reblogs_count' => $status->reblogs_count,
|
||||
'reply_count' => $status->reply_count ?? 0,
|
||||
];
|
||||
})
|
||||
->first(),
|
||||
'posts_count' => Status::whereProfileId($pid)
|
||||
->whereIn('type', ['photo','photo:album','video','video:album','photo:video:album'])
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->count(),
|
||||
'likes_count' => Like::whereProfileId($pid)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->count(),
|
||||
'hashtag' => StatusHashtag::selectRaw('*, count(hashtag_id) as count')
|
||||
->whereProfileId($pid)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->groupBy('profile_id')
|
||||
->orderByDesc('count')
|
||||
->take(1)
|
||||
->get()
|
||||
->map(function($sh) {
|
||||
return [
|
||||
'name' => $sh->hashtag->name,
|
||||
'count' => $sh->count
|
||||
];
|
||||
})
|
||||
->first(),
|
||||
'places' => Status::selectRaw('*, count(place_id) as count')
|
||||
->whereNotNull('place_id')
|
||||
->having('count', '>', 1)
|
||||
->whereProfileId($pid)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->groupBy('place_id')
|
||||
->orderByDesc('count')
|
||||
->take(1)
|
||||
->get()
|
||||
->map(function($sh) {
|
||||
return [
|
||||
'name' => $sh->place->getName(),
|
||||
'url' => $sh->place->url(),
|
||||
'count' => $sh->count
|
||||
];
|
||||
})
|
||||
->first(),
|
||||
'places_total' => Status::whereProfileId($pid)
|
||||
->where('created_at', '>', $epochStart)
|
||||
->where('created_at', '<', $epochEnd)
|
||||
->whereNotNull('place_id')
|
||||
->count()
|
||||
]
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json(array_merge($res, $shared));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
abort_if(now()->gt('2021-03-01 00:00:00'), 404);
|
||||
abort_if(config('database.default') != 'mysql', 404);
|
||||
$this->validate($request, [
|
||||
'profile_id' => 'required',
|
||||
'type' => 'required|string|in:view,hide'
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
$log = new AccountLog();
|
||||
$log->user_id = $user->id;
|
||||
$log->item_type = 'App\User';
|
||||
$log->item_id = $user->id;
|
||||
$log->action = $request->input('type') == 'view' ? 'seasonal.my2020.view' : 'seasonal.my2020.hide';
|
||||
$log->ip_address = $request->ip();
|
||||
$log->user_agent = $request->user_agent();
|
||||
$log->save();
|
||||
}
|
||||
}
|
||||
|
|
228
resources/assets/js/components/My2020.vue
Normal file
228
resources/assets/js/components/My2020.vue
Normal file
|
@ -0,0 +1,228 @@
|
|||
<template>
|
||||
<div class="bg-dark text-white">
|
||||
<div v-if="!loaded" style="height: 100vh;" class="d-flex justify-content-center align-items-center">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<p class="mb-0 lead mt-2">Loading</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loaded && notEnoughData" style="height: 100vh;" class="d-flex justify-content-center align-items-center">
|
||||
<div class="text-center">
|
||||
<p class="display-4">Oops!</p>
|
||||
<p class="h3 font-weight-light py-3">We don't have enough data to display your <span class="font-weight-bold">#my2020</span>.</p>
|
||||
<p class="mb-0 h5 font-weight-light">We hope to see you next year!</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="loaded && !notEnoughData" class="d-flex justify-content-center align-items-center" style="width:100%;height:100vh;min-height:500px; padding: 0 15px;">
|
||||
|
||||
<div v-if="page == 1" class="text-center">
|
||||
<p class="h1 font-weight-light">Hello {{user.username}}!</p>
|
||||
<p class="h1 py-4">Your 2020 on Pixelfed.</p>
|
||||
<p class="h4 font-weight-light mb-0 animate__animated animate__bounceInDown">Use the buttons below to navigate.</p>
|
||||
</div>
|
||||
|
||||
<div v-if="page == 2" class="text-center mw-500">
|
||||
<p class="display-4">User #<span class="font-weight-bold">{{stats.account.user_id}}</span></p>
|
||||
<p class="h3 font-weight-light mb-0">You joined Pixelfed on {{stats.account.created_at}}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="page == 3" class="text-center mw-500">
|
||||
<p class="display-4">You created <span class="font-weight-bold">{{stats.account.posts_count}}</span> posts</p>
|
||||
<p class="h3 font-weight-light mb-0">The average user created <span class="font-weight-bold">{{stats.average.posts}}</span> posts this year.</p>
|
||||
</div>
|
||||
|
||||
<div v-if="page == 4" class="text-center mw-500">
|
||||
<p class="display-4">You liked <span class="font-weight-bold">{{stats.account.likes_count}}</span> posts</p>
|
||||
<p class="h3 font-weight-light mb-0">The average user liked <span class="font-weight-bold">{{stats.average.likes}}</span> posts this year.</p>
|
||||
</div>
|
||||
|
||||
<div v-if="page == 5" class="text-center mw-500">
|
||||
<div v-if="stats.account.most_popular">
|
||||
<p class="h1 font-weight-light mb-0 text-break md-line-height">Your most popular post of 2020 was created on <span class="font-weight-bold">{{stats.account.most_popular.created_at}}</span> with <span class="font-weight-bold">{{stats.account.most_popular.likes_count}}</span> likes.</p>
|
||||
<p class="mt-4 mb-0">
|
||||
<a class="btn btn-outline-light btn-lg btn-block rounded-pill" :href="stats.account.most_popular.url">View Post</a>
|
||||
</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="h1 font-weight-light mb-0 text-break md-line-height">The most popular post of 2020 was created by <span class="font-weight-bold">{{stats.popular.post.username}}</span> on <span class="font-weight-bold">{{stats.popular.post.created_at}}</span> with <span class="font-weight-bold">{{stats.popular.post.likes_count}}</span> likes.</p>
|
||||
<p class="mt-4 mb-0">
|
||||
<a class="btn btn-outline-light btn-lg btn-block rounded-pill" :href="stats.popular.post.url">View Post</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="page == 6" class="text-center mw-500">
|
||||
<p class="display-4"><span class="font-weight-bold">{{stats.account.followers_this_year}}</span> New Followers</p>
|
||||
<p class="h3 font-weight-light mb-0">You followed <span class="font-weight-bold">{{stats.account.followed_this_year}}</span> accounts this year!</p>
|
||||
</div>
|
||||
|
||||
<div v-if="page == 7" class="text-center mw-500">
|
||||
<div v-if="stats.account.hashtag">
|
||||
<p class="h1 text-break">Your favourite hashtag was <span class="font-weight-bold">#{{stats.account.hashtag.name}}</span>.</p>
|
||||
<p class="h3 font-weight-light mb-0">You used it <span class="font-weight-bold">{{stats.account.hashtag.count}}</span> times!</p>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p class="h1 text-break">The most popular hashtag was <span class="font-weight-bold">#{{stats.popular.hashtag.name}}</span></p>
|
||||
<p class="h3 font-weight-light mb-0">It was used <span class="font-weight-bold">{{stats.popular.hashtag.count}}</span> times!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="page == 8" class="text-center mw-500">
|
||||
<p class="display-4">You tagged <span class="font-weight-bold">{{stats.account.places_total}}</span> places.</p>
|
||||
<p v-if="stats.account.places_total" class="h3 font-weight-light mb-0">You tagged <span class="font-weight-bold">{{stats.account.places.name}}</span> a total of <span class="font-weight-bold">{{stats.account.places.count}}</span> times!</p>
|
||||
<p v-else class="h3 font-weight-light mb-0">The most tagged place was <span class="font-weight-bold">{{stats.popular.places.name}}</span> that was tagged a total of <span class="font-weight-bold">{{stats.popular.places.count}}</span> times!</p>
|
||||
</div>
|
||||
|
||||
<div v-if="page == 9" class="text-center">
|
||||
<p class="display-4">Happy 2021!</p>
|
||||
<p class="h3 font-weight-light mb-0">We wish you the best in the new year.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div v-if="loaded" class="fixed-top">
|
||||
<p class="text-center mt-3 d-flex justify-content-center align-items-center mb-0">
|
||||
<img src="/img/pixelfed-icon-grey.svg" width="60" height="60">
|
||||
<span class="text-light font-weight-bold ml-3" style="font-size: 22px;">#my2020</span>
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="loaded" class="fixed-bottom">
|
||||
<p class="text-center">
|
||||
<a v-if="!notEnoughData" :class="prevClass()" href="#" @click.prevent="prevPage()" :disabled="page == 1"><i class="fas fa-chevron-left"></i> Back</a>
|
||||
<a class="btn btn-outline-light rounded-pill mx-3" href="/">Back to Pixelfed</a>
|
||||
<a v-if="!notEnoughData" :class="nextClass()" href="#" @click.prevent="nextPage()">Next <i class="fas fa-chevron-right"></i></a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style type="text/css" scoped>
|
||||
.md-line-height {
|
||||
line-height: 1.65 !important;
|
||||
}
|
||||
.mw-500 {
|
||||
max-width: 500px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
config: window.App.config,
|
||||
user: {},
|
||||
loggedIn: false,
|
||||
loaded: false,
|
||||
page: 1,
|
||||
stats: [],
|
||||
notEnoughData: false,
|
||||
reportedView: false
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
let u = new URLSearchParams(window.location.search);
|
||||
if( u.has('v') &&
|
||||
u.has('ned') &&
|
||||
u.has('sl') &&
|
||||
u.get('v') == 20 &&
|
||||
u.get('sl') >= 1 &&
|
||||
u.get('sl') <= 9
|
||||
) {
|
||||
if(u.get('ned') == 0) {
|
||||
this.page = u.get('sl');
|
||||
} else {
|
||||
this.notEnoughData = true;
|
||||
}
|
||||
}
|
||||
|
||||
axios.get('/api/pixelfed/v1/accounts/verify_credentials')
|
||||
.then(res => {
|
||||
this.user = res.data;
|
||||
window._sharedData.curUser = res.data;
|
||||
});
|
||||
|
||||
this.fetchData();
|
||||
},
|
||||
|
||||
updated() {
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchData() {
|
||||
axios.get('/api/pixelfed/v2/seasonal/yir')
|
||||
.then(res => {
|
||||
this.stats = res.data;
|
||||
this.loaded = true;
|
||||
this.shortcuts();
|
||||
})
|
||||
},
|
||||
|
||||
nextPage() {
|
||||
if(this.page == 9) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.page == 8) {
|
||||
axios.post('/api/pixelfed/v2/seasonal/yir', {
|
||||
'profile_id' : this.user.profile_id
|
||||
})
|
||||
}
|
||||
++this.page;
|
||||
window.history.pushState({}, {}, '/i/my2020?v=20&ned=0&sl=' + this.page);
|
||||
},
|
||||
|
||||
prevPage() {
|
||||
if(this.page == 1) {
|
||||
return;
|
||||
}
|
||||
--this.page;
|
||||
if(this.page == 1) {
|
||||
window.history.pushState({}, {}, '/i/my2020');
|
||||
} else {
|
||||
window.history.pushState({}, {}, '/i/my2020?v=20&ned=0&sl=' + this.page);
|
||||
}
|
||||
},
|
||||
|
||||
prevClass() {
|
||||
return this.page == 1
|
||||
? 'btn btn-outline-muted rounded-pill'
|
||||
: 'btn btn-outline-light rounded-pill';
|
||||
},
|
||||
|
||||
nextClass() {
|
||||
return this.page == 9
|
||||
? 'btn btn-outline-muted rounded-pill'
|
||||
: 'btn btn-outline-light rounded-pill';
|
||||
},
|
||||
|
||||
dateFormat(d) {
|
||||
},
|
||||
|
||||
shortcuts() {
|
||||
let self = this;
|
||||
window.addEventListener("keydown", function(event) {
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch(event.code) {
|
||||
case "KeyA":
|
||||
case "ArrowLeft":
|
||||
self.prevPage();
|
||||
break;
|
||||
case "KeyD":
|
||||
case "ArrowRight":
|
||||
self.nextPage();
|
||||
break;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
4
resources/assets/js/my2020.js
vendored
Normal file
4
resources/assets/js/my2020.js
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
Vue.component(
|
||||
'my-yearreview',
|
||||
require('./components/My2020.vue').default
|
||||
);
|
10
resources/views/account/yir.blade.php
Normal file
10
resources/views/account/yir.blade.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
@extends('layouts.blank')
|
||||
|
||||
@section('content')
|
||||
<my-yearreview></my-yearreview>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript" src="{{mix('js/my2020.js')}}"></script>
|
||||
<script type="text/javascript">App.boot();</script>
|
||||
@endpush
|
Loading…
Reference in a new issue