完成後台編輯文章架構

This commit is contained in:
kroutony 2020-02-23 20:41:59 +08:00
parent 31286d4e85
commit 142d787436
37 changed files with 821 additions and 87 deletions

View File

@ -0,0 +1,42 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class CreatePostType extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'command:name';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace App\Console\Commands;
use App\Post;
use App\User;
use App\Repositories\PostRepository;
use Illuminate\Console\Command;
class GeneratePosts extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'post:generate {postType} {amount?} {--user_id=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate posts';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
* @throws \Exception
*/
public function handle()
{
$user_id = $this->option('user_id');
if($user_id) {
$user = User::find($user_id);
if(!$user) {
throw new \Exception("User $user_id doesn't exist");
}
} else {
$user = User::inRandomOrder()->first();
if(!$user) {
throw new \Exception("User $user_id doesn't exist");
}
}
$amount = $this->argument('amount');
if(!$amount) {
$amount = 1;
}
$postType = $this->argument('postType');
$postTypes = config('postTypes');
if(array_key_exists($postType, $postTypes)) {
/** @var Post $model */
$postRepo = new PostRepository($postTypes[$postType]['model']);
$faker = \Faker\Factory::create();
for($i = 0; $i < $amount; $i++) {
$post = $postRepo->createModel();
$post->fill([
'title' => $this->randomTextLength($faker, 10, 25),
'excerpt' => $this->randomTextLength($faker, 10, 30),
'introduction' => $faker->realText($faker->numberBetween(20, 400)),
'user_id' => $user->id
]);
$post->save();
}
}
}
private function randomTextLength($faker, $min, $max)
{
return mb_substr($this->rtrim($faker->realText($max)), 0, $faker->numberBetween($min, $max - 1));
}
private function rtrim($text)
{
return mb_substr($text, 0, -1);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Admin\Menu\Articles;
use App\Http\Controllers\Admin\Menu\BaseMenuItemController;
use Illuminate\Http\Request;
class ArticlesMenuItemController extends BaseMenuItemController
{
public function __construct()
{
$this->name = 'adminMenu.items.articles.articles';
$this->slug = 'articles';
$this->permissions = ['admin manage post articles'];
$this->iconClasses = 'nav-icon icon-wrench';
}
public function handle(Request $request)
{
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Admin\Menu\Articles\Children;
use App\Http\Controllers\Admin\Menu\BaseMenuItemController;
use App\Http\Controllers\Admin\Menu\PostCreateMenuItemController;
use Illuminate\Http\Request;
class CreateMenuItemController extends PostCreateMenuItemController
{
public function __construct()
{
$this->name = 'adminMenu.items.articles.create';
$this->slug = 'create';
$this->iconClasses = 'nav-icon icon-wrench';
$this->postType = 'article';
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Admin\Menu\Articles\Children;
use App\Http\Controllers\Admin\Menu\PostListMenuItemController;
class ListMenuItemController extends PostListMenuItemController
{
public function __construct()
{
$this->name = 'adminMenu.items.articles.list';
$this->slug = 'list';
$this->iconClasses = 'nav-icon icon-wrench';
$this->postType = 'article';
$this->pageHeader = trans('adminPageHeader.articles.articles');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers\Admin\Menu\Portfolios\Children;
use App\Http\Controllers\Admin\Menu\PostCreateMenuItemController;
class CreateMenuItemController extends PostCreateMenuItemController
{
public function __construct()
{
$this->name = 'adminMenu.items.portfolios.create';
$this->slug = 'create';
$this->iconClasses = 'nav-icon icon-wrench';
$this->postType = 'portfolio';
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Admin\Menu\Portfolios\Children;
use App\Http\Controllers\Admin\Menu\PostListMenuItemController;
class ListMenuItemController extends PostListMenuItemController
{
public function __construct()
{
$this->name = 'adminMenu.items.portfolios.list';
$this->slug = 'list';
$this->iconClasses = 'nav-icon icon-wrench';
$this->postType = 'portfolio';
$this->pageHeader = trans('adminPageHeader.portfolios.portfolios');
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers\Admin\Menu\Portfolios;
use App\Http\Controllers\Admin\Menu\BaseMenuItemController;
use Illuminate\Http\Request;
class PortfoliosMenuItemController extends BaseMenuItemController
{
public function __construct()
{
$this->name = 'adminMenu.items.portfolios.portfolios';
$this->slug = 'portfolios';
$this->permissions = ['admin manage post portfolio'];
$this->iconClasses = 'nav-icon icon-wrench';
}
public function handle(Request $request)
{
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Admin\Menu;
use Illuminate\Http\Request;
class PostCreateMenuItemController extends BaseMenuItemController
{
protected $postType;
public function handle(Request $request)
{
$languages = app('SiteState')->languageTranslations;
return view('admin.menu.posts.edit', [
'adminRouteNamePrefix' => config('admin.route_name_prefix'),
'languages' => $languages,
'post' => null,
'resource' => $this->postType
]);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\Admin\Menu;
use App\Repositories\PostRepository;
use Illuminate\Http\Request;
class PostListMenuItemController extends BaseMenuItemController
{
protected $postType;
protected $pageHeader;
public function handle(Request $request)
{
$postTypes = config('postTypes.' . $this->postType);
$postRepo = new PostRepository($postTypes['model']);
$posts = $postRepo->getPostsPager();
return view('admin.menu.posts.list', [
'adminRouteNamePrefix' => config('admin.route_name_prefix'),
'resource' => $this->postType,
'pageHeader' => $this->pageHeader,
'posts' => $posts
]);
}
}

View File

@ -0,0 +1,106 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Traits\ModelAttributeTranslationsUpdattable;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Auth;
class PostController extends Controller
{
use ModelAttributeTranslationsUpdattable;
private function getModelClass($resource)
{
return config('postTypes.' . $resource)['model'];
}
private function getResource(Request $request)
{
return explode('.', str_replace(config('admin.route_name_prefix'), '', $request->route()->getName()))[0];
}
public function index(Request $request)
{
}
public function edit(Request $request, $postId)
{
$resource = $this->getResource($request);
$modelClass = $this->getModelClass($resource);
$post = $modelClass::find($postId);
$languages = app('SiteState')->languageTranslations;
return view('admin.menu.posts.edit', [
'adminRouteNamePrefix' => config('admin.route_name_prefix'),
'resource' => $resource,
'post' => $post,
'languages' => $languages
]);
}
public function create(Request $request)
{
}
public function store(Request $request)
{
return $this->update($request, null);
}
public function update(Request $request, $postId)
{
$defaultLocale = config('app.locale');
$resource = $this->getResource($request);
$modelClass = $this->getModelClass($resource);
/** @var \App\Post $post */
if($postId) {
$post = $modelClass::find($postId);
} else {
$post = app($modelClass);
$translatableAttributes = $post->getTranslatableAttributes();
$translatableAttributeValues = $request->all($translatableAttributes);
$translatableAttributeValues = collect($translatableAttributeValues)->map(function($translatableAttributeValue) use ($defaultLocale){
return $translatableAttributeValue[$defaultLocale];
});
$post = $modelClass::create(array_merge($translatableAttributeValues->toArray(), [
'user_id' => Auth::id()
]));
}
// Post的可翻譯屬性
$translatableAttributes = $post->getTranslatableAttributes();
// 更新所有可翻譯的屬性
foreach ($translatableAttributes as $translatableAttribute) {
$translations = $request->get($translatableAttribute);
if($translations) {
$this->updateAttributeTranslations($post, $translatableAttribute, $translations);
}
}
$post->fill([
'feature_image_id' => $request->get('feature_image_id')
]);
$post->save();
return redirect(route(config('admin.route_name_prefix') . $resource . '.edit', [$resource => $post->id]));
}
public function show(Request $request)
{
}
public function destroy(Request $request, $postId)
{
$resource = $this->getResource($request);
$modelClass = $this->getModelClass($resource);
$post = $modelClass::find($postId);
$post->delete();
return back();
}
}

View File

@ -9,4 +9,18 @@ abstract class Post extends TranslatableModel
protected $fillable = [ protected $fillable = [
'title', 'content', 'excerpt', 'user_id', 'feature_image_id' 'title', 'content', 'excerpt', 'user_id', 'feature_image_id'
]; ];
protected $translatableAttributes = [
'title', 'content', 'excerpt'
];
public function author()
{
return $this->belongsTo(User::class, 'user_id');
}
public function featureImage()
{
return $this->belongsTo(MediaFile::class, 'feature_image_id');
}
} }

View File

@ -60,7 +60,7 @@ class MainMenuItemPresenter
case 'title': case 'title':
$html .= $htmlPresenter->li([ $html .= $htmlPresenter->li([
'class' => 'nav-title', 'class' => 'nav-title',
'html' => trans('menu.titles.' . $item['name']) 'html' => trans('adminMenu.titles.' . $item['name'])
]); ]);
break; break;
case 'item': case 'item':

View File

@ -0,0 +1,60 @@
<?php
namespace App\Presenters\Admin;
use App\Repositories\MediaFileRepository;
class MediaSelectionFieldPresenter
{
public function render($mediaFileId = null, $inputHtmlArgs = [], $wrapperClasses = [])
{
$presenter = app('Html');
$mediaFileRepo = app(MediaFileRepository::class);
$mediaFile = $mediaFileRepo->findModel($mediaFileId);
$previewImgHtml = '';
$mediaFieldContentHtml = '';
if($mediaFile) {
$previewImgHtml = $presenter->img([
'src' => $mediaFile->url
]);
}
$mediaFieldContentHtml .= $presenter->input(array_merge([
'type' => 'hidden',
'class' => 'input',
'value' => $mediaFileId
], $inputHtmlArgs));
$mediaFieldContentHtml .= $presenter->div([
'class' => 'form-group row',
'html' => $presenter->div([
'class' => 'col-6',
'html' => $presenter->div([
'class' => 'preview',
'html' => $previewImgHtml
])
])
]);
$mediaFieldContentHtml .= $presenter->div([
'class' => 'form-group row',
'html' => [
$presenter->div([
'class' => 'col-3',
'html' => $presenter->button([
'class' => ['btn', 'btn-success', 'select-media'],
'html' => trans('form.buttons.select')
])
]),
$presenter->div([
'class' => 'col-3',
'html' => $presenter->button([
'class' => ['btn', 'btn-danger', 'clear-media'],
'style' => !$mediaFile ? 'display:none;' : '',
'html' => trans('form.buttons.remove')
])
])
]
]);
return $presenter->div([
'class' => array_merge($wrapperClasses, ['media-selection-field']),
'html' => $mediaFieldContentHtml
]);
}
}

View File

@ -12,7 +12,7 @@ class OptionFormFieldsPresenter
$options = config('admin.options.' . $page); $options = config('admin.options.' . $page);
$presenter = app('Html'); $presenter = app('Html');
$optionRepo = app('Option'); $optionRepo = app('Option');
$mediaFileRepo = app(MediaFileRepository::class); $mediaSelectionFieldPresenter = app(MediaSelectionFieldPresenter::class);
$html = ''; $html = '';
if(!empty($options['fields'])) { if(!empty($options['fields'])) {
foreach($options['fields'] as $key => $option) { foreach($options['fields'] as $key => $option) {
@ -37,7 +37,7 @@ class OptionFormFieldsPresenter
]), ]),
$presenter->div([ $presenter->div([
'class' => 'col-12 col-md-6', 'class' => 'col-12 col-md-6',
'html' => function() use ($key, $type, $required, $presenter, $optionRepo, $mediaFileRepo) { 'html' => function() use ($key, $type, $required, $presenter, $optionRepo, $mediaSelectionFieldPresenter) {
$html = ''; $html = '';
$bastHtmlArgs = [ $bastHtmlArgs = [
@ -46,54 +46,8 @@ class OptionFormFieldsPresenter
]; ];
switch ($type) { switch ($type) {
case 'media': case 'media':
$mediaFiledId = $optionRepo->$key; $mediaFileId = $optionRepo->$key;
$mediaFile = $mediaFileRepo->findModel($mediaFiledId); $html .= $mediaSelectionFieldPresenter->render($mediaFileId, $bastHtmlArgs, ['option-media-field']);
$previewImgHtml = '';
$mediaFieldContentHtml = '';
if($mediaFile) {
$previewImgHtml = $presenter->img([
'src' => $mediaFile->url
]);
}
$mediaFieldContentHtml .= $presenter->input(array_merge([
'type' => 'hidden',
'class' => 'input',
'value' => $mediaFiledId
], $bastHtmlArgs));
$mediaFieldContentHtml .= $presenter->div([
'class' => 'form-group row',
'html' => $presenter->div([
'class' => 'col-6',
'html' => $presenter->div([
'class' => 'preview',
'html' => $previewImgHtml
])
])
]);
$mediaFieldContentHtml .= $presenter->div([
'class' => 'form-group row',
'html' => [
$presenter->div([
'class' => 'col-3',
'html' => $presenter->button([
'class' => ['btn', 'btn-success', 'select-media'],
'html' => trans('form.buttons.select')
])
]),
$presenter->div([
'class' => 'col-3',
'html' => $presenter->button([
'class' => ['btn', 'btn-danger', 'clear-media'],
'style' => !$mediaFile ? 'display:none;' : '',
'html' => trans('form.buttons.remove')
])
])
]
]);
$html .= $presenter->div([
'class' => 'option-media-field',
'html' => $mediaFieldContentHtml
]);
break; break;
case 'text': case 'text':
case 'password': case 'password':

View File

@ -0,0 +1,27 @@
<?php
namespace App\Repositories;
use App\Post;
use Illuminate\Pagination\LengthAwarePaginator;
class PostRepository extends BaseRepository
{
public function __construct($model)
{
if(is_string($model)) {
$this->setModel(app($model));
} elseif($model instanceof Post) {
$this->setModel($model);
}
}
/**
* @param int $postsPerPage
* @return LengthAwarePaginator
*/
public function getPostsPager($postsPerPage = 15)
{
return $this->getModelClass()::orderByDesc('created_at')->paginate($postsPerPage);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Traits;
use App\Repositories\ModelTranslationRepository;
trait ModelAttributeTranslationsUpdattable
{
private function updateAttributeTranslations($model, $attribute, $translations)
{
$transRepo = app(ModelTranslationRepository::class);
$transRepo->setTranslatedModel($model);
$locales = app('SiteState')->languages;
$defaultLocale = config('app.locale');
foreach ($translations as $locale => $translation) {
if(in_array($locale, $locales)) {
$transRepo->updateModelTranslation($model->id, $attribute, $locale, $translation);
}
}
if(isset($translations[$defaultLocale])) {
$model->$attribute = $translations[$defaultLocale];
$model->save();
}
}
}

View File

@ -8,6 +8,29 @@ return [
'route_name_prefix' => 'admin.', 'route_name_prefix' => 'admin.',
// 後台選單項目 // 後台選單項目
'menuItems' => [ 'menuItems' => [
[
'type' => 'title',
'name' => 'posts',
],
[
'type' => 'item',
'controller' => Menu\Articles\ArticlesMenuItemController::class,
'children' => [
Menu\Articles\Children\ListMenuItemController::class,
Menu\Articles\Children\CreateMenuItemController::class,
]
],
[
'type' => 'item',
'controller' => Menu\Portfolios\PortfoliosMenuItemController::class,
'children' => [
Menu\Portfolios\Children\ListMenuItemController::class,
Menu\Portfolios\Children\CreateMenuItemController::class,
]
],
[
'type' => 'divider'
],
[ [
'type' => 'item', 'type' => 'item',
'controller' => Menu\Options\OptionsMenuItemController::class, 'controller' => Menu\Options\OptionsMenuItemController::class,

9
config/postTypes.php Normal file
View File

@ -0,0 +1,9 @@
<?php
return [
'article' => [
'model' => \App\Article::class
],
'portfolio' => [
'model' => \App\Portfolio::class
],
];

View File

@ -1,5 +0,0 @@
<?php
return [
\App\Article::class,
\App\Portfolio::class,
];

View File

@ -106,4 +106,33 @@ $(() => {
$('#app-header .medialibrary').on('click', function(){ $('#app-header .medialibrary').on('click', function(){
app.adminMediaLibrary = app.methods.media(false, null, false); app.adminMediaLibrary = app.methods.media(false, null, false);
}); });
$('.media-selection-field .select-media').on('click', e => {
e.preventDefault()
let $this = $(e.currentTarget),
$wrapper = $this.closest('.media-selection-field'),
$input = $wrapper.find('input'),
$preview = $wrapper.find('.preview'),
$clearButton = $wrapper.find('.clear-media')
app.methods.media(true, $input.get(0), true, (data) => {
let media = data.medias[0]
$preview.empty().append(
$('<img>').attr('src', media.url)
)
$clearButton.show()
})
})
$('.media-selection-field .clear-media').on('click', e => {
e.preventDefault()
let $this = $(e.currentTarget),
$wrapper = $this.closest('.media-selection-field'),
$input = $wrapper.find('input'),
$preview = $wrapper.find('.preview')
$input.val(null)
$preview.empty()
$this.hide()
})
}) })

View File

@ -1,28 +0,0 @@
$('.select-media').on('click', e => {
e.preventDefault()
let $this = $(e.currentTarget),
$wrapper = $this.closest('.option-media-field'),
$input = $wrapper.find('input'),
$preview = $wrapper.find('.preview'),
$clearButton = $wrapper.find('.clear-media')
app.methods.media(true, $input.get(0), true, (data) => {
let media = data.medias[0]
$preview.empty().append(
$('<img>').attr('src', media.url)
)
$clearButton.show()
})
})
$('.clear-media').on('click', e => {
e.preventDefault()
let $this = $(e.currentTarget),
$wrapper = $this.closest('.option-media-field'),
$input = $wrapper.find('input'),
$preview = $wrapper.find('.preview')
$input.val(null)
$preview.empty()
$this.hide()
})

View File

@ -12,6 +12,19 @@ return array(
'routes' => 'Route List', 'routes' => 'Route List',
'system' => 'System', 'system' => 'System',
'gateAbilities' => 'Gate Abilities', 'gateAbilities' => 'Gate Abilities',
],
'articles' => [
'articles' => 'Articles',
'list' => 'List',
'create' => 'Create',
],
'portfolios' => [
'portfolios' => 'Portfolios',
'list' => 'List',
'create' => 'Create'
] ]
], ],
'titles' => [
'posts' => 'Posts'
]
); );

View File

@ -6,4 +6,10 @@ return [
'development' => 'Development Settings', 'development' => 'Development Settings',
'platform' => 'Platform Settings', 'platform' => 'Platform Settings',
], ],
'articles' => [
'articles' => 'Articles'
],
'portfolios' => [
'portfolios' => 'Portfolios'
]
]; ];

View File

@ -3,6 +3,19 @@ return [
'buttons' => [ 'buttons' => [
'update' => 'Update', 'update' => 'Update',
'select' => 'Select', 'select' => 'Select',
'remove' => 'Remove' 'remove' => 'Remove',
'create' => 'Create',
'delete' => 'Delete',
'logout' => 'Logout',
],
'titles' => [
'title' => 'Title',
'author' => 'Author',
'created' => 'Created',
'updated' => 'Updated',
'operations' => 'Operations',
'excerpt' => 'Excerpt',
'content' => 'Content',
'featureImage' => 'Feature Image'
] ]
]; ];

View File

@ -0,0 +1,9 @@
<?php
return array (
'en' => 'English',
'zh-tw' => 'Chinese Traditional',
'zh-cn' => 'Chinese Simplified',
'ja' => 'Japanese',
'ko' => 'Korean',
);

View File

@ -1,5 +1,6 @@
<?php <?php
return [ return [
'mediaLibrary' => 'Media Library',
'tabCategory' => 'Category', 'tabCategory' => 'Category',
'tabUpload' => 'Upload', 'tabUpload' => 'Upload',
'tabBrowse' => 'Browse', 'tabBrowse' => 'Browse',

View File

@ -0,0 +1,30 @@
<?php
return array(
'items' => [
'options' => [
'options' => '設定',
'general' => '一般',
'development' => '開發',
'platform' => '平台',
],
'systemStatus' => [
'systemStatus' => '系統狀態',
'routes' => '路由列表',
'system' => '系統資訊',
'gateAbilities' => 'Gate能力列表',
],
'articles' => [
'articles' => '文章',
'list' => '列表',
'create' => '建立',
],
'portfolios' => [
'portfolios' => '作品集',
'list' => '列表',
'create' => '建立'
]
],
'titles' => [
'posts' => '文章內容'
]
);

View File

@ -0,0 +1,9 @@
<?php
return array (
'en' => '英文',
'zh-tw' => '繁體中文',
'zh-cn' => '簡體中文',
'ja' => '日文',
'ko' => '韓文',
);

View File

@ -1,5 +1,6 @@
<?php <?php
return [ return [
'mediaLibrary' => '媒體庫',
'category' => '分類', 'category' => '分類',
'date' => '日期', 'date' => '日期',
'delete' => '刪除', 'delete' => '刪除',

View File

@ -5,3 +5,21 @@
@import "components/blockui"; @import "components/blockui";
@import "../components/media-library"; @import "../components/media-library";
@import "../app-common"; @import "../app-common";
//媒體庫圖片預覽
.media-selection-field {
.preview {
img {
max-width: 100%;
}
}
}
//左側主選單子項目
.sidebar-nav {
ul.submenu {
li.nav-item {
a.nav-link {
padding: 0.3rem 0.6rem 0.3rem 1.4rem;
}
}
}
}

View File

@ -0,0 +1,5 @@
td.feature-image {
img {
max-width: 50px;
}
}

View File

@ -34,7 +34,7 @@
@include('components.navBrand') @include('components.navBrand')
</li> </li>
<li class="nav-item px-3"> <li class="nav-item px-3">
<a href="#" class="nav-link medialibrary">Media Library</a> <a href="#" class="nav-link medialibrary">{{ trans('mediaLibrary.mediaLibrary') }}</a>
</li> </li>
</ul> </ul>
<ul class="nav navbar-nav ml-auto"> <ul class="nav navbar-nav ml-auto">
@ -49,7 +49,7 @@
</button> </button>
<form action="{{ route('logout') }}" method="post"> <form action="{{ route('logout') }}" method="post">
@csrf @csrf
<button class="btn btn-outline-danger btn-sm">登出</button> <button class="btn btn-outline-danger btn-sm">{{ trans('form.buttons.logout') }}</button>
</form> </form>
</header> </header>
<div id="app-body" class="app-body"> <div id="app-body" class="app-body">

View File

@ -0,0 +1,56 @@
@extends('admin.layouts.app')
@section('admin-page-content')
<form action="{{ $post ? route($adminRouteNamePrefix . $resource . '.update', [$post->id]) : route($adminRouteNamePrefix . $resource . '.store') }}" method="POST">
@if($post)
@method('PUT')
@endif
@csrf
<div class="row">
<div class="col-8">
<ul class="nav nav-tabs" id="language-tab" role="tablist">
@foreach ($languages as $languageCode => $name)
<li class="nav-item">
<a class="nav-link {{ $languageCode == config('app.locale') ? 'active' : ''}}" id="{{ $languageCode }}-tab" data-toggle="tab" href="#{{ $languageCode }}-content" role="tab" aria-controls="{{ $languageCode }}-content" aria-selected="true">@lang('languages.' . $languageCode)</a>
</li>
@endforeach
</ul>
<div class="tab-content">
<?php foreach ($languages as $languageCode => $name): ?>
<div class="tab-pane {{ $languageCode == config('app.locale') ? 'active' : ''}}" id="{{$languageCode}}-content" role="tabpanel" aria-labelledby="{{ $languageCode }}-tab">
<div class="form-group">
<label>{{ trans('form.titles.title') }}</label>
<input type="text" name="title[{{ $languageCode }}]" class="form-control" value="{{ $post ? $post->trans('title', $languageCode) : '' }}" required>
</div>
<div class="form-group">
<label>{{ trans('form.titles.excerpt') }}</label>
<input type="text" name="excerpt[{{ $languageCode }}]" class="form-control" value="{{ $post ? $post->trans('excerpt', $languageCode) : '' }}">
</div>
<div class="form-group">
<label>{{ trans('form.titles.content') }}</label>
<textarea name="content[{{ $languageCode }}]" class="form-control" rows="10">{{ $post ? $post->trans('content', $languageCode) : ''}}</textarea>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<div class="col-4">
@if($post)
<div class="form-group">
<label>{{ trans('form.titles.author') }}</label>
<div>{{ $post->author->email}}</div>
</div>
@endif
@inject('mediaSelectionFieldPresenter', 'App\Presenters\Admin\MediaSelectionFieldPresenter')
<div class="form-group">
<label>{{ trans('form.titles.featureImage') }}</label>
{!! $mediaSelectionFieldPresenter->render($post ? $post->feature_image_id : null, ['name' => 'feature_image_id']) !!}
</div>
<div class="form-group">
<button class="btn btn-success">{{ $post ? trans('form.buttons.update') : trans('form.buttons.create') }}</button>
</div>
</div>
</div>
</form>
@endsection

View File

@ -0,0 +1,46 @@
@extends('admin.layouts.app')
@push('admin-app-styles')
<link rel="stylesheet" href="{{ asset('css/admin/page/post-list.css') }}">
@endpush
@section('admin-page-content')
<h2>{{ $pageHeader }}</h2>
<table class="table table-striped">
<thead>
<tr>
<th></th>
<th>{{ trans('form.titles.title') }}</th>
<th>{{ trans('form.titles.author') }}</th>
<th>{{ trans('form.titles.created') }}</th>
<th>{{ trans('form.titles.updated') }}</th>
<th>{{ trans('form.titles.operations') }}</th>
</tr>
</thead>
<tbody>
@foreach($posts as $post)
<tr>
<td class="feature-image">
@if($post->featureImage)
<img src="{{ $post->featureImage->url }}" alt="{{ $post->featureImage->description }}">
@endif
</td>
<td><a href="{{ route($adminRouteNamePrefix . $resource . '.edit', [$resource => $post->id]) }}">{{ $post->title }}</a></td>
<td>{{ $post->author->email }}</td>
<td>{{ $post->created_at }}</td>
<td>{{ $post->updated_at }}</td>
<td>
<form action="{{ route($adminRouteNamePrefix . $resource . '.destroy', [$resource => $post->id]) }}" method="POST">
@method('delete')
@csrf
<button class="btn btn-danger">{{ trans('form.buttons.delete') }}</button>
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
{!! $posts->links() !!}
@endsection

View File

@ -28,6 +28,10 @@ Route::get('/robots.txt', 'PageController@robotstxt');
*/ */
Route::group(['prefix' => config('admin.route'), 'middleware' => ['admin.area'], 'as' => config('admin.route_name_prefix')], function() { Route::group(['prefix' => config('admin.route'), 'middleware' => ['admin.area'], 'as' => config('admin.route_name_prefix')], function() {
Route::get('/', 'AdminPageController@index')->name('index'); Route::get('/', 'AdminPageController@index')->name('index');
foreach (config('postTypes') as $resource => $postType) {
Route::resource($resource, 'Admin\PostController');
}
}); });
/** /**

1
webpack.mix.js vendored
View File

@ -28,3 +28,4 @@ mix.sass('resources/sass/admin/lib.scss', publicAdminCssDir)
.sass('resources/sass/admin/app.scss', publicAdminCssDir) .sass('resources/sass/admin/app.scss', publicAdminCssDir)
.sass('resources/sass/admin/page/system-status.scss', publicAdminCssDir + '/page') .sass('resources/sass/admin/page/system-status.scss', publicAdminCssDir + '/page')
.sass('resources/sass/admin/page/options.scss', publicAdminCssDir + '/page') .sass('resources/sass/admin/page/options.scss', publicAdminCssDir + '/page')
.sass('resources/sass/admin/page/post-list.scss', publicAdminCssDir + '/page')