浏览代码

Add Announcements/Newsroom feature

Daniel Supernault 5 年之前
父节点
当前提交
30c1af7c78

+ 92 - 0
app/Http/Controllers/NewsroomController.php

@@ -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 - 0
app/Newsroom.php

@@ -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}");
+    }
+}

+ 45 - 0
database/migrations/2019_12_10_023604_create_newsroom_table.php

@@ -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 - 0
resources/assets/js/components/AnnouncementsCard.vue

@@ -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 - 0
resources/views/site/news/archive/index.blade.php

@@ -0,0 +1,7 @@
+@extends('site.news.partial.layout')
+
+@section('body')
+<div class="container">
+	<p class="text-center">Archive here</p>
+</div>
+@endsection

+ 26 - 0
resources/views/site/news/home.blade.php

@@ -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 - 0
resources/views/site/news/partial/layout.blade.php

@@ -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 - 0
resources/views/site/news/partial/nav.blade.php

@@ -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 - 0
resources/views/site/news/post/show.blade.php

@@ -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