diff --git a/Modules/Authors/Config/config.php b/Modules/Authors/Config/config.php new file mode 100644 index 0000000..e09f04e --- /dev/null +++ b/Modules/Authors/Config/config.php @@ -0,0 +1,2 @@ + 'Authors']; diff --git a/Modules/Authors/Database/Migrations/2026_07_24_030300_create_authors_table.php b/Modules/Authors/Database/Migrations/2026_07_24_030300_create_authors_table.php new file mode 100644 index 0000000..1566e6c --- /dev/null +++ b/Modules/Authors/Database/Migrations/2026_07_24_030300_create_authors_table.php @@ -0,0 +1,3 @@ +id(); $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); $table->string('name'); $table->string('slug')->unique(); $table->string('job_title')->nullable(); $table->text('biography')->nullable(); $table->string('avatar_path')->nullable(); $table->json('social_links')->nullable(); $table->string('meta_title')->nullable(); $table->text('meta_description')->nullable(); $table->boolean('is_active')->default(true); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('authors'); } }; diff --git a/Modules/Authors/Http/Controllers/AuthorController.php b/Modules/Authors/Http/Controllers/AuthorController.php new file mode 100644 index 0000000..e065d83 --- /dev/null +++ b/Modules/Authors/Http/Controllers/AuthorController.php @@ -0,0 +1,4 @@ +Author::with('user')->latest()->paginate(25)]); } public function create(): View { return view('authors::admin.form',['users'=>User::orderBy('name')->get()]); } public function store(Request $r): RedirectResponse { Author::create($this->data($r)); return redirect()->route('administration.authors.index')->with('status','Author created.'); } public function edit(Author $author): View { return view('authors::admin.form',['record'=>$author,'users'=>User::orderBy('name')->get()]); } public function update(Request $r, Author $author): RedirectResponse { $author->update($this->data($r)); return redirect()->route('administration.authors.index')->with('status','Author updated.'); } public function destroy(Author $author): RedirectResponse { $author->delete(); return back()->with('status','Author deleted.'); } private function data(Request $r): array { $d=$r->validate(['user_id'=>['nullable','exists:users,id'],'name'=>['required','string','max:255'],'slug'=>['required','string','max:255'],'job_title'=>['nullable','string','max:255'],'biography'=>['nullable','string'],'avatar_path'=>['nullable','string','max:255'],'meta_title'=>['nullable','string','max:255'],'meta_description'=>['nullable','string'],'is_active'=>['sometimes','boolean']]); $d['is_active']=$r->boolean('is_active'); return $d; } } diff --git a/Modules/Authors/Models/Author.php b/Modules/Authors/Models/Author.php new file mode 100644 index 0000000..0b51f11 --- /dev/null +++ b/Modules/Authors/Models/Author.php @@ -0,0 +1,3 @@ +'array','is_active'=>'boolean']; } public function user(): BelongsTo { return $this->belongsTo(User::class); } } diff --git a/Modules/Authors/Providers/AuthorsServiceProvider.php b/Modules/Authors/Providers/AuthorsServiceProvider.php new file mode 100644 index 0000000..a80374d --- /dev/null +++ b/Modules/Authors/Providers/AuthorsServiceProvider.php @@ -0,0 +1,16 @@ +app->register(RouteServiceProvider::class); + $this->mergeConfigFrom(module_path('Authors', 'Config/config.php'), 'authors'); + } + public function boot(): void + { + $this->loadMigrationsFrom(module_path('Authors', 'Database/Migrations')); + $this->loadViewsFrom(module_path('Authors', 'Resources/views'), 'authors'); + } +} diff --git a/Modules/Authors/Providers/RouteServiceProvider.php b/Modules/Authors/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..c75b852 --- /dev/null +++ b/Modules/Authors/Providers/RouteServiceProvider.php @@ -0,0 +1,12 @@ +group(module_path('Authors', 'Routes/web.php')); + } +} diff --git a/Modules/Authors/Resources/views/admin/form.blade.php b/Modules/Authors/Resources/views/admin/form.blade.php new file mode 100644 index 0000000..c224794 --- /dev/null +++ b/Modules/Authors/Resources/views/admin/form.blade.php @@ -0,0 +1 @@ +
@csrf @isset($record) @method('PUT') @endisset @foreach($users as $user)@endforeach{{ old('biography',$record->biography ?? '') }}Save
diff --git a/Modules/Authors/Resources/views/admin/index.blade.php b/Modules/Authors/Resources/views/admin/index.blade.php new file mode 100644 index 0000000..5db3b07 --- /dev/null +++ b/Modules/Authors/Resources/views/admin/index.blade.php @@ -0,0 +1 @@ +

Authors

Manage publishing authors.

Create
@if(session('status'))
{{ session('status') }}
@endif
@forelse($records as $record)@empty@endforelse
Name / titleStatusActions
{{ $record->title ?? $record->name }}{{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }}Edit
@csrf @method('DELETE')Delete
No records yet.
{{ $records->links() }}
diff --git a/Modules/Authors/Routes/web.php b/Modules/Authors/Routes/web.php new file mode 100644 index 0000000..9e7408c --- /dev/null +++ b/Modules/Authors/Routes/web.php @@ -0,0 +1,3 @@ +prefix('administration/authors')->name('administration.authors.')->group(function (): void { Route::resource('/', AuthorController::class)->parameters([''=>'author'])->except('show'); }); diff --git a/Modules/Authors/Tests/Feature/ModuleBootTest.php b/Modules/Authors/Tests/Feature/ModuleBootTest.php new file mode 100644 index 0000000..b391a0e --- /dev/null +++ b/Modules/Authors/Tests/Feature/ModuleBootTest.php @@ -0,0 +1,10 @@ +assertSame('Authors', config('authors.name')); + } +} diff --git a/Modules/Authors/module.json b/Modules/Authors/module.json new file mode 100644 index 0000000..c972181 --- /dev/null +++ b/Modules/Authors/module.json @@ -0,0 +1,12 @@ +{ + "name": "Authors", + "alias": "authors", + "version": "1.0.0", + "description": "Authors publishing module.", + "keywords": ["publishing", "authors"], + "priority": 30, + "providers": ["Modules\\Authors\\Providers\\AuthorsServiceProvider"], + "aliases": {}, + "files": [], + "requires": ["Core", "Administration"] +} diff --git a/Modules/Banners/Database/Migrations/2026_07_24_000600_create_banners_table.php b/Modules/Banners/Database/Migrations/2026_07_24_000600_create_banners_table.php deleted file mode 100644 index 672055a..0000000 --- a/Modules/Banners/Database/Migrations/2026_07_24_000600_create_banners_table.php +++ /dev/null @@ -1,16 +0,0 @@ -id(); - $table->string('title'); - $table->text('content')->nullable(); - $table->string('link_url')->nullable(); - $table->string('position')->default('default'); - $table->boolean('enabled')->default(true); - $table->timestamps(); $table->softDeletes(); }); } - public function down(): void { Schema::dropIfExists('banners'); } -}; diff --git a/Modules/Blog/Config/config.php b/Modules/Blog/Config/config.php new file mode 100644 index 0000000..e68ab05 --- /dev/null +++ b/Modules/Blog/Config/config.php @@ -0,0 +1,2 @@ + 'Blog']; diff --git a/Modules/Blog/Database/Migrations/2026_07_24_03041_create_blog_posts_table.php b/Modules/Blog/Database/Migrations/2026_07_24_03041_create_blog_posts_table.php new file mode 100644 index 0000000..0e1305b --- /dev/null +++ b/Modules/Blog/Database/Migrations/2026_07_24_03041_create_blog_posts_table.php @@ -0,0 +1,3 @@ +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'); } }; diff --git a/Modules/Blog/Http/Controllers/BlogController.php b/Modules/Blog/Http/Controllers/BlogController.php new file mode 100644 index 0000000..c5f1de1 --- /dev/null +++ b/Modules/Blog/Http/Controllers/BlogController.php @@ -0,0 +1,4 @@ +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; } } diff --git a/Modules/Blog/Http/Controllers/PublicBlogController.php b/Modules/Blog/Http/Controllers/PublicBlogController.php new file mode 100644 index 0000000..84600d4 --- /dev/null +++ b/Modules/Blog/Http/Controllers/PublicBlogController.php @@ -0,0 +1,3 @@ +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()]); } } diff --git a/Modules/Blog/Models/Blog.php b/Modules/Blog/Models/Blog.php new file mode 100644 index 0000000..754009f --- /dev/null +++ b/Modules/Blog/Models/Blog.php @@ -0,0 +1,4 @@ +'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())); } } diff --git a/Modules/Blog/Providers/BlogServiceProvider.php b/Modules/Blog/Providers/BlogServiceProvider.php new file mode 100644 index 0000000..7f1ff3d --- /dev/null +++ b/Modules/Blog/Providers/BlogServiceProvider.php @@ -0,0 +1,16 @@ +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'); + } +} diff --git a/Modules/Blog/Providers/RouteServiceProvider.php b/Modules/Blog/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..38fcab9 --- /dev/null +++ b/Modules/Blog/Providers/RouteServiceProvider.php @@ -0,0 +1,12 @@ +group(module_path('Blog', 'Routes/web.php')); + } +} diff --git a/Modules/Blog/Resources/views/admin/form.blade.php b/Modules/Blog/Resources/views/admin/form.blade.php new file mode 100644 index 0000000..cf54f2b --- /dev/null +++ b/Modules/Blog/Resources/views/admin/form.blade.php @@ -0,0 +1 @@ +
@csrf @isset($record) @method('PUT') @endisset
@foreach($authors as $author)@endforeach@foreach($categories as $category)@endforeach@foreach(['draft','review','published','archived'] as $status)@endforeach
{{ old('excerpt',$record->excerpt ?? '') }}{{ old('content',$record->content ?? '') }}
@foreach($tags as $tag)@endforeach
{{ old('meta_description',$record->meta_description ?? '') }}Save
diff --git a/Modules/Blog/Resources/views/admin/index.blade.php b/Modules/Blog/Resources/views/admin/index.blade.php new file mode 100644 index 0000000..9ec9eda --- /dev/null +++ b/Modules/Blog/Resources/views/admin/index.blade.php @@ -0,0 +1 @@ +

Blog

Manage publishing blog.

Create
@if(session('status'))
{{ session('status') }}
@endif
@forelse($records as $record)@empty@endforelse
Name / titleStatusActions
{{ $record->title ?? $record->name }}{{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }}Edit
@csrf @method('DELETE')Delete
No records yet.
{{ $records->links() }}
diff --git a/Modules/Blog/Resources/views/public/index.blade.php b/Modules/Blog/Resources/views/public/index.blade.php new file mode 100644 index 0000000..64a2e23 --- /dev/null +++ b/Modules/Blog/Resources/views/public/index.blade.php @@ -0,0 +1 @@ +

Blog

@foreach($records as $record)@endforeach
{{ $records->links() }}
diff --git a/Modules/Blog/Resources/views/public/show.blade.php b/Modules/Blog/Resources/views/public/show.blade.php new file mode 100644 index 0000000..882c377 --- /dev/null +++ b/Modules/Blog/Resources/views/public/show.blade.php @@ -0,0 +1 @@ +

{{ $record->title }}

{{ optional($record->published_at)->format('j F Y') }}

{!! nl2br(e($record->content)) !!}
diff --git a/Modules/Blog/Routes/web.php b/Modules/Blog/Routes/web.php new file mode 100644 index 0000000..8333954 --- /dev/null +++ b/Modules/Blog/Routes/web.php @@ -0,0 +1,4 @@ +prefix('administration/blog')->name('administration.blog.')->group(function (): void { Route::resource('/', BlogController::class)->parameters([''=>'blog_post'])->except('show'); }); +Route::get('/blog',[PublicBlogController::class,'index'])->name('blog.public.index'); Route::get('/blog/{slug}',[PublicBlogController::class,'show'])->name('blog.public.show'); diff --git a/Modules/Blog/Tests/Feature/ModuleBootTest.php b/Modules/Blog/Tests/Feature/ModuleBootTest.php new file mode 100644 index 0000000..f57b6fc --- /dev/null +++ b/Modules/Blog/Tests/Feature/ModuleBootTest.php @@ -0,0 +1,10 @@ +assertSame('Blog', config('blog.name')); + } +} diff --git a/Modules/Blog/module.json b/Modules/Blog/module.json new file mode 100644 index 0000000..e72c145 --- /dev/null +++ b/Modules/Blog/module.json @@ -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"] +} diff --git a/Modules/Categories/Config/config.php b/Modules/Categories/Config/config.php new file mode 100644 index 0000000..1381bd6 --- /dev/null +++ b/Modules/Categories/Config/config.php @@ -0,0 +1,2 @@ + 'Categories']; diff --git a/Modules/Categories/Database/Migrations/2026_07_24_030100_create_categories_table.php b/Modules/Categories/Database/Migrations/2026_07_24_030100_create_categories_table.php new file mode 100644 index 0000000..28e530b --- /dev/null +++ b/Modules/Categories/Database/Migrations/2026_07_24_030100_create_categories_table.php @@ -0,0 +1,8 @@ +id(); $table->foreignId('parent_id')->nullable()->constrained('categories')->nullOnDelete(); $table->string('name'); $table->string('slug')->unique(); $table->text('description')->nullable(); $table->string('type')->default('content')->index(); $table->unsignedInteger('sort_order')->default(0); $table->boolean('is_active')->default(true); $table->timestamps(); }); } + public function down(): void { Schema::dropIfExists('categories'); } +}; diff --git a/Modules/Categories/Http/Controllers/CategoryController.php b/Modules/Categories/Http/Controllers/CategoryController.php new file mode 100644 index 0000000..e40003c --- /dev/null +++ b/Modules/Categories/Http/Controllers/CategoryController.php @@ -0,0 +1,17 @@ +Category::with('parent')->orderBy('type')->orderBy('sort_order')->paginate(30)]); } + public function create(): View { return view('categories::admin.form', ['parents'=>Category::orderBy('name')->get()]); } + public function store(Request $request): RedirectResponse { Category::create($this->validated($request)); return redirect()->route('administration.categories.index')->with('status','Category created.'); } + public function edit(Category $category): View { return view('categories::admin.form', ['record'=>$category,'parents'=>Category::whereKeyNot($category->getKey())->orderBy('name')->get()]); } + public function update(Request $request, Category $category): RedirectResponse { $category->update($this->validated($request)); return redirect()->route('administration.categories.index')->with('status','Category updated.'); } + public function destroy(Category $category): RedirectResponse { $category->delete(); return back()->with('status','Category deleted.'); } + private function validated(Request $request): array { $data=$request->validate(['parent_id'=>['nullable','exists:categories,id'],'name'=>['required','string','max:255'],'slug'=>['required','string','max:255'],'description'=>['nullable','string'],'type'=>['required','string','max:50'],'sort_order'=>['nullable','integer','min:0'],'is_active'=>['sometimes','boolean']]); $data['is_active']=$request->boolean('is_active'); return $data; } +} diff --git a/Modules/Categories/Models/Category.php b/Modules/Categories/Models/Category.php new file mode 100644 index 0000000..afe2af0 --- /dev/null +++ b/Modules/Categories/Models/Category.php @@ -0,0 +1,12 @@ +'boolean','sort_order'=>'integer']; } + public function parent(): BelongsTo { return $this->belongsTo(self::class, 'parent_id'); } + public function children(): HasMany { return $this->hasMany(self::class, 'parent_id')->orderBy('sort_order'); } +} diff --git a/Modules/Categories/Providers/CategoriesServiceProvider.php b/Modules/Categories/Providers/CategoriesServiceProvider.php new file mode 100644 index 0000000..4ce9af4 --- /dev/null +++ b/Modules/Categories/Providers/CategoriesServiceProvider.php @@ -0,0 +1,16 @@ +app->register(RouteServiceProvider::class); + $this->mergeConfigFrom(module_path('Categories', 'Config/config.php'), 'categories'); + } + public function boot(): void + { + $this->loadMigrationsFrom(module_path('Categories', 'Database/Migrations')); + $this->loadViewsFrom(module_path('Categories', 'Resources/views'), 'categories'); + } +} diff --git a/Modules/Categories/Providers/RouteServiceProvider.php b/Modules/Categories/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..5b0de5b --- /dev/null +++ b/Modules/Categories/Providers/RouteServiceProvider.php @@ -0,0 +1,12 @@ +group(module_path('Categories', 'Routes/web.php')); + } +} diff --git a/Modules/Categories/Resources/views/admin/form.blade.php b/Modules/Categories/Resources/views/admin/form.blade.php new file mode 100644 index 0000000..3ec530b --- /dev/null +++ b/Modules/Categories/Resources/views/admin/form.blade.php @@ -0,0 +1 @@ +
@csrf @isset($record) @method('PUT') @endisset @foreach($parents as $parent)@endforeach{{ old('description',$record->description ?? '') }}Save
diff --git a/Modules/Categories/Resources/views/admin/index.blade.php b/Modules/Categories/Resources/views/admin/index.blade.php new file mode 100644 index 0000000..8b15801 --- /dev/null +++ b/Modules/Categories/Resources/views/admin/index.blade.php @@ -0,0 +1 @@ +

Categories

Manage publishing categories.

Create
@if(session('status'))
{{ session('status') }}
@endif
@forelse($records as $record)@empty@endforelse
Name / titleStatusActions
{{ $record->title ?? $record->name }}{{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }}Edit
@csrf @method('DELETE')Delete
No records yet.
{{ $records->links() }}
diff --git a/Modules/Categories/Routes/web.php b/Modules/Categories/Routes/web.php new file mode 100644 index 0000000..e65f08d --- /dev/null +++ b/Modules/Categories/Routes/web.php @@ -0,0 +1,4 @@ +prefix('administration/categories')->name('administration.categories.')->group(function (): void { Route::resource('/', CategoryController::class)->parameters([''=>'category'])->except('show'); }); diff --git a/Modules/Categories/Tests/Feature/ModuleBootTest.php b/Modules/Categories/Tests/Feature/ModuleBootTest.php new file mode 100644 index 0000000..827a912 --- /dev/null +++ b/Modules/Categories/Tests/Feature/ModuleBootTest.php @@ -0,0 +1,10 @@ +assertSame('Categories', config('categories.name')); + } +} diff --git a/Modules/Categories/module.json b/Modules/Categories/module.json new file mode 100644 index 0000000..bde3d86 --- /dev/null +++ b/Modules/Categories/module.json @@ -0,0 +1,12 @@ +{ + "name": "Categories", + "alias": "categories", + "version": "1.0.0", + "description": "Categories publishing module.", + "keywords": ["publishing", "categories"], + "priority": 30, + "providers": ["Modules\\Categories\\Providers\\CategoriesServiceProvider"], + "aliases": {}, + "files": [], + "requires": ["Core", "Administration"] +} diff --git a/Modules/Comments/Config/config.php b/Modules/Comments/Config/config.php new file mode 100644 index 0000000..f396772 --- /dev/null +++ b/Modules/Comments/Config/config.php @@ -0,0 +1,2 @@ + 'Comments']; diff --git a/Modules/Comments/Database/Migrations/2026_07_24_030600_create_comments_table.php b/Modules/Comments/Database/Migrations/2026_07_24_030600_create_comments_table.php new file mode 100644 index 0000000..9f27f7b --- /dev/null +++ b/Modules/Comments/Database/Migrations/2026_07_24_030600_create_comments_table.php @@ -0,0 +1,3 @@ +id(); $table->nullableMorphs('commentable'); $table->foreignId('parent_id')->nullable()->constrained('comments')->cascadeOnDelete(); $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); $table->string('author_name'); $table->string('author_email'); $table->text('body'); $table->string('status')->default('pending')->index(); $table->string('ip_address',45)->nullable(); $table->text('moderation_notes')->nullable(); $table->timestamp('approved_at')->nullable(); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('comments'); } }; diff --git a/Modules/Comments/Http/Controllers/CommentController.php b/Modules/Comments/Http/Controllers/CommentController.php new file mode 100644 index 0000000..f5dc98c --- /dev/null +++ b/Modules/Comments/Http/Controllers/CommentController.php @@ -0,0 +1,3 @@ +filled('status')) $q->where('status',$r->string('status')); return view('comments::admin.index',['records'=>$q->paginate(30)]); } public function update(Request $r, Comment $comment): RedirectResponse { $data=$r->validate(['status'=>['required','in:pending,approved,spam,rejected'],'moderation_notes'=>['nullable','string']]); $data['approved_at']=$data['status']==='approved' ? now() : null; $comment->update($data); return back()->with('status','Comment moderated.'); } public function destroy(Comment $comment): RedirectResponse { $comment->delete(); return back()->with('status','Comment deleted.'); } } diff --git a/Modules/Comments/Models/Comment.php b/Modules/Comments/Models/Comment.php new file mode 100644 index 0000000..41b6791 --- /dev/null +++ b/Modules/Comments/Models/Comment.php @@ -0,0 +1,3 @@ +'datetime']; } public function commentable(): MorphTo { return $this->morphTo(); } } diff --git a/Modules/Comments/Providers/CommentsServiceProvider.php b/Modules/Comments/Providers/CommentsServiceProvider.php new file mode 100644 index 0000000..d82fda6 --- /dev/null +++ b/Modules/Comments/Providers/CommentsServiceProvider.php @@ -0,0 +1,16 @@ +app->register(RouteServiceProvider::class); + $this->mergeConfigFrom(module_path('Comments', 'Config/config.php'), 'comments'); + } + public function boot(): void + { + $this->loadMigrationsFrom(module_path('Comments', 'Database/Migrations')); + $this->loadViewsFrom(module_path('Comments', 'Resources/views'), 'comments'); + } +} diff --git a/Modules/Comments/Providers/RouteServiceProvider.php b/Modules/Comments/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..49209ce --- /dev/null +++ b/Modules/Comments/Providers/RouteServiceProvider.php @@ -0,0 +1,12 @@ +group(module_path('Comments', 'Routes/web.php')); + } +} diff --git a/Modules/Comments/Resources/views/admin/index.blade.php b/Modules/Comments/Resources/views/admin/index.blade.php new file mode 100644 index 0000000..a9e6652 --- /dev/null +++ b/Modules/Comments/Resources/views/admin/index.blade.php @@ -0,0 +1 @@ +

Comment moderation

@if(session('status'))
{{ session('status') }}
@endif
@forelse($records as $comment)
{{ $comment->author_name }}{{ $comment->status }}

{{ $comment->body }}

@csrf @method('PUT')Update
@empty

No comments awaiting moderation.

@endforelse
{{ $records->links() }}
diff --git a/Modules/Comments/Routes/web.php b/Modules/Comments/Routes/web.php new file mode 100644 index 0000000..5ca010e --- /dev/null +++ b/Modules/Comments/Routes/web.php @@ -0,0 +1,3 @@ +prefix('administration/comments')->name('administration.comments.')->group(function (): void { Route::get('/',[CommentController::class,'index'])->name('index'); Route::put('/{comment}',[CommentController::class,'update'])->name('update'); Route::delete('/{comment}',[CommentController::class,'destroy'])->name('destroy'); }); diff --git a/Modules/Comments/Tests/Feature/ModuleBootTest.php b/Modules/Comments/Tests/Feature/ModuleBootTest.php new file mode 100644 index 0000000..64fca51 --- /dev/null +++ b/Modules/Comments/Tests/Feature/ModuleBootTest.php @@ -0,0 +1,10 @@ +assertSame('Comments', config('comments.name')); + } +} diff --git a/Modules/Comments/module.json b/Modules/Comments/module.json new file mode 100644 index 0000000..ab9f85e --- /dev/null +++ b/Modules/Comments/module.json @@ -0,0 +1,12 @@ +{ + "name": "Comments", + "alias": "comments", + "version": "1.0.0", + "description": "Comments publishing module.", + "keywords": ["publishing", "comments"], + "priority": 30, + "providers": ["Modules\\Comments\\Providers\\CommentsServiceProvider"], + "aliases": {}, + "files": [], + "requires": ["Core", "Administration"] +} diff --git a/Modules/Feeds/Config/config.php b/Modules/Feeds/Config/config.php new file mode 100644 index 0000000..0a91fe1 --- /dev/null +++ b/Modules/Feeds/Config/config.php @@ -0,0 +1,2 @@ + 'Feeds']; diff --git a/Modules/Feeds/Http/Controllers/FeedController.php b/Modules/Feeds/Http/Controllers/FeedController.php new file mode 100644 index 0000000..fae5be4 --- /dev/null +++ b/Modules/Feeds/Http/Controllers/FeedController.php @@ -0,0 +1,3 @@ +view('feeds::rss',['title'=>config('app.name').' Blog','records'=>Blog::published()->latest('published_at')->limit(50)->get(),'routeName'=>'blog.public.show'])->header('Content-Type','application/rss+xml; charset=UTF-8'); } public function news(): Response { return response()->view('feeds::rss',['title'=>config('app.name').' News','records'=>News::published()->latest('published_at')->limit(50)->get(),'routeName'=>'news.public.show'])->header('Content-Type','application/rss+xml; charset=UTF-8'); } } diff --git a/Modules/Feeds/Providers/FeedsServiceProvider.php b/Modules/Feeds/Providers/FeedsServiceProvider.php new file mode 100644 index 0000000..324230b --- /dev/null +++ b/Modules/Feeds/Providers/FeedsServiceProvider.php @@ -0,0 +1,16 @@ +app->register(RouteServiceProvider::class); + $this->mergeConfigFrom(module_path('Feeds', 'Config/config.php'), 'feeds'); + } + public function boot(): void + { + $this->loadMigrationsFrom(module_path('Feeds', 'Database/Migrations')); + $this->loadViewsFrom(module_path('Feeds', 'Resources/views'), 'feeds'); + } +} diff --git a/Modules/Feeds/Providers/RouteServiceProvider.php b/Modules/Feeds/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..443ab7f --- /dev/null +++ b/Modules/Feeds/Providers/RouteServiceProvider.php @@ -0,0 +1,12 @@ +group(module_path('Feeds', 'Routes/web.php')); + } +} diff --git a/Modules/Feeds/Resources/views/rss.blade.php b/Modules/Feeds/Resources/views/rss.blade.php new file mode 100644 index 0000000..e516351 --- /dev/null +++ b/Modules/Feeds/Resources/views/rss.blade.php @@ -0,0 +1 @@ +{!! '' !!}{{ $title }}{{ url('/') }}{{ $title }}@foreach($records as $record){{ $record->title }}{{ route($routeName, $record->slug) }}{{ route($routeName, $record->slug) }}excerpt }}]]>{{ optional($record->published_at)->toRssString() }}@endforeach diff --git a/Modules/Feeds/Routes/web.php b/Modules/Feeds/Routes/web.php new file mode 100644 index 0000000..97819a9 --- /dev/null +++ b/Modules/Feeds/Routes/web.php @@ -0,0 +1,3 @@ +name('feeds.blog'); Route::get('/feeds/news.xml',[FeedController::class,'news'])->name('feeds.news'); diff --git a/Modules/Feeds/Tests/Feature/ModuleBootTest.php b/Modules/Feeds/Tests/Feature/ModuleBootTest.php new file mode 100644 index 0000000..8e40d43 --- /dev/null +++ b/Modules/Feeds/Tests/Feature/ModuleBootTest.php @@ -0,0 +1,10 @@ +assertSame('Feeds', config('feeds.name')); + } +} diff --git a/Modules/Feeds/module.json b/Modules/Feeds/module.json new file mode 100644 index 0000000..9569047 --- /dev/null +++ b/Modules/Feeds/module.json @@ -0,0 +1,12 @@ +{ + "name": "Feeds", + "alias": "feeds", + "version": "1.0.0", + "description": "Feeds publishing module.", + "keywords": ["publishing", "feeds"], + "priority": 30, + "providers": ["Modules\\Feeds\\Providers\\FeedsServiceProvider"], + "aliases": {}, + "files": [], + "requires": ["Core", "Administration"] +} diff --git a/Modules/News/Config/config.php b/Modules/News/Config/config.php new file mode 100644 index 0000000..0db6d9a --- /dev/null +++ b/Modules/News/Config/config.php @@ -0,0 +1,2 @@ + 'News']; diff --git a/Modules/News/Database/Migrations/2026_07_24_03042_create_news_articles_table.php b/Modules/News/Database/Migrations/2026_07_24_03042_create_news_articles_table.php new file mode 100644 index 0000000..d1a675a --- /dev/null +++ b/Modules/News/Database/Migrations/2026_07_24_03042_create_news_articles_table.php @@ -0,0 +1,3 @@ +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('news_article_tag', function(Blueprint $table): void { $table->foreignId('news_article_id')->constrained('news_articles')->cascadeOnDelete(); $table->foreignId('tag_id')->constrained('tags')->cascadeOnDelete(); $table->primary(['news_article_id','tag_id']); }); } public function down(): void { Schema::dropIfExists('news_article_tag'); Schema::dropIfExists('news_articles'); } }; diff --git a/Modules/News/Http/Controllers/NewsController.php b/Modules/News/Http/Controllers/NewsController.php new file mode 100644 index 0000000..34e9073 --- /dev/null +++ b/Modules/News/Http/Controllers/NewsController.php @@ -0,0 +1,4 @@ +News::with(['author','category'])->latest()->paginate(25)]); } public function create(): View { return view('news::admin.form',$this->lookups()); } public function store(Request $r): RedirectResponse { $record=News::create($this->data($r)); $record->tags()->sync($r->input('tag_ids',[])); return redirect()->route('administration.news.index')->with('status','News created.'); } public function edit(News $news_article): View { return view('news::admin.form',array_merge($this->lookups(),['record'=>$news_article])); } public function update(Request $r, News $news_article): RedirectResponse { $news_article->update($this->data($r)); $news_article->tags()->sync($r->input('tag_ids',[])); return redirect()->route('administration.news.index')->with('status','News updated.'); } public function destroy(News $news_article): RedirectResponse { $news_article->delete(); return back()->with('status','News 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; } } diff --git a/Modules/News/Http/Controllers/PublicNewsController.php b/Modules/News/Http/Controllers/PublicNewsController.php new file mode 100644 index 0000000..c0fef89 --- /dev/null +++ b/Modules/News/Http/Controllers/PublicNewsController.php @@ -0,0 +1,3 @@ +News::published()->with(['author','category'])->latest('published_at')->paginate(12)]); } public function show(string $slug): View { return view('news::public.show',['record'=>News::published()->with(['author','category','tags'])->where('slug',$slug)->firstOrFail()]); } } diff --git a/Modules/News/Models/News.php b/Modules/News/Models/News.php new file mode 100644 index 0000000..57b32d4 --- /dev/null +++ b/Modules/News/Models/News.php @@ -0,0 +1,4 @@ +'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, 'news_article_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())); } } diff --git a/Modules/News/Providers/NewsServiceProvider.php b/Modules/News/Providers/NewsServiceProvider.php new file mode 100644 index 0000000..b791919 --- /dev/null +++ b/Modules/News/Providers/NewsServiceProvider.php @@ -0,0 +1,16 @@ +app->register(RouteServiceProvider::class); + $this->mergeConfigFrom(module_path('News', 'Config/config.php'), 'news'); + } + public function boot(): void + { + $this->loadMigrationsFrom(module_path('News', 'Database/Migrations')); + $this->loadViewsFrom(module_path('News', 'Resources/views'), 'news'); + } +} diff --git a/Modules/News/Providers/RouteServiceProvider.php b/Modules/News/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..4a8265e --- /dev/null +++ b/Modules/News/Providers/RouteServiceProvider.php @@ -0,0 +1,12 @@ +group(module_path('News', 'Routes/web.php')); + } +} diff --git a/Modules/News/Resources/views/admin/form.blade.php b/Modules/News/Resources/views/admin/form.blade.php new file mode 100644 index 0000000..2e122f0 --- /dev/null +++ b/Modules/News/Resources/views/admin/form.blade.php @@ -0,0 +1 @@ +
@csrf @isset($record) @method('PUT') @endisset
@foreach($authors as $author)@endforeach@foreach($categories as $category)@endforeach@foreach(['draft','review','published','archived'] as $status)@endforeach
{{ old('excerpt',$record->excerpt ?? '') }}{{ old('content',$record->content ?? '') }}
@foreach($tags as $tag)@endforeach
{{ old('meta_description',$record->meta_description ?? '') }}Save
diff --git a/Modules/News/Resources/views/admin/index.blade.php b/Modules/News/Resources/views/admin/index.blade.php new file mode 100644 index 0000000..f8860b7 --- /dev/null +++ b/Modules/News/Resources/views/admin/index.blade.php @@ -0,0 +1 @@ +

News

Manage publishing news.

Create
@if(session('status'))
{{ session('status') }}
@endif
@forelse($records as $record)@empty@endforelse
Name / titleStatusActions
{{ $record->title ?? $record->name }}{{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }}Edit
@csrf @method('DELETE')Delete
No records yet.
{{ $records->links() }}
diff --git a/Modules/News/Resources/views/public/index.blade.php b/Modules/News/Resources/views/public/index.blade.php new file mode 100644 index 0000000..e0a05ca --- /dev/null +++ b/Modules/News/Resources/views/public/index.blade.php @@ -0,0 +1 @@ +

News

@foreach($records as $record)@endforeach
{{ $records->links() }}
diff --git a/Modules/News/Resources/views/public/show.blade.php b/Modules/News/Resources/views/public/show.blade.php new file mode 100644 index 0000000..882c377 --- /dev/null +++ b/Modules/News/Resources/views/public/show.blade.php @@ -0,0 +1 @@ +

{{ $record->title }}

{{ optional($record->published_at)->format('j F Y') }}

{!! nl2br(e($record->content)) !!}
diff --git a/Modules/News/Routes/web.php b/Modules/News/Routes/web.php new file mode 100644 index 0000000..3ff96fd --- /dev/null +++ b/Modules/News/Routes/web.php @@ -0,0 +1,4 @@ +prefix('administration/news')->name('administration.news.')->group(function (): void { Route::resource('/', NewsController::class)->parameters([''=>'news_article'])->except('show'); }); +Route::get('/news',[PublicNewsController::class,'index'])->name('news.public.index'); Route::get('/news/{slug}',[PublicNewsController::class,'show'])->name('news.public.show'); diff --git a/Modules/News/Tests/Feature/ModuleBootTest.php b/Modules/News/Tests/Feature/ModuleBootTest.php new file mode 100644 index 0000000..443ef12 --- /dev/null +++ b/Modules/News/Tests/Feature/ModuleBootTest.php @@ -0,0 +1,10 @@ +assertSame('News', config('news.name')); + } +} diff --git a/Modules/News/module.json b/Modules/News/module.json new file mode 100644 index 0000000..dcb3490 --- /dev/null +++ b/Modules/News/module.json @@ -0,0 +1,12 @@ +{ + "name": "News", + "alias": "news", + "version": "1.0.0", + "description": "News publishing module.", + "keywords": ["publishing", "news"], + "priority": 30, + "providers": ["Modules\\News\\Providers\\NewsServiceProvider"], + "aliases": {}, + "files": [], + "requires": ["Core", "Administration"] +} diff --git a/Modules/Pages/Database/Migrations/2026_07_24_000100_create_pages_table.php b/Modules/Pages/Database/Migrations/2026_07_24_000100_create_pages_table.php index 6095217..d4cb696 100644 --- a/Modules/Pages/Database/Migrations/2026_07_24_000100_create_pages_table.php +++ b/Modules/Pages/Database/Migrations/2026_07_24_000100_create_pages_table.php @@ -4,8 +4,12 @@ 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('pages', function (Blueprint $table): void { $table->id(); +return new class extends Migration +{ + public function up(): void + { + Schema::create('pages', function (Blueprint $table): void { + $table->id(); $table->string('title'); $table->string('slug'); $table->text('summary')->nullable(); @@ -13,6 +17,13 @@ return new class extends Migration { $table->string('status')->default('draft'); $table->timestamp('published_at')->nullable(); $table->boolean('is_home')->default(false); - $table->timestamps(); $table->softDeletes(); }); } - public function down(): void { Schema::dropIfExists('pages'); } + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down(): void + { + Schema::dropIfExists('pages'); + } }; diff --git a/Modules/Pages/Database/Migrations/2026_07_24_030700_enhance_pages_for_publishing.php b/Modules/Pages/Database/Migrations/2026_07_24_030700_enhance_pages_for_publishing.php new file mode 100644 index 0000000..c23f1fd --- /dev/null +++ b/Modules/Pages/Database/Migrations/2026_07_24_030700_enhance_pages_for_publishing.php @@ -0,0 +1,3 @@ +string('template')->default('default'); $table->timestamp('expires_at')->nullable(); $table->string('preview_token',64)->nullable()->unique(); $table->foreignId('locked_by')->nullable()->constrained('users')->nullOnDelete(); $table->timestamp('locked_at')->nullable(); }); Schema::create('page_revisions', function(Blueprint $table): void { $table->id(); $table->foreignId('page_id')->constrained('pages')->cascadeOnDelete(); $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); $table->string('title'); $table->longText('content')->nullable(); $table->json('snapshot'); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('page_revisions'); Schema::table('pages', function(Blueprint $table): void { $table->dropConstrainedForeignId('locked_by'); $table->dropColumn(['template','expires_at','preview_token','locked_at']); }); } }; diff --git a/Modules/Pages/Support/ModuleInfo.php b/Modules/Pages/Support/ModuleInfo.php deleted file mode 100644 index b0c7b4a..0000000 --- a/Modules/Pages/Support/ModuleInfo.php +++ /dev/null @@ -1,5 +0,0 @@ - 'Sitemap']; diff --git a/Modules/Sitemap/Http/Controllers/SitemapController.php b/Modules/Sitemap/Http/Controllers/SitemapController.php new file mode 100644 index 0000000..004ce05 --- /dev/null +++ b/Modules/Sitemap/Http/Controllers/SitemapController.php @@ -0,0 +1,3 @@ +push(['loc'=>url('/'),'updated_at'=>now()]); Page::query()->where('status','published')->get()->each(fn($r)=>$urls->push(['loc'=>route('pages.public.show',$r->slug),'updated_at'=>$r->updated_at])); Blog::published()->get()->each(fn($r)=>$urls->push(['loc'=>route('blog.public.show',$r->slug),'updated_at'=>$r->updated_at])); News::published()->get()->each(fn($r)=>$urls->push(['loc'=>route('news.public.show',$r->slug),'updated_at'=>$r->updated_at])); return response()->view('sitemap::index',compact('urls'))->header('Content-Type','application/xml; charset=UTF-8'); } } diff --git a/Modules/Sitemap/Providers/RouteServiceProvider.php b/Modules/Sitemap/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..ad8dfc3 --- /dev/null +++ b/Modules/Sitemap/Providers/RouteServiceProvider.php @@ -0,0 +1,12 @@ +group(module_path('Sitemap', 'Routes/web.php')); + } +} diff --git a/Modules/Sitemap/Providers/SitemapServiceProvider.php b/Modules/Sitemap/Providers/SitemapServiceProvider.php new file mode 100644 index 0000000..b734e5d --- /dev/null +++ b/Modules/Sitemap/Providers/SitemapServiceProvider.php @@ -0,0 +1,16 @@ +app->register(RouteServiceProvider::class); + $this->mergeConfigFrom(module_path('Sitemap', 'Config/config.php'), 'sitemap'); + } + public function boot(): void + { + $this->loadMigrationsFrom(module_path('Sitemap', 'Database/Migrations')); + $this->loadViewsFrom(module_path('Sitemap', 'Resources/views'), 'sitemap'); + } +} diff --git a/Modules/Sitemap/Resources/views/index.blade.php b/Modules/Sitemap/Resources/views/index.blade.php new file mode 100644 index 0000000..8c513e9 --- /dev/null +++ b/Modules/Sitemap/Resources/views/index.blade.php @@ -0,0 +1 @@ +{!! '' !!}@foreach($urls as $url){{ $url['loc'] }}{{ $url['updated_at']->toAtomString() }}@endforeach diff --git a/Modules/Sitemap/Routes/web.php b/Modules/Sitemap/Routes/web.php new file mode 100644 index 0000000..da78f26 --- /dev/null +++ b/Modules/Sitemap/Routes/web.php @@ -0,0 +1,2 @@ +name('sitemap.index'); diff --git a/Modules/Sitemap/Tests/Feature/ModuleBootTest.php b/Modules/Sitemap/Tests/Feature/ModuleBootTest.php new file mode 100644 index 0000000..aebeb40 --- /dev/null +++ b/Modules/Sitemap/Tests/Feature/ModuleBootTest.php @@ -0,0 +1,10 @@ +assertSame('Sitemap', config('sitemap.name')); + } +} diff --git a/Modules/Sitemap/module.json b/Modules/Sitemap/module.json new file mode 100644 index 0000000..ecf9761 --- /dev/null +++ b/Modules/Sitemap/module.json @@ -0,0 +1,12 @@ +{ + "name": "Sitemap", + "alias": "sitemap", + "version": "1.0.0", + "description": "Sitemap publishing module.", + "keywords": ["publishing", "sitemap"], + "priority": 30, + "providers": ["Modules\\Sitemap\\Providers\\SitemapServiceProvider"], + "aliases": {}, + "files": [], + "requires": ["Core", "Administration"] +} diff --git a/Modules/Tags/Config/config.php b/Modules/Tags/Config/config.php new file mode 100644 index 0000000..75e8829 --- /dev/null +++ b/Modules/Tags/Config/config.php @@ -0,0 +1,2 @@ + 'Tags']; diff --git a/Modules/Tags/Database/Migrations/2026_07_24_030200_create_tags_table.php b/Modules/Tags/Database/Migrations/2026_07_24_030200_create_tags_table.php new file mode 100644 index 0000000..b4fc39a --- /dev/null +++ b/Modules/Tags/Database/Migrations/2026_07_24_030200_create_tags_table.php @@ -0,0 +1,3 @@ +id(); $table->string('name'); $table->string('slug')->unique(); $table->text('description')->nullable(); $table->unsignedInteger('usage_count')->default(0); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('tags'); } }; diff --git a/Modules/Tags/Http/Controllers/TagController.php b/Modules/Tags/Http/Controllers/TagController.php new file mode 100644 index 0000000..a32d431 --- /dev/null +++ b/Modules/Tags/Http/Controllers/TagController.php @@ -0,0 +1,4 @@ +Tag::orderByDesc('usage_count')->paginate(30)]); } public function create(): View { return view('tags::admin.form'); } public function store(Request $request): RedirectResponse { Tag::create($request->validate(['name'=>['required','string','max:255'],'slug'=>['required','string','max:255','unique:tags,slug'],'description'=>['nullable','string']])); return redirect()->route('administration.tags.index')->with('status','Tag created.'); } public function edit(Tag $tag): View { return view('tags::admin.form',['record'=>$tag]); } public function update(Request $request, Tag $tag): RedirectResponse { $tag->update($request->validate(['name'=>['required','string','max:255'],'slug'=>['required','string','max:255','unique:tags,slug,'.$tag->id],'description'=>['nullable','string']])); return redirect()->route('administration.tags.index')->with('status','Tag updated.'); } public function destroy(Tag $tag): RedirectResponse { $tag->delete(); return back()->with('status','Tag deleted.'); } } diff --git a/Modules/Tags/Models/Tag.php b/Modules/Tags/Models/Tag.php new file mode 100644 index 0000000..3077f09 --- /dev/null +++ b/Modules/Tags/Models/Tag.php @@ -0,0 +1,3 @@ +'integer']; } } diff --git a/Modules/Tags/Providers/RouteServiceProvider.php b/Modules/Tags/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..4d09871 --- /dev/null +++ b/Modules/Tags/Providers/RouteServiceProvider.php @@ -0,0 +1,12 @@ +group(module_path('Tags', 'Routes/web.php')); + } +} diff --git a/Modules/Tags/Providers/TagsServiceProvider.php b/Modules/Tags/Providers/TagsServiceProvider.php new file mode 100644 index 0000000..5ff8ac6 --- /dev/null +++ b/Modules/Tags/Providers/TagsServiceProvider.php @@ -0,0 +1,16 @@ +app->register(RouteServiceProvider::class); + $this->mergeConfigFrom(module_path('Tags', 'Config/config.php'), 'tags'); + } + public function boot(): void + { + $this->loadMigrationsFrom(module_path('Tags', 'Database/Migrations')); + $this->loadViewsFrom(module_path('Tags', 'Resources/views'), 'tags'); + } +} diff --git a/Modules/Tags/Resources/views/admin/form.blade.php b/Modules/Tags/Resources/views/admin/form.blade.php new file mode 100644 index 0000000..9dfe384 --- /dev/null +++ b/Modules/Tags/Resources/views/admin/form.blade.php @@ -0,0 +1 @@ +
@csrf @isset($record) @method('PUT') @endisset {{ old('description',$record->description ?? '') }}Save
diff --git a/Modules/Tags/Resources/views/admin/index.blade.php b/Modules/Tags/Resources/views/admin/index.blade.php new file mode 100644 index 0000000..537b5d9 --- /dev/null +++ b/Modules/Tags/Resources/views/admin/index.blade.php @@ -0,0 +1 @@ +

Tags

Manage publishing tags.

Create
@if(session('status'))
{{ session('status') }}
@endif
@forelse($records as $record)@empty@endforelse
Name / titleStatusActions
{{ $record->title ?? $record->name }}{{ $record->status ?? ($record->is_active ? 'Active' : 'Inactive') }}Edit
@csrf @method('DELETE')Delete
No records yet.
{{ $records->links() }}
diff --git a/Modules/Tags/Routes/web.php b/Modules/Tags/Routes/web.php new file mode 100644 index 0000000..3face93 --- /dev/null +++ b/Modules/Tags/Routes/web.php @@ -0,0 +1,3 @@ +prefix('administration/tags')->name('administration.tags.')->group(function (): void { Route::resource('/', TagController::class)->parameters([''=>'tag'])->except('show'); }); diff --git a/Modules/Tags/Tests/Feature/ModuleBootTest.php b/Modules/Tags/Tests/Feature/ModuleBootTest.php new file mode 100644 index 0000000..2d9807c --- /dev/null +++ b/Modules/Tags/Tests/Feature/ModuleBootTest.php @@ -0,0 +1,10 @@ +assertSame('Tags', config('tags.name')); + } +} diff --git a/Modules/Tags/module.json b/Modules/Tags/module.json new file mode 100644 index 0000000..53001bb --- /dev/null +++ b/Modules/Tags/module.json @@ -0,0 +1,12 @@ +{ + "name": "Tags", + "alias": "tags", + "version": "1.0.0", + "description": "Tags publishing module.", + "keywords": ["publishing", "tags"], + "priority": 30, + "providers": ["Modules\\Tags\\Providers\\TagsServiceProvider"], + "aliases": {}, + "files": [], + "requires": ["Core", "Administration"] +} diff --git a/modules_statuses.json b/modules_statuses.json index 31f9129..842b241 100644 --- a/modules_statuses.json +++ b/modules_statuses.json @@ -1,21 +1,5 @@ { - "Stylist": true, - "Core": true, - "Administration": true, - "Users": true, - "Roles": true, - "Settings": true, - "Audit": true, - "Media": true, - "Seo": true, - "Search": true, - "Pages": true, - "Menus": true, - "Redirects": true, - "Forms": true, - "Documents": true, - "Banners": true, - "Faq": true, - "Team": true, - "Testimonials": true + "Stylist": true, "Core": true, "Administration": true, "Users": true, "Roles": true, "Settings": true, "Audit": true, "Media": true, "Seo": true, "Search": true, + "Pages": true, "Menus": true, "Redirects": true, "Forms": true, "Documents": true, "Banners": true, "Faq": true, "Team": true, "Testimonials": true, + "Categories": true, "Tags": true, "Authors": true, "Blog": true, "News": true, "Comments": true, "Feeds": true, "Sitemap": true } diff --git a/resources/themes/common/views/components/layouts/app/sidebar.blade.php b/resources/themes/common/views/components/layouts/app/sidebar.blade.php index 39773a2..a65cdb4 100644 --- a/resources/themes/common/views/components/layouts/app/sidebar.blade.php +++ b/resources/themes/common/views/components/layouts/app/sidebar.blade.php @@ -1,118 +1,35 @@ - - @include('stylist-theme-common::partials.head') - - - - - - - - - - - - {{ __('Pages') }} - {{ __('Menus') }} - {{ __('Redirects') }} - {{ __('Forms') }} - {{ __('Documents') }} - {{ __('Banners') }} - {{ __('FAQ') }} - {{ __('Team') }} - {{ __('Testimonials') }} - - - - {{ __('Dashboard') }} - {{ __('Administration') }} - - - - {{ __('Users') }} - {{ __('Roles & Permissions') }} - {{ __('Media') }} - {{ __('Search') }} - - - - {{ __('SEO') }} - {{ __('Settings') }} - {{ __('Modules') }} - {{ __('Audit Log') }} - - - - - - - - - - - - - - - - - - - -
-
- - -
- {{ auth()->user()->name }} - {{ auth()->user()->email }} -
-
-
-
- - - - - - {{ __('Settings') }} - - - - - -
- @csrf - - {{ __('Log out') }} - -
-
-
-
- - {{ $slot }} - - @persist('toast') - - - - @endpersist - - @fluxScripts - - +@include('stylist-theme-common::partials.head') + + + + + +{{ __('Blog') }} +{{ __('News') }} +{{ __('Categories') }} +{{ __('Tags') }} +{{ __('Authors') }} +{{ __('Comments') }} + + +{{ __('Pages') }} +{{ __('Menus') }} +{{ __('Redirects') }} +{{ __('Forms') }} +{{ __('Documents') }} +{{ __('Banners') }} +{{ __('FAQ') }} +{{ __('Team') }} +{{ __('Testimonials') }} + +{{ __('Dashboard') }}{{ __('Administration') }} +{{ __('Users') }}{{ __('Roles & Permissions') }}{{ __('Media') }}{{ __('Search') }} +{{ __('SEO') }}{{ __('Settings') }}{{ __('Modules') }}{{ __('Audit Log') }} + +
{{ auth()->user()->name }}{{ auth()->user()->email }}
{{ __('Settings') }}
@csrf{{ __('Log out') }}
+{{ $slot }} +@persist('toast')@endpersist +@fluxScripts +