Add (article): ai gen, front views
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
/node_modules
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/sitemap.xml
|
||||
/storage/*.key
|
||||
/vendor
|
||||
.env
|
||||
|
||||
44
app/Console/Commands/GenerateSitemap.php
Normal file
44
app/Console/Commands/GenerateSitemap.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Spatie\Sitemap\SitemapGenerator;
|
||||
|
||||
class GenerateSitemap extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'sitemap:generate';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generate the sitemap.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
SitemapGenerator::create(config('app.url'))
|
||||
->writeToFile(public_path('sitemap.xml'));
|
||||
}
|
||||
}
|
||||
53
app/Helpers/FirstParty/OSSUploader/OSSUploader.php
Normal file
53
app/Helpers/FirstParty/OSSUploader/OSSUploader.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers\FirstParty\OSSUploader;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class OSSUploader
|
||||
{
|
||||
public static function readJson($storage_driver, $relative_directory, $filename)
|
||||
{
|
||||
$filepath = rtrim($relative_directory, '/').'/'.$filename;
|
||||
|
||||
try {
|
||||
$jsonContent = Storage::disk($storage_driver)->get($filepath);
|
||||
|
||||
$decodedJson = json_decode($jsonContent, false, 512);
|
||||
|
||||
return $decodedJson;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function uploadJson($storage_driver, $relative_directory, $filename, $jsonData)
|
||||
{
|
||||
$jsonString = json_encode($jsonData, JSON_PRETTY_PRINT);
|
||||
|
||||
try {
|
||||
return self::uploadFile($storage_driver, $relative_directory, $filename, $jsonString);
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
public static function uploadFile($storage_driver, $relative_directory, $filename, $file)
|
||||
{
|
||||
$filepath = rtrim($relative_directory, '/').'/'.$filename;
|
||||
|
||||
// if(!Storage::disk($storage_driver)->exists($relative_directory))
|
||||
// {
|
||||
// Storage::disk($storage_driver)->makeDirectory($relative_directory);
|
||||
// }
|
||||
|
||||
return Storage::disk($storage_driver)->put($filepath, $file);
|
||||
}
|
||||
}
|
||||
90
app/Helpers/FirstParty/OpenAI/OpenAI.php
Normal file
90
app/Helpers/FirstParty/OpenAI/OpenAI.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers\FirstParty\OpenAI;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class OpenAI
|
||||
{
|
||||
public static function writeArticle($title, $description, $article_type, $min, $max)
|
||||
{
|
||||
$system_prompt = "
|
||||
Using the general article structure, please create a Markdown format article on the topic given. The article should prioritize accuracy and provide genuine value to readers. Avoid making assumptions or adding unverified facts. Ensure the article is between {$min}-{$max} words. Write with 8th & 9th grade US english standard.\n\n
|
||||
Structure:\n\n
|
||||
Title\n
|
||||
Provide a headline that captures the essence of the article's focus and is tailored to the reader's needs.\n\n
|
||||
Introduction\n
|
||||
Offer a brief overview or background of the topic, ensuring it's engaging and invites readers to continue reading.\n\n
|
||||
Main Body\n\n
|
||||
Subsection\n
|
||||
Introduce foundational information about the topic, ensuring content is accurate and based on known facts. Avoid generic or speculative statements.\n\n
|
||||
Subsection (if applicable)\n
|
||||
If helpful, use Markdown to create tables to convey comparison of data. Ensure data is accurate and relevant to the reader.\n\n
|
||||
Subsection\n
|
||||
Dive deep into primary elements or facets of the topic, ensuring content is factual and offers value.\n\n
|
||||
Subsection\n
|
||||
Discuss real-world applications or significance, highlighting practical implications or actionable insights for the reader.\n\n
|
||||
Subsection (optional)\n
|
||||
Provide context or relate the topic to relevant past events or trends, making it relatable and more comprehensive.\n\n
|
||||
Subsection (if applicable)\n
|
||||
Predict outcomes, trends, or ramifications, but ensure predictions are rooted in known information or logical reasoning.\n\n
|
||||
Subsection\n
|
||||
Summarise key points or lessons, ensuring they resonate with the initial objectives of the article and provide clear takeaways.\n\n
|
||||
Conclusion\n
|
||||
Revisit main points and offer final thoughts or recommendations that are actionable and beneficial to the reader.\n\n
|
||||
FAQs\n
|
||||
Address anticipated questions or misconceptions about the topic. Prioritize questions that readers are most likely to have and provide clear, concise answers based on factual information.\n
|
||||
Q: Question\n
|
||||
A: Answer\n
|
||||
";
|
||||
|
||||
$user_prompt = "Title: {$title}\nDescription: {$description}\nArticleType: {$article_type}";
|
||||
|
||||
return self::chatCompletion($system_prompt, $user_prompt, 'gpt-3.5-turbo');
|
||||
|
||||
}
|
||||
|
||||
public static function suggestArticleTitles($current_title, $supporting_data, $suggestion_counts)
|
||||
{
|
||||
$system_prompt = "Based on provided article title, identify the main keyword in 1-2 words. Once identified, use the main keyword only to generate {$suggestion_counts} easy-to-read unique, helpful title articles.\n\n
|
||||
Requirements:\n
|
||||
2 descriptive photos keywords to represent article title when put together side-by-side\n
|
||||
No realtime information required\n
|
||||
No guides and how tos\n
|
||||
No punctuation in titles especially colons :\n
|
||||
90-130 characters\n\n
|
||||
|
||||
return in following json format {\"main_keyword\":\"(Main Keyword)\",\"suggestions\":[{\"title\":\"(Title in 90-130 letters)\",\"short_title\":\"(Short Title in 30-40 letters)\",\"article_type\":\"(How-tos|Guides|Interview|Review|Commentary|Feature|News|Editorial|Report|Research|Case-study|Overview|Tutorial|Update|Spotlight|Insights)\",\"description\":\"(SEO description based on main keyword)\",\"photo_keywords\":[\"photo keyword 1\",\"photo keyword 2\"]}]}";
|
||||
|
||||
$user_prompt = "Article Title: {$current_title}";
|
||||
|
||||
$reply = self::chatCompletion($system_prompt, $current_title, 'gpt-3.5-turbo');
|
||||
|
||||
try {
|
||||
return json_decode($reply, false);
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static function chatCompletion($system_prompt, $user_prompt, $model)
|
||||
{
|
||||
$response = Http::timeout(500)->withToken(config('platform.ai.openai.api_key'))
|
||||
->post('https://api.openai.com/v1/chat/completions', [
|
||||
'model' => $model,
|
||||
'max_tokens' => 2500,
|
||||
'messages' => [
|
||||
['role' => 'user', 'content' => $system_prompt.' '.$user_prompt],
|
||||
],
|
||||
]);
|
||||
|
||||
$json_response = json_decode($response->body());
|
||||
|
||||
$reply = $json_response?->choices[0]?->message?->content;
|
||||
|
||||
return $reply;
|
||||
|
||||
}
|
||||
}
|
||||
46
app/Helpers/Global/geo_helper.php
Normal file
46
app/Helpers/Global/geo_helper.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
if (! function_exists('get_current_ip')) {
|
||||
function get_current_ip()
|
||||
{
|
||||
$ip = null;
|
||||
|
||||
if (app()->environment() == 'local') {
|
||||
return config('platform.general.dev_default_ip');
|
||||
}
|
||||
|
||||
if (getenv('HTTP_CF_CONNECTING_IP')) {
|
||||
$ip = getenv('HTTP_CF_CONNECTING_IP');
|
||||
} elseif (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
|
||||
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
|
||||
}
|
||||
|
||||
if (! is_empty($ip)) {
|
||||
return $ip;
|
||||
}
|
||||
|
||||
$ip_add_set = null;
|
||||
|
||||
if (getenv('HTTP_CLIENT_IP')) {
|
||||
$ip_add_set = getenv('HTTP_CLIENT_IP');
|
||||
} elseif (getenv('HTTP_X_FORWARDED_FOR')) {
|
||||
$ip_add_set = getenv('HTTP_X_FORWARDED_FOR');
|
||||
} elseif (getenv('HTTP_X_FORWARDED')) {
|
||||
$ip_add_set = getenv('HTTP_X_FORWARDED');
|
||||
} elseif (getenv('HTTP_FORWARDED_FOR')) {
|
||||
$ip_add_set = getenv('HTTP_FORWARDED_FOR');
|
||||
} elseif (getenv('HTTP_FORWARDED')) {
|
||||
$ip_add_set = getenv('HTTP_FORWARDED');
|
||||
} elseif (getenv('REMOTE_ADDR')) {
|
||||
$ip_add_set = getenv('REMOTE_ADDR');
|
||||
} elseif (isset($_SERVER['REMOTE_ADDR'])) {
|
||||
$ip_add_set = $_SERVER['REMOTE_ADDR'];
|
||||
} else {
|
||||
$ip_add_set = '127.0.0.0';
|
||||
}
|
||||
|
||||
$ip_add_set = explode(',', $ip_add_set);
|
||||
|
||||
return $ip_add_set[0];
|
||||
}
|
||||
}
|
||||
5
app/Helpers/Global/helpers.php
Normal file
5
app/Helpers/Global/helpers.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
require 'string_helper.php';
|
||||
require 'geo_helper.php';
|
||||
require 'platform_helper.php';
|
||||
13
app/Helpers/Global/platform_helper.php
Normal file
13
app/Helpers/Global/platform_helper.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
if (! function_exists('get_slack_channel_by_env')) {
|
||||
|
||||
function get_slack_channel_by_env($slack_channel = 'slack')
|
||||
{
|
||||
if (app()->environment() == 'local') {
|
||||
return config("platform.notifications.{$slack_channel}.development_channel");
|
||||
} else {
|
||||
return config("platform.notifications.{$slack_channel}.production_channel");
|
||||
}
|
||||
}
|
||||
}
|
||||
120
app/Helpers/Global/string_helper.php
Normal file
120
app/Helpers/Global/string_helper.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
if (! function_exists('epoch_now_timestamp')) {
|
||||
function epoch_now_timestamp()
|
||||
{
|
||||
return (int) round(microtime(true) * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('unslug')) {
|
||||
function unslug($slug, $delimiter = '-')
|
||||
{
|
||||
return ucwords(str_replace($delimiter, ' ', $slug));
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('str_slug')) {
|
||||
function str_slug($string, $delimiter = '-')
|
||||
{
|
||||
return Str::of(trim($string))->slug($delimiter);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('str_first_sentence')) {
|
||||
function str_first_sentence($str)
|
||||
{
|
||||
// Split the string at ., !, or ?
|
||||
$sentences = preg_split('/(\.|!|\?)(\s|$)/', $str, 2);
|
||||
|
||||
// Return the first part of the array if available
|
||||
if (isset($sentences[0])) {
|
||||
return trim($sentences[0]);
|
||||
}
|
||||
|
||||
// If no sentence ending found, return the whole string
|
||||
return $str;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (! function_exists('is_empty')) {
|
||||
/**
|
||||
* A better function to check if a value is empty or null. Strings, arrays, and Objects are supported.
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
function is_empty($value): bool
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
if ($value === '') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
if (count($value) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_object($value)) {
|
||||
$value = (array) $value;
|
||||
|
||||
if (count($value) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_country_name_by_iso')) {
|
||||
function get_country_name_by_iso($country_iso)
|
||||
{
|
||||
if (! is_empty($country_iso)) {
|
||||
|
||||
$country_iso = strtoupper($country_iso);
|
||||
|
||||
try {
|
||||
return config("platform.country_codes.{$country_iso}")['name'];
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return 'International';
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_country_emoji_by_iso')) {
|
||||
function get_country_emoji_by_iso($country_iso)
|
||||
{
|
||||
if (! is_empty($country_iso)) {
|
||||
|
||||
$country_iso = strtoupper($country_iso);
|
||||
|
||||
try {
|
||||
return config("platform.country_codes.{$country_iso}")['emoji'];
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return '🌎';
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('str_random')) {
|
||||
function str_random($length = 10)
|
||||
{
|
||||
return Str::random($length);
|
||||
}
|
||||
}
|
||||
580
app/Helpers/ThirdParty/DFS/AbstractModel.php
vendored
Normal file
580
app/Helpers/ThirdParty/DFS/AbstractModel.php
vendored
Normal file
@@ -0,0 +1,580 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers\ThirdParty\DFS;
|
||||
|
||||
use DFSClientV3\Bootstrap\Application;
|
||||
use DFSClientV3\Models\DataMapper;
|
||||
use DFSClientV3\Services\HttpClient\Handlers\Responses;
|
||||
use DFSClientV3\Services\HttpClient\HttpClient;
|
||||
|
||||
abstract class AbstractModel
|
||||
{
|
||||
/**
|
||||
* @var null|int timeout for http request
|
||||
*/
|
||||
protected $timeOut = null;
|
||||
|
||||
protected $apiVersion = null;
|
||||
|
||||
/**
|
||||
* @var string | null url to DataForSeo API
|
||||
*/
|
||||
protected $url = null;
|
||||
|
||||
/**
|
||||
* @var int|null PostId is needed for a request, for more information check an example or DFSApi
|
||||
*/
|
||||
protected $postId = null;
|
||||
|
||||
/**
|
||||
* @var null This field is not working at the moment
|
||||
*/
|
||||
public $statusCode;
|
||||
|
||||
/**
|
||||
* @var null This field is not working at the moment
|
||||
*/
|
||||
public $statusMessage;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $headers = [];
|
||||
|
||||
/**
|
||||
* @var null This field is not working at the moment
|
||||
*/
|
||||
protected $requiredField;
|
||||
|
||||
/**
|
||||
* @var null This field is not working at the moment
|
||||
*/
|
||||
protected $mainData;
|
||||
|
||||
/**
|
||||
* @var static RequestType, this param will be sent to DFS API, .xml, .gzip etc...
|
||||
*/
|
||||
protected $requestType;
|
||||
|
||||
/**
|
||||
* @var string Method for http request. POST, GET , PUT, DELETE
|
||||
*/
|
||||
protected $method = 'POST';
|
||||
|
||||
/**
|
||||
* This variable is required for all extended Classes from AbstractModel.
|
||||
*
|
||||
* @var null|string It is a name, when you want to send a request to api, use this param as an api endpoint, example: cmn_user
|
||||
*/
|
||||
protected $requestToFunction;
|
||||
|
||||
/**
|
||||
* This variable is required for all extended Classes from AbstractModel
|
||||
* example for path: results->0->related.
|
||||
*
|
||||
* results - is object link from DFSResponse
|
||||
* 0 - is Index of array or postID
|
||||
* related - is object link containing main data
|
||||
*
|
||||
* @var null|string It is a system variable, it contains a path to main data from response and creates iterable(IteratorAggregator) response.
|
||||
*/
|
||||
protected $pathToMainData = null;
|
||||
|
||||
/**
|
||||
* @var bool If payload contains postId
|
||||
*/
|
||||
protected $isSupportedMerge = false;
|
||||
|
||||
/**
|
||||
* @var null|string
|
||||
*/
|
||||
private $DFSLogin = null;
|
||||
|
||||
/**
|
||||
* @var null|string
|
||||
*/
|
||||
private $DFSPassword = null;
|
||||
|
||||
public $response;
|
||||
|
||||
protected $application;
|
||||
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $payload;
|
||||
|
||||
protected $isProcessed;
|
||||
|
||||
protected $mappedMainModel;
|
||||
|
||||
protected $seTypes = ['organic', 'maps', 'local_finder', 'news', 'images', 'search_by_image'];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $jsonContainVariadicType = false;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $pathsToVariadicTypesAndValue = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $customFunctionForPaths = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $pathsToDictionary = [];
|
||||
|
||||
/**
|
||||
* @var bool Temp variable, for detect when use new mapper
|
||||
*/
|
||||
protected $useNewMapper = false;
|
||||
|
||||
/**
|
||||
* new version of DataForSeo has two variations of result
|
||||
* 1. Object contains other objects for response.
|
||||
* 2. Object contains other objects, but they is iterable.
|
||||
*
|
||||
* if model has property $resultShouldBeTransformedToArray as true, result index will be transformed to array
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $resultShouldBeTransformedToArray = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->application = Application::getInstance();
|
||||
$this->config = $this->application->getConfig();
|
||||
$this->initDefaultMethods();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $postID mixed
|
||||
*/
|
||||
public function setPostID($postID)
|
||||
{
|
||||
$this->postId = $postID;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $headers array
|
||||
*/
|
||||
public function setHeaders($headers)
|
||||
{
|
||||
if (count($headers) > 0) {
|
||||
foreach ($headers as $key => $value) {
|
||||
$this->config['headers'][$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will run request to api.
|
||||
*/
|
||||
protected function get()
|
||||
{
|
||||
$response = $this->process();
|
||||
// check if response contain valid json
|
||||
|
||||
$validResponse = json_decode($response->getResponse());
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$validResponse = ['status_code' => 50000, 'status_message' => 'error.'];
|
||||
}
|
||||
|
||||
return $this->mapData(json_encode($validResponse), $response->getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function getAfterMerge(array $modelPool)
|
||||
{
|
||||
if (empty($modelPool)) {
|
||||
throw new \Exception('Model pool can not be empty');
|
||||
}
|
||||
|
||||
$payload = [];
|
||||
$url = null;
|
||||
$apiVersion = null;
|
||||
$timeOut = null;
|
||||
$login = null;
|
||||
$password = null;
|
||||
$method = null;
|
||||
$requestToFunction = null;
|
||||
$config = Application::getInstance()->getConfig();
|
||||
$pathToMainData = null;
|
||||
$resultShouldTransformedToArray = false;
|
||||
$useNewMapper = false;
|
||||
$isJsonContainVariadicType = false;
|
||||
$pathsToVariadicTypesAndValue = [];
|
||||
$customFunctionForPaths = [];
|
||||
|
||||
foreach ($modelPool as $key => $model) {
|
||||
|
||||
// kostyl, all variable will be rewrite every iteration
|
||||
$url = $model->url;
|
||||
$apiVersion = $model->apiVersion;
|
||||
$timeOut = $model->timeOut;
|
||||
$login = $model->DFSLogin;
|
||||
$password = $model->DFSPassword;
|
||||
$method = $model->method;
|
||||
$requestToFunction = $model->requestToFunction;
|
||||
$pathToMainData = $model->pathToMainData;
|
||||
$resultShouldTransformedToArray = $model->resultShouldBeTransformedToArray;
|
||||
$useNewMapper = $model->isUseNewMapper();
|
||||
$isJsonContainVariadicType = $model->isJsonContainVariadicType();
|
||||
$pathsToVariadicTypesAndValue = $model->getPathsToVariadicTypesAndValue();
|
||||
$customFunctionForPaths = $model->getCustomFunctionForPaths();
|
||||
|
||||
if ($model->isSupportedMerge) {
|
||||
if ($model->postId === null) {
|
||||
$payload['json'][] = $model->payload;
|
||||
}
|
||||
|
||||
if ($model->postId !== null) {
|
||||
$payload['json'][$model->postId] = $model->payload;
|
||||
}
|
||||
} else {
|
||||
throw new \Exception('Provided model '.get_class($model).' is not supported merge');
|
||||
}
|
||||
}
|
||||
|
||||
$payload['headers'] = $config['headers'];
|
||||
|
||||
$http = new HttpClient($url, $apiVersion, $timeOut, $login, $password);
|
||||
|
||||
$res = $http->sendSingleRequest($method, $requestToFunction, $payload);
|
||||
// check if response contain valid json
|
||||
|
||||
$validResponse = json_decode($res->getResponse());
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
$validResponse = ['status_code' => 50000, 'status_message' => 'error.'];
|
||||
}
|
||||
|
||||
//get called class.
|
||||
$calledClassNameWithNapeSpace = get_called_class();
|
||||
$classNameArray = explode('\\', $calledClassNameWithNapeSpace);
|
||||
//for php 7.3 can be use array_last_key
|
||||
$classNameArray[count($classNameArray) - 1];
|
||||
|
||||
$mapper = new DataMapper($classNameArray[count($classNameArray) - 1], $res->getStatus(), $pathToMainData);
|
||||
|
||||
if ($useNewMapper) {
|
||||
$paveDataOptions = new PaveDataOptions();
|
||||
$paveDataOptions->setJson(json_encode($validResponse));
|
||||
$paveDataOptions->setJsonContainVariadicType($isJsonContainVariadicType);
|
||||
$paveDataOptions->setPathsToVariadicTypesAndValue($pathsToVariadicTypesAndValue);
|
||||
$paveDataOptions->setCustomFunctionForPaths($customFunctionForPaths);
|
||||
|
||||
$mappedModel = $mapper->paveDataNew($paveDataOptions);
|
||||
|
||||
return $mappedModel;
|
||||
}
|
||||
|
||||
$mappedModel = $mapper->paveData(json_encode($validResponse), null, $resultShouldTransformedToArray);
|
||||
|
||||
return $mappedModel;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|AbstractModel[] $modelPool
|
||||
* @return array|null
|
||||
*/
|
||||
public static function getAsync(array $modelPool, int $timeout = 100)
|
||||
{
|
||||
|
||||
if (count($modelPool) > 100) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$requestsPool = [];
|
||||
$results = [];
|
||||
|
||||
$payload = [];
|
||||
$url = null;
|
||||
$apiVersion = null;
|
||||
$timeOut = null;
|
||||
$login = null;
|
||||
$password = null;
|
||||
|
||||
$config = Application::getInstance()->getConfig();
|
||||
$pathToMainData = null;
|
||||
$resultShouldTransformedToArray = false;
|
||||
$useNewMapper = false;
|
||||
$isJsonContainVariadicType = false;
|
||||
$pathsToVariadicTypesAndValue = [];
|
||||
$customFunctionForPaths = [];
|
||||
$pathsToDictionary = [];
|
||||
|
||||
foreach ($modelPool as $key => $model) {
|
||||
|
||||
// kostyl, all variable will be rewrite every iteration
|
||||
$url = $model->url;
|
||||
$apiVersion = $model->apiVersion;
|
||||
$login = $model->DFSLogin;
|
||||
$password = $model->DFSPassword;
|
||||
|
||||
$pathToMainData = $model->pathToMainData;
|
||||
$resultShouldTransformedToArray = $model->resultShouldBeTransformedToArray;
|
||||
$useNewMapper = $model->isUseNewMapper();
|
||||
$isJsonContainVariadicType = $model->isJsonContainVariadicType();
|
||||
$pathsToVariadicTypesAndValue = $model->getPathsToVariadicTypesAndValue();
|
||||
$customFunctionForPaths = $model->getCustomFunctionForPaths();
|
||||
$pathsToDictionary = $model->getPathsToDictionary();
|
||||
|
||||
$payload['json'][] = $model->getPayload();
|
||||
$payload['headers'] = $config['headers'];
|
||||
|
||||
$requestsPool[$key]['method'] = $model->getHttpMethod();
|
||||
$requestsPool[$key]['url'] = $model->getRequestToFunction();
|
||||
$requestsPool[$key]['params'] = $payload;
|
||||
$requestsPool[$key]['pathToMainData'] = $model->getPathToMainData();
|
||||
}
|
||||
|
||||
$http = new HttpClient($url, $apiVersion, $timeout, $login, $password);
|
||||
$responses = $http->sendAsyncRequests($requestsPool, null);
|
||||
|
||||
foreach ($responses as $response) {
|
||||
|
||||
/**
|
||||
* @var Responses $response
|
||||
*/
|
||||
|
||||
//get called class.
|
||||
$calledClassNameWithNapeSpace = get_called_class();
|
||||
$classNameArray = explode('\\', $calledClassNameWithNapeSpace);
|
||||
//for php 7.3 can be use array_last_key
|
||||
$classNameArray[count($classNameArray) - 1];
|
||||
|
||||
$mapper = new DataMapper($classNameArray[count($classNameArray) - 1], $response->getStatus(), $pathToMainData);
|
||||
|
||||
if ($useNewMapper) {
|
||||
$paveDataOptions = new PaveDataOptions();
|
||||
$paveDataOptions->setJson($response->getResponse());
|
||||
$paveDataOptions->setJsonContainVariadicType($isJsonContainVariadicType);
|
||||
$paveDataOptions->setPathsToDictionary($pathsToDictionary);
|
||||
$paveDataOptions->setPathsToVariadicTypesAndValue($pathsToVariadicTypesAndValue);
|
||||
$paveDataOptions->setCustomFunctionForPaths($customFunctionForPaths);
|
||||
|
||||
$mappedModel = $mapper->paveDataNew($paveDataOptions);
|
||||
|
||||
$results[] = $mappedModel;
|
||||
}
|
||||
|
||||
if (! $useNewMapper) {
|
||||
$results[] = $mapper->paveData($response->getResponse(), null, $resultShouldTransformedToArray);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DFSClientV3\Services\HttpClient\Handlers\Responses
|
||||
*/
|
||||
public function process()
|
||||
{
|
||||
$http = new HttpClient($this->url, $this->apiVersion, $this->timeOut, $this->DFSLogin, $this->DFSPassword);
|
||||
$payload = [];
|
||||
|
||||
if (! $this->application) {
|
||||
dd('DFSClient was not init, add to your code: $DFSClient = new DFSClient() ');
|
||||
}
|
||||
|
||||
if (! $this->requestToFunction) {
|
||||
dd('requestFunction can not be null, set this field in your model: '.get_called_class());
|
||||
}
|
||||
|
||||
if ($this->postId === null) {
|
||||
$payload['json'][0] = $this->payload;
|
||||
}
|
||||
|
||||
if ($this->postId != null) {
|
||||
$payload['json'][$this->postId] = $this->payload;
|
||||
}
|
||||
|
||||
$payload['headers'] = $this->config['headers'];
|
||||
|
||||
$res = $http->sendSingleRequest($this->method, $this->requestToFunction, $payload);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function getAsJson()
|
||||
{
|
||||
$result = $this->process()->getResponse();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getPathToMainData()
|
||||
{
|
||||
return $this->pathToMainData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCalledClass()
|
||||
{
|
||||
$calledClassNameWithNapeSpace = get_called_class();
|
||||
$classNameArray = explode('\\', $calledClassNameWithNapeSpace);
|
||||
|
||||
//for php 7.3 can be use array_last_key
|
||||
return $classNameArray[count($classNameArray) - 1];
|
||||
}
|
||||
|
||||
public function setOpt()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function setLogin(string $newLogin)
|
||||
{
|
||||
$this->DFSLogin = $newLogin;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPassword(string $newPassword)
|
||||
{
|
||||
$this->DFSPassword = $newPassword;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isSuccesful
|
||||
*
|
||||
* return mixed;
|
||||
*/
|
||||
protected function mapData(string $json, bool $isSuccesful = false)
|
||||
{
|
||||
|
||||
$mapper = new DataMapper($this->getCalledClass(), $isSuccesful, $this->pathToMainData);
|
||||
|
||||
if ($this->useNewMapper) {
|
||||
$paveDataOptions = new PaveDataOptions();
|
||||
$paveDataOptions->setJson($json);
|
||||
$paveDataOptions->setJsonContainVariadicType($this->isJsonContainVariadicType());
|
||||
$paveDataOptions->setPathsToVariadicTypesAndValue($this->getPathsToVariadicTypesAndValue());
|
||||
$paveDataOptions->setPathsToDictionary($this->getPathsToDictionary());
|
||||
$paveDataOptions->setCustomFunctionForPaths($this->getCustomFunctionForPaths());
|
||||
|
||||
$mappedModel = $mapper->paveDataNew($paveDataOptions);
|
||||
|
||||
return $mappedModel;
|
||||
}
|
||||
|
||||
$mappedModel = $mapper->paveData($json, null, $this->resultShouldBeTransformedToArray);
|
||||
|
||||
return $mappedModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function resultShouldBeTransformedToArray()
|
||||
{
|
||||
return $this->resultShouldBeTransformedToArray;
|
||||
}
|
||||
|
||||
public function useSandbox(string $url = null)
|
||||
{
|
||||
$this->url = $this->config['sandboxUrl'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $timeOut int
|
||||
*/
|
||||
public function setTimeOut(int $timeOut)
|
||||
{
|
||||
$this->timeOut = $timeOut;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isJsonContainVariadicType(): bool
|
||||
{
|
||||
return $this->jsonContainVariadicType;
|
||||
}
|
||||
|
||||
public function getPathsToVariadicTypesAndValue(): array
|
||||
{
|
||||
return $this->pathsToVariadicTypesAndValue;
|
||||
}
|
||||
|
||||
public function getPathsToDictionary(): array
|
||||
{
|
||||
return $this->pathsToDictionary;
|
||||
}
|
||||
|
||||
public function addCustomFunctionForPath(array $customFunction)
|
||||
{
|
||||
$this->customFunctionForPaths = array_merge($this->customFunctionForPaths, $customFunction);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCustomFunctionForPaths(): array
|
||||
{
|
||||
return $this->customFunctionForPaths;
|
||||
}
|
||||
|
||||
private function initDefaultMethods()
|
||||
{
|
||||
$this->addCustomFunctionForPath($this->initCustomFunctionForPaths());
|
||||
|
||||
}
|
||||
|
||||
protected function initCustomFunctionForPaths(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function isUseNewMapper(): bool
|
||||
{
|
||||
return $this->useNewMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getHttpMethod()
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getRequestToFunction()
|
||||
{
|
||||
return $this->requestToFunction;
|
||||
}
|
||||
|
||||
public function getPayload(): array
|
||||
{
|
||||
return $this->payload ?? [];
|
||||
}
|
||||
}
|
||||
203
app/Helpers/ThirdParty/DFS/SettingSerpLiveAdvanced.php
vendored
Normal file
203
app/Helpers/ThirdParty/DFS/SettingSerpLiveAdvanced.php
vendored
Normal file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace App\Helpers\ThirdParty\DFS;
|
||||
|
||||
class SettingSerpLiveAdvanced extends AbstractModel
|
||||
{
|
||||
protected $method = 'POST';
|
||||
|
||||
protected $isSupportedMerge = true;
|
||||
|
||||
protected $pathToMainData = 'tasks->{$postID}->result';
|
||||
|
||||
protected $requestToFunction = 'serp/{$se}/{$seType}/live/advanced';
|
||||
|
||||
protected $resultShouldBeTransformedToArray = true;
|
||||
|
||||
protected $jsonContainVariadicType = true;
|
||||
|
||||
protected $pathsToVariadicTypesAndValue = ['tasks->(:number)->result->(:number)->items->(:number)' => 'type'];
|
||||
|
||||
protected $useNewMapper = true;
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setUrl(string $url)
|
||||
{
|
||||
$this->payload['url'] = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLanguageCode(string $langCode)
|
||||
{
|
||||
$this->payload['language_code'] = $langCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setKeyword(string $keyword)
|
||||
{
|
||||
$this->payload['keyword'] = $keyword;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setPriority(string $priority)
|
||||
{
|
||||
$this->payload['priority'] = $priority;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLocationName(string $locationName)
|
||||
{
|
||||
$this->payload['location_name'] = $locationName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLocationCode(int $locationCode)
|
||||
{
|
||||
$this->payload['location_code'] = $locationCode;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLocationCoordinate(string $locationCoordinate)
|
||||
{
|
||||
$this->payload['location_coordinate'] = $locationCoordinate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setLanguageName(string $languageName)
|
||||
{
|
||||
$this->payload['language_name'] = $languageName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setDevice(string $device)
|
||||
{
|
||||
$this->payload['device'] = $device;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setOs(string $os)
|
||||
{
|
||||
$this->payload['os'] = $os;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setSeDomain(string $seDomain)
|
||||
{
|
||||
$this->payload['se_domain'] = $seDomain;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setDepth(int $depth)
|
||||
{
|
||||
$this->payload['depth'] = $depth;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setSearchParam(string $searchParam)
|
||||
{
|
||||
$this->payload['search_param'] = $searchParam;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setTag(string $tag)
|
||||
{
|
||||
$this->payload['tag'] = $tag;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function setSeType(string $seType)
|
||||
{
|
||||
if (! in_array($seType, $this->seTypes)) {
|
||||
throw new \Exception('Provided se type not allowed');
|
||||
}
|
||||
|
||||
$this->requestToFunction = str_replace('{$seType}', $seType, $this->requestToFunction);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function setSe(string $seName)
|
||||
{
|
||||
$this->requestToFunction = str_replace('{$se}', $seName, $this->requestToFunction);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function get(): \DFSClientV3\Entity\Custom\SettingSerpLiveAdvancedEntityMain
|
||||
{
|
||||
return parent::get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function getAfterMerge(array $modelPool)
|
||||
{
|
||||
return parent::getAfterMerge($modelPool); // TODO: Change the autogenerated stub
|
||||
}
|
||||
}
|
||||
89
app/Http/Controllers/Front/FrontHomeController.php
Normal file
89
app/Http/Controllers/Front/FrontHomeController.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Front;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Post;
|
||||
use Artesaos\SEOTools\Facades\SEOMeta;
|
||||
use Artesaos\SEOTools\Facades\SEOTools;
|
||||
use GrahamCampbell\Markdown\Facades\Markdown;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FrontHomeController extends Controller
|
||||
{
|
||||
public function home(Request $request)
|
||||
{
|
||||
$featured_post = Post::where('status', 'publish')->orderBy('published_at', 'desc')->first();
|
||||
$latest_posts = Post::where(function ($query) use ($featured_post) {
|
||||
$query->whereNotIn('id', [$featured_post->id]);
|
||||
})->where('status', 'publish')->orderBy('published_at', 'desc')->limit(5)->get();
|
||||
|
||||
return response(view('front.welcome', compact('featured_post', 'latest_posts')), 200);
|
||||
}
|
||||
|
||||
public function terms(Request $request)
|
||||
{
|
||||
$markdown = file_get_contents(resource_path('markdown/terms.md'));
|
||||
|
||||
$title = 'Terms of Service';
|
||||
$description = 'Our Terms of Service outline your rights, responsibilities, and the standards we uphold for a seamless experience.';
|
||||
|
||||
SEOTools::metatags();
|
||||
SEOTools::twitter();
|
||||
SEOTools::opengraph();
|
||||
SEOTools::jsonLd();
|
||||
SEOTools::setTitle($title);
|
||||
SEOTools::setDescription($description);
|
||||
SEOMeta::setRobots('noindex');
|
||||
|
||||
$content = Markdown::convert($markdown)->getContent();
|
||||
|
||||
//$content = $this->injectTailwindClasses($content);
|
||||
|
||||
return view('front.pages', compact('content', 'title', 'description'));
|
||||
}
|
||||
|
||||
public function privacy(Request $request)
|
||||
{
|
||||
$markdown = file_get_contents(resource_path('markdown/privacy.md'));
|
||||
|
||||
$title = 'Privacy Policy';
|
||||
$description = 'Our Privacy Policy provides clarity about the data we collect and how we use it, ensuring your peace of mind.';
|
||||
|
||||
SEOTools::metatags();
|
||||
SEOTools::twitter();
|
||||
SEOTools::opengraph();
|
||||
SEOTools::jsonLd();
|
||||
SEOTools::setTitle($title);
|
||||
SEOTools::setDescription($description);
|
||||
SEOMeta::setRobots('noindex');
|
||||
|
||||
$content = Markdown::convert($markdown)->getContent();
|
||||
|
||||
//$content = $this->injectTailwindClasses($content);
|
||||
|
||||
return view('front.pages', compact('content', 'title', 'description'));
|
||||
}
|
||||
|
||||
public function disclaimer(Request $request)
|
||||
{
|
||||
$markdown = file_get_contents(resource_path('markdown/disclaimer.md'));
|
||||
|
||||
$title = 'Disclaimer';
|
||||
$description = 'EchoScoop provides the content on this website purely for informational purposes and should not be interpreted as legal, financial, or medical guidance.';
|
||||
|
||||
SEOTools::metatags();
|
||||
SEOTools::twitter();
|
||||
SEOTools::opengraph();
|
||||
SEOTools::jsonLd();
|
||||
SEOTools::setTitle($title);
|
||||
SEOTools::setDescription($description);
|
||||
SEOMeta::setRobots('noindex');
|
||||
|
||||
$content = Markdown::convert($markdown)->getContent();
|
||||
|
||||
//$content = $this->injectTailwindClasses($content);
|
||||
|
||||
return view('front.pages', compact('content', 'title', 'description'));
|
||||
}
|
||||
}
|
||||
27
app/Http/Controllers/Front/FrontListController.php
Normal file
27
app/Http/Controllers/Front/FrontListController.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Front;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Category;
|
||||
use App\Models\Post;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FrontListController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$posts = Post::where('status', 'publish')->orderBy('published_at', 'desc')->simplePaginate(10) ?? collect();
|
||||
|
||||
return view('front.post_list', compact('posts'));
|
||||
}
|
||||
|
||||
public function category(Request $request, $category_slug)
|
||||
{
|
||||
$category = Category::where('slug', $category_slug)->first();
|
||||
|
||||
$posts = $category?->posts()->where('status', 'publish')->orderBy('published_at', 'desc')->simplePaginate(10) ?? collect();
|
||||
|
||||
return view('front.post_list', compact('category', 'posts'));
|
||||
}
|
||||
}
|
||||
132
app/Http/Controllers/Front/FrontPostController.php
Normal file
132
app/Http/Controllers/Front/FrontPostController.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Front;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Post;
|
||||
use GrahamCampbell\Markdown\Facades\Markdown;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
class FrontPostController extends Controller
|
||||
{
|
||||
public function index(Request $request, $slug)
|
||||
{
|
||||
$post = Post::where('slug', $slug)->where('status', 'publish')->first();
|
||||
|
||||
if (is_null($post)) {
|
||||
return abort(404);
|
||||
}
|
||||
|
||||
$content = Markdown::convert($post->body)->getContent();
|
||||
|
||||
//dd($content);
|
||||
$content = $this->injectBootstrapClasses($content);
|
||||
$content = $this->injectTableOfContents($content);
|
||||
$content = $this->injectFeaturedImage($post, $content);
|
||||
|
||||
return view('front.single_post', compact('post', 'content'));
|
||||
}
|
||||
|
||||
private function injectBootstrapClasses($content)
|
||||
{
|
||||
$crawler = new Crawler($content);
|
||||
|
||||
$crawler->filter('h1')->each(function (Crawler $node) {
|
||||
$node->getNode(0)->setAttribute('class', trim($node->attr('class').' display-6 fw-bolder mt-3 mb-4'));
|
||||
});
|
||||
|
||||
$crawler->filter('h2')->each(function (Crawler $node) {
|
||||
$node->getNode(0)->setAttribute('class', trim($node->attr('class').'h4 mb-3'));
|
||||
});
|
||||
|
||||
$crawler->filter('h3')->each(function (Crawler $node) {
|
||||
$node->getNode(0)->setAttribute('class', trim($node->attr('class').'h6 mb-2'));
|
||||
});
|
||||
|
||||
$crawler->filter('p')->each(function (Crawler $pNode) {
|
||||
$precedingHeaders = $pNode->previousAll()->filter('h2');
|
||||
|
||||
// If there are no preceding <h2> tags, just process the <p>
|
||||
if (! $precedingHeaders->count()) {
|
||||
$existingClasses = $pNode->attr('class');
|
||||
$newClasses = trim($existingClasses.' mt-2 mb-4');
|
||||
$pNode->getNode(0)->setAttribute('class', $newClasses);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$precedingHeader = $precedingHeaders->first();
|
||||
if (trim($precedingHeader->text()) !== 'FAQs') {
|
||||
$existingClasses = $pNode->attr('class');
|
||||
$newClasses = trim($existingClasses.' mt-2 mb-4');
|
||||
$pNode->getNode(0)->setAttribute('class', $newClasses);
|
||||
}
|
||||
|
||||
if (strpos($pNode->text(), 'Q:') === 0) {
|
||||
$currentClasses = $pNode->attr('class');
|
||||
$newClasses = trim($currentClasses.' fw-bold');
|
||||
$pNode->getNode(0)->setAttribute('class', $newClasses);
|
||||
}
|
||||
});
|
||||
|
||||
$crawler->filter('ul')->each(function (Crawler $node) {
|
||||
$node->getNode(0)->setAttribute('class', trim($node->attr('class').'py-2'));
|
||||
});
|
||||
|
||||
$crawler->filter('ol')->each(function (Crawler $node) {
|
||||
$node->getNode(0)->setAttribute('class', trim($node->attr('class').'py-2'));
|
||||
});
|
||||
|
||||
// Convert the modified DOM back to string
|
||||
$modifiedContent = '';
|
||||
foreach ($crawler as $domElement) {
|
||||
$modifiedContent .= $domElement->ownerDocument->saveHTML($domElement);
|
||||
}
|
||||
|
||||
return $modifiedContent;
|
||||
}
|
||||
|
||||
private function injectTableOfContents($html)
|
||||
{
|
||||
$crawler = new Crawler($html);
|
||||
|
||||
// Create the Table of Contents
|
||||
$toc = '<div class="p-3 rounded-3 bg-light mb-3"><ol>';
|
||||
$crawler->filter('h2')->each(function (Crawler $node, $i) use (&$toc) {
|
||||
$content = $node->text();
|
||||
$id = 'link-'.$i; // Creating a simple id based on the index
|
||||
$node->getNode(0)->setAttribute('id', $id); // Set the id to the h2 tag
|
||||
$toc .= "<li class=\"py-1\"><a class=\"text-decoration-none hover-text-decoration-underline\" href='#{$id}'>{$content}</a></li>";
|
||||
});
|
||||
$toc .= '</ol></div>';
|
||||
|
||||
// Insert TOC after h1
|
||||
$domDocument = $crawler->getNode(0)->ownerDocument;
|
||||
$fragment = $domDocument->createDocumentFragment();
|
||||
$fragment->appendXML($toc);
|
||||
$h1Node = $crawler->filter('h1')->getNode(0);
|
||||
$h1Node->parentNode->insertBefore($fragment, $h1Node->nextSibling);
|
||||
|
||||
// Get the updated HTML
|
||||
$updatedHtml = $crawler->filter('body')->html();
|
||||
|
||||
return $updatedHtml;
|
||||
|
||||
}
|
||||
|
||||
private function injectFeaturedImage($post, $content)
|
||||
{
|
||||
if (! is_empty($post->featured_image)) {
|
||||
$featured_image_alt = strtolower($post->short_title);
|
||||
$featured_image_alt_caps = strtoupper($post->short_title);
|
||||
$featured_image = "<div class=\"w-100 d-flex justify-content-center\"><figure class=\"text-center\"><img decoding=\"async\" class=\"img-fluid rounded mb-2 shadow\" src=\"{$post->featured_image_cdn}\" alt=\"{$featured_image_alt}\"><figcaption class=\"text-secondary small\">{$featured_image_alt_caps}</figcaption></figure></div>";
|
||||
|
||||
$content = preg_replace('/(<\/h1>)/', '$1'.$featured_image, $content, 1);
|
||||
|
||||
}
|
||||
|
||||
return $content;
|
||||
|
||||
}
|
||||
}
|
||||
37
app/Jobs/GenerateArticleFeaturedImageJob.php
Normal file
37
app/Jobs/GenerateArticleFeaturedImageJob.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Jobs\Tasks\GenerateArticleFeaturedImageTask;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class GenerateArticleFeaturedImageJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $post;
|
||||
|
||||
public $timeout = 600;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct($post)
|
||||
{
|
||||
$this->post = $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
if (! is_null($this->post)) {
|
||||
GenerateArticleFeaturedImageTask::handle($this->post);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
app/Jobs/GenerateArticleJob.php
Normal file
37
app/Jobs/GenerateArticleJob.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Jobs\Tasks\GenerateArticleTask;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class GenerateArticleJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $serp_url;
|
||||
|
||||
public $timeout = 600;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct($serp_url)
|
||||
{
|
||||
$this->serp_url = $serp_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
if (! is_null($this->serp_url)) {
|
||||
GenerateArticleTask::handle($this->serp_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
243
app/Jobs/Tasks/GenerateArticleFeaturedImageTask.php
Normal file
243
app/Jobs/Tasks/GenerateArticleFeaturedImageTask.php
Normal file
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Tasks;
|
||||
|
||||
use App\Helpers\FirstParty\OSSUploader\OSSUploader;
|
||||
use App\Helpers\ThirdParty\DFS\SettingSerpLiveAdvanced;
|
||||
use App\Models\NewsSerpResult;
|
||||
use DFSClientV3\DFSClient;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Image;
|
||||
|
||||
class GenerateArticleFeaturedImageTask
|
||||
{
|
||||
public static function handle($post)
|
||||
{
|
||||
$keyword = $post->main_keyword;
|
||||
$title = $post->short_title;
|
||||
$article_type = $post->type;
|
||||
$country_iso = 'US';
|
||||
$country_name = get_country_name_by_iso($country_iso);
|
||||
|
||||
$images = [];
|
||||
|
||||
$client = new DFSClient(
|
||||
config('dataforseo.login'),
|
||||
config('dataforseo.password'),
|
||||
config('dataforseo.timeout'),
|
||||
config('dataforseo.api_version'),
|
||||
config('dataforseo.url'),
|
||||
);
|
||||
|
||||
// You will receive SERP data specific to the indicated keyword, search engine, and location parameters
|
||||
$serp_model = new SettingSerpLiveAdvanced();
|
||||
|
||||
$serp_model->setSe('google');
|
||||
$serp_model->setSeType('images');
|
||||
$serp_model->setKeyword($keyword);
|
||||
$serp_model->setLocationName($country_name);
|
||||
$serp_model->setDepth(100);
|
||||
$serp_model->setLanguageCode('en');
|
||||
$serp_res = $serp_model->getAsJson();
|
||||
|
||||
// try {
|
||||
$serp_obj = json_decode($serp_res, false, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
//dd($serp_obj);
|
||||
|
||||
if ($serp_obj?->status_code == 20000) {
|
||||
$json_file_name = config('platform.dataset.news.images_serp.file_prefix').str_slug($keyword).'-'.epoch_now_timestamp().'.json';
|
||||
|
||||
$upload_status = OSSUploader::uploadJson(
|
||||
config('platform.dataset.news.images_serp.driver'),
|
||||
config('platform.dataset.news.images_serp.path'),
|
||||
$json_file_name,
|
||||
$serp_obj);
|
||||
|
||||
if ($upload_status) {
|
||||
$news_serp_result = new NewsSerpResult();
|
||||
$news_serp_result->serp_provider = 'dfs';
|
||||
$news_serp_result->serp_se = 'google';
|
||||
$news_serp_result->serp_se_type = 'images';
|
||||
$news_serp_result->serp_keyword = $keyword;
|
||||
$news_serp_result->serp_country_iso = strtoupper($country_iso);
|
||||
$news_serp_result->serp_cost = $serp_obj?->cost;
|
||||
$news_serp_result->result_count = $serp_obj?->tasks[0]?->result[0]?->items_count;
|
||||
$news_serp_result->filename = $json_file_name;
|
||||
$news_serp_result->status = 'initial';
|
||||
|
||||
if ($news_serp_result->save()) {
|
||||
|
||||
$serp_items = $serp_obj?->tasks[0]?->result[0]?->items;
|
||||
|
||||
//dd($serp_items);
|
||||
|
||||
foreach ($serp_items as $item) {
|
||||
if ($item->type == 'images_search') {
|
||||
//dd($item);
|
||||
$images[] = $item->source_url;
|
||||
|
||||
if (count($images) > 20) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
//return $news_serp_result;
|
||||
} else {
|
||||
throw new Exception('Uploading failed', 1);
|
||||
}
|
||||
} else {
|
||||
throw new Exception('Data failed', 1);
|
||||
}
|
||||
// } catch (Exception $e) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
$numImagesInCanvas = 2;
|
||||
|
||||
if ($numImagesInCanvas > count($images)) {
|
||||
$numImagesInCanvas = count($images);
|
||||
}
|
||||
|
||||
$canvasWidth = 720;
|
||||
$canvasHeight = 405;
|
||||
|
||||
$canvas = Image::canvas($canvasWidth, $canvasHeight);
|
||||
|
||||
// Add Images
|
||||
$imageWidth = $canvasWidth / $numImagesInCanvas;
|
||||
|
||||
// Process and place each image
|
||||
$xOffset = 0; // Horizontal offset to place each image
|
||||
for ($i = 0; $i < count($images); $i++) {
|
||||
$url = $images[$i];
|
||||
|
||||
try {
|
||||
$imageResponse = Http::timeout(300)->withHeaders([
|
||||
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
|
||||
])->get($url);
|
||||
|
||||
$imageContent = $imageResponse->body();
|
||||
|
||||
$image = Image::make($imageContent)
|
||||
->resize(null, $canvasHeight, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
})
|
||||
->resizeCanvas($imageWidth, $canvasHeight, 'center', false, [255, 255, 255, 0]);
|
||||
//->blur(6)
|
||||
|
||||
$canvas->insert($image, 'top-left', $xOffset, 0);
|
||||
$xOffset += $imageWidth;
|
||||
} catch (Exception $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
$fontSize = 28;
|
||||
$articleTypeFontSize = 24;
|
||||
$padding = 15;
|
||||
|
||||
$fontPath = resource_path('fonts/Inter/Inter-Black.ttf');
|
||||
|
||||
// Split title into words and reconstruct lines
|
||||
$words = explode(' ', $title);
|
||||
$lines = [''];
|
||||
$currentLineIndex = 0;
|
||||
|
||||
foreach ($words as $word) {
|
||||
$potentialLine = $lines[$currentLineIndex] ? $lines[$currentLineIndex].' '.$word : $word;
|
||||
|
||||
$box = imagettfbbox($fontSize, 0, $fontPath, $potentialLine);
|
||||
$textWidth = abs($box[2] - $box[0]);
|
||||
|
||||
if ($textWidth < $canvasWidth * 0.9) {
|
||||
$lines[$currentLineIndex] = $potentialLine;
|
||||
} else {
|
||||
$currentLineIndex++;
|
||||
$lines[$currentLineIndex] = $word;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the dimensions of the article_type text
|
||||
$articleTypeBox = imagettfbbox($articleTypeFontSize, 0, $fontPath, $article_type);
|
||||
$articleTypeWidth = abs($articleTypeBox[2] - $articleTypeBox[0]);
|
||||
$articleTypeHeight = abs($articleTypeBox[7] - $articleTypeBox[1]);
|
||||
|
||||
// Define the start Y position for the article type overlay
|
||||
$articleOverlayStartY = $canvasHeight - ($fontSize * count($lines)) - ($articleTypeHeight + 4 * $padding);
|
||||
|
||||
// Create the blue overlay for the article type
|
||||
$overlayWidth = $articleTypeWidth + 2 * $padding;
|
||||
$canvas->rectangle(20, $articleOverlayStartY, 10 + $overlayWidth, $articleOverlayStartY + $articleTypeHeight + 2 * $padding, function ($draw) {
|
||||
$draw->background([255, 255, 255, 0.8]);
|
||||
});
|
||||
|
||||
// Overlay the article_type text within its overlay, centered horizontally and vertically
|
||||
$textStartX = 20 + ($overlayWidth - $articleTypeWidth) / 2; // Center the text horizontally
|
||||
$canvas->text(strtoupper($article_type), $textStartX, $articleOverlayStartY + ($articleTypeHeight + 2 * $padding) / 2, function ($font) use ($articleTypeFontSize, $fontPath) {
|
||||
$font->file($fontPath);
|
||||
$font->size($articleTypeFontSize);
|
||||
$font->color('#0000FF');
|
||||
$font->align('left');
|
||||
$font->valign('middle'); // This ensures the text is vertically centered within the overlay
|
||||
});
|
||||
|
||||
// Create the blue overlay for the title
|
||||
$titleOverlayStartY = $articleOverlayStartY + $articleTypeHeight + 2 * $padding;
|
||||
|
||||
$canvas->rectangle(0, $titleOverlayStartY, $canvasWidth, $canvasHeight, function ($draw) {
|
||||
$draw->background([0, 0, 255, 0.5]);
|
||||
});
|
||||
|
||||
// Draw each line for the title
|
||||
$yPosition = $titleOverlayStartY + $padding;
|
||||
foreach ($lines as $line) {
|
||||
$canvas->text($line, $canvasWidth / 2, $yPosition, function ($font) use ($fontSize, $fontPath) {
|
||||
$font->file($fontPath);
|
||||
$font->size($fontSize);
|
||||
$font->color('#FFFFFF');
|
||||
$font->align('center');
|
||||
$font->valign('top');
|
||||
});
|
||||
$yPosition += $fontSize + $padding;
|
||||
}
|
||||
|
||||
$filename = $post->slug.'-'.epoch_now_timestamp().'.jpg';
|
||||
|
||||
$ok = OSSUploader::uploadFile('r2', 'post_images/', $filename, (string) $canvas->stream('jpeg'));
|
||||
|
||||
// LQIP
|
||||
// Clone the main image for LQIP version
|
||||
$lqipImage = clone $canvas;
|
||||
|
||||
// Create the LQIP version of the image
|
||||
$lqipImage->fit(10, 10, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
});
|
||||
|
||||
$lqipImage->encode('jpg', 5);
|
||||
|
||||
// LQIP filename
|
||||
$lqip_filename = $post->slug.'-'.epoch_now_timestamp().'_lqip.jpg';
|
||||
|
||||
// Upload the LQIP version using OSSUploader
|
||||
$lqip_ok = OSSUploader::uploadFile('r2', 'post_images/', $lqip_filename, (string) $lqipImage->stream('jpeg'));
|
||||
|
||||
if ($ok && $lqip_ok) {
|
||||
|
||||
$post->featured_image = 'post_images/'.$filename;
|
||||
$post->status = 'publish';
|
||||
$post->save();
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
82
app/Jobs/Tasks/GenerateArticleTask.php
Normal file
82
app/Jobs/Tasks/GenerateArticleTask.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Tasks;
|
||||
|
||||
use App\Helpers\FirstParty\OpenAI\OpenAI;
|
||||
use App\Models\Author;
|
||||
use App\Models\Post;
|
||||
use App\Models\PostCategory;
|
||||
use App\Models\SerpUrl;
|
||||
use Exception;
|
||||
|
||||
class GenerateArticleTask
|
||||
{
|
||||
public static function handle(SerpUrl $serp_url)
|
||||
{
|
||||
|
||||
$ai_titles = OpenAI::suggestArticleTitles($serp_url->title, $serp_url->description, 1);
|
||||
|
||||
if (is_null($ai_titles)) {
|
||||
return self::saveAndReturnSerpProcessStatus($serp_url, -2);
|
||||
}
|
||||
|
||||
$suggestion = null;
|
||||
|
||||
// dump($ai_titles);
|
||||
|
||||
try {
|
||||
$random_key = array_rand($ai_titles?->suggestions, 1);
|
||||
|
||||
$suggestion = $ai_titles->suggestions[$random_key];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return self::saveAndReturnSerpProcessStatus($serp_url, -1);
|
||||
}
|
||||
|
||||
if (is_null($suggestion)) {
|
||||
return self::saveAndReturnSerpProcessStatus($serp_url, -3);
|
||||
}
|
||||
|
||||
$markdown = OpenAI::writeArticle($suggestion->title, $suggestion->description, $suggestion->article_type, 500, 800);
|
||||
|
||||
if (is_empty($markdown)) {
|
||||
return self::saveAndReturnSerpProcessStatus($serp_url, -4);
|
||||
}
|
||||
|
||||
$post = new Post;
|
||||
$post->title = $suggestion->title;
|
||||
$post->type = $suggestion->article_type;
|
||||
$post->short_title = $ai_titles->short_title;
|
||||
$post->main_keyword = $ai_titles->main_keyword;
|
||||
$post->keywords = $suggestion->photo_keywords;
|
||||
$post->slug = str_slug($suggestion->title);
|
||||
$post->excerpt = $suggestion->description;
|
||||
$post->author_id = Author::find(1)->id;
|
||||
$post->featured = false;
|
||||
$post->featured_image = null;
|
||||
$post->body = $markdown;
|
||||
$post->status = 'draft';
|
||||
|
||||
if ($post->save()) {
|
||||
$post_category = new PostCategory;
|
||||
$post_category->post_id = $post->id;
|
||||
$post_category->category_id = $serp_url->category->id;
|
||||
|
||||
if ($post_category->save()) {
|
||||
return self::saveAndReturnSerpProcessStatus($serp_url, 1);
|
||||
} else {
|
||||
return self::saveAndReturnSerpProcessStatus($serp_url, -5);
|
||||
}
|
||||
}
|
||||
|
||||
return self::saveAndReturnSerpProcessStatus($serp_url, -6);
|
||||
}
|
||||
|
||||
private static function saveAndReturnSerpProcessStatus($serp_url, $process_status)
|
||||
{
|
||||
$serp_url->process_status = $process_status;
|
||||
$serp_url->save();
|
||||
|
||||
return $serp_url->process_status;
|
||||
}
|
||||
}
|
||||
79
app/Jobs/Tasks/GetNewsSerpTask.php
Normal file
79
app/Jobs/Tasks/GetNewsSerpTask.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Tasks;
|
||||
|
||||
use App\Helpers\FirstParty\OSSUploader\OSSUploader;
|
||||
use App\Helpers\ThirdParty\DFS\SettingSerpLiveAdvanced;
|
||||
use App\Models\Category;
|
||||
use App\Models\NewsSerpResult;
|
||||
use DFSClientV3\DFSClient;
|
||||
use Exception;
|
||||
|
||||
class GetNewsSerpTask
|
||||
{
|
||||
public static function handle(Category $category, $country_iso)
|
||||
{
|
||||
$country_name = get_country_name_by_iso($country_iso);
|
||||
|
||||
$keyword = strtolower("{$category->name}");
|
||||
|
||||
$client = new DFSClient(
|
||||
config('dataforseo.login'),
|
||||
config('dataforseo.password'),
|
||||
config('dataforseo.timeout'),
|
||||
config('dataforseo.api_version'),
|
||||
config('dataforseo.url'),
|
||||
);
|
||||
|
||||
// You will receive SERP data specific to the indicated keyword, search engine, and location parameters
|
||||
$serp_model = new SettingSerpLiveAdvanced();
|
||||
|
||||
$serp_model->setSe('google');
|
||||
$serp_model->setSeType('news');
|
||||
$serp_model->setKeyword($keyword);
|
||||
$serp_model->setLocationName($country_name);
|
||||
$serp_model->setDepth(100);
|
||||
$serp_model->setLanguageCode('en');
|
||||
$serp_res = $serp_model->getAsJson();
|
||||
|
||||
try {
|
||||
$serp_obj = json_decode($serp_res, false, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
if ($serp_obj?->status_code == 20000) {
|
||||
$json_file_name = config('platform.dataset.news.news_serp.file_prefix').str_slug($category->name).'-'.epoch_now_timestamp().'.json';
|
||||
|
||||
$upload_status = OSSUploader::uploadJson(
|
||||
config('platform.dataset.news.news_serp.driver'),
|
||||
config('platform.dataset.news.news_serp.path'),
|
||||
$json_file_name,
|
||||
$serp_obj);
|
||||
|
||||
if ($upload_status) {
|
||||
$news_serp_result = new NewsSerpResult;
|
||||
$news_serp_result->category_id = $category->id;
|
||||
$news_serp_result->category_name = $category->name;
|
||||
$news_serp_result->serp_provider = 'dfs';
|
||||
$news_serp_result->serp_se = 'google';
|
||||
$news_serp_result->serp_se_type = 'news';
|
||||
$news_serp_result->serp_keyword = $keyword;
|
||||
$news_serp_result->serp_country_iso = strtoupper($country_iso);
|
||||
$news_serp_result->serp_cost = $serp_obj?->cost;
|
||||
$news_serp_result->result_count = $serp_obj?->tasks[0]?->result[0]?->items_count;
|
||||
$news_serp_result->filename = $json_file_name;
|
||||
$news_serp_result->status = 'initial';
|
||||
if ($news_serp_result->save()) {
|
||||
$category->serp_at = now();
|
||||
$category->save();
|
||||
}
|
||||
|
||||
return $news_serp_result;
|
||||
} else {
|
||||
throw new Exception('Uploading failed', 1);
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
146
app/Jobs/Tasks/ParseNewsSerpDomainsTask.php
Normal file
146
app/Jobs/Tasks/ParseNewsSerpDomainsTask.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Tasks;
|
||||
|
||||
use App\Helpers\FirstParty\OSSUploader\OSSUploader;
|
||||
use App\Models\Category;
|
||||
use App\Models\NewsSerpResult;
|
||||
use App\Models\SerpUrl;
|
||||
use Carbon\Carbon;
|
||||
use Exception;
|
||||
|
||||
class ParseNewsSerpDomainsTask
|
||||
{
|
||||
public static function handle(NewsSerpResult $news_serp_result, $serp_counts = 1)
|
||||
{
|
||||
//dd($news_serp_result->category->serp_at);
|
||||
|
||||
$serp_results = null;
|
||||
|
||||
$success = false;
|
||||
|
||||
try {
|
||||
|
||||
$serp_results = OSSUploader::readJson(
|
||||
config('platform.dataset.news.news_serp.driver'),
|
||||
config('platform.dataset.news.news_serp.path'),
|
||||
$news_serp_result->filename)?->tasks[0]?->result[0]?->items;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$serp_results = null;
|
||||
}
|
||||
|
||||
if (! is_null($serp_results)) {
|
||||
|
||||
$valid_serps = [];
|
||||
|
||||
foreach ($serp_results as $serp_item) {
|
||||
|
||||
$news_date = Carbon::parse($serp_item->timestamp);
|
||||
|
||||
if (is_empty($serp_item->url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if (!str_contains($serp_item->time_published, "hours"))
|
||||
// {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
$serp_url = SerpUrl::where('url', $serp_item->url)->first();
|
||||
|
||||
if (! is_null($serp_url)) {
|
||||
if ($serp_url->status == 'blocked') {
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (str_contains($serp_item->title, ':')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$valid_serps[] = $serp_item;
|
||||
|
||||
if (count($valid_serps) >= $serp_counts) {
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//dd($valid_serps);
|
||||
|
||||
foreach ($valid_serps as $serp_item) {
|
||||
|
||||
//dd($serp_item);
|
||||
|
||||
if (is_null($serp_url)) {
|
||||
$serp_url = new SerpUrl;
|
||||
$serp_url->category_id = $news_serp_result->category_id;
|
||||
$serp_url->category_name = $news_serp_result->category_name;
|
||||
$serp_url->news_serp_result_id = $news_serp_result->id;
|
||||
}
|
||||
|
||||
$serp_url->source = 'serp';
|
||||
$serp_url->url = self::normalizeUrl($serp_item->url);
|
||||
$serp_url->country_iso = $news_serp_result->serp_country_iso;
|
||||
|
||||
if (! is_empty($serp_item->title)) {
|
||||
$serp_url->title = $serp_item->title;
|
||||
}
|
||||
|
||||
if (! is_empty($serp_item->snippet)) {
|
||||
$serp_url->description = $serp_item->snippet;
|
||||
}
|
||||
|
||||
if ($serp_url->isDirty()) {
|
||||
$serp_url->serp_at = $news_serp_result->category->serp_at;
|
||||
}
|
||||
|
||||
if ($serp_url->save()) {
|
||||
$success = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
private static function normalizeUrl($url)
|
||||
{
|
||||
try {
|
||||
$parsedUrl = parse_url($url);
|
||||
|
||||
// Force the scheme to https to avoid duplicate content issues
|
||||
$parsedUrl['scheme'] = 'https';
|
||||
|
||||
if (! isset($parsedUrl['host'])) {
|
||||
// If the host is not present, throw an exception
|
||||
throw new \Exception('Host not found in URL');
|
||||
}
|
||||
|
||||
// Check if the path is set and ends with a trailing slash, if so, remove it
|
||||
if (isset($parsedUrl['path']) && substr($parsedUrl['path'], -1) === '/') {
|
||||
$parsedUrl['path'] = rtrim($parsedUrl['path'], '/');
|
||||
}
|
||||
|
||||
// Remove query parameters
|
||||
unset($parsedUrl['query']);
|
||||
|
||||
$normalizedUrl = sprintf(
|
||||
'%s://%s%s',
|
||||
$parsedUrl['scheme'],
|
||||
$parsedUrl['host'],
|
||||
$parsedUrl['path'] ?? ''
|
||||
);
|
||||
|
||||
// Remove fragment if exists
|
||||
$normalizedUrl = preg_replace('/#.*$/', '', $normalizedUrl);
|
||||
|
||||
return $normalizedUrl;
|
||||
} catch (\Exception $e) {
|
||||
// In case of an exception, return the original URL
|
||||
return $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
47
app/Models/Author.php
Normal file
47
app/Models/Author.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class Author
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $avatar
|
||||
* @property string $bio
|
||||
* @property bool $enabled
|
||||
* @property bool $public
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Collection|Post[] $posts
|
||||
*/
|
||||
class Author extends Model
|
||||
{
|
||||
protected $table = 'authors';
|
||||
|
||||
protected $casts = [
|
||||
'enabled' => 'bool',
|
||||
'public' => 'bool',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'avatar',
|
||||
'bio',
|
||||
'enabled',
|
||||
'public',
|
||||
];
|
||||
|
||||
public function posts()
|
||||
{
|
||||
return $this->hasMany(Post::class);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $short_name
|
||||
* @property string|null $slug
|
||||
* @property bool $enabled
|
||||
* @property Carbon|null $created_at
|
||||
@@ -39,6 +40,7 @@ class Category extends Model
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'short_name',
|
||||
'slug',
|
||||
'enabled',
|
||||
'_lft',
|
||||
@@ -64,4 +66,16 @@ public function saveQuietly(array $options = [])
|
||||
return $this->save($options);
|
||||
});
|
||||
}
|
||||
|
||||
public function posts()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Post::class,
|
||||
PostCategory::class,
|
||||
'category_id', // Foreign key on PostCategory table
|
||||
'id', // Local key on Post table
|
||||
'id', // Local key on Category table
|
||||
'post_id' // Foreign key on PostCategory table
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
59
app/Models/NewsSerpResult.php
Normal file
59
app/Models/NewsSerpResult.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class NewsSerpResult
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $category_id
|
||||
* @property string $category_name
|
||||
* @property string $serp_provider
|
||||
* @property string $serp_se
|
||||
* @property string $serp_se_type
|
||||
* @property string $serp_keyword
|
||||
* @property string $serp_country_iso
|
||||
* @property float|null $serp_cost
|
||||
* @property int|null $result_count
|
||||
* @property string $filename
|
||||
* @property string $status
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Category $category
|
||||
*/
|
||||
class NewsSerpResult extends Model
|
||||
{
|
||||
protected $table = 'news_serp_results';
|
||||
|
||||
protected $casts = [
|
||||
'category_id' => 'int',
|
||||
'serp_cost' => 'float',
|
||||
'result_count' => 'int',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'category_id',
|
||||
'category_name',
|
||||
'serp_provider',
|
||||
'serp_se',
|
||||
'serp_se_type',
|
||||
'serp_keyword',
|
||||
'serp_country_iso',
|
||||
'serp_cost',
|
||||
'result_count',
|
||||
'filename',
|
||||
'status',
|
||||
];
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
}
|
||||
123
app/Models/Post.php
Normal file
123
app/Models/Post.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Spatie\Feed\Feedable;
|
||||
use Spatie\Feed\FeedItem;
|
||||
|
||||
/**
|
||||
* Class Post
|
||||
*
|
||||
* @property int $id
|
||||
* @property string|null $title
|
||||
* @property string|null $slug
|
||||
* @property string|null $type
|
||||
* @property string|null $excerpt
|
||||
* @property int|null $author_id
|
||||
* @property bool $featured
|
||||
* @property string|null $featured_image
|
||||
* @property string|null $body
|
||||
* @property int $views_count
|
||||
* @property string $status
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Author|null $author
|
||||
* @property Collection|PostCategory[] $post_categories
|
||||
* @property Carbon $published_at
|
||||
*/
|
||||
class Post extends Model implements Feedable
|
||||
{
|
||||
protected $table = 'posts';
|
||||
|
||||
protected $casts = [
|
||||
'author_id' => 'int',
|
||||
'featured' => 'bool',
|
||||
'views_count' => 'int',
|
||||
'keywords' => 'array',
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'short_title',
|
||||
'slug',
|
||||
'type',
|
||||
'excerpt',
|
||||
'author_id',
|
||||
'featured',
|
||||
'featured_image',
|
||||
'body',
|
||||
'views_count',
|
||||
'status',
|
||||
'main_keyword',
|
||||
'keywords',
|
||||
'published_at',
|
||||
];
|
||||
|
||||
public function getFeaturedImageLqipCdnAttribute()
|
||||
{
|
||||
if (! is_empty($this->featured_image)) {
|
||||
// Get the extension of the original featured image
|
||||
$extension = pathinfo($this->featured_image, PATHINFO_EXTENSION);
|
||||
|
||||
// Append "_lqip" before the extension to create the LQIP image URL
|
||||
$lqipFeaturedImage = str_replace(".{$extension}", "_lqip.{$extension}", $this->featured_image);
|
||||
|
||||
return 'https://'.Storage::disk('r2')->url($lqipFeaturedImage);
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getFeaturedImageCdnAttribute()
|
||||
{
|
||||
if (! is_empty($this->featured_image)) {
|
||||
return 'https://'.Storage::disk('r2')->url($this->featured_image);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function author()
|
||||
{
|
||||
return $this->belongsTo(Author::class);
|
||||
}
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->hasOneThrough(
|
||||
Category::class, // The target model
|
||||
PostCategory::class, // The through model
|
||||
'post_id', // The foreign key on the through model
|
||||
'id', // The local key on the parent model (Post)
|
||||
'id', // The local key on the through model (PostCategory)
|
||||
'category_id' // The foreign key on the target model (Category)
|
||||
);
|
||||
}
|
||||
|
||||
public function toFeedItem(): FeedItem
|
||||
{
|
||||
return FeedItem::create([
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'summary' => $this->excerpt,
|
||||
'updated' => $this->updated_at,
|
||||
'link' => route('posts.show', $this->slug),
|
||||
'authorName' => optional($this->author)->name,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getFeedItems()
|
||||
{
|
||||
return self::where('status', 'published')->latest('published_at')->take(10)->get();
|
||||
}
|
||||
}
|
||||
46
app/Models/PostCategory.php
Normal file
46
app/Models/PostCategory.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class PostCategory
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $post_id
|
||||
* @property int $category_id
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property Post $post
|
||||
* @property Category $category
|
||||
*/
|
||||
class PostCategory extends Model
|
||||
{
|
||||
protected $table = 'post_categories';
|
||||
|
||||
protected $casts = [
|
||||
'post_id' => 'int',
|
||||
'category_id' => 'int',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'post_id',
|
||||
'category_id',
|
||||
];
|
||||
|
||||
public function post()
|
||||
{
|
||||
return $this->belongsTo(Post::class);
|
||||
}
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
}
|
||||
65
app/Models/SerpUrl.php
Normal file
65
app/Models/SerpUrl.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by Reliese Model.
|
||||
*/
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Class SerpUrl
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $news_serp_result_id
|
||||
* @property int $category_id
|
||||
* @property string $category_name
|
||||
* @property string $source
|
||||
* @property string $url
|
||||
* @property string $country_iso
|
||||
* @property string|null $title
|
||||
* @property string|null $description
|
||||
* @property Carbon|null $serp_at
|
||||
* @property string $status
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property NewsSerpResult $news_serp_result
|
||||
* @property Category $category
|
||||
*/
|
||||
class SerpUrl extends Model
|
||||
{
|
||||
protected $table = 'serp_urls';
|
||||
|
||||
protected $casts = [
|
||||
'news_serp_result_id' => 'int',
|
||||
'category_id' => 'int',
|
||||
'process_status' => 'int',
|
||||
'serp_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'news_serp_result_id',
|
||||
'category_id',
|
||||
'category_name',
|
||||
'source',
|
||||
'url',
|
||||
'country_iso',
|
||||
'title',
|
||||
'description',
|
||||
'process_status',
|
||||
'serp_at',
|
||||
'status',
|
||||
];
|
||||
|
||||
public function news_serp_result()
|
||||
{
|
||||
return $this->belongsTo(NewsSerpResult::class);
|
||||
}
|
||||
|
||||
public function category()
|
||||
{
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,10 @@ public function boot(): void
|
||||
|
||||
Route::middleware('web')
|
||||
->group(base_path('routes/web.php'));
|
||||
|
||||
Route::middleware('web')
|
||||
->prefix('tests')
|
||||
->group(base_path('routes/tests.php'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
32
app/Providers/ViewServiceProvider.php
Normal file
32
app/Providers/ViewServiceProvider.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Views\Composers\ParentCategoryComposer;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class ViewServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
// Using class based composers...
|
||||
View::composer('front.layouts.partials.nav', ParentCategoryComposer::class);
|
||||
|
||||
}
|
||||
}
|
||||
14
app/Views/Composers/ParentCategoryComposer.php
Normal file
14
app/Views/Composers/ParentCategoryComposer.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Views\Composers;
|
||||
|
||||
use App\Models\Category;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ParentCategoryComposer
|
||||
{
|
||||
public function compose(View $view)
|
||||
{
|
||||
$view->with('parent_categories', Category::whereNull('parent_id')->get());
|
||||
}
|
||||
}
|
||||
153
composer.json
153
composer.json
@@ -1,72 +1,87 @@
|
||||
{
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"artesaos/seotools": "^1.2",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"kalnoy/nestedset": "^6.0",
|
||||
"laravel/framework": "^10.10",
|
||||
"laravel/sanctum": "^3.2",
|
||||
"laravel/tinker": "^2.8",
|
||||
"spatie/laravel-googletagmanager": "^2.6",
|
||||
"tightenco/ziggy": "^1.6"
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": [
|
||||
"laravel",
|
||||
"framework"
|
||||
],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"artesaos/seotools": "^1.2",
|
||||
"graham-campbell/markdown": "^15.0",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"intervention/image": "^2.7",
|
||||
"jovix/dataforseo-clientv3": "^1.1",
|
||||
"kalnoy/nestedset": "^6.0",
|
||||
"laravel/framework": "^10.10",
|
||||
"laravel/sanctum": "^3.2",
|
||||
"laravel/tinker": "^2.8",
|
||||
"league/flysystem-aws-s3-v3": "^3.0",
|
||||
"predis/predis": "^2.2",
|
||||
"spatie/laravel-feed": "^4.3",
|
||||
"spatie/laravel-googletagmanager": "^2.6",
|
||||
"spatie/laravel-sitemap": "^6.3",
|
||||
"symfony/dom-crawler": "^6.3",
|
||||
"tightenco/ziggy": "^1.6",
|
||||
"watson/active": "^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"laravel/pint": "^1.0",
|
||||
"laravel/sail": "^1.18",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
"nunomaduro/collision": "^7.0",
|
||||
"pestphp/pest": "^2.0",
|
||||
"pestphp/pest-plugin-laravel": "^2.0",
|
||||
"reliese/laravel": "^1.2",
|
||||
"spatie/laravel-ignition": "^2.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"laravel/pint": "^1.0",
|
||||
"laravel/sail": "^1.18",
|
||||
"mockery/mockery": "^1.4.4",
|
||||
"nunomaduro/collision": "^7.0",
|
||||
"pestphp/pest": "^2.0",
|
||||
"pestphp/pest-plugin-laravel": "^2.0",
|
||||
"reliese/laravel": "^1.2",
|
||||
"spatie/laravel-ignition": "^2.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
"files": [
|
||||
"app/Helpers/Global/helpers.php"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
|
||||
1468
composer.lock
generated
1468
composer.lock
generated
File diff suppressed because it is too large
Load Diff
17
config/active.php
Normal file
17
config/active.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Active class
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may set the class string to be returned when the provided routes
|
||||
| or paths were identified as applicable for the current route.
|
||||
|
|
||||
*/
|
||||
|
||||
'class' => 'active text-primary',
|
||||
|
||||
];
|
||||
@@ -160,6 +160,7 @@
|
||||
* Package Service Providers...
|
||||
*/
|
||||
Artesaos\SEOTools\Providers\SEOToolsServiceProvider::class,
|
||||
Intervention\Image\ImageServiceProvider::class,
|
||||
|
||||
/*
|
||||
* Application Service Providers...
|
||||
@@ -169,6 +170,7 @@
|
||||
// App\Providers\BroadcastServiceProvider::class,
|
||||
App\Providers\EventServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\ViewServiceProvider::class,
|
||||
])->toArray(),
|
||||
|
||||
/*
|
||||
@@ -189,6 +191,9 @@
|
||||
'JsonLd' => Artesaos\SEOTools\Facades\JsonLd::class,
|
||||
'JsonLdMulti' => Artesaos\SEOTools\Facades\JsonLdMulti::class,
|
||||
'SEO' => Artesaos\SEOTools\Facades\SEOTools::class,
|
||||
'Image' => Intervention\Image\Facades\Image::class,
|
||||
'Markdown' => GrahamCampbell\Markdown\Facades\Markdown::class,
|
||||
|
||||
])->toArray(),
|
||||
|
||||
];
|
||||
|
||||
14
config/dataforseo.php
Normal file
14
config/dataforseo.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'login' => env('DATAFORSEO_LOGIN', 'login'),
|
||||
|
||||
'password' => env('DATAFORSEO_PASSWORD', 'password'),
|
||||
|
||||
'timeout' => 120,
|
||||
|
||||
'api_version' => '/v3/',
|
||||
|
||||
'url' => 'https://api.dataforseo.com',
|
||||
|
||||
];
|
||||
55
config/feed.php
Normal file
55
config/feed.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'feeds' => [
|
||||
'main' => [
|
||||
/*
|
||||
* Here you can specify which class and method will return
|
||||
* the items that should appear in the feed. For example:
|
||||
* [App\Model::class, 'getAllFeedItems']
|
||||
*
|
||||
* You can also pass an argument to that method. Note that their key must be the name of the parameter:
|
||||
* [App\Model::class, 'getAllFeedItems', 'parameterName' => 'argument']
|
||||
*/
|
||||
'items' => \App\Models\Post::class.'@getFeedItems',
|
||||
|
||||
/*
|
||||
* The feed will be available on this url.
|
||||
*/
|
||||
'url' => '/posts-feed',
|
||||
|
||||
'title' => 'Latest News from EchoSCoop',
|
||||
'description' => 'Bite-sized scoop for world news.',
|
||||
'language' => 'en-US',
|
||||
|
||||
/*
|
||||
* The image to display for the feed. For Atom feeds, this is displayed as
|
||||
* a banner/logo; for RSS and JSON feeds, it's displayed as an icon.
|
||||
* An empty value omits the image attribute from the feed.
|
||||
*/
|
||||
'image' => '',
|
||||
|
||||
/*
|
||||
* The format of the feed. Acceptable values are 'rss', 'atom', or 'json'.
|
||||
*/
|
||||
'format' => 'atom',
|
||||
|
||||
/*
|
||||
* The view that will render the feed.
|
||||
*/
|
||||
'view' => 'feed::atom',
|
||||
|
||||
/*
|
||||
* The mime type to be used in the <link> tag. Set to an empty string to automatically
|
||||
* determine the correct value.
|
||||
*/
|
||||
'type' => '',
|
||||
|
||||
/*
|
||||
* The content type for the feed response. Set to an empty string to automatically
|
||||
* determine the correct value.
|
||||
*/
|
||||
'contentType' => '',
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -56,6 +56,18 @@
|
||||
'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,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
156
config/markdown.php
Normal file
156
config/markdown.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Laravel Markdown.
|
||||
*
|
||||
* (c) Graham Campbell <hello@gjcampbell.co.uk>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Enable View Integration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies if the view integration is enabled so you can write
|
||||
| markdown views and have them rendered as html. The following extensions
|
||||
| are currently supported: ".md", ".md.php", and ".md.blade.php". You may
|
||||
| disable this integration if it is conflicting with another package.
|
||||
|
|
||||
| Default: true
|
||||
|
|
||||
*/
|
||||
|
||||
'views' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| CommonMark Extensions
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies what extensions will be automatically enabled.
|
||||
| Simply provide your extension class names here.
|
||||
|
|
||||
| Default: [
|
||||
| League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension::class,
|
||||
| League\CommonMark\Extension\Table\TableExtension::class,
|
||||
| ]
|
||||
|
|
||||
*/
|
||||
|
||||
'extensions' => [
|
||||
League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension::class,
|
||||
League\CommonMark\Extension\Table\TableExtension::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Renderer Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies an array of options for rendering HTML.
|
||||
|
|
||||
| Default: [
|
||||
| 'block_separator' => "\n",
|
||||
| 'inner_separator' => "\n",
|
||||
| 'soft_break' => "\n",
|
||||
| ]
|
||||
|
|
||||
*/
|
||||
|
||||
'renderer' => [
|
||||
'block_separator' => "\n",
|
||||
'inner_separator' => "\n",
|
||||
'soft_break' => "\n",
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Commonmark Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies an array of options for commonmark.
|
||||
|
|
||||
| Default: [
|
||||
| 'enable_em' => true,
|
||||
| 'enable_strong' => true,
|
||||
| 'use_asterisk' => true,
|
||||
| 'use_underscore' => true,
|
||||
| 'unordered_list_markers' => ['-', '+', '*'],
|
||||
| ]
|
||||
|
|
||||
*/
|
||||
|
||||
'commonmark' => [
|
||||
'enable_em' => true,
|
||||
'enable_strong' => true,
|
||||
'use_asterisk' => true,
|
||||
'use_underscore' => true,
|
||||
'unordered_list_markers' => ['-', '+', '*'],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTML Input
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies how to handle untrusted HTML input.
|
||||
|
|
||||
| Default: 'strip'
|
||||
|
|
||||
*/
|
||||
|
||||
'html_input' => 'strip',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Allow Unsafe Links
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies whether to allow risky image URLs and links.
|
||||
|
|
||||
| Default: true
|
||||
|
|
||||
*/
|
||||
|
||||
'allow_unsafe_links' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maximum Nesting Level
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies the maximum permitted block nesting level.
|
||||
|
|
||||
| Default: PHP_INT_MAX
|
||||
|
|
||||
*/
|
||||
|
||||
'max_nesting_level' => PHP_INT_MAX,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Slug Normalizer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option specifies an array of options for slug normalization.
|
||||
|
|
||||
| Default: [
|
||||
| 'max_length' => 255,
|
||||
| 'unique' => 'document',
|
||||
| ]
|
||||
|
|
||||
*/
|
||||
|
||||
'slug_normalizer' => [
|
||||
'max_length' => 255,
|
||||
'unique' => 'document',
|
||||
],
|
||||
|
||||
];
|
||||
9
config/platform/ai.php
Normal file
9
config/platform/ai.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'openai' => [
|
||||
'api_key' => env('OPENAI_API_KEY'),
|
||||
],
|
||||
|
||||
];
|
||||
3418
config/platform/country_codes.php
Normal file
3418
config/platform/country_codes.php
Normal file
File diff suppressed because it is too large
Load Diff
29
config/platform/dataset.php
Normal file
29
config/platform/dataset.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'news' => [
|
||||
|
||||
'news_serp' => [
|
||||
|
||||
'path' => '/datasets/news_serp/',
|
||||
|
||||
'driver' => 'r2',
|
||||
|
||||
'file_prefix' => 'news-serp-',
|
||||
|
||||
],
|
||||
|
||||
'images_serp' => [
|
||||
|
||||
'path' => '/datasets/images_serp/',
|
||||
|
||||
'driver' => 'r2',
|
||||
|
||||
'file_prefix' => 'images-serp-',
|
||||
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
@@ -30,6 +30,14 @@
|
||||
|
||||
'connections' => [
|
||||
|
||||
'default' => [
|
||||
'driver' => 'database',
|
||||
'table' => 'jobs',
|
||||
'queue' => 'default',
|
||||
'retry_after' => 90,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
57
config/sitemap.php
Normal file
57
config/sitemap.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Spatie\Sitemap\Crawler\Profile;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
* These options will be passed to GuzzleHttp\Client when it is created.
|
||||
* For in-depth information on all options see the Guzzle docs:
|
||||
*
|
||||
* http://docs.guzzlephp.org/en/stable/request-options.html
|
||||
*/
|
||||
'guzzle_options' => [
|
||||
|
||||
/*
|
||||
* Whether or not cookies are used in a request.
|
||||
*/
|
||||
RequestOptions::COOKIES => true,
|
||||
|
||||
/*
|
||||
* The number of seconds to wait while trying to connect to a server.
|
||||
* Use 0 to wait indefinitely.
|
||||
*/
|
||||
RequestOptions::CONNECT_TIMEOUT => 10,
|
||||
|
||||
/*
|
||||
* The timeout of the request in seconds. Use 0 to wait indefinitely.
|
||||
*/
|
||||
RequestOptions::TIMEOUT => 10,
|
||||
|
||||
/*
|
||||
* Describes the redirect behavior of a request.
|
||||
*/
|
||||
RequestOptions::ALLOW_REDIRECTS => false,
|
||||
],
|
||||
|
||||
/*
|
||||
* The sitemap generator can execute JavaScript on each page so it will
|
||||
* discover links that are generated by your JS scripts. This feature
|
||||
* is powered by headless Chrome.
|
||||
*/
|
||||
'execute_javascript' => false,
|
||||
|
||||
/*
|
||||
* The package will make an educated guess as to where Google Chrome is installed.
|
||||
* You can also manually pass its location here.
|
||||
*/
|
||||
'chrome_binary_path' => null,
|
||||
|
||||
/*
|
||||
* The sitemap generator uses a CrawlProfile implementation to determine
|
||||
* which urls should be crawled for the sitemap.
|
||||
*/
|
||||
'crawl_profile' => Profile::class,
|
||||
|
||||
];
|
||||
@@ -14,8 +14,11 @@ public function up(): void
|
||||
Schema::create('categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('short_name');
|
||||
$table->string('slug')->nullable();
|
||||
$table->boolean('enabled')->default(true);
|
||||
$table->bigInteger('counts')->default(0);
|
||||
$table->timestamp('serp_at')->nullable();
|
||||
$table->timestamps();
|
||||
$table->nestedSet();
|
||||
$table->index(['name', 'slug']);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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::create('news_serp_results', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('category_id')->nullable();
|
||||
$table->string('category_name')->nullable();
|
||||
$table->string('serp_provider');
|
||||
$table->string('serp_se');
|
||||
$table->string('serp_se_type');
|
||||
$table->string('serp_keyword');
|
||||
$table->string('serp_country_iso');
|
||||
$table->decimal('serp_cost')->nullable();
|
||||
$table->unsignedInteger('result_count')->nullable();
|
||||
$table->string('filename');
|
||||
$table->enum('status', ['initial'/* other potential statuses */]);
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('news_serp_results');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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::create('serp_urls', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('news_serp_result_id');
|
||||
$table->foreignId('category_id');
|
||||
$table->string('category_name');
|
||||
$table->string('source')->default('serp');
|
||||
$table->string('url');
|
||||
$table->string('country_iso');
|
||||
$table->string('title')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamp('serp_at')->nullable();
|
||||
$table->enum('status', ['initial', 'processing', 'complete', 'failed', 'blocked', 'limited'])->default('initial');
|
||||
$table->tinyInteger('process_status')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade');
|
||||
$table->foreign('news_serp_result_id')->references('id')->on('news_serp_results')->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('serp_urls');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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::create('authors', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('avatar')->nullable();
|
||||
$table->string('bio')->nullable();
|
||||
$table->boolean('enabled')->default(true);
|
||||
$table->boolean('public')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('authors');
|
||||
}
|
||||
};
|
||||
43
database/migrations/2023_09_22_165123_create_posts_table.php
Normal file
43
database/migrations/2023_09_22_165123_create_posts_table.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?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::create('posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('title')->nullable();
|
||||
$table->string('short_title')->nullable();
|
||||
$table->string('slug')->nullable();
|
||||
$table->string('type')->nullable();
|
||||
$table->string('main_keyword')->nullable();
|
||||
$table->json('keywords')->nullable();
|
||||
$table->mediumText('excerpt')->nullable();
|
||||
$table->foreignId('author_id')->nullable();
|
||||
$table->boolean('featured')->default(false);
|
||||
$table->string('featured_image')->nullable();
|
||||
$table->text('body')->nullable();
|
||||
$table->integer('views_count')->default(0);
|
||||
$table->enum('status', ['publish', 'future', 'draft', 'private', 'trash'])->default('draft');
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('author_id')->references('id')->on('authors');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('posts');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?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::create('post_categories', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('post_id');
|
||||
$table->foreignId('category_id');
|
||||
|
||||
$table->foreign('post_id')->references('id')->on('posts');
|
||||
$table->foreign('category_id')->references('id')->on('categories');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('post_categories');
|
||||
}
|
||||
};
|
||||
32
database/migrations/2023_09_24_144901_create_jobs_table.php
Normal file
32
database/migrations/2023_09_24_144901_create_jobs_table.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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::create('jobs', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
}
|
||||
};
|
||||
23
database/seeders/AuthorSeeder.php
Normal file
23
database/seeders/AuthorSeeder.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Author;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class AuthorSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
Author::create([
|
||||
'name' => 'EchoScoop Team',
|
||||
'bio' => null,
|
||||
'avatar' => null,
|
||||
'enabled' => true, // Assuming you want this author to be enabled by default
|
||||
'public' => true, // Assuming you want this author to be public by default
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -13,88 +13,90 @@ class CategorySeeder extends Seeder
|
||||
public function run(): void
|
||||
{
|
||||
$categories = [
|
||||
['name' => 'Automotive'],
|
||||
['name' => 'Automotive', 'short_name' => 'Automotive'],
|
||||
[
|
||||
'name' => 'Business',
|
||||
'short_name' => 'Business',
|
||||
'children' => [
|
||||
['name' => 'Trading'],
|
||||
['name' => 'Information Technology'],
|
||||
['name' => 'Marketing'],
|
||||
['name' => 'Office'],
|
||||
['name' => 'Telecommunications'],
|
||||
['name' => 'Trading', 'short_name' => 'Trading'],
|
||||
['name' => 'Information Technology', 'short_name' => 'IT'],
|
||||
['name' => 'Marketing', 'short_name' => 'Marketing'],
|
||||
['name' => 'Office', 'short_name' => 'Office'],
|
||||
['name' => 'Telecommunications', 'short_name' => 'Telecom'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'Entertainment',
|
||||
'children' => [
|
||||
['name' => 'Dating'],
|
||||
['name' => 'Film & Television'],
|
||||
['name' => 'Games & Toys'],
|
||||
['name' => 'Music and Video'],
|
||||
['name' => 'Adult Entertainment'],
|
||||
],
|
||||
],
|
||||
['name' => 'Food & Drink'],
|
||||
['name' => 'Food & Drink', 'short_name' => 'Food'],
|
||||
[
|
||||
'name' => 'Hobbies & Gifts',
|
||||
'short_name' => 'Hobbies',
|
||||
'children' => [
|
||||
['name' => 'Collectibles'],
|
||||
['name' => 'Pets'],
|
||||
['name' => 'Photography'],
|
||||
['name' => 'Hunting & Fishing'],
|
||||
['name' => 'Collectibles', 'short_name' => 'Collectibles'],
|
||||
['name' => 'Pets', 'short_name' => 'Pets'],
|
||||
['name' => 'Photography', 'short_name' => 'Photography'],
|
||||
['name' => 'Hunting & Fishing', 'short_name' => 'Hunting'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'Education',
|
||||
'children' => [
|
||||
['name' => 'Languages'],
|
||||
],
|
||||
],
|
||||
['name' => 'Law'],
|
||||
['name' => 'Politics'],
|
||||
['name' => 'Law', 'short_name' => 'Law'],
|
||||
['name' => 'Politics', 'short_name' => 'Politics'],
|
||||
[
|
||||
'name' => 'Shopping',
|
||||
'short_name' => 'Shopping',
|
||||
'children' => [
|
||||
['name' => 'Home & Garden'],
|
||||
['name' => 'Clothing & Accessories'],
|
||||
['name' => 'Computer & Electronics'],
|
||||
['name' => 'Home & Garden', 'short_name' => 'Home'],
|
||||
['name' => 'Fashion & Clothing', 'short_name' => 'Fashion'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'Religion & Spirituality',
|
||||
'children' => [
|
||||
['name' => 'Holistic Health'],
|
||||
],
|
||||
],
|
||||
['name' => 'Real Estate'],
|
||||
['name' => 'Social Networks'],
|
||||
['name' => 'Real Estate', 'short_name' => 'Real Estate'],
|
||||
[
|
||||
'name' => 'Society',
|
||||
'short_name' => 'Society',
|
||||
'children' => [
|
||||
['name' => 'Family'],
|
||||
['name' => 'Wedding'],
|
||||
['name' => 'Immigration'],
|
||||
['name' => 'Family', 'short_name' => 'Family'],
|
||||
['name' => 'Wedding', 'short_name' => 'Wedding'],
|
||||
['name' => 'Immigration', 'short_name' => 'Immigration'],
|
||||
[
|
||||
'name' => 'Education',
|
||||
'short_name' => 'Education',
|
||||
'children' => [
|
||||
['name' => 'Languages', 'short_name' => 'Languages'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'Wellness',
|
||||
'short_name' => 'Wellness',
|
||||
'children' => [
|
||||
['name' => 'Health & Beauty'],
|
||||
['name' => 'Psychology & Psychotherapy'],
|
||||
['name' => 'Health', 'short_name' => 'Health'],
|
||||
['name' => 'Beauty', 'short_name' => 'Beauty'],
|
||||
['name' => 'Psychology', 'short_name' => 'Psychology'],
|
||||
['name' => 'Religion & Spirituality', 'short_name' => 'Religion'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'Tips & Tricks',
|
||||
'short_name' => 'Tips',
|
||||
'children' => [
|
||||
['name' => 'How to'],
|
||||
['name' => 'How to', 'short_name' => 'How to'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'Travel',
|
||||
'short_name' => 'Travel',
|
||||
'children' => [
|
||||
['name' => 'Holiday'],
|
||||
['name' => 'World Festivals'],
|
||||
['name' => 'Outdoors'],
|
||||
['name' => 'Holiday', 'short_name' => 'Holiday'],
|
||||
['name' => 'World Festivals', 'short_name' => 'Festivals'],
|
||||
['name' => 'Outdoors', 'short_name' => 'Outdoors'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'name' => 'Technology',
|
||||
'short_name' => 'Tech',
|
||||
'children' => [
|
||||
['name' => 'Computer', 'short_name' => 'Computer'],
|
||||
['name' => 'Phones', 'short_name' => 'Phones'],
|
||||
['name' => 'Gadgets', 'short_name' => 'Gadgets'],
|
||||
['name' => 'Social Networks', 'short_name' => 'Social Networks'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
BIN
public/build/assets/Inter-Black-3afb2b05.ttf
Normal file
BIN
public/build/assets/Inter-Black-3afb2b05.ttf
Normal file
Binary file not shown.
BIN
public/build/assets/Inter-Bold-790c108b.ttf
Normal file
BIN
public/build/assets/Inter-Bold-790c108b.ttf
Normal file
Binary file not shown.
BIN
public/build/assets/Inter-ExtraBold-4e2473b9.ttf
Normal file
BIN
public/build/assets/Inter-ExtraBold-4e2473b9.ttf
Normal file
Binary file not shown.
BIN
public/build/assets/Inter-ExtraLight-edba5be0.ttf
Normal file
BIN
public/build/assets/Inter-ExtraLight-edba5be0.ttf
Normal file
Binary file not shown.
BIN
public/build/assets/Inter-Light-c44ff7a5.ttf
Normal file
BIN
public/build/assets/Inter-Light-c44ff7a5.ttf
Normal file
Binary file not shown.
BIN
public/build/assets/Inter-Medium-10d48331.ttf
Normal file
BIN
public/build/assets/Inter-Medium-10d48331.ttf
Normal file
Binary file not shown.
BIN
public/build/assets/Inter-Regular-41ab0f70.ttf
Normal file
BIN
public/build/assets/Inter-Regular-41ab0f70.ttf
Normal file
Binary file not shown.
BIN
public/build/assets/Inter-SemiBold-e8cbc2b8.ttf
Normal file
BIN
public/build/assets/Inter-SemiBold-e8cbc2b8.ttf
Normal file
Binary file not shown.
BIN
public/build/assets/Inter-Thin-b778a52b.ttf
Normal file
BIN
public/build/assets/Inter-Thin-b778a52b.ttf
Normal file
Binary file not shown.
@@ -1 +1 @@
|
||||
import{_ as o,o as p,c,a as r,b as u,p as i,d as m,e as g,f as _,g as d,v as f,Z as n,h as l}from"./vue-a36422cb.js";const A={name:"AppAuth"};function $(s,a,t,Z,w,x){return p(),c("div")}const h=o(A,[["render",$]]),e=r({AppAuth:h}),v=Object.assign({});e.use(u());e.use(i,m);e.use(g);e.use(_);e.use(d);e.use(f.ZiggyVue,n);window.Ziggy=n;Object.entries({...v}).forEach(([s,a])=>{const t=s.split("/").pop().replace(/\.\w+$/,"").replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase();e.component(t,l(a))});e.mount("#app");
|
||||
import{_ as o,o as p,c,a as r,b as u,p as i,d as m,e as g,f as _,g as d,v as f,Z as n,h as l}from"./vue-8a418f6a.js";const A={name:"AppAuth"};function $(s,a,t,Z,w,x){return p(),c("div")}const h=o(A,[["render",$]]),e=r({AppAuth:h}),v=Object.assign({});e.use(u());e.use(i,m);e.use(g);e.use(_);e.use(d);e.use(f.ZiggyVue,n);window.Ziggy=n;Object.entries({...v}).forEach(([s,a])=>{const t=s.split("/").pop().replace(/\.\w+$/,"").replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase();e.component(t,l(a))});e.mount("#app");
|
||||
9
public/build/assets/app-front-48c01c04.css
Normal file
9
public/build/assets/app-front-48c01c04.css
Normal file
File diff suppressed because one or more lines are too long
BIN
public/build/assets/app-front-48c01c04.css.gz
Normal file
BIN
public/build/assets/app-front-48c01c04.css.gz
Normal file
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
public/build/assets/app-front-d35e891f.js.gz
Normal file
BIN
public/build/assets/app-front-d35e891f.js.gz
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
public/build/assets/vue-8a418f6a.js.gz
Normal file
BIN
public/build/assets/vue-8a418f6a.js.gz
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"_vue-a36422cb.js": {
|
||||
"_vue-8a418f6a.js": {
|
||||
"css": [
|
||||
"assets/vue-935fc652.css"
|
||||
],
|
||||
"file": "assets/vue-a36422cb.js"
|
||||
"file": "assets/vue-8a418f6a.js"
|
||||
},
|
||||
"node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff": {
|
||||
"file": "assets/bootstrap-icons-4d4572ef.woff",
|
||||
@@ -13,31 +13,67 @@
|
||||
"file": "assets/bootstrap-icons-bacd70af.woff2",
|
||||
"src": "node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff2"
|
||||
},
|
||||
"resources/fonts/Inter/Inter-Black.ttf": {
|
||||
"file": "assets/Inter-Black-3afb2b05.ttf",
|
||||
"src": "resources/fonts/Inter/Inter-Black.ttf"
|
||||
},
|
||||
"resources/fonts/Inter/Inter-Bold.ttf": {
|
||||
"file": "assets/Inter-Bold-790c108b.ttf",
|
||||
"src": "resources/fonts/Inter/Inter-Bold.ttf"
|
||||
},
|
||||
"resources/fonts/Inter/Inter-ExtraBold.ttf": {
|
||||
"file": "assets/Inter-ExtraBold-4e2473b9.ttf",
|
||||
"src": "resources/fonts/Inter/Inter-ExtraBold.ttf"
|
||||
},
|
||||
"resources/fonts/Inter/Inter-ExtraLight.ttf": {
|
||||
"file": "assets/Inter-ExtraLight-edba5be0.ttf",
|
||||
"src": "resources/fonts/Inter/Inter-ExtraLight.ttf"
|
||||
},
|
||||
"resources/fonts/Inter/Inter-Light.ttf": {
|
||||
"file": "assets/Inter-Light-c44ff7a5.ttf",
|
||||
"src": "resources/fonts/Inter/Inter-Light.ttf"
|
||||
},
|
||||
"resources/fonts/Inter/Inter-Medium.ttf": {
|
||||
"file": "assets/Inter-Medium-10d48331.ttf",
|
||||
"src": "resources/fonts/Inter/Inter-Medium.ttf"
|
||||
},
|
||||
"resources/fonts/Inter/Inter-Regular.ttf": {
|
||||
"file": "assets/Inter-Regular-41ab0f70.ttf",
|
||||
"src": "resources/fonts/Inter/Inter-Regular.ttf"
|
||||
},
|
||||
"resources/fonts/Inter/Inter-SemiBold.ttf": {
|
||||
"file": "assets/Inter-SemiBold-e8cbc2b8.ttf",
|
||||
"src": "resources/fonts/Inter/Inter-SemiBold.ttf"
|
||||
},
|
||||
"resources/fonts/Inter/Inter-Thin.ttf": {
|
||||
"file": "assets/Inter-Thin-b778a52b.ttf",
|
||||
"src": "resources/fonts/Inter/Inter-Thin.ttf"
|
||||
},
|
||||
"resources/js/app-auth.js": {
|
||||
"file": "assets/app-auth-34561e68.js",
|
||||
"file": "assets/app-auth-c0ceb740.js",
|
||||
"imports": [
|
||||
"_vue-a36422cb.js"
|
||||
"_vue-8a418f6a.js"
|
||||
],
|
||||
"isEntry": true,
|
||||
"src": "resources/js/app-auth.js"
|
||||
},
|
||||
"resources/js/app-front.js": {
|
||||
"file": "assets/app-front-948dc8d8.js",
|
||||
"file": "assets/app-front-d35e891f.js",
|
||||
"imports": [
|
||||
"_vue-a36422cb.js"
|
||||
"_vue-8a418f6a.js"
|
||||
],
|
||||
"isEntry": true,
|
||||
"src": "resources/js/app-front.js"
|
||||
},
|
||||
"resources/sass/app-auth.scss": {
|
||||
"file": "assets/app-front-d32494d2.css",
|
||||
"file": "assets/app-auth-d32494d2.css",
|
||||
"isEntry": true,
|
||||
"src": "resources/sass/app-auth.scss"
|
||||
},
|
||||
"resources/sass/app-front.scss": {
|
||||
"file": "assets/app-front-d32494d2.css",
|
||||
"file": "assets/app-front-48c01c04.css",
|
||||
"isEntry": true,
|
||||
"src": "resources/sass/app-auth.scss"
|
||||
"src": "resources/sass/app-front.scss"
|
||||
},
|
||||
"vue.css": {
|
||||
"file": "assets/vue-935fc652.css",
|
||||
|
||||
Binary file not shown.
82
public/vendor/feed/atom.xsl
vendored
Normal file
82
public/vendor/feed/atom.xsl
vendored
Normal file
@@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
|
||||
<xsl:template match="/">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||
<head>
|
||||
<title>
|
||||
RSS Feed | <xsl:value-of select="/atom:feed/atom:title"/>
|
||||
</title>
|
||||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<link rel="stylesheet" href="/vendor/feed/style.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<main class="layout-content">
|
||||
<h1 class="flex items-start">
|
||||
<!-- https://commons.wikimedia.org/wiki/File:Feed-icon.svg -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1"
|
||||
class="mr-5"
|
||||
style="flex-shrink: 0; width: 1em; height: 1em;"
|
||||
viewBox="0 0 256 256">
|
||||
<defs>
|
||||
<linearGradient x1="0.085" y1="0.085" x2="0.915" y2="0.915"
|
||||
id="RSSg">
|
||||
<stop offset="0.0" stop-color="#E3702D"/>
|
||||
<stop offset="0.1071" stop-color="#EA7D31"/>
|
||||
<stop offset="0.3503" stop-color="#F69537"/>
|
||||
<stop offset="0.5" stop-color="#FB9E3A"/>
|
||||
<stop offset="0.7016" stop-color="#EA7C31"/>
|
||||
<stop offset="0.8866" stop-color="#DE642B"/>
|
||||
<stop offset="1.0" stop-color="#D95B29"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="256" height="256" rx="55" ry="55" x="0" y="0"
|
||||
fill="#CC5D15"/>
|
||||
<rect width="246" height="246" rx="50" ry="50" x="5" y="5"
|
||||
fill="#F49C52"/>
|
||||
<rect width="236" height="236" rx="47" ry="47" x="10" y="10"
|
||||
fill="url(#RSSg)"/>
|
||||
<circle cx="68" cy="189" r="24" fill="#FFF"/>
|
||||
<path
|
||||
d="M160 213h-34a82 82 0 0 0 -82 -82v-34a116 116 0 0 1 116 116z"
|
||||
fill="#FFF"/>
|
||||
<path
|
||||
d="M184 213A140 140 0 0 0 44 73 V 38a175 175 0 0 1 175 175z"
|
||||
fill="#FFF"/>
|
||||
</svg>
|
||||
RSS Feed
|
||||
</h1>
|
||||
<h2><xsl:value-of select="/atom:feed/atom:title"/></h2>
|
||||
<p>
|
||||
<xsl:value-of select="/atom:feed/atom:subtitle"/>
|
||||
</p>
|
||||
<hr/>
|
||||
<xsl:for-each select="/atom:feed/atom:entry">
|
||||
<div class="post">
|
||||
<div class="title">
|
||||
<a>
|
||||
<xsl:attribute name="href">
|
||||
<xsl:value-of select="atom:link/@href"/>
|
||||
</xsl:attribute>
|
||||
<xsl:value-of select="atom:title"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="summary">
|
||||
<xsl:value-of select="atom:summary" disable-output-escaping="yes"/>
|
||||
</div>
|
||||
|
||||
<div class="published-info">
|
||||
Published on
|
||||
<xsl:value-of select="substring(atom:updated, 0, 11)" /> by <xsl:value-of select="atom:author/atom:name"/>
|
||||
</div>
|
||||
</div>
|
||||
</xsl:for-each>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
37
public/vendor/feed/style.css
vendored
Normal file
37
public/vendor/feed/style.css
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
.layout-content {
|
||||
max-width: 640px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.post {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.post .title {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.post .summary {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.post .published-info {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.nav-scroller {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 2.75rem;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.nav-scroller .nav {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
padding-bottom: 1rem;
|
||||
margin-top: -1px;
|
||||
overflow-x: auto;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.hover-text-decoration-underline:hover {
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
BIN
resources/fonts/Inter/Inter-Black.ttf
Normal file
BIN
resources/fonts/Inter/Inter-Black.ttf
Normal file
Binary file not shown.
BIN
resources/fonts/Inter/Inter-Bold.ttf
Normal file
BIN
resources/fonts/Inter/Inter-Bold.ttf
Normal file
Binary file not shown.
BIN
resources/fonts/Inter/Inter-ExtraBold.ttf
Normal file
BIN
resources/fonts/Inter/Inter-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
resources/fonts/Inter/Inter-ExtraLight.ttf
Normal file
BIN
resources/fonts/Inter/Inter-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
resources/fonts/Inter/Inter-Light.ttf
Normal file
BIN
resources/fonts/Inter/Inter-Light.ttf
Normal file
Binary file not shown.
BIN
resources/fonts/Inter/Inter-Medium.ttf
Normal file
BIN
resources/fonts/Inter/Inter-Medium.ttf
Normal file
Binary file not shown.
BIN
resources/fonts/Inter/Inter-Regular.ttf
Normal file
BIN
resources/fonts/Inter/Inter-Regular.ttf
Normal file
Binary file not shown.
BIN
resources/fonts/Inter/Inter-SemiBold.ttf
Normal file
BIN
resources/fonts/Inter/Inter-SemiBold.ttf
Normal file
Binary file not shown.
BIN
resources/fonts/Inter/Inter-Thin.ttf
Normal file
BIN
resources/fonts/Inter/Inter-Thin.ttf
Normal file
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
const Ziggy = {"url":"https:\/\/echoscoop.com","port":null,"defaults":{},"routes":{"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"]}}};
|
||||
const Ziggy = {"url":"https:\/\/echoscoop.com","port":null,"defaults":{},"routes":{"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"]},"feeds.main":{"uri":"feeds\/posts-feed","methods":["GET","HEAD"]},"front.home":{"uri":"\/","methods":["GET","HEAD"]},"front.terms":{"uri":"terms","methods":["GET","HEAD"]},"front.privacy":{"uri":"privacy","methods":["GET","HEAD"]},"front.disclaimer":{"uri":"disclaimer","methods":["GET","HEAD"]},"front.all":{"uri":"news","methods":["GET","HEAD"]},"front.post":{"uri":"news\/{slug}","methods":["GET","HEAD"]},"front.category":{"uri":"{category_slug}","methods":["GET","HEAD"]}}};
|
||||
|
||||
if (typeof window !== 'undefined' && typeof window.Ziggy !== 'undefined') {
|
||||
Object.assign(Ziggy.routes, window.Ziggy.routes);
|
||||
|
||||
29
resources/markdown/disclaimer.md
Normal file
29
resources/markdown/disclaimer.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# **Disclaimer**
|
||||
|
||||
## **1. General Information**
|
||||
|
||||
- **Purpose**: The content on this website is solely for **informational purposes**. It should not be taken as legal, financial, or medical advice.
|
||||
- **Professional Advice**: Before making any decisions based on this content, consult with a **lawyer** for legal matters, a **financial advisor** for financial queries, and a **medical professional** for health-related issues.
|
||||
|
||||
## **2. Accuracy & Completeness**
|
||||
|
||||
- **No Guarantee**: The accuracy, completeness, or suitability of the information on this website for any particular purpose is not guaranteed.
|
||||
- **Third-party Sources**: We hold no responsibility for the accuracy or completeness of information sourced from third parties.
|
||||
|
||||
## **3. Liability**
|
||||
|
||||
- **Risks**: Any use of the information on this website is **at your own risk**.
|
||||
- **No Liability**: We shall not be liable for any consequences including but not limited to loss of profits, data, or other losses resulting from the use of this website.
|
||||
- **Legal Actions**: Users acknowledge that we bear no liability for any legal actions arising from their use of this website.
|
||||
|
||||
## **4. Third-Party Links & Content**
|
||||
|
||||
- **External Links**: This website may provide links to external sites. We neither control nor are responsible for the content, policies, or practices of such third-party websites.
|
||||
|
||||
## **5. Agreement**
|
||||
|
||||
- **Terms**: By using this website, you agree to this disclaimer and all related terms of use.
|
||||
|
||||
## **6. Contact**
|
||||
|
||||
If you have any questions regarding this disclaimer, contact us via email: support@echoscoop.com
|
||||
241
resources/markdown/privacy.md
Normal file
241
resources/markdown/privacy.md
Normal file
@@ -0,0 +1,241 @@
|
||||
Updated at 2023-09-24
|
||||
|
||||
EchoScoop.com (“we,” “our,” or “us”) is committed to protecting your privacy. This Privacy Policy explains how your personal information is collected, used, and disclosed by EchoScoop.com.
|
||||
|
||||
This Privacy Policy applies to our website, and its associated subdomains (collectively, our “Service”) alongside our application, EchoScoop.com. By accessing or using our Service, you signify that you have read, understood, and agree to our collection, storage, use, and disclosure of your personal information as described in this Privacy Policy and our Terms of Service.
|
||||
|
||||
## Definitions and key terms
|
||||
|
||||
To help explain things as clearly as possible in this Privacy Policy, every time any of these terms are referenced, are strictly defined as:
|
||||
|
||||
- Cookie: small amount of data generated by a website and saved by your web browser. It is used to identify your browser, provide analytics, remember information about you such as your language preference or login information.
|
||||
- Company: when this policy mentions “Company,” “we,” “us,” or “our,” it refers to Exastellar Industries, (35A, Jalan PSK 6, Pusat Perdagangan Seri Kembangan) that is responsible for your information under this Privacy Policy.
|
||||
- Country: where EchoScoop.com or the owners/founders of EchoScoop.com are based, in this case is Malaysia
|
||||
- Customer: refers to the company, organization or person that signs up to use the EchoScoop.com Service to manage the relationships with your consumers or service users.
|
||||
- Device: any internet connected device such as a phone, tablet, computer or any other device that can be used to visit EchoScoop.com and use the services.
|
||||
- IP address: Every device connected to the Internet is assigned a number known as an Internet protocol (IP) address. These numbers are usually assigned in geographic blocks. An IP address can often be used to identify the location from which a device is connecting to the Internet.
|
||||
- Personnel: refers to those individuals who are employed by EchoScoop.com or are under contract to perform a service on behalf of one of the parties.
|
||||
- Personal Data: any information that directly, indirectly, or in connection with other information — including a personal identification number — allows for the identification or identifiability of a natural person.
|
||||
- Service: refers to the service provided by EchoScoop.com as described in the relative terms (if available) and on this platform.
|
||||
- Third-party service: refers to advertisers, contest sponsors, promotional and marketing partners, and others who provide our content or whose products or services we think may interest you.
|
||||
- Website: EchoScoop.com."’s" site, which can be accessed via this URL: https://EchoScoop.com
|
||||
- You: a person or entity that is registered with EchoScoop.com to use the Services.
|
||||
|
||||
## What Information Do We Collect?
|
||||
|
||||
We collect information from you when you visit our website, register on our site, place an order, subscribe to our newsletter, respond to a survey or fill out a form.
|
||||
|
||||
- Name / Username
|
||||
- Email Addresses
|
||||
- Job Titles
|
||||
- Billing Addresses
|
||||
- Debit/credit card numbers
|
||||
|
||||
We also collect information from mobile devices for a better user experience, although these features are completely optional:
|
||||
|
||||
## How Do We Use The Information We Collect?
|
||||
|
||||
Any of the information we collect from you may be used in one of the following ways:
|
||||
|
||||
- To personalize your experience (your information helps us to better respond to your individual needs)
|
||||
- To improve our website (we continually strive to improve our website offerings based on the information and feedback we receive from you)
|
||||
- To improve customer service (your information helps us to more effectively respond to your customer service requests and support needs)
|
||||
- To process transactions
|
||||
- To administer a contest, promotion, survey or other site feature
|
||||
- To send periodic emails
|
||||
|
||||
## When does EchoScoop.com use end user information from third parties?
|
||||
|
||||
EchoScoop.com will collect End User Data necessary to provide the EchoScoop.com services to our customers.
|
||||
|
||||
End users may voluntarily provide us with information they have made available on social media websites. If you provide us with any such information, we may collect publicly available information from the social media websites you have indicated. You can control how much of your information social media websites make public by visiting these websites and changing your privacy settings.
|
||||
|
||||
## When does EchoScoop.com use customer information from third parties?
|
||||
|
||||
We receive some information from the third parties when you contact us. For example, when you submit your email address to us to show interest in becoming a EchoScoop.com customer, we receive information from a third party that provides automated fraud detection services to EchoScoop.com. We also occasionally collect information that is made publicly available on social media websites. You can control how much of your information social media websites make public by visiting these websites and changing your privacy settings.
|
||||
|
||||
## Do we share the information we collect with third parties?
|
||||
|
||||
We may share the information that we collect, both personal and non-personal, with third parties such as advertisers, contest sponsors, promotional and marketing partners, and others who provide our content or whose products or services we think may interest you. We may also share it with our current and future affiliated companies and business partners, and if we are involved in a merger, asset sale or other business reorganization, we may also share or transfer your personal and non-personal information to our successors-in-interest.
|
||||
|
||||
We may engage trusted third party service providers to perform functions and provide services to us, such as hosting and maintaining our servers and the website, database storage and management, e-mail management, storage marketing, credit card processing, customer service and fulfilling orders for products and services you may purchase through the website. We will likely share your personal information, and possibly some non-personal information, with these third parties to enable them to perform these services for us and for you.
|
||||
|
||||
We may share portions of our log file data, including IP addresses, for analytics purposes with third parties such as web analytics partners, application developers, and ad networks. If your IP address is shared, it may be used to estimate general location and other technographics such as connection speed, whether you have visited the website in a shared location, and type of the device used to visit the website. They may aggregate information about our advertising and what you see on the website and then provide auditing, research and reporting for us and our advertisers.
|
||||
|
||||
We may also disclose personal and non-personal information about you to government or law enforcement officials or private parties as we, in our sole discretion, believe necessary or appropriate in order to respond to claims, legal process (including subpoenas), to protect our rights and interests or those of a third party, the safety of the public or any person, to prevent or stop any illegal, unethical, or legally actionable activity, or to otherwise comply with applicable court orders, laws, rules and regulations.
|
||||
|
||||
## Where and when is information collected from customers and end users?
|
||||
|
||||
EchoScoop.com will collect personal information that you submit to us. We may also receive personal information about you from third parties as described above.
|
||||
|
||||
## How Do We Use Your Email Address?
|
||||
|
||||
By submitting your email address on this website, you agree to receive emails from us. You can cancel your participation in any of these email lists at any time by clicking on the opt-out link or other unsubscribe option that is included in the respective email. We only send emails to people who have authorized us to contact them, either directly, or through a third party. We do not send unsolicited commercial emails, because we hate spam as much as you do. By submitting your email address, you also agree to allow us to use your email address for customer audience targeting on sites like Facebook, where we display custom advertising to specific people who have opted-in to receive communications from us. Email addresses submitted only through the order processing page will be used for the sole purpose of sending you information and updates pertaining to your order. If, however, you have provided the same email to us through another method, we may use it for any of the purposes stated in this Policy. Note: If at any time you would like to unsubscribe from receiving future emails, we include detailed unsubscribe instructions at the bottom of each email.
|
||||
|
||||
## How Long Do We Keep Your Information?
|
||||
|
||||
We keep your information only so long as we need it to provide EchoScoop.com to you and fulfill the purposes described in this policy. This is also the case for anyone that we share your information with and who carries out services on our behalf. When we no longer need to use your information and there is no need for us to keep it to comply with our legal or regulatory obligations, we’ll either remove it from our systems or depersonalize it so that we can't identify you.
|
||||
|
||||
## How Do We Protect Your Information?
|
||||
|
||||
We implement a variety of security measures to maintain the safety of your personal information when you place an order or enter, submit, or access your personal information. We offer the use of a secure server. All supplied sensitive/credit information is transmitted via Secure Socket Layer (SSL) technology and then encrypted into our Payment gateway providers database only to be accessible by those authorized with special access rights to such systems, and are required to keep the information confidential. After a transaction, your private information (credit cards, social security numbers, financials, etc.) is never kept on file. We cannot, however, ensure or warrant the absolute security of any information you transmit to EchoScoop.com or guarantee that your information on the Service may not be accessed, disclosed, altered, or destroyed by a breach of any of our physical, technical, or managerial safeguards.
|
||||
|
||||
## Could my information be transferred to other countries?
|
||||
|
||||
Exastellar Industries is incorporated in Malaysia. Information collected via our website, through direct interactions with you, or from use of our help services may be transferred from time to time to our offices or personnel, or to third parties, located throughout the world, and may be viewed and hosted anywhere in the world, including countries that may not have laws of general applicability regulating the use and transfer of such data. To the fullest extent allowed by applicable law, by using any of the above, you voluntarily consent to the trans-border transfer and hosting of such information.
|
||||
|
||||
## Is the information collected through the EchoScoop.com Service secure?
|
||||
|
||||
We take precautions to protect the security of your information. We have physical, electronic, and managerial procedures to help safeguard, prevent unauthorized access, maintain data security, and correctly use your information. However, neither people nor security systems are foolproof, including encryption systems. In addition, people can commit intentional crimes, make mistakes or fail to follow policies. Therefore, while we use reasonable efforts to protect your personal information, we cannot guarantee its absolute security. If applicable law imposes any non-disclaimable duty to protect your personal information, you agree that intentional misconduct will be the standards used to measure our compliance with that duty.
|
||||
|
||||
## Can I update or correct my information?
|
||||
|
||||
The rights you have to request updates or corrections to the information EchoScoop.com collects depend on your relationship with EchoScoop.com. Personnel may update or correct their information as detailed in our internal company employment policies.
|
||||
|
||||
Customers have the right to request the restriction of certain uses and disclosures of personally identifiable information as follows. You can contact us in order to (1) update or correct your personally identifiable information, (2) change your preferences with respect to communications and other information you receive from us, or (3) delete the personally identifiable information maintained about you on our systems (subject to the following paragraph), by cancelling your account. Such updates, corrections, changes and deletions will have no effect on other information that we maintain, or information that we have provided to third parties in accordance with this Privacy Policy prior to such update, correction, change or deletion. To protect your privacy and security, we may take reasonable steps (such as requesting a unique password) to verify your identity before granting you profile access or making corrections. You are responsible for maintaining the secrecy of your unique password and account information at all times.
|
||||
|
||||
You should be aware that it is not technologically possible to remove each and every record of the information you have provided to us from our system. The need to back up our systems to protect information from inadvertent loss means that a copy of your information may exist in a non-erasable form that will be difficult or impossible for us to locate. Promptly after receiving your request, all personal information stored in databases we actively use, and other readily searchable media will be updated, corrected, changed or deleted, as appropriate, as soon as and to the extent reasonably and technically practicable.
|
||||
|
||||
If you are an end user and wish to update, delete, or receive any information we have about you, you may do so by contacting the organization of which you are a customer.
|
||||
|
||||
## Personnel
|
||||
|
||||
If you are a EchoScoop.com worker or applicant, we collect information you voluntarily provide to us. We use the information collected for Human Resources purposes in order to administer benefits to workers and screen applicants.
|
||||
|
||||
You may contact us in order to (1) update or correct your information, (2) change your preferences with respect to communications and other information you receive from us, or (3) receive a record of the information we have relating to you. Such updates, corrections, changes and deletions will have no effect on other information that we maintain, or information that we have provided to third parties in accordance with this Privacy Policy prior to such update, correction, change or deletion.
|
||||
|
||||
## Sale of Business
|
||||
|
||||
We reserve the right to transfer information to a third party in the event of a sale, merger or other transfer of all or substantially all of the assets of EchoScoop.com or any of its Corporate Affiliates (as defined herein), or that portion of EchoScoop.com or any of its Corporate Affiliates to which the Service relates, or in the event that we discontinue our business or file a petition or have filed against us a petition in bankruptcy, reorganization or similar proceeding, provided that the third party agrees to adhere to the terms of this Privacy Policy.
|
||||
|
||||
## Affiliates
|
||||
|
||||
We may disclose information (including personal information) about you to our Corporate Affiliates. For purposes of this Privacy Policy, "Corporate Affiliate" means any person or entity which directly or indirectly controls, is controlled by or is under common control with EchoScoop.com, whether by ownership or otherwise. Any information relating to you that we provide to our Corporate Affiliates will be treated by those Corporate Affiliates in accordance with the terms of this Privacy Policy.
|
||||
|
||||
## Governing Law
|
||||
|
||||
This Privacy Policy is governed by the laws of Malaysia without regard to its conflict of laws provision. You consent to the exclusive jurisdiction of the courts in connection with any action or dispute arising between the parties under or in connection with this Privacy Policy except for those individuals who may have rights to make claims under Privacy Shield, or the Swiss-US framework.
|
||||
|
||||
The laws of Malaysia, excluding its conflicts of law rules, shall govern this Agreement and your use of the website. Your use of the website may also be subject to other local, state, national, or international laws.
|
||||
|
||||
By using EchoScoop.com or contacting us directly, you signify your acceptance of this Privacy Policy. If you do not agree to this Privacy Policy, you should not engage with our website, or use our services. Continued use of the website, direct engagement with us, or following the posting of changes to this Privacy Policy that do not significantly affect the use or disclosure of your personal information will mean that you accept those changes.
|
||||
|
||||
## Your Consent
|
||||
|
||||
We've updated our Privacy Policy to provide you with complete transparency into what is being set when you visit our site and how it's being used. By using our website, registering an account, or making a purchase, you hereby consent to our Privacy Policy and agree to its terms.
|
||||
|
||||
## Links to Other Websites
|
||||
|
||||
This Privacy Policy applies only to the Services. The Services may contain links to other websites not operated or controlled by EchoScoop.com. We are not responsible for the content, accuracy or opinions expressed in such websites, and such websites are not investigated, monitored or checked for accuracy or completeness by us. Please remember that when you use a link to go from the Services to another website, our Privacy Policy is no longer in effect. Your browsing and interaction on any other website, including those that have a link on our platform, is subject to that website’s own rules and policies. Such third parties may use their own cookies or other methods to collect information about you.
|
||||
|
||||
## Advertising
|
||||
|
||||
This website may contain third party advertisements and links to third party sites. EchoScoop.com does not make any representation as to the accuracy or suitability of any of the information contained in those advertisements or sites and does not accept any responsibility or liability for the conduct or content of those advertisements and sites and the offerings made by the third parties.
|
||||
|
||||
Advertising keeps EchoScoop.com and many of the websites and services you use free of charge. We work hard to make sure that ads are safe, unobtrusive, and as relevant as possible.
|
||||
|
||||
Third party advertisements and links to other sites where goods or services are advertised are not endorsements or recommendations by EchoScoop.com of the third party sites, goods or services. EchoScoop.com takes no responsibility for the content of any of the ads, promises made, or the quality/reliability of the products or services offered in all advertisements.
|
||||
|
||||
## Cookies for Advertising
|
||||
|
||||
These cookies collect information over time about your online activity on the website and other online services to make online advertisements more relevant and effective to you. This is known as interest-based advertising. They also perform functions like preventing the same ad from continuously reappearing and ensuring that ads are properly displayed for advertisers. Without cookies, it’s really hard for an advertiser to reach its audience, or to know how many ads were shown and how many clicks they received.
|
||||
|
||||
## Cookies
|
||||
|
||||
EchoScoop.com uses "Cookies" to identify the areas of our website that you have visited. A Cookie is a small piece of data stored on your computer or mobile device by your web browser. We use Cookies to enhance the performance and functionality of our website but are non-essential to their use. However, without these cookies, certain functionality like videos may become unavailable or you would be required to enter your login details every time you visit the website as we would not be able to remember that you had logged in previously. Most web browsers can be set to disable the use of Cookies. However, if you disable Cookies, you may not be able to access functionality on our website correctly or at all. We never place Personally Identifiable Information in Cookies.
|
||||
|
||||
## Blocking and disabling cookies and similar technologies
|
||||
|
||||
Wherever you're located you may also set your browser to block cookies and similar technologies, but this action may block our essential cookies and prevent our website from functioning properly, and you may not be able to fully utilize all of its features and services. You should also be aware that you may also lose some saved information (e.g. saved login details, site preferences) if you block cookies on your browser. Different browsers make different controls available to you. Disabling a cookie or category of cookie does not delete the cookie from your browser, you will need to do this yourself from within your browser, you should visit your browser's help menu for more information.
|
||||
|
||||
## Remarketing Services
|
||||
|
||||
We use remarketing services. What Is Remarketing? In digital marketing, remarketing (or retargeting) is the practice of serving ads across the internet to people who have already visited your website. It allows your company to seem like they're “following” people around the internet by serving ads on the websites and platforms they use most.
|
||||
|
||||
## Payment Details
|
||||
|
||||
In respect to any credit card or other payment processing details you have provided us, we commit that this confidential information will be stored in the most secure manner possible.
|
||||
|
||||
## Kids' Privacy
|
||||
|
||||
We do not address anyone under the age of 13. We do not knowingly collect personally identifiable information from anyone under the age of 13. If You are a parent or guardian and You are aware that Your child has provided Us with Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under the age of 13 without verification of parental consent, We take steps to remove that information from Our servers.
|
||||
|
||||
## Changes To Our Privacy Policy
|
||||
|
||||
We may change our Service and policies, and we may need to make changes to this Privacy Policy so that they accurately reflect our Service and policies. Unless otherwise required by law, we will notify you (for example, through our Service) before we make changes to this Privacy Policy and give you an opportunity to review them before they go into effect. Then, if you continue to use the Service, you will be bound by the updated Privacy Policy. If you do not want to agree to this or any updated Privacy Policy, you can delete your account.
|
||||
|
||||
## Third-Party Services
|
||||
|
||||
We may display, include or make available third-party content (including data, information, applications and other products services) or provide links to third-party websites or services ("Third- Party Services").
|
||||
You acknowledge and agree that EchoScoop.com shall not be responsible for any Third-Party Services, including their accuracy, completeness, timeliness, validity, copyright compliance, legality, decency, quality or any other aspect thereof. EchoScoop.com does not assume and shall not have any liability or responsibility to you or any other person or entity for any Third-Party Services.
|
||||
Third-Party Services and links thereto are provided solely as a convenience to you and you access and use them entirely at your own risk and subject to such third parties' terms and conditions.
|
||||
|
||||
## Facebook Pixel
|
||||
|
||||
Facebook pixel is an analytics tool that allows you to measure the effectiveness of your advertising by understanding the actions people take on your website. You can use the pixel to: Make sure your ads are shown to the right people. Facebook pixel may collect information from your device when you use the service. Facebook pixel collects information that is held in accordance with its Privacy Policy
|
||||
|
||||
## Tracking Technologies
|
||||
|
||||
- Cookies
|
||||
|
||||
We use Cookies to enhance the performance and functionality of our website but are non-essential to their use. However, without these cookies, certain functionality like videos may become unavailable or you would be required to enter your login details every time you visit the website as we would not be able to remember that you had logged in previously.
|
||||
|
||||
- Local Storage
|
||||
|
||||
Local Storage sometimes known as DOM storage, provides web apps with methods and protocols for storing client-side data. Web storage supports persistent data storage, similar to cookies but with a greatly enhanced capacity and no information stored in the HTTP request header.
|
||||
|
||||
- Sessions
|
||||
|
||||
EchoScoop.com uses "Sessions" to identify the areas of our website that you have visited. A Session is a small piece of data stored on your computer or mobile device by your web browser.
|
||||
|
||||
## Information about General Data Protection Regulation (GDPR)
|
||||
|
||||
We may be collecting and using information from you if you are from the European Economic Area (EEA), and in this section of our Privacy Policy we are going to explain exactly how and why is this data collected, and how we maintain this data under protection from being replicated or used in the wrong way.
|
||||
|
||||
### What is GDPR?
|
||||
|
||||
GDPR is an EU-wide privacy and data protection law that regulates how EU residents' data is protected by companies and enhances the control the EU residents have, over their personal data.
|
||||
|
||||
The GDPR is relevant to any globally operating company and not just the EU-based businesses and EU residents. Our customers’ data is important irrespective of where they are located, which is why we have implemented GDPR controls as our baseline standard for all our operations worldwide.
|
||||
|
||||
### What is personal data?
|
||||
|
||||
Any data that relates to an identifiable or identified individual. GDPR covers a broad spectrum of information that could be used on its own, or in combination with other pieces of information, to identify a person. Personal data extends beyond a person’s name or email address. Some examples include financial information, political opinions, genetic data, biometric data, IP addresses, physical address, sexual orientation, and ethnicity.
|
||||
|
||||
The Data Protection Principles include requirements such as:
|
||||
|
||||
- Personal data collected must be processed in a fair, legal, and transparent way and should only be used in a way that a person would reasonably expect.
|
||||
- Personal data should only be collected to fulfil a specific purpose and it should only be used for that purpose. Organizations must specify why they need the personal data when they collect it.
|
||||
- Personal data should be held no longer than necessary to fulfil its purpose.
|
||||
- People covered by the GDPR have the right to access their own personal data. They can also request a copy of their data, and that their data be updated, deleted, restricted, or moved to another organization.
|
||||
|
||||
### Why is GDPR important?
|
||||
|
||||
GDPR adds some new requirements regarding how companies should protect individuals' personal data that they collect and process. It also raises the stakes for compliance by increasing enforcement and imposing greater fines for breach. Beyond these facts it's simply the right thing to do. At EchoScoop.com we strongly believe that your data privacy is very important and we already have solid security and privacy practices in place that go beyond the requirements of this new regulation.
|
||||
|
||||
### Individual Data Subject's Rights - Data Access, Portability and Deletion
|
||||
|
||||
We are committed to helping our customers meet the data subject rights requirements of GDPR. EchoScoop.com processes or stores all personal data in fully vetted, DPA compliant vendors. We do store all conversation and personal data for up to 6 years unless your account is deleted. In which case, we dispose of all data in accordance with our Terms of Service and Privacy Policy, but we will not hold it longer than 60 days.
|
||||
|
||||
We are aware that if you are working with EU customers, you need to be able to provide them with the ability to access, update, retrieve and remove personal data. We got you! We've been set up as self service from the start and have always given you access to your data and your customers data. Our customer support team is here for you to answer any questions you might have about working with the API.
|
||||
|
||||
## California Residents
|
||||
|
||||
The California Consumer Privacy Act (CCPA) requires us to disclose categories of Personal Information we collect and how we use it, the categories of sources from whom we collect Personal Information, and the third parties with whom we share it, which we have explained above.
|
||||
|
||||
We are also required to communicate information about rights California residents have under California law. You may exercise the following rights:
|
||||
|
||||
- Right to Know and Access. You may submit a verifiable request for information regarding the: (1) categories of Personal Information we collect, use, or share; (2) purposes for which categories of Personal Information are collected or used by us; (3) categories of sources from which we collect Personal Information; and (4) specific pieces of Personal Information we have collected about you.
|
||||
- Right to Equal Service. We will not discriminate against you if you exercise your privacy rights.
|
||||
- Right to Delete. You may submit a verifiable request to close your account and we will delete Personal Information about you that we have collected.
|
||||
- Request that a business that sells a consumer's personal data, not sell the consumer's personal data.
|
||||
|
||||
If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, please contact us.
|
||||
We do not sell the Personal Information of our users.
|
||||
For more information about these rights, please contact us.
|
||||
|
||||
## Contact Us
|
||||
|
||||
Don't hesitate to contact us if you have any questions.
|
||||
|
||||
-Via Email: support@echoscoop.com
|
||||
201
resources/markdown/terms.md
Normal file
201
resources/markdown/terms.md
Normal file
@@ -0,0 +1,201 @@
|
||||
Updated at 2023-09-24
|
||||
|
||||
## General Terms
|
||||
|
||||
By accessing and placing an order with EchoScoop.com, you confirm that you are in agreement with and bound by the terms of service contained in the Terms & Conditions outlined below. These terms apply to the entire website and any email or other type of communication between you and EchoScoop.com.
|
||||
|
||||
Under no circumstances shall EchoScoop.com team be liable for any direct, indirect, special, incidental or consequential damages, including, but not limited to, loss of data or profit, arising out of the use, or the inability to use, the materials on this site, even if EchoScoop.com team or an authorized representative has been advised of the possibility of such damages. If your use of materials from this site results in the need for servicing, repair or correction of equipment or data, you assume any costs thereof.
|
||||
|
||||
EchoScoop.com will not be responsible for any outcome that may occur during the course of usage of our resources. We reserve the rights to change prices and revise the resources usage policy in any moment.
|
||||
|
||||
## License
|
||||
|
||||
EchoScoop.com grants you a revocable, non-exclusive, non-transferable, limited license to download, install and use the website strictly in accordance with the terms of this Agreement.
|
||||
|
||||
These Terms & Conditions are a contract between you and EchoScoop.com (referred to in these Terms & Conditions as "EchoScoop.com", "us", "we" or "our"), the provider of the EchoScoop.com website and the services accessible from the EchoScoop.com website (which are collectively referred to in these Terms & Conditions as the "EchoScoop.com Service").
|
||||
|
||||
You are agreeing to be bound by these Terms & Conditions. If you do not agree to these Terms & Conditions, please do not use the EchoScoop.com Service. In these Terms & Conditions, "you" refers both to you as an individual and to the entity you represent. If you violate any of these Terms & Conditions, we reserve the right to cancel your account or block access to your account without notice.
|
||||
|
||||
## Meanings
|
||||
|
||||
For this Terms & Conditions:
|
||||
|
||||
- Cookie: small amount of data generated by a website and saved by your web browser. It is used to identify your browser, provide analytics, remember information about you such as your language preference or login information.
|
||||
- Company: when this policy mentions “Company,” “we,” “us,” or “our,” it refers to Exastellar Industries, that is responsible for your information under this Terms & Conditions.
|
||||
- Country: where EchoScoop.com or the owners/founders of EchoScoop.com are based, in this case is Malaysia
|
||||
- Device: any internet connected device such as a phone, tablet, computer or any other device that can be used to visit EchoScoop.com and use the services.
|
||||
- Service: refers to the service provided by EchoScoop.com as described in the relative terms (if available) and on this platform.
|
||||
- Third-party service: refers to advertisers, contest sponsors, promotional and marketing partners, and others who provide our content or whose products or services we think may interest you.
|
||||
- Website: EchoScoop.com."’s" site, which can be accessed via this URL: EchoScoop.com.com
|
||||
- You: a person or entity that is registered with EchoScoop.com to use the Services.
|
||||
|
||||
## Restrictions
|
||||
|
||||
You agree not to, and you will not permit others to:
|
||||
|
||||
- License, sell, rent, lease, assign, distribute, transmit, host, outsource, disclose or otherwise commercially exploit the website or make the platform available to any third party.
|
||||
- Modify, make derivative works of, disassemble, decrypt, reverse compile or reverse engineer any part of the website.
|
||||
- Remove, alter or obscure any proprietary notice (including any notice of copyright or trademark) of EchoScoop.com or its affiliates, partners, suppliers or the licensors of the website.
|
||||
|
||||
## Return and Refund Policy
|
||||
|
||||
Thanks for shopping at EchoScoop.com. We appreciate the fact that you like to buy the stuff we build. We also want to make sure you have a rewarding experience while you’re exploring, evaluating, and purchasing our products.
|
||||
|
||||
As with any shopping experience, there are terms and conditions that apply to transactions at EchoScoop.com. We’ll be as brief as our attorneys will allow. The main thing to remember is that by placing an order or making a purchase at EchoScoop.com, you agree to the terms along with EchoScoop.com."’s" Privacy Policy.
|
||||
|
||||
If, for any reason, You are not completely satisfied with any good or service that we provide, don't hesitate to contact us and we will discuss any of the issues you are going through with our product.
|
||||
|
||||
## Your Suggestions
|
||||
|
||||
Any feedback, comments, ideas, improvements or suggestions (collectively, "Suggestions") provided by you to EchoScoop.com with respect to the website shall remain the sole and exclusive property of EchoScoop.com.
|
||||
|
||||
EchoScoop.com shall be free to use, copy, modify, publish, or redistribute the Suggestions for any purpose and in any way without any credit or any compensation to you.
|
||||
|
||||
## Your Consent
|
||||
|
||||
We've updated our Terms & Conditions to provide you with complete transparency into what is being set when you visit our site and how it's being used. By using our website, registering an account, or making a purchase, you hereby consent to our Terms & Conditions.
|
||||
|
||||
## Links to Other Websites
|
||||
|
||||
This Terms & Conditions applies only to the Services. The Services may contain links to other websites not operated or controlled by EchoScoop.com. We are not responsible for the content, accuracy or opinions expressed in such websites, and such websites are not investigated, monitored or checked for accuracy or completeness by us. Please remember that when you use a link to go from the Services to another website, our Terms & Conditions are no longer in effect. Your browsing and interaction on any other website, including those that have a link on our platform, is subject to that website’s own rules and policies. Such third parties may use their own cookies or other methods to collect information about you.
|
||||
|
||||
## Cookies
|
||||
|
||||
EchoScoop.com uses "Cookies" to identify the areas of our website that you have visited. A Cookie is a small piece of data stored on your computer or mobile device by your web browser. We use Cookies to enhance the performance and functionality of our website but are non-essential to their use. However, without these cookies, certain functionality like videos may become unavailable or you would be required to enter your login details every time you visit the website as we would not be able to remember that you had logged in previously. Most web browsers can be set to disable the use of Cookies. However, if you disable Cookies, you may not be able to access functionality on our website correctly or at all. We never place Personally Identifiable Information in Cookies.
|
||||
|
||||
## Changes To Our Terms & Conditions
|
||||
|
||||
You acknowledge and agree that EchoScoop.com may stop (permanently or temporarily) providing the Service (or any features within the Service) to you or to users generally at EchoScoop.com’s sole discretion, without prior notice to you. You may stop using the Service at any time. You do not need to specifically inform EchoScoop.com when you stop using the Service. You acknowledge and agree that if EchoScoop.com disables access to your account, you may be prevented from accessing the Service, your account details or any files or other materials which is contained in your account.
|
||||
|
||||
If we decide to change our Terms & Conditions, we will post those changes on this page, and/or update the Terms & Conditions modification date below.
|
||||
|
||||
## Modifications to Our website
|
||||
|
||||
EchoScoop.com reserves the right to modify, suspend or discontinue, temporarily or permanently, the website or any service to which it connects, with or without notice and without liability to you.
|
||||
|
||||
## Updates to Our website
|
||||
|
||||
EchoScoop.com may from time to time provide enhancements or improvements to the features/ functionality of the website, which may include patches, bug fixes, updates, upgrades and other modifications ("Updates").
|
||||
|
||||
Updates may modify or delete certain features and/or functionalities of the website. You agree that EchoScoop.com has no obligation to (i) provide any Updates, or (ii) continue to provide or enable any particular features and/or functionalities of the website to you.
|
||||
|
||||
You further agree that all Updates will be (i) deemed to constitute an integral part of the website, and (ii) subject to the terms and conditions of this Agreement.
|
||||
|
||||
## Third-Party Services
|
||||
|
||||
We may display, include or make available third-party content (including data, information, applications and other products services) or provide links to third-party websites or services ("Third- Party Services").
|
||||
|
||||
You acknowledge and agree that EchoScoop.com shall not be responsible for any Third-Party Services, including their accuracy, completeness, timeliness, validity, copyright compliance, legality, decency, quality or any other aspect thereof. EchoScoop.com does not assume and shall not have any liability or responsibility to you or any other person or entity for any Third-Party Services.
|
||||
|
||||
Third-Party Services and links thereto are provided solely as a convenience to you and you access and use them entirely at your own risk and subject to such third parties' terms and conditions.
|
||||
|
||||
## Term and Termination
|
||||
|
||||
This Agreement shall remain in effect until terminated by you or EchoScoop.com.
|
||||
|
||||
EchoScoop.com may, in its sole discretion, at any time and for any or no reason, suspend or terminate this Agreement with or without prior notice.
|
||||
|
||||
This Agreement will terminate immediately, without prior notice from EchoScoop.com, in the event that you fail to comply with any provision of this Agreement. You may also terminate this Agreement by deleting the website and all copies thereof from your computer.
|
||||
|
||||
Upon termination of this Agreement, you shall cease all use of the website and delete all copies of the website from your computer.
|
||||
Termination of this Agreement will not limit any of EchoScoop.com's rights or remedies at law or in equity in case of breach by you (during the term of this Agreement) of any of your obligations under the present Agreement.
|
||||
|
||||
## Copyright Infringement Notice
|
||||
|
||||
If you are a copyright owner or such owner’s agent and believe any material on our website constitutes an infringement on your copyright, please contact us setting forth the following information: (a) a physical or electronic signature of the copyright owner or a person authorized to act on his behalf; (b) identification of the material that is claimed to be infringing; (c) your contact information, including your address, telephone number, and an email; (d) a statement by you that you have a good faith belief that use of the material is not authorized by the copyright owners; and (e) the a statement that the information in the notification is accurate, and, under penalty of perjury you are authorized to act on behalf of the owner.
|
||||
|
||||
## Indemnification
|
||||
|
||||
You agree to indemnify and hold EchoScoop.com and its parents, subsidiaries, affiliates, officers, employees, agents, partners and licensors (if any) harmless from any claim or demand, including reasonable attorneys' fees, due to or arising out of your: (a) use of the website; (b) violation of this Agreement or any law or regulation; or (c) violation of any right of a third party.
|
||||
|
||||
## No Warranties
|
||||
|
||||
The website is provided to you "AS IS" and "AS AVAILABLE" and with all faults and defects without warranty of any kind. To the maximum extent permitted under applicable law, EchoScoop.com, on its own behalf and on behalf of its affiliates and its and their respective licensors and service providers, expressly disclaims all warranties, whether express, implied, statutory or otherwise, with respect to the website, including all implied warranties of merchantability, fitness for a particular purpose, title and non-infringement, and warranties that may arise out of course of dealing, course of performance, usage or trade practice. Without limitation to the foregoing, EchoScoop.com provides no warranty or undertaking, and makes no representation of any kind that the website will meet your requirements, achieve any intended results, be compatible or work with any other software, , systems or services, operate without interruption, meet any performance or reliability standards or be error free or that any errors or defects can or will be corrected.
|
||||
|
||||
Without limiting the foregoing, neither EchoScoop.com nor any EchoScoop.com's provider makes any representation or warranty of any kind, express or implied: (i) as to the operation or availability of the website, or the information, content, and materials or products included thereon; (ii) that the website will be uninterrupted or error-free; (iii) as to the accuracy, reliability, or currency of any information or content provided through the website; or (iv) that the website, its servers, the content, or e-mails sent from or on behalf of EchoScoop.com are free of viruses, scripts, trojan horses, worms, malware, timebombs or other harmful components.
|
||||
|
||||
Some jurisdictions do not allow the exclusion of or limitations on implied warranties or the limitations on the applicable statutory rights of a consumer, so some or all of the above exclusions and limitations may not apply to you.
|
||||
|
||||
## Limitation of Liability
|
||||
|
||||
Notwithstanding any damages that you might incur, the entire liability of EchoScoop.com and any of its suppliers under any provision of this Agreement and your exclusive remedy for all of the foregoing shall be limited to the amount actually paid by you for the website.
|
||||
|
||||
To the maximum extent permitted by applicable law, in no event shall EchoScoop.com or its suppliers be liable for any special, incidental, indirect, or consequential damages whatsoever (including, but not limited to, damages for loss of profits, for loss of data or other information, for business interruption, for personal injury, for loss of privacy arising out of or in any way related to the use of or inability to use the website, third-party software and/or third-party hardware used with the website, or otherwise in connection with any provision of this Agreement), even if EchoScoop.com or any supplier has been advised of the possibility of such damages and even if the remedy fails of its essential purpose.
|
||||
|
||||
Some states/jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so the above limitation or exclusion may not apply to you.
|
||||
|
||||
## Severability
|
||||
|
||||
If any provision of this Agreement is held to be unenforceable or invalid, such provision will be changed and interpreted to accomplish the objectives of such provision to the greatest extent possible under applicable law and the remaining provisions will continue in full force and effect.
|
||||
|
||||
This Agreement, together with the Privacy Policy and any other legal notices published by EchoScoop.com on the Services, shall constitute the entire agreement between you and EchoScoop.com concerning the Services. If any provision of this Agreement is deemed invalid by a court of competent jurisdiction, the invalidity of such provision shall not affect the validity of the remaining provisions of this Agreement, which shall remain in full force and effect. No waiver of any term of this Agreement shall be deemed a further or continuing waiver of such term or any other term, and EchoScoop.com."’s" failure to assert any right or provision under this Agreement shall not constitute a waiver of such right or provision. YOU AND EchoScoop.com AGREE THAT ANY CAUSE OF ACTION ARISING OUT OF OR RELATED TO THE SERVICES MUST COMMENCE WITHIN ONE (1) YEAR AFTER THE CAUSE OF ACTION ACCRUES. OTHERWISE, SUCH CAUSE OF ACTION IS PERMANENTLY BARRED.
|
||||
|
||||
## Waiver
|
||||
|
||||
Except as provided herein, the failure to exercise a right or to require performance of an obligation under this Agreement shall not effect a party's ability to exercise such right or require such performance at any time thereafter nor shall be the waiver of a breach constitute waiver of any subsequent breach.
|
||||
|
||||
No failure to exercise, and no delay in exercising, on the part of either party, any right or any power under this Agreement shall operate as a waiver of that right or power. Nor shall any single or partial exercise of any right or power under this Agreement preclude further exercise of that or any other right granted herein. In the event of a conflict between this Agreement and any applicable purchase or other terms, the terms of this Agreement shall govern.
|
||||
|
||||
## Amendments to this Agreement
|
||||
|
||||
EchoScoop.com reserves the right, at its sole discretion, to modify or replace this Agreement at any time. If a revision is material we will provide at least 30 days' notice prior to any new terms taking effect. What constitutes a material change will be determined at our sole discretion.
|
||||
By continuing to access or use our website after any revisions become effective, you agree to be bound by the revised terms. If you do not agree to the new terms, you are no longer authorized to use EchoScoop.com.
|
||||
|
||||
## Entire Agreement
|
||||
|
||||
The Agreement constitutes the entire agreement between you and EchoScoop.com regarding your use of the website and supersedes all prior and contemporaneous written or oral agreements between you and EchoScoop.com.
|
||||
You may be subject to additional terms and conditions that apply when you use or purchase other EchoScoop.com's services, which EchoScoop.com will provide to you at the time of such use or purchase.
|
||||
|
||||
## Updates to Our Terms
|
||||
|
||||
We may change our Service and policies, and we may need to make changes to these Terms so that they accurately reflect our Service and policies. Unless otherwise required by law, we will notify you (for example, through our Service) before we make changes to these Terms and give you an opportunity to review them before they go into effect. Then, if you continue to use the Service, you will be bound by the updated Terms. If you do not want to agree to these or any updated Terms, you can delete your account.
|
||||
|
||||
## Intellectual Property
|
||||
|
||||
The website and its entire contents, features and functionality (including but not limited to all information, software, text, displays, images, video and audio, and the design, selection and arrangement thereof), are owned by EchoScoop.com, its licensors or other providers of such material and are protected by Malaysia and international copyright, trademark, patent, trade secret and other intellectual property or proprietary rights laws. The material may not be copied, modified, reproduced, downloaded or distributed in any way, in whole or in part, without the express prior written permission of EchoScoop.com, unless and except as is expressly provided in these Terms & Conditions. Any unauthorized use of the material is prohibited.
|
||||
|
||||
## Agreement to Arbitrate
|
||||
|
||||
This section applies to any dispute EXCEPT IT DOESN’T INCLUDE A DISPUTE RELATING TO CLAIMS FOR INJUNCTIVE OR EQUITABLE RELIEF REGARDING THE ENFORCEMENT OR VALIDITY OF YOUR OR EchoScoop.com."’s" INTELLECTUAL PROPERTY RIGHTS. The term “dispute” means any dispute, action, or other controversy between you and EchoScoop.com concerning the Services or this agreement, whether in contract, warranty, tort, statute, regulation, ordinance, or any other legal or equitable basis. “Dispute” will be given the broadest possible meaning allowable under law.
|
||||
|
||||
## Notice of Dispute
|
||||
|
||||
In the event of a dispute, you or EchoScoop.com must give the other a Notice of Dispute, which is a written statement that sets forth the name, address, and contact information of the party giving it, the facts giving rise to the dispute, and the relief requested. You must send any Notice of Dispute via email to: support@EchoScoop.com. EchoScoop.com will send any Notice of Dispute to you by mail to your address if we have it, or otherwise to your email address. You and EchoScoop.com will attempt to resolve any dispute through informal negotiation within sixty (60) days from the date the Notice of Dispute is sent. After sixty (60) days, you or EchoScoop.com may commence arbitration.
|
||||
|
||||
## Binding Arbitration
|
||||
|
||||
If you and EchoScoop.com don’t resolve any dispute by informal negotiation, any other effort to resolve the dispute will be conducted exclusively by binding arbitration as described in this section. You are giving up the right to litigate (or participate in as a party or class member) all disputes in court before a judge or jury. The dispute shall be settled by binding arbitration in accordance with the commercial arbitration rules of the American Arbitration Association. Either party may seek any interim or preliminary injunctive relief from any court of competent jurisdiction, as necessary to protect the party’s rights or property pending the completion of arbitration. Any and all legal, accounting, and other costs, fees, and expenses incurred by the prevailing party shall be borne by the non-prevailing party.
|
||||
|
||||
## Submissions and Privacy
|
||||
|
||||
In the event that you submit or post any ideas, creative suggestions, designs, photographs, information, advertisements, data or proposals, including ideas for new or improved products, services, features, technologies or promotions, you expressly agree that such submissions will automatically be treated as non-confidential and non-proprietary and will become the sole property of EchoScoop.com without any compensation or credit to you whatsoever. EchoScoop.com and its affiliates shall have no obligations with respect to such submissions or posts and may use the ideas contained in such submissions or posts for any purposes in any medium in perpetuity, including, but not limited to, developing, manufacturing, and marketing products and services using such ideas.
|
||||
|
||||
## Promotions
|
||||
|
||||
EchoScoop.com may, from time to time, include contests, promotions, sweepstakes, or other activities (“Promotions”) that require you to submit material or information concerning yourself. Please note that all Promotions may be governed by separate rules that may contain certain eligibility requirements, such as restrictions as to age and geographic location. You are responsible to read all Promotions rules to determine whether or not you are eligible to participate. If you enter any Promotion, you agree to abide by and to comply with all Promotions Rules.
|
||||
|
||||
Additional terms and conditions may apply to purchases of goods or services on or through the Services, which terms and conditions are made a part of this Agreement by this reference.
|
||||
|
||||
## Typographical Errors
|
||||
|
||||
In the event a product and/or service is listed at an incorrect price or with incorrect information due to typographical error, we shall have the right to refuse or cancel any orders placed for the product and/or service listed at the incorrect price. We shall have the right to refuse or cancel any such order whether or not the order has been confirmed and your credit card charged. If your credit card has already been charged for the purchase and your order is canceled, we shall immediately issue a credit to your credit card account or other payment account in the amount of the charge.
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
If for any reason a court of competent jurisdiction finds any provision or portion of these Terms & Conditions to be unenforceable, the remainder of these Terms & Conditions will continue in full force and effect. Any waiver of any provision of these Terms & Conditions will be effective only if in writing and signed by an authorized representative of EchoScoop.com. EchoScoop.com will be entitled to injunctive or other equitable relief (without the obligations of posting any bond or surety) in the event of any breach or anticipatory breach by you. EchoScoop.com operates and controls the EchoScoop.com Service from its offices in Malaysia. The Service is not intended for distribution to or use by any person or entity in any jurisdiction or country where such distribution or use would be contrary to law or regulation. Accordingly, those persons who choose to access the EchoScoop.com Service from other locations do so on their own initiative and are solely responsible for compliance with local laws, if and to the extent local laws are applicable. These Terms & Conditions (which include and incorporate the EchoScoop.com Privacy Policy) contains the entire understanding, and supersedes all prior understandings, between you and EchoScoop.com concerning its subject matter, and cannot be changed or modified by you. The section headings used in this Agreement are for convenience only and will not be given any legal import.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
EchoScoop.com is not responsible for any content, code or any other imprecision.
|
||||
|
||||
EchoScoop.com does not provide warranties or guarantees.
|
||||
|
||||
In no event shall EchoScoop.com be liable for any special, direct, indirect, consequential, or incidental damages or any damages whatsoever, whether in an action of contract, negligence or other tort, arising out of or in connection with the use of the Service or the contents of the Service. EchoScoop.com reserves the right to make additions, deletions, or modifications to the contents on the Service at any time without prior notice.
|
||||
|
||||
The EchoScoop.com Service and its contents are provided "as is" and "as available" without any warranty or representations of any kind, whether express or implied. EchoScoop.com is a distributor and not a publisher of the content supplied by third parties; as such, EchoScoop.com exercises no editorial control over such content and makes no warranty or representation as to the accuracy, reliability or currency of any information, content, service or merchandise provided through or accessible via the EchoScoop.com Service. Without limiting the foregoing, EchoScoop.com specifically disclaims all warranties and representations in any content transmitted on or in connection with the EchoScoop.com Service or on sites that may appear as links on the EchoScoop.com Service, or in the products provided as a part of, or otherwise in connection with, the EchoScoop.com Service, including without limitation any warranties of merchantability, fitness for a particular purpose or non-infringement of third party rights. No oral advice or written information given by EchoScoop.com or any of its affiliates, employees, officers, directors, agents, or the like will create a warranty. Price and availability information is subject to change without notice. Without limiting the foregoing, EchoScoop.com does not warrant that the EchoScoop.com Service will be uninterrupted, uncorrupted, timely, or error-free.
|
||||
|
||||
## Contact Us
|
||||
|
||||
Don't hesitate to contact us if you have any questions.
|
||||
|
||||
- Via Email: support@echoscoop.com
|
||||
@@ -5,3 +5,11 @@
|
||||
@import "~/bootstrap-icons/font/bootstrap-icons.css";
|
||||
|
||||
@import "../css/app-front.css";
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
@include('front.layouts.partials.head')
|
||||
|
||||
<body>
|
||||
@include('googletagmanager::body')
|
||||
<div id="app">
|
||||
@include('front.layouts.partials.nav')
|
||||
<main>
|
||||
@yield('content')
|
||||
</main>
|
||||
@include('front.layouts.partials.footer')
|
||||
</div>
|
||||
@include('googletagmanager::body')
|
||||
<div id="app">
|
||||
@include('front.layouts.partials.nav')
|
||||
<main>
|
||||
@yield('content')
|
||||
</main>
|
||||
@include('front.layouts.partials.footer')
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
<div class="container">
|
||||
<footer></footer>
|
||||
</div>
|
||||
<footer class="py-3 my-4">
|
||||
<ul class="nav justify-content-center pb-2 mb-2">
|
||||
<li class="nav-item"><a href="{{ route('front.home') }}" class="nav-link px-2 text-body-secondary">Home</a>
|
||||
</li>
|
||||
<li class="nav-item"><a href="{{ route('front.terms') }}" class="nav-link px-2 text-body-secondary">Terms</a>
|
||||
</li>
|
||||
<li class="nav-item"><a href="{{ route('front.privacy') }}"
|
||||
class="nav-link px-2 text-body-secondary">Privacy</a></li>
|
||||
<li class="nav-item"><a href="{{ route('front.disclaimer') }}"
|
||||
class="nav-link px-2 text-body-secondary">Disclaimer</a></li>
|
||||
<li class="nav-item"><a href="sitemap.xml" class="nav-link px-2 text-body-secondary">Sitemap</a></li>
|
||||
<li class="nav-item"><a href="/feeds/posts-feed" class="nav-link px-2 text-body-secondary">RSS</a></li>
|
||||
|
||||
</ul>
|
||||
<p class="text-center text-body-secondary">{{ date('Y') }} EchoScoop. All rights reserved.</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
|
||||
<link rel="dns-prefetch" href="//fonts.bunny.net">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
@include('feed::links')
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
|
||||
@vite(['resources/sass/app-front.scss', 'resources/js/app-front.js'])
|
||||
{{-- @laravelPWA --}}
|
||||
@include('googletagmanager::head')
|
||||
@include('googletagmanager::head')
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
<div class="container">
|
||||
<header class="d-flex flex-wrap align-items-center justify-content-center justify-content-md-between py-3 mb-4 border-bottom">
|
||||
<a href="/" class="d-flex align-items-center col-md-3 mb-2 mb-md-0 text-dark text-decoration-none">
|
||||
<span class="fs-4 fw-bolder align-self-center">EchoScoop</span>
|
||||
</a>
|
||||
<header class="border-bottom lh-1 py-3">
|
||||
<div class="row flex-nowrap justify-content-center align-items-center">
|
||||
|
||||
<span>Breaking down news to bite-sized scoops.</span>
|
||||
|
||||
{{-- <ul class="nav col-12 col-md-auto mb-2 justify-content-center mb-md-0">
|
||||
<li><a href="#" class="nav-link px-2 link-secondary">Home</a></li>
|
||||
<li><a href="#" class="nav-link px-2 link-dark">Features</a></li>
|
||||
<li><a href="#" class="nav-link px-2 link-dark">Pricing</a></li>
|
||||
<li><a href="#" class="nav-link px-2 link-dark">FAQs</a></li>
|
||||
<li><a href="#" class="nav-link px-2 link-dark">About</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="col-md-3 text-end">
|
||||
<button type="button" class="btn btn-outline-primary me-2">Login</button>
|
||||
<button type="button" class="btn btn-primary">Sign-up</button>
|
||||
</div> --}}
|
||||
<div class="col-4 text-center">
|
||||
<a class="blog-header-logo text-body-emphasis text-decoration-none" href="{{ route('front.home') }}">
|
||||
<span class="fw-bolder fs-3">EchoScoop</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="nav-scroller py-1 mb-3 border-bottom">
|
||||
<nav class="nav nav-underline justify-content-between">
|
||||
@foreach ($parent_categories as $category)
|
||||
<a class="fw-bold nav-item nav-link link-body-emphasis {{ active(route('front.category', ['category_slug' => $category->slug])) }}"
|
||||
href="{{ route('front.category', ['category_slug' => $category->slug]) }}">{{ $category->short_name }}</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
13
resources/views/front/pages.blade.php
Normal file
13
resources/views/front/pages.blade.php
Normal file
@@ -0,0 +1,13 @@
|
||||
@extends('front.layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="container mt-4 mb-0">
|
||||
<h1 class="pb-2 fw-normal h2">{{ $title }}</h1>
|
||||
<p class="mb-8 text-center p-3 bg-gradient bg-light text-dark">
|
||||
{{ $description }}
|
||||
</p>
|
||||
<div>
|
||||
{!! $content !!}
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
7
resources/views/front/partials/about.blade.php
Normal file
7
resources/views/front/partials/about.blade.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="p-4 mb-3 bg-body-tertiary rounded">
|
||||
<h4 class="fst-italic">About EchoScoop</h4>
|
||||
<p class="mb-0">
|
||||
EchoScoop is a streamlined news platform delivering concise global updates. Our goal is to keep you promptly
|
||||
informed with each scoop.
|
||||
</p>
|
||||
</div>
|
||||
17
resources/views/front/partials/post_detail.blade.php
Normal file
17
resources/views/front/partials/post_detail.blade.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<div class="row g-0 border rounded overflow-hidden flex-md-row mb-4 shadow-sm h-md-250 position-relative">
|
||||
<div class="col p-4 d-flex flex-column position-static">
|
||||
<strong class="d-inline-block mb-2 text-success-emphasis">{{ $post->category->name }}</strong>
|
||||
<h3 class="mb-0 h4">{{ $post->title }}</h3>
|
||||
@if (!is_empty($post->published_at))
|
||||
<div class="mb-1 text-body-secondary">{{ $post->published_at->format('M j') }}</div>
|
||||
@endif
|
||||
<p class="mb-3">{{ $post->excerpt }}</p>
|
||||
<a href="{{ route('front.post', ['slug' => $post->slug]) }}"
|
||||
class="icon-link gap-1 icon-link-hover stretched-link">
|
||||
Continue reading
|
||||
<svg class="bi">
|
||||
<use xlink:href="#chevron-right"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
155
resources/views/front/partials/welcome_blog_post.blade.php
Normal file
155
resources/views/front/partials/welcome_blog_post.blade.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<h3 class="pb-4 mb-4 fst-italic border-bottom">
|
||||
From the Firehose
|
||||
</h3>
|
||||
|
||||
<article class="blog-post">
|
||||
<h2 class="display-5 link-body-emphasis mb-1">Sample blog post</h2>
|
||||
<p class="blog-post-meta">January 1, 2021 by <a href="#">Mark</a></p>
|
||||
|
||||
<p>This blog post shows a few different types of content that’s supported and styled with Bootstrap.
|
||||
Basic typography, lists, tables, images, code, and more are all supported as expected.</p>
|
||||
<hr>
|
||||
<p>This is some additional paragraph placeholder content. It has been written to fill the available
|
||||
space and show how a longer snippet of text affects the surrounding content. We'll repeat it often
|
||||
to keep the demonstration flowing, so be on the lookout for this exact same string of text.</p>
|
||||
<h2>Blockquotes</h2>
|
||||
<p>This is an example blockquote in action:</p>
|
||||
<blockquote class="blockquote">
|
||||
<p>Quoted text goes here.</p>
|
||||
</blockquote>
|
||||
<p>This is some additional paragraph placeholder content. It has been written to fill the available
|
||||
space and show how a longer snippet of text affects the surrounding content. We'll repeat it often
|
||||
to keep the demonstration flowing, so be on the lookout for this exact same string of text.</p>
|
||||
<h3>Example lists</h3>
|
||||
<p>This is some additional paragraph placeholder content. It's a slightly shorter version of the other
|
||||
highly repetitive body text used throughout. This is an example unordered list:</p>
|
||||
<ul>
|
||||
<li>First list item</li>
|
||||
<li>Second list item with a longer description</li>
|
||||
<li>Third list item to close it out</li>
|
||||
</ul>
|
||||
<p>And this is an ordered list:</p>
|
||||
<ol>
|
||||
<li>First list item</li>
|
||||
<li>Second list item with a longer description</li>
|
||||
<li>Third list item to close it out</li>
|
||||
</ol>
|
||||
<p>And this is a definition list:</p>
|
||||
<dl>
|
||||
<dt>HyperText Markup Language (HTML)</dt>
|
||||
<dd>The language used to describe and define the content of a Web page</dd>
|
||||
<dt>Cascading Style Sheets (CSS)</dt>
|
||||
<dd>Used to describe the appearance of Web content</dd>
|
||||
<dt>JavaScript (JS)</dt>
|
||||
<dd>The programming language used to build advanced Web sites and applications</dd>
|
||||
</dl>
|
||||
<h2>Inline HTML elements</h2>
|
||||
<p>HTML defines a long list of available inline tags, a complete list of which can be found on the <a
|
||||
href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element">Mozilla Developer Network</a>.
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>To bold text</strong>, use <code class="language-plaintext highlighter-rouge"><strong></code>.
|
||||
</li>
|
||||
<li><em>To italicize text</em>, use <code class="language-plaintext highlighter-rouge"><em></code>.</li>
|
||||
<li>Abbreviations, like <abbr title="HyperText Markup Language">HTML</abbr> should use <code
|
||||
class="language-plaintext highlighter-rouge"><abbr></code>, with an optional <code
|
||||
class="language-plaintext highlighter-rouge">title</code> attribute for the full phrase.
|
||||
</li>
|
||||
<li>Citations, like <cite>— Mark Otto</cite>, should use <code
|
||||
class="language-plaintext highlighter-rouge"><cite></code>.</li>
|
||||
<li><del>Deleted</del> text should use <code class="language-plaintext highlighter-rouge"><del></code> and
|
||||
<ins>inserted</ins> text
|
||||
should use <code class="language-plaintext highlighter-rouge"><ins></code>.
|
||||
</li>
|
||||
<li>Superscript <sup>text</sup> uses <code class="language-plaintext highlighter-rouge"><sup></code> and
|
||||
subscript
|
||||
<sub>text</sub> uses <code class="language-plaintext highlighter-rouge"><sub></code>.
|
||||
</li>
|
||||
</ul>
|
||||
<p>Most of these elements are styled by browsers with few modifications on our part.</p>
|
||||
<h2>Heading</h2>
|
||||
<p>This is some additional paragraph placeholder content. It has been written to fill the available
|
||||
space and show how a longer snippet of text affects the surrounding content. We'll repeat it often
|
||||
to keep the demonstration flowing, so be on the lookout for this exact same string of text.</p>
|
||||
<h3>Sub-heading</h3>
|
||||
<p>This is some additional paragraph placeholder content. It has been written to fill the available
|
||||
space and show how a longer snippet of text affects the surrounding content. We'll repeat it often
|
||||
to keep the demonstration flowing, so be on the lookout for this exact same string of text.</p>
|
||||
<pre><code>Example code block</code></pre>
|
||||
<p>This is some additional paragraph placeholder content. It's a slightly shorter version of the other
|
||||
highly repetitive body text used throughout.</p>
|
||||
</article>
|
||||
|
||||
<article class="blog-post">
|
||||
<h2 class="display-5 link-body-emphasis mb-1">Another blog post</h2>
|
||||
<p class="blog-post-meta">December 23, 2020 by <a href="#">Jacob</a></p>
|
||||
|
||||
<p>This is some additional paragraph placeholder content. It has been written to fill the available
|
||||
space and show how a longer snippet of text affects the surrounding content. We'll repeat it often
|
||||
to keep the demonstration flowing, so be on the lookout for this exact same string of text.</p>
|
||||
<blockquote>
|
||||
<p>Longer quote goes here, maybe with some <strong>emphasized text</strong> in the middle of it.</p>
|
||||
</blockquote>
|
||||
<p>This is some additional paragraph placeholder content. It has been written to fill the available
|
||||
space and show how a longer snippet of text affects the surrounding content. We'll repeat it often
|
||||
to keep the demonstration flowing, so be on the lookout for this exact same string of text.</p>
|
||||
<h3>Example table</h3>
|
||||
<p>And don't forget about tables in these posts:</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Upvotes</th>
|
||||
<th>Downvotes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Alice</td>
|
||||
<td>10</td>
|
||||
<td>11</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bob</td>
|
||||
<td>4</td>
|
||||
<td>3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Charlie</td>
|
||||
<td>7</td>
|
||||
<td>9</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>Totals</td>
|
||||
<td>21</td>
|
||||
<td>23</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<p>This is some additional paragraph placeholder content. It's a slightly shorter version of the other
|
||||
highly repetitive body text used throughout.</p>
|
||||
</article>
|
||||
|
||||
<article class="blog-post">
|
||||
<h2 class="display-5 link-body-emphasis mb-1">New feature</h2>
|
||||
<p class="blog-post-meta">December 14, 2020 by <a href="#">Chris</a></p>
|
||||
|
||||
<p>This is some additional paragraph placeholder content. It has been written to fill the available
|
||||
space and show how a longer snippet of text affects the surrounding content. We'll repeat it often
|
||||
to keep the demonstration flowing, so be on the lookout for this exact same string of text.</p>
|
||||
<ul>
|
||||
<li>First list item</li>
|
||||
<li>Second list item with a longer description</li>
|
||||
<li>Third list item to close it out</li>
|
||||
</ul>
|
||||
<p>This is some additional paragraph placeholder content. It's a slightly shorter version of the other
|
||||
highly repetitive body text used throughout.</p>
|
||||
</article>
|
||||
|
||||
<nav class="blog-pagination" aria-label="Pagination">
|
||||
<a class="btn btn-outline-primary rounded-pill" href="#">Older</a>
|
||||
<a class="btn btn-outline-secondary rounded-pill disabled" aria-disabled="true">Newer</a>
|
||||
</nav>
|
||||
49
resources/views/front/post_list.blade.php
Normal file
49
resources/views/front/post_list.blade.php
Normal file
@@ -0,0 +1,49 @@
|
||||
@extends('front.layouts.app')
|
||||
@section('content')
|
||||
<main class="container">
|
||||
<nav style="--bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='%236c757d'/%3E%3C/svg%3E");"
|
||||
aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('front.home') }}">Home</a></li>
|
||||
|
||||
@if (isset($category) && !is_null($category))
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ $category->name }}</li>
|
||||
@else
|
||||
<li class="breadcrumb-item active" aria-current="page">Latest News</li>
|
||||
@endif
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="row g-5">
|
||||
<div class="col-md-8">
|
||||
<h1 class="pb-2 fw-normal h2">
|
||||
@if (isset($category) && !is_null($category))
|
||||
{{ $category->name }} News from EchoScoop
|
||||
@else
|
||||
Latest News from EchoScoop
|
||||
@endif
|
||||
</h1>
|
||||
@if ($posts->count() > 0)
|
||||
@foreach ($posts as $post)
|
||||
@include('front.partials.post_detail', ['post' => $post])
|
||||
@endforeach
|
||||
@if ($posts instanceof \Illuminate\Pagination\Paginator)
|
||||
<div class="flex justify-center">
|
||||
{{ $posts->links('pagination::simple-bootstrap-5-rounded') }}
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="py-3 text-center">
|
||||
<div class="mb-2">No posts found yet.</div>
|
||||
<div><a href="{{ route('front.home') }}">Back to Home</a></div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="position-sticky" style="top: 2rem;">
|
||||
@include('front.partials.about')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@endsection
|
||||
33
resources/views/front/single_post.blade.php
Normal file
33
resources/views/front/single_post.blade.php
Normal file
@@ -0,0 +1,33 @@
|
||||
@extends('front.layouts.app')
|
||||
@section('content')
|
||||
<main class="container">
|
||||
<nav style="--bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='%236c757d'/%3E%3C/svg%3E");"
|
||||
aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('front.home') }}">Home</a></li>
|
||||
|
||||
<li class="breadcrumb-item"><a
|
||||
href="{{ route('front.category', ['category_slug' => $post->category->slug]) }}">{{ $post->category->name }}</a>
|
||||
</li>
|
||||
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ $post->title }}</li>
|
||||
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="row g-5">
|
||||
<div class="col-md-8">
|
||||
|
||||
<article class="blog-post">
|
||||
{!! $content !!}
|
||||
</article>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="position-sticky" style="top: 2rem;">
|
||||
@include('front.partials.about')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@endsection
|
||||
35
resources/views/front/welcome.blade.php
Normal file
35
resources/views/front/welcome.blade.php
Normal file
@@ -0,0 +1,35 @@
|
||||
@extends('front.layouts.app')
|
||||
@section('content')
|
||||
<main class="container">
|
||||
<div class="p-4 p-md-5 mb-4 rounded text-body-emphasis bg-body-secondary">
|
||||
<div class="col-lg-12 px-0">
|
||||
<h1 class="display-4 fst-italic">{{ $featured_post->title }}</h1>
|
||||
<p class="lead my-3">{{ $featured_post->excerpt }}</p>
|
||||
<p class="lead mb-0"><a href="{{ route('front.post', ['slug' => $featured_post->slug]) }}"
|
||||
class=" fw-bold">Continue reading...</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row g-5">
|
||||
<div class="col-md-8">
|
||||
@foreach ($latest_posts as $post)
|
||||
@include('front.partials.post_detail', ['post' => $post])
|
||||
@endforeach
|
||||
|
||||
<div class="w-100 d-flex justify-content-center">
|
||||
<a class="btn btn-outline-primary rounded-pill px-3 text-decoration-none"
|
||||
href="{{ route('front.all') }}">View Latest News</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="position-sticky" style="top: 2rem;">
|
||||
@include('front.partials.about')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
@endsection
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user