change posts to articles
This commit is contained in:
@@ -13,13 +13,13 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\Request;
|
||||
use LogicException;
|
||||
|
||||
class PostConductor extends Conductor
|
||||
class ArticleConductor extends Conductor
|
||||
{
|
||||
/**
|
||||
* The Model Class
|
||||
* @var string
|
||||
*/
|
||||
protected $class = '\App\Models\Post';
|
||||
protected $class = '\App\Models\Article';
|
||||
|
||||
/**
|
||||
* The default sorting field
|
||||
@@ -44,7 +44,7 @@ class PostConductor extends Conductor
|
||||
public function scope(Builder $builder)
|
||||
{
|
||||
$user = auth()->user();
|
||||
if ($user === null || $user->hasPermission('admin/posts') === false) {
|
||||
if ($user === null || $user->hasPermission('admin/articles') === false) {
|
||||
$builder
|
||||
->where('publish_at', '<=', now());
|
||||
}
|
||||
@@ -60,7 +60,7 @@ class PostConductor extends Conductor
|
||||
{
|
||||
if (Carbon::parse($model->publish_at)->isFuture() === true) {
|
||||
$user = auth()->user();
|
||||
if ($user === null || $user->hasPermission('admin/posts') === false) {
|
||||
if ($user === null || $user->hasPermission('admin/articles') === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@ class PostConductor extends Conductor
|
||||
public static function creatable()
|
||||
{
|
||||
$user = auth()->user();
|
||||
return ($user !== null && $user->hasPermission('admin/posts') === true);
|
||||
return ($user !== null && $user->hasPermission('admin/articles') === true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,7 +88,7 @@ class PostConductor extends Conductor
|
||||
public static function updatable(Model $model)
|
||||
{
|
||||
$user = auth()->user();
|
||||
return ($user !== null && $user->hasPermission('admin/posts') === true);
|
||||
return ($user !== null && $user->hasPermission('admin/articles') === true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,7 +100,7 @@ class PostConductor extends Conductor
|
||||
public static function destroyable(Model $model)
|
||||
{
|
||||
$user = auth()->user();
|
||||
return ($user !== null && $user->hasPermission('admin/posts') === true);
|
||||
return ($user !== null && $user->hasPermission('admin/articles') === true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3,11 +3,11 @@
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Conductors\MediaConductor;
|
||||
use App\Conductors\PostConductor;
|
||||
use App\Conductors\ArticleConductor;
|
||||
use App\Enum\HttpResponseCodes;
|
||||
use App\Http\Requests\PostRequest;
|
||||
use App\Http\Requests\ArticleRequest;
|
||||
use App\Models\Media;
|
||||
use App\Models\Post;
|
||||
use App\Models\Article;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Carbon\Exceptions\InvalidFormatException;
|
||||
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||
@@ -15,7 +15,7 @@ use Illuminate\Database\Eloquent\InvalidCastException;
|
||||
use Illuminate\Database\Eloquent\MassAssignmentException;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PostController extends ApiController
|
||||
class ArticleController extends ApiController
|
||||
{
|
||||
/**
|
||||
* ApplicationController constructor.
|
||||
@@ -38,12 +38,13 @@ class PostController extends ApiController
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
list($collection, $total) = PostConductor::request($request);
|
||||
list($collection, $total) = ArticleConductor::request($request);
|
||||
|
||||
return $this->respondAsResource(
|
||||
$collection,
|
||||
['isCollection' => true,
|
||||
'appendData' => ['total' => $total]]
|
||||
'appendData' => ['total' => $total]
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,13 +52,13 @@ class PostController extends ApiController
|
||||
* Display the specified resource.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request The endpoint request.
|
||||
* @param \App\Models\Post $post The post model.
|
||||
* @param \App\Models\Article $article The article model.
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function show(Request $request, Post $post)
|
||||
public function show(Request $request, Article $article)
|
||||
{
|
||||
if (PostConductor::viewable($post) === true) {
|
||||
return $this->respondAsResource(PostConductor::model($request, $post));
|
||||
if (ArticleConductor::viewable($article) === true) {
|
||||
return $this->respondAsResource(ArticleConductor::model($request, $article));
|
||||
}
|
||||
|
||||
return $this->respondForbidden();
|
||||
@@ -66,15 +67,15 @@ class PostController extends ApiController
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
* @param \App\Http\Requests\PostRequest $request The user request.
|
||||
* @param \App\Http\Requests\ArticleRequest $request The user request.
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function store(PostRequest $request)
|
||||
public function store(ArticleRequest $request)
|
||||
{
|
||||
if (PostConductor::creatable() === true) {
|
||||
$post = Post::create($request->all());
|
||||
if (ArticleConductor::creatable() === true) {
|
||||
$article = Article::create($request->all());
|
||||
return $this->respondAsResource(
|
||||
PostConductor::model($request, $post),
|
||||
ArticleConductor::model($request, $article),
|
||||
['respondCode' => HttpResponseCodes::HTTP_CREATED]
|
||||
);
|
||||
} else {
|
||||
@@ -85,15 +86,15 @@ class PostController extends ApiController
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
* @param \App\Http\Requests\PostRequest $request The post update request.
|
||||
* @param \App\Models\Post $post The specified post.
|
||||
* @param \App\Http\Requests\ArticleRequest $request The article update request.
|
||||
* @param \App\Models\Article $article The specified article.
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function update(PostRequest $request, Post $post)
|
||||
public function update(ArticleRequest $request, Article $article)
|
||||
{
|
||||
if (PostConductor::updatable($post) === true) {
|
||||
$post->update($request->all());
|
||||
return $this->respondAsResource(PostConductor::model($request, $post));
|
||||
if (ArticleConductor::updatable($article) === true) {
|
||||
$article->update($request->all());
|
||||
return $this->respondAsResource(ArticleConductor::model($request, $article));
|
||||
}
|
||||
|
||||
return $this->respondForbidden();
|
||||
@@ -102,13 +103,13 @@ class PostController extends ApiController
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param \App\Models\Post $post The specified post.
|
||||
* @param \App\Models\Article $article The specified article.
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function destroy(Post $post)
|
||||
public function destroy(Article $article)
|
||||
{
|
||||
if (PostConductor::destroyable($post) === true) {
|
||||
$post->delete();
|
||||
if (ArticleConductor::destroyable($article) === true) {
|
||||
$article->delete();
|
||||
return $this->respondNoContent();
|
||||
} else {
|
||||
return $this->respondForbidden();
|
||||
@@ -119,16 +120,16 @@ class PostController extends ApiController
|
||||
* Get a list of attachments related to this model.
|
||||
*
|
||||
* @param Request $request The user request.
|
||||
* @param Post $post The post model.
|
||||
* @return JsonResponse Returns the post attachments.
|
||||
* @param Article $article The article model.
|
||||
* @return JsonResponse Returns the article attachments.
|
||||
* @throws InvalidFormatException
|
||||
* @throws BindingResolutionException
|
||||
* @throws InvalidCastException
|
||||
*/
|
||||
public function getAttachments(Request $request, Post $post)
|
||||
public function getAttachments(Request $request, Article $article)
|
||||
{
|
||||
if (PostConductor::viewable($post) === true) {
|
||||
$medium = $post->attachments->map(function ($attachment) {
|
||||
if (ArticleConductor::viewable($article) === true) {
|
||||
$medium = $article->attachments->map(function ($attachment) {
|
||||
return $attachment->media;
|
||||
});
|
||||
|
||||
@@ -142,16 +143,16 @@ class PostController extends ApiController
|
||||
* Store an attachment related to this model.
|
||||
*
|
||||
* @param Request $request The user request.
|
||||
* @param Post $post The post model.
|
||||
* @param Article $article The article model.
|
||||
* @return JsonResponse The response.
|
||||
* @throws BindingResolutionException
|
||||
* @throws MassAssignmentException
|
||||
*/
|
||||
public function storeAttachment(Request $request, Post $post)
|
||||
public function storeAttachment(Request $request, Article $article)
|
||||
{
|
||||
if (PostConductor::updatable($post) === true) {
|
||||
if($request->has("medium") && Media::find($request->medium)) {
|
||||
$post->attachments()->create(['media_id' => $request->medium]);
|
||||
if (ArticleConductor::updatable($article) === true) {
|
||||
if ($request->has("medium") && Media::find($request->medium)) {
|
||||
$article->attachments()->create(['media_id' => $request->medium]);
|
||||
return $this->respondCreated();
|
||||
}
|
||||
|
||||
@@ -165,21 +166,21 @@ class PostController extends ApiController
|
||||
* Update/replace attachments related to this model.
|
||||
*
|
||||
* @param Request $request The user request.
|
||||
* @param Post $post The related model.
|
||||
* @param Article $article The related model.
|
||||
* @return JsonResponse
|
||||
* @throws BindingResolutionException
|
||||
* @throws MassAssignmentException
|
||||
*/
|
||||
public function updateAttachments(Request $request, Post $post)
|
||||
public function updateAttachments(Request $request, Article $article)
|
||||
{
|
||||
if (PostConductor::updatable($post) === true) {
|
||||
if (ArticleConductor::updatable($article) === true) {
|
||||
$mediaIds = $request->attachments;
|
||||
if(is_array($mediaIds) === false) {
|
||||
if (is_array($mediaIds) === false) {
|
||||
$mediaIds = explode(',', $request->attachments);
|
||||
}
|
||||
|
||||
$mediaIds = array_map('trim', $mediaIds); // trim each media ID
|
||||
$attachments = $post->attachments;
|
||||
$attachments = $article->attachments;
|
||||
|
||||
// Delete attachments that are not in $mediaIds
|
||||
foreach ($attachments as $attachment) {
|
||||
@@ -188,7 +189,7 @@ class PostController extends ApiController
|
||||
}
|
||||
}
|
||||
|
||||
// Create new attachments for media IDs that are not already in $post->attachments()
|
||||
// Create new attachments for media IDs that are not already in $article->attachments()
|
||||
foreach ($mediaIds as $mediaId) {
|
||||
$found = false;
|
||||
|
||||
@@ -200,12 +201,12 @@ class PostController extends ApiController
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
$post->attachments()->create(['media_id' => $mediaId]);
|
||||
$article->attachments()->create(['media_id' => $mediaId]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->respondNoContent();
|
||||
}
|
||||
}//end if
|
||||
|
||||
return $this->respondForbidden();
|
||||
}
|
||||
@@ -213,15 +214,15 @@ class PostController extends ApiController
|
||||
/**
|
||||
* Delete a specific related attachment.
|
||||
* @param Request $request The user request.
|
||||
* @param Post $post The model.
|
||||
* @param Article $article The model.
|
||||
* @param Media $medium The attachment medium.
|
||||
* @return JsonResponse
|
||||
* @throws BindingResolutionException
|
||||
*/
|
||||
public function deleteAttachment(Request $request, Post $post, Media $medium)
|
||||
public function deleteAttachment(Request $request, Article $article, Media $medium)
|
||||
{
|
||||
if (PostConductor::updatable($post) === true) {
|
||||
$attachments = $post->attachments;
|
||||
if (ArticleConductor::updatable($article) === true) {
|
||||
$attachments = $article->attachments;
|
||||
$deleted = false;
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
@@ -111,8 +111,8 @@ class EventController extends ApiController
|
||||
* Get a list of attachments related to this model.
|
||||
*
|
||||
* @param Request $request The user request.
|
||||
* @param Post $post The post model.
|
||||
* @return JsonResponse Returns the post attachments.
|
||||
* @param Article $article The article model.
|
||||
* @return JsonResponse Returns the article attachments.
|
||||
* @throws InvalidFormatException
|
||||
* @throws BindingResolutionException
|
||||
* @throws InvalidCastException
|
||||
@@ -134,7 +134,7 @@ class EventController extends ApiController
|
||||
* Store an attachment related to this model.
|
||||
*
|
||||
* @param Request $request The user request.
|
||||
* @param Post $post The post model.
|
||||
* @param Article $article The article model.
|
||||
* @return JsonResponse The response.
|
||||
* @throws BindingResolutionException
|
||||
* @throws MassAssignmentException
|
||||
@@ -157,7 +157,7 @@ class EventController extends ApiController
|
||||
* Update/replace attachments related to this model.
|
||||
*
|
||||
* @param Request $request The user request.
|
||||
* @param Post $post The related model.
|
||||
* @param Article $article The related model.
|
||||
* @return JsonResponse
|
||||
* @throws BindingResolutionException
|
||||
* @throws MassAssignmentException
|
||||
@@ -180,7 +180,7 @@ class EventController extends ApiController
|
||||
}
|
||||
}
|
||||
|
||||
// Create new attachments for media IDs that are not already in $post->attachments()
|
||||
// Create new attachments for media IDs that are not already in $article->attachments()
|
||||
foreach ($mediaIds as $mediaId) {
|
||||
$found = false;
|
||||
|
||||
@@ -205,7 +205,7 @@ class EventController extends ApiController
|
||||
/**
|
||||
* Delete a specific related attachment.
|
||||
* @param Request $request The user request.
|
||||
* @param Post $post The model.
|
||||
* @param Article $article The model.
|
||||
* @param Media $medium The attachment medium.
|
||||
* @return JsonResponse
|
||||
* @throws BindingResolutionException
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class PostRequest extends BaseRequest
|
||||
class ArticleRequest extends BaseRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to POST requests.
|
||||
@@ -14,7 +14,7 @@ class PostRequest extends BaseRequest
|
||||
public function postRules()
|
||||
{
|
||||
return [
|
||||
'slug' => 'required|string|min:6|unique:posts',
|
||||
'slug' => 'required|string|min:6|unique:articles',
|
||||
'title' => 'required|string|min:6|max:255',
|
||||
'publish_at' => 'required|date',
|
||||
'user_id' => 'required|uuid|exists:users,id',
|
||||
@@ -34,7 +34,7 @@ class PostRequest extends BaseRequest
|
||||
'slug' => [
|
||||
'string',
|
||||
'min:6',
|
||||
Rule::unique('posts')->ignoreModel($this->post),
|
||||
Rule::unique('articles')->ignoreModel($this->article),
|
||||
],
|
||||
'title' => 'string|min:6|max:255',
|
||||
'publish_at' => 'date',
|
||||
@@ -7,7 +7,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
|
||||
class Post extends Model
|
||||
class Article extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use Uuids;
|
||||
@@ -28,7 +28,7 @@ class Post extends Model
|
||||
|
||||
|
||||
/**
|
||||
* Get the post user
|
||||
* Get the article user
|
||||
*
|
||||
* @return BelongsTo
|
||||
*/
|
||||
@@ -38,7 +38,7 @@ class Post extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all of the post's attachments.
|
||||
* Get all of the article's attachments.
|
||||
*
|
||||
* @return MorphMany
|
||||
*/
|
||||
@@ -34,7 +34,7 @@ class Event extends Model
|
||||
|
||||
|
||||
/**
|
||||
* Get all of the post's attachments.
|
||||
* Get all of the article's attachments.
|
||||
*/
|
||||
public function attachments()
|
||||
{
|
||||
|
||||
@@ -142,7 +142,7 @@ class User extends Authenticatable implements Auditable
|
||||
* Revoke permissions from the user
|
||||
*
|
||||
* @param string|array $permissions The permission(s) to revoke.
|
||||
* @return int
|
||||
* @return integer
|
||||
*/
|
||||
public function revokePermission($permissions)
|
||||
{
|
||||
@@ -170,9 +170,9 @@ class User extends Authenticatable implements Auditable
|
||||
*
|
||||
* @return HasMany
|
||||
*/
|
||||
public function posts()
|
||||
public function articles()
|
||||
{
|
||||
return $this->hasMany(Post::class);
|
||||
return $this->hasMany(Article::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Event>
|
||||
*/
|
||||
class PostFactory extends Factory
|
||||
class ArticleFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
35
database/migrations/2023_04_25_235615_update_posts_table.php
Normal file
35
database/migrations/2023_04_25_235615_update_posts_table.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::rename('posts', 'articles');
|
||||
|
||||
// Update permissions to use articles instead of posts
|
||||
DB::table('permissions')->select('id', 'permission')->where('permission', 'admin/posts')->update(['permission' => 'admin/articles']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::rename('articles', 'posts');
|
||||
|
||||
// Update permissions to use posts instead of articles
|
||||
DB::table('permissions')->select('id', 'permission')->where('permission', 'admin/articles')->update(['permission' => 'admin/posts']);
|
||||
}
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB |
@@ -33,7 +33,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { api, getApiResultData } from "../helpers/api";
|
||||
import { PostCollection } from "../helpers/api.types";
|
||||
import { ArticleCollection } from "../helpers/api.types";
|
||||
import { mediaGetVariantUrl } from "../helpers/media";
|
||||
import { excerpt } from "../helpers/string";
|
||||
import SMButton from "./SMButton.vue";
|
||||
@@ -70,30 +70,31 @@ onBeforeUnmount(() => {
|
||||
|
||||
const handleLoad = async () => {
|
||||
try {
|
||||
let postsResult = await api.get({
|
||||
url: "/posts",
|
||||
let articlesResult = await api.get({
|
||||
url: "/articles",
|
||||
params: {
|
||||
limit: 3,
|
||||
},
|
||||
});
|
||||
|
||||
const postsData = getApiResultData<PostCollection>(postsResult);
|
||||
const articlesData =
|
||||
getApiResultData<ArticleCollection>(articlesResult);
|
||||
|
||||
if (postsData && postsData.posts) {
|
||||
if (articlesData && articlesData.articles) {
|
||||
const randomIndex = Math.floor(
|
||||
Math.random() * postsData.posts.length
|
||||
Math.random() * articlesData.articles.length
|
||||
);
|
||||
heroTitle.value = postsData.posts[randomIndex].title;
|
||||
heroTitle.value = articlesData.articles[randomIndex].title;
|
||||
heroExcerpt.value = excerpt(
|
||||
postsData.posts[randomIndex].content,
|
||||
articlesData.articles[randomIndex].content,
|
||||
200
|
||||
);
|
||||
heroImageUrl.value = mediaGetVariantUrl(
|
||||
postsData.posts[randomIndex].hero,
|
||||
articlesData.articles[randomIndex].hero,
|
||||
"large"
|
||||
);
|
||||
heroImageTitle = postsData.posts[randomIndex].hero.title;
|
||||
heroSlug.value = postsData.posts[randomIndex].slug;
|
||||
heroImageTitle = articlesData.articles[randomIndex].hero.title;
|
||||
heroSlug.value = articlesData.articles[randomIndex].slug;
|
||||
|
||||
heroStyles.value.backgroundImage = `linear-gradient(to right, rgba(0, 0, 0, 0.7),rgba(0, 0, 0, 0.2)),url('${heroImageUrl.value}')`;
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ export interface Article {
|
||||
attachments: Array<Media>;
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
export interface Article {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
@@ -80,12 +80,12 @@ export interface Post {
|
||||
attachments: Array<Media>;
|
||||
}
|
||||
|
||||
export interface PostResponse {
|
||||
post: Post;
|
||||
export interface ArticleResponse {
|
||||
article: Article;
|
||||
}
|
||||
|
||||
export interface PostCollection {
|
||||
posts: Array<Post>;
|
||||
export interface ArticleCollection {
|
||||
articles: Array<Article>;
|
||||
total: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -197,37 +197,37 @@ export const routes = [
|
||||
component: () => import("@/views/dashboard/Dashboard.vue"),
|
||||
},
|
||||
{
|
||||
path: "posts",
|
||||
path: "articles",
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "dashboard-post-list",
|
||||
name: "dashboard-article-list",
|
||||
meta: {
|
||||
title: "Posts",
|
||||
title: "Articles",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/PostList.vue"),
|
||||
import("@/views/dashboard/ArticleList.vue"),
|
||||
},
|
||||
{
|
||||
path: "create",
|
||||
name: "dashboard-post-create",
|
||||
name: "dashboard-article-create",
|
||||
meta: {
|
||||
title: "Create Post",
|
||||
title: "Create Article",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/PostEdit.vue"),
|
||||
import("@/views/dashboard/ArticleEdit.vue"),
|
||||
},
|
||||
{
|
||||
path: ":id",
|
||||
name: "dashboard-post-edit",
|
||||
name: "dashboard-article-edit",
|
||||
meta: {
|
||||
title: "Edit Post",
|
||||
title: "Edit Article",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
import("@/views/dashboard/PostEdit.vue"),
|
||||
import("@/views/dashboard/ArticleEdit.vue"),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -258,7 +258,7 @@ export const routes = [
|
||||
path: ":id",
|
||||
name: "dashboard-event-edit",
|
||||
meta: {
|
||||
title: "Event Post",
|
||||
title: "Event",
|
||||
middleware: "authenticated",
|
||||
},
|
||||
component: () =>
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
class="thumbnail"
|
||||
:style="{ backgroundImage: `url('${backgroundImageUrl}')` }"></div>
|
||||
<SMContainer narrow>
|
||||
<h1 class="title">{{ post.title }}</h1>
|
||||
<div class="author">By {{ post.user.username }}</div>
|
||||
<div class="date">{{ formattedDate(post.publish_at) }}</div>
|
||||
<SMHTML :html="post.content" class="content" />
|
||||
<SMAttachments :attachments="post.attachments || []" />
|
||||
<h1 class="title">{{ article.title }}</h1>
|
||||
<div class="author">By {{ article.user.username }}</div>
|
||||
<div class="date">{{ formattedDate(article.publish_at) }}</div>
|
||||
<SMHTML :html="article.content" class="content" />
|
||||
<SMAttachments :attachments="article.attachments || []" />
|
||||
</SMContainer>
|
||||
</template>
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useRoute } from "vue-router";
|
||||
import SMAttachments from "../components/SMAttachments.vue";
|
||||
import SMHTML from "../components/SMHTML.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { Post, PostCollection, User } from "../helpers/api.types";
|
||||
import { Article, ArticleCollection, User } from "../helpers/api.types";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import { useApplicationStore } from "../store/ApplicationStore";
|
||||
import { mediaGetVariantUrl } from "../helpers/media";
|
||||
@@ -25,9 +25,9 @@ import { mediaGetVariantUrl } from "../helpers/media";
|
||||
const applicationStore = useApplicationStore();
|
||||
|
||||
/**
|
||||
* The post data.
|
||||
* The article data.
|
||||
*/
|
||||
let post: Ref<Post> = ref({
|
||||
let article: Ref<Article> = ref({
|
||||
title: "",
|
||||
user: { username: "" },
|
||||
});
|
||||
@@ -43,9 +43,9 @@ let pageError = ref(200);
|
||||
let pageLoading = ref(false);
|
||||
|
||||
/**
|
||||
* Post user.
|
||||
* Article user.
|
||||
*/
|
||||
let postUser: User | null = null;
|
||||
let articleUser: User | null = null;
|
||||
|
||||
/**
|
||||
* Thumbnail image URL.
|
||||
@@ -62,25 +62,30 @@ const handleLoad = async () => {
|
||||
try {
|
||||
if (slug.length > 0) {
|
||||
let result = await api.get({
|
||||
url: "/posts/",
|
||||
url: "/articles",
|
||||
params: {
|
||||
slug: `=${slug}`,
|
||||
limit: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const data = result.data as PostCollection;
|
||||
const data = result.data as ArticleCollection;
|
||||
|
||||
if (data && data.posts && data.total && data.total > 0) {
|
||||
post.value = data.posts[0];
|
||||
if (data && data.articles && data.total && data.total > 0) {
|
||||
article.value = data.articles[0];
|
||||
|
||||
post.value.publish_at = new SMDate(post.value.publish_at, {
|
||||
article.value.publish_at = new SMDate(
|
||||
article.value.publish_at,
|
||||
{
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
}).format("yyyy/MM/dd HH:mm:ss");
|
||||
}
|
||||
).format("yyyy/MM/dd HH:mm:ss");
|
||||
|
||||
backgroundImageUrl.value = mediaGetVariantUrl(post.value.hero);
|
||||
applicationStore.setDynamicTitle(post.value.title);
|
||||
backgroundImageUrl.value = mediaGetVariantUrl(
|
||||
article.value.hero
|
||||
);
|
||||
applicationStore.setDynamicTitle(article.value.title);
|
||||
} else {
|
||||
pageError.value = 404;
|
||||
}
|
||||
@@ -140,7 +145,7 @@ handleLoad();
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) {
|
||||
.page-post-view .heading-image {
|
||||
.page-article-view .heading-image {
|
||||
height: #{calc(map-get($spacing, 3) * 10)};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,34 +16,34 @@
|
||||
/></template>
|
||||
</SMInput>
|
||||
<SMLoading v-if="pageLoading" large />
|
||||
<SMNoItems v-else-if="posts.length == 0" text="No Articles Found" />
|
||||
<SMNoItems v-else-if="articles.length == 0" text="No Articles Found" />
|
||||
<template v-else>
|
||||
<SMPagination
|
||||
v-if="postsTotal > postsPerPage"
|
||||
v-model="postsPage"
|
||||
:total="postsTotal"
|
||||
:per-page="postsPerPage" />
|
||||
<div class="posts">
|
||||
v-if="articlesTotal > articlesPerPage"
|
||||
v-model="articlesPage"
|
||||
:total="articlesTotal"
|
||||
:per-page="articlesPerPage" />
|
||||
<div class="articles">
|
||||
<router-link
|
||||
:to="{ name: 'article', params: { slug: post.slug } }"
|
||||
:to="{ name: 'article', params: { slug: article.slug } }"
|
||||
class="article-card"
|
||||
v-for="(post, idx) in posts"
|
||||
v-for="(article, idx) in articles"
|
||||
:key="idx">
|
||||
<div
|
||||
class="thumbnail"
|
||||
:style="{
|
||||
backgroundImage: `url(${mediaGetVariantUrl(
|
||||
post.hero,
|
||||
article.hero,
|
||||
'medium'
|
||||
)})`,
|
||||
}"></div>
|
||||
<div class="info">
|
||||
{{ post.user.display_name }} -
|
||||
{{ computedDate(post.publish_at) }}
|
||||
{{ article.user.display_name }} -
|
||||
{{ computedDate(article.publish_at) }}
|
||||
</div>
|
||||
<h3 class="title">{{ post.title }}</h3>
|
||||
<h3 class="title">{{ article.title }}</h3>
|
||||
<p class="content">
|
||||
{{ excerpt(post.content) }}
|
||||
{{ excerpt(article.content) }}
|
||||
</p>
|
||||
</router-link>
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@
|
||||
import { Ref, ref, watch } from "vue";
|
||||
import SMPagination from "../components/SMPagination.vue";
|
||||
import { api } from "../helpers/api";
|
||||
import { Post, PostCollection } from "../helpers/api.types";
|
||||
import { Article, ArticleCollection } from "../helpers/api.types";
|
||||
import { SMDate } from "../helpers/datetime";
|
||||
import { mediaGetVariantUrl } from "../helpers/media";
|
||||
import SMMastHead from "../components/SMMastHead.vue";
|
||||
@@ -67,16 +67,16 @@ import SMNoItems from "../components/SMNoItems.vue";
|
||||
|
||||
const message = ref("");
|
||||
const pageLoading = ref(true);
|
||||
const posts: Ref<Post[]> = ref([]);
|
||||
const articles: Ref<Article[]> = ref([]);
|
||||
|
||||
const postsPerPage = 24;
|
||||
let postsPage = ref(1);
|
||||
let postsTotal = ref(0);
|
||||
const articlesPerPage = 24;
|
||||
let articlesPage = ref(1);
|
||||
let articlesTotal = ref(0);
|
||||
|
||||
let searchInput = ref("");
|
||||
|
||||
const handleClickSearch = () => {
|
||||
postsPage.value = 1;
|
||||
articlesPage.value = 1;
|
||||
handleLoad();
|
||||
};
|
||||
|
||||
@@ -86,11 +86,11 @@ const handleClickSearch = () => {
|
||||
const handleLoad = () => {
|
||||
message.value = "";
|
||||
pageLoading.value = true;
|
||||
posts.value = [];
|
||||
articles.value = [];
|
||||
|
||||
let params = {
|
||||
limit: postsPerPage,
|
||||
page: postsPage.value,
|
||||
limit: articlesPerPage,
|
||||
page: articlesPage.value,
|
||||
};
|
||||
|
||||
if (searchInput.value.length > 0) {
|
||||
@@ -100,16 +100,16 @@ const handleLoad = () => {
|
||||
}
|
||||
|
||||
api.get({
|
||||
url: "/posts",
|
||||
url: "/articles",
|
||||
params: params,
|
||||
})
|
||||
.then((result) => {
|
||||
const data = result.data as PostCollection;
|
||||
const data = result.data as ArticleCollection;
|
||||
|
||||
posts.value = data.posts;
|
||||
postsTotal.value = data.total;
|
||||
posts.value.forEach((post) => {
|
||||
post.publish_at = new SMDate(post.publish_at, {
|
||||
articles.value = data.articles;
|
||||
articlesTotal.value = data.total;
|
||||
articles.value.forEach((article) => {
|
||||
article.publish_at = new SMDate(article.publish_at, {
|
||||
format: "ymd",
|
||||
utc: true,
|
||||
}).format("yyyy/MM/dd HH:mm:ss");
|
||||
@@ -132,7 +132,7 @@ const computedDate = (date) => {
|
||||
};
|
||||
|
||||
watch(
|
||||
() => postsPage.value,
|
||||
() => articlesPage.value,
|
||||
() => {
|
||||
handleLoad();
|
||||
}
|
||||
@@ -143,7 +143,7 @@ handleLoad();
|
||||
|
||||
<style lang="scss">
|
||||
.page-blog {
|
||||
.posts {
|
||||
.articles {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 30px;
|
||||
@@ -188,13 +188,13 @@ handleLoad();
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.page-blog .posts {
|
||||
.page-blog .articles {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.page-blog .posts {
|
||||
.page-blog .articles {
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<SMPage
|
||||
class="page-post-edit"
|
||||
class="page-article-edit"
|
||||
:page-error="pageError"
|
||||
permission="admin/posts">
|
||||
permission="admin/articles">
|
||||
<template #container>
|
||||
<h1>{{ page_title }}</h1>
|
||||
<SMForm
|
||||
@@ -74,7 +74,7 @@ import SMButtonRow from "../../components/SMButtonRow.vue";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
import SMInputAttachments from "../../components/SMInputAttachments.vue";
|
||||
import { api } from "../../helpers/api";
|
||||
import { PostResponse, UserCollection } from "../../helpers/api.types";
|
||||
import { ArticleResponse, UserCollection } from "../../helpers/api.types";
|
||||
import { SMDate } from "../../helpers/datetime";
|
||||
import { Form, FormControl } from "../../helpers/form";
|
||||
import { And, DateTime, Min, Required } from "../../helpers/validate";
|
||||
@@ -84,7 +84,7 @@ import { useUserStore } from "../../store/UserStore";
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const page_title = route.params.id ? "Edit Post" : "Create New Post";
|
||||
const page_title = route.params.id ? "Edit Article" : "Create New Article";
|
||||
let pageError = ref(200);
|
||||
const authors = ref({});
|
||||
const attachments = ref([]);
|
||||
@@ -122,7 +122,7 @@ const updateSlug = async () => {
|
||||
}
|
||||
|
||||
await api.get({
|
||||
url: "/posts",
|
||||
url: "/articles",
|
||||
params: {
|
||||
slug: slug,
|
||||
},
|
||||
@@ -149,33 +149,33 @@ const loadData = async () => {
|
||||
if (route.params.id) {
|
||||
form.loading(true);
|
||||
let result = await api.get({
|
||||
url: "/posts/{id}",
|
||||
url: "/articles/{id}",
|
||||
params: {
|
||||
id: route.params.id,
|
||||
},
|
||||
});
|
||||
|
||||
const data = result.data as PostResponse;
|
||||
const data = result.data as ArticleResponse;
|
||||
|
||||
if (data && data.post) {
|
||||
form.controls.title.value = data.post.title;
|
||||
form.controls.slug.value = data.post.slug;
|
||||
form.controls.user_id.value = data.post.user.id;
|
||||
form.controls.content.value = data.post.content;
|
||||
form.controls.publish_at.value = data.post.publish_at
|
||||
? new SMDate(data.post.publish_at, {
|
||||
if (data && data.article) {
|
||||
form.controls.title.value = data.article.title;
|
||||
form.controls.slug.value = data.article.slug;
|
||||
form.controls.user_id.value = data.article.user.id;
|
||||
form.controls.content.value = data.article.content;
|
||||
form.controls.publish_at.value = data.article.publish_at
|
||||
? new SMDate(data.article.publish_at, {
|
||||
format: "yMd",
|
||||
utc: true,
|
||||
}).format("dd/MM/yyyy HH:mm")
|
||||
: "";
|
||||
form.controls.content.value = data.post.content;
|
||||
form.controls.hero.value = data.post.hero.id;
|
||||
form.controls.content.value = data.article.content;
|
||||
form.controls.hero.value = data.article.hero.id;
|
||||
|
||||
attachments.value = (data.post.attachments || []).map(function (
|
||||
attachment
|
||||
) {
|
||||
attachments.value = (data.article.attachments || []).map(
|
||||
function (attachment) {
|
||||
return attachment.id.toString();
|
||||
});
|
||||
}
|
||||
);
|
||||
} else {
|
||||
pageError.value = 404;
|
||||
}
|
||||
@@ -201,12 +201,12 @@ const handleSubmit = async () => {
|
||||
hero: form.controls.hero.value,
|
||||
};
|
||||
|
||||
let post_id = "";
|
||||
let article_id = "";
|
||||
|
||||
if (route.params.id) {
|
||||
post_id = route.params.id as string;
|
||||
article_id = route.params.id as string;
|
||||
await api.put({
|
||||
url: `/posts/{id}`,
|
||||
url: `/articles/{id}`,
|
||||
params: {
|
||||
id: route.params.id,
|
||||
},
|
||||
@@ -214,32 +214,32 @@ const handleSubmit = async () => {
|
||||
});
|
||||
} else {
|
||||
let result = await api.post({
|
||||
url: "/posts",
|
||||
url: "/articles",
|
||||
body: data,
|
||||
});
|
||||
|
||||
if (result.data) {
|
||||
const data = result.data as PostResponse;
|
||||
post_id = data.post.id;
|
||||
const data = result.data as ArticleResponse;
|
||||
article_id = data.article.id;
|
||||
}
|
||||
}
|
||||
|
||||
await api.put({
|
||||
url: `/posts/${post_id}/attachments`,
|
||||
url: `/articles/${article_id}/attachments`,
|
||||
body: {
|
||||
attachments: attachments.value,
|
||||
},
|
||||
});
|
||||
|
||||
useToastStore().addToast({
|
||||
title: route.params.id ? "Post Updated" : "Post Created",
|
||||
title: route.params.id ? "Article Updated" : "Article Created",
|
||||
content: route.params.id
|
||||
? "The post has been updated."
|
||||
: "The post has been created.",
|
||||
? "The article has been updated."
|
||||
: "The article has been created.",
|
||||
type: "success",
|
||||
});
|
||||
|
||||
router.push({ name: "dashboard-post-list" });
|
||||
router.push({ name: "dashboard-article-list" });
|
||||
} catch (error) {
|
||||
form.apiErrors(error);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<SMPage permission="admin/posts" :page-error="pageError">
|
||||
<SMPage permission="admin/articles" :page-error="pageError">
|
||||
<template #container>
|
||||
<SMToolbar>
|
||||
<template #left>
|
||||
<SMButton
|
||||
type="primary"
|
||||
label="Create Post"
|
||||
label="Create Article"
|
||||
:small="true"
|
||||
@click="handleCreate" />
|
||||
</template>
|
||||
@@ -31,7 +31,7 @@
|
||||
<template #item-title="item">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'dashboard-post-edit',
|
||||
name: 'dashboard-article-edit',
|
||||
params: { id: item.id },
|
||||
}"
|
||||
>{{ item.title }}</router-link
|
||||
@@ -64,7 +64,7 @@ import SMInput from "../../components/SMInput.vue";
|
||||
import SMLoadingIcon from "../../components/SMLoadingIcon.vue";
|
||||
import SMToolbar from "../../components/SMToolbar.vue";
|
||||
import { api } from "../../helpers/api";
|
||||
import { PostCollection, PostResponse } from "../../helpers/api.types";
|
||||
import { ArticleCollection, ArticleResponse } from "../../helpers/api.types";
|
||||
import { SMDate } from "../../helpers/datetime";
|
||||
import { debounce } from "../../helpers/debounce";
|
||||
import { useToastStore } from "../../store/ToastStore";
|
||||
@@ -103,7 +103,7 @@ const handleClick = (item, extra: string): void => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Load the post data from the server.
|
||||
* Load the article data from the server.
|
||||
*/
|
||||
const loadFromServer = async () => {
|
||||
formLoading.value = true;
|
||||
@@ -128,17 +128,17 @@ const loadFromServer = async () => {
|
||||
}
|
||||
|
||||
const result = await api.get({
|
||||
url: "/posts",
|
||||
url: "/articles",
|
||||
params: params,
|
||||
});
|
||||
|
||||
const data = result.data as PostCollection;
|
||||
const data = result.data as ArticleCollection;
|
||||
|
||||
if (!data || !data.posts) {
|
||||
if (!data || !data.articles) {
|
||||
throw new Error("The server is currently not available");
|
||||
}
|
||||
|
||||
items.value = data.posts;
|
||||
items.value = data.articles;
|
||||
|
||||
items.value.forEach((row) => {
|
||||
if (row.created_at !== "undefined") {
|
||||
@@ -185,15 +185,15 @@ watch(search, () => {
|
||||
});
|
||||
|
||||
const handleClickRow = (item) => {
|
||||
router.push({ name: "dashboard-post-edit", params: { id: item.id } });
|
||||
router.push({ name: "dashboard-article-edit", params: { id: item.id } });
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
router.push({ name: "dashboard-post-create" });
|
||||
router.push({ name: "dashboard-article-create" });
|
||||
};
|
||||
|
||||
const handleEdit = (item) => {
|
||||
router.push({ name: "dashboard-post-edit", params: { id: item.id } });
|
||||
router.push({ name: "dashboard-article-edit", params: { id: item.id } });
|
||||
};
|
||||
|
||||
const handleDuplicate = async (item) => {
|
||||
@@ -223,7 +223,7 @@ const handleDuplicate = async (item) => {
|
||||
const slug = `${originalSlug}-${number}`;
|
||||
try {
|
||||
await api.get({
|
||||
url: `/posts/?slug=${slug}`,
|
||||
url: `/articles/?slug=${slug}`,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
@@ -233,7 +233,7 @@ const handleDuplicate = async (item) => {
|
||||
} else {
|
||||
useToastStore().addToast({
|
||||
title: "Server error",
|
||||
content: "The post could not be duplicated.",
|
||||
content: "The article could not be duplicated.",
|
||||
type: "danger",
|
||||
});
|
||||
return;
|
||||
@@ -245,28 +245,28 @@ const handleDuplicate = async (item) => {
|
||||
}
|
||||
|
||||
const result = await api.post({
|
||||
url: "/posts",
|
||||
url: "/articles",
|
||||
body: item,
|
||||
});
|
||||
|
||||
const data = result.data as PostResponse;
|
||||
const data = result.data as ArticleResponse;
|
||||
|
||||
loadFromServer();
|
||||
|
||||
useToastStore().addToast({
|
||||
title: "Post duplicated",
|
||||
content: "The post was duplicated successfully.",
|
||||
title: "Article duplicated",
|
||||
content: "The article was duplicated successfully.",
|
||||
type: "success",
|
||||
});
|
||||
|
||||
router.push({
|
||||
name: "dashboard-post-edit",
|
||||
params: { id: data.post.id },
|
||||
name: "dashboard-article-edit",
|
||||
params: { id: data.article.id },
|
||||
});
|
||||
} catch (err) {
|
||||
useToastStore().addToast({
|
||||
title: "Server error",
|
||||
content: "The post could not be duplicated.",
|
||||
content: "The article could not be duplicated.",
|
||||
type: "danger",
|
||||
});
|
||||
}
|
||||
@@ -274,24 +274,24 @@ const handleDuplicate = async (item) => {
|
||||
|
||||
const handleDelete = async (item) => {
|
||||
let result = await openDialog(SMDialogConfirm, {
|
||||
title: "Delete Post?",
|
||||
text: `Are you sure you want to delete the post <strong>${item.title}</strong>?`,
|
||||
title: "Delete Article?",
|
||||
text: `Are you sure you want to delete the article <strong>${item.title}</strong>?`,
|
||||
cancel: {
|
||||
type: "secondary",
|
||||
label: "Cancel",
|
||||
},
|
||||
confirm: {
|
||||
type: "danger",
|
||||
label: "Delete Post",
|
||||
label: "Delete Article",
|
||||
},
|
||||
});
|
||||
|
||||
if (result == true) {
|
||||
try {
|
||||
await api.delete(`posts${item.id}`);
|
||||
await api.delete(`articles${item.id}`);
|
||||
loadFromServer();
|
||||
|
||||
formMessage.value.message = "Post deleted successfully";
|
||||
formMessage.value.message = "Article deleted successfully";
|
||||
formMessage.value.type = "success";
|
||||
} catch (err) {
|
||||
formMessage.value.message = err.response?.data?.message;
|
||||
@@ -1,114 +0,0 @@
|
||||
<template>
|
||||
<SMPage>
|
||||
<SMForm v-model="form" @submit="handleSubmit">
|
||||
<SMRow>
|
||||
<SMInput control="title" />
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
<SMEditor
|
||||
id="content"
|
||||
v-model="form.content.value"
|
||||
@file-accept="fileAccept"
|
||||
@attachment-add="attachmentAdd" />
|
||||
</SMRow>
|
||||
<SMRow>
|
||||
<SMButton type="submit" label="Save" />
|
||||
</SMRow>
|
||||
</SMForm>
|
||||
</SMPage>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import SMButton from "../../components/SMButton.vue";
|
||||
import SMForm from "../../components/SMForm.vue";
|
||||
import SMInput from "../../components/SMInput.vue";
|
||||
|
||||
import { api } from "../../helpers/api";
|
||||
import { Form, FormControl } from "../../helpers/form";
|
||||
import { And, Min, Required } from "../../helpers/validate";
|
||||
|
||||
const route = useRoute();
|
||||
let form = reactive(
|
||||
Form({
|
||||
title: FormControl("", And([Required(), Min(2)])),
|
||||
content: FormControl("", Required()),
|
||||
})
|
||||
);
|
||||
|
||||
// const getPostById = async () => {
|
||||
// try {
|
||||
// if (isValidated(formData)) {
|
||||
// let res = await axios.get("posts/" + route.params.id);
|
||||
|
||||
// formData.title.value = res.data.title;
|
||||
// formData.content.value = res.data.content;
|
||||
// }
|
||||
// } catch (err) {
|
||||
// formMessage.icon = "";
|
||||
// formMessage.type = "error";
|
||||
// formMessage.message = "";
|
||||
// restParseErrors(formData, [formMessage, "message"], err);
|
||||
// }
|
||||
// };
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await api.post({
|
||||
url: "/posts",
|
||||
body: {
|
||||
title: form.title.value,
|
||||
content: form.content.value,
|
||||
},
|
||||
});
|
||||
|
||||
form.message("The post has been saved", "success");
|
||||
} catch (error) {
|
||||
form.apiError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const fileAccept = (event) => {
|
||||
if (event.file.type != "image/png") {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const createStorageKey = (file) => {
|
||||
var date = new Date();
|
||||
var day = date.toISOString().slice(0, 10);
|
||||
var name = date.getTime() + "-" + file.name;
|
||||
return ["tmp", day, name].join("/");
|
||||
};
|
||||
|
||||
const attachmentAdd = async (event) => {
|
||||
if (event.attachment.file) {
|
||||
const key = createStorageKey(event.attachment.file);
|
||||
|
||||
var fileFormData = new FormData();
|
||||
fileFormData.append("key", key);
|
||||
fileFormData.append("Content-Type", event.attachment.file.type);
|
||||
fileFormData.append("file", event.attachment.file);
|
||||
|
||||
try {
|
||||
let res = await axios.post("upload", fileFormData, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
onUploadProgress: (progressEvent) =>
|
||||
event.attachment.setUploadProgress(
|
||||
(progressEvent.loaded * progressEvent.total) / 100
|
||||
),
|
||||
});
|
||||
|
||||
event.attachment.setAttributes({
|
||||
url: res.data.url,
|
||||
href: res.data.url,
|
||||
});
|
||||
} catch (err) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -7,9 +7,9 @@
|
||||
<h3>My Details</h3>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-if="userStore.permissions.includes('admin/posts')"
|
||||
:to="{ name: 'dashboard-post-list' }"
|
||||
class="admin-card posts">
|
||||
v-if="userStore.permissions.includes('admin/articles')"
|
||||
:to="{ name: 'dashboard-article-list' }"
|
||||
class="admin-card articles">
|
||||
<ion-icon name="newspaper-outline" />
|
||||
<h3>Articles</h3>
|
||||
</router-link>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<SMColumn
|
||||
><SMInput
|
||||
type="checkbox"
|
||||
label="Edit Posts"
|
||||
label="Edit Articles"
|
||||
v-model="permissions.users"
|
||||
/></SMColumn>
|
||||
<SMColumn
|
||||
|
||||
@@ -7,7 +7,7 @@ use App\Http\Controllers\Api\EventController;
|
||||
use App\Http\Controllers\Api\LogController;
|
||||
use App\Http\Controllers\Api\MediaController;
|
||||
use App\Http\Controllers\Api\OCRController;
|
||||
use App\Http\Controllers\Api\PostController;
|
||||
use App\Http\Controllers\Api\ArticleController;
|
||||
use App\Http\Controllers\Api\SubscriptionController;
|
||||
use App\Http\Controllers\Api\UserController;
|
||||
|
||||
@@ -35,8 +35,8 @@ Route::post('/users/verifyEmail', [UserController::class, 'verifyEmail']);
|
||||
Route::apiResource('media', MediaController::class);
|
||||
Route::get('media/{medium}/download', [MediaController::class, 'download']);
|
||||
|
||||
Route::apiResource('posts', PostController::class);
|
||||
Route::apiAttachmentResource('posts', PostController::class);
|
||||
Route::apiResource('articles', ArticleController::class);
|
||||
Route::apiAttachmentResource('articles', ArticleController::class);
|
||||
|
||||
Route::apiResource('events', EventController::class);
|
||||
Route::apiAttachmentResource('events', EventController::class);
|
||||
|
||||
136
tests/Feature/ArticlesApiTest.php
Normal file
136
tests/Feature/ArticlesApiTest.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
use App\Models\User;
|
||||
use App\Models\Media;
|
||||
use App\Models\Article;
|
||||
use Faker\Factory as FakerFactory;
|
||||
|
||||
class ArticlesApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected $faker;
|
||||
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->faker = FakerFactory::create();
|
||||
}
|
||||
|
||||
public function testAnyUserCanViewArticle()
|
||||
{
|
||||
// Create an event
|
||||
$article = Article::factory()->create([
|
||||
'publish_at' => $this->faker->dateTimeBetween('-2 months', '-1 month'),
|
||||
]);
|
||||
|
||||
// Create a future event
|
||||
$futureArticle = Article::factory()->create([
|
||||
'publish_at' => $this->faker->dateTimeBetween('+1 month', '+2 months'),
|
||||
]);
|
||||
|
||||
// Send GET request to the /api/articles endpoint
|
||||
$response = $this->getJson('/api/articles');
|
||||
$response->assertStatus(200);
|
||||
|
||||
// Assert that the event is in the response data
|
||||
$response->assertJsonCount(1, 'articles');
|
||||
$response->assertJsonFragment([
|
||||
'id' => $article->id,
|
||||
'title' => $article->title,
|
||||
'content' => $article->content,
|
||||
]);
|
||||
|
||||
$response->assertJsonMissing([
|
||||
'id' => $futureArticle->id,
|
||||
'title' => $futureArticle->title,
|
||||
'content' => $futureArticle->content,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testAdminCanCreateUpdateDeleteArticle()
|
||||
{
|
||||
// Create a user with the admin/events permission
|
||||
$adminUser = User::factory()->create();
|
||||
$adminUser->givePermission('admin/articles');
|
||||
|
||||
// Create media data
|
||||
$media = Media::factory()->create(['user_id' => $adminUser->id]);
|
||||
|
||||
// Create event data
|
||||
$articleData = Article::factory()->make([
|
||||
'user_id' => $adminUser->id,
|
||||
'hero' => $media->id,
|
||||
])->toArray();
|
||||
|
||||
// Test creating event
|
||||
$response = $this->actingAs($adminUser)->postJson('/api/articles', $articleData);
|
||||
$response->assertStatus(201);
|
||||
$this->assertDatabaseHas('articles', [
|
||||
'title' => $articleData['title'],
|
||||
'content' => $articleData['content'],
|
||||
]);
|
||||
|
||||
// Test viewing event
|
||||
$article = Article::where('title', $articleData['title'])->first();
|
||||
$response = $this->get("/api/articles/$article->id");
|
||||
$response->assertStatus(200);
|
||||
$response->assertJsonStructure([
|
||||
'article' => [
|
||||
'id',
|
||||
'title',
|
||||
'content',
|
||||
]
|
||||
]);
|
||||
|
||||
// Test updating event
|
||||
$articleData['title'] = 'Updated Article';
|
||||
$response = $this->actingAs($adminUser)->putJson("/api/articles/$article->id", $articleData);
|
||||
$response->assertStatus(200);
|
||||
$this->assertDatabaseHas('articles', [
|
||||
'title' => 'Updated Article',
|
||||
]);
|
||||
|
||||
// Test deleting event
|
||||
$response = $this->actingAs($adminUser)->delete("/api/articles/$article->id");
|
||||
$response->assertStatus(204);
|
||||
$this->assertDatabaseMissing('articles', [
|
||||
'title' => 'Updated Article',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testNonAdminCannotCreateUpdateDeleteArticle()
|
||||
{
|
||||
// Create a user without admin/events permission
|
||||
$user = User::factory()->create();
|
||||
|
||||
// Authenticate as the user
|
||||
$this->actingAs($user);
|
||||
|
||||
// Try to create a new article
|
||||
$media = Media::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
$newArticleData = Article::factory()->make(['user_id' => $user->id, 'hero' => $media->id])->toArray();
|
||||
|
||||
$response = $this->postJson('/api/articles', $newArticleData);
|
||||
$response->assertStatus(403);
|
||||
|
||||
// Try to update an event
|
||||
$article = Article::factory()->create();
|
||||
$updatedArticleData = [
|
||||
'title' => 'Updated Event',
|
||||
'content' => 'This is an updated event.',
|
||||
// Add more fields as needed
|
||||
];
|
||||
$response = $this->putJson('/api/articles/' . $article->id, $updatedArticleData);
|
||||
$response->assertStatus(403);
|
||||
|
||||
// Try to delete an event
|
||||
$article = Article::factory()->create();
|
||||
$response = $this->deleteJson('/api/articles/' . $article->id);
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
<?php
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
use App\Models\User;
|
||||
use App\Models\Media;
|
||||
use App\Models\Post;
|
||||
use Faker\Factory as FakerFactory;
|
||||
|
||||
class PostsApiTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected $faker;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->faker = FakerFactory::create();
|
||||
}
|
||||
|
||||
public function testAnyUserCanViewPost()
|
||||
{
|
||||
// Create an event
|
||||
$post = Post::factory()->create([
|
||||
'publish_at' => $this->faker->dateTimeBetween('-2 months', '-1 month'),
|
||||
]);
|
||||
|
||||
// Create a future event
|
||||
$futurePost = Post::factory()->create([
|
||||
'publish_at' => $this->faker->dateTimeBetween('+1 month', '+2 months'),
|
||||
]);
|
||||
|
||||
// Send GET request to the /api/posts endpoint
|
||||
$response = $this->getJson('/api/posts');
|
||||
$response->assertStatus(200);
|
||||
|
||||
// Assert that the event is in the response data
|
||||
$response->assertJsonCount(1, 'posts');
|
||||
$response->assertJsonFragment([
|
||||
'id' => $post->id,
|
||||
'title' => $post->title,
|
||||
'content' => $post->content,
|
||||
]);
|
||||
|
||||
$response->assertJsonMissing([
|
||||
'id' => $futurePost->id,
|
||||
'title' => $futurePost->title,
|
||||
'content' => $futurePost->content,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testAdminCanCreateUpdateDeletePost()
|
||||
{
|
||||
// Create a user with the admin/events permission
|
||||
$adminUser = User::factory()->create();
|
||||
$adminUser->givePermission('admin/posts');
|
||||
|
||||
// Create media data
|
||||
$media = Media::factory()->create(['user_id' => $adminUser->id]);
|
||||
|
||||
// Create event data
|
||||
$postData = Post::factory()->make([
|
||||
'user_id' => $adminUser->id,
|
||||
'hero' => $media->id,
|
||||
])->toArray();
|
||||
|
||||
// Test creating event
|
||||
$response = $this->actingAs($adminUser)->postJson('/api/posts', $postData);
|
||||
$response->assertStatus(201);
|
||||
$this->assertDatabaseHas('posts', [
|
||||
'title' => $postData['title'],
|
||||
'content' => $postData['content'],
|
||||
]);
|
||||
|
||||
// Test viewing event
|
||||
$post = Post::where('title', $postData['title'])->first();
|
||||
$response = $this->get("/api/posts/$post->id");
|
||||
$response->assertStatus(200);
|
||||
$response->assertJsonStructure([
|
||||
'post' => [
|
||||
'id',
|
||||
'title',
|
||||
'content',
|
||||
]
|
||||
]);
|
||||
|
||||
// Test updating event
|
||||
$postData['title'] = 'Updated Post';
|
||||
$response = $this->actingAs($adminUser)->putJson("/api/posts/$post->id", $postData);
|
||||
$response->assertStatus(200);
|
||||
$this->assertDatabaseHas('posts', [
|
||||
'title' => 'Updated Post',
|
||||
]);
|
||||
|
||||
// Test deleting event
|
||||
$response = $this->actingAs($adminUser)->delete("/api/posts/$post->id");
|
||||
$response->assertStatus(204);
|
||||
$this->assertDatabaseMissing('posts', [
|
||||
'title' => 'Updated Post',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testNonAdminCannotCreateUpdateDeletePost()
|
||||
{
|
||||
// Create a user without admin/events permission
|
||||
$user = User::factory()->create();
|
||||
|
||||
// Authenticate as the user
|
||||
$this->actingAs($user);
|
||||
|
||||
// Try to create a new post
|
||||
$media = Media::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
$newPostData = Post::factory()->make(['user_id' => $user->id, 'hero' => $media->id])->toArray();
|
||||
|
||||
$response = $this->postJson('/api/posts', $newPostData);
|
||||
$response->assertStatus(403);
|
||||
|
||||
// Try to update an event
|
||||
$post = Post::factory()->create();
|
||||
$updatedPostData = [
|
||||
'title' => 'Updated Event',
|
||||
'content' => 'This is an updated event.',
|
||||
// Add more fields as needed
|
||||
];
|
||||
$response = $this->putJson('/api/posts/' . $post->id, $updatedPostData);
|
||||
$response->assertStatus(403);
|
||||
|
||||
// Try to delete an event
|
||||
$post = Post::factory()->create();
|
||||
$response = $this->deleteJson('/api/posts/' . $post->id);
|
||||
$response->assertStatus(403);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user