This commit is contained in:
2023-07-30 02:15:19 +08:00
parent 58b939f72e
commit 277a919436
28 changed files with 807 additions and 99 deletions

View File

@@ -4,6 +4,7 @@
use App\Http\Controllers\Controller;
use App\Models\Post;
use App\Models\PostCategory;
use Illuminate\Http\Request;
class PostController extends Controller
@@ -27,10 +28,113 @@ public function edit(Request $request, $post_id)
{
$post = Post::find($post_id);
if (!is_null($post))
{
if (! is_null($post)) {
return view('admin.posts.upsert', compact('post'));
}
return redirect()->back()->with('error','Post does not exist.');
return redirect()->back()->with('error', 'Post does not exist.');
}
public function postUpsert(Request $request)
{
$post_data = [
'id' => $request->input('id', null),
'publish_date' => $request->input('publish_date', null),
'title' => $request->input('title'),
'slug' => $request->input('slug'),
'excerpt' => $request->input('excerpt'),
'author_id' => intval($request->input('author_id', 1)),
'featured' => filter_var($request->input('featured'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE),
'featured_image' => $request->input('featured_image'),
'editor' => 'editorjs',
'body' => json_decode($request->input('body')),
'post_format' => 'standard',
'comment_count' => 0,
'likes_count' => 0,
'status' => $request->input('status'),
];
$post_categories = $this->normalizeCategories($request->input('categories'));
if (! empty($post_data['id'])) {
// It's an update - find the existing post by its ID
$existingPost = Post::find($post_data['id']);
// Check if the post with the given ID exists
if ($existingPost) {
// Update the existing post with the new data
$existingPost->update($post_data);
// Handle PostCategory records for the existing post
$existingPostCategoryIds = $existingPost?->post_categories->pluck('id')->toArray();
// Find the IDs of PostCategory records that should be removed
$postCategoriesToRemove = array_diff($existingPostCategoryIds, $post_categories);
// Remove the unwanted PostCategory records
if (! empty($postCategoriesToRemove)) {
PostCategory::whereIn('id', $postCategoriesToRemove)->delete();
}
// Find the new PostCategory records that should be added
$postCategoriesToAdd = array_diff($post_categories, $existingPostCategoryIds);
// Create the new PostCategory records
foreach ($postCategoriesToAdd as $categoryId) {
PostCategory::create([
'post_id' => $existingPost->id,
'category_id' => $categoryId,
]);
}
// Return a response indicating a successful update
return response()->json(['message' => 'Post updated successfully', 'action' => 'redirect_back']);
} else {
// If the post with the given ID doesn't exist, you can handle the error as per your requirement
return response()->json(['error' => 'Post not found'], 404);
}
} else {
// It's a new post - create a new record using Post::create
$newPost = Post::create($post_data);
// Create the new PostCategory records for the new post
foreach ($post_categories as $categoryId) {
PostCategory::create([
'post_id' => $newPost->id,
'category_id' => $categoryId,
]);
}
// Return a response indicating a successful creation
return response()->json(['message' => 'Post created successfully', 'action' => 'redirect_back']);
}
}
private function normalizeCategories($categories)
{
if (empty($categories) || is_null($categories)) {
// If the input is empty or null, return an empty array
return [];
} elseif (is_numeric($categories)) {
// If the input is a numeric value (integer or string), return an array with the integer value
return [(int) $categories];
} else {
// If the input is a string with separated commas or a JSON string that becomes an array of integers, return an array of integers
if (is_string($categories)) {
// Attempt to convert the string to an array of integers
$categoryIds = json_decode($categories, true);
// Check if the decoding was successful and the result is an array of integers
if (is_array($categoryIds) && ! empty($categoryIds)) {
$categoryIds = array_map('intval', $categoryIds);
return $categoryIds;
}
}
// If the input format doesn't match any of the above cases, return an empty array
return [];
}
}
}

View File

@@ -8,10 +8,25 @@
use App\Models\Post;
use Illuminate\Http\Request;
use Artesaos\SEOTools\Facades\SEOTools;
use Artesaos\SEOTools\Facades\SEOMeta;
use Artesaos\SEOTools\Facades\OpenGraph;
use Artesaos\SEOTools\Facades\JsonLd;
use Artesaos\SEOTools\Facades\JsonLdMulti;
class HomeController extends Controller
{
public function index(Request $request)
{
SEOTools::metatags();
SEOTools::twitter();
SEOTools::opengraph();
SEOTools::jsonLd();
SEOTools::setTitle("Top Product Reviews, Deals & New Launches");
SEOTools::setDescription("Explore ProductAlert for in-depth product reviews and incredible deals. We cover Beauty, Tech, Home Appliances, Health & Fitness, Parenting, and more.");
$country = strtolower($request->session()->get('country'));
return redirect()->route('home.country', ['country' => $country]);
@@ -19,6 +34,8 @@ public function index(Request $request)
public function country(Request $request, $country)
{
$country_locale = CountryLocale::where('slug', $country)->first();
if (! is_null($country_locale)) {
@@ -50,6 +67,17 @@ public function country(Request $request, $country)
->take(10)
->get();
SEOTools::metatags();
SEOTools::twitter();
SEOTools::opengraph();
SEOTools::jsonLd();
$country_name = get_country_name_by_iso($country_locale->country_iso);
SEOTools::setTitle("Your {$country_name} Guide to Product Reviews & Top Deals");
SEOTools::setDescription("Discover trusted product reviews and unbeatable deals at ProductAlert {$country_name}, your local guide to smart shopping.");
return view('front.country', compact('country_locale', 'featured_posts', 'latest_posts')
);
}
@@ -83,6 +111,20 @@ public function countryCategory(Request $request, $country, $category)
->distinct()
->paginate(15);
SEOTools::metatags();
SEOTools::twitter();
SEOTools::opengraph();
SEOTools::jsonLd();
$country_name = get_country_name_by_iso($country_locale->country_iso);
SEOTools::setTitle("Top {$category->name} Reviews in {$country_name}");
$category_name = strtolower($category->name);
SEOTools::setDescription("Stay updated with the latest {$category_name} product launches in {$country_name}. Find in-depth reviews and exciting deals with ProductAlert, your guide to {$category_name} shopping.");
return view('front.country_category', compact('country_locale', 'category', 'latest_posts'));
}
@@ -101,6 +143,17 @@ public function all(Request $request, $country)
->distinct()
->paginate(15);
SEOTools::metatags();
SEOTools::twitter();
SEOTools::opengraph();
SEOTools::jsonLd();
$country_name = get_country_name_by_iso($country_locale->country_iso);
SEOTools::setTitle("Find Product Reviews and Best Deals for {$country_name}");
SEOTools::setDescription("Discover the latest product reviews and unbeatable deals at ProductAlert, your guide to shopping in {$country_name}. Stay on top of fresh product updates.");
return view('front.country_all', compact('country_locale', 'latest_posts'));
}
@@ -110,6 +163,32 @@ public function post(Request $request, $country, $post_slug)
if (! is_null($post)) {
SEOMeta::setTitle($post->title);
SEOMeta::setDescription($post->excerpt);
SEOMeta::addMeta('article:published_time', $post->publish_date, 'property');
SEOMeta::addMeta('article:section', $post->post_category->category->name, 'property');
OpenGraph::setDescription($post->excerpt);
OpenGraph::setTitle($post->title);
OpenGraph::setUrl(url()->current());
OpenGraph::addProperty('type', 'article');
OpenGraph::addProperty('locale', $post->post_category->category->country_locale->i18n);
OpenGraph::addImage($post->featured_image);
$jsonld_multi = JsonLdMulti::newJsonLd();
$jsonld_multi->setTitle($post->title)
->setDescription($post->excerpt)
->setType('Article')
->addImage($post->featured_image)
->addValue('author', $post->author->name)
->addValue('datePublished', $post->publish_at)
->addValue('dateCreated', $post->publish_at)
->addValue('dateModified', $post->updated_at->format('Y-m-d'))
->addValue('description', $post->excerpt)
->addValue('articleBody', trim(preg_replace('/\s\s+/', ' ', strip_tags($post->html_body))))
;
return view('front.post', compact('post'));
}
abort(404);

View File

@@ -4,13 +4,9 @@
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;
use Illuminate\Support\Str;
use Intervention\Image\Facades\Image;
class ImageUploadController extends Controller
{
@@ -26,8 +22,8 @@ public function index(Request $request)
// Generate a unique filename for the uploaded file and LQIP version
$uuid = Str::uuid()->toString();
$fileName = time() . '_' . $uuid . '.jpg';
$lqipFileName = time() . '_' . $uuid . '_lqip.jpg';
$fileName = time().'_'.$uuid.'.jpg';
$lqipFileName = time().'_'.$uuid.'_lqip.jpg';
// Convert the file to JPEG format using Intervention Image library
$image = Image::make($file->getRealPath())->encode('jpg', 100);
@@ -46,8 +42,8 @@ public function index(Request $request)
$image->encode('jpg', 50);
// Save the processed image to the 'r2' storage driver under the 'uploads' directory
$filePath = 'uploads/' . $fileName;
$lqipFilePath = 'uploads/' . $lqipFileName;
$filePath = 'uploads/'.$fileName;
$lqipFilePath = 'uploads/'.$lqipFileName;
Storage::disk('r2')->put($filePath, $image->stream()->detach());
// Save the original image to a temporary file and open it again
@@ -56,7 +52,7 @@ public function index(Request $request)
$clonedImage = Image::make($tempImagePath);
// Create the LQIP version of the image using a small size while maintaining the aspect ratio
$lqipImage = $clonedImage->fit(10, 10, function ($constraint) use ($originalWidth, $originalHeight) {
$lqipImage = $clonedImage->fit(10, 10, function ($constraint) {
$constraint->aspectRatio();
});
$lqipImage->encode('jpg', 5);

View File

@@ -40,6 +40,7 @@ class Post extends Model
'body' => 'json',
'comment_count' => 'int',
'likes_count' => 'int',
'featured' => 'bool',
];
protected $fillable = [
@@ -54,10 +55,12 @@ class Post extends Model
'comment_count',
'likes_count',
'status',
'featured',
'publish_date',
];
protected $appends = [
'html_body',
//'html_body',
];
public function author()
@@ -78,7 +81,7 @@ public function post_category()
public function getHtmlBodyAttribute()
{
if (! is_empty($this->body)) {
return LaravelEditorJs::render($this->body);
return LaravelEditorJs::render(json_encode($this->body));
}
return '';

View File

@@ -161,6 +161,7 @@
*/
Barryvdh\Debugbar\ServiceProvider::class,
Stevebauman\Location\LocationServiceProvider::class,
Artesaos\SEOTools\Providers\SEOToolsServiceProvider::class,
/*
* Application Service Providers...
@@ -187,6 +188,12 @@
'aliases' => Facade::defaultAliases()->merge([
// 'Example' => App\Facades\Example::class,
'Debugbar' => Barryvdh\Debugbar\Facades\Debugbar::class,
'SEOMeta' => Artesaos\SEOTools\Facades\SEOMeta::class,
'OpenGraph' => Artesaos\SEOTools\Facades\OpenGraph::class,
'Twitter' => Artesaos\SEOTools\Facades\TwitterCard::class,
'JsonLd' => Artesaos\SEOTools\Facades\JsonLd::class,
'JsonLdMulti' => Artesaos\SEOTools\Facades\JsonLdMulti::class,
'SEO' => Artesaos\SEOTools\Facades\SEOTools::class,
])->toArray(),

View File

@@ -69,7 +69,6 @@
'throw' => true,
],
],
/*

View File

@@ -9,9 +9,9 @@
* The default configurations to be used by the meta generator.
*/
'defaults' => [
'title' => "It's Over 9000!", // set false to total remove
'title' => "ProductAlert", // set false to total remove
'titleBefore' => false, // Put defaults.title before page title, like 'It's Over 9000! - Dashboard'
'description' => 'For those who helped create the Genki Dama', // set false to total remove
'description' => 'Find top-rated product reviews at ProductAlert. Discover the latest trends, best brands, and right prices. Your guide to making the best purchase decisions!', // set false to total remove
'separator' => ' - ',
'keywords' => [],
'canonical' => false, // Set to null or 'full' to use Url::full(), set to 'current' to use Url::current(), set false to total remove
@@ -36,8 +36,8 @@
* The default configurations to be used by the opengraph generator.
*/
'defaults' => [
'title' => 'Over 9000 Thousand!', // set false to total remove
'description' => 'For those who helped create the Genki Dama', // set false to total remove
'title' => 'ProductAlert', // set false to total remove
'description' => 'Find top-rated product reviews at ProductAlert. Discover the latest trends, best brands, and right prices. Your guide to making the best purchase decisions!', // set false to total remove
'url' => false, // Set null for using Url::current(), set false to total remove
'type' => false,
'site_name' => false,
@@ -58,8 +58,8 @@
* The default configurations to be used by the json-ld generator.
*/
'defaults' => [
'title' => 'Over 9000 Thousand!', // set false to total remove
'description' => 'For those who helped create the Genki Dama', // set false to total remove
'title' => 'ProductAlert', // set false to total remove
'description' => 'Find top-rated product reviews at ProductAlert. Discover the latest trends, best brands, and right prices. Your guide to making the best purchase decisions!', // set false to total remove
'url' => false, // Set to null or 'full' to use Url::full(), set to 'current' to use Url::current(), set false to total remove
'type' => 'WebPage',
'images' => [],

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->date('publish_date')->nullable()->after('author_id');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn('publish_date');
});
}
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,32 @@
{
"NativeImageBlock.css": {
"file": "assets/NativeImageBlock-e3b0c442.css",
"src": "NativeImageBlock.css"
},
"_NativeImageBlock-bcbff98b.js": {
"css": [
"assets/NativeImageBlock-e3b0c442.css"
],
"file": "assets/NativeImageBlock-bcbff98b.js",
"imports": [
"resources/js/admin-app.js"
],
"isDynamicEntry": true
},
"_bundle-43b5b4d7.js": {
"file": "assets/bundle-43b5b4d7.js",
"imports": [
"resources/js/admin-app.js"
],
"isDynamicEntry": true
},
"_bundle-94bef551.js": {
"file": "assets/bundle-94bef551.js",
"imports": [
"resources/js/admin-app.js"
],
"isDynamicEntry": true
},
"_index-8746c87e.js": {
"file": "assets/index-8746c87e.js"
},
@@ -10,8 +38,20 @@
"file": "assets/bootstrap-icons-cfe45b98.woff2",
"src": "node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff2"
},
"resources/js/admin-app.css": {
"file": "assets/admin-app-935fc652.css",
"src": "resources/js/admin-app.css"
},
"resources/js/admin-app.js": {
"file": "assets/admin-app-ff7516d6.js",
"css": [
"assets/admin-app-935fc652.css"
],
"dynamicImports": [
"_NativeImageBlock-bcbff98b.js",
"resources/js/vue/PostEditor.vue",
"resources/js/vue/VueEditorJs.vue"
],
"file": "assets/admin-app-0df052b8.js",
"imports": [
"_index-8746c87e.js"
],
@@ -26,6 +66,32 @@
"isEntry": true,
"src": "resources/js/front-app.js"
},
"resources/js/vue/PostEditor.vue": {
"file": "assets/PostEditor-a6038129.js",
"imports": [
"resources/js/vue/VueEditorJs.vue",
"_NativeImageBlock-bcbff98b.js",
"_bundle-43b5b4d7.js",
"_bundle-94bef551.js",
"resources/js/admin-app.js",
"_index-8746c87e.js"
],
"isDynamicEntry": true,
"src": "resources/js/vue/PostEditor.vue"
},
"resources/js/vue/VueEditorJs.vue": {
"dynamicImports": [
"_bundle-94bef551.js",
"_bundle-43b5b4d7.js"
],
"file": "assets/VueEditorJs-6310d292.js",
"imports": [
"resources/js/admin-app.js",
"_index-8746c87e.js"
],
"isDynamicEntry": true,
"src": "resources/js/vue/VueEditorJs.vue"
},
"resources/sass/admin-app.scss": {
"file": "assets/admin-app-bade20ce.css",
"isEntry": true,

View File

@@ -10,6 +10,7 @@ export const usePostStore = defineStore("postStore", {
defaultLocaleSlug: "my",
countryLocales: [],
localeCategories: [],
authors: [],
},
}),
getters: {
@@ -22,8 +23,21 @@ export const usePostStore = defineStore("postStore", {
localeCategories(state) {
return state.data.localeCategories;
},
authors(state) {
return state.data.authors;
},
},
actions: {
async fetchAuthors() {
try {
const response = await axios.get(route("api.admin.authors"));
console.log(response);
this.data.authors = response.data.authors;
} catch (error) {
// alert(error);
console.log(error);
}
},
async fetchCountryLocales() {
try {
const response = await axios.get(route("api.admin.country-locales"));

View File

@@ -9,7 +9,7 @@
<div
class="position-absolute w-100 h-100 d-flex justify-content-center text-center"
>
<div v-if="isUploading" class="align-self-center">
<div v-if="isUploading || !isLoaded" class="align-self-center">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
</div>
@@ -34,16 +34,18 @@
<script>
import axios from "axios";
import route from "ziggy-js/src/js/index";
export default {
name: "NativeImageBlock",
props: {
apiUrl: {
inputImage: {
type: String,
default: "https://productalert.test/api/admin/image/upload",
default: null,
},
},
data: () => ({
isLoaded: false,
isUploading: false,
imgSrc: null,
placeholderSrc: "https://placekitten.com/g/2100/900",
@@ -91,7 +93,7 @@ export default {
formData.append("file", file);
axios
.post(this.apiUrl, formData, {
.post(route("api.admin.upload.cloud.image"), formData, {
headers: {
"Content-Type": "multipart/form-data",
},
@@ -115,9 +117,23 @@ export default {
this.isUploading = false;
});
},
setInputImage() {
if (this.inputImage != null && this.inputImage?.length > 0) {
this.imgSrc = this.inputImage;
}
this.isLoaded = true;
},
},
mounted() {
this.isUploading = false;
setTimeout(
function () {
this.setInputImage();
this.isLoaded = true;
}.bind(this),
3000
);
},
};
</script>

View File

@@ -31,12 +31,13 @@
</div>
<native-image-block
ref="imageBlock"
class="mb-3"
:input-image="post.featured_image"
@saved="imageSaved"
></native-image-block>
<div class="card">
<div v-if="showEditorJs" class="card">
<div class="card-body">
<vue-editor-js
v-on:saved="editorSaved"
@@ -62,8 +63,56 @@
Post Status: {{ item }}
</option>
</select>
<button @click="checkAndSave" class="btn btn-primary">
Save as {{ post.status }}
<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>
<input
type="date"
v-model="post.publish_date"
class="form-control"
placeholder="Select a date"
id="datepicker-icon-prepend"
/>
</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">
@@ -101,6 +150,36 @@
</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>
@@ -116,16 +195,29 @@ import { mapActions, mapState } from "pinia";
import { usePostStore } from "@/stores/postStore.js";
import axios from "axios";
import route from "ziggy-js/src/js/index";
export default {
components: { VueEditorJs, List, Header },
props: {
postId: {
type: Number, // The prop type is Number
default: null, // Default value if the prop is not provided
},
},
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,
@@ -179,6 +271,7 @@ export default {
"countryLocales",
"localeCategories",
"defaultLocaleSlug",
"authors",
]),
getPostFullUrl() {
if (this.post.slug?.length > 0) {
@@ -200,6 +293,7 @@ export default {
...mapActions(usePostStore, [
"fetchCountryLocales",
"fetchLocaleCategories",
"fetchAuthors",
]),
checkAndSave() {
let errors = [];
@@ -208,6 +302,10 @@ export default {
errors.push("post title");
}
if (!(this.post.publish_date?.length > 0)) {
errors.push("publish date");
}
if (!(this.post.slug?.length > 0)) {
errors.push("post slug");
}
@@ -241,7 +339,36 @@ export default {
this.savePost();
}
},
savePost() {},
savePost() {
this.isSaving = true;
const formData = new FormData();
for (const [key, _item] of Object.entries(this.post)) {
if (key == "body") {
formData.append(key, JSON.stringify(_item));
} else {
formData.append(key, _item);
}
}
axios
.post(route("api.admin.post.upsert"), formData, {
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
console.warn(response);
});
setTimeout(
function () {
this.isSaving = false;
}.bind(this),
1000
);
},
onInitialized(editor) {},
imageSaved(src) {
this.post.featured_image = src;
@@ -278,6 +405,34 @@ export default {
}
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
@@ -300,6 +455,25 @@ export default {
setTimeout(
function () {
this.fetchLocaleCategories(this.post.locale_slug);
this.fetchAuthors();
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
);

View File

@@ -1,4 +1,4 @@
const Ziggy = {"url":"https:\/\/productalert.test","port":null,"defaults":{},"routes":{"debugbar.openhandler":{"uri":"_debugbar\/open","methods":["GET","HEAD"]},"debugbar.clockwork":{"uri":"_debugbar\/clockwork\/{id}","methods":["GET","HEAD"]},"debugbar.assets.css":{"uri":"_debugbar\/assets\/stylesheets","methods":["GET","HEAD"]},"debugbar.assets.js":{"uri":"_debugbar\/assets\/javascript","methods":["GET","HEAD"]},"debugbar.cache.delete":{"uri":"_debugbar\/cache\/{key}\/{tags?}","methods":["DELETE"]},"sanctum.csrf-cookie":{"uri":"sanctum\/csrf-cookie","methods":["GET","HEAD"]},"ignition.healthCheck":{"uri":"_ignition\/health-check","methods":["GET","HEAD"]},"ignition.executeSolution":{"uri":"_ignition\/execute-solution","methods":["POST"]},"ignition.updateConfig":{"uri":"_ignition\/update-config","methods":["POST"]},"api.auth.login.post":{"uri":"api\/login","methods":["POST"]},"api.auth.logout.post":{"uri":"api\/logout","methods":["POST"]},"api.admin.country-locales":{"uri":"api\/admin\/country-locales","methods":["GET","HEAD"]},"api.admin.categories":{"uri":"api\/admin\/categories\/{country_locale_slug}","methods":["GET","HEAD"]},"api.admin.upload.cloud.image":{"uri":"api\/admin\/image\/upload","methods":["GET","HEAD"]},"login":{"uri":"login","methods":["GET","HEAD"]},"logout":{"uri":"logout","methods":["POST"]},"register":{"uri":"register","methods":["GET","HEAD"]},"password.request":{"uri":"password\/reset","methods":["GET","HEAD"]},"password.email":{"uri":"password\/email","methods":["POST"]},"password.reset":{"uri":"password\/reset\/{token}","methods":["GET","HEAD"]},"password.update":{"uri":"password\/reset","methods":["POST"]},"password.confirm":{"uri":"password\/confirm","methods":["GET","HEAD"]},"dashboard":{"uri":"admin","methods":["GET","HEAD"]},"about":{"uri":"admin\/about","methods":["GET","HEAD"]},"users.index":{"uri":"admin\/users","methods":["GET","HEAD"]},"posts.manage":{"uri":"admin\/posts","methods":["GET","HEAD"]},"posts.manage.edit":{"uri":"admin\/posts\/edit\/{post_id}","methods":["GET","HEAD"]},"posts.manage.new":{"uri":"admin\/posts\/new","methods":["GET","HEAD"]},"profile.show":{"uri":"admin\/profile","methods":["GET","HEAD"]},"profile.update":{"uri":"admin\/profile","methods":["PUT"]},"home":{"uri":"\/","methods":["GET","HEAD"]},"home.country":{"uri":"{country}","methods":["GET","HEAD"]},"home.country.posts":{"uri":"{country}\/posts","methods":["GET","HEAD"]},"home.country.post":{"uri":"{country}\/posts\/{post_slug}","methods":["GET","HEAD"]},"home.country.category":{"uri":"{country}\/{category}","methods":["GET","HEAD"]}}};
const Ziggy = {"url":"https:\/\/productalert.test","port":null,"defaults":{},"routes":{"debugbar.openhandler":{"uri":"_debugbar\/open","methods":["GET","HEAD"]},"debugbar.clockwork":{"uri":"_debugbar\/clockwork\/{id}","methods":["GET","HEAD"]},"debugbar.assets.css":{"uri":"_debugbar\/assets\/stylesheets","methods":["GET","HEAD"]},"debugbar.assets.js":{"uri":"_debugbar\/assets\/javascript","methods":["GET","HEAD"]},"debugbar.cache.delete":{"uri":"_debugbar\/cache\/{key}\/{tags?}","methods":["DELETE"]},"sanctum.csrf-cookie":{"uri":"sanctum\/csrf-cookie","methods":["GET","HEAD"]},"ignition.healthCheck":{"uri":"_ignition\/health-check","methods":["GET","HEAD"]},"ignition.executeSolution":{"uri":"_ignition\/execute-solution","methods":["POST"]},"ignition.updateConfig":{"uri":"_ignition\/update-config","methods":["POST"]},"api.auth.login.post":{"uri":"api\/login","methods":["POST"]},"api.auth.logout.post":{"uri":"api\/logout","methods":["POST"]},"api.admin.post.get":{"uri":"api\/admin\/post\/{id}","methods":["GET","HEAD"]},"api.admin.country-locales":{"uri":"api\/admin\/country-locales","methods":["GET","HEAD"]},"api.admin.categories":{"uri":"api\/admin\/categories\/{country_locale_slug}","methods":["GET","HEAD"]},"api.admin.authors":{"uri":"api\/admin\/authors","methods":["GET","HEAD"]},"api.admin.upload.cloud.image":{"uri":"api\/admin\/image\/upload","methods":["POST"]},"api.admin.post.upsert":{"uri":"api\/admin\/admin\/post\/upsert","methods":["POST"]},"login":{"uri":"login","methods":["GET","HEAD"]},"logout":{"uri":"logout","methods":["POST"]},"register":{"uri":"register","methods":["GET","HEAD"]},"password.request":{"uri":"password\/reset","methods":["GET","HEAD"]},"password.email":{"uri":"password\/email","methods":["POST"]},"password.reset":{"uri":"password\/reset\/{token}","methods":["GET","HEAD"]},"password.update":{"uri":"password\/reset","methods":["POST"]},"password.confirm":{"uri":"password\/confirm","methods":["GET","HEAD"]},"dashboard":{"uri":"admin","methods":["GET","HEAD"]},"about":{"uri":"admin\/about","methods":["GET","HEAD"]},"users.index":{"uri":"admin\/users","methods":["GET","HEAD"]},"posts.manage":{"uri":"admin\/posts","methods":["GET","HEAD"]},"posts.manage.edit":{"uri":"admin\/posts\/edit\/{post_id}","methods":["GET","HEAD"]},"posts.manage.new":{"uri":"admin\/posts\/new","methods":["GET","HEAD"]},"profile.show":{"uri":"admin\/profile","methods":["GET","HEAD"]},"profile.update":{"uri":"admin\/profile","methods":["PUT"]},"home":{"uri":"\/","methods":["GET","HEAD"]},"home.country":{"uri":"{country}","methods":["GET","HEAD"]},"home.country.posts":{"uri":"{country}\/posts","methods":["GET","HEAD"]},"home.country.post":{"uri":"{country}\/posts\/{post_slug}","methods":["GET","HEAD"]},"home.country.category":{"uri":"{country}\/{category}","methods":["GET","HEAD"]}}};
if (typeof window !== 'undefined' && typeof window.Ziggy !== 'undefined') {
Object.assign(Ziggy.routes, window.Ziggy.routes);

View File

@@ -29,9 +29,9 @@
<th>
Image
</th>
<th>Status</th>
<th>Title</th>
<th>{{ __('Created at') }}</th>
<th>{{ __('Updated in') }}</th>
<th>Datetime</th>
<th>Actions</th>
</tr>
</thead>
@@ -42,12 +42,33 @@
<td><img width="80" height="60" src="{{ $post->featured_image }}"
class="img-fluid rounded-2" alt=""></td>
<td>
@if($post->status === 'publish')
<span class="badge bg-success">{{ ucfirst($post->status) }}</span>
@elseif($post->status === 'future')
<span class="badge bg-primary">{{ ucfirst($post->status) }}</span>
@elseif($post->status === 'draft')
<span class="badge bg-secondary">{{ ucfirst($post->status) }}</span>
@elseif($post->status === 'private')
<span class="badge bg-info">{{ ucfirst($post->status) }}</span>
@elseif ($post->status == 'trash')
<span class="badge bg-danger">{{ ucfirst($post->status) }}</span>
@else
<span class="badge bg-secondary">{{ ucfirst($post->status) }}</span>
@endif
</td>
<td>
@if(!is_empty($post->post_category?->category?->country_locale_slug) && $post->status == 'publish')
<a
href="{{ route('home.country.post', ['country' => $post->post_category->category->country_locale_slug, 'post_slug' => $post->slug]) }}">{{ $post->title }}</a>
href="{{ route('home.country.post', ['country' => $post->post_category?->category?->country_locale_slug, 'post_slug' => $post->slug]) }}">{{ $post->title }}</a>
@else
{{ $post->title }}
@endif
</td>
<td>{{ $post->created_at }}</td>
<td>{{ $post->updated_at->diffForhumans() }}</td>
<td>
Created at {{ $post->created_at->timezone(session()->get('timezone'))->isoFormat('Do MMMM YYYY, h:mm A') }}<br>
Updated {{ $post->updated_at->diffForhumans() }}
</td>
<td>
<div><a href="{{ route('posts.manage.edit', ['post_id' => $post->id]) }}"
class="btn">Edit</a></div>

View File

@@ -11,14 +11,17 @@
@else
New Post
@endif
</div>
</h2>
</div>
</div>
<div class="page-body">
<div class="container-xl">
@if (!is_null($post))
<post-editor :post-id="{{ $post->id }}"></post-editor>
@else
<post-editor></post-editor>
@endif
</div>
</div>
@endsection

View File

@@ -31,10 +31,10 @@
<h2 class="h5">{{ $post->excerpt }}</h2>
</div>
<div class="mb-3">
<img src="{{ $post->featured_image }}" alt="" class="img-fluid rounded-3">
<img src="{{ $post->featured_image }}" alt="Photo of {{ $post->name }}" class="img-fluid rounded-3">
</div>
<div class="mb-3">
{{ $post->html_body }}
{!! $post->html_body !!}
</div>
</div>

View File

@@ -5,8 +5,11 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>{{ config('app.name', 'Laravel') }}</title>
{!! SEOMeta::generate() !!}
{!! OpenGraph::generate() !!}
{!! Twitter::generate() !!}
{!! JsonLdMulti::generate() !!}
<meta property="fb:app_id" content="{{ config('seotools.fb_app_id') }}" />
@vite('resources/sass/front-app.scss')

View File

@@ -1,35 +1,34 @@
<div class="container-fluid border-top">
<footer class="py-5 container">
<div class="row">
<div class="row justify-content-center">
<div class="col-6 col-md-2 mb-3">
<ul class="nav flex-column">
@foreach ($categories as $category)
@if ($category->id % 2 == 0)
<li class="nav-item mb-2">
<a class="nav-link p-0 text-body-secondary"
href="{{ route('home.country.category', ['country' => $category->country_locale_slug, 'category' => $category->slug]) }}">{{ $category->name }}</a>
</li>
@endif
@endforeach
</ul>
</div>
<div class="col-6 col-md-2 mb-3">
<ul class="nav flex-column">
@foreach ($categories as $category)
@if ($category->id % 2 == 1)
<li class="nav-item mb-2">
<a href="#" class="nav-link p-0 text-body-secondary">About Us</a>
</li>
<li class="nav-item mb-2">
<a href="#" class="nav-link p-0 text-body-secondary">Contact Us</a>
</li>
<li class="nav-item mb-2">
<a href="#" class="nav-link p-0 text-body-secondary">Advertise with us</a>
<a class="nav-link p-0 text-body-secondary"
href="{{ route('home.country.category', ['country' => $category->country_locale_slug, 'category' => $category->slug]) }}">{{ $category->name }}</a>
</li>
@endif
@endforeach
</ul>
</div>
<div class="col-md-5 offset-md-1 mb-3">
<div class="col-md-5 mb-3">
@if ($country_locales->count() > 1)
@@ -52,15 +51,15 @@
</div>
@endif
<form>
{{-- <form>
<h5>Subscribe to our newsletter</h5>
<p>Monthly digest of what's new and exciting from us.</p>
<div class="d-flex flex-column flex-sm-row w-100 gap-2">
<label for="newsletter1" class="visually-hidden">Email address</label>
<input id="newsletter1" type="text" class="form-control" placeholder="Email address">
<input id="newsletter1" type="disabled" class="form-control disabled" placeholder="Email address">
<button class="btn btn-primary" type="button">Subscribe</button>
</div>
</form>
</form> --}}
</div>
</div>

View File

@@ -1,10 +1,10 @@
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Models\CountryLocale;
use App\Models\Post;
use App\Models\Author;
use App\Models\Category;
use App\Models\CountryLocale;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
@@ -17,30 +17,44 @@
|
*/
Route::post('login', [App\Http\Controllers\Auth\LoginController::class, 'login'])->name('api.auth.login.post');
Route::post('login', [App\Http\Controllers\Auth\LoginController::class, 'login'])->name('api.auth.login.post');
Route::middleware('auth:sanctum')->post('logout',[App\Http\Controllers\Auth\LoginController::class,'logout'])->name('api.auth.logout.post');
Route::middleware('auth:sanctum')->post('logout', [App\Http\Controllers\Auth\LoginController::class, 'logout'])->name('api.auth.logout.post');
Route::prefix('admin')->middleware('auth:sanctum')->group(function () {
Route::get('/country-locales', function() {
Route::get('/post/{id}', function ($id) {
$post = Post::with('post_category.category')->find($id);
return response()->json(compact('post'));
})->name('api.admin.post.get');
Route::get('/country-locales', function () {
$country_locales = CountryLocale::where('enabled', true)->get();
$default_locale_slug = 'my';
return response()->json(compact('country_locales','default_locale_slug'));
return response()->json(compact('country_locales', 'default_locale_slug'));
})->name('api.admin.country-locales');
Route::get('/categories/{country_locale_slug}', function($country_locale_slug) {
Route::get('/categories/{country_locale_slug}', function ($country_locale_slug) {
$categories = Category::where('enabled', true)->where('country_locale_slug', $country_locale_slug)->get();
return response()->json(compact('categories'));
})->name('api.admin.categories');
Route::get('/authors', function () {
$authors = Author::where('enabled', true)->get();
return response()->json(compact('authors'));
})->name('api.admin.authors');
Route::post('image/upload', [App\Http\Controllers\Services\ImageUploadController::class, 'index'])->name('api.admin.upload.cloud.image');
Route::post('admin/post/upsert', [App\Http\Controllers\Admin\PostController::class, 'postUpsert'])->name('api.admin.post.upsert');
});
Route::post('admin/image/upload', [App\Http\Controllers\Services\ImageUploadController::class, 'index'])->name('api.admin.upload.cloud.image');