move into sonardesk
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
return ['name' => 'Blog'];
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('blog_posts', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('author_id')->nullable()->constrained('authors')->nullOnDelete();
|
||||
$table->foreignId('category_id')->nullable()->constrained('categories')->nullOnDelete();
|
||||
$table->string('title');
|
||||
$table->string('slug')->unique();
|
||||
$table->text('excerpt')->nullable();
|
||||
$table->longText('content')->nullable();
|
||||
$table->string('featured_image')->nullable();
|
||||
$table->string('status')->default('draft')->index();
|
||||
$table->boolean('is_featured')->default(false);
|
||||
$table->timestamp('published_at')->nullable()->index();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->string('meta_title')->nullable();
|
||||
$table->text('meta_description')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('blog_post_tag', function (Blueprint $table): void {
|
||||
$table->foreignId('blog_post_id')->constrained('blog_posts')->cascadeOnDelete();
|
||||
$table->foreignId('tag_id')->constrained('tags')->cascadeOnDelete();
|
||||
$table->primary(['blog_post_id', 'tag_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('blog_post_tag');
|
||||
Schema::dropIfExists('blog_posts');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Blog\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Modules\Authors\Models\Author;
|
||||
use Modules\Blog\Models\Blog;
|
||||
use Modules\Categories\Models\Category;
|
||||
use Modules\Tags\Models\Tag;
|
||||
|
||||
final class BlogController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('blog::admin.index', ['records' => Blog::with(['author', 'category'])->latest()->paginate(25)]);
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
return view('blog::admin.form', $this->lookups());
|
||||
}
|
||||
|
||||
public function store(Request $r): RedirectResponse
|
||||
{
|
||||
$record = Blog::create($this->data($r));
|
||||
$record->tags()->sync($r->input('tag_ids', []));
|
||||
|
||||
return redirect()->route('administration.blog.index')->with('status', 'Blog created.');
|
||||
}
|
||||
|
||||
public function edit(Blog $blog_post): View
|
||||
{
|
||||
return view('blog::admin.form', array_merge($this->lookups(), ['record' => $blog_post]));
|
||||
}
|
||||
|
||||
public function update(Request $r, Blog $blog_post): RedirectResponse
|
||||
{
|
||||
$blog_post->update($this->data($r));
|
||||
$blog_post->tags()->sync($r->input('tag_ids', []));
|
||||
|
||||
return redirect()->route('administration.blog.index')->with('status', 'Blog updated.');
|
||||
}
|
||||
|
||||
public function destroy(Blog $blog_post): RedirectResponse
|
||||
{
|
||||
$blog_post->delete();
|
||||
|
||||
return back()->with('status', 'Blog deleted.');
|
||||
}
|
||||
|
||||
private function lookups(): array
|
||||
{
|
||||
return ['authors' => Author::where('is_active', true)->orderBy('name')->get(), 'categories' => Category::where('is_active', true)->orderBy('name')->get(), 'tags' => Tag::orderBy('name')->get()];
|
||||
}
|
||||
|
||||
private function data(Request $r): array
|
||||
{
|
||||
$d = $r->validate(['author_id' => ['nullable', 'exists:authors,id'], 'category_id' => ['nullable', 'exists:categories,id'], 'title' => ['required', 'string', 'max:255'], 'slug' => ['required', 'string', 'max:255'], 'excerpt' => ['nullable', 'string'], 'content' => ['nullable', 'string'], 'featured_image' => ['nullable', 'string', 'max:255'], 'status' => ['required', 'in:draft,review,published,archived'], 'is_featured' => ['sometimes', 'boolean'], 'published_at' => ['nullable', 'date'], 'expires_at' => ['nullable', 'date', 'after:published_at'], 'meta_title' => ['nullable', 'string', 'max:255'], 'meta_description' => ['nullable', 'string']]);
|
||||
$d['is_featured'] = $r->boolean('is_featured');
|
||||
|
||||
return $d;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Blog\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\View\View;
|
||||
use Modules\Blog\Models\Blog;
|
||||
|
||||
final class PublicBlogController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
return view('blog::public.index', ['records' => Blog::published()->with(['author', 'category'])->latest('published_at')->paginate(12)]);
|
||||
}
|
||||
|
||||
public function show(string $slug): View
|
||||
{
|
||||
return view('blog::public.show', ['record' => Blog::published()->with(['author', 'category', 'tags'])->where('slug', $slug)->firstOrFail()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Blog\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Modules\Authors\Models\Author;
|
||||
use Modules\Categories\Models\Category;
|
||||
use Modules\Localization\Concerns\HasTranslations;
|
||||
use Modules\Sites\Concerns\BelongsToSites;
|
||||
use Modules\Tags\Models\Tag;
|
||||
use Modules\Workflow\Concerns\HasWorkflow;
|
||||
|
||||
final class Blog extends Model
|
||||
{
|
||||
use BelongsToSites, HasTranslations, HasWorkflow;
|
||||
|
||||
protected $table = 'blog_posts';
|
||||
|
||||
protected $fillable = ['author_id', 'category_id', 'title', 'slug', 'excerpt', 'content', 'featured_image', 'status', 'is_featured', 'published_at', 'expires_at', 'meta_title', 'meta_description'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['is_featured' => 'boolean', 'published_at' => 'datetime', 'expires_at' => 'datetime'];
|
||||
}
|
||||
|
||||
public function author(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Author::class);
|
||||
}
|
||||
|
||||
public function category(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
|
||||
public function tags(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Tag::class, 'blog_post_tag');
|
||||
}
|
||||
|
||||
public function scopePublished(Builder $query): Builder
|
||||
{
|
||||
return $query->where('status', 'published')->where(fn (Builder $q) => $q->whereNull('published_at')->orWhere('published_at', '<=', now()))->where(fn (Builder $q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Blog\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
final class BlogServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->register(RouteServiceProvider::class);
|
||||
$this->mergeConfigFrom(module_path('Blog', 'Config/config.php'), 'blog');
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->loadMigrationsFrom(module_path('Blog', 'Database/Migrations'));
|
||||
$this->loadViewsFrom(module_path('Blog', 'Resources/views'), 'blog');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Blog\Providers;
|
||||
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
final class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
Route::middleware('web')->group(module_path('Blog', 'Routes/web.php'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="isset($record) ? 'Edit Blog' : 'Create Blog'"><form method="POST" action="{{ isset($record) ? route('administration.blog.update',$record) : route('administration.blog.store') }}" class="space-y-5">@csrf @isset($record) @method('PUT') @endisset <div class="grid gap-5 md:grid-cols-2"><flux:input name="title" label="Title" value="{{ old('title',$record->title ?? '') }}" required/><flux:input name="slug" label="Slug" value="{{ old('slug',$record->slug ?? '') }}" required/><flux:select name="author_id" label="Author"><option value="">None</option>@foreach($authors as $author)<option value="{{ $author->id }}" @selected(old('author_id',$record->author_id ?? null)==$author->id)>{{ $author->name }}</option>@endforeach</flux:select><flux:select name="category_id" label="Category"><option value="">None</option>@foreach($categories as $category)<option value="{{ $category->id }}" @selected(old('category_id',$record->category_id ?? null)==$category->id)>{{ $category->name }}</option>@endforeach</flux:select><flux:select name="status" label="Status">@foreach(['draft','review','published','archived'] as $status)<option @selected(old('status',$record->status ?? 'draft')===$status)>{{ $status }}</option>@endforeach</flux:select><flux:input type="datetime-local" name="published_at" label="Publish at" value="{{ old('published_at',isset($record->published_at)?$record->published_at->format('Y-m-d\TH:i'):'') }}"/><flux:input type="datetime-local" name="expires_at" label="Expires at" value="{{ old('expires_at',isset($record->expires_at)?$record->expires_at->format('Y-m-d\TH:i'):'') }}"/><flux:input name="featured_image" label="Featured image path" value="{{ old('featured_image',$record->featured_image ?? '') }}"/></div><flux:textarea name="excerpt" label="Excerpt">{{ old('excerpt',$record->excerpt ?? '') }}</flux:textarea><flux:textarea name="content" label="Content" rows="16">{{ old('content',$record->content ?? '') }}</flux:textarea><div><label class="text-sm font-medium">Tags</label><div class="mt-2 grid gap-2 md:grid-cols-3">@foreach($tags as $tag)<label><input type="checkbox" name="tag_ids[]" value="{{ $tag->id }}" @checked(in_array($tag->id,old('tag_ids',isset($record)?$record->tags->pluck('id')->all():[])))> {{ $tag->name }}</label>@endforeach</div></div><flux:checkbox name="is_featured" value="1" label="Featured" :checked="old('is_featured',$record->is_featured ?? false)"/><flux:input name="meta_title" label="SEO title" value="{{ old('meta_title',$record->meta_title ?? '') }}"/><flux:textarea name="meta_description" label="SEO description">{{ old('meta_description',$record->meta_description ?? '') }}</flux:textarea><flux:button type="submit" variant="primary">Save</flux:button></form></x-layouts.app>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layouts.app :title="__('Blog')"><div class="flex items-center justify-between mb-6"><div><h1 class="text-2xl font-semibold">Blog</h1><p class="text-sm text-zinc-500">Manage publishing blog.</p></div><flux:button variant="primary" :href="route('administration.blog.create')" wire:navigate>Create</flux:button></div>@if(session('status'))<div class="mb-4 rounded border border-green-300 bg-green-50 p-3 text-green-800">{{ session('status') }}</div>@endif<div class="overflow-hidden rounded-xl border border-zinc-200 bg-white"><table class="w-full text-sm"><thead class="bg-zinc-50"><tr><th class="p-3 text-left">Name / title</th><th class="p-3 text-left">Status</th><th class="p-3 text-right">Actions</th></tr></thead><tbody>@forelse($records as $record)<tr class="border-t"><td class="p-3 font-medium">{{ $record->title ?? $record->name }}</td><td class="p-3">{{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }}</td><td class="p-3 text-right"><flux:button size="sm" :href="route('administration.blog.edit', $record)" wire:navigate>Edit</flux:button><form class="inline" method="POST" action="{{ route('administration.blog.destroy', $record) }}">@csrf @method('DELETE')<flux:button size="sm" variant="danger" type="submit">Delete</flux:button></form></td></tr>@empty<tr><td colspan="3" class="p-6 text-center text-zinc-500">No records yet.</td></tr>@endforelse</tbody></table></div><div class="mt-4">{{ $records->links() }}</div></x-layouts.app>
|
||||
@@ -0,0 +1,31 @@
|
||||
@extends('stylist-theme-saasland::layouts.app')
|
||||
|
||||
@php
|
||||
$title = 'Blog';
|
||||
$description = 'Ideas, guides and updates from our team.';
|
||||
$eyebrow = 'Insights';
|
||||
$breadcrumbs = ['Blog' => null];
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
<section class="content-section">
|
||||
<div class="wrap">
|
||||
<div class="content-grid">
|
||||
@forelse($records as $record)
|
||||
<article class="post-card">
|
||||
<div class="post-card-accent"></div>
|
||||
<div class="post-card-body">
|
||||
<span class="post-meta">{{ optional($record->published_at)->format('j F Y') ?: 'Latest article' }}</span>
|
||||
<h2><a href="{{ route('blog.public.show', $record->slug) }}">{{ $record->title }}</a></h2>
|
||||
@if($record->excerpt)<p>{{ $record->excerpt }}</p>@endif
|
||||
<a class="read-more" href="{{ route('blog.public.show', $record->slug) }}">Read article →</a>
|
||||
</div>
|
||||
</article>
|
||||
@empty
|
||||
<div class="empty-state">No blog posts have been published yet.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="theme-pagination">{{ $records->links() }}</div>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,23 @@
|
||||
@extends('stylist-theme-saasland::layouts.app')
|
||||
|
||||
@php
|
||||
$title = $record->title;
|
||||
$description = $record->excerpt;
|
||||
$eyebrow = 'Blog';
|
||||
$breadcrumbs = ['Blog' => route('blog.public.index'), $record->title => null];
|
||||
@endphp
|
||||
|
||||
@section('content')
|
||||
<section class="content-section">
|
||||
<div class="wrap content-layout">
|
||||
<article class="content-card prose-content">
|
||||
<div class="article-meta">
|
||||
@if($record->author)<span>{{ $record->author->name }}</span>@endif
|
||||
@if($record->published_at)<time datetime="{{ $record->published_at->toDateString() }}">{{ $record->published_at->format('j F Y') }}</time>@endif
|
||||
</div>
|
||||
@if($record->excerpt)<p class="content-lead">{{ $record->excerpt }}</p>@endif
|
||||
<div class="cms-content">{!! nl2br(e($record->content)) !!}</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Modules\Blog\Http\Controllers\BlogController;
|
||||
use Modules\Blog\Http\Controllers\PublicBlogController;
|
||||
|
||||
Route::middleware(['auth', 'verified'])->prefix('administration/blog')->name('administration.blog.')->group(function (): void {
|
||||
Route::get('/', [BlogController::class, 'index'])->name('index');
|
||||
Route::get('/create', [BlogController::class, 'create'])->name('create');
|
||||
Route::post('/', [BlogController::class, 'store'])->name('store');
|
||||
Route::get('/{blog_post}/edit', [BlogController::class, 'edit'])->name('edit');
|
||||
Route::match(['put', 'patch'], '/{blog_post}', [BlogController::class, 'update'])->name('update');
|
||||
Route::delete('/{blog_post}', [BlogController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
Route::get('/blog', [PublicBlogController::class, 'index'])->name('blog.public.index');
|
||||
Route::get('/blog/{slug}', [PublicBlogController::class, 'show'])->name('blog.public.show');
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "Blog",
|
||||
"alias": "blog",
|
||||
"version": "1.0.0",
|
||||
"description": "Blog publishing module.",
|
||||
"keywords": ["publishing", "blog"],
|
||||
"priority": 30,
|
||||
"providers": ["Modules\\Blog\\Providers\\BlogServiceProvider"],
|
||||
"aliases": {},
|
||||
"files": [],
|
||||
"requires": ["Core", "Administration"]
|
||||
}
|
||||
Reference in New Issue
Block a user