Merge pull request #1903 from pixelfed/staging

Add Announcements/Newsroom feature
This commit is contained in:
daniel 2019-12-23 23:56:15 -07:00 committed by GitHub
commit d4d85634fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 938 additions and 62 deletions

View file

@ -9,6 +9,7 @@ use App\{
Instance, Instance,
Media, Media,
Like, Like,
Newsroom,
OauthClient, OauthClient,
Profile, Profile,
Report, Report,
@ -258,4 +259,153 @@ class AdminController extends Controller
$message->save(); $message->save();
return; return;
} }
public function newsroomHome(Request $request)
{
$newsroom = Newsroom::latest()->paginate(10);
return view('admin.newsroom.home', compact('newsroom'));
}
public function newsroomCreate(Request $request)
{
return view('admin.newsroom.create');
}
public function newsroomEdit(Request $request, $id)
{
$news = Newsroom::findOrFail($id);
return view('admin.newsroom.edit', compact('news'));
}
public function newsroomDelete(Request $request, $id)
{
$news = Newsroom::findOrFail($id);
$news->delete();
return redirect('/i/admin/newsroom');
}
public function newsroomUpdate(Request $request, $id)
{
$this->validate($request, [
'title' => 'required|string|min:1|max:100',
'summary' => 'nullable|string|max:200',
'body' => 'nullable|string'
]);
$changed = false;
$changedFields = [];
$news = Newsroom::findOrFail($id);
$fields = [
'title' => 'string',
'summary' => 'string',
'body' => 'string',
'category' => 'string',
'show_timeline' => 'boolean',
'auth_only' => 'boolean',
'show_link' => 'boolean',
'force_modal' => 'boolean',
'published' => 'published'
];
foreach($fields as $field => $type) {
switch ($type) {
case 'string':
if($request->{$field} != $news->{$field}) {
if($field == 'title') {
$news->slug = str_slug($request->{$field});
}
$news->{$field} = $request->{$field};
$changed = true;
array_push($changedFields, $field);
}
break;
case 'boolean':
$state = $request->{$field} == 'on' ? true : false;
if($state != $news->{$field}) {
$news->{$field} = $state;
$changed = true;
array_push($changedFields, $field);
}
break;
case 'published':
$state = $request->{$field} == 'on' ? true : false;
$published = $news->published_at != null;
if($state != $published) {
$news->published_at = $state ? now() : null;
$changed = true;
array_push($changedFields, $field);
}
break;
}
}
if($changed) {
$news->save();
}
$redirect = $news->published_at ? $news->permalink() : $news->editUrl();
return redirect($redirect);
}
public function newsroomStore(Request $request)
{
$this->validate($request, [
'title' => 'required|string|min:1|max:100',
'summary' => 'nullable|string|max:200',
'body' => 'nullable|string'
]);
$changed = false;
$changedFields = [];
$news = new Newsroom();
$fields = [
'title' => 'string',
'summary' => 'string',
'body' => 'string',
'category' => 'string',
'show_timeline' => 'boolean',
'auth_only' => 'boolean',
'show_link' => 'boolean',
'force_modal' => 'boolean',
'published' => 'published'
];
foreach($fields as $field => $type) {
switch ($type) {
case 'string':
if($request->{$field} != $news->{$field}) {
if($field == 'title') {
$news->slug = str_slug($request->{$field});
}
$news->{$field} = $request->{$field};
$changed = true;
array_push($changedFields, $field);
}
break;
case 'boolean':
$state = $request->{$field} == 'on' ? true : false;
if($state != $news->{$field}) {
$news->{$field} = $state;
$changed = true;
array_push($changedFields, $field);
}
break;
case 'published':
$state = $request->{$field} == 'on' ? true : false;
$published = $news->published_at != null;
if($state != $published) {
$news->published_at = $state ? now() : null;
$changed = true;
array_push($changedFields, $field);
}
break;
}
}
if($changed) {
$news->save();
}
$redirect = $news->published_at ? $news->permalink() : $news->editUrl();
return redirect($redirect);
}
} }

View file

@ -0,0 +1,94 @@
<?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)
{
abort(404);
$this->validate($request, [
'q' => 'nullable'
]);
}
public function archive(Request $request)
{
abort(404);
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]);
}
}

27
app/Newsroom.php Normal file
View file

@ -0,0 +1,27 @@
<?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}");
}
public function editUrl()
{
return url("/i/admin/newsroom/edit/{$this->id}");
}
}

View file

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

Binary file not shown.

BIN
public/js/compose.js vendored

Binary file not shown.

BIN
public/js/profile.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

Binary file not shown.

View 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/pixelfed/v1/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>

View file

@ -1,5 +1,6 @@
<template> <template>
<div> <div>
<transition name="fade">
<div class="card notification-card shadow-none border"> <div class="card notification-card shadow-none border">
<div class="card-header bg-white"> <div class="card-header bg-white">
<p class="mb-0 d-flex align-items-center justify-content-between"> <p class="mb-0 d-flex align-items-center justify-content-between">
@ -57,7 +58,8 @@
</div> </div>
</div> </div>
</div> </div>
</div> </transition>
</div>
</template> </template>
<style type="text/css" scoped></style> <style type="text/css" scoped></style>

View file

@ -90,41 +90,6 @@
<button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu(status)"> <button class="btn btn-link text-dark py-0" type="button" @click="ctxMenu(status)">
<span class="fas fa-ellipsis-h text-lighter"></span> <span class="fas fa-ellipsis-h text-lighter"></span>
</button> </button>
<!-- <div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item font-weight-bold" :href="status.url">Go to post</a>
<!-- <a class="dropdown-item font-weight-bold" href="#">Share</a>
<a class="dropdown-item font-weight-bold" href="#">Embed</a> ->
<span v-if="statusOwner(status) == false">
<a class="dropdown-item font-weight-bold" :href="reportUrl(status)">Report</a>
<a class="dropdown-item font-weight-bold" v-on:click="muteProfile(status)">Mute Profile</a>
<a class="dropdown-item font-weight-bold" v-on:click="blockProfile(status)">Block Profile</a>
</span>
<span v-if="statusOwner(status) == true">
<a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
</span>
<span v-if="profile.is_admin == true && modes.mod == true">
<div class="dropdown-divider"></div>
<a v-if="!statusOwner(status)" class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
<div class="dropdown-divider"></div>
<h6 class="dropdown-header">Mod Tools</h6>
<a class="dropdown-item font-weight-bold" v-on:click="moderatePost(status, 'autocw')">
<p class="mb-0" data-toggle="tooltip" data-placement="bottom" title="Adds a CW to every post made by this account.">Enforce CW</p>
</a>
<a class="dropdown-item font-weight-bold" v-on:click="moderatePost(status, 'noautolink')">
<p class="mb-0" title="Do not transform mentions, hashtags or urls into HTML.">No Autolinking</p>
</a>
<a class="dropdown-item font-weight-bold" v-on:click="moderatePost(status, 'unlisted')">
<p class="mb-0" title="Removes account from public/network timelines.">Unlisted Posts</p>
</a>
<a class="dropdown-item font-weight-bold" v-on:click="moderatePost(status, 'disable')">
<p class="mb-0" title="Temporarily disable account until next time user log in.">Disable Account</p>
</a>
<a class="dropdown-item font-weight-bold" v-on:click="moderatePost(status, 'suspend')">
<p class="mb-0" title="This prevents any new interactions, without deleting existing data.">Suspend Account</p>
</a>
</span>
</div> -->
</div> </div>
</div> </div>
@ -281,21 +246,13 @@
</div> </div>
<div class="mb-4"> <div class="mb-4">
<a class="btn btn-light btn-block btn-sm font-weight-bold text-dark mb-3 border" href="/i/compose" data-toggle="modal" data-target="#composeModal"><i class="far fa-plus-square pr-3 fa-lg pt-1"></i> Compose Post</a> <a class="btn btn-light btn-block btn-sm font-weight-bold text-dark mb-3 border bg-white" href="/i/compose" data-toggle="modal" data-target="#composeModal">
<i class="far fa-plus-square pr-3 fa-lg pt-1"></i> Compose Post
</a>
</div> </div>
<div v-if="showTips" class="mb-4 card-tips"> <div v-if="showTips && !loading" class="mb-4 card-tips">
<div class="card border shadow-none mb-3" style="max-width: 18rem;"> <announcements-card v-on:show-tips="showTips = $event"></announcements-card>
<div class="card-body">
<div class="card-title">
<span class="font-weight-bold">Tip: Hide follower counts</span>
<span class="float-right cursor-pointer" @click.prevent="hideTips()"><i class="fas fa-times text-lighter"></i></span>
</div>
<p class="card-text">
<span style="font-size:13px;">You can hide followers or following count and lists on your profile.</span>
<br><a href="/settings/privacy/" class="small font-weight-bold">Privacy Settings</a></p>
</div>
</div>
</div> </div>
<div v-show="modes.notify == true && !loading" class="mb-4"> <div v-show="modes.notify == true && !loading" class="mb-4">
@ -565,7 +522,6 @@
beforeMount() { beforeMount() {
this.fetchProfile(); this.fetchProfile();
this.fetchTimelineApi(); this.fetchTimelineApi();
}, },
mounted() { mounted() {
@ -1359,11 +1315,6 @@
this.$refs.ctxModModal.hide(); this.$refs.ctxModModal.hide();
}, },
hideTips() {
this.showTips = false;
window.localStorage.setItem('metro-tips', false);
},
formatCount(count) { formatCount(count) {
return App.util.format.count(count); return App.util.format.count(count);
}, },
@ -1431,7 +1382,7 @@
return _.truncate(caption, { return _.truncate(caption, {
length: len length: len
}); });
} },
} }
} }
</script> </script>

View file

@ -37,3 +37,8 @@ Vue.component(
'timeline', 'timeline',
require('./components/Timeline.vue').default require('./components/Timeline.vue').default
); );
Vue.component(
'announcements-card',
require('./components/AnnouncementsCard.vue').default
);

View file

@ -0,0 +1,135 @@
@extends('admin.partial.template-full')
@section('section')
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div class="title">
<p class="h1 font-weight-bold">Newsroom</p>
<p class="lead mb-0">Create Announcement</p>
</div>
<div>
<a class="btn btn-outline-secondary px-2" style="font-size:13px;" href="{{route('admin.newsroom.home')}}"><i class="fas fa-chevron-left fa-sm text-lighter mr-1"></i> Back to Newsroom </a>
</div>
</div>
<hr>
</div>
<div class="col-md-7 border-right">
<div>
<form method="post">
@csrf
<div class="form-group">
<label for="title" class="small font-weight-bold text-muted text-uppercase">Title</label>
<input type="text" class="form-control" id="title" name="title">
<p class="help-text mb-0 small font-weight-bold text-lighter">We recommend titles shorter than 80 characters.</p>
</div>
<div class="form-group">
<label for="summary" class="small font-weight-bold text-muted text-uppercase">Summary</label>
<textarea class="form-control" id="summary" name="summary" rows="3"></textarea>
</div>
<div class="form-group">
<label for="body" class="small font-weight-bold text-muted text-uppercase">Body</label>
<textarea class="form-control" id="body" name="body" rows="6"></textarea>
<p class="help-text mb-0 small font-weight-bold text-lighter">Click <a href="#">here</a> to enable the rich text editor.</p>
</div>
<div class="form-group">
<label for="category" class="small font-weight-bold text-muted text-uppercase">Category</label>
<input type="text" class="form-control" id="category" name="category" value="update">
</div>
</div>
</div>
<div class="col-md-5">
<label class="small font-weight-bold text-muted text-uppercase">Preview</label>
<div class="card border shadow-none mb-3">
<div class="card-body">
<div class="card-title mb-0">
<span class="font-weight-bold" id="preview_title">Untitled</span>
<span class="float-right cursor-pointer" title="Close"><i class="fas fa-times text-lighter"></i></span>
</div>
<p class="card-text">
<span style="font-size:13px;" id="preview_summary">Add a summary</span>
</p>
<p class="d-flex align-items-center justify-content-between mb-0">
<a href="#" class="small font-weight-bold mb-0">Read more</a>
<span>
<span class="btn btn-outline-secondary btn-sm py-0 disabled">
<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">
<i class="fas fa-check fa-sm"></i>
</span>
<span class="btn btn-outline-secondary btn-sm py-0">
<i class="fas fa-chevron-right fa-sm"></i>
</span>
</span>
</p>
</div>
</div>
<hr>
<p class="mt-3">
<button type="submit" class="btn btn-primary btn-block font-weight-bold py-1 px-4">Save</button>
</p>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="published" name="published">
<label class="custom-control-label font-weight-bold text-uppercase text-muted" for="published">Published</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="show_timeline" name="show_timeline">
<label class="custom-control-label font-weight-bold text-uppercase text-muted" for="show_timeline">Show On Timelines</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="auth_only" name="auth_only">
<label class="custom-control-label font-weight-bold text-uppercase text-muted" for="auth_only">Logged in users only</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="show_link" name="show_link">
<label class="custom-control-label font-weight-bold text-uppercase text-muted" for="show_link">Show Read More Link</label>
</div>
</div>
{{-- <div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="force_modal" name="force_modal">
<label class="custom-control-label font-weight-bold text-uppercase text-muted" for="force_modal">Show Modal on timelines</label>
</div>
</div> --}}
</form>
</div>
</div>
<form id="delete-form" method="post">
@method('delete')
@csrf
</form>
@endsection
@push('scripts')
<script type="text/javascript">
$('#title').on('change keyup paste',function(e) {
let el = $(this);
let title = el.val()
$('#preview_title').text(title);
});
$('#summary').on('change keyup paste',function(e) {
let el = $(this);
let title = el.val()
$('#preview_summary').text(title);
});
$('#btn-delete').on('click', function(e) {
e.preventDefault();
if(window.confirm('Are you sure you want to delete this post?') == true) {
document.getElementById('delete-form').submit();
}
})
</script>
@endpush

View file

@ -0,0 +1,141 @@
@extends('admin.partial.template-full')
@section('section')
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div class="title">
<p class="h1 font-weight-bold">Newsroom</p>
<p class="lead mb-0">Edit Announcement</p>
</div>
<div>
<a class="btn btn-outline-secondary px-2" style="font-size:13px;" href="{{route('admin.newsroom.home')}}"><i class="fas fa-chevron-left fa-sm text-lighter mr-1"></i> Back to Newsroom </a>
</div>
</div>
<hr>
</div>
<div class="col-md-7 border-right">
<div>
<form method="post">
@csrf
<div class="form-group">
<label for="title" class="small font-weight-bold text-muted text-uppercase">Title</label>
<input type="text" class="form-control" id="title" name="title" value="{{$news->title}}">
<p class="help-text mb-0 small font-weight-bold text-lighter">We recommend titles shorter than 80 characters.</p>
</div>
<div class="form-group">
<label for="summary" class="small font-weight-bold text-muted text-uppercase">Summary</label>
<textarea class="form-control" id="summary" name="summary" rows="3">{{$news->summary}}</textarea>
</div>
<div class="form-group">
<label for="body" class="small font-weight-bold text-muted text-uppercase">Body</label>
<textarea class="form-control" id="body" name="body" rows="6">{{$news->body}}</textarea>
<p class="help-text mb-0 small font-weight-bold text-lighter">Click <a href="#">here</a> to enable the rich text editor.</p>
</div>
<div class="form-group">
<label for="category" class="small font-weight-bold text-muted text-uppercase">Category</label>
<input type="text" class="form-control" id="category" name="category" value="{{$news->category}}">
</div>
</div>
</div>
<div class="col-md-5">
<label class="small font-weight-bold text-muted text-uppercase">Preview</label>
<div class="card border shadow-none mb-3">
<div class="card-body">
<div class="card-title mb-0">
<span class="font-weight-bold" id="preview_title">{{$news->title}}</span>
<span class="float-right cursor-pointer" title="Close"><i class="fas fa-times text-lighter"></i></span>
</div>
<p class="card-text">
<span style="font-size:13px;" id="preview_summary">{{$news->summary}}</span>
</p>
<p class="d-flex align-items-center justify-content-between mb-0">
<a href="#" class="small font-weight-bold mb-0">Read more</a>
<span>
<span class="btn btn-outline-secondary btn-sm py-0 disabled">
<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">
<i class="fas fa-check fa-sm"></i>
</span>
<span class="btn btn-outline-secondary btn-sm py-0">
<i class="fas fa-chevron-right fa-sm"></i>
</span>
</span>
</p>
</div>
</div>
<hr>
<p class="mt-3">
<button type="submit" class="btn btn-primary btn-block font-weight-bold py-1 px-4">Save</button>
</p>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="published" name="published" {{$news->published_at ? 'checked="checked"' : ''}}>
<label class="custom-control-label font-weight-bold text-uppercase text-muted" for="published">Published</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="show_timeline" name="show_timeline" {{$news->show_timeline ? 'checked="checked"' : ''}}>
<label class="custom-control-label font-weight-bold text-uppercase text-muted" for="show_timeline">Show On Timelines</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="auth_only" name="auth_only" {{$news->auth_only ? 'checked="checked"' : ''}}>
<label class="custom-control-label font-weight-bold text-uppercase text-muted" for="auth_only">Logged in users only</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="show_link" name="show_link" {{$news->show_link ? 'checked="checked"' : ''}}>
<label class="custom-control-label font-weight-bold text-uppercase text-muted" for="show_link">Show Read More Link</label>
</div>
</div>
{{-- <div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="force_modal" name="force_modal" {{$news->force_modal ? 'checked="checked"' : ''}}>
<label class="custom-control-label font-weight-bold text-uppercase text-muted" for="force_modal">Show Modal on timelines</label>
</div>
</div> --}}
<hr>
</form>
<p class="mt-1 d-flex justify-content-between">
<button type="button" class="btn btn-outline-secondary btn-sm font-weight-bold py-1 px-3">Preview</button>
<button type="button" class="btn btn-outline-danger btn-sm font-weight-bold py-1 px-3" id="btn-delete">Delete</button>
</p>
</div>
</div>
<form id="delete-form" method="post">
@method('delete')
@csrf
</form>
@endsection
@push('scripts')
<script type="text/javascript">
$('#title').on('change keyup paste',function(e) {
let el = $(this);
let title = el.val()
$('#preview_title').text(title);
});
$('#summary').on('change keyup paste',function(e) {
let el = $(this);
let title = el.val()
$('#preview_summary').text(title);
});
$('#btn-delete').on('click', function(e) {
e.preventDefault();
if(window.confirm('Are you sure you want to delete this post?') == true) {
document.getElementById('delete-form').submit();
}
})
</script>
@endpush

View file

@ -0,0 +1,62 @@
@extends('admin.partial.template-full')
@section('section')
<div class="d-flex justify-content-between align-items-center">
<div class="title">
<p class="h1 font-weight-bold">Newsroom</p>
<p class="lead mb-0">Manage News and Platform Tips</p>
</div>
<div>
<a class="btn btn-outline-success px-4" style="font-size:13px;" href="{{route('admin.newsroom.create')}}">New Announcement</a>
<a class="btn btn-outline-secondary px-2 mr-3" style="font-size:13px;" href="/site/newsroom">View Newsroom <i class="fas fa-chevron-right fa-sm text-lighter ml-1"></i></a>
</div>
</div>
<div class="my-5 row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header bg-light lead font-weight-bold">
Announcements
</div>
@if($newsroom->count() > 0)
<ul class="list-group list-group-flush">
@foreach($newsroom as $news)
<li class="list-group-item d-flex align-items-center justify-content-between">
<div>
<p class="mb-0 font-weight-bold">{{str_limit($news->title,30)}}</p>
<p class="mb-0 small">{{str_limit($news->summary, 40)}}</p>
</div>
<div>
@if($news->published_at != null)
<span class="btn btn-success btn-sm px-2 py-0 font-weight-bold mr-3">PUBLISHED</span>
@else
<span class="btn btn-outline-secondary btn-sm px-2 py-0 font-weight-bold mr-3">DRAFT</span>
@endif
<a class="btn btn-outline-lighter btn-sm mr-2" title="Edit Post" data-toggle="tooltip" data-placement="bottom" href="{{$news->editUrl()}}">
<i class="fas fa-edit"></i>
</a>
@if($news->published_at)
<a class="btn btn-outline-lighter btn-sm" title="View Post" data-toggle="tooltip" data-placement="bottom" href="{{$news->permalink()}}">
<i class="fas fa-eye"></i>
</a>
@endif
</div>
</li>
@endforeach
</ul>
@else
<div class="card-body text-center">
<p class="lead mb-0 p-5">No Announcements Found!</p>
</div>
@endif
</div>
<div class="d-flex justify-content-center mt-4">
{!!$newsroom->links()!!}
</div>
</div>
</div>
@endsection

View file

@ -1,4 +1,4 @@
<nav class="navbar navbar-expand-lg navbar-light bg-white"> <nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container"> <div class="container">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topbarNav" aria-controls="topbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#topbarNav" aria-controls="topbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
@ -11,9 +11,6 @@
<li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}"> <li class="nav-item mx-2 {{request()->is('*messages*')?'active':''}}">
<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Messages</a> <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.messages')}}">Messages</a>
</li> </li>
<li class="nav-item mx-2 {{request()->is('*hashtags*')?'active':''}}">
<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.hashtags')}}">Hashtags</a>
</li>
<li class="nav-item mx-2 {{request()->is('*instances*')?'active':''}}"> <li class="nav-item mx-2 {{request()->is('*instances*')?'active':''}}">
<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.instances')}}">Instances</a> <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.instances')}}">Instances</a>
</li> </li>
@ -32,13 +29,15 @@
<li class="nav-item mx-2 {{request()->is('*users*')?'active':''}}"> <li class="nav-item mx-2 {{request()->is('*users*')?'active':''}}">
<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.users')}}">Users</a> <a class="nav-link font-weight-lighter text-muted" href="{{route('admin.users')}}">Users</a>
</li> </li>
<li class="nav-item dropdown mx-2 {{request()->is(['*settings*','*discover*'])?'active':''}}"> <li class="nav-item dropdown mx-2 {{request()->is(['*settings*','*discover*', '*site-news*'])?'active':''}}">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
More More
</a> </a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown"> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<a class="dropdown-item font-weight-bold {{request()->is('*apps*')?'active':''}}" href="{{route('admin.apps')}}">Apps</a> <a class="dropdown-item font-weight-bold {{request()->is('*apps*')?'active':''}}" href="{{route('admin.apps')}}">Apps</a>
<a class="dropdown-item font-weight-bold {{request()->is('*discover*')?'active':''}}" href="{{route('admin.discover')}}">Discover</a> <a class="dropdown-item font-weight-bold {{request()->is('*discover*')?'active':''}}" href="{{route('admin.discover')}}">Discover</a>
<a class="dropdown-item font-weight-bold {{request()->is('*hashtags*')?'active':''}}" href="{{route('admin.hashtags')}}">Hashtags</a>
<a class="dropdown-item font-weight-bold {{request()->is('*site-news*')?'active':''}}" href="/i/admin/site-news">Newsroom</a>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<a class="dropdown-item font-weight-bold" href="/horizon">Horizon</a> <a class="dropdown-item font-weight-bold" href="/horizon">Horizon</a>
{{-- <a class="dropdown-item font-weight-bold" href="#">Websockets</a> --}} {{-- <a class="dropdown-item font-weight-bold" href="#">Websockets</a> --}}

View file

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

View 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

View 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

View file

@ -0,0 +1,14 @@
<div class="container py-4">
<div class="col-12 d-flex justify-content-between border-bottom align-items-center pb-3 px-0">
<div>
<p class="h4 mb-0"><a href="/site/newsroom" class="text-dark text-decoration-none">Newsroom</a></p>
</div>
<div>
<a class="btn btn-outline-secondary btn-sm py-1" href="/"><i class="fas fa-chevron-left fa-sm text-lighter mr-2"></i> Back to Pixelfed</a>
</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>

View 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

View file

@ -52,6 +52,13 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
Route::get('messages/home', 'AdminController@messagesHome')->name('admin.messages'); Route::get('messages/home', 'AdminController@messagesHome')->name('admin.messages');
Route::get('messages/show/{id}', 'AdminController@messagesShow'); Route::get('messages/show/{id}', 'AdminController@messagesShow');
Route::post('messages/mark-read', 'AdminController@messagesMarkRead'); Route::post('messages/mark-read', 'AdminController@messagesMarkRead');
Route::redirect('site-news', '/i/admin/newsroom');
Route::get('newsroom', 'AdminController@newsroomHome')->name('admin.newsroom.home');
Route::get('newsroom/create', 'AdminController@newsroomCreate')->name('admin.newsroom.create');
Route::get('newsroom/edit/{id}', 'AdminController@newsroomEdit');
Route::post('newsroom/edit/{id}', 'AdminController@newsroomUpdate');
Route::delete('newsroom/edit/{id}', 'AdminController@newsroomDelete');
Route::post('newsroom/create', 'AdminController@newsroomStore');
}); });
Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization'])->group(function () { Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization'])->group(function () {
@ -113,6 +120,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('notifications', 'ApiController@notifications'); Route::get('notifications', 'ApiController@notifications');
Route::get('timelines/public', 'PublicApiController@publicTimelineApi'); Route::get('timelines/public', 'PublicApiController@publicTimelineApi');
Route::get('timelines/home', 'PublicApiController@homeTimelineApi'); Route::get('timelines/home', 'PublicApiController@homeTimelineApi');
Route::get('newsroom/timeline', 'NewsroomController@timelineApi');
Route::post('newsroom/markasread', 'NewsroomController@markAsRead');
}); });
Route::group(['prefix' => 'v2'], function() { Route::group(['prefix' => 'v2'], function() {
@ -360,6 +369,10 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::view('report-something', 'site.help.report-something')->name('help.report-something'); Route::view('report-something', 'site.help.report-something')->name('help.report-something');
Route::view('data-policy', 'site.help.data-policy')->name('help.data-policy'); Route::view('data-policy', 'site.help.data-policy')->name('help.data-policy');
}); });
Route::get('newsroom/{year}/{month}/{slug}', 'NewsroomController@show');
Route::get('newsroom/archive', 'NewsroomController@archive');
Route::get('newsroom/search', 'NewsroomController@search');
Route::get('newsroom', 'NewsroomController@index');
}); });
Route::group(['prefix' => 'timeline'], function () { Route::group(['prefix' => 'timeline'], function () {