Add (new post)

This commit is contained in:
2023-07-29 02:20:51 +08:00
parent cb371fae26
commit 58b939f72e
26 changed files with 3754 additions and 62 deletions

View File

@@ -43,6 +43,13 @@ AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
CLOUDFLARE_R2_ACCESS_KEY_ID=xxxx
CLOUDFLARE_R2_SECRET_ACCESS_yyyy
CLOUDFLARE_R2_BUCKET=zzz
CLOUDFLARE_R2_ENDPOINT=https://xxxxxx.r2.cloudflarestorage.com/zzz
CLOUDFLARE_R2_URL=https://pub-xxxx.r2.dev/zzzz
CLOUDFLARE_R2_REGION=us-east-1
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=

View File

@@ -17,11 +17,20 @@ public function index(Request $request)
public function new(Request $request)
{
return 'PostController@new';
$post = null;
return view('admin.posts.upsert', compact('post'));
}
public function edit(Request $request, $post_id)
{
return 'PostController@edit : '.$post_id;
$post = Post::find($post_id);
if (!is_null($post))
{
return view('admin.posts.upsert', compact('post'));
}
return redirect()->back()->with('error','Post does not exist.');
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers\Services;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;
use Illuminate\Support\Str;
class ImageUploadController extends Controller
{
public function index(Request $request)
{
// Validate the incoming request to ensure it contains a file
$request->validate([
'file' => 'required|file|mimes:jpeg,png,gif,bmp,tiff,webp,heic|max:20480', // Allow all image file types, maximum size of 2MB (you can adjust the size as needed)
]);
// Get the file from the request
$file = $request->file('file');
// Generate a unique filename for the uploaded file and LQIP version
$uuid = Str::uuid()->toString();
$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);
// Get the original image width and height
$originalWidth = $image->width();
$originalHeight = $image->height();
// Resize/upscale the image to 1920x1080 maintaining the aspect ratio and cropping if needed
$image->fit(1920, 1080, function ($constraint) {
$constraint->upsize();
$constraint->aspectRatio();
});
// Compress the image to reduce file size to 50%
$image->encode('jpg', 50);
// Save the processed image to the 'r2' storage driver under the 'uploads' directory
$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
$tempImagePath = tempnam(sys_get_temp_dir(), 'temp_image');
file_put_contents($tempImagePath, $file->get());
$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) {
$constraint->aspectRatio();
});
$lqipImage->encode('jpg', 5);
Storage::disk('r2')->put($lqipFilePath, $lqipImage->stream()->detach());
// Cleanup the temporary image file
unlink($tempImagePath);
// Get the final URL of the uploaded image (non-LQIP version)
$url = Storage::disk('r2')->url($filePath);
// Return the JSON response with the image URL (non-LQIP version)
return response()->json([
'success' => 1,
'file' => [
'url' => $url,
],
]);
}
}

View File

@@ -40,7 +40,7 @@ class Kernel extends HttpKernel
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],

View File

@@ -15,12 +15,15 @@
"genealabs/laravel-model-caching": "^0.13.4",
"glhd/laravel-timezone-mapper": "^1.4",
"guzzlehttp/guzzle": "^7.2",
"intervention/image": "^2.7",
"kalnoy/nestedset": "^6.0",
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.2",
"laravel/tinker": "^2.8",
"laravel/ui": "^4.0",
"stevebauman/location": "^7.0"
"stevebauman/location": "^7.0",
"tightenco/ziggy": "^1.6",
"league/flysystem-aws-s3-v3": "^3.0"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.8",

489
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "531ac89c0e295fb99620a832403db966",
"content-hash": "a0cadb98cfe6b8b1794e8816dbe50752",
"packages": [
{
"name": "alaminfirdows/laravel-editorjs",
@@ -131,6 +131,155 @@
},
"time": "2023-05-09T14:20:42+00:00"
},
{
"name": "aws/aws-crt-php",
"version": "v1.2.1",
"source": {
"type": "git",
"url": "https://github.com/awslabs/aws-crt-php.git",
"reference": "1926277fc71d253dfa820271ac5987bdb193ccf5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/1926277fc71d253dfa820271ac5987bdb193ccf5",
"reference": "1926277fc71d253dfa820271ac5987bdb193ccf5",
"shasum": ""
},
"require": {
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35||^5.6.3||^9.5",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
"ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality."
},
"type": "library",
"autoload": {
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "AWS SDK Common Runtime Team",
"email": "aws-sdk-common-runtime@amazon.com"
}
],
"description": "AWS Common Runtime for PHP",
"homepage": "https://github.com/awslabs/aws-crt-php",
"keywords": [
"amazon",
"aws",
"crt",
"sdk"
],
"support": {
"issues": "https://github.com/awslabs/aws-crt-php/issues",
"source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.1"
},
"time": "2023-03-24T20:22:19+00:00"
},
{
"name": "aws/aws-sdk-php",
"version": "3.269.0",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78",
"reference": "6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78",
"shasum": ""
},
"require": {
"aws/aws-crt-php": "^1.0.4",
"ext-json": "*",
"ext-pcre": "*",
"ext-simplexml": "*",
"guzzlehttp/guzzle": "^6.5.8 || ^7.4.5",
"guzzlehttp/promises": "^1.4.0",
"guzzlehttp/psr7": "^1.9.1 || ^2.4.5",
"mtdowling/jmespath.php": "^2.6",
"php": ">=5.5"
},
"require-dev": {
"andrewsville/php-token-reflection": "^1.4",
"aws/aws-php-sns-message-validator": "~1.0",
"behat/behat": "~3.0",
"composer/composer": "^1.10.22",
"dms/phpunit-arraysubset-asserts": "^0.4.0",
"doctrine/cache": "~1.4",
"ext-dom": "*",
"ext-openssl": "*",
"ext-pcntl": "*",
"ext-sockets": "*",
"nette/neon": "^2.3",
"paragonie/random_compat": ">= 2",
"phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5",
"psr/cache": "^1.0",
"psr/http-message": "^1.0",
"psr/simple-cache": "^1.0",
"sebastian/comparator": "^1.2.3 || ^4.0",
"yoast/phpunit-polyfills": "^1.0"
},
"suggest": {
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
"doctrine/cache": "To use the DoctrineCacheAdapter",
"ext-curl": "To send requests using cURL",
"ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
"ext-sockets": "To use client-side monitoring"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"files": [
"src/functions.php"
],
"psr-4": {
"Aws\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Amazon Web Services",
"homepage": "http://aws.amazon.com"
}
],
"description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
"homepage": "http://aws.amazon.com/sdkforphp",
"keywords": [
"amazon",
"aws",
"cloud",
"dynamodb",
"ec2",
"glacier",
"s3",
"sdk"
],
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.269.0"
},
"time": "2023-04-26T18:21:04+00:00"
},
{
"name": "brick/math",
"version": "0.11.0",
@@ -1239,33 +1388,29 @@
},
{
"name": "guzzlehttp/promises",
"version": "2.0.0",
"version": "1.5.3",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6"
"reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/3a494dc7dc1d7d12e511890177ae2d0e6c107da6",
"reference": "3a494dc7dc1d7d12e511890177ae2d0e6c107da6",
"url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e",
"reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
"php": ">=5.5"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.1",
"phpunit/phpunit": "^8.5.29 || ^9.5.23"
"symfony/phpunit-bridge": "^4.4 || ^5.1"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
}
},
"autoload": {
"files": [
"src/functions_include.php"
],
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
}
@@ -1302,7 +1447,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.0.0"
"source": "https://github.com/guzzle/promises/tree/1.5.3"
},
"funding": [
{
@@ -1318,7 +1463,7 @@
"type": "tidelift"
}
],
"time": "2023-05-21T13:50:22+00:00"
"time": "2023-05-21T12:31:43+00:00"
},
{
"name": "guzzlehttp/psr7",
@@ -1520,6 +1665,90 @@
],
"time": "2021-10-07T12:57:01+00:00"
},
{
"name": "intervention/image",
"version": "2.7.2",
"source": {
"type": "git",
"url": "https://github.com/Intervention/image.git",
"reference": "04be355f8d6734c826045d02a1079ad658322dad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/04be355f8d6734c826045d02a1079ad658322dad",
"reference": "04be355f8d6734c826045d02a1079ad658322dad",
"shasum": ""
},
"require": {
"ext-fileinfo": "*",
"guzzlehttp/psr7": "~1.1 || ^2.0",
"php": ">=5.4.0"
},
"require-dev": {
"mockery/mockery": "~0.9.2",
"phpunit/phpunit": "^4.8 || ^5.7 || ^7.5.15"
},
"suggest": {
"ext-gd": "to use GD library based image processing.",
"ext-imagick": "to use Imagick based image processing.",
"intervention/imagecache": "Caching extension for the Intervention Image library"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.4-dev"
},
"laravel": {
"providers": [
"Intervention\\Image\\ImageServiceProvider"
],
"aliases": {
"Image": "Intervention\\Image\\Facades\\Image"
}
}
},
"autoload": {
"psr-4": {
"Intervention\\Image\\": "src/Intervention/Image"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Oliver Vogel",
"email": "oliver@intervention.io",
"homepage": "https://intervention.io/"
}
],
"description": "Image handling and manipulation library with support for Laravel integration",
"homepage": "http://image.intervention.io/",
"keywords": [
"gd",
"image",
"imagick",
"laravel",
"thumbnail",
"watermark"
],
"support": {
"issues": "https://github.com/Intervention/image/issues",
"source": "https://github.com/Intervention/image/tree/2.7.2"
},
"funding": [
{
"url": "https://paypal.me/interventionio",
"type": "custom"
},
{
"url": "https://github.com/Intervention",
"type": "github"
}
],
"time": "2022-05-21T17:30:32+00:00"
},
{
"name": "kalnoy/nestedset",
"version": "v6.0.2",
@@ -1585,16 +1814,16 @@
},
{
"name": "laravel/framework",
"version": "v10.16.0",
"version": "v10.16.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "7589f09fe0e8735615cf25f0f1807682aac124c1"
"reference": "5c93d2795c393b462481179ce42dedfb30cc19b5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/7589f09fe0e8735615cf25f0f1807682aac124c1",
"reference": "7589f09fe0e8735615cf25f0f1807682aac124c1",
"url": "https://api.github.com/repos/laravel/framework/zipball/5c93d2795c393b462481179ce42dedfb30cc19b5",
"reference": "5c93d2795c393b462481179ce42dedfb30cc19b5",
"shasum": ""
},
"require": {
@@ -1781,7 +2010,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2023-07-25T14:31:32+00:00"
"time": "2023-07-26T03:30:46+00:00"
},
{
"name": "laravel/sanctum",
@@ -2315,6 +2544,72 @@
],
"time": "2023-05-04T09:04:26+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
"version": "3.15.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
"reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/d8de61ee10b6a607e7996cff388c5a3a663e8c8a",
"reference": "d8de61ee10b6a607e7996cff388c5a3a663e8c8a",
"shasum": ""
},
"require": {
"aws/aws-sdk-php": "^3.220.0",
"league/flysystem": "^3.10.0",
"league/mime-type-detection": "^1.0.0",
"php": "^8.0.2"
},
"conflict": {
"guzzlehttp/guzzle": "<7.0",
"guzzlehttp/ringphp": "<1.1.1"
},
"type": "library",
"autoload": {
"psr-4": {
"League\\Flysystem\\AwsS3V3\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frank de Jonge",
"email": "info@frankdejonge.nl"
}
],
"description": "AWS S3 filesystem adapter for Flysystem.",
"keywords": [
"Flysystem",
"aws",
"file",
"files",
"filesystem",
"s3",
"storage"
],
"support": {
"issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues",
"source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.15.0"
},
"funding": [
{
"url": "https://ecologi.com/frankdejonge",
"type": "custom"
},
{
"url": "https://github.com/frankdejonge",
"type": "github"
}
],
"time": "2023-05-02T20:02:14+00:00"
},
{
"name": "league/flysystem-local",
"version": "3.15.0",
@@ -2648,6 +2943,67 @@
],
"time": "2023-06-21T08:46:11+00:00"
},
{
"name": "mtdowling/jmespath.php",
"version": "2.6.1",
"source": {
"type": "git",
"url": "https://github.com/jmespath/jmespath.php.git",
"reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/9b87907a81b87bc76d19a7fb2d61e61486ee9edb",
"reference": "9b87907a81b87bc76d19a7fb2d61e61486ee9edb",
"shasum": ""
},
"require": {
"php": "^5.4 || ^7.0 || ^8.0",
"symfony/polyfill-mbstring": "^1.17"
},
"require-dev": {
"composer/xdebug-handler": "^1.4 || ^2.0",
"phpunit/phpunit": "^4.8.36 || ^7.5.15"
},
"bin": [
"bin/jp.php"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.6-dev"
}
},
"autoload": {
"files": [
"src/JmesPath.php"
],
"psr-4": {
"JmesPath\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Declaratively specify how to extract elements from a JSON document",
"keywords": [
"json",
"jsonpath"
],
"support": {
"issues": "https://github.com/jmespath/jmespath.php/issues",
"source": "https://github.com/jmespath/jmespath.php/tree/2.6.1"
},
"time": "2021-06-14T00:11:39+00:00"
},
{
"name": "nesbot/carbon",
"version": "2.68.1",
@@ -6092,6 +6448,73 @@
],
"time": "2023-06-21T12:08:28+00:00"
},
{
"name": "tightenco/ziggy",
"version": "v1.6.0",
"source": {
"type": "git",
"url": "https://github.com/tighten/ziggy.git",
"reference": "3beb080be60b1eadad043f3773a160df13fa215f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tighten/ziggy/zipball/3beb080be60b1eadad043f3773a160df13fa215f",
"reference": "3beb080be60b1eadad043f3773a160df13fa215f",
"shasum": ""
},
"require": {
"ext-json": "*",
"laravel/framework": ">=5.4@dev"
},
"require-dev": {
"orchestra/testbench": "^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0",
"phpunit/phpunit": "^6.0 || ^7.0 || ^8.0 || ^9.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Tightenco\\Ziggy\\ZiggyServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Tightenco\\Ziggy\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Daniel Coulbourne",
"email": "daniel@tighten.co"
},
{
"name": "Jake Bathman",
"email": "jake@tighten.co"
},
{
"name": "Jacob Baker-Kretzmar",
"email": "jacob@tighten.co"
}
],
"description": "Generates a Blade directive exporting all of your named Laravel routes. Also provides a nice route() helper function in JavaScript.",
"homepage": "https://github.com/tighten/ziggy",
"keywords": [
"Ziggy",
"javascript",
"laravel",
"routes"
],
"support": {
"issues": "https://github.com/tighten/ziggy/issues",
"source": "https://github.com/tighten/ziggy/tree/v1.6.0"
},
"time": "2023-05-12T20:08:56+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
"version": "2.2.6",
@@ -7574,16 +7997,16 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "10.1.2",
"version": "10.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "db1497ec8dd382e82c962f7abbe0320e4882ee4e"
"reference": "be1fe461fdc917de2a29a452ccf2657d325b443d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/db1497ec8dd382e82c962f7abbe0320e4882ee4e",
"reference": "db1497ec8dd382e82c962f7abbe0320e4882ee4e",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/be1fe461fdc917de2a29a452ccf2657d325b443d",
"reference": "be1fe461fdc917de2a29a452ccf2657d325b443d",
"shasum": ""
},
"require": {
@@ -7640,7 +8063,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.2"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.3"
},
"funding": [
{
@@ -7648,7 +8071,7 @@
"type": "github"
}
],
"time": "2023-05-22T09:04:27+00:00"
"time": "2023-07-26T13:45:28+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -9080,16 +9503,16 @@
},
{
"name": "spatie/flare-client-php",
"version": "1.4.1",
"version": "1.4.2",
"source": {
"type": "git",
"url": "https://github.com/spatie/flare-client-php.git",
"reference": "943894c6a6b00501365ac0b91ae0dce56f2226fa"
"reference": "5f2c6a7a0d2c1d90c12559dc7828fd942911a544"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/flare-client-php/zipball/943894c6a6b00501365ac0b91ae0dce56f2226fa",
"reference": "943894c6a6b00501365ac0b91ae0dce56f2226fa",
"url": "https://api.github.com/repos/spatie/flare-client-php/zipball/5f2c6a7a0d2c1d90c12559dc7828fd942911a544",
"reference": "5f2c6a7a0d2c1d90c12559dc7828fd942911a544",
"shasum": ""
},
"require": {
@@ -9138,7 +9561,7 @@
],
"support": {
"issues": "https://github.com/spatie/flare-client-php/issues",
"source": "https://github.com/spatie/flare-client-php/tree/1.4.1"
"source": "https://github.com/spatie/flare-client-php/tree/1.4.2"
},
"funding": [
{
@@ -9146,7 +9569,7 @@
"type": "github"
}
],
"time": "2023-07-06T09:29:49+00:00"
"time": "2023-07-28T08:07:24+00:00"
},
{
"name": "spatie/ignition",

View File

@@ -56,6 +56,20 @@
'throw' => false,
],
'r2' => [
'driver' => 's3',
'key' => env('CLOUDFLARE_R2_ACCESS_KEY_ID'),
'secret' => env('CLOUDFLARE_R2_SECRET_ACCESS_KEY'),
'region' => env('CLOUDFLARE_R2_REGION'),
'bucket' => env('CLOUDFLARE_R2_BUCKET'),
'url' => env('CLOUDFLARE_R2_URL'),
'visibility' => 'public',
'endpoint' => env('CLOUDFLARE_R2_ENDPOINT'),
'use_path_style_endpoint' => env('CLOUDFLARE_R2_USE_PATH_STYLE_ENDPOINT', false),
'throw' => true,
],
],
/*

1682
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,18 +6,38 @@
"build": "vite build"
},
"devDependencies": {
"@editorjs/image": "^2.8.1",
"@editorjs/raw": "^2.4.0",
"@popperjs/core": "^2.11.8",
"@tabler/core": "^1.0.0-beta19",
"@vitejs/plugin-vue": "^4.2.3",
"autosize": "^6.0.1",
"axios": "^1.1.2",
"bootstrap": "~5.3.0",
"editorjs-inline-image": "^1.2.4",
"imask": "^6.6.1",
"laravel-vite-plugin": "^0.7.5",
"resolve-url-loader": "^5.0.0",
"sass": "^1.64.1",
"unplugin-vue-components": "^0.25.1",
"vite": "^4.4.6"
},
"dependencies": {
"bootstrap-icons": "^1.10.5"
"@editorjs/editorjs": "^2.27.2",
"@editorjs/embed": "^2.5.3",
"@editorjs/header": "^2.7.0",
"@editorjs/list": "^1.8.0",
"@editorjs/paragraph": "^2.9.0",
"bootstrap-icons": "^1.10.5",
"js-cookie": "^3.0.5",
"mitt": "^3.0.1",
"pinia": "^2.1.6",
"vue": "^3.3.4",
"vue-axios": "^3.5.2",
"vue-loader": "^17.2.2",
"vue-router": "^4.2.4",
"vue-scrollto": "^2.20.0",
"vue3-toastify": "^0.1.11",
"ziggy-js": "^1.6.0"
}
}

622
resources/css/toastify.css Normal file
View File

@@ -0,0 +1,622 @@
:root {
--toastify-color-light: #fff;
--toastify-color-dark: #121212;
--toastify-color-info: #3498db;
--toastify-color-success: #07bc0c;
--toastify-color-warning: #f1c40f;
--toastify-color-error: #e74c3c;
--toastify-color-transparent: #ffffffb3;
--toastify-icon-color-info: var(--toastify-color-info);
--toastify-icon-color-success: var(--toastify-color-success);
--toastify-icon-color-warning: var(--toastify-color-warning);
--toastify-icon-color-error: var(--toastify-color-error);
--toastify-toast-width: 320px;
--toastify-toast-background: #fff;
--toastify-toast-min-height: 64px;
--toastify-toast-max-height: 800px;
--toastify-font-family: inherit;
--toastify-z-index: 9999;
--toastify-text-color-light: #757575;
--toastify-text-color-dark: #fff;
--toastify-text-color-info: #fff;
--toastify-text-color-success: #fff;
--toastify-text-color-warning: #fff;
--toastify-text-color-error: #fff;
--toastify-spinner-color: #616161;
--toastify-spinner-color-empty-area: #e0e0e0;
--toastify-color-progress-light: linear-gradient(
90deg,
#4cd964,
#5ac8fa,
#007aff,
#34aadc,
#5856d6,
#ff2d55
);
--toastify-color-progress-dark: #bb86fc;
--toastify-color-progress-info: var(--toastify-color-info);
--toastify-color-progress-success: var(--toastify-color-success);
--toastify-color-progress-warning: var(--toastify-color-warning);
--toastify-color-progress-error: var(--toastify-color-error);
--toastify-color-progress-colored: #ddd;
}
.Toastify__toast-container {
box-sizing: border-box;
color: #fff;
padding: 4px;
position: fixed;
transform: translate3d(0, 0, var(--toastify-z-index) px);
width: var(--toastify-toast-width);
z-index: var(--toastify-z-index);
}
.Toastify__toast-container--top-left {
left: 1em;
top: 1em;
}
.Toastify__toast-container--top-center {
left: 50%;
top: 1em;
transform: translateX(-50%);
}
.Toastify__toast-container--top-right {
right: 1em;
top: 1em;
}
.Toastify__toast-container--bottom-left {
bottom: 1em;
left: 1em;
}
.Toastify__toast-container--bottom-center {
bottom: 1em;
left: 50%;
transform: translateX(-50%);
}
.Toastify__toast-container--bottom-right {
bottom: 1em;
right: 1em;
}
@media only screen and (max-width: 480px) {
.Toastify__toast-container {
left: 0;
margin: 0;
padding: 0;
width: 100vw;
}
.Toastify__toast-container--top-center,
.Toastify__toast-container--top-left,
.Toastify__toast-container--top-right {
top: 0;
transform: translateX(0);
}
.Toastify__toast-container--bottom-center,
.Toastify__toast-container--bottom-left,
.Toastify__toast-container--bottom-right {
bottom: 0;
transform: translateX(0);
}
.Toastify__toast-container--rtl {
left: auto;
right: 0;
}
}
.Toastify__toast {
border-radius: 4px;
box-shadow: 0 1px 10px 0 #0000001a, 0 2px 15px 0 #0000000d;
box-sizing: border-box;
cursor: pointer;
direction: ltr;
display: flex;
font-family: var(--toastify-font-family);
justify-content: space-between;
margin-bottom: 1rem;
max-height: var(--toastify-toast-max-height);
min-height: var(--toastify-toast-min-height);
overflow: hidden;
padding: 8px;
position: relative;
z-index: 0;
}
.Toastify__toast--rtl {
direction: rtl;
}
.Toastify__toast-body {
align-items: center;
display: flex;
flex: 1 1 auto;
margin: auto 0;
padding: 6px;
white-space: pre-wrap;
}
.Toastify__toast-body > div:last-child {
flex: 1;
}
.Toastify__toast-icon {
display: flex;
flex-shrink: 0;
margin-inline-end: 10px;
width: 20px;
}
.Toastify--animate {
animation-duration: 0.7s;
animation-fill-mode: both;
}
.Toastify--animate-icon {
animation-duration: 0.3s;
animation-fill-mode: both;
}
@media only screen and (max-width: 480px) {
.Toastify__toast {
border-radius: 0;
margin-bottom: 0;
}
}
.Toastify__toast-theme--dark {
background: var(--toastify-color-dark);
color: var(--toastify-text-color-dark);
}
.Toastify__toast-theme--colored.Toastify__toast--default,
.Toastify__toast-theme--light {
background: var(--toastify-color-light);
color: var(--toastify-text-color-light);
}
.Toastify__toast-theme--colored.Toastify__toast--info {
background: var(--toastify-color-info);
color: var(--toastify-text-color-info);
}
.Toastify__toast-theme--colored.Toastify__toast--success {
background: var(--toastify-color-success);
color: var(--toastify-text-color-success);
}
.Toastify__toast-theme--colored.Toastify__toast--warning {
background: var(--toastify-color-warning);
color: var(--toastify-text-color-warning);
}
.Toastify__toast-theme--colored.Toastify__toast--error {
background: var(--toastify-color-error);
color: var(--toastify-text-color-error);
}
.Toastify__progress-bar-theme--light {
background: var(--toastify-color-progress-light);
}
.Toastify__progress-bar-theme--dark {
background: var(--toastify-color-progress-dark);
}
.Toastify__progress-bar--info {
background: var(--toastify-color-progress-info);
}
.Toastify__progress-bar--success {
background: var(--toastify-color-progress-success);
}
.Toastify__progress-bar--warning {
background: var(--toastify-color-progress-warning);
}
.Toastify__progress-bar--error {
background: var(--toastify-color-progress-error);
}
.Toastify__progress-bar-theme--colored.Toastify__progress-bar--default {
background: var(--toastify-color-progress-colored);
}
.Toastify__progress-bar-theme--colored.Toastify__progress-bar--error,
.Toastify__progress-bar-theme--colored.Toastify__progress-bar--info,
.Toastify__progress-bar-theme--colored.Toastify__progress-bar--success,
.Toastify__progress-bar-theme--colored.Toastify__progress-bar--warning {
background: var(--toastify-color-transparent);
}
.Toastify__close-button {
align-self: flex-start;
background: #0000;
border: none;
color: #fff;
cursor: pointer;
opacity: 0.7;
outline: none;
padding: 0;
transition: 0.3s ease;
}
.Toastify__close-button--light {
color: #000;
opacity: 0.3;
}
.Toastify__close-button > svg {
fill: currentcolor;
height: 16px;
width: 14px;
}
.Toastify__close-button:focus,
.Toastify__close-button:hover {
opacity: 1;
}
@keyframes Toastify__trackProgress {
0% {
transform: scaleX(1);
}
to {
transform: scaleX(0);
}
}
.Toastify__progress-bar {
bottom: 0;
height: 5px;
left: 0;
opacity: 0.7;
position: absolute;
transform-origin: left;
width: 100%;
z-index: var(--toastify-z-index);
}
.Toastify__progress-bar--animated {
animation: Toastify__trackProgress linear 1 forwards;
}
.Toastify__progress-bar--controlled {
transition: transform 0.2s;
}
.Toastify__progress-bar--rtl {
left: auto;
right: 0;
transform-origin: right;
}
.Toastify__spinner {
animation: Toastify__spin 0.65s linear infinite;
border: 2px solid;
border-color: var(--toastify-spinner-color-empty-area);
border-radius: 100%;
border-right-color: var(--toastify-spinner-color);
box-sizing: border-box;
height: 20px;
width: 20px;
}
@keyframes Toastify__bounceInRight {
0%,
60%,
75%,
90%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: translate3d(3000px, 0, 0);
}
60% {
opacity: 1;
transform: translate3d(-25px, 0, 0);
}
75% {
transform: translate3d(10px, 0, 0);
}
90% {
transform: translate3d(-5px, 0, 0);
}
to {
transform: none;
}
}
@keyframes Toastify__bounceOutRight {
20% {
opacity: 1;
transform: translate3d(-20px, 0, 0);
}
to {
opacity: 0;
transform: translate3d(2000px, 0, 0);
}
}
@keyframes Toastify__bounceInLeft {
0%,
60%,
75%,
90%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: translate3d(-3000px, 0, 0);
}
60% {
opacity: 1;
transform: translate3d(25px, 0, 0);
}
75% {
transform: translate3d(-10px, 0, 0);
}
90% {
transform: translate3d(5px, 0, 0);
}
to {
transform: none;
}
}
@keyframes Toastify__bounceOutLeft {
20% {
opacity: 1;
transform: translate3d(20px, 0, 0);
}
to {
opacity: 0;
transform: translate3d(-2000px, 0, 0);
}
}
@keyframes Toastify__bounceInUp {
0%,
60%,
75%,
90%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: translate3d(0, 3000px, 0);
}
60% {
opacity: 1;
transform: translate3d(0, -20px, 0);
}
75% {
transform: translate3d(0, 10px, 0);
}
90% {
transform: translate3d(0, -5px, 0);
}
to {
transform: translateZ(0);
}
}
@keyframes Toastify__bounceOutUp {
20% {
transform: translate3d(0, -10px, 0);
}
40%,
45% {
opacity: 1;
transform: translate3d(0, 20px, 0);
}
to {
opacity: 0;
transform: translate3d(0, -2000px, 0);
}
}
@keyframes Toastify__bounceInDown {
0%,
60%,
75%,
90%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: translate3d(0, -3000px, 0);
}
60% {
opacity: 1;
transform: translate3d(0, 25px, 0);
}
75% {
transform: translate3d(0, -10px, 0);
}
90% {
transform: translate3d(0, 5px, 0);
}
to {
transform: none;
}
}
@keyframes Toastify__bounceOutDown {
20% {
transform: translate3d(0, 10px, 0);
}
40%,
45% {
opacity: 1;
transform: translate3d(0, -20px, 0);
}
to {
opacity: 0;
transform: translate3d(0, 2000px, 0);
}
}
.Toastify__bounce-enter--bottom-left,
.Toastify__bounce-enter--top-left {
animation-name: Toastify__bounceInLeft;
}
.Toastify__bounce-enter--bottom-right,
.Toastify__bounce-enter--top-right {
animation-name: Toastify__bounceInRight;
}
.Toastify__bounce-enter--top-center {
animation-name: Toastify__bounceInDown;
}
.Toastify__bounce-enter--bottom-center {
animation-name: Toastify__bounceInUp;
}
.Toastify__bounce-exit--bottom-left,
.Toastify__bounce-exit--top-left {
animation-name: Toastify__bounceOutLeft;
}
.Toastify__bounce-exit--bottom-right,
.Toastify__bounce-exit--top-right {
animation-name: Toastify__bounceOutRight;
}
.Toastify__bounce-exit--top-center {
animation-name: Toastify__bounceOutUp;
}
.Toastify__bounce-exit--bottom-center {
animation-name: Toastify__bounceOutDown;
}
@keyframes Toastify__zoomIn {
0% {
opacity: 0;
transform: scale3d(0.3, 0.3, 0.3);
}
50% {
opacity: 1;
}
}
@keyframes Toastify__zoomOut {
0% {
opacity: 1;
}
50% {
opacity: 0;
transform: scale3d(0.3, 0.3, 0.3);
}
to {
opacity: 0;
}
}
.Toastify__zoom-enter {
animation-name: Toastify__zoomIn;
}
.Toastify__zoom-exit {
animation-name: Toastify__zoomOut;
}
@keyframes Toastify__flipIn {
0% {
animation-timing-function: ease-in;
opacity: 0;
transform: perspective(400px) rotateX(90deg);
}
40% {
animation-timing-function: ease-in;
transform: perspective(400px) rotateX(-20deg);
}
60% {
opacity: 1;
transform: perspective(400px) rotateX(10deg);
}
80% {
transform: perspective(400px) rotateX(-5deg);
}
to {
transform: perspective(400px);
}
}
@keyframes Toastify__flipOut {
0% {
transform: perspective(400px);
}
30% {
opacity: 1;
transform: perspective(400px) rotateX(-20deg);
}
to {
opacity: 0;
transform: perspective(400px) rotateX(90deg);
}
}
.Toastify__flip-enter {
animation-name: Toastify__flipIn;
}
.Toastify__flip-exit {
animation-name: Toastify__flipOut;
}
@keyframes Toastify__slideInRight {
0% {
transform: translate3d(110%, 0, 0);
visibility: visible;
}
to {
transform: translateZ(0);
}
}
@keyframes Toastify__slideInLeft {
0% {
transform: translate3d(-110%, 0, 0);
visibility: visible;
}
to {
transform: translateZ(0);
}
}
@keyframes Toastify__slideInUp {
0% {
transform: translate3d(0, 110%, 0);
visibility: visible;
}
to {
transform: translateZ(0);
}
}
@keyframes Toastify__slideInDown {
0% {
transform: translate3d(0, -110%, 0);
visibility: visible;
}
to {
transform: translateZ(0);
}
}
@keyframes Toastify__slideOutRight {
0% {
transform: translateZ(0);
}
to {
transform: translate3d(110%, 0, 0);
visibility: hidden;
}
}
@keyframes Toastify__slideOutLeft {
0% {
transform: translateZ(0);
}
to {
transform: translate3d(-110%, 0, 0);
visibility: hidden;
}
}
@keyframes Toastify__slideOutDown {
0% {
transform: translateZ(0);
}
to {
transform: translate3d(0, 500px, 0);
visibility: hidden;
}
}
@keyframes Toastify__slideOutUp {
0% {
transform: translateZ(0);
}
to {
transform: translate3d(0, -500px, 0);
visibility: hidden;
}
}
.Toastify__slide-enter--bottom-left,
.Toastify__slide-enter--top-left {
animation-name: Toastify__slideInLeft;
}
.Toastify__slide-enter--bottom-right,
.Toastify__slide-enter--top-right {
animation-name: Toastify__slideInRight;
}
.Toastify__slide-enter--top-center {
animation-name: Toastify__slideInDown;
}
.Toastify__slide-enter--bottom-center {
animation-name: Toastify__slideInUp;
}
.Toastify__slide-exit--bottom-left,
.Toastify__slide-exit--top-left {
animation-name: Toastify__slideOutLeft;
}
.Toastify__slide-exit--bottom-right,
.Toastify__slide-exit--top-right {
animation-name: Toastify__slideOutRight;
}
.Toastify__slide-exit--top-center {
animation-name: Toastify__slideOutUp;
}
.Toastify__slide-exit--bottom-center {
animation-name: Toastify__slideOutDown;
}
@keyframes Toastify__spin {
0% {
transform: rotate(0deg);
}
to {
transform: rotate(1turn);
}
}

View File

@@ -1,4 +1,57 @@
import "@tabler/core/src/js/tabler.js";
import '@tabler/core/src/js/tabler.js';
import "./bootstrap";
import './bootstrap';
import { createApp, defineAsyncComponent } from "vue";
import AdminApp from "@/vue/AdminApp.vue";
const app = createApp({ AdminApp });
const vueComponents = import.meta.glob("@/vue/**/*.vue", {
eager: false,
});
console.log(vueComponents);
import.meta.glob(["../images/**", "../fonts/**"]);
import { createPinia } from "pinia";
app.use(createPinia());
import axios from "./plugins/axios";
import VueAxios from "vue-axios";
app.use(VueAxios, axios);
import auth from "./plugins/auth";
app.use(auth);
import eventBus from "./plugins/event-bus";
app.use(eventBus);
import Vue3Toastify from "vue3-toastify";
import "../css/toastify.css";
app.use(Vue3Toastify);
import { Ziggy as ZiggyRoute } from "./ziggy";
import { ZiggyVue } from "ziggy-js/dist/vue";
app.use(ZiggyVue, ZiggyRoute);
window.Ziggy = ZiggyRoute;
Object.entries({ ...vueComponents }).forEach(([path, definition]) => {
// Get name of component, based on filename
// "./components/Fruits.vue" will become "Fruits"
const componentName = path
.split("/")
.pop()
.replace(/\.\w+$/, "")
.replace(/([a-z])([A-Z])/g, "$1-$2")
.toLowerCase();
// console.log(componentName);
// console.log(typeof definition);
// Register component on this Vue instance
app.component(componentName, defineAsyncComponent(definition));
});
app.mount("#app");

View File

@@ -0,0 +1,11 @@
import { useAuthStore } from "@/stores/useAuth";
export default {
install: ({ config }) => {
config.globalProperties.$auth = useAuthStore();
if (useAuthStore().loggedIn) {
useAuthStore().ftechUser();
}
},
};

View File

@@ -0,0 +1,71 @@
import { useErrorStore } from "../stores/useError";
import axios from "axios";
import Cookies from "js-cookie";
axios.defaults.headers.common["Authorization"] = localStorage.getItem("token");
axios.defaults.withCredentials = true;
axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
const setCSRFToken = () => {
return axios.get("/sanctum/csrf-cookie"); // resolves to '/api/csrf-cookie'.
};
// Add a request interceptor
axios.interceptors.request.use(
function (config) {
// Do something before request is sent
useErrorStore().$reset();
if (!Cookies.get("XSRF-TOKEN")) {
return setCSRFToken().then((response) => config);
}
return config;
},
function (error) {
// Do something with request error
return Promise.reject(error);
}
);
// Add a response interceptor
axios.interceptors.response.use(
function (response) {
// console.warn("axios.interceptors.response");
// console.warn(response);
if (response?.data?.data?.csrf_token?.length > 0) {
Cookies.set("XSRF-TOKEN", response.data.data.csrf_token);
} else if (response?.data?.data?.token?.length > 0) {
Cookies.set("XSRF-TOKEN", response.data.data.csrf_token);
}
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response;
},
function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
switch (error.response.status) {
case 401:
localStorage.removeItem("token");
window.location.reload();
break;
case 403:
case 404:
console.error("404");
break;
case 422:
useErrorStore().$state = error.response.data;
break;
default:
console.log(error.response.data);
}
return Promise.reject(error);
}
);
export default axios;

View File

@@ -0,0 +1,7 @@
import mitt from "mitt";
export default {
install: (app, options) => {
app.config.globalProperties.$eventBus = mitt();
},
};

View File

@@ -0,0 +1,53 @@
import { defineStore } from "pinia";
import route from "ziggy-js/src/js/index";
import axios from "axios";
export const usePostStore = defineStore("postStore", {
state: () => ({
data: {
defaultLocaleSlug: "my",
countryLocales: [],
localeCategories: [],
},
}),
getters: {
defaultLocaleSlug(state) {
return state.data.defaultLocaleSlug;
},
countryLocales(state) {
return state.data.countryLocales;
},
localeCategories(state) {
return state.data.localeCategories;
},
},
actions: {
async fetchCountryLocales() {
try {
const response = await axios.get(route("api.admin.country-locales"));
console.log(response);
this.data.countryLocales = response.data.country_locales;
this.data.defaultLocaleSlug = response.data.default_locale_slug;
} catch (error) {
// alert(error);
console.log(error);
}
},
async fetchLocaleCategories(countryLocaleSlug) {
try {
const response = await axios.get(
route("api.admin.categories", {
country_locale_slug: countryLocaleSlug,
})
);
console.log(response);
this.data.localeCategories = response.data.categories;
} catch (error) {
// alert(error);
console.log(error);
}
},
},
});

View File

@@ -0,0 +1,44 @@
import { defineStore } from "pinia";
import axios from "axios";
export const useAuthStore = defineStore("auth", {
state: () => ({
loggedIn: localStorage.getItem("token") ? true : false,
user: null,
}),
getters: {},
actions: {
async login(credentials) {
await axios.get("sanctum/csrf-cookie");
const response = (await axios.post("api/login", credentials)).data;
if (response) {
const token = `Bearer ${response.token}`;
localStorage.setItem("token", token);
axios.defaults.headers.common["Authorization"] = token;
await this.ftechUser();
}
},
async logout() {
const response = (await axios.post("api/logout")).data;
if (response) {
localStorage.removeItem("token");
this.$reset();
}
},
async ftechUser() {
this.user = (await axios.get("api/me")).data;
this.loggedIn = true;
},
},
});

View File

@@ -0,0 +1,8 @@
import { defineStore } from "pinia";
export const useErrorStore = defineStore("error", {
state: () => ({
message: null,
errors: {},
}),
});

View File

@@ -0,0 +1,9 @@
<template>
<div></div>
</template>
<script>
export default {
name: "App",
};
</script>
<style></style>

View File

@@ -0,0 +1,127 @@
<template>
<div>
<div class="card">
<div class="card-body ratio ratio-21x9 bg-dark overflow-hidden">
<div
class="d-flex justify-content-center text-center rounded-2"
:style="bgStyle"
></div>
<div
class="position-absolute w-100 h-100 d-flex justify-content-center text-center"
>
<div v-if="isUploading" class="align-self-center">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div v-else class="align-self-center">
<input
type="file"
@change="handleFileChange"
accept="image/*"
ref="fileInput"
style="display: none"
/>
<button class="btn btn-primary" @click="openFileInput">
{{ getButtonName }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from "axios";
export default {
name: "NativeImageBlock",
props: {
apiUrl: {
type: String,
default: "https://productalert.test/api/admin/image/upload",
},
},
data: () => ({
isUploading: false,
imgSrc: null,
placeholderSrc: "https://placekitten.com/g/2100/900",
}),
computed: {
getButtonName() {
if (this.imgSrc != null && this.imgSrc?.length > 0) {
return "Change featured image";
} else {
return "Upload featured image";
}
},
getBlurPx() {
return this.imgSrc ? 0 : 12;
},
bgStyle() {
return {
backgroundImage: `url(${this.getImgSrc})`,
backgroundPosition: "center",
backgroundSize: "cover",
filter: `blur(${this.getBlurPx}px)`,
webkitFilter: `blur(${this.getBlurPx}px)`,
};
},
getImgSrc() {
if (this.imgSrc != null && this.imgSrc?.length > 0) {
return this.imgSrc;
}
return this.placeholderSrc;
},
},
methods: {
openFileInput() {
this.$refs.fileInput.click();
},
handleFileChange(event) {
const file = event.target.files[0];
if (file) {
this.uploadImage(file);
}
},
uploadImage(file) {
this.isUploading = true;
const formData = new FormData();
formData.append("file", file);
axios
.post(this.apiUrl, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})
.then((response) => {
if (
response.data.success === 1 &&
response.data.file &&
response.data.file.url
) {
this.imgSrc = response.data.file.url;
this.$emit("saved", response.data.file.url);
} else {
console.error("Image upload failed. Invalid response format.");
}
})
.catch((error) => {
console.error("Image upload failed:", error.response); // Log the full response
})
.finally(() => {
this.isUploading = false;
});
},
},
mounted() {
this.isUploading = false;
},
};
</script>
<style scoped>
/* Add your custom styles here */
</style>

View File

@@ -0,0 +1,311 @@
<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
class="mb-3"
:input-image="post.featured_image"
@saved="imageSaved"
></native-image-block>
<div 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>
<button @click="checkAndSave" class="btn btn-primary">
Save as {{ post.status }}
</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>
</div>
</div>
</template>
<script>
import VueEditorJs from "./VueEditorJs.vue";
import List from "@editorjs/list";
import Header from "@editorjs/header";
import { mapActions, mapState } from "pinia";
import { usePostStore } from "@/stores/postStore.js";
export default {
components: { VueEditorJs, List, Header },
data() {
return {
post: {
title: "",
slug: "",
excerpt: "",
author_id: null,
featured: false,
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,
},
},
onReady: () => {},
onChange: (args) => {},
data: {
time: 1591362820044,
blocks: [],
version: "2.25.0",
},
},
};
},
watch: {
"post.title": {
deep: true,
handler(after, before) {
this.post.slug = this.slugify(after);
},
},
},
computed: {
...mapState(usePostStore, [
"countryLocales",
"localeCategories",
"defaultLocaleSlug",
]),
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",
]),
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.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! pls fix " + errors.join(", "));
} else {
this.savePost();
}
},
savePost() {},
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;
},
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;
},
},
mounted() {
this.fetchCountryLocales().then(() => {
this.setDefaultLocale();
setTimeout(
function () {
this.fetchLocaleCategories(this.post.locale_slug);
}.bind(this),
100
);
});
},
};
</script>
<style></style>

View File

@@ -0,0 +1,109 @@
<template>
<div :id="holder" />
</template>
<script>
import EditorJS from "@editorjs/editorjs";
import { defineComponent, onMounted, reactive } from "vue";
export const PLUGINS = {
header: import("@editorjs/header"),
list: import("@editorjs/list"),
};
export default defineComponent({
name: "vue-editor-js",
props: {
holder: {
type: String,
default: () => "vue-editor-js",
require: true,
},
config: {
type: Object,
default: () => ({}),
require: true,
},
initialized: {
type: Function,
default: () => {},
},
},
setup: (props, context) => {
const state = reactive({ editor: null });
function initEditor(props) {
destroyEditor();
state.editor = new EditorJS({
holder: props.holder || "vue-editor-js",
...props.config,
onChange: (api, event) => {
saveEditor();
},
});
props.initialized(state.editor);
}
function destroyEditor() {
if (state.editor) {
state.editor.destroy();
state.editor = null;
}
}
function saveEditor() {
console.log("saveEditor");
if (state.editor) {
state.editor.save().then((data) => {
// Do what you want with the data here
console.log(data);
context.emit("saved", data);
});
}
}
onMounted((_) => initEditor(props));
return { props, state };
},
methods: {
useTools(props, config) {
const pluginKeys = Object.keys(PLUGINS);
const tools = { ...props.customTools };
if (pluginKeys.every((p) => !props[p])) {
pluginKeys.forEach((key) => (tools[key] = { class: PLUGINS[key] }));
Object.keys(config).forEach((key) => {
if (tools[key] !== undefined && tools[key] !== null) {
tools[key]["config"] = config[key];
}
});
return tools;
}
pluginKeys.forEach((key) => {
const prop = props[key];
if (!prop) {
return;
}
tools[key] = { class: PLUGINS[key] };
if (typeof prop === "object") {
const options = Object.assign({}, props[key]);
delete options["class"];
tools[key] = Object.assign(tools[key], options);
}
});
Object.keys(config).forEach((key) => {
if (tools[key] !== undefined && tools[key] !== null) {
tools[key]["config"] = config[key];
}
});
return tools;
},
},
});
</script>

7
resources/js/ziggy.js Normal file
View File

@@ -0,0 +1,7 @@
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"]}}};
if (typeof window !== 'undefined' && typeof window.Ziggy !== 'undefined') {
Object.assign(Ziggy.routes, window.Ziggy.routes);
}
export { Ziggy };

View File

@@ -0,0 +1,24 @@
@extends('layouts.admin.app')
@section('content')
<div class="container-xl text-center">
<!-- Page title -->
<div class="page-header d-print-none">
<h2 class="page-title text-center justify-content-center">
<div class="align-self-center">
@if (!is_null($post))
Edit Post
@else
New Post
@endif
</div>
</h2>
</div>
</div>
<div class="page-body">
<div class="container-xl">
<post-editor></post-editor>
</div>
</div>
@endsection

View File

@@ -16,7 +16,7 @@
</head>
<body class="theme-light">
<div class="page">
<div class="page" id="app">
<div class="sticky-top">
@include('layouts.admin.header')
@@ -32,7 +32,6 @@
</div>
</div>
</div>
<!-- Core plugin JavaScript-->
@vite('resources/js/admin-app.js')

View File

@@ -3,6 +3,9 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Models\CountryLocale;
use App\Models\Category;
/*
|--------------------------------------------------------------------------
| API Routes
@@ -14,6 +17,30 @@
|
*/
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
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::prefix('admin')->middleware('auth:sanctum')->group(function () {
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'));
})->name('api.admin.country-locales');
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::post('admin/image/upload', [App\Http\Controllers\Services\ImageUploadController::class, 'index'])->name('api.admin.upload.cloud.image');

View File

@@ -1,9 +1,12 @@
import { defineConfig } from "vite";
import laravel from "laravel-vite-plugin";
import vue from "@vitejs/plugin-vue";
import path from "path";
import Components from "unplugin-vue-components/vite";
export default defineConfig({
plugins: [
vue(),
laravel({
input: [
"resources/sass/admin-app.scss",
@@ -13,9 +16,15 @@ export default defineConfig({
],
refresh: true,
}),
Components({
dirs: ["resources/js/vue"],
dts: false,
}),
],
resolve: {
alias: {
vue: "vue/dist/vue.esm-bundler.js",
"@": path.resolve(__dirname, "./resources/js"),
"~": path.resolve(__dirname, "node_modules"),
"~bootstrap": path.resolve(__dirname, "node_modules/bootstrap"),
},