Files
productalert/resources/js/vue/PostEditor.vue
2023-08-01 22:37:51 +08:00

694 lines
21 KiB
Vue

<template>
<div>
<div class="row justify-content-center">
<div class="col-9" style="max-width: 700px">
<div class="mb-3">
<div class="form-floating">
<input
v-model="post.title"
type="text"
class="form-control"
placeholder="Post title"
/>
<label>Write a SEO post title</label>
</div>
<small>
<span class="text-secondary">{{ getPostFullUrl }}</span>
</small>
</div>
<div class="form-floating mb-3">
<textarea
v-model="post.excerpt"
class="form-control"
style="min-height: 150px"
placeholder="Enter a post excerpt/summary"
></textarea>
<label
>Write a simple excerpt to convince & entice users to view this
post!</label
>
</div>
<native-image-block
ref="imageBlock"
class="mb-3"
:input-image="post.featured_image"
@saved="imageSaved"
></native-image-block>
<div v-if="showEditorJs" class="card">
<div class="card-body">
<vue-editor-js
v-on:saved="editorSaved"
:config="config"
:initialized="onInitialized"
/>
</div>
</div>
</div>
<div class="col-3">
<div class="d-grid mb-2">
<select
class="form-select mb-2"
aria-label="Default select example"
v-on:change="statusChanged"
>
<option
v-for="item in status"
v-bind:key="item"
:selected="item == post.status"
:value="item"
>
Post Status: {{ item }}
</option>
</select>
<div class="fw-bold">Publish Date</div>
<div class="input-icon mb-2">
<span class="input-icon-addon"
><!-- Download SVG icon from http://tabler-icons.io/i/calendar -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path
d="M4 7a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2v-12z"
></path>
<path d="M16 3v4"></path>
<path d="M8 3v4"></path>
<path d="M4 11h16"></path>
<path d="M11 15h1"></path>
<path d="M12 15v3"></path>
</svg>
</span>
<VueDatePicker
:timezone="timezone"
v-model="post.publish_date"
></VueDatePicker>
</div>
<button
@click="checkAndSave"
class="btn btn-primary"
style="height: 50px"
>
<div
v-if="isSaving"
class="spinner-border"
role="status"
:disabled="isSaving"
:class="isSaving ? 'disabled' : ''"
>
<span class="visually-hidden">Saving...</span>
</div>
<span v-else>Save as {{ post.status }}</span>
</button>
</div>
<div class="card mb-2">
<div class="card-header fw-bold">Country Locality</div>
<div class="card-body">
<select class="form-select" v-on:change="localeChanged">
<option
v-for="item in countryLocales"
v-bind:key="item.id"
:value="item.slug"
:selected="item.slug == post.locale_slug"
>
{{ item.name }}
</option>
</select>
</div>
</div>
<div class="card mb-2">
<div class="card-header fw-bold">Categories</div>
<div class="card-body">
<div
class="py-1"
v-for="item in localeCategories"
v-bind:key="item.id"
>
<label>
<input
type="radio"
:id="item.id"
:value="item.id"
v-model="post.categories"
/>
{{ item.name }}
</label>
</div>
</div>
</div>
<div class="card mb-2">
<div class="card-header fw-bold">Authors</div>
<div class="card-body">
<div class="py-1" v-for="item in authors" v-bind:key="item.id">
<label>
<input
type="radio"
:id="item.id"
:value="item.id"
v-model="post.author_id"
/>
{{ item.name }}
</label>
</div>
</div>
</div>
<div class="card mb-2">
<div class="card-header fw-bold">Other Settings</div>
<div class="card-body">
<div class="form-check form-switch">
<input
v-model="post.featured"
class="form-check-input"
type="checkbox"
role="switch"
/>
<label class="form-check-label">Feature this Post</label>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import VueEditorJs from "./VueEditorJs.vue";
import List from "@editorjs/list";
import Header from "@editorjs/header";
import ImageTool from "@editorjs/image";
import { mapActions, mapState } from "pinia";
import { usePostStore } from "@/stores/postStore.js";
import axios from "axios";
import route from "ziggy-js/src/js/index";
import VueDatePicker from "@vuepic/vue-datepicker";
import "@vuepic/vue-datepicker/dist/main.css";
import { addMinutes } from "date-fns";
export default {
components: { VueEditorJs, List, Header, VueDatePicker },
props: {
postId: {
type: Number, // The prop type is Number
default: null, // Default value if the prop is not provided
},
timezone: {
type: String,
default: null,
},
},
data() {
return {
isSaving: false,
showEditorJs: false,
post: {
id: null,
title: "",
slug: "",
excerpt: "",
author_id: null,
featured: false,
publish_date: null,
featured_image: null,
body: {
time: 1591362820044,
blocks: [],
version: "2.25.0",
},
locale_slug: null,
locale_id: null,
status: "draft",
categories: null,
},
status: ["publish", "future", "draft", "private", "trash"],
config: {
placeholder: "Write something (ノ◕ヮ◕)ノ*:・゚✧",
tools: {
header: {
class: Header,
config: {
placeholder: "Enter a header",
levels: [2, 3, 4],
defaultLevel: 3,
},
},
list: {
class: List,
inlineToolbar: true,
},
image: {
class: ImageTool,
config: {
field: "file",
endpoints: {
byFile: null, // Your backend file uploader endpoint
byUrl: null, // Your endpoint that provides uploading by Url
},
},
},
},
onReady: () => {},
onChange: (args) => {},
data: {
time: 1690738306815,
blocks: [
{
id: "DYr36VT6KH",
data: {
text: "Introduction",
level: 3,
},
type: "header",
},
{
id: "TAh-E2RIrs",
data: {
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
},
type: "paragraph",
},
{
id: "sQWS7Ivg74",
data: {
text: "First Point",
level: 3,
},
type: "header",
},
{
id: "Y9GYmrtsEk",
data: {
file: {
url: "https://cdn1.productalert.co/uploads/1690738207_3b4cf9ff-c617-4062-b910-22e61e1751d0.jpg",
},
caption: "Picture of First Point",
stretched: false,
withBorder: false,
withBackground: false,
},
type: "image",
},
{
id: "7qzQF_jale",
data: {
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
},
type: "paragraph",
},
{
id: "_oYWs021IJ",
data: {
text: "Second Point",
level: 3,
},
type: "header",
},
{
id: "PzXRqEDx1Z",
data: {
file: {
url: "https://cdn1.productalert.co/uploads/1690738243_8eb9f5b2-f3ad-45d9-a626-8ef160ef4068.jpg",
},
caption: "Picture of Second Point",
stretched: false,
withBorder: false,
withBackground: false,
},
type: "image",
},
{
id: "oD5oZ_q0Qo",
data: {
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
},
type: "paragraph",
},
{
id: "am9pIHopIw",
data: {
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
},
type: "paragraph",
},
{
id: "iFvJ1tYZk-",
data: {
text: "Third Point",
level: 3,
},
type: "header",
},
{
id: "zqwukyGttU",
data: {
file: {
url: "https://cdn1.productalert.co/uploads/1690738271_180a520a-22df-4b98-aad3-9962e10832d6.jpg",
},
caption: "Picture of Third Point",
stretched: false,
withBorder: false,
withBackground: false,
},
type: "image",
},
{
id: "uuR88uia0m",
data: {
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
},
type: "paragraph",
},
{
id: "KNVtnJ5lou",
data: {
text: "Fourth Point",
level: 3,
},
type: "header",
},
{
id: "SWdpL4jh6G",
data: {
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
},
type: "paragraph",
},
{
id: "dQqWsgP_FO",
data: {
text: "Conclusion",
level: 3,
},
type: "header",
},
{
id: "I7FOByi69M",
data: {
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
},
type: "paragraph",
},
],
version: "2.27.2",
},
},
};
},
watch: {
"post.title": {
deep: true,
handler(after, before) {
this.post.slug = this.slugify(after);
},
},
},
computed: {
...mapState(usePostStore, [
"countryLocales",
"localeCategories",
"defaultLocaleSlug",
"authors",
]),
getPostFullUrl() {
if (this.post.slug?.length > 0) {
return (
"https://productalert.co/" +
this.post.locale_slug +
"/posts/" +
this.post.slug
);
}
return (
"https://productalert.co/" +
this.post.locale_slug +
"/posts/enter-a-post-title-to-autogen-slug"
);
},
},
methods: {
...mapActions(usePostStore, [
"fetchCountryLocales",
"fetchLocaleCategories",
"fetchAuthors",
]),
checkAndSave() {
let errors = [];
if (!(this.post.title?.length > 0)) {
errors.push("post title");
}
if (!(this.post.slug?.length > 0)) {
errors.push("post slug");
}
if (this.post.status == "publish") {
if (!(this.post.publish_date != null)) {
errors.push("publish date");
}
if (!(this.post.excerpt?.length > 0)) {
errors.push("post excerpt");
}
if (!(this.post.featured_image?.length > 0)) {
errors.push("post featured image");
}
if (!(this.post.body.blocks?.length > 0)) {
errors.push("Post body");
}
if (
!(this.post.locale_slug?.length > 0) ||
!(this.post.locale_id != null)
) {
errors.push("Country locality");
}
if (!(this.post.categories != null)) {
errors.push("Category");
}
}
if (errors.length > 0) {
alert(
"HAIYA many errors! For " +
this.post.status +
" status, pls fix " +
errors.join(", ")
);
} else {
this.savePost();
}
},
savePost() {
this.isSaving = true;
const formData = new FormData();
for (const [key, _item] of Object.entries(this.post)) {
if (_item != null) {
if (key == "body") {
formData.append(key, JSON.stringify(_item));
} else if (key == "publish_date") {
if (_item instanceof Date) {
// Now utcDate is the equivalent UTC date of your original date
let isoDate = _item.toISOString();
formData.append(key, isoDate);
} else {
formData.append(key, _item);
}
} else {
formData.append(key, _item);
}
}
}
axios
.post(route("api.admin.post.upsert"), formData, {
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
console.warn(response);
if (response.data.action == "redirect_back") {
window.location.replace(route("posts.manage"));
}
});
setTimeout(
function () {
this.isSaving = false;
}.bind(this),
1000
);
},
onInitialized(editor) {},
imageSaved(src) {
this.post.featured_image = src;
},
editorSaved(payload) {
this.post.body = payload;
},
statusChanged(e) {
this.post.status = e.target.value;
},
localeChanged(e) {
this.post.locale_slug = e.target.value;
this.post.locale_id = this.getLocaleIdBySlug(e.target.value);
this.post.categories = [];
setTimeout(
function () {
this.fetchLocaleCategories(this.post.locale_slug);
}.bind(this),
100
);
},
setDefaultLocale() {
if (this.post.locale_slug == null || this.post.locale_slug == "") {
this.post.locale_slug = this.defaultLocaleSlug;
this.post.locale_id = this.getLocaleIdBySlug(this.defaultLocaleSlug);
}
},
getLocaleIdBySlug(slug) {
for (const [key, _item] of Object.entries(this.countryLocales)) {
if (_item.slug == slug) {
return _item.id;
}
}
return null;
},
async fetchPostData(id) {
const response = await axios.get(route("api.admin.post.get", { id: id }));
if (response?.data?.post != null) {
let tmp = this.post;
let post = response.data.post;
tmp.id = post.id;
tmp.title = post.title;
tmp.slug = post.slug;
tmp.publish_date = post.publish_date;
tmp.excerpt = post.excerpt;
tmp.author_id = post.author_id;
tmp.featured = post.featured;
tmp.featured_image = post.featured_image;
tmp.body = post.body;
tmp.locale_slug = post.post_category.category.country_locale_slug;
tmp.locale_id = post.post_category.category.country_locale_id;
tmp.status = post.status;
tmp.categories = post.post_category.category.id;
this.post = tmp;
this.config.data = post.body;
}
console.log(response.data.post);
},
slugify: function (title) {
var slug = "";
// Change to lower case
var titleLower = title.toLowerCase();
// Replace characters that are not alphabets (a-z), digits (0-9), or spaces with an empty string
slug = titleLower.replace(/[^a-z0-9\s]/g, "");
// Replace consecutive spaces with a single space
slug = slug.replace(/\s+/g, " ");
// Trim any leading or trailing spaces
slug = slug.trim();
// Replace spaces with a single dash
slug = slug.replace(/\s+/g, "-");
return slug;
},
setAuthor() {
if (this.post.id == null) {
if (this.post.author_id == null) {
for (const [key, _item] of Object.entries(this.authors)) {
this.post.author_id = _item.id;
break;
}
}
}
},
setLocalCategory() {
if (this.post.id == null) {
if (this.post.categories == null) {
for (const [key, _item] of Object.entries(this.localeCategories)) {
this.post.categories = _item.id;
break;
}
}
}
},
},
mounted() {
this.config.tools.image.config.endpoints.byFile = route(
"api.admin.upload.cloud.image"
);
this.config.tools.image.config.additionalRequestHeaders = {
"X-CSRF-TOKEN": document
.querySelector('meta[name="csrf-token"]')
.getAttribute("content"),
};
this.fetchCountryLocales().then(() => {
this.setDefaultLocale();
setTimeout(
function () {
this.fetchLocaleCategories(this.post.locale_slug).then(() => {
this.setLocalCategory();
});
this.fetchAuthors().then(() => {
this.setAuthor();
});
if (this.postId != null) {
this.fetchPostData(this.postId).then(() => {
setTimeout(
function () {
this.showEditorJs = true;
}.bind(this),
1000
);
});
} else {
setTimeout(
function () {
this.showEditorJs = true;
}.bind(this),
1000
);
}
}.bind(this),
100
);
});
},
};
</script>
<style></style>