mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-25 15:55:22 +00:00
Add Announcements/Newsroom feature
This commit is contained in:
parent
279c57d9a5
commit
30c1af7c78
9 changed files with 408 additions and 0 deletions
92
app/Http/Controllers/NewsroomController.php
Normal file
92
app/Http/Controllers/NewsroomController.php
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Auth;
|
||||||
|
use App\Newsroom;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
|
||||||
|
class NewsroomController extends Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
if(Auth::check()) {
|
||||||
|
$posts = Newsroom::whereNotNull('published_at')->latest()->paginate(9);
|
||||||
|
} else {
|
||||||
|
$posts = Newsroom::whereNotNull('published_at')
|
||||||
|
->whereAuthOnly(false)
|
||||||
|
->latest()
|
||||||
|
->paginate(3);
|
||||||
|
}
|
||||||
|
return view('site.news.home', compact('posts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Request $request, $year, $month, $slug)
|
||||||
|
{
|
||||||
|
$post = Newsroom::whereNotNull('published_at')
|
||||||
|
->whereSlug($slug)
|
||||||
|
->whereYear('published_at', $year)
|
||||||
|
->whereMonth('published_at', $month)
|
||||||
|
->firstOrFail();
|
||||||
|
abort_if($post->auth_only && !$request->user(), 404);
|
||||||
|
return view('site.news.post.show', compact('post'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function search(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'q' => 'nullable'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function archive(Request $request)
|
||||||
|
{
|
||||||
|
return view('site.news.archive.index');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function timelineApi(Request $request)
|
||||||
|
{
|
||||||
|
abort_if(!Auth::check(), 404);
|
||||||
|
|
||||||
|
$key = 'newsroom:read:profileid:' . $request->user()->profile_id;
|
||||||
|
$read = Redis::smembers($key);
|
||||||
|
|
||||||
|
$posts = Newsroom::whereNotNull('published_at')
|
||||||
|
->whereShowTimeline(true)
|
||||||
|
->whereNotIn('id', $read)
|
||||||
|
->orderBy('id', 'desc')
|
||||||
|
->take(9)
|
||||||
|
->get()
|
||||||
|
->map(function($post) {
|
||||||
|
return [
|
||||||
|
'id' => $post->id,
|
||||||
|
'title' => Str::limit($post->title, 25),
|
||||||
|
'summary' => $post->summary,
|
||||||
|
'url' => $post->show_link ? $post->permalink() : null,
|
||||||
|
'published_at' => $post->published_at->format('F m, Y')
|
||||||
|
];
|
||||||
|
});
|
||||||
|
return response()->json($posts, 200, [], JSON_PRETTY_PRINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markAsRead(Request $request)
|
||||||
|
{
|
||||||
|
abort_if(!Auth::check(), 404);
|
||||||
|
|
||||||
|
$this->validate($request, [
|
||||||
|
'id' => 'required|integer|min:1'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$news = Newsroom::whereNotNull('published_at')
|
||||||
|
->findOrFail($request->input('id'));
|
||||||
|
|
||||||
|
$key = 'newsroom:read:profileid:' . $request->user()->profile_id;
|
||||||
|
|
||||||
|
Redis::sadd($key, $news->id);
|
||||||
|
|
||||||
|
return response()->json(['code' => 200]);
|
||||||
|
}
|
||||||
|
}
|
22
app/Newsroom.php
Normal file
22
app/Newsroom.php
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Newsroom extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'newsroom';
|
||||||
|
protected $fillable = ['title'];
|
||||||
|
|
||||||
|
protected $dates = ['published_at'];
|
||||||
|
|
||||||
|
public function permalink()
|
||||||
|
{
|
||||||
|
$year = $this->published_at->year;
|
||||||
|
$month = $this->published_at->format('m');
|
||||||
|
$slug = $this->slug;
|
||||||
|
|
||||||
|
return url("/site/newsroom/{$year}/{$month}/{$slug}");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateNewsroomTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('newsroom', function (Blueprint $table) {
|
||||||
|
$table->bigIncrements('id');
|
||||||
|
$table->bigInteger('user_id')->unsigned()->nullable();
|
||||||
|
$table->string('header_photo_url')->nullable();
|
||||||
|
$table->string('title')->nullable();
|
||||||
|
$table->string('slug')->nullable()->unique()->index();
|
||||||
|
$table->string('category')->default('update');
|
||||||
|
$table->text('summary')->nullable();
|
||||||
|
$table->text('body')->nullable();
|
||||||
|
$table->text('body_rendered')->nullable();
|
||||||
|
$table->string('link')->nullable();
|
||||||
|
$table->boolean('force_modal')->default(false);
|
||||||
|
$table->boolean('show_timeline')->default(false);
|
||||||
|
$table->boolean('show_link')->default(false);
|
||||||
|
$table->boolean('auth_only')->default(true);
|
||||||
|
$table->timestamp('published_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('site_news');
|
||||||
|
}
|
||||||
|
}
|
155
resources/assets/js/components/AnnouncementsCard.vue
Normal file
155
resources/assets/js/components/AnnouncementsCard.vue
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<transition name="fade">
|
||||||
|
<div v-if="announcements.length" class="card border shadow-none mb-3" style="max-width: 18rem;">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-title mb-0">
|
||||||
|
<span class="font-weight-bold">{{announcement.title}}</span>
|
||||||
|
<span class="float-right cursor-pointer" title="Close" @click="close"><i class="fas fa-times text-lighter"></i></span>
|
||||||
|
</div>
|
||||||
|
<p class="card-text">
|
||||||
|
<span style="font-size:13px;">{{announcement.summary}}</span>
|
||||||
|
</p>
|
||||||
|
<p class="d-flex align-items-center justify-content-between mb-0">
|
||||||
|
<a v-if="announcement.url" :href="announcement.url" class="small font-weight-bold mb-0">Read more</a>
|
||||||
|
<span v-else></span>
|
||||||
|
<span>
|
||||||
|
<span :class="[showPrev ? 'btn btn-outline-secondary btn-sm py-0':'btn btn-outline-secondary btn-sm py-0 disabled']" :disabled="showPrev == false" @click="loadPrev()">
|
||||||
|
<i class="fas fa-chevron-left fa-sm"></i>
|
||||||
|
</span>
|
||||||
|
<span class="btn btn-outline-success btn-sm py-0 mx-1" title="Mark as Read" data-toggle="tooltip" data-placement="bottom" @click="markAsRead()">
|
||||||
|
<i class="fas fa-check fa-sm"></i>
|
||||||
|
</span>
|
||||||
|
<span :class="[showNext ? 'btn btn-outline-secondary btn-sm py-0':'btn btn-outline-secondary btn-sm py-0 disabled']" :disabled="showNext == false" @click="loadNext()">
|
||||||
|
<i class="fas fa-chevron-right fa-sm"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style type="text/css" scoped>
|
||||||
|
.fade-enter-active, .fade-leave-active {
|
||||||
|
transition: opacity .5s;
|
||||||
|
}
|
||||||
|
.fade-enter, .fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
announcements: [],
|
||||||
|
announcement: {},
|
||||||
|
cursor: 0,
|
||||||
|
showNext: true,
|
||||||
|
showPrev: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.fetchAnnouncements();
|
||||||
|
},
|
||||||
|
|
||||||
|
updated() {
|
||||||
|
$('[data-toggle="tooltip"]').tooltip()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
fetchAnnouncements() {
|
||||||
|
let self = this;
|
||||||
|
let key = 'metro-tips-closed';
|
||||||
|
let cached = JSON.parse(window.localStorage.getItem(key));
|
||||||
|
axios.get('/api/v1/pixelfed/newsroom/timeline')
|
||||||
|
.then(res => {
|
||||||
|
self.announcements = res.data.filter(p => {
|
||||||
|
if(cached) {
|
||||||
|
return cached.indexOf(p.id) == -1;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.announcement = self.announcements[0]
|
||||||
|
if(self.announcements.length == 1) {
|
||||||
|
self.showNext = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
loadNext() {
|
||||||
|
if(!this.showNext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.cursor += 1;
|
||||||
|
this.announcement = this.announcements[this.cursor];
|
||||||
|
if((this.cursor + 1) == this.announcements.length) {
|
||||||
|
this.showNext = false;
|
||||||
|
}
|
||||||
|
if(this.cursor >= 1) {
|
||||||
|
this.showPrev = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loadPrev() {
|
||||||
|
if(!this.showPrev) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.cursor -= 1;
|
||||||
|
this.announcement = this.announcements[this.cursor];
|
||||||
|
if(this.cursor == 0) {
|
||||||
|
this.showPrev = false;
|
||||||
|
}
|
||||||
|
if(this.cursor < this.announcements.length) {
|
||||||
|
this.showNext = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeNewsroomPost(id, index) {
|
||||||
|
let key = 'metro-tips-closed';
|
||||||
|
let ctx = [];
|
||||||
|
let cached = window.localStorage.getItem(key);
|
||||||
|
if(cached) {
|
||||||
|
ctx = JSON.parse(cached);
|
||||||
|
}
|
||||||
|
ctx.push(id);
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(ctx));
|
||||||
|
this.newsroomPosts = this.newsroomPosts.filter(res => {
|
||||||
|
return res.id !== id
|
||||||
|
});
|
||||||
|
if(this.newsroomPosts.length == 0) {
|
||||||
|
this.showTips = false;
|
||||||
|
} else {
|
||||||
|
this.newsroomPost = [ this.newsroomPosts[0] ];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
window.localStorage.setItem('metro-tips', false);
|
||||||
|
this.$emit('show-tips', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
markAsRead() {
|
||||||
|
let vm = this;
|
||||||
|
axios.post('/api/pixelfed/v1/newsroom/markasread', {
|
||||||
|
id: this.announcement.id
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
let cur = vm.cursor;
|
||||||
|
vm.announcements.splice(cur, 1);
|
||||||
|
vm.announcement = vm.announcements[0];
|
||||||
|
vm.cursor = 0;
|
||||||
|
vm.showPrev = false;
|
||||||
|
vm.showNext = vm.announcements.length > 1;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
swal('Oops, Something went wrong', 'There was a problem with your request, please try again later.', 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
7
resources/views/site/news/archive/index.blade.php
Normal file
7
resources/views/site/news/archive/index.blade.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
@extends('site.news.partial.layout')
|
||||||
|
|
||||||
|
@section('body')
|
||||||
|
<div class="container">
|
||||||
|
<p class="text-center">Archive here</p>
|
||||||
|
</div>
|
||||||
|
@endsection
|
26
resources/views/site/news/home.blade.php
Normal file
26
resources/views/site/news/home.blade.php
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
@extends('site.news.partial.layout')
|
||||||
|
|
||||||
|
@section('body')
|
||||||
|
<div class="container">
|
||||||
|
<div class="row px-3">
|
||||||
|
@foreach($posts->slice(0,1) as $post)
|
||||||
|
<div class="col-12 bg-light d-flex justify-content-center align-items-center mt-2 mb-4" style="height:300px;">
|
||||||
|
<div class="mx-5">
|
||||||
|
<p class="small text-danger mb-0 text-uppercase">{{$post->category}}</p>
|
||||||
|
<p class="small text-muted">{{$post->published_at->format('F d, Y')}}</p>
|
||||||
|
<p class="h1" style="font-size: 2.6rem;font-weight: 700;"><a class="text-dark text-decoration-none" href="{{$post->permalink()}}">{{$post->title}}</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
@foreach($posts->slice(1) as $post)
|
||||||
|
<div class="col-6 bg-light d-flex justify-content-center align-items-center mt-3 px-5" style="height:300px;">
|
||||||
|
<div class="mx-0">
|
||||||
|
<p class="small text-danger mb-0 text-uppercase">{{$post->category}}</p>
|
||||||
|
<p class="small text-muted">{{$post->published_at->format('F d, Y')}}</p>
|
||||||
|
<p class="h1" style="font-size: 2rem;font-weight: 700;"><a class="text-dark text-decoration-none" href="{{$post->permalink()}}">{{$post->title}}</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
17
resources/views/site/news/partial/layout.blade.php
Normal file
17
resources/views/site/news/partial/layout.blade.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
@extends('layouts.anon')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
@include('site.news.partial.nav')
|
||||||
|
@yield('body');
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('styles')
|
||||||
|
<style type="text/css">
|
||||||
|
html, body {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.navbar-laravel {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@endpush
|
11
resources/views/site/news/partial/nav.blade.php
Normal file
11
resources/views/site/news/partial/nav.blade.php
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<div class="container py-4">
|
||||||
|
<div class="col-12 d-flex justify-content-between border-bottom pb-1 px-0">
|
||||||
|
<div>
|
||||||
|
<p class="h4"><a href="/site/newsroom" class="text-dark text-decoration-none">Newsroom</a></p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/site/newsroom/search" class="small text-muted mr-4 text-decoration-none">Search Newsroom</a>
|
||||||
|
<a href="/site/newsroom/archive" class="small text-muted text-decoration-none">Archive</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
33
resources/views/site/news/post/show.blade.php
Normal file
33
resources/views/site/news/post/show.blade.php
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
@extends('site.news.partial.layout')
|
||||||
|
|
||||||
|
@section('body')
|
||||||
|
<div class="container mt-3">
|
||||||
|
<div class="row px-3">
|
||||||
|
<div class="col-12 bg-light d-flex justify-content-center align-items-center" style="min-height: 400px">
|
||||||
|
<div style="max-width: 550px;">
|
||||||
|
<p class="small text-danger mb-0 text-uppercase">{{$post->category}}</p>
|
||||||
|
<p class="small text-muted">{{$post->published_at->format('F d, Y')}}</p>
|
||||||
|
<p class="h1" style="font-size: 2.6rem;font-weight: 700;">{{$post->title}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 mt-4">
|
||||||
|
<div class="d-flex justify-content-center">
|
||||||
|
<p class="lead text-center py-5" style="font-size:25px; font-weight: 200; max-width: 550px;">
|
||||||
|
{{$post->summary}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if($post->body)
|
||||||
|
<div class="col-12 mt-4">
|
||||||
|
<div class="d-flex justify-content-center border-top">
|
||||||
|
<p class="lead py-5" style="max-width: 550px;">
|
||||||
|
{!!$post->body!!}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="col-12 mt-4"></div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
Loading…
Reference in a new issue