diff --git a/composer.json b/composer.json index 51fd74d..1f1ebc9 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,16 @@ { + "autoload": { + "psr-4": { + "PWA_Generator\\": "inc" + } + }, "require-dev": { - "phpstan/phpstan": "^1.10" + "phpstan/phpstan": "^1.10", + "squizlabs/php_codesniffer": "^3.7" }, "scripts": { + "phpcbf": "vendor/bin/phpcbf ./", + "phpcs": "vendor/bin/phpcs ./", "phpstan": "vendor/bin/phpstan analyze" } } diff --git a/composer.lock b/composer.lock index 2b39fff..6256f6a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "06a282a9caaa357cc75a987b3fac0f2f", + "content-hash": "9fb1ef516ecc03169734c2ee2103818d", "packages": [], "packages-dev": [ { @@ -68,6 +68,63 @@ } ], "time": "2023-08-22T13:48:25+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.7.2", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879", + "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2023-02-22T23:07:41+00:00" } ], "aliases": [], @@ -77,5 +134,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/inc/Build.php b/inc/Build.php index 797add1..741f869 100644 --- a/inc/Build.php +++ b/inc/Build.php @@ -1,1155 +1,1165 @@ - */ - private array $cache_files = [ - 'manifest.json', - ]; - - /** - * Files to compile into build directory. - * - * @var array> - */ - private array $compile_files = []; - - /** - * Files to copy into build directory. - * - * @var array - */ - private array $copy_files = [ - '_redirects', - 'manifest.json', - 'robots.txt', - 'sitemap.xml', - 'sw.js', - 'tsconfig.json', - 'webpack.config.js', - ]; - - /** - * JavaScript bundle filename (including hash). - * - * @var string - */ - private string $js_bundle_filename = ''; - - /** - * Constructor. - * - * Sets up project data and performs actions based on build options provided. - * - * @param string $project Project name. - * @param array $options Build options. - */ - public function __construct( - string $project, - array $options = [ - 'generate_favicons' => false, - 'build' => false, - 'deploy' => false, - ] - ) { - if (empty($project)) { - CLI::display_error("Failed to construct build: project not provided."); - exit; - } - - // Set up project data. - $project = new Project($project); - - // Generate favicons. - if (!empty($options['generate_favicons'])) { - $this->generate_favicons(); - } - - // Build project. - if (!empty($options['build'])) { - $this->build_project(); - } - - // Deploy project. - if (!empty($options['deploy'])) { - $this->deploy_project(); - } - } - - /** - * Builds a project according to build data. - * - * @return void - */ - private function build_project(): void { - - if (empty(Project::$directory_path)) { - CLI::display_error("Failed to build project: project directory path is empty."); - exit; - } - - if (empty(Project::$url)) { - CLI::display_error("Failed to build project: URL is empty."); - exit; - } - - // Create and populate new website directory if it doesn't already exist. - if (!is_dir(Project::$directory_path)) { - $this->create_build_directory(); - $this->populate_build_directory(); - } - - $this->set_build_version(); - - if (empty(Project::$version)) { - CLI::display_error("Failed to build project: project version is empty."); - exit; - } - - echo "\nBuilding project " . Project::$url . ' v' . Project::$version; - - $this->set_file_arrays(); - $this->process_font_files(); - $this->copy_files(); - $this->compile_files(); - $this->generate_manifest(); - $this->template_replace_files(); - - echo CLI::$verbose - ? "\nBuilding project " . Project::$url . " DONE\n" - : " - DONE\n"; - } - - /** - * Deploys a project to Netlify according to build data. - * - * @return void - */ - private function deploy_project(): void { - - if (empty(Project::$directory_path)) { - CLI::display_error("Failed to deploy project: project directory path is empty."); - exit; - } - - if (!is_dir(Project::$directory_path)) { - CLI::display_error("Failed to deploy project: project directory doesn't exist."); - exit; - } - - if (empty(Project::$url)) { - CLI::display_error("Failed to deploy project: URL is empty."); - exit; - } - - if (empty(Project::$netlify_id)) { - CLI::display_error("Failed to deploy project: Netlify ID is empty."); - exit; - } - - // Increment build version. - $this->set_build_version(true); - - echo 'Deploying project ' . Project::$netlify_id . ' v' . Project::$version . ' to ' . Project::$url; - - // Execute Netlify CLI deployment. - $output = []; - exec('npx netlify deploy --prod --dir=' . Project::$directory_path . ' --site=' . Project::$netlify_id, $output); - $result = array_filter($output, fn($output_line) => stripos($output_line, 'Deploy URL') !== false); - - if (empty($result)) { - CLI::display_error("Failed to deploy project."); - exit; - } - - echo CLI::$verbose ? "\nDeploying project " . Project::$url . " DONE\n" : ""; - } - - /** - * Set build version number in service worker file. - * - * @param bool $increment If true, increments build version number. - * @return void - */ - private function set_build_version(bool $increment = false): void { - - if (empty(Project::$directory_path)) { - CLI::display_error("Failed to set build version: project directory path is empty."); - exit; - } - - if (!is_dir(Project::$directory_path)) { - CLI::display_error("Failed to set build version: project directory doesn't exist."); - exit; - } - - $service_worker_file = Project::$directory_path . '/sw.js'; - - if (!file_exists($service_worker_file)) { - CLI::display_error("Failed to set build version: could not find {$service_worker_file} file"); - exit; - } - - $file_contents = file_get_contents($service_worker_file, true); - - if (empty($file_contents)) { - CLI::display_error("Failed to set build version: contents could not be extracted from {$service_worker_file}"); - exit; - } - - // Extract version from project sw.js file before overwriting it. - preg_match('/cache([0-9]+)/', $file_contents, $matches); - $project_version = !empty($matches[1]) ? $matches[1] : '1'; - $project_version_length = strlen($project_version); - - // Increment build version in service worker file. - if (!empty($increment)) { - $position = strpos($file_contents, $project_version); - - if (empty($position)) { - CLI::display_error("Failed to set build version: project version could not be found in {$service_worker_file}"); - exit; - } - - $project_version++; - - $data = substr_replace($file_contents, $project_version, $position, $project_version_length); - file_put_contents($service_worker_file, $data); - } - - // Set build version in package.json. - exec("npm pkg set version={$project_version} --prefix=" . Project::$directory_path); - - Project::set_project_version($project_version); - } - - /** - * Create build directory. - * - * @return void - */ - private function create_build_directory(): void { - - if (empty(Project::$directory_path)) { - CLI::display_error("Failed to create build directory: project directory path is empty."); - exit; - } - - // build directory already exists. - if (is_dir(Project::$directory_path)) { - return; - } - - echo "\nCreating build directory " . Project::$directory_path; - mkdir(Project::$directory_path); - - if (!is_dir(Project::$directory_path)) { - CLI::display_error("Failed to create build directory: mkdir() failed."); - exit; - } - - echo CLI::$verbose ? "\nCreating build directory DONE\n" : " - DONE"; - } - - /** - * Populate new build directory. - * - * @return void - */ - private function populate_build_directory(): void { - - if (empty(Project::$directory_path)) { - CLI::display_error("Failed to populate build directory: project directory path is empty."); - exit; - } - - if (!is_dir(Project::$directory_path)) { - CLI::display_error("Failed to populate build directory: project directory doesn't exist."); - exit; - } - - echo "\nPopulating new build directory " . Project::$directory_path; - - // Create package.json. - chdir(Project::$directory_path); - exec("npm init -y"); - exec("npm pkg set name='" . Project::$url . "'"); - exec("npm pkg set description='" . Project::$description . "'"); - exec("npm pkg set version='1'"); - exec("npm pkg delete main"); - exec("npm pkg delete scripts.test"); - exec("npm pkg set scripts.build='webpack --mode production'"); - exec("npm install --save-dev webpack-cli ts-loader sass css-loader sass-loader style-loader"); - chdir(dirname(__DIR__)); - - // Create scss directory. - $scss_dir = Project::$directory_path . '/scss/'; - if (!is_dir($scss_dir)) { - echo CLI::$verbose ? "\nCreating scss directory..." : ""; - mkdir($scss_dir); - if (!is_dir($scss_dir)) { - CLI::display_error("Failed to populate build directory: cound not create scss directory"); - exit; - } - echo CLI::$verbose ? "\nCreating scss directory DONE" : ""; - } - - // Create js directory. - $js_dir = Project::$directory_path . '/js/'; - if (!is_dir($js_dir)) { - echo CLI::$verbose ? "\nCreating js directory..." : ""; - mkdir($js_dir); - if (!is_dir($js_dir)) { - CLI::display_error("Failed to populate build directory: cound not create js directory"); - exit; - } - echo CLI::$verbose ? "\nCreating js directory DONE" : ""; - } - - // Create opt directory. - $opt_dir = Project::$directory_path . '/opt/'; - if (!is_dir($opt_dir)) { - echo CLI::$verbose ? "\nCreating opt directory..." : ""; - mkdir($opt_dir); - if (!is_dir($opt_dir)) { - CLI::display_error("Failed to populate build directory: cound not create opt directory"); - exit; - } - echo CLI::$verbose ? "\nCreating opt directory DONE" : ""; - } - - echo CLI::$verbose ? "\n\tCreating .gitignore file" : ""; - file_put_contents(Project::$directory_path . '/.gitignore', 'node_modules'); - - echo CLI::$verbose ? "\n\tCreating index.php file" : ""; - file_put_contents(Project::$directory_path . '/index.php', ''); - - echo CLI::$verbose ? "\n\tCreating scss/style.scss file" : ""; - $data = ''; - if ( - !empty(Project::$fonts['heading']) - && !empty(Project::$fonts['body']) - && !empty(Project::$colors['main']['normal']) - && !empty(Project::$colors['main']['dark']) - && !empty(Project::$colors['accent']['normal']) - && !empty(Project::$colors['accent']['dark']) - ) { - $data = ':root {' . - "\n\t" . '--font-heading: "' . Project::$fonts['heading'] . '";' . - "\n\t" . '--font-body: "' . Project::$fonts['body'] . '";' . - "\n\t" . '--color-main: #' . Project::$colors['main']['normal'] . ';' . - "\n\t" . '--color-main-dark: #' . Project::$colors['main']['dark'] . ';' . - "\n\t" . '--color-accent: #' . Project::$colors['accent']['normal'] . ';' . - "\n\t" . '--color-accent-dark: #' . Project::$colors['accent']['dark'] . ';' . - "\n" . '}'; - } - file_put_contents(Project::$directory_path . '/scss/style.scss', $data); - - echo CLI::$verbose ? "\n\tCreating js/main.ts file" : ""; - file_put_contents(Project::$directory_path . '/js/main.ts', ''); - - echo CLI::$verbose ? "\n\tCreating sw.js file" : ""; - file_put_contents(Project::$directory_path . '/sw.js', 'const cacheName = "cache1";'); - - echo CLI::$verbose ? "\nPopulating new build directory DONE\n" : " - DONE"; - } - - /** - * Set up file arrays for caching, compiling, and copying. - */ - private function set_file_arrays(): void { - // Add data.json or posts.json to cache file list if they exist. - $project_data_file = Project::$directory_path . '/data.json'; - if (file_exists($project_data_file)) { - $this->cache_files[] = 'data.json'; - } - - $project_posts_file = Project::$directory_path . '/posts.json'; - if (file_exists($project_posts_file)) { - $this->cache_files[] = 'posts.json'; - } - - // Add "cache_files" defined in project.json to cache file array. - if (!empty(Project::$cache_files) && is_array(Project::$cache_files)) { - foreach (Project::$cache_files as $file) { - $this->cache_files[] = $file; - } - } - - // Add "opt_files" defined in project.json to cache and copy file arrays if they exist in files/opt. - $opt_files = glob(dirname(__DIR__) . '/files/opt/*'); - if (!empty($opt_files) && is_array($opt_files)) { - foreach ($opt_files as $file) { - if (in_array('opt/' . basename($file), Project::$opt_files, true)) { - $this->cache_files[] = 'opt/' . basename($file); - $this->copy_files[] = 'opt/' . basename($file); - } - } - } - - // Add files in files/scss directory to copy file array. - $sass_files = glob(dirname(__DIR__) . '/files/scss/*'); - if (!empty($sass_files) && is_array($sass_files)) { - foreach ($sass_files as $file) { - $this->copy_files[] = 'scss/' . basename($file); - } - } - - // Add files in files/js directory to copy file array. - $js_files = glob(dirname(__DIR__) . '/files/js/*'); - if (!empty($js_files) && is_array($js_files)) { - foreach ($js_files as $file) { - $this->copy_files[] = 'js/' . basename($file); - } - } - - /** - * Add pages to: - * $this->compile_files, for static HTML file generation. - * $this->cache_files, for caching by service worker (also add .jpg if page type === post). - * Project::$sitemap_urls, for sitemap generation. - */ - if (!empty(Project::$pages) && is_array(Project::$pages)) { - foreach (Project::$pages as $page_data) { - if (empty($page_data['url']) || !is_string($page_data['url'])) { - continue; - } - - $page_filename = "{$page_data['url']}.html"; - - // Add page to compile file list. - if (!in_array($page_data['url'], array_column($this->compile_files, 'page'), true)) { - $this->compile_files[] = [ - 'source_file' => dirname(__DIR__) . '/templates/home.php', - 'target_file' => $page_filename, - 'page' => $page_data['url'], - ]; - } - - // Add page to cache file list. - if (!in_array($page_filename, $this->cache_files)) { - $this->cache_files[] = $page_filename; - } - - // Add page featured image to cache file list if page is a post. - if ($page_data['type'] === 'post' && !in_array("img/{$page_data['url']}.jpg", $this->cache_files)) { - $this->cache_files[] = "img/{$page_data['url']}.jpg"; - } - - // Add page to sitemap URLs. - if (!in_array($page_data['url'], Project::$sitemap_urls)) { - Project::$sitemap_urls[] = $page_data['url']; - } - } - } - } - - /** - * Copy files in project copy array into build directory. - * - * @return void - */ - private function copy_files(): void { - - if (empty($this->copy_files) || !is_array($this->copy_files)) { - return; - } - - if (empty(Project::$directory_path)) { - CLI::display_error("Failed to copy files: project directory path is empty."); - exit; - } - - if (!is_dir(Project::$directory_path)) { - CLI::display_error("Failed to copy files: project directory doesn't exist."); - exit; - } - - echo CLI::$verbose ? "\nCopying " . count($this->copy_files) . " files" : ""; - - // Copy files into build directory. - foreach ($this->copy_files as $file) { - $target_file = Project::$directory_path . "/{$file}"; - echo CLI::$verbose ? "\n\tCopying {$file} to {$target_file}" : ""; - copy(dirname(__DIR__) . "/files/{$file}", $target_file); - } - - echo CLI::$verbose ? "\nCopying files DONE" : ""; - } - - /** - * Compiles and minifies style.scss and files in project compile array into build directory. - * - * @return void - */ - private function compile_files(): void { - - if (empty(Project::$directory_path)) { - CLI::display_error("Failed to compile files: project directory path is empty."); - exit; - } - - if (!is_dir(Project::$directory_path)) { - CLI::display_error("Failed to compile files: project directory doesn't exist."); - exit; - } - - $content_to_minify = null; - - // Compile Sass and JavaScript. - if (file_exists(Project::$directory_path . '/js/index.ts')) { - // First, remove any existing bundle.*.js files. - exec('rm -rf ' . Project::$directory_path . '/js/bundle.*.js'); - - // Create new bundle. - echo CLI::$verbose ? "\nCompiling bundle" : ""; - exec('npm run build --prefix=' . Project::$directory_path); - - // Set bundle file name for use in . - $bundle_path = glob(Project::$directory_path . '/js/bundle.*.js'); - - $bundle_filename = !empty($bundle_path[0]) ? basename($bundle_path[0]) : ''; - - if (empty($bundle_filename)) { - CLI::display_error("Failed to compile files: bundle filename is empty."); - exit; - } - - $this->js_bundle_filename = 'js/' . $bundle_filename; - $this->cache_files[] = 'js/' . $bundle_filename; - - echo CLI::$verbose ? "\nCompiling {$bundle_filename} DONE" : ""; - } - - if (empty($this->compile_files || !is_array($this->compile_files))) { - return; - } - - // Compile PHP files. - echo CLI::$verbose ? "\nCompiling " . count($this->compile_files) . " files" : ""; - - // Gather, encode, and escape project data for passing to php files. - $project_reflection_class = new ReflectionClass('Project'); - $project_data = $project_reflection_class->getStaticProperties(); - - // Add build files array and js bundle filename to project data. - $project_data['files'] = [ - 'cache' => $this->cache_files, - 'compile' => $this->compile_files, - 'copy' => $this->copy_files, - ]; - $project_data['js_bundle_filename'] = $this->js_bundle_filename; - - // Encode project data. - $project_data_json = json_encode($project_data); - - if (empty($project_data_json)) { - CLI::display_error("Failed to compile files: encoded project data is empty."); - exit; - } - - // Escape project data. - $project_data_json_escaped = escapeshellarg($project_data_json); - - foreach ($this->compile_files as $file) { - if (empty($file['source_file']) || empty($file['target_file']) || empty($file['page'])) { - continue; - } - - // Skip any non-php files. - if (stripos($file['source_file'], '.php') === false) { - continue; - } - - $target_file_path = Project::$directory_path . "/{$file['target_file']}"; - - echo CLI::$verbose ? "\n\tCompiling {$file['source_file']} to {$target_file_path}" : ""; - - // Open the file using PHP with the page and project_data as arguments. - exec("php {$file['source_file']} {$file['page']} {$project_data_json_escaped} > {$target_file_path}"); - - if (!file_exists($target_file_path)) { - CLI::display_error("Failed to compile files: file {$target_file_path} does not exist."); - continue; - } - - // Get the file contents. - $file_contents = file_get_contents($target_file_path); - - if (empty($file_contents)) { - CLI::display_error("Failed to compile files: target file {$target_file_path} content is empty."); - continue; - } - - // Minify the file contents. - $minified_file_contents = preg_replace('/\s+/S', ' ', $file_contents); - - if (empty($minified_file_contents)) { - CLI::display_error("Failed to compile files: target file {$target_file_path} minified content is empty."); - continue; - } - - // Replace file. - file_put_contents($target_file_path, $minified_file_contents); - } - - echo CLI::$verbose ? "\nCompiling files DONE" : ""; - } - - /** - * Find and replace patterns in compiled and copied files. - * - * @return void - */ - private function template_replace_files(): void { - - if (empty(Project::$url)) { - CLI::display_error("Failed to replace patterns in template files: URL is empty."); - exit; - } - - if (empty(Project::$title)) { - CLI::display_error("Failed to replace patterns in template files: title is empty."); - exit; - } - - if (empty(Project::$description)) { - CLI::display_error("Failed to replace patterns in template files: description is empty."); - exit; - } - - if (empty(Project::$version)) { - CLI::display_error("Failed to replace patterns in template files: project version is empty."); - exit; - } - - echo CLI::$verbose ? "\nReplacing data in project files..." : ""; - - $files = ''; - - // Set files string from cache files array. - if (!empty($this->cache_files) && is_array($this->cache_files)) { - - foreach ($this->cache_files as $index => $file) { - $files .= "\"{$file}\","; - - // Only add new line if followed by another file. - if (!empty($this->cache_files[$index+1])) { - $files .= "\n\t"; - } - } - } - - // Set patterns to find and replace. - $patterns = [ - '// ***FILES***' => $files, - '***URLS***' => $this->get_sitemap_urls(), - '***REDIRECT_URLS***' => $this->get_redirect_urls(), - '***URL***' => Project::$url, - '***TITLE***' => Project::$title, - '***DESCRIPTION***' => Project::$description, - '***VERSION***' => Project::$version, - '***DATE***' => date('Y-m-d'), - ]; - - // Set array of files in which to replace patterns. - $replace_files = array_unique( - array_merge( - array_column($this->compile_files, 'target_file'), - $this->copy_files, - ), - ); - - if (empty($replace_files)) { - CLI::display_error("Failed to replace patterns in template files: replace files array is empty."); - exit; - } - - // Replace common patterns in files. - foreach ($replace_files as $file) { - $replace_file = Project::$url . "/{$file}"; - $replace_file_path = dirname(__DIR__, 2) . "/{$replace_file}"; - - if (!file_exists($replace_file_path)) { - CLI::display_error("Failed to replace patterns in template files: file {$replace_file} does not exist."); - continue; - } - - echo CLI::$verbose ? "\n\tReplacing content in {$replace_file}" : ""; - - $file_contents = file_get_contents($replace_file_path); - - if (!empty($file_contents)) { - foreach ($patterns as $find => $replace) { - if (stripos($file_contents, $find) !== false) { - $file_contents = str_replace($find, $replace, $file_contents); - } - } - } - - // Replace file. - file_put_contents($replace_file_path, $file_contents); - } - - echo CLI::$verbose ? "\nReplacing data in project files DONE" : ""; - } - - /** - * Process font files. - * - * Creates font directories, copies appropriate font files over, and adds them to cached files list. - * - * @return void - */ - private function process_font_files(): void { - - if (empty(Project::$directory_path)) { - CLI::display_error("Failed to process font files: project directory path is empty."); - exit; - } - - if (!is_dir(Project::$directory_path)) { - CLI::display_error("Failed to process font files: project directory doesn't exist."); - exit; - } - - // Create font directories and add heading and body font files to project data. - if (!empty(Project::$fonts)) { - // Create fonts directory. - $fonts_dir = Project::$directory_path . '/fonts'; - if (!is_dir($fonts_dir)) { - echo CLI::$verbose ? "\nCreating fonts directory..." : ""; - mkdir($fonts_dir); - if (!is_dir($fonts_dir)) { - CLI::display_error("Failed to process font files: cound not create fonts directory"); - exit; - } - echo CLI::$verbose ? "\nCreating fonts directory DONE" : ""; - } - - if (!empty(Project::$fonts['heading'])) { - $heading_font = Project::$fonts['heading']; - $heading_font_decoded = strtolower(str_ireplace(' ', '-', $heading_font)); - - // Create heading font directory. - $heading_font_dir = Project::$directory_path . "/fonts/{$heading_font_decoded}"; - if (!is_dir($heading_font_dir)) { - echo CLI::$verbose ? "\nCreating {$heading_font_dir} directory..." : ""; - mkdir($heading_font_dir); - if (!is_dir($heading_font_dir)) { - CLI::display_error("Failed to process font files: cound not create heading font directory"); - exit; - } - echo CLI::$verbose ? "\nCreating {$heading_font_dir} directory DONE" : ""; - } - - // Add heading font files to copy and cache arrays. - $heading_font_files = glob(dirname(__DIR__) . "/files/fonts/{$heading_font_decoded}/*"); - if (!empty($heading_font_files) && is_array($heading_font_files)) { - foreach ($heading_font_files as $index => $file) { - $heading_font_file = basename($file); - $heading_font_files[$index] = "fonts/{$heading_font_decoded}/{$heading_font_file}"; - } - - $this->copy_files = !empty($this->copy_files) - ? array_merge($this->copy_files, $heading_font_files) - : $heading_font_files; - $this->cache_files = !empty($this->cache_files) - ? array_merge($this->cache_files, $heading_font_files) - : $heading_font_files; - } - } - - if (!empty(Project::$fonts['body'])) { - $body_font = Project::$fonts['body']; - $body_font_decoded = strtolower(str_ireplace(' ', '-', $body_font)); - - // Create body font directory. - $body_font_dir = Project::$directory_path . "/fonts/{$body_font_decoded}"; - if (!is_dir($body_font_dir)) { - echo CLI::$verbose ? "\nCreating {$body_font_dir} directory..." : ""; - mkdir($body_font_dir); - if (!is_dir($body_font_dir)) { - CLI::display_error("Failed to process font files: cound not create body font directory"); - exit; - } - echo CLI::$verbose ? "\nCreating {$body_font_dir} directory DONE" : ""; - } - - // Add body font files to copy and cache arrays. - $body_font_files = glob(dirname(__DIR__) . "/files/fonts/{$body_font_decoded}/*"); - if (!empty($body_font_files) && is_array($body_font_files)) { - foreach ($body_font_files as $index => $file) { - $body_font_file = basename($file); - $body_font_files[$index] = "fonts/{$body_font_decoded}/{$body_font_file}"; - } - - $this->copy_files = !empty($this->copy_files) - ? array_merge($this->copy_files, $body_font_files) - : $body_font_files; - $this->cache_files = !empty($this->cache_files) - ? array_merge($this->cache_files, $body_font_files) - : $body_font_files; - } - } - } - - // Create fontawesome directories and add files to project data. - if (!empty(Project::$fontawesome)) { - // Create fontawesome directories. - $directories = [ - Project::$directory_path . '/fonts/fontawesome', - Project::$directory_path . '/fonts/fontawesome/css', - Project::$directory_path . '/fonts/fontawesome/webfonts', - ]; - foreach ($directories as $directory_path) { - if (!is_dir($directory_path)) { - echo CLI::$verbose ? "\nCreating {$directory_path} directory..." : ""; - mkdir($directory_path); - if (!is_dir($directory_path)) { - CLI::display_error("Failed to process font files: cound not create {$directory_path} directory"); - exit; - } - echo CLI::$verbose ? "\nCreating {$directory_path} directory DONE" : ""; - } - } - - // Add fontawesome files to copy and cache arrays. - $fontawesome_files = [ - 'fonts/fontawesome/css/brands.min.css', - 'fonts/fontawesome/css/fontawesome.min.css', - 'fonts/fontawesome/css/solid.min.css', - 'fonts/fontawesome/webfonts/fa-brands-400.eot', - 'fonts/fontawesome/webfonts/fa-brands-400.svg', - 'fonts/fontawesome/webfonts/fa-brands-400.ttf', - 'fonts/fontawesome/webfonts/fa-brands-400.woff', - 'fonts/fontawesome/webfonts/fa-brands-400.woff2', - 'fonts/fontawesome/webfonts/fa-solid-900.eot', - 'fonts/fontawesome/webfonts/fa-solid-900.svg', - 'fonts/fontawesome/webfonts/fa-solid-900.ttf', - 'fonts/fontawesome/webfonts/fa-solid-900.woff', - 'fonts/fontawesome/webfonts/fa-solid-900.woff2' - ]; - - $this->copy_files = !empty($this->copy_files) - ? array_merge($this->copy_files, $fontawesome_files) - : $fontawesome_files; - $this->cache_files = !empty($this->cache_files) - ? array_merge($this->cache_files, $fontawesome_files) - : $fontawesome_files; - } - } - - /** - * Get sitemap URLs. - * - * Used in sitemap.xml file. - * - * @return string Sitemap URLs. - */ - private function get_sitemap_urls(): string { - - // Add any redirected URLs to sitemap URLs. - if (!empty(Project::$redirects) && is_array(Project::$redirects)) { - foreach (Project::$redirects as $index => $redirect) { - $url = trim($redirect['from'], '/'); - if (($url !== '') && ($url !== 'index') && (stripos($url, '.') !== false) && (stripos($url, '*') !== false)) { - Project::$sitemap_urls[] = $url; - } - } - } - - $sitemap_urls = ''; - - // Add data for each URL. - if (!empty(Project::$sitemap_urls) && is_array(Project::$sitemap_urls)) { - foreach (Project::$sitemap_urls as $index => $url) { - $sitemap_urls .= str_ireplace( - [ - '{url}', - '{date}', - ], - [ - Project::$url . '/' . ($url !== 'index' ? $url : ''), - date('Y-m-d'), - ], - "\t\n" . - "\t\thttps://{url}\n" . - "\t\t{date}\n" . - "\t\n", - ); - } - } - - return $sitemap_urls; - } - - /** - * Get redirect URLs. - * - * Used in _redirects file. - * - * @return string Sitemap URLs. - */ - private function get_redirect_urls(): string { - - $redirect_urls = ''; - - if (!empty($this->compile_files) && is_array($this->compile_files)) { - foreach ($this->compile_files as $file) { - if (empty($file['target_file'])) { - continue; - } - - if ($file['page'] !== 'index') { - $redirect_urls .= str_ireplace('{title}', $file['page'], '/{title}/* /{title}.html 200') . "\n"; - } - } - } - - // If redirects are set, add to urls. - if (!empty(Project::$redirects) && is_array(Project::$redirects)) { - foreach (Project::$redirects as $index => $redirect_url) { - $redirect_urls .= "{$redirect_url['from']} {$redirect_url['to']}\n"; - } - } - - $redirect_urls = trim($redirect_urls, "\n"); - $redirect_urls .= "\n/* /index.html 200"; - - return $redirect_urls; - } - - /** - * Generate manifest.json. - * - * @return void - */ - private function generate_manifest(): void { - - if (empty(Project::$directory_path)) { - CLI::display_error("Failed to generate manifest: project directory path is empty."); - exit; - } - - if (!is_dir(Project::$directory_path)) { - CLI::display_error("Failed to generate manifest: project directory doesn't exist."); - exit; - } - - $manifest_file_path = Project::$directory_path . '/manifest.json'; - - if (!file_exists($manifest_file_path)) { - CLI::display_error("Failed to generate manifest: file {$manifest_file_path} does not exist."); - exit; - } - - $file_contents = file_get_contents($manifest_file_path); - - if (empty($file_contents)) { - CLI::display_error("Failed to generate manifest: manifest file contents is empty."); - exit; - } - - // Modify manifest data from project data. - $manifest_data = json_decode($file_contents, true); - - if (empty($manifest_data) || !is_array($manifest_data)) { - CLI::display_error("Failed to generate manifest: manifest file data is empty or is not an array."); - exit; - } - - echo CLI::$verbose ? "\n\tGenerating manifest.json" : ""; - - if (!empty(Project::$categories)) { - $manifest_data['categories'] = Project::$categories; - } - - if (!empty(Project::$screenshots) && is_array(Project::$screenshots)) { - $manifest_data['screenshots'] = []; - foreach (Project::$screenshots as $file) { - $manifest_data['screenshots'][] = [ - 'src' => $file, - 'sizes' => '1500x800', - 'type' => 'image/png' - ]; - } - } - - if (!empty(Project::$shortcuts)) { - $manifest_data['shortcuts'] = Project::$shortcuts; - } - - $manifest_data['related_applications'][] = [ - 'platform' => 'webapp', - 'url' => 'https://' . Project::$url . '/manifest.json', - ]; - - if (!empty(Project::$android_app_id) || !empty(Project::$apple_app_id)) { - - $android_app_id = null; - if (!empty(Project::$android_app_id['paid'])) { - $android_app_id = Project::$android_app_id['paid']; - } else if (!empty(Project::$android_app_id['free'])) { - $android_app_id = Project::$android_app_id['free']; - } - - $apple_app_id = null; - if (!empty(Project::$apple_app_id['paid'])) { - $apple_app_id = Project::$apple_app_id['paid']; - } else if (!empty(Project::$apple_app_id['free'])) { - $apple_app_id = Project::$apple_app_id['free']; - } - - if (!empty($android_app_id) && ($android_app_id !== 'disabled')) { - $manifest_data['related_applications'][] = [ - 'platform' => 'play', - 'url' => "https://play.google.com/store/apps/details?id={$android_app_id}", - 'id' => $android_app_id - ]; - $manifest_data['prefer_related_applications'] = true; - } - - if (!empty($apple_app_id) && ($apple_app_id !== 'disabled')) { - $manifest_data['related_applications'][] = [ - 'platform' => 'itunes', - 'url' => "https://itunes.apple.com/app/example-app1/id123456789{$apple_app_id}", - ]; - $manifest_data['prefer_related_applications'] = true; - } - } - - // Replace file. - file_put_contents($manifest_file_path, json_encode($manifest_data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); - - echo CLI::$verbose ? "\n\tGenerating manifest.json DONE" : ""; - } - - /** - * Generate favicons. - * - * @return void - */ - private function generate_favicons(): void { - - if (empty(Project::$directory_path)) { - CLI::display_error("Failed to generate favicons: build directory path is empty."); - exit; - } - - if (!is_dir(Project::$directory_path)) { - CLI::display_error("Failed to generate favicons: build directory doesn't exist."); - exit; - } - - if (!file_exists(Project::$directory_path . '/img/favicon.svg')) { - CLI::display_error("Failed to generate favicons: favicon.svg is missing."); - exit; - } - - echo "Generating favicons..."; - - // Set up favicon config data. - $favicon_data_config = [ - 'masterPicture' => Project::$directory_path . '/img/favicon.svg', - 'iconsPath' => '/img/', - 'design' => - [ - 'ios' => - [ - 'pictureAspect' => 'backgroundAndMargin', - 'backgroundColor' => '#000000', - 'margin' => '18%', - 'assets' => - [ - 'ios6AndPriorIcons' => true, - 'ios7AndLaterIcons' => true, - 'precomposedIcons' => false, - 'declareOnlyDefaultIcon' => true, - ], - ], - 'desktopBrowser' => - [ - 'design' => 'raw', - ], - 'windows' => - [ - 'pictureAspect' => 'noChange', - 'backgroundColor' => '#000000', - 'onConflict' => 'override', - 'assets' => - [ - 'windows80Ie10Tile' => false, - 'windows10Ie11EdgeTiles' => - [ - 'small' => false, - 'medium' => true, - 'big' => false, - 'rectangle' => false, - ], - ], - ], - 'androidChrome' => - [ - 'pictureAspect' => 'noChange', - 'themeColor' => '#000000', - 'manifest' => - [ - 'display' => 'standalone', - 'orientation' => 'notSet', - 'onConflict' => 'override', - 'declared' => true, - ], - 'assets' => - [ - 'legacyIcon' => false, - 'lowResolutionIcons' => true, - ], - ], - 'safariPinnedTab' => - [ - 'pictureAspect' => 'blackAndWhite', - 'threshold' => 89.21875, - 'themeColor' => '#000000', - ], - ], - 'settings' => - [ - 'compression' => 2, - 'scalingAlgorithm' => 'Mitchell', - 'errorOnImageTooSmall' => false, - 'readmeFile' => false, - 'htmlCodeFile' => false, - 'usePathAsIs' => false, - ], - ]; - - // Create favicon data config file. - file_put_contents(Project::$directory_path . '/favicon_config.json', json_encode($favicon_data_config, JSON_PRETTY_PRINT)); - - // Generate favicons. - exec('npx real-favicon generate ' . Project::$directory_path . '/favicon_config.json favicon_data.json ' . Project::$directory_path . '/img/'); - - // Remove generated favicon data file. - exec('rm -rf favicon_data.json'); - - // Remove favicon data config file. - exec('rm -rf ' . Project::$directory_path . '/favicon_config.json'); - - // Remove favicon manifest file. - exec('rm -rf ' . Project::$directory_path . '/img/site.webmanifest'); - - echo CLI::$verbose ? "\nGenerating favicons DONE" : " - DONE\n"; - } -} \ No newline at end of file + /** + * Files for service worker to cache. + * + * @var array + */ + private array $cache_files = [ + 'manifest.json', + ]; + + /** + * Files to compile into build directory. + * + * @var array> + */ + private array $compile_files = []; + + /** + * Files to copy into build directory. + * + * @var array + */ + private array $copy_files = [ + '_redirects', + 'manifest.json', + 'robots.txt', + 'sitemap.xml', + 'sw.js', + 'tsconfig.json', + 'webpack.config.js', + ]; + + /** + * JavaScript bundle filename (including hash). + * + * @var string + */ + private string $js_bundle_filename = ''; + + /** + * Constructor. + * + * Sets up project data and performs actions based on build options provided. + * + * @param string $project Project name. + * @param array $options Build options. + */ + public function __construct( + string $project, + array $options = [ + 'generate_favicons' => false, + 'build' => false, + 'deploy' => false, + ] + ) { + if (empty($project)) { + CLI::display_error("Failed to construct build: project not provided."); + exit; + } + + // Set up project data. + $project = new Project($project); + + // Generate favicons. + if (!empty($options['generate_favicons'])) { + $this->generate_favicons(); + } + + // Build project. + if (!empty($options['build'])) { + $this->build_project(); + } + + // Deploy project. + if (!empty($options['deploy'])) { + $this->deploy_project(); + } + } + + /** + * Builds a project according to build data. + * + * @return void + */ + private function build_project(): void { + + if (empty(Project::$directory_path)) { + CLI::display_error("Failed to build project: project directory path is empty."); + exit; + } + + if (empty(Project::$url)) { + CLI::display_error("Failed to build project: URL is empty."); + exit; + } + + // Create and populate new website directory if it doesn't already exist. + if (!is_dir(Project::$directory_path)) { + $this->create_build_directory(); + $this->populate_build_directory(); + } + + $this->set_build_version(); + + if (empty(Project::$version)) { + CLI::display_error("Failed to build project: project version is empty."); + exit; + } + + echo "\nBuilding project " . Project::$url . ' v' . Project::$version; + + $this->set_file_arrays(); + $this->process_font_files(); + $this->copy_files(); + $this->compile_files(); + $this->generate_manifest(); + $this->template_replace_files(); + + echo CLI::$verbose + ? "\nBuilding project " . Project::$url . " DONE\n" + : " - DONE\n"; + } + + /** + * Deploys a project to Netlify according to build data. + * + * @return void + */ + private function deploy_project(): void { + + if (empty(Project::$directory_path)) { + CLI::display_error("Failed to deploy project: project directory path is empty."); + exit; + } + + if (!is_dir(Project::$directory_path)) { + CLI::display_error("Failed to deploy project: project directory doesn't exist."); + exit; + } + + if (empty(Project::$url)) { + CLI::display_error("Failed to deploy project: URL is empty."); + exit; + } + + if (empty(Project::$netlify_id)) { + CLI::display_error("Failed to deploy project: Netlify ID is empty."); + exit; + } + + // Increment build version. + $this->set_build_version(true); + + echo 'Deploying project ' . Project::$netlify_id . ' v' . Project::$version . ' to ' . Project::$url; + + // Execute Netlify CLI deployment. + $output = []; + // phpcs:ignore Generic.Files.LineLength.TooLong + exec('npx netlify deploy --prod --dir=' . Project::$directory_path . ' --site=' . Project::$netlify_id, $output); + $result = array_filter($output, fn($output_line) => stripos($output_line, 'Deploy URL') !== false); + + if (empty($result)) { + CLI::display_error("Failed to deploy project."); + exit; + } + + echo CLI::$verbose ? "\nDeploying project " . Project::$url . " DONE\n" : ""; + } + + /** + * Set build version number in service worker file. + * + * @param bool $increment If true, increments build version number. + * @return void + */ + private function set_build_version(bool $increment = false): void { + + if (empty(Project::$directory_path)) { + CLI::display_error("Failed to set build version: project directory path is empty."); + exit; + } + + if (!is_dir(Project::$directory_path)) { + CLI::display_error("Failed to set build version: project directory doesn't exist."); + exit; + } + + $service_worker_file = Project::$directory_path . '/sw.js'; + + if (!file_exists($service_worker_file)) { + CLI::display_error("Failed to set build version: could not find {$service_worker_file} file"); + exit; + } + + $file_contents = file_get_contents($service_worker_file, true); + + if (empty($file_contents)) { + CLI::display_error( + "Failed to set build version: contents could not be extracted from {$service_worker_file}" + ); + exit; + } + + // Extract version from project sw.js file before overwriting it. + preg_match('/cache([0-9]+)/', $file_contents, $matches); + $project_version = !empty($matches[1]) ? $matches[1] : '1'; + $project_version_length = strlen($project_version); + + // Increment build version in service worker file. + if (!empty($increment)) { + $position = strpos($file_contents, $project_version); + + if (empty($position)) { + CLI::display_error( + "Failed to set build version: project version could not be found in {$service_worker_file}" + ); + exit; + } + + $project_version++; + + $data = substr_replace($file_contents, $project_version, $position, $project_version_length); + file_put_contents($service_worker_file, $data); + } + + // Set build version in package.json. + exec("npm pkg set version={$project_version} --prefix=" . Project::$directory_path); + + Project::set_project_version($project_version); + } + + /** + * Create build directory. + * + * @return void + */ + private function create_build_directory(): void { + + if (empty(Project::$directory_path)) { + CLI::display_error("Failed to create build directory: project directory path is empty."); + exit; + } + + // build directory already exists. + if (is_dir(Project::$directory_path)) { + return; + } + + echo "\nCreating build directory " . Project::$directory_path; + mkdir(Project::$directory_path); + + if (!is_dir(Project::$directory_path)) { + CLI::display_error("Failed to create build directory: mkdir() failed."); + exit; + } + + echo CLI::$verbose ? "\nCreating build directory DONE\n" : " - DONE"; + } + + /** + * Populate new build directory. + * + * @return void + */ + private function populate_build_directory(): void { + + if (empty(Project::$directory_path)) { + CLI::display_error("Failed to populate build directory: project directory path is empty."); + exit; + } + + if (!is_dir(Project::$directory_path)) { + CLI::display_error("Failed to populate build directory: project directory doesn't exist."); + exit; + } + + echo "\nPopulating new build directory " . Project::$directory_path; + + // Create package.json. + chdir(Project::$directory_path); + exec("npm init -y"); + exec("npm pkg set name='" . Project::$url . "'"); + exec("npm pkg set description='" . Project::$description . "'"); + exec("npm pkg set version='1'"); + exec("npm pkg delete main"); + exec("npm pkg delete scripts.test"); + exec("npm pkg set scripts.build='webpack --mode production'"); + exec("npm install --save-dev webpack-cli ts-loader sass css-loader sass-loader style-loader"); + chdir(dirname(__DIR__)); + + // Create scss directory. + $scss_dir = Project::$directory_path . '/scss/'; + if (!is_dir($scss_dir)) { + echo CLI::$verbose ? "\nCreating scss directory..." : ""; + mkdir($scss_dir); + if (!is_dir($scss_dir)) { + CLI::display_error("Failed to populate build directory: cound not create scss directory"); + exit; + } + echo CLI::$verbose ? "\nCreating scss directory DONE" : ""; + } + + // Create js directory. + $js_dir = Project::$directory_path . '/js/'; + if (!is_dir($js_dir)) { + echo CLI::$verbose ? "\nCreating js directory..." : ""; + mkdir($js_dir); + if (!is_dir($js_dir)) { + CLI::display_error("Failed to populate build directory: cound not create js directory"); + exit; + } + echo CLI::$verbose ? "\nCreating js directory DONE" : ""; + } + + // Create opt directory. + $opt_dir = Project::$directory_path . '/opt/'; + if (!is_dir($opt_dir)) { + echo CLI::$verbose ? "\nCreating opt directory..." : ""; + mkdir($opt_dir); + if (!is_dir($opt_dir)) { + CLI::display_error("Failed to populate build directory: cound not create opt directory"); + exit; + } + echo CLI::$verbose ? "\nCreating opt directory DONE" : ""; + } + + echo CLI::$verbose ? "\n\tCreating .gitignore file" : ""; + file_put_contents(Project::$directory_path . '/.gitignore', 'node_modules'); + + echo CLI::$verbose ? "\n\tCreating index.php file" : ""; + file_put_contents(Project::$directory_path . '/index.php', ''); + + echo CLI::$verbose ? "\n\tCreating scss/style.scss file" : ""; + $data = ''; + if ( + !empty(Project::$fonts['heading']) + && !empty(Project::$fonts['body']) + && !empty(Project::$colors['main']['normal']) + && !empty(Project::$colors['main']['dark']) + && !empty(Project::$colors['accent']['normal']) + && !empty(Project::$colors['accent']['dark']) + ) { + $data = ':root {' . + "\n\t" . '--font-heading: "' . Project::$fonts['heading'] . '";' . + "\n\t" . '--font-body: "' . Project::$fonts['body'] . '";' . + "\n\t" . '--color-main: #' . Project::$colors['main']['normal'] . ';' . + "\n\t" . '--color-main-dark: #' . Project::$colors['main']['dark'] . ';' . + "\n\t" . '--color-accent: #' . Project::$colors['accent']['normal'] . ';' . + "\n\t" . '--color-accent-dark: #' . Project::$colors['accent']['dark'] . ';' . + "\n" . '}'; + } + file_put_contents(Project::$directory_path . '/scss/style.scss', $data); + + echo CLI::$verbose ? "\n\tCreating js/main.ts file" : ""; + file_put_contents(Project::$directory_path . '/js/main.ts', ''); + + echo CLI::$verbose ? "\n\tCreating sw.js file" : ""; + file_put_contents(Project::$directory_path . '/sw.js', 'const cacheName = "cache1";'); + + echo CLI::$verbose ? "\nPopulating new build directory DONE\n" : " - DONE"; + } + + /** + * Set up file arrays for caching, compiling, and copying. + */ + private function set_file_arrays(): void { + // Add data.json or posts.json to cache file list if they exist. + $project_data_file = Project::$directory_path . '/data.json'; + if (file_exists($project_data_file)) { + $this->cache_files[] = 'data.json'; + } + + $project_posts_file = Project::$directory_path . '/posts.json'; + if (file_exists($project_posts_file)) { + $this->cache_files[] = 'posts.json'; + } + + // Add "cache_files" defined in project.json to cache file array. + if (!empty(Project::$cache_files) && is_array(Project::$cache_files)) { + foreach (Project::$cache_files as $file) { + $this->cache_files[] = $file; + } + } + + // Add "opt_files" defined in project.json to cache and copy file arrays if they exist in files/opt. + $opt_files = glob(dirname(__DIR__) . '/files/opt/*'); + if (!empty($opt_files) && is_array($opt_files)) { + foreach ($opt_files as $file) { + if (in_array('opt/' . basename($file), Project::$opt_files, true)) { + $this->cache_files[] = 'opt/' . basename($file); + $this->copy_files[] = 'opt/' . basename($file); + } + } + } + + // Add files in files/scss directory to copy file array. + $sass_files = glob(dirname(__DIR__) . '/files/scss/*'); + if (!empty($sass_files) && is_array($sass_files)) { + foreach ($sass_files as $file) { + $this->copy_files[] = 'scss/' . basename($file); + } + } + + // Add files in files/js directory to copy file array. + $js_files = glob(dirname(__DIR__) . '/files/js/*'); + if (!empty($js_files) && is_array($js_files)) { + foreach ($js_files as $file) { + $this->copy_files[] = 'js/' . basename($file); + } + } + + /** + * Add pages to: + * $this->compile_files, for static HTML file generation. + * $this->cache_files, for caching by service worker (also add .jpg if page type === post). + * Project::$sitemap_urls, for sitemap generation. + */ + if (!empty(Project::$pages) && is_array(Project::$pages)) { + foreach (Project::$pages as $page_data) { + if (empty($page_data['url']) || !is_string($page_data['url'])) { + continue; + } + + $page_filename = "{$page_data['url']}.html"; + + // Add page to compile file list. + if (!in_array($page_data['url'], array_column($this->compile_files, 'page'), true)) { + $this->compile_files[] = [ + 'source_file' => dirname(__DIR__) . '/templates/home.php', + 'target_file' => $page_filename, + 'page' => $page_data['url'], + ]; + } + + // Add page to cache file list. + if (!in_array($page_filename, $this->cache_files)) { + $this->cache_files[] = $page_filename; + } + + // Add page featured image to cache file list if page is a post. + if ($page_data['type'] === 'post' && !in_array("img/{$page_data['url']}.jpg", $this->cache_files)) { + $this->cache_files[] = "img/{$page_data['url']}.jpg"; + } + + // Add page to sitemap URLs. + if (!in_array($page_data['url'], Project::$sitemap_urls)) { + Project::$sitemap_urls[] = $page_data['url']; + } + } + } + } + + /** + * Copy files in project copy array into build directory. + * + * @return void + */ + private function copy_files(): void { + + if (empty($this->copy_files) || !is_array($this->copy_files)) { + return; + } + + if (empty(Project::$directory_path)) { + CLI::display_error("Failed to copy files: project directory path is empty."); + exit; + } + + if (!is_dir(Project::$directory_path)) { + CLI::display_error("Failed to copy files: project directory doesn't exist."); + exit; + } + + echo CLI::$verbose ? "\nCopying " . count($this->copy_files) . " files" : ""; + + // Copy files into build directory. + foreach ($this->copy_files as $file) { + $target_file = Project::$directory_path . "/{$file}"; + echo CLI::$verbose ? "\n\tCopying {$file} to {$target_file}" : ""; + copy(dirname(__DIR__) . "/files/{$file}", $target_file); + } + + echo CLI::$verbose ? "\nCopying files DONE" : ""; + } + + /** + * Compiles and minifies style.scss and files in project compile array into build directory. + * + * @return void + */ + private function compile_files(): void { + + if (empty(Project::$directory_path)) { + CLI::display_error("Failed to compile files: project directory path is empty."); + exit; + } + + if (!is_dir(Project::$directory_path)) { + CLI::display_error("Failed to compile files: project directory doesn't exist."); + exit; + } + + $content_to_minify = null; + + // Compile Sass and JavaScript. + if (file_exists(Project::$directory_path . '/js/index.ts')) { + // First, remove any existing bundle.*.js files. + exec('rm -rf ' . Project::$directory_path . '/js/bundle.*.js'); + + // Create new bundle. + echo CLI::$verbose ? "\nCompiling bundle" : ""; + exec('npm run build --prefix=' . Project::$directory_path); + + // Set bundle file name for use in . + $bundle_path = glob(Project::$directory_path . '/js/bundle.*.js'); + + $bundle_filename = !empty($bundle_path[0]) ? basename($bundle_path[0]) : ''; + + if (empty($bundle_filename)) { + CLI::display_error("Failed to compile files: bundle filename is empty."); + exit; + } + + $this->js_bundle_filename = 'js/' . $bundle_filename; + $this->cache_files[] = 'js/' . $bundle_filename; + + echo CLI::$verbose ? "\nCompiling {$bundle_filename} DONE" : ""; + } + + if (empty($this->compile_files || !is_array($this->compile_files))) { + return; + } + + // Compile PHP files. + echo CLI::$verbose ? "\nCompiling " . count($this->compile_files) . " files" : ""; + + // Gather, encode, and escape project data for passing to php files. + $project_reflection_class = new \ReflectionClass('PWA_Generator\Project'); + $project_data = $project_reflection_class->getStaticProperties(); + + // Add build files array and js bundle filename to project data. + $project_data['files'] = [ + 'cache' => $this->cache_files, + 'compile' => $this->compile_files, + 'copy' => $this->copy_files, + ]; + $project_data['js_bundle_filename'] = $this->js_bundle_filename; + + // Encode project data. + $project_data_json = json_encode($project_data); + + if (empty($project_data_json)) { + CLI::display_error("Failed to compile files: encoded project data is empty."); + exit; + } + + // Escape project data. + $project_data_json_escaped = escapeshellarg($project_data_json); + + foreach ($this->compile_files as $file) { + if (empty($file['source_file']) || empty($file['target_file']) || empty($file['page'])) { + continue; + } + + // Skip any non-php files. + if (stripos($file['source_file'], '.php') === false) { + continue; + } + + $target_file_path = Project::$directory_path . "/{$file['target_file']}"; + + echo CLI::$verbose ? "\n\tCompiling {$file['source_file']} to {$target_file_path}" : ""; + + // Open the file using PHP with the page and project_data as arguments. + exec("php {$file['source_file']} {$file['page']} {$project_data_json_escaped} > {$target_file_path}"); + + if (!file_exists($target_file_path)) { + CLI::display_error("Failed to compile files: file {$target_file_path} does not exist."); + continue; + } + + // Get the file contents. + $file_contents = file_get_contents($target_file_path); + + if (empty($file_contents)) { + CLI::display_error("Failed to compile files: target file {$target_file_path} content is empty."); + continue; + } + + // Minify the file contents. + $minified_file_contents = preg_replace('/\s+/S', ' ', $file_contents); + + if (empty($minified_file_contents)) { + CLI::display_error( + "Failed to compile files: target file {$target_file_path} minified content is empty." + ); + continue; + } + + // Replace file. + file_put_contents($target_file_path, $minified_file_contents); + } + + echo CLI::$verbose ? "\nCompiling files DONE" : ""; + } + + /** + * Find and replace patterns in compiled and copied files. + * + * @return void + */ + private function template_replace_files(): void { + + if (empty(Project::$url)) { + CLI::display_error("Failed to replace patterns in template files: URL is empty."); + exit; + } + + if (empty(Project::$title)) { + CLI::display_error("Failed to replace patterns in template files: title is empty."); + exit; + } + + if (empty(Project::$description)) { + CLI::display_error("Failed to replace patterns in template files: description is empty."); + exit; + } + + if (empty(Project::$version)) { + CLI::display_error("Failed to replace patterns in template files: project version is empty."); + exit; + } + + echo CLI::$verbose ? "\nReplacing data in project files..." : ""; + + $files = ''; + + // Set files string from cache files array. + if (!empty($this->cache_files) && is_array($this->cache_files)) { + foreach ($this->cache_files as $index => $file) { + $files .= "\"{$file}\","; + + // Only add new line if followed by another file. + if (!empty($this->cache_files[$index + 1])) { + $files .= "\n\t"; + } + } + } + + // Set patterns to find and replace. + $patterns = [ + '// ***FILES***' => $files, + '***URLS***' => $this->get_sitemap_urls(), + '***REDIRECT_URLS***' => $this->get_redirect_urls(), + '***URL***' => Project::$url, + '***TITLE***' => Project::$title, + '***DESCRIPTION***' => Project::$description, + '***VERSION***' => Project::$version, + '***DATE***' => date('Y-m-d'), + ]; + + // Set array of files in which to replace patterns. + $replace_files = array_unique( + array_merge( + array_column($this->compile_files, 'target_file'), + $this->copy_files, + ), + ); + + if (empty($replace_files)) { + CLI::display_error("Failed to replace patterns in template files: replace files array is empty."); + exit; + } + + // Replace common patterns in files. + foreach ($replace_files as $file) { + $replace_file = Project::$url . "/{$file}"; + $replace_file_path = dirname(__DIR__, 2) . "/{$replace_file}"; + + if (!file_exists($replace_file_path)) { + CLI::display_error( + "Failed to replace patterns in template files: file {$replace_file} does not exist." + ); + continue; + } + + echo CLI::$verbose ? "\n\tReplacing content in {$replace_file}" : ""; + + $file_contents = file_get_contents($replace_file_path); + + if (!empty($file_contents)) { + foreach ($patterns as $find => $replace) { + if (stripos($file_contents, $find) !== false) { + $file_contents = str_replace($find, $replace, $file_contents); + } + } + } + + // Replace file. + file_put_contents($replace_file_path, $file_contents); + } + + echo CLI::$verbose ? "\nReplacing data in project files DONE" : ""; + } + + /** + * Process font files. + * + * Creates font directories, copies appropriate font files over, and adds them to cached files list. + * + * @return void + */ + private function process_font_files(): void { + + if (empty(Project::$directory_path)) { + CLI::display_error("Failed to process font files: project directory path is empty."); + exit; + } + + if (!is_dir(Project::$directory_path)) { + CLI::display_error("Failed to process font files: project directory doesn't exist."); + exit; + } + + // Create font directories and add heading and body font files to project data. + if (!empty(Project::$fonts)) { + // Create fonts directory. + $fonts_dir = Project::$directory_path . '/fonts'; + if (!is_dir($fonts_dir)) { + echo CLI::$verbose ? "\nCreating fonts directory..." : ""; + mkdir($fonts_dir); + if (!is_dir($fonts_dir)) { + CLI::display_error("Failed to process font files: cound not create fonts directory"); + exit; + } + echo CLI::$verbose ? "\nCreating fonts directory DONE" : ""; + } + + if (!empty(Project::$fonts['heading'])) { + $heading_font = Project::$fonts['heading']; + $heading_font_decoded = strtolower(str_ireplace(' ', '-', $heading_font)); + + // Create heading font directory. + $heading_font_dir = Project::$directory_path . "/fonts/{$heading_font_decoded}"; + if (!is_dir($heading_font_dir)) { + echo CLI::$verbose ? "\nCreating {$heading_font_dir} directory..." : ""; + mkdir($heading_font_dir); + if (!is_dir($heading_font_dir)) { + CLI::display_error("Failed to process font files: cound not create heading font directory"); + exit; + } + echo CLI::$verbose ? "\nCreating {$heading_font_dir} directory DONE" : ""; + } + + // Add heading font files to copy and cache arrays. + $heading_font_files = glob(dirname(__DIR__) . "/files/fonts/{$heading_font_decoded}/*"); + if (!empty($heading_font_files) && is_array($heading_font_files)) { + foreach ($heading_font_files as $index => $file) { + $heading_font_file = basename($file); + $heading_font_files[$index] = "fonts/{$heading_font_decoded}/{$heading_font_file}"; + } + + $this->copy_files = !empty($this->copy_files) + ? array_merge($this->copy_files, $heading_font_files) + : $heading_font_files; + $this->cache_files = !empty($this->cache_files) + ? array_merge($this->cache_files, $heading_font_files) + : $heading_font_files; + } + } + + if (!empty(Project::$fonts['body'])) { + $body_font = Project::$fonts['body']; + $body_font_decoded = strtolower(str_ireplace(' ', '-', $body_font)); + + // Create body font directory. + $body_font_dir = Project::$directory_path . "/fonts/{$body_font_decoded}"; + if (!is_dir($body_font_dir)) { + echo CLI::$verbose ? "\nCreating {$body_font_dir} directory..." : ""; + mkdir($body_font_dir); + if (!is_dir($body_font_dir)) { + CLI::display_error("Failed to process font files: cound not create body font directory"); + exit; + } + echo CLI::$verbose ? "\nCreating {$body_font_dir} directory DONE" : ""; + } + + // Add body font files to copy and cache arrays. + $body_font_files = glob(dirname(__DIR__) . "/files/fonts/{$body_font_decoded}/*"); + if (!empty($body_font_files) && is_array($body_font_files)) { + foreach ($body_font_files as $index => $file) { + $body_font_file = basename($file); + $body_font_files[$index] = "fonts/{$body_font_decoded}/{$body_font_file}"; + } + + $this->copy_files = !empty($this->copy_files) + ? array_merge($this->copy_files, $body_font_files) + : $body_font_files; + $this->cache_files = !empty($this->cache_files) + ? array_merge($this->cache_files, $body_font_files) + : $body_font_files; + } + } + } + + // Create fontawesome directories and add files to project data. + if (!empty(Project::$fontawesome)) { + // Create fontawesome directories. + $directories = [ + Project::$directory_path . '/fonts/fontawesome', + Project::$directory_path . '/fonts/fontawesome/css', + Project::$directory_path . '/fonts/fontawesome/webfonts', + ]; + foreach ($directories as $directory_path) { + if (!is_dir($directory_path)) { + echo CLI::$verbose ? "\nCreating {$directory_path} directory..." : ""; + mkdir($directory_path); + if (!is_dir($directory_path)) { + CLI::display_error( + "Failed to process font files: could not create {$directory_path} directory" + ); + exit; + } + echo CLI::$verbose ? "\nCreating {$directory_path} directory DONE" : ""; + } + } + + // Add fontawesome files to copy and cache arrays. + $fontawesome_files = [ + 'fonts/fontawesome/css/brands.min.css', + 'fonts/fontawesome/css/fontawesome.min.css', + 'fonts/fontawesome/css/solid.min.css', + 'fonts/fontawesome/webfonts/fa-brands-400.eot', + 'fonts/fontawesome/webfonts/fa-brands-400.svg', + 'fonts/fontawesome/webfonts/fa-brands-400.ttf', + 'fonts/fontawesome/webfonts/fa-brands-400.woff', + 'fonts/fontawesome/webfonts/fa-brands-400.woff2', + 'fonts/fontawesome/webfonts/fa-solid-900.eot', + 'fonts/fontawesome/webfonts/fa-solid-900.svg', + 'fonts/fontawesome/webfonts/fa-solid-900.ttf', + 'fonts/fontawesome/webfonts/fa-solid-900.woff', + 'fonts/fontawesome/webfonts/fa-solid-900.woff2' + ]; + + $this->copy_files = !empty($this->copy_files) + ? array_merge($this->copy_files, $fontawesome_files) + : $fontawesome_files; + $this->cache_files = !empty($this->cache_files) + ? array_merge($this->cache_files, $fontawesome_files) + : $fontawesome_files; + } + } + + /** + * Get sitemap URLs. + * + * Used in sitemap.xml file. + * + * @return string Sitemap URLs. + */ + private function get_sitemap_urls(): string { + + // Add any redirected URLs to sitemap URLs. + if (!empty(Project::$redirects) && is_array(Project::$redirects)) { + foreach (Project::$redirects as $index => $redirect) { + $url = trim($redirect['from'], '/'); + if ($url !== '' && $url !== 'index' && stripos($url, '.') !== false && stripos($url, '*') !== false) { + Project::$sitemap_urls[] = $url; + } + } + } + + $sitemap_urls = ''; + + // Add data for each URL. + if (!empty(Project::$sitemap_urls) && is_array(Project::$sitemap_urls)) { + foreach (Project::$sitemap_urls as $index => $url) { + $sitemap_urls .= str_ireplace( + [ + '{url}', + '{date}', + ], + [ + Project::$url . '/' . ($url !== 'index' ? $url : ''), + date('Y-m-d'), + ], + "\t\n" . + "\t\thttps://{url}\n" . + "\t\t{date}\n" . + "\t\n", + ); + } + } + + return $sitemap_urls; + } + + /** + * Get redirect URLs. + * + * Used in _redirects file. + * + * @return string Sitemap URLs. + */ + private function get_redirect_urls(): string { + + $redirect_urls = ''; + + if (!empty($this->compile_files) && is_array($this->compile_files)) { + foreach ($this->compile_files as $file) { + if (empty($file['target_file'])) { + continue; + } + + if ($file['page'] !== 'index') { + $redirect_urls .= str_ireplace('{title}', $file['page'], '/{title}/* /{title}.html 200') . "\n"; + } + } + } + + // If redirects are set, add to urls. + if (!empty(Project::$redirects) && is_array(Project::$redirects)) { + foreach (Project::$redirects as $index => $redirect_url) { + $redirect_urls .= "{$redirect_url['from']} {$redirect_url['to']}\n"; + } + } + + $redirect_urls = trim($redirect_urls, "\n"); + $redirect_urls .= "\n/* /index.html 200"; + + return $redirect_urls; + } + + /** + * Generate manifest.json. + * + * @return void + */ + private function generate_manifest(): void { + + if (empty(Project::$directory_path)) { + CLI::display_error("Failed to generate manifest: project directory path is empty."); + exit; + } + + if (!is_dir(Project::$directory_path)) { + CLI::display_error("Failed to generate manifest: project directory doesn't exist."); + exit; + } + + $manifest_file_path = Project::$directory_path . '/manifest.json'; + + if (!file_exists($manifest_file_path)) { + CLI::display_error("Failed to generate manifest: file {$manifest_file_path} does not exist."); + exit; + } + + $file_contents = file_get_contents($manifest_file_path); + + if (empty($file_contents)) { + CLI::display_error("Failed to generate manifest: manifest file contents is empty."); + exit; + } + + // Modify manifest data from project data. + $manifest_data = json_decode($file_contents, true); + + if (empty($manifest_data) || !is_array($manifest_data)) { + CLI::display_error("Failed to generate manifest: manifest file data is empty or is not an array."); + exit; + } + + echo CLI::$verbose ? "\n\tGenerating manifest.json" : ""; + + if (!empty(Project::$categories)) { + $manifest_data['categories'] = Project::$categories; + } + + if (!empty(Project::$screenshots) && is_array(Project::$screenshots)) { + $manifest_data['screenshots'] = []; + foreach (Project::$screenshots as $file) { + $manifest_data['screenshots'][] = [ + 'src' => $file, + 'sizes' => '1500x800', + 'type' => 'image/png' + ]; + } + } + + if (!empty(Project::$shortcuts)) { + $manifest_data['shortcuts'] = Project::$shortcuts; + } + + $manifest_data['related_applications'][] = [ + 'platform' => 'webapp', + 'url' => 'https://' . Project::$url . '/manifest.json', + ]; + + if (!empty(Project::$android_app_id) || !empty(Project::$apple_app_id)) { + $android_app_id = null; + if (!empty(Project::$android_app_id['paid'])) { + $android_app_id = Project::$android_app_id['paid']; + } elseif (!empty(Project::$android_app_id['free'])) { + $android_app_id = Project::$android_app_id['free']; + } + + $apple_app_id = null; + if (!empty(Project::$apple_app_id['paid'])) { + $apple_app_id = Project::$apple_app_id['paid']; + } elseif (!empty(Project::$apple_app_id['free'])) { + $apple_app_id = Project::$apple_app_id['free']; + } + + if (!empty($android_app_id) && ($android_app_id !== 'disabled')) { + $manifest_data['related_applications'][] = [ + 'platform' => 'play', + 'url' => "https://play.google.com/store/apps/details?id={$android_app_id}", + 'id' => $android_app_id + ]; + $manifest_data['prefer_related_applications'] = true; + } + + if (!empty($apple_app_id) && ($apple_app_id !== 'disabled')) { + $manifest_data['related_applications'][] = [ + 'platform' => 'itunes', + 'url' => "https://itunes.apple.com/app/example-app1/id123456789{$apple_app_id}", + ]; + $manifest_data['prefer_related_applications'] = true; + } + } + + // Replace file. + file_put_contents($manifest_file_path, json_encode($manifest_data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); + + echo CLI::$verbose ? "\n\tGenerating manifest.json DONE" : ""; + } + + /** + * Generate favicons. + * + * @return void + */ + private function generate_favicons(): void { + + if (empty(Project::$directory_path)) { + CLI::display_error("Failed to generate favicons: build directory path is empty."); + exit; + } + + if (!is_dir(Project::$directory_path)) { + CLI::display_error("Failed to generate favicons: build directory doesn't exist."); + exit; + } + + if (!file_exists(Project::$directory_path . '/img/favicon.svg')) { + CLI::display_error("Failed to generate favicons: favicon.svg is missing."); + exit; + } + + echo "Generating favicons..."; + + // Set up favicon config data. + $favicon_data_config = [ + 'masterPicture' => Project::$directory_path . '/img/favicon.svg', + 'iconsPath' => '/img/', + 'design' => + [ + 'ios' => + [ + 'pictureAspect' => 'backgroundAndMargin', + 'backgroundColor' => '#000000', + 'margin' => '18%', + 'assets' => + [ + 'ios6AndPriorIcons' => true, + 'ios7AndLaterIcons' => true, + 'precomposedIcons' => false, + 'declareOnlyDefaultIcon' => true, + ], + ], + 'desktopBrowser' => + [ + 'design' => 'raw', + ], + 'windows' => + [ + 'pictureAspect' => 'noChange', + 'backgroundColor' => '#000000', + 'onConflict' => 'override', + 'assets' => + [ + 'windows80Ie10Tile' => false, + 'windows10Ie11EdgeTiles' => + [ + 'small' => false, + 'medium' => true, + 'big' => false, + 'rectangle' => false, + ], + ], + ], + 'androidChrome' => + [ + 'pictureAspect' => 'noChange', + 'themeColor' => '#000000', + 'manifest' => + [ + 'display' => 'standalone', + 'orientation' => 'notSet', + 'onConflict' => 'override', + 'declared' => true, + ], + 'assets' => + [ + 'legacyIcon' => false, + 'lowResolutionIcons' => true, + ], + ], + 'safariPinnedTab' => + [ + 'pictureAspect' => 'blackAndWhite', + 'threshold' => 89.21875, + 'themeColor' => '#000000', + ], + ], + 'settings' => + [ + 'compression' => 2, + 'scalingAlgorithm' => 'Mitchell', + 'errorOnImageTooSmall' => false, + 'readmeFile' => false, + 'htmlCodeFile' => false, + 'usePathAsIs' => false, + ], + ]; + + // Create favicon data config file. + file_put_contents( + Project::$directory_path . '/favicon_config.json', + json_encode($favicon_data_config, JSON_PRETTY_PRINT) + ); + + // Generate favicons. + // phpcs:ignore Generic.Files.LineLength.TooLong + exec('npx real-favicon generate ' . Project::$directory_path . '/favicon_config.json favicon_data.json ' . Project::$directory_path . '/img/'); + + // Remove generated favicon data file. + exec('rm -rf favicon_data.json'); + + // Remove favicon data config file. + exec('rm -rf ' . Project::$directory_path . '/favicon_config.json'); + + // Remove favicon manifest file. + exec('rm -rf ' . Project::$directory_path . '/img/site.webmanifest'); + + echo CLI::$verbose ? "\nGenerating favicons DONE" : " - DONE\n"; + } +} diff --git a/inc/CLI.php b/inc/CLI.php index 357dca7..248d348 100644 --- a/inc/CLI.php +++ b/inc/CLI.php @@ -1,80 +1,80 @@ projects_data; - - if (empty($projects_data) || !is_array($projects_data)) { - $this->display_error("Failed to list projects: projects data is empty."); - exit; - } - - echo "Projects listed in project configuration file (projects.json):"; + $projects = new Projects(); + $projects_data = $projects->projects_data; - foreach ($projects_data as $project_data) { - echo "\n\e[96m- " . $project_data["url"] . "\e[0m: " . $project_data["title"]; - } + if (empty($projects_data) || !is_array($projects_data)) { + $this->display_error("Failed to list projects: projects data is empty."); + exit; + } - echo "\n"; - } + echo "Projects listed in project configuration file (projects.json):"; - /** - * Displays a color formatted error message on the command line. - * - * @param string $error Error message to display. - * @return void - */ - public static function display_error($error): void { - if (empty($error)) { - return; - } + foreach ($projects_data as $project_data) { + echo "\n\e[96m- " . $project_data["url"] . "\e[0m: " . $project_data["title"]; + } - echo "\n\e[31m ERROR: " . $error . "\e[0m\n"; - } -} \ No newline at end of file + echo "\n"; + } + + /** + * Displays a color formatted error message on the command line. + * + * @param string $error Error message to display. + * @return void + */ + public static function display_error($error): void { + if (empty($error)) { + return; + } + + echo "\n\e[31m ERROR: " . $error . "\e[0m\n"; + } +} diff --git a/inc/Project.php b/inc/Project.php index a5a7201..865a983 100644 --- a/inc/Project.php +++ b/inc/Project.php @@ -1,1079 +1,1083 @@ - */ - public static array $categories = []; - - /** - * Netlify ID. - * - * @var string - */ - public static string $netlify_id = ''; - - /** - * Google Tag Manager (GTM) ID. - * - * @var string - */ - public static string $gtm_id = ''; - - /** - * Facebook Pixel ID. - * - * @var string - */ - public static string $fbpixel_id = ''; - - /** - * Repixel ID. - * - * @var string - */ - public static string $repixel_id = ''; - - /** - * Google Ads (enabled/disabled). - * - * @var boolean - */ - public static bool $google_ads = false; - - /** - * Amazon Ads (enabled/disabled). - * - * @var boolean - */ - public static bool $amazon_ads = false; - - /** - * Other Ads (enabled/disabled). - * - * @var boolean - */ - public static bool $other_ads = false; - - /** - * Google API data. - * - * @var array{ - * google_api_client_id: string, - * google_api_client_key: string, - * google_api_key: string, - * google_api_scope: string, - * google_api_url: string, - * google_api_callback: string, - * } - */ - public static array $google_api = [ - 'google_api_client_id' => '', - 'google_api_client_key' => '', - 'google_api_key' => '', - 'google_api_scope' => '', - 'google_api_url' => '', - 'google_api_callback' => '', - ]; - - /** - * Fonts. - * - * @var array{ - * heading: string, - * body: string, - * } - */ - public static array $fonts = [ - 'heading' => '', - 'body' => '', - ]; - - /** - * Colors. - * - * @var array{ - * main: array{ - * normal: string, - * dark: string, - * }, - * accent: array{ - * normal: string, - * dark: string, - * }, - * } - */ - public static array $colors = [ - 'main' => [ - 'normal' => '', - 'dark' => '', - ], - 'accent' => [ - 'normal' => '', - 'dark' => '', - ] - ]; - - /** - * Fontawesome (enabled/disabled). - * - * @var boolean - */ - public static bool $fontawesome = false; - - /** - * Android App ID. - * - * @var array{ - * free: string, - * paid: string, - * } - */ - public static array $android_app_id = [ - 'free' => '', - 'paid' => '', - ]; - - /** - * Apple App ID. - * - * @var array{ - * free: string, - * paid: string, - * } - */ - public static array $apple_app_id = [ - 'free' => '', - 'paid' => '', - ]; - - /** - * Screenshots. - * - * @var array - */ - public static array $screenshots = []; - - /** - * Shortcuts. - * - * @var array - * }> - */ - public static array $shortcuts = [ - [ - 'name' => '', - 'description' => '', - 'url' => '', - 'icons' => [ - [ - 'src' => '', - 'sizes' => '', - ] - ], - ] - ]; - - /** - * Links. - * - * @var array - */ - public static array $links = [ - [ - 'url' => '', - 'title' => '', - ] - ]; - - /** - * Navigation. - * - * @var array{ - * image: string, - * items: array, - * } - */ - public static array $nav = [ - 'image' => '', - 'items' => [ - [ - 'url' => '', - 'title' => '', - ] - ], - ]; - - /** - * Header. - * - * @var array{ - * title: string, - * description: string, - * image: string, - * image_credit: string, - * } - */ - public static array $header = [ - 'title' => '', - 'description' => '', - 'image' => '', - 'image_credit' => '', - ]; - - /** - * Social. - * - * @var array{ - * mailto: string, - * facebook: string, - * twitter: string, - * github: string, - * patreon: string, - * paypal: string, - * portfolio: string, - * yelp: string, - * tripadvisor: string, - * custom: array, - * } - */ - public static array $social = [ - 'mailto' => '', - 'facebook' => '', - 'twitter' => '', - 'github' => '', - 'patreon' => '', - 'paypal' => '', - 'portfolio' => '', - 'yelp' => '', - 'tripadvisor' => '', - 'custom' => [ - [ - 'url' => '', - 'label' => '', - 'text' => '', - 'link' => '', - ] - ] - ]; - - /** - * Author. - * - * @var array{ - * name: string, - * twitter: string, - * } - */ - public static array $author = [ - 'name' => '', - 'twitter' => '', - ]; - - /** - * Redirects. - * - * @var array - */ - public static array $redirects = [ - [ - 'from' => '', - 'to' => '', - ] - ]; - - /** - * Cache files. - * - * @var array - */ - public static array $cache_files = []; - - /** - * Optional files. - * - * @var array - */ - public static array $opt_files = []; - - /** - * Sitemap URLs. - * - * @var array - */ - public static array $sitemap_urls = [ - 'index', - ]; - - /** - * Pages. - * - * @var array - */ - public static array $pages = [ - '' => [ - 'type' => '', - 'url' => '', - 'title' => '', - 'title_seo' => '', - 'description' => '', - 'image' => '', - 'image_credit' => '', - 'image_type' => '', - 'keywords' => '', - 'author' => [ - 'name' => '', - 'twitter' => '', - ], - 'date' => '', - 'date_full' => '', - 'content' => '', - ], - ]; - - /** - * Set project version. - * - * @param string $version Project version. - */ - public static function set_project_version(string $version): void { - self::$version = $version; - } - - /** - * Constructor. - * - * Sets up project data from projects.json. - * - * @param string $project Project name. - */ - public function __construct(string $project) { - if (empty($project)) { - CLI::display_error("Failed to construct project data: project is empty."); - exit; - } - - // Get project from projects.json. - $project_data = $this->get_project($project); - - // Set project directory. - self::$directory_path = dirname(__DIR__, 2) . '/' . $project; - - // Set all class variables from $project_data. - foreach ($project_data as $field => $value) { - self::${$field} = $value; - } - - // Set pages. - $this->set_pages(); - } - - /** - * Get project from projects.json. - * - * @param string $project Project name. - * @return array{ - * url: string, - * title: string, - * description: string, - * keywords: string, - * categories: array, - * netlify_id: string, - * gtm_id: string, - * fbpixel_id: string, - * repixel_id: string, - * google_ads: boolean, - * amazon_ads: boolean, - * other_ads: boolean, - * google_api: array{ - * google_api_client_id: string, - * google_api_client_key: string, - * google_api_key: string, - * google_api_scope: string, - * google_api_url: string, - * google_api_callback: string, - * }, - * fonts: array{ - * heading: string, - * body: string, - * }, - * colors: array{ - * main: array{ - * normal: string, - * dark: string, - * }, - * accent: array{ - * normal: string, - * dark: string, - * }, - * }, - * fontawesome: boolean, - * android_app_id: array{ - * free: string, - * paid: string, - * }, - * apple_app_id: array{ - * free: string, - * paid: string, - * }, - * screenshots: array, - * shortcuts: array - * }>, - * links: array, - * nav: array{ - * image: string, - * items: array, - * }, - * header: array{ - * title: string, - * description: string, - * image: string, - * image_credit: string, - * }, - * social: array{ - * mailto: string, - * facebook: string, - * twitter: string, - * github: string, - * patreon: string, - * paypal: string, - * portfolio: string, - * yelp: string, - * tripadvisor: string, - * custom: array, - * }, - * author: array{ - * name: string, - * twitter: string, - * }, - * redirects: array, - * cache_files: array, - * opt_files: array, - * sitemap_urls: array, - * pages: array - * } - */ - private function get_project(string $project): array { - if (empty($project)) { - CLI::display_error("Failed to get project: project is empty."); - exit; - } - - // Get all projects. - $projects = new Projects(); - - if (empty($projects->projects_data) || !is_array($projects->projects_data)) { - CLI::display_error("Failed to get project: projects data is empty or is not an array."); - exit; - } - - // Get data for current project. - $project_data = array_values( - array_filter( - $projects->projects_data, - fn($current_project_data) => $current_project_data['url'] === $project, - ) - ); - - if (empty($project_data[0])) { - CLI::display_error("Failed to get project: could not get project from projects.json file"); - exit; - } - - /** - * @var array{ - * url: string, - * title: string, - * description: string, - * keywords: string, - * categories: array, - * netlify_id: string, - * gtm_id: string, - * fbpixel_id: string, - * repixel_id: string, - * google_ads: boolean, - * amazon_ads: boolean, - * other_ads: boolean, - * google_api: array{ - * google_api_client_id: string, - * google_api_client_key: string, - * google_api_key: string, - * google_api_scope: string, - * google_api_url: string, - * google_api_callback: string, - * }, - * fonts: array{ - * heading: string, - * body: string, - * }, - * colors: array{ - * main: array{ - * normal: string, - * dark: string, - * }, - * accent: array{ - * normal: string, - * dark: string, - * }, - * }, - * fontawesome: boolean, - * android_app_id: array{ - * free: string, - * paid: string, - * }, - * apple_app_id: array{ - * free: string, - * paid: string, - * }, - * screenshots: array, - * shortcuts: array - * }>, - * links: array, - * nav: array{ - * image: string, - * items: array, - * }, - * header: array{ - * title: string, - * description: string, - * image: string, - * image_credit: string, - * }, - * social: array{ - * mailto: string, - * facebook: string, - * twitter: string, - * github: string, - * patreon: string, - * paypal: string, - * portfolio: string, - * yelp: string, - * tripadvisor: string, - * custom: array, - * }, - * author: array{ - * name: string, - * twitter: string, - * }, - * redirects: array, - * cache_files: array, - * opt_files: array, - * sitemap_urls: array, - * pages: array - * } - */ - return $project_data[0]; - } - - /** - * Set pages. - */ - private function set_pages(): void { - // Set up base pages for all projects. - self::$pages = array_merge( - self::$pages ?? [], - [ - 'index' => [ - 'type' => 'index', - 'url' => 'index', - 'title' => self::$title, - 'description' => self::$description, - ], - 'disclaimer' => [ - 'type' => 'template', - 'url' => 'disclaimer', - 'title' => 'Disclaimer', - 'description' => self::$title . ' disclaimer', - ], - 'privacy-policy' => [ - 'type' => 'template', - 'url' => 'privacy-policy', - 'title' => 'Privacy Policy', - 'description' => self::$title . ' privacy policy', - ], - 'terms-and-conditions' => [ - 'type' => 'template', - 'url' => 'terms-and-conditions', - 'title' => 'Terms and Conditions', - 'description' => self::$title . ' terms and conditions', - ] - ] - ); - - $this->set_data_file_pages(); - $this->set_posts_file_pages(); - $this->set_page_defaults(); - } - - /** - * Get page data. Ensures only certain array keys are included in page data. - * - * @param array{ - * url: string, - * title: string, - * title_seo: string, - * description: string, - * image: string, - * image_credit: string, - * image_type: string, - * keywords: string, - * author: array{ - * name: string, - * twitter: string, - * }, - * date: string, - * date_full: string, - * content: string, - * } $page Page data. - * @return array{ - * url: string, - * title: string, - * title_seo: string, - * description: string, - * image: string, - * image_credit: string, - * image_type: string, - * keywords: string, - * author: array{ - * name: string, - * twitter: string, - * }, - * date: string, - * date_full: string, - * content: string, - * } - */ - private function get_page_data(array $page): array { - return [ - 'url' => $page['url'] ?? '', - 'title' => $page['title'] ?? '', - 'title_seo' => $page['title_seo'] ?? '', - 'description' => $page['description'] ?? '', - 'image' => !empty($page['image']) && is_string($page['image']) ? 'img/' . $page['image'] : '', - 'image_credit' => $page['image_credit'] ?? '', - 'image_type' => $page['image_type'] ?? '', - 'keywords' => $page['keywords'] ?? '', - 'author' => $page['author'] ?? [], - 'date' => $page['date'] ?? '', - 'date_full' => $page['date_full'] ?? '', - 'content' => $page['content'] ?? '', - ]; - } - - /** - * Set data file pages. Get data from data.json and add pages to self::$pages. - */ - private function set_data_file_pages(): void { - if (empty(self::$directory_path)) { - CLI::display_error("Failed to set data file pages: project directory path is empty."); - exit; - } - - if (!is_dir(self::$directory_path)) { - CLI::display_error("Failed to set data file pages: project directory doesn't exist."); - exit; - } - - $data_file_path = self::$directory_path . '/data.json'; - - // Data file is optional. If it doesn't exist, do nothing. - if (!file_exists($data_file_path)) { - return; - } - - $search_result_pages = $this->get_json_file_data($data_file_path); - - if (empty($search_result_pages)) { - CLI::display_error("Failed to set posts file pages: posts file json is empty"); - exit; - } - - if (is_array($search_result_pages)) { - foreach ($search_result_pages as $page) { - if (empty($page['title'])) { - continue; - } - - /** - * In data.json, page titles can be an array, to allow for generating different URLs with the same content. - * Add each title to self::$pages so that a page will be generated for each. - */ - if (is_array($page['title'])) { - foreach ($page['title'] as $title) { - if (!empty($title)) { - - // If URL isn't provided, create it from the title. - $url = (string) ($page['url'] ?? Text::normalize_text($title, 'url')); - - $page_data = $this->get_page_data($page); - - self::$pages[$url] = [ - 'type' => 'search', - ...$page_data, - 'title' => $title, - ]; - } - } - } else if (is_string($page['title'])) { - // If URL isn't provided, create it from the title. - $url = (string) ($page['url'] ?? Text::normalize_text($page['title'], 'url')); - - $page_data = $this->get_page_data($page); - - self::$pages[$url] = [ - 'type' => 'search', - ...$page_data, - ]; - } - } - } - } - - /** - * Set posts file pages. Get data from posts.json and add pages to self::$pages. - */ - private function set_posts_file_pages(): void { - if (empty(self::$directory_path)) { - CLI::display_error("Failed to set posts file pages: project directory path is empty."); - exit; - } - - if (!is_dir(self::$directory_path)) { - CLI::display_error("Failed to set posts file pages: project directory doesn't exist."); - exit; - } - - $posts_file_path = self::$directory_path . '/posts.json'; - - // Posts file is optional. If it doesn't exist, do nothing. - if (!file_exists($posts_file_path)) { - return; - } - - $post_pages = $this->get_json_file_data($posts_file_path); - - if (empty($post_pages)) { - CLI::display_error("Failed to set posts file pages: posts file json is empty."); - exit; - } - - if (is_array($post_pages)) { - foreach ($post_pages as $page) { - if (empty($page['title']) || !is_string($page['title'])) { - continue; - } - - // If URL isn't provided, create it from the title. - $url = (string) ($page['url'] ?? Text::normalize_text($page['title'], 'url')); - - $page_data = $this->get_page_data($page); - - self::$pages[$url] = [ - 'type' => 'post', - ...$page_data, - ]; - } - } - } - - /** - * Get JSON file data. - * - * @param string $path JSON file path. - * @return array - */ - private function get_json_file_data(string $path): array { - if (empty($path)) { - CLI::display_error("Failed to get json file contents: path is empty."); - exit; - } - - $file_contents = file_get_contents($path); - - if (empty($file_contents)) { - CLI::display_error("Failed to get json file contents: file contents is empty."); - exit; - } - - $json = (array) json_decode($file_contents, true); - - if (empty($json)) { - CLI::display_error("Failed to get json file contents: json is empty."); - exit; - } - - return $json; - } - - /** - * Set page defaults. Set data values for each page that aren't already set. - */ - private function set_page_defaults(): void { - if (empty(self::$pages) || !is_array(self::$pages)) { - CLI::display_error("Failed to set page defaults: pages is empty or is not an array."); - exit; - } - - // Set defaults. - foreach (self::$pages as $url => $page) { - - // Type. - if (empty($page['type'])) { - $page['type'] = 'special'; - self::$pages[$url]['type'] = 'special'; - } - - // URL. - if (empty($page['url']) && !empty($page['title']) && is_string($page['title'])) { - self::$pages[$url]['url'] = !empty(self::$pages['*']['url']) - ? (string) str_ireplace('***TITLE***', Text::normalize_text($page['title'], 'url'), self::$pages['*']['url']) - : Text::normalize_text($page['title'], 'url'); - } - - // Title. - if (empty($page['title']) && !empty(self::$title)) { - self::$pages[$url]['title'] = !empty(self::$pages['*']['title']) - ? str_ireplace('***TITLE***', self::$title, self::$pages['*']['title']) - : self::$title; - } - - // SEO title. - if (empty($page['title_seo']) && !empty(self::$title)) { - if ($page['type'] === 'index') { - self::$pages[$url]['title_seo'] = self::$title; - } else { - if (!empty($page['title']) && is_string($page['title'])) { - self::$pages[$url]['title_seo'] = self::$title . " - {$page['title']}"; - } else { - self::$pages[$url]['title_seo'] = self::$title; - } - } - } - - // Description. - if (empty($page['description']) && !empty(self::$description) && $page['type'] !== 'template') { - self::$pages[$url]['description'] = self::$description; - } - - // Image and image credit. - if (empty($page['image']) && !empty(self::$header['image'])) { - self::$pages[$url]['image'] = self::$header['image']; - - if (!empty(self::$header['image_credit'])) { - self::$pages[$url]['image_credit'] = self::$header['image_credit']; - } - } - - // Image type. - if (!empty(self::$pages[$url]['image']) && is_string(self::$pages[$url]['image'])) { - self::$pages[$url]['image_type'] = stripos(self::$pages[$url]['image'], 'logo') !== false - ? 'logo' - : 'background'; - } - - // Keywords. - if (empty($page['keywords'])) { - - $project_title = Text::normalize_text(self::$title); - $page_title = is_string($page['title']) ? Text::normalize_text($page['title']) : ''; - - if (!empty($page['title']) && $project_title !== $page_title) { - - if ($page['type'] === 'template') { - self::$pages[$url]['keywords'] = "{$project_title} {$page_title}"; - } else if (!empty(self::$pages['*']['keywords'])) { - self::$pages[$url]['keywords'] = str_ireplace('***TITLE***', $page_title, self::$pages['*']['keywords']); - } else { - self::$pages[$url]['keywords'] = $page_title; - } - - } else if (!empty(self::$keywords)) { - self::$pages[$url]['keywords'] = self::$keywords; - } - } - - // Author. - if (empty($page['author']) && !empty(self::$author)) { - self::$pages[$url]['author'] = self::$author; - } - - // Date. - if (empty($page['date'])) { - self::$pages[$url]['date'] = date('Y-m-d'); - } else if (is_string($page['date'])) { - $timestamp = strtotime($page['date']); - - if (is_int($timestamp)) { - self::$pages[$url]['date_full'] = date('F j, Y', $timestamp); - } - } - - // Links. - if (empty($page['links']) && !empty(self::$links)) { - self::$pages[$url]['links'] = self::$links; - } - } - } + /** + * Directory path. + * + * @var string + */ + public static string $directory_path = ''; + + /** + * Version. + * + * @var string + */ + public static string $version = ''; + + /** + * URL. + * + * @var string + */ + public static string $url = ''; + + /** + * Title. + * + * @var string + */ + public static string $title = ''; + + /** + * Description. + * + * @var string + */ + public static string $description = ''; + + /** + * Keywords. + * + * @var string + */ + public static string $keywords = ''; + + /** + * Categories. + * + * @var array + */ + public static array $categories = []; + + /** + * Netlify ID. + * + * @var string + */ + public static string $netlify_id = ''; + + /** + * Google Tag Manager (GTM) ID. + * + * @var string + */ + public static string $gtm_id = ''; + + /** + * Facebook Pixel ID. + * + * @var string + */ + public static string $fbpixel_id = ''; + + /** + * Repixel ID. + * + * @var string + */ + public static string $repixel_id = ''; + + /** + * Google Ads (enabled/disabled). + * + * @var boolean + */ + public static bool $google_ads = false; + + /** + * Amazon Ads (enabled/disabled). + * + * @var boolean + */ + public static bool $amazon_ads = false; + + /** + * Other Ads (enabled/disabled). + * + * @var boolean + */ + public static bool $other_ads = false; + + /** + * Google API data. + * + * @var array{ + * google_api_client_id: string, + * google_api_client_key: string, + * google_api_key: string, + * google_api_scope: string, + * google_api_url: string, + * google_api_callback: string, + * } + */ + public static array $google_api = [ + 'google_api_client_id' => '', + 'google_api_client_key' => '', + 'google_api_key' => '', + 'google_api_scope' => '', + 'google_api_url' => '', + 'google_api_callback' => '', + ]; + + /** + * Fonts. + * + * @var array{ + * heading: string, + * body: string, + * } + */ + public static array $fonts = [ + 'heading' => '', + 'body' => '', + ]; + + /** + * Colors. + * + * @var array{ + * main: array{ + * normal: string, + * dark: string, + * }, + * accent: array{ + * normal: string, + * dark: string, + * }, + * } + */ + public static array $colors = [ + 'main' => [ + 'normal' => '', + 'dark' => '', + ], + 'accent' => [ + 'normal' => '', + 'dark' => '', + ] + ]; + + /** + * Fontawesome (enabled/disabled). + * + * @var boolean + */ + public static bool $fontawesome = false; + + /** + * Android App ID. + * + * @var array{ + * free: string, + * paid: string, + * } + */ + public static array $android_app_id = [ + 'free' => '', + 'paid' => '', + ]; + + /** + * Apple App ID. + * + * @var array{ + * free: string, + * paid: string, + * } + */ + public static array $apple_app_id = [ + 'free' => '', + 'paid' => '', + ]; + + /** + * Screenshots. + * + * @var array + */ + public static array $screenshots = []; + + /** + * Shortcuts. + * + * @var array + * }> + */ + public static array $shortcuts = [ + [ + 'name' => '', + 'description' => '', + 'url' => '', + 'icons' => [ + [ + 'src' => '', + 'sizes' => '', + ] + ], + ] + ]; + + /** + * Links. + * + * @var array + */ + public static array $links = [ + [ + 'url' => '', + 'title' => '', + ] + ]; + + /** + * Navigation. + * + * @var array{ + * image: string, + * items: array, + * } + */ + public static array $nav = [ + 'image' => '', + 'items' => [ + [ + 'url' => '', + 'title' => '', + ] + ], + ]; + + /** + * Header. + * + * @var array{ + * title: string, + * description: string, + * image: string, + * image_credit: string, + * } + */ + public static array $header = [ + 'title' => '', + 'description' => '', + 'image' => '', + 'image_credit' => '', + ]; + + /** + * Social. + * + * @var array{ + * mailto: string, + * facebook: string, + * twitter: string, + * github: string, + * patreon: string, + * paypal: string, + * portfolio: string, + * yelp: string, + * tripadvisor: string, + * custom: array, + * } + */ + public static array $social = [ + 'mailto' => '', + 'facebook' => '', + 'twitter' => '', + 'github' => '', + 'patreon' => '', + 'paypal' => '', + 'portfolio' => '', + 'yelp' => '', + 'tripadvisor' => '', + 'custom' => [ + [ + 'url' => '', + 'label' => '', + 'text' => '', + 'link' => '', + ] + ] + ]; + + /** + * Author. + * + * @var array{ + * name: string, + * twitter: string, + * } + */ + public static array $author = [ + 'name' => '', + 'twitter' => '', + ]; + + /** + * Redirects. + * + * @var array + */ + public static array $redirects = [ + [ + 'from' => '', + 'to' => '', + ] + ]; + + /** + * Cache files. + * + * @var array + */ + public static array $cache_files = []; + + /** + * Optional files. + * + * @var array + */ + public static array $opt_files = []; + + /** + * Sitemap URLs. + * + * @var array + */ + public static array $sitemap_urls = [ + 'index', + ]; + + /** + * Pages. + * + * @var array + */ + public static array $pages = [ + '' => [ + 'type' => '', + 'url' => '', + 'title' => '', + 'title_seo' => '', + 'description' => '', + 'image' => '', + 'image_credit' => '', + 'image_type' => '', + 'keywords' => '', + 'author' => [ + 'name' => '', + 'twitter' => '', + ], + 'date' => '', + 'date_full' => '', + 'content' => '', + ], + ]; + + /** + * Set project version. + * + * @param string $version Project version. + */ + public static function set_project_version(string $version): void { + self::$version = $version; + } + + /** + * Constructor. + * + * Sets up project data from projects.json. + * + * @param string $project Project name. + */ + public function __construct(string $project) { + if (empty($project)) { + CLI::display_error("Failed to construct project data: project is empty."); + exit; + } + + // Get project from projects.json. + $project_data = $this->get_project($project); + + // Set project directory. + self::$directory_path = dirname(__DIR__, 2) . '/' . $project; + + // Set all class variables from $project_data. + foreach ($project_data as $field => $value) { + self::${$field} = $value; + } + + // Set pages. + $this->set_pages(); + } + + /** + * Get project from projects.json. + * + * @param string $project Project name. + * @return array{ + * url: string, + * title: string, + * description: string, + * keywords: string, + * categories: array, + * netlify_id: string, + * gtm_id: string, + * fbpixel_id: string, + * repixel_id: string, + * google_ads: boolean, + * amazon_ads: boolean, + * other_ads: boolean, + * google_api: array{ + * google_api_client_id: string, + * google_api_client_key: string, + * google_api_key: string, + * google_api_scope: string, + * google_api_url: string, + * google_api_callback: string, + * }, + * fonts: array{ + * heading: string, + * body: string, + * }, + * colors: array{ + * main: array{ + * normal: string, + * dark: string, + * }, + * accent: array{ + * normal: string, + * dark: string, + * }, + * }, + * fontawesome: boolean, + * android_app_id: array{ + * free: string, + * paid: string, + * }, + * apple_app_id: array{ + * free: string, + * paid: string, + * }, + * screenshots: array, + * shortcuts: array + * }>, + * links: array, + * nav: array{ + * image: string, + * items: array, + * }, + * header: array{ + * title: string, + * description: string, + * image: string, + * image_credit: string, + * }, + * social: array{ + * mailto: string, + * facebook: string, + * twitter: string, + * github: string, + * patreon: string, + * paypal: string, + * portfolio: string, + * yelp: string, + * tripadvisor: string, + * custom: array, + * }, + * author: array{ + * name: string, + * twitter: string, + * }, + * redirects: array, + * cache_files: array, + * opt_files: array, + * sitemap_urls: array, + * pages: array + * } + */ + private function get_project(string $project): array { + if (empty($project)) { + CLI::display_error("Failed to get project: project is empty."); + exit; + } + + // Get all projects. + $projects = new Projects(); + + if (empty($projects->projects_data) || !is_array($projects->projects_data)) { + CLI::display_error("Failed to get project: projects data is empty or is not an array."); + exit; + } + + // Get data for current project. + $project_data = array_values( + array_filter( + $projects->projects_data, + fn($current_project_data) => $current_project_data['url'] === $project, + ) + ); + + if (empty($project_data[0])) { + CLI::display_error("Failed to get project: could not get project from projects.json file"); + exit; + } + + /** + * @var array{ + * url: string, + * title: string, + * description: string, + * keywords: string, + * categories: array, + * netlify_id: string, + * gtm_id: string, + * fbpixel_id: string, + * repixel_id: string, + * google_ads: boolean, + * amazon_ads: boolean, + * other_ads: boolean, + * google_api: array{ + * google_api_client_id: string, + * google_api_client_key: string, + * google_api_key: string, + * google_api_scope: string, + * google_api_url: string, + * google_api_callback: string, + * }, + * fonts: array{ + * heading: string, + * body: string, + * }, + * colors: array{ + * main: array{ + * normal: string, + * dark: string, + * }, + * accent: array{ + * normal: string, + * dark: string, + * }, + * }, + * fontawesome: boolean, + * android_app_id: array{ + * free: string, + * paid: string, + * }, + * apple_app_id: array{ + * free: string, + * paid: string, + * }, + * screenshots: array, + * shortcuts: array + * }>, + * links: array, + * nav: array{ + * image: string, + * items: array, + * }, + * header: array{ + * title: string, + * description: string, + * image: string, + * image_credit: string, + * }, + * social: array{ + * mailto: string, + * facebook: string, + * twitter: string, + * github: string, + * patreon: string, + * paypal: string, + * portfolio: string, + * yelp: string, + * tripadvisor: string, + * custom: array, + * }, + * author: array{ + * name: string, + * twitter: string, + * }, + * redirects: array, + * cache_files: array, + * opt_files: array, + * sitemap_urls: array, + * pages: array + * } + */ + return $project_data[0]; + } + + /** + * Set pages. + */ + private function set_pages(): void { + // Set up base pages for all projects. + self::$pages = array_merge( + self::$pages ?? [], + [ + 'index' => [ + 'type' => 'index', + 'url' => 'index', + 'title' => self::$title, + 'description' => self::$description, + ], + 'disclaimer' => [ + 'type' => 'template', + 'url' => 'disclaimer', + 'title' => 'Disclaimer', + 'description' => self::$title . ' disclaimer', + ], + 'privacy-policy' => [ + 'type' => 'template', + 'url' => 'privacy-policy', + 'title' => 'Privacy Policy', + 'description' => self::$title . ' privacy policy', + ], + 'terms-and-conditions' => [ + 'type' => 'template', + 'url' => 'terms-and-conditions', + 'title' => 'Terms and Conditions', + 'description' => self::$title . ' terms and conditions', + ] + ] + ); + + $this->set_data_file_pages(); + $this->set_posts_file_pages(); + $this->set_page_defaults(); + } + + /** + * Get page data. Ensures only certain array keys are included in page data. + * + * @param array{ + * url: string, + * title: string, + * title_seo: string, + * description: string, + * image: string, + * image_credit: string, + * image_type: string, + * keywords: string, + * author: array{ + * name: string, + * twitter: string, + * }, + * date: string, + * date_full: string, + * content: string, + * } $page Page data. + * @return array{ + * url: string, + * title: string, + * title_seo: string, + * description: string, + * image: string, + * image_credit: string, + * image_type: string, + * keywords: string, + * author: array{ + * name: string, + * twitter: string, + * }, + * date: string, + * date_full: string, + * content: string, + * } + */ + private function get_page_data(array $page): array { + return [ + 'url' => $page['url'] ?? '', + 'title' => $page['title'] ?? '', + 'title_seo' => $page['title_seo'] ?? '', + 'description' => $page['description'] ?? '', + 'image' => !empty($page['image']) && is_string($page['image']) ? 'img/' . $page['image'] : '', + 'image_credit' => $page['image_credit'] ?? '', + 'image_type' => $page['image_type'] ?? '', + 'keywords' => $page['keywords'] ?? '', + 'author' => $page['author'] ?? [], + 'date' => $page['date'] ?? '', + 'date_full' => $page['date_full'] ?? '', + 'content' => $page['content'] ?? '', + ]; + } + + /** + * Set data file pages. Get data from data.json and add pages to self::$pages. + */ + private function set_data_file_pages(): void { + if (empty(self::$directory_path)) { + CLI::display_error("Failed to set data file pages: project directory path is empty."); + exit; + } + + if (!is_dir(self::$directory_path)) { + CLI::display_error("Failed to set data file pages: project directory doesn't exist."); + exit; + } + + $data_file_path = self::$directory_path . '/data.json'; + + // Data file is optional. If it doesn't exist, do nothing. + if (!file_exists($data_file_path)) { + return; + } + + $search_result_pages = $this->get_json_file_data($data_file_path); + + if (empty($search_result_pages)) { + CLI::display_error("Failed to set posts file pages: posts file json is empty"); + exit; + } + + if (is_array($search_result_pages)) { + foreach ($search_result_pages as $page) { + if (empty($page['title'])) { + continue; + } + + /** + * In data.json, page titles can be an array to generate different URLs with the same content. + * Add each title to self::$pages so that a page will be generated for each. + */ + if (is_array($page['title'])) { + foreach ($page['title'] as $title) { + if (!empty($title)) { + // If URL isn't provided, create it from the title. + $url = (string) ($page['url'] ?? Text::normalize_text($title, 'url')); + + $page_data = $this->get_page_data($page); + + self::$pages[$url] = [ + 'type' => 'search', + ...$page_data, + 'title' => $title, + ]; + } + } + } elseif (is_string($page['title'])) { + // If URL isn't provided, create it from the title. + $url = (string) ($page['url'] ?? Text::normalize_text($page['title'], 'url')); + + $page_data = $this->get_page_data($page); + + self::$pages[$url] = [ + 'type' => 'search', + ...$page_data, + ]; + } + } + } + } + + /** + * Set posts file pages. Get data from posts.json and add pages to self::$pages. + */ + private function set_posts_file_pages(): void { + if (empty(self::$directory_path)) { + CLI::display_error("Failed to set posts file pages: project directory path is empty."); + exit; + } + + if (!is_dir(self::$directory_path)) { + CLI::display_error("Failed to set posts file pages: project directory doesn't exist."); + exit; + } + + $posts_file_path = self::$directory_path . '/posts.json'; + + // Posts file is optional. If it doesn't exist, do nothing. + if (!file_exists($posts_file_path)) { + return; + } + + $post_pages = $this->get_json_file_data($posts_file_path); + + if (empty($post_pages)) { + CLI::display_error("Failed to set posts file pages: posts file json is empty."); + exit; + } + + if (is_array($post_pages)) { + foreach ($post_pages as $page) { + if (empty($page['title']) || !is_string($page['title'])) { + continue; + } + + // If URL isn't provided, create it from the title. + $url = (string) ($page['url'] ?? Text::normalize_text($page['title'], 'url')); + + $page_data = $this->get_page_data($page); + + self::$pages[$url] = [ + 'type' => 'post', + ...$page_data, + ]; + } + } + } + + /** + * Get JSON file data. + * + * @param string $path JSON file path. + * @return array + */ + private function get_json_file_data(string $path): array { + if (empty($path)) { + CLI::display_error("Failed to get json file contents: path is empty."); + exit; + } + + $file_contents = file_get_contents($path); + + if (empty($file_contents)) { + CLI::display_error("Failed to get json file contents: file contents is empty."); + exit; + } + + $json = (array) json_decode($file_contents, true); + + if (empty($json)) { + CLI::display_error("Failed to get json file contents: json is empty."); + exit; + } + + return $json; + } + + /** + * Set page defaults. Set data values for each page that aren't already set. + */ + private function set_page_defaults(): void { + if (empty(self::$pages) || !is_array(self::$pages)) { + CLI::display_error("Failed to set page defaults: pages is empty or is not an array."); + exit; + } + + // Set defaults. + foreach (self::$pages as $url => $page) { + // Type. + if (empty($page['type'])) { + $page['type'] = 'special'; + self::$pages[$url]['type'] = 'special'; + } + + // URL. + if (empty($page['url']) && !empty($page['title']) && is_string($page['title'])) { + self::$pages[$url]['url'] = !empty(self::$pages['*']['url']) + ? (string) str_ireplace( + '***TITLE***', + Text::normalize_text($page['title'], 'url'), + self::$pages['*']['url'] + ) + : Text::normalize_text($page['title'], 'url'); + } + + // Title. + if (empty($page['title']) && !empty(self::$title)) { + self::$pages[$url]['title'] = !empty(self::$pages['*']['title']) + ? str_ireplace('***TITLE***', self::$title, self::$pages['*']['title']) + : self::$title; + } + + // SEO title. + if (empty($page['title_seo']) && !empty(self::$title)) { + if ($page['type'] === 'index') { + self::$pages[$url]['title_seo'] = self::$title; + } else { + if (!empty($page['title']) && is_string($page['title'])) { + self::$pages[$url]['title_seo'] = self::$title . " - {$page['title']}"; + } else { + self::$pages[$url]['title_seo'] = self::$title; + } + } + } + + // Description. + if (empty($page['description']) && !empty(self::$description) && $page['type'] !== 'template') { + self::$pages[$url]['description'] = self::$description; + } + + // Image and image credit. + if (empty($page['image']) && !empty(self::$header['image'])) { + self::$pages[$url]['image'] = self::$header['image']; + + if (!empty(self::$header['image_credit'])) { + self::$pages[$url]['image_credit'] = self::$header['image_credit']; + } + } + + // Image type. + if (!empty(self::$pages[$url]['image']) && is_string(self::$pages[$url]['image'])) { + self::$pages[$url]['image_type'] = stripos(self::$pages[$url]['image'], 'logo') !== false + ? 'logo' + : 'background'; + } + + // Keywords. + if (empty($page['keywords'])) { + $project_title = Text::normalize_text(self::$title); + $page_title = is_string($page['title']) ? Text::normalize_text($page['title']) : ''; + + if (!empty($page['title']) && $project_title !== $page_title) { + if ($page['type'] === 'template') { + self::$pages[$url]['keywords'] = "{$project_title} {$page_title}"; + } elseif (!empty(self::$pages['*']['keywords'])) { + self::$pages[$url]['keywords'] = str_ireplace( + '***TITLE***', + $page_title, + self::$pages['*']['keywords'] + ); + } else { + self::$pages[$url]['keywords'] = $page_title; + } + } elseif (!empty(self::$keywords)) { + self::$pages[$url]['keywords'] = self::$keywords; + } + } + + // Author. + if (empty($page['author']) && !empty(self::$author)) { + self::$pages[$url]['author'] = self::$author; + } + + // Date. + if (empty($page['date'])) { + self::$pages[$url]['date'] = date('Y-m-d'); + } elseif (is_string($page['date'])) { + $timestamp = strtotime($page['date']); + + if (is_int($timestamp)) { + self::$pages[$url]['date_full'] = date('F j, Y', $timestamp); + } + } + + // Links. + if (empty($page['links']) && !empty(self::$links)) { + self::$pages[$url]['links'] = self::$links; + } + } + } } diff --git a/inc/Projects.php b/inc/Projects.php index 0fe0d56..8b945d3 100644 --- a/inc/Projects.php +++ b/inc/Projects.php @@ -1,51 +1,113 @@ , + * netlify_id: string, + * gtm_id: string, + * fbpixel_id: string, + * repixel_id: string, + * google_ads: boolean, + * amazon_ads: boolean, + * other_ads: boolean, + * google_api: array, + * fonts: array, + * colors: array>, + * fontawesome: boolean, + * android_app_id: array, + * apple_app_id: array, + * screenshots: array, + * shortcuts: array>>>, + * links: array>, + * nav: array>>, + * header: array, + * social: array>>, + * author: array, + * redirects: array>, + * cache_files: array, + * opt_files: array, + * sitemap_urls: array, + * pages: array|string>> + * }[] + */ + public array $projects_data = []; - /** - * Projects data from projects.json. - * - * @var array{url: string, title: string, description: string, keywords: string, categories: array, netlify_id: string, gtm_id: string, fbpixel_id: string, repixel_id: string, google_ads: boolean, amazon_ads: boolean, other_ads: boolean, google_api: array, fonts: array, colors: array>, fontawesome: boolean, android_app_id: array, apple_app_id: array, screenshots: array, shortcuts: array>>>, links: array>, nav: array>>, header: array, social: array>>, author: array, redirects: array>, cache_files: array, opt_files: array, sitemap_urls: array, pages: array|string>>}[] - */ - public array $projects_data = []; - - /** - * Constructor. - * - * Sets projects data. - */ - public function __construct() { - $this->projects_data = $this->get_projects(); - } - - /** - * Gets projects data from projects.json file contents. - * - * @return array{url: string, title: string, description: string, keywords: string, categories: array, netlify_id: string, gtm_id: string, fbpixel_id: string, repixel_id: string, google_ads: boolean, amazon_ads: boolean, other_ads: boolean, google_api: array, fonts: array, colors: array>, fontawesome: boolean, android_app_id: array, apple_app_id: array, screenshots: array, shortcuts: array>>>, links: array>, nav: array>>, header: array, social: array>>, author: array, redirects: array>, cache_files: array, opt_files: array, sitemap_urls: array, pages: array|string>>}[] Array of projects from projects.json. - */ - private function get_projects(): array { - - $projects_file_path = dirname(__FILE__, 2) . '/projects.json'; - - if (!file_exists($projects_file_path)) { - CLI::display_error("Failed to get projects data from projects.json: projects.json is missing."); - exit; - } - - $projects_data_json = file_get_contents($projects_file_path, true); - - if (empty($projects_data_json)) { - CLI::display_error("Failed to get projects data from projects.json: projects.json file contents is empty."); - exit; - } - - $projects_data = json_decode($projects_data_json, true); - - return is_array($projects_data) ? $projects_data : []; - } -} \ No newline at end of file + /** + * Constructor. + * + * Sets projects data. + */ + public function __construct() { + $this->projects_data = $this->get_projects(); + } + + /** + * Gets projects data from projects.json file contents. + * + * @return array{ + * url: string, + * title: string, + * description: string, + * keywords: string, + * categories: array, + * netlify_id: string, + * gtm_id: string, + * fbpixel_id: string, + * repixel_id: string, + * google_ads: boolean, + * amazon_ads: boolean, + * other_ads: boolean, + * google_api: array, + * fonts: array, + * colors: array>, + * fontawesome: boolean, + * android_app_id: array, + * apple_app_id: array, + * screenshots: array, + * shortcuts: array>>>, + * links: array>, + * nav: array>>, + * header: array, + * social: array>>, + * author: array, + * redirects: array>, + * cache_files: array, + * opt_files: array, + * sitemap_urls: array, + * pages: array|string>> + * }[] + * Array of projects from projects.json. + */ + private function get_projects(): array { + + $projects_file_path = dirname(__FILE__, 2) . '/projects.json'; + + if (!file_exists($projects_file_path)) { + CLI::display_error("Failed to get projects data from projects.json: projects.json is missing."); + exit; + } + + $projects_data_json = file_get_contents($projects_file_path, true); + + if (empty($projects_data_json)) { + CLI::display_error("Failed to get projects data from projects.json: projects.json file contents is empty."); + exit; + } + + $projects_data = json_decode($projects_data_json, true); + + return is_array($projects_data) ? $projects_data : []; + } +} diff --git a/inc/Text.php b/inc/Text.php index a063d04..7226a3e 100644 --- a/inc/Text.php +++ b/inc/Text.php @@ -1,83 +1,92 @@ + + + + + + + + + *.js + *.css + */files/* + */node_modules/* + */tests/* + */vendor/* + + + + + + + + + + \ No newline at end of file diff --git a/pwagenerator.php b/pwagenerator.php index a5500b6..2c46876 100755 --- a/pwagenerator.php +++ b/pwagenerator.php @@ -1,54 +1,54 @@ #!/usr/local/bin/php - * @link https://github.com/nikkifurls/pwagenerator - * + * * Usage: * ./pwagenerator.php projects List all projects configured for building in projects.json - * ./pwagenerator.php [project] [option] Build, deploy, and/or generate icons for a project using project configuration in projects.json - * + * ./pwagenerator.php [project] [option] Build, deploy, and/or generate icons for a project using project + * configuration in projects.json + * * Options: * -v verbose * -b build * -i generate icons * -d deploy - * + * * Example: * ./pwagenerator exampleproject.com -b - * + * * To create a new project, add it to projects.json, then build it. */ -require_once('inc/Build.php'); -require_once('inc/CLI.php'); +require 'vendor/autoload.php'; // Only run via CLI. if ('cli' !== php_sapi_name()) { - exit; + exit; } -$cli = new CLI(); +$cli = new PWA_Generator\CLI(); // Display usage if insufficient arguments provided. if (empty($argv) || count($argv) <= 1) { - $cli->show_usage(); - exit; + $cli->show_usage(); + exit; } // List projects. if (in_array('projects', $argv, true)) { - $cli->list_projects(); - exit; + $cli->list_projects(); + exit; } // Set verbose mode. if (in_array('-v', $argv, true)) { - $cli->set_verbose(true); + $cli->set_verbose(true); } $project = ''; @@ -57,26 +57,26 @@ $deploy = false; if (!empty($argv[1])) { - $project = $argv[1]; + $project = $argv[1]; } if (in_array('-i', $argv, true)) { - $generate_favicons = true; + $generate_favicons = true; } if (in_array('-b', $argv, true)) { - $build = true; + $build = true; } if (in_array('-d', $argv, true)) { - $deploy = true; + $deploy = true; } -$build = new Build( - $project, - [ - 'generate_favicons' => $generate_favicons, - 'build' => $build, - 'deploy' => $deploy, - ] -); \ No newline at end of file +$build = new PWA_Generator\Build( + $project, + [ + 'generate_favicons' => $generate_favicons, + 'build' => $build, + 'deploy' => $deploy, + ] +); diff --git a/template-parts/footer.php b/template-parts/footer.php index 9806ae8..8694779 100644 --- a/template-parts/footer.php +++ b/template-parts/footer.php @@ -1,56 +1,64 @@ - +
-
- -

- Hi! I'm ' title='Portfolio'> 👩‍💻. I created ! If you find it useful, please . - - - Support me directly on - - ' title='Patreon'>Patreon - - - or - - - ' title='Paypal'>Paypal. - - Your support is greatly appreciated!  - -

- - - - I participate in the Amazon Associates Program, an affiliate advertising program designed to provide a means for websites to earn advertising fees by advertising and linking to Amazon.com. Some links on this website may be affiliate links, and I may get some commission if you buy something or take an action after clicking a link on this website. Of course, I only link to products that I know and love enough to endorse. - - - -

- - -

Built with PWA Generator

- - - - -
+
+ +

+ Hi! I'm ' title='Portfolio'> 👩‍💻. I created ! If you find it useful, please . + + + Support me directly on + + ' title='Patreon'>Patreon + + + or + + + ' title='Paypal'>Paypal. + + Your support is greatly appreciated!  + +

+ + + + I participate in the Amazon Associates Program, an affiliate advertising program designed to provide a means for websites to earn advertising fees by advertising and linking to Amazon.com. Some links on this website may be affiliate links, and I may get some commission if you buy something or take an action after clicking a link on this website. Of course, I only link to products that I know and love enough to endorse. + + + +

+ + +

Built with PWA Generator

+ + + + +
\ No newline at end of file diff --git a/template-parts/head.php b/template-parts/head.php index b56646c..77c6632 100644 --- a/template-parts/head.php +++ b/template-parts/head.php @@ -1,7 +1,9 @@ @@ -11,93 +13,85 @@ - - - - - + + + + - - - - - + + + - - - - - - - + + + + + - - - - - - + + + + - - <?php echo $project_data['pages'][$page]['title_seo'] ?> + + <?php echo $project_data['pages'][$page]['title_seo'] ?> - - /'> + + /'> - - '> + + '> - - '> + + '> @@ -111,66 +105,66 @@ function gtag(){dataLayer.push(arguments);} - - '/> + + '/> - - '> + + '> - - '> + + '> - - /'> + + /'> - - '> + + '> - - /'> + + /'> - - /'> - - - /img/share.jpg'> - + + /'> + + + /img/share.jpg'> + - - '> + + '> - - '> + + '> - - '> + + '> - - /'> - - - /img/share.jpg'> - + + /'> + + + /img/share.jpg'> + - - '> + + '> '; if (!empty($project_data['fonts'])) { - - $heading_font = $project_data['fonts']['heading'] ?? null; - $heading_font_decoded = strtolower(str_ireplace(' ', '-', $heading_font)); - $body_font = $project_data['fonts']['body'] ?? null; - $body_font_decoded = strtolower(str_ireplace(' ', '-', $body_font)); - - $fonts = [ - [ - $heading_font, - $heading_font_decoded - ], - [ - $body_font, - $body_font_decoded - ], - ]; - $font_weights = [ - 'regular', - '700', - ]; - - foreach ($font_weights as $font_weight) { - foreach ($fonts as $font) { - $font_url = 'fonts/' . $font[1] . '/' . $font[1] . '-' . $font_weight; - $font_url_eot = $font_url . '.eot'; - $font_url_woff = $font_url . '.woff'; - $font_url_woff2 = $font_url . '.woff2'; - $font_url_ttf = $font_url . '.ttf'; - $font_url_svg = $font_url . '.svg'; - if (file_exists(dirname(__DIR__, 2) . '/' . $project_data['url'] . '/' . $font_url_eot) && - file_exists(dirname(__DIR__, 2) . '/' . $project_data['url'] . '/' . $font_url_woff) && - file_exists(dirname(__DIR__, 2) . '/' . $project_data['url'] . '/' . $font_url_woff2) && - file_exists(dirname(__DIR__, 2) . '/' . $project_data['url'] . '/' . $font_url_ttf) && - file_exists(dirname(__DIR__, 2) . '/' . $project_data['url'] . '/' . $font_url_svg)) { - echo " + $heading_font = $project_data['fonts']['heading'] ?? null; + $heading_font_decoded = strtolower(str_ireplace(' ', '-', $heading_font)); + $body_font = $project_data['fonts']['body'] ?? null; + $body_font_decoded = strtolower(str_ireplace(' ', '-', $body_font)); + + $fonts = [ + [ + $heading_font, + $heading_font_decoded + ], + [ + $body_font, + $body_font_decoded + ], + ]; + $font_weights = [ + 'regular', + '700', + ]; + + foreach ($font_weights as $font_weight) { + foreach ($fonts as $font) { + $font_url = 'fonts/' . $font[1] . '/' . $font[1] . '-' . $font_weight; + $font_url_eot = $font_url . '.eot'; + $font_url_woff = $font_url . '.woff'; + $font_url_woff2 = $font_url . '.woff2'; + $font_url_ttf = $font_url . '.ttf'; + $font_url_svg = $font_url . '.svg'; + if ( + file_exists(dirname(__DIR__, 2) . '/' . $project_data['url'] . '/' . $font_url_eot) && + file_exists(dirname(__DIR__, 2) . '/' . $project_data['url'] . '/' . $font_url_woff) && + file_exists(dirname(__DIR__, 2) . '/' . $project_data['url'] . '/' . $font_url_woff2) && + file_exists(dirname(__DIR__, 2) . '/' . $project_data['url'] . '/' . $font_url_ttf) && + file_exists(dirname(__DIR__, 2) . '/' . $project_data['url'] . '/' . $font_url_svg) + ) { + echo " @font-face { font-family: " . $font[0] . "; font-style: normal; @@ -227,115 +222,128 @@ function gtag(){dataLayer.push(arguments);} url('" . $font_url_svg . "#" . str_ireplace(" ", "-", $font[0]) . "') format('svg'); } "; - } - } - } + } + } + } } echo ''; ?> - - - + + + - - + + - - + + - - '> + + '> - - + + - - - + + + - - - - - - + + + onreadystatechange="if (this.readyState === 'complete') this.onload()"> - - - - + + + + - - - - + + + + \ No newline at end of file diff --git a/template-parts/header.php b/template-parts/header.php index 5fef362..c6a3167 100644 --- a/template-parts/header.php +++ b/template-parts/header.php @@ -1,47 +1,49 @@
> -
- - - -
- -
-

-

-
- -
-

-

-
- -
-

-

-
- - - -
- - - - - - - - - -
-
+
+ + + +
+ +
+

+

+
+ +
+

+

+
+ +
+

+

+
+ + + +
+ + + + + + + + + +
+
\ No newline at end of file diff --git a/template-parts/nav.php b/template-parts/nav.php index dfce754..82eb63c 100644 --- a/template-parts/nav.php +++ b/template-parts/nav.php @@ -1,31 +1,31 @@
-
- -
+
+ +
\ No newline at end of file diff --git a/template-parts/social.php b/template-parts/social.php index 5a4a8d2..8ffe8e6 100644 --- a/template-parts/social.php +++ b/template-parts/social.php @@ -1,51 +1,53 @@ \ No newline at end of file + + ' title='Email' class='icon icon-email'> + + + + ' title='Facebook' class='icon icon-facebook'> + + + + ' title='Twitter' class='icon icon-twitter'> + + + + ' title='GitHub' class='icon icon-github'> + + + + ' title='Paypal' class='icon icon-paypal'> + + + + ' title='Patreon' class='icon icon-patreon'> + + + + ' title='Yelp' class='icon icon-yelp'> + + + + .html' title='Tripadvisor' class='icon icon-tripadvisor'> + + + + + + + ' class='custom icon' > + + + + \ No newline at end of file diff --git a/templates/disclaimer.php b/templates/disclaimer.php index 8d1f721..82721c9 100644 --- a/templates/disclaimer.php +++ b/templates/disclaimer.php @@ -1,66 +1,72 @@ + +

The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural.

For the purposes of this Disclaimer:

    -
  • Website refers to ***URL***.
  • -
  • Service refers to the Website.
  • +
  • Website refers to ***URL***.
  • +
  • Service refers to the Website.
  • Company (referred to as either "the Company", "We", "Us" or "Our" in this policy) refers to ***TITLE***.
  • -
  • You refers to the individual accessing the Website, Service, Company, or other legal entity.
  • +
  • You refers to the individual accessing the Website, Service, Company, or other legal entity.
Disclaimer -

The information contained on the Service is for general information purposes only.

-

The Company assumes no responsibility for errors or omissions in the contents of the Service.

-

In no event shall the Company 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. The Company reserves the right to make additions, deletions, or modifications to the contents on the Service at any time without prior notice.

+

The information contained on the Service is for general information purposes only.

+

The Company assumes no responsibility for errors or omissions in the contents of the Service.

+

In no event shall the Company 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. The Company reserves the right to make additions, deletions, or modifications to the contents on the Service at any time without prior notice.

External Links Disclaimer -

The Service may contain links to external websites that are not provided or maintained by or in any way affiliated with the Company.

-

Please note that the Company does not guarantee the accuracy, relevance, timeliness, or completeness of any information on these external websites.

+

The Service may contain links to external websites that are not provided or maintained by or in any way affiliated with the Company.

+

Please note that the Company does not guarantee the accuracy, relevance, timeliness, or completeness of any information on these external websites.

Errors and Omissions Disclaimer -

The information given by the Service is for general guidance on matters of interest only. Even if the Company takes every precaution to insure that the content of the Service is both current and accurate, errors can occur. Plus, given the changing nature of laws, rules and regulations, there may be delays, omissions or inaccuracies in the information contained on the Service.

-

The Company is not responsible for any errors or omissions, or for the results obtained from the use of this information.

+

The information given by the Service is for general guidance on matters of interest only. Even if the Company takes every precaution to insure that the content of the Service is both current and accurate, errors can occur. Plus, given the changing nature of laws, rules and regulations, there may be delays, omissions or inaccuracies in the information contained on the Service.

+

The Company is not responsible for any errors or omissions, or for the results obtained from the use of this information.

Fair Use Disclaimer -

The Company may use copyrighted material which has not always been specifically authorized by the copyright owner. The Company is making such material available for criticism, comment, news reporting, teaching, scholarship, or research.

-

The Company believes this constitutes a "fair use" of any such copyrighted material as provided for in section 107 of the United States Copyright law.

-

If You wish to use copyrighted material from the Service for your own purposes that go beyond fair use, You must obtain permission from the copyright owner.

+

The Company may use copyrighted material which has not always been specifically authorized by the copyright owner. The Company is making such material available for criticism, comment, news reporting, teaching, scholarship, or research.

+

The Company believes this constitutes a "fair use" of any such copyrighted material as provided for in section 107 of the United States Copyright law.

+

If You wish to use copyrighted material from the Service for your own purposes that go beyond fair use, You must obtain permission from the copyright owner.

Views Expressed Disclaimer -

The Service may contain views and opinions which are those of the authors and do not necessarily reflect the official policy or position of any other author, agency, organization, employer or company, including the Company.

-

Comments published by users are their sole responsibility and the users will take full responsibility, liability and blame for any libel or litigation that results from something written in or as a direct result of something written in a comment. The Company is not liable for any comment published by users and reserve the right to delete any comment for any reason whatsoever.

+

The Service may contain views and opinions which are those of the authors and do not necessarily reflect the official policy or position of any other author, agency, organization, employer or company, including the Company.

+

Comments published by users are their sole responsibility and the users will take full responsibility, liability and blame for any libel or litigation that results from something written in or as a direct result of something written in a comment. The Company is not liable for any comment published by users and reserve the right to delete any comment for any reason whatsoever.

No Responsibility Disclaimer -

The information on the Service is provided with the understanding that the Company is not herein engaged in rendering legal, accounting, tax, or other professional advice and services. As such, it should not be used as a substitute for consultation with professional accounting, tax, legal or other competent advisers.

-

In no event shall the Company or its suppliers be liable for any special, incidental, indirect, or consequential damages whatsoever arising out of or in connection with your access or use or inability to access or use the Service.

+

The information on the Service is provided with the understanding that the Company is not herein engaged in rendering legal, accounting, tax, or other professional advice and services. As such, it should not be used as a substitute for consultation with professional accounting, tax, legal or other competent advisers.

+

In no event shall the Company or its suppliers be liable for any special, incidental, indirect, or consequential damages whatsoever arising out of or in connection with your access or use or inability to access or use the Service.

"Use at Your Own Risk" Disclaimer -

All information in the Service is provided "as is", with no guarantee of completeness, accuracy, timeliness or of the results obtained from the use of this information, and without warranty of any kind, express or implied, including, but not limited to warranties of performance, merchantability and fitness for a particular purpose.

-

The Company will not be liable to You or anyone else for any decision made or action taken in reliance on the information given by the Service or for any consequential, special or similar damages, even if advised of the possibility of such damages.

+

All information in the Service is provided "as is", with no guarantee of completeness, accuracy, timeliness or of the results obtained from the use of this information, and without warranty of any kind, express or implied, including, but not limited to warranties of performance, merchantability and fitness for a particular purpose.

+

The Company will not be liable to You or anyone else for any decision made or action taken in reliance on the information given by the Service or for any consequential, special or similar damages, even if advised of the possibility of such damages.

Contact Us -

If you have any questions about this Disclaimer, contact us by email at info@***URL***

+

If you have any questions about this Disclaimer, contact us by email at info@***URL***

Last updated: June 22, 2020

\ No newline at end of file diff --git a/templates/home.php b/templates/home.php index 92f8021..844f5a9 100644 --- a/templates/home.php +++ b/templates/home.php @@ -1,61 +1,63 @@ - - '> - - -
-
- -
- -
- -
- -
- - - - - - - -
-
- - - -
-

- -
-
- + + '> + + +
+
+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ + + +
+

+ +
+
+ \ No newline at end of file diff --git a/templates/privacy-policy.php b/templates/privacy-policy.php index b24b247..0b2f182 100644 --- a/templates/privacy-policy.php +++ b/templates/privacy-policy.php @@ -1,11 +1,17 @@ + +

This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You. We use Your Personal data to provide and improve the Service. By using the Service, You agree to the collection and use of information in accordance with this Privacy Policy.

@@ -13,152 +19,152 @@

For the purposes of this Privacy Policy:

    -
  • You means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.
  • -
  • Company (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to ***TITLE***.
  • -
  • Affiliate means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority.
  • -
  • Account means a unique account created for You to access our Service or parts of our Service.
  • -
  • Website refers to ***TITLE***, accessible from ***URL***.
  • -
  • Service refers to the Website.
  • -
  • Country refers to the United States
  • -
  • Service Provider means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used.
  • -
  • Third-party Social Media Service refers to any website or any social network website through which a User can log in or create an account to use the Service.
  • -
  • Personal Data is any information that relates to an identified or identifiable individual.
  • -
  • Cookies are small files that are placed on Your computer, mobile device or any other device by a website, containing the details of Your browsing history on that website among its many uses.
  • -
  • Device means any device that can access the Service such as a computer, a cellphone or a digital tablet.
  • -
  • Usage Data refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit).
  • +
  • You means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.
  • +
  • Company (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to ***TITLE***.
  • +
  • Affiliate means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority.
  • +
  • Account means a unique account created for You to access our Service or parts of our Service.
  • +
  • Website refers to ***TITLE***, accessible from ***URL***.
  • +
  • Service refers to the Website.
  • +
  • Country refers to the United States
  • +
  • Service Provider means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used.
  • +
  • Third-party Social Media Service refers to any website or any social network website through which a User can log in or create an account to use the Service.
  • +
  • Personal Data is any information that relates to an identified or identifiable individual.
  • +
  • Cookies are small files that are placed on Your computer, mobile device or any other device by a website, containing the details of Your browsing history on that website among its many uses.
  • +
  • Device means any device that can access the Service such as a computer, a cellphone or a digital tablet.
  • +
  • Usage Data refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit).
Collecting and Using Your Personal Data -

Types of Data Collected

- -

Personal Data

-

While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to contact or identify You. Personally identifiable information may include, but is not limited to Usage Data.

-

Usage Data is collected automatically when using the Service. Usage Data may include information such as Your Device's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that You visit, the time and date of Your visit, the time spent on those pages, unique device identifiers and other diagnostic data.

-

When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the type of mobile device You use, Your mobile device unique ID, the IP address of Your mobile device, Your mobile operating system, the type of mobile Internet browser You use, unique device identifiers and other diagnostic data.

-

We may also collect information that Your browser sends whenever You visit our Service or when You access the Service by or through a mobile device.

- -

Tracking Technologies and Cookies

-

We use Cookies and similar tracking technologies to track the activity on Our Service and store certain information. Tracking technologies used are beacons, tags, and scripts to collect and track information and to improve and analyze Our Service.

-

You can instruct Your browser to refuse all Cookies or to indicate when a Cookie is being sent. However, if You do not accept Cookies, You may not be able to use some parts of our Service.

-

Cookies can be "Persistent" or "Session" Cookies. Persistent Cookies remain on your personal computer or mobile device when You go offline, while Session Cookies are deleted as soon as You close your web browser.

-

We use both session and persistent Cookies for the purposes set out below:

-
    -
  • -

    Necessary / Essential Cookies

    -

    Type: Session Cookies

    -

    Administered by: Us

    -

    Purpose: These Cookies are essential to provide You with services available through the Website and to enable You to use some of its features. They help to authenticate users and prevent fraudulent use of user accounts. Without these Cookies, the services that You have asked for cannot be provided, and We only use these Cookies to provide You with those services.

    -
  • -
  • -

    Cookies Policy / Notice Acceptance Cookies

    -

    Type: Persistent Cookies

    -

    Administered by: Us

    -

    Purpose: These Cookies identify if users have accepted the use of cookies on the Website.

    -
  • -
  • -

    Functionality Cookies

    -

    Type: Persistent Cookies

    -

    Administered by: Us

    -

    Purpose: These Cookies allow us to remember choices You make when You use the Website, such as remembering your login details or language preference. The purpose of these Cookies is to provide You with a more personal experience and to avoid You having to re-enter your preferences every time You use the Website.

    -
  • -
  • -

    Tracking and Performance Cookies

    -

    Type: Persistent Cookies

    -

    Administered by: Third-Parties

    -

    Purpose: These Cookies are used to track information about traffic to the Website and how users use the Website. The information gathered via these Cookies may directly or indirectly identify you as an individual visitor. This is because the information collected is typically linked to a pseudonymous identifier associated with the device you use to access the Website. We may also use these Cookies to test new advertisements, pages, features or new functionality of the Website to see how our users react to them.

    -
  • -
  • -

    Targeting and Advertising Cookies

    -

    Type: Persistent Cookies

    -

    Administered by: Third-Parties

    -

    Purpose: These Cookies track your browsing habits to enable Us to show advertising which is more likely to be of interest to You. These Cookies use information about your browsing history to group You with other users who have similar interests. Based on that information, and with Our permission, third party advertisers can place Cookies to enable them to show adverts which We think will be relevant to your interests while You are on third party websites.

    -
  • -
-

For more information about the cookies we use and your choices regarding cookies, please visit our Cookies Policy.

- -

Use of Your Personal Data

-

The Company may use Personal Data for the following purposes:

-
    -
  • To provide and maintain our Service  including to monitor the usage of our Service.
  • -
  • To manage Your Account: to manage Your registration as a user of the Service. The Personal Data You provide can give You access to different functionalities of the Service that are available to You as a registered user.
  • -
  • For the performance of a contract: the development, compliance and undertaking of the purchase contract for the products, items or services You have purchased or of any other contract with Us through the Service.
  • -
  • To contact You: To contact You by email, telephone calls, SMS, or other equivalent forms of electronic communication, such as a mobile application's push notifications regarding updates or informative communications related to the functionalities, products or contracted services, including the security updates, when necessary or reasonable for their implementation.
  • -
  • To provide You with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless You have opted not to receive such information.
  • -
  • To manage Your requests: To attend and manage Your requests to Us.
  • -
- -

We may share your personal information in the following situations:

- -
    -
  • With Service Providers: We may share Your personal information with Service Providers to monitor and analyze the use of our Service, to show advertisements to You to help support and maintain Our Service, to contact You.
  • -
  • For Business transfers: We may share or transfer Your personal information in connection with, or during negotiations of, any merger, sale of Company assets, financing, or acquisition of all or a portion of our business to another company.
  • -
  • With Affiliates: We may share Your information with Our affiliates, in which case we will require those affiliates to honor this Privacy Policy. Affiliates include Our parent company and any other subsidiaries, joint venture partners or other companies that We control or that are under common control with Us.
  • -
  • With Business partners: We may share Your information with Our business partners to offer You certain products, services or promotions.
  • -
  • With other users: when You share personal information or otherwise interact in the public areas with other users, such information may be viewed by all users and may be publicly distributed outside. If You interact with other users or register through a Third-Party Social Media Service, Your contacts on the Third-Party Social Media Service may see You name, profile, pictures and description of Your activity. Similarly, other users will be able to view descriptions of Your activity, communicate with You and view Your profile.
  • -
- -

Retention of Your Personal Data

-

The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies.

-

The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods.

- -

Transfer of Your Personal Data

-

Your information, including Personal Data, is processed at the Company's operating offices and in any other places where the parties involved in the processing are located. It means that this information may be transferred to—and maintained on—computers located outside of Your state, province, country or other governmental jurisdiction where the data protection laws may differ than those from Your jurisdiction.

-

Your consent to this Privacy Policy followed by Your submission of such information represents Your agreement to that transfer.

-

The Company will take all steps reasonably necessary to ensure that Your data is treated securely and in accordance with this Privacy Policy and no transfer of Your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of Your data and other personal information.

- -

Disclosure of Your Personal Data

-

Business Transactions

-

If the Company is involved in a merger, acquisition or asset sale, Your Personal Data may be transferred. We will provide notice before Your Personal Data is transferred and becomes subject to a different Privacy Policy.

-

Law enforcement

-

Under certain circumstances, the Company may be required to disclose Your Personal Data if required to do so by law or in response to valid requests by public authorities (e.g. a court or a government agency).

-

Other legal requirements

-

The Company may disclose Your Personal Data in the good faith belief that such action is necessary to:

-
    -
  • Comply with a legal obligation
  • -
  • Protect and defend the rights or property of the Company
  • -
  • Prevent or investigate possible wrongdoing in connection with the Service
  • -
  • Protect the personal safety of Users of the Service or the public
  • -
  • Protect against legal liability
  • -
- -

Security of Your Personal Data

-

The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We strive to use commercially acceptable means to protect Your Personal Data, We cannot guarantee its absolute security.

+

Types of Data Collected

+ +

Personal Data

+

While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to contact or identify You. Personally identifiable information may include, but is not limited to Usage Data.

+

Usage Data is collected automatically when using the Service. Usage Data may include information such as Your Device's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that You visit, the time and date of Your visit, the time spent on those pages, unique device identifiers and other diagnostic data.

+

When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the type of mobile device You use, Your mobile device unique ID, the IP address of Your mobile device, Your mobile operating system, the type of mobile Internet browser You use, unique device identifiers and other diagnostic data.

+

We may also collect information that Your browser sends whenever You visit our Service or when You access the Service by or through a mobile device.

+ +

Tracking Technologies and Cookies

+

We use Cookies and similar tracking technologies to track the activity on Our Service and store certain information. Tracking technologies used are beacons, tags, and scripts to collect and track information and to improve and analyze Our Service.

+

You can instruct Your browser to refuse all Cookies or to indicate when a Cookie is being sent. However, if You do not accept Cookies, You may not be able to use some parts of our Service.

+

Cookies can be "Persistent" or "Session" Cookies. Persistent Cookies remain on your personal computer or mobile device when You go offline, while Session Cookies are deleted as soon as You close your web browser.

+

We use both session and persistent Cookies for the purposes set out below:

+
    +
  • +

    Necessary / Essential Cookies

    +

    Type: Session Cookies

    +

    Administered by: Us

    +

    Purpose: These Cookies are essential to provide You with services available through the Website and to enable You to use some of its features. They help to authenticate users and prevent fraudulent use of user accounts. Without these Cookies, the services that You have asked for cannot be provided, and We only use these Cookies to provide You with those services.

    +
  • +
  • +

    Cookies Policy / Notice Acceptance Cookies

    +

    Type: Persistent Cookies

    +

    Administered by: Us

    +

    Purpose: These Cookies identify if users have accepted the use of cookies on the Website.

    +
  • +
  • +

    Functionality Cookies

    +

    Type: Persistent Cookies

    +

    Administered by: Us

    +

    Purpose: These Cookies allow us to remember choices You make when You use the Website, such as remembering your login details or language preference. The purpose of these Cookies is to provide You with a more personal experience and to avoid You having to re-enter your preferences every time You use the Website.

    +
  • +
  • +

    Tracking and Performance Cookies

    +

    Type: Persistent Cookies

    +

    Administered by: Third-Parties

    +

    Purpose: These Cookies are used to track information about traffic to the Website and how users use the Website. The information gathered via these Cookies may directly or indirectly identify you as an individual visitor. This is because the information collected is typically linked to a pseudonymous identifier associated with the device you use to access the Website. We may also use these Cookies to test new advertisements, pages, features or new functionality of the Website to see how our users react to them.

    +
  • +
  • +

    Targeting and Advertising Cookies

    +

    Type: Persistent Cookies

    +

    Administered by: Third-Parties

    +

    Purpose: These Cookies track your browsing habits to enable Us to show advertising which is more likely to be of interest to You. These Cookies use information about your browsing history to group You with other users who have similar interests. Based on that information, and with Our permission, third party advertisers can place Cookies to enable them to show adverts which We think will be relevant to your interests while You are on third party websites.

    +
  • +
+

For more information about the cookies we use and your choices regarding cookies, please visit our Cookies Policy.

+ +

Use of Your Personal Data

+

The Company may use Personal Data for the following purposes:

+
    +
  • To provide and maintain our Service  including to monitor the usage of our Service.
  • +
  • To manage Your Account: to manage Your registration as a user of the Service. The Personal Data You provide can give You access to different functionalities of the Service that are available to You as a registered user.
  • +
  • For the performance of a contract: the development, compliance and undertaking of the purchase contract for the products, items or services You have purchased or of any other contract with Us through the Service.
  • +
  • To contact You: To contact You by email, telephone calls, SMS, or other equivalent forms of electronic communication, such as a mobile application's push notifications regarding updates or informative communications related to the functionalities, products or contracted services, including the security updates, when necessary or reasonable for their implementation.
  • +
  • To provide You with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless You have opted not to receive such information.
  • +
  • To manage Your requests: To attend and manage Your requests to Us.
  • +
+ +

We may share your personal information in the following situations:

+ +
    +
  • With Service Providers: We may share Your personal information with Service Providers to monitor and analyze the use of our Service, to show advertisements to You to help support and maintain Our Service, to contact You.
  • +
  • For Business transfers: We may share or transfer Your personal information in connection with, or during negotiations of, any merger, sale of Company assets, financing, or acquisition of all or a portion of our business to another company.
  • +
  • With Affiliates: We may share Your information with Our affiliates, in which case we will require those affiliates to honor this Privacy Policy. Affiliates include Our parent company and any other subsidiaries, joint venture partners or other companies that We control or that are under common control with Us.
  • +
  • With Business partners: We may share Your information with Our business partners to offer You certain products, services or promotions.
  • +
  • With other users: when You share personal information or otherwise interact in the public areas with other users, such information may be viewed by all users and may be publicly distributed outside. If You interact with other users or register through a Third-Party Social Media Service, Your contacts on the Third-Party Social Media Service may see You name, profile, pictures and description of Your activity. Similarly, other users will be able to view descriptions of Your activity, communicate with You and view Your profile.
  • +
+ +

Retention of Your Personal Data

+

The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies.

+

The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods.

+ +

Transfer of Your Personal Data

+

Your information, including Personal Data, is processed at the Company's operating offices and in any other places where the parties involved in the processing are located. It means that this information may be transferred to—and maintained on—computers located outside of Your state, province, country or other governmental jurisdiction where the data protection laws may differ than those from Your jurisdiction.

+

Your consent to this Privacy Policy followed by Your submission of such information represents Your agreement to that transfer.

+

The Company will take all steps reasonably necessary to ensure that Your data is treated securely and in accordance with this Privacy Policy and no transfer of Your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of Your data and other personal information.

+ +

Disclosure of Your Personal Data

+

Business Transactions

+

If the Company is involved in a merger, acquisition or asset sale, Your Personal Data may be transferred. We will provide notice before Your Personal Data is transferred and becomes subject to a different Privacy Policy.

+

Law enforcement

+

Under certain circumstances, the Company may be required to disclose Your Personal Data if required to do so by law or in response to valid requests by public authorities (e.g. a court or a government agency).

+

Other legal requirements

+

The Company may disclose Your Personal Data in the good faith belief that such action is necessary to:

+
    +
  • Comply with a legal obligation
  • +
  • Protect and defend the rights or property of the Company
  • +
  • Prevent or investigate possible wrongdoing in connection with the Service
  • +
  • Protect the personal safety of Users of the Service or the public
  • +
  • Protect against legal liability
  • +
+ +

Security of Your Personal Data

+

The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We strive to use commercially acceptable means to protect Your Personal Data, We cannot guarantee its absolute security.

Detailed Information on the Processing of Your Personal Data -

Service Providers have access to Your Personal Data only to perform their tasks on Our behalf and are obligated not to disclose or use it for any other purpose.

-
    -
  • Google Analytics: We may use third-party Service providers to monitor and analyze the use of our Service. For more information on what type of information Google Analytics collects, please visit Google's Privacy Policy page: policies.google.com/privacy
  • -
  • Google Ads (AdSense): We may use Service providers to show advertisements to You to help support and maintain Our Service. You can opt-out from Google Adsense service by following the instructions as described on Google's Privacy Policy page: policies.google.com/privacy.
  • -
+

Service Providers have access to Your Personal Data only to perform their tasks on Our behalf and are obligated not to disclose or use it for any other purpose.

+
    +
  • Google Analytics: We may use third-party Service providers to monitor and analyze the use of our Service. For more information on what type of information Google Analytics collects, please visit Google's Privacy Policy page: policies.google.com/privacy
  • +
  • Google Ads (AdSense): We may use Service providers to show advertisements to You to help support and maintain Our Service. You can opt-out from Google Adsense service by following the instructions as described on Google's Privacy Policy page: policies.google.com/privacy.
  • +
Children's Privacy -

Our Service does 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.

-

If We need to rely on consent as a legal basis for processing Your information and Your country requires consent from a parent, We may require Your parent's consent before We collect and use that information.

+

Our Service does 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.

+

If We need to rely on consent as a legal basis for processing Your information and Your country requires consent from a parent, We may require Your parent's consent before We collect and use that information.

Links to Other Websites -

Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit.

-

We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.

+

Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit.

+

We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.

Changes to this Privacy Policy -

We may update our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page.

-

We will let You know via email and/or a prominent notice on Our Service, prior to the change becoming effective and update the "Last updated" date at the top of this Privacy Policy.

-

You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.

+

We may update our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page.

+

We will let You know via email and/or a prominent notice on Our Service, prior to the change becoming effective and update the "Last updated" date at the top of this Privacy Policy.

+

You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.

Contact Us -

If you have any questions about this Privacy Policy, contact us by email at info@***URL***

+

If you have any questions about this Privacy Policy, contact us by email at info@***URL***

Last updated: June 22, 2020

\ No newline at end of file diff --git a/templates/terms-and-conditions.php b/templates/terms-and-conditions.php index 34d999e..d6acc40 100644 --- a/templates/terms-and-conditions.php +++ b/templates/terms-and-conditions.php @@ -1,99 +1,105 @@ + +

The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural.

For the purposes of these Terms and Conditions:

    -
  • Affiliate means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority.
  • -
  • Company (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to ***TITLE***.
  • -
  • Country refers to the United States
  • -
  • Device means any device that can access the Service such as a computer, a cellphone or a digital tablet.
  • -
  • Service refers to the Website.
  • -
  • Terms and Conditions (also referred as "Terms") mean these Terms and Conditions that form the entire agreement between You and the Company regarding the use of the Service.
  • -
  • Third-party Social Media Service means any services or content (including data, information, products or services) provided by a third-party that may be displayed, included or made available by the Service.
  • -
  • Website refers to ***TITLE***, accessible from ***URL***.
  • -
  • You means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.
  • +
  • Affiliate means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority.
  • +
  • Company (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to ***TITLE***.
  • +
  • Country refers to the United States
  • +
  • Device means any device that can access the Service such as a computer, a cellphone or a digital tablet.
  • +
  • Service refers to the Website.
  • +
  • Terms and Conditions (also referred as "Terms") mean these Terms and Conditions that form the entire agreement between You and the Company regarding the use of the Service.
  • +
  • Third-party Social Media Service means any services or content (including data, information, products or services) provided by a third-party that may be displayed, included or made available by the Service.
  • +
  • Website refers to ***TITLE***, accessible from ***URL***.
  • +
  • You means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.
Acknowledgement -

These are the Terms and Conditions governing the use of this Service and the agreement that operates between You and the Company. These Terms and Conditions set out the rights and obligations of all users regarding the use of the Service.

-

Your access to and use of the Service is conditioned on Your acceptance of and compliance with these Terms and Conditions. These Terms and Conditions apply to all visitors, users and others who access or use the Service.

-

By accessing or using the Service You agree to be bound by these Terms and Conditions. If You disagree with any part of these Terms and Conditions then You may not access the Service.

-

You represent that you are over the age of 18. The Company does not permit those under 18 to use the Service.

-

Your access to and use of the Service is also conditioned on Your acceptance of and compliance with the Privacy Policy of the Company. Our Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your personal information when You use the Application or the Website and tells You about Your privacy rights and how the law protects You. Please read Our Privacy Policy carefully before using Our Service.

+

These are the Terms and Conditions governing the use of this Service and the agreement that operates between You and the Company. These Terms and Conditions set out the rights and obligations of all users regarding the use of the Service.

+

Your access to and use of the Service is conditioned on Your acceptance of and compliance with these Terms and Conditions. These Terms and Conditions apply to all visitors, users and others who access or use the Service.

+

By accessing or using the Service You agree to be bound by these Terms and Conditions. If You disagree with any part of these Terms and Conditions then You may not access the Service.

+

You represent that you are over the age of 18. The Company does not permit those under 18 to use the Service.

+

Your access to and use of the Service is also conditioned on Your acceptance of and compliance with the Privacy Policy of the Company. Our Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your personal information when You use the Application or the Website and tells You about Your privacy rights and how the law protects You. Please read Our Privacy Policy carefully before using Our Service.

Links to Other Websites -

Our Service may contain links to third-party web sites or services that are not owned or controlled by the Company.

-

The Company has no control over, and assumes no responsibility for, the content, privacy policies, or practices of any third party web sites or services. You further acknowledge and agree that the Company shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with the use of or reliance on any such content, goods or services available on or through any such web sites or services.

-

We strongly advise You to read the terms and conditions and privacy policies of any third-party web sites or services that You visit.

+

Our Service may contain links to third-party web sites or services that are not owned or controlled by the Company.

+

The Company has no control over, and assumes no responsibility for, the content, privacy policies, or practices of any third party web sites or services. You further acknowledge and agree that the Company shall not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with the use of or reliance on any such content, goods or services available on or through any such web sites or services.

+

We strongly advise You to read the terms and conditions and privacy policies of any third-party web sites or services that You visit.

Termination -

We may terminate or suspend Your access immediately, without prior notice or liability, for any reason whatsoever, including without limitation if You breach these Terms and Conditions.

-

Upon termination, Your right to use the Service will cease immediately.

+

We may terminate or suspend Your access immediately, without prior notice or liability, for any reason whatsoever, including without limitation if You breach these Terms and Conditions.

+

Upon termination, Your right to use the Service will cease immediately.

Limitation of Liability -

Notwithstanding any damages that You might incur, the entire liability of the Company and any of its suppliers under any provision of this Terms and Your exclusive remedy for all of the foregoing shall be limited to the amount actually paid by You through the Service or 100 USD if You haven't purchased anything through the Service.

-

To the maximum extent permitted by applicable law, in no event shall the Company or its suppliers be liable for any special, incidental, indirect, or consequential damages whatsoever (including, but not limited to, damages for loss of profits, loss of data or other information, for business interruption, for personal injury, loss of privacy arising out of or in any way related to the use of or inability to use the Service, third-party software and/or third-party hardware used with the Service, or otherwise in connection with any provision of this Terms), even if the Company or any supplier has been advised of the possibility of such damages and even if the remedy fails of its essential purpose.

-

Some states do not allow the exclusion of implied warranties or limitation of liability for incidental or consequential damages, which means that some of the above limitations may not apply. In these states, each party's liability will be limited to the greatest extent permitted by law.

+

Notwithstanding any damages that You might incur, the entire liability of the Company and any of its suppliers under any provision of this Terms and Your exclusive remedy for all of the foregoing shall be limited to the amount actually paid by You through the Service or 100 USD if You haven't purchased anything through the Service.

+

To the maximum extent permitted by applicable law, in no event shall the Company or its suppliers be liable for any special, incidental, indirect, or consequential damages whatsoever (including, but not limited to, damages for loss of profits, loss of data or other information, for business interruption, for personal injury, loss of privacy arising out of or in any way related to the use of or inability to use the Service, third-party software and/or third-party hardware used with the Service, or otherwise in connection with any provision of this Terms), even if the Company or any supplier has been advised of the possibility of such damages and even if the remedy fails of its essential purpose.

+

Some states do not allow the exclusion of implied warranties or limitation of liability for incidental or consequential damages, which means that some of the above limitations may not apply. In these states, each party's liability will be limited to the greatest extent permitted by law.

"AS IS" and "AS AVAILABLE" Disclaimer -

The Service 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, the Company, 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 Service, 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, the Company provides no warranty or undertaking, and makes no representation of any kind that the Service will meet Your requirements, achieve any intended results, be compatible or work with any other software, applications, 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 the Company nor any of the company's provider makes any representation or warranty of any kind, express or implied: (i) as to the operation or availability of the Service, or the information, content, and materials or products included thereon; (ii) that the Service will be uninterrupted or error-free; (iii) as to the accuracy, reliability, or currency of any information or content provided through the Service; or (iv) that the Service, its servers, the content, or e-mails sent from or on behalf of the Company are free of viruses, scripts, trojan horses, worms, malware, timebombs or other harmful components.

-

Some jurisdictions do not allow the exclusion of certain types of warranties or limitations on applicable statutory rights of a consumer, so some or all of the above exclusions and limitations may not apply to You. But in such a case the exclusions and limitations set forth in this section shall be applied to the greatest extent enforceable under applicable law.

+

The Service 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, the Company, 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 Service, 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, the Company provides no warranty or undertaking, and makes no representation of any kind that the Service will meet Your requirements, achieve any intended results, be compatible or work with any other software, applications, 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 the Company nor any of the company's provider makes any representation or warranty of any kind, express or implied: (i) as to the operation or availability of the Service, or the information, content, and materials or products included thereon; (ii) that the Service will be uninterrupted or error-free; (iii) as to the accuracy, reliability, or currency of any information or content provided through the Service; or (iv) that the Service, its servers, the content, or e-mails sent from or on behalf of the Company are free of viruses, scripts, trojan horses, worms, malware, timebombs or other harmful components.

+

Some jurisdictions do not allow the exclusion of certain types of warranties or limitations on applicable statutory rights of a consumer, so some or all of the above exclusions and limitations may not apply to You. But in such a case the exclusions and limitations set forth in this section shall be applied to the greatest extent enforceable under applicable law.

Governing Law -

The laws of the Country, excluding its conflicts of law rules, shall govern this Terms and Your use of the Service. Your use of the Application may also be subject to other local, state, national, or international laws.

+

The laws of the Country, excluding its conflicts of law rules, shall govern this Terms and Your use of the Service. Your use of the Application may also be subject to other local, state, national, or international laws.

Disputes Resolution -

If You have any concern or dispute about the Service, You agree to first try to resolve the dispute informally by contacting the Company.

+

If You have any concern or dispute about the Service, You agree to first try to resolve the dispute informally by contacting the Company.

For European Union (EU) Users -

If You are a European Union consumer, you will benefit from any mandatory provisions of the law of the country in which you are resident in.

+

If You are a European Union consumer, you will benefit from any mandatory provisions of the law of the country in which you are resident in.

United States Legal Compliance -

You represent and warrant that (i) You are not located in a country that is subject to the United States government embargo, or that has been designated by the United States government as a "terrorist supporting" country, and (ii) You are not listed on any United States government list of prohibited or restricted parties.

+

You represent and warrant that (i) You are not located in a country that is subject to the United States government embargo, or that has been designated by the United States government as a "terrorist supporting" country, and (ii) You are not listed on any United States government list of prohibited or restricted parties.

Severability and Waiver -

If any provision of these Terms 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.

-

Except as provided herein, the failure to exercise a right or to require performance of an obligation under this Terms 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 a waiver of any subsequent breach.

+

If any provision of these Terms 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.

+

Except as provided herein, the failure to exercise a right or to require performance of an obligation under this Terms 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 a waiver of any subsequent breach.

Translation Interpretation -

These Terms and Conditions may have been translated if We have made them available to You on our Service.

-

You agree that the original English text shall prevail in the case of a dispute.

+

These Terms and Conditions may have been translated if We have made them available to You on our Service.

+

You agree that the original English text shall prevail in the case of a dispute.

Changes to These Terms and Conditions -

We reserve the right, at Our sole discretion, to modify or replace these Terms at any time. If a revision is material We will make reasonable efforts to 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 Service after those revisions become effective, You agree to be bound by the revised terms. If You do not agree to the new terms, in whole or in part, please stop using the website and the Service.

+

We reserve the right, at Our sole discretion, to modify or replace these Terms at any time. If a revision is material We will make reasonable efforts to 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 Service after those revisions become effective, You agree to be bound by the revised terms. If You do not agree to the new terms, in whole or in part, please stop using the website and the Service.

Contact Us -

If you have any questions about these Terms and Conditions, contact us by email at info@***URL***

+

If you have any questions about these Terms and Conditions, contact us by email at info@***URL***

Last updated: June 22, 2020

\ No newline at end of file