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 948 additions and 72 deletions

View file

@ -9,6 +9,7 @@ use App\{
Instance,
Media,
Like,
Newsroom,
OauthClient,
Profile,
Report,
@ -258,4 +259,153 @@ class AdminController extends Controller
$message->save();
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');
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
public/js/status.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -11,18 +11,18 @@
"/js/collectioncompose.js": "/js/collectioncompose.js?id=3fd79944492361ec7347",
"/js/collections.js": "/js/collections.js?id=38be4150f3d2ebb15f50",
"/js/components.js": "/js/components.js?id=d8581521aef135284631",
"/js/compose.js": "/js/compose.js?id=ab40e1564f78b4b933cc",
"/js/compose-classic.js": "/js/compose-classic.js?id=21c9236d34fcc24c9664",
"/js/compose.js": "/js/compose.js?id=f06b87dba21d21c96906",
"/js/compose-classic.js": "/js/compose-classic.js?id=283f19c895f4118a2a8b",
"/js/developers.js": "/js/developers.js?id=f75deca5ccf47d43eb07",
"/js/discover.js": "/js/discover.js?id=ea7279e1612a1989941d",
"/js/hashtag.js": "/js/hashtag.js?id=e6b41cab117cb03c7d2a",
"/js/loops.js": "/js/loops.js?id=ac610897b12207c829b9",
"/js/mode-dot.js": "/js/mode-dot.js?id=1225a9aac7a93d5d232f",
"/js/profile.js": "/js/profile.js?id=4d4dd8cb76bfe4e8ad6f",
"/js/profile.js": "/js/profile.js?id=0b84a74043019413e09e",
"/js/profile-directory.js": "/js/profile-directory.js?id=7160b00d9beda164f1bc",
"/js/quill.js": "/js/quill.js?id=9b15ab0ae830e7293390",
"/js/search.js": "/js/search.js?id=22e8bccee621e57963d9",
"/js/status.js": "/js/status.js?id=11f714d45fa9163b5346",
"/js/status.js": "/js/status.js?id=e79505d19162a11cb404",
"/js/theme-monokai.js": "/js/theme-monokai.js?id=68116b72a65e5437af52",
"/js/timeline.js": "/js/timeline.js?id=1360934527079b965055"
"/js/timeline.js": "/js/timeline.js?id=fbfe5ae8d4edf779c820"
}

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>
<div>
<div>
<transition name="fade">
<div class="card notification-card shadow-none border">
<div class="card-header bg-white">
<p class="mb-0 d-flex align-items-center justify-content-between">
@ -57,7 +58,8 @@
</div>
</div>
</div>
</div>
</transition>
</div>
</template>
<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)">
<span class="fas fa-ellipsis-h text-lighter"></span>
</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>
@ -281,21 +246,13 @@
</div>
<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 v-if="showTips" class="mb-4 card-tips">
<div class="card border shadow-none mb-3" style="max-width: 18rem;">
<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 v-if="showTips && !loading" class="mb-4 card-tips">
<announcements-card v-on:show-tips="showTips = $event"></announcements-card>
</div>
<div v-show="modes.notify == true && !loading" class="mb-4">
@ -565,7 +522,6 @@
beforeMount() {
this.fetchProfile();
this.fetchTimelineApi();
},
mounted() {
@ -1359,11 +1315,6 @@
this.$refs.ctxModModal.hide();
},
hideTips() {
this.showTips = false;
window.localStorage.setItem('metro-tips', false);
},
formatCount(count) {
return App.util.format.count(count);
},
@ -1431,7 +1382,7 @@
return _.truncate(caption, {
length: len
});
}
},
}
}
</script>

View file

@ -36,4 +36,9 @@ Vue.component(
Vue.component(
'timeline',
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">
<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>
@ -11,9 +11,6 @@
<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>
</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':''}}">
<a class="nav-link font-weight-lighter text-muted" href="{{route('admin.instances')}}">Instances</a>
</li>
@ -32,13 +29,15 @@
<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>
</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">
More
</a>
<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('*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>
<a class="dropdown-item font-weight-bold" href="/horizon">Horizon</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/show/{id}', 'AdminController@messagesShow');
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 () {
@ -113,6 +120,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('notifications', 'ApiController@notifications');
Route::get('timelines/public', 'PublicApiController@publicTimelineApi');
Route::get('timelines/home', 'PublicApiController@homeTimelineApi');
Route::get('newsroom/timeline', 'NewsroomController@timelineApi');
Route::post('newsroom/markasread', 'NewsroomController@markAsRead');
});
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('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 () {