change posts to articles

This commit is contained in:
2023-04-26 10:57:27 +10:00
parent c6d318bbc3
commit 3ee97468f9
23 changed files with 416 additions and 486 deletions

View File

@@ -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);
}
/**

View File

@@ -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) {

View File

@@ -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

View File

@@ -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',

View File

@@ -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
*/

View File

@@ -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()
{

View File

@@ -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);
}
/**

View File

@@ -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.

View 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

View File

@@ -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}')`;

View File

@@ -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;
}

View File

@@ -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: () =>

View File

@@ -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)};
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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>

View File

@@ -41,7 +41,7 @@
<SMColumn
><SMInput
type="checkbox"
label="Edit Posts"
label="Edit Articles"
v-model="permissions.users"
/></SMColumn>
<SMColumn

View File

@@ -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);

View 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);
}
}

View File

@@ -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);
}
}