diff --git a/.distignore b/.distignore index 84dd374..753003b 100644 --- a/.distignore +++ b/.distignore @@ -8,6 +8,10 @@ phpcs.xml .eslintrc .gitattributes .releaserc.yml +.DS_Store +.editorconfig +.browserslistrc +postcss.config.js docker-compose.yml webpack.config.js CONTRIBUTING.md diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..87cb608 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,66 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": "wordpress", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 2021, + "sourceType": "module" + }, + "ignorePatterns": [ "node_modules", "assets" ], + "rules": { + "indent": [ + "error", + "tab" + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ], + "prefer-destructuring": [ + "warn", + { + "array": false, + "object": true + }, + { + "enforceForRenamedProperties": false + } + ], + "array-bracket-spacing": [ + "warn", + "always", + { + "arraysInArrays": false, + "objectsInArrays": false + } + ], + "key-spacing": [ + "warn", + { + "beforeColon": false, + "afterColon": true + } + ], + "object-curly-spacing": [ + "warn", + "always", + { + "arraysInObjects": true, + "objectsInObjects": false + } + ], + } +} diff --git a/inc/class-api.php b/inc/class-api.php new file mode 100644 index 0000000..03ceb26 --- /dev/null +++ b/inc/class-api.php @@ -0,0 +1,623 @@ +register_route(); + } + + /** + * Get endpoint. + * + * @return string + */ + public function get_endpoint() { + return $this->namespace . '/' . $this->version; + } + + /** + * Register hooks and actions. + * + * @return void + */ + private function register_route() { + add_action( 'rest_api_init', array( $this, 'register_routes' ) ); + } + + /** + * Register REST API route + * + * @return void + */ + public function register_routes() { + $namespace = $this->namespace . '/' . $this->version; + + $routes = array( + 'send' => array( + 'methods' => \WP_REST_Server::CREATABLE, + 'args' => array( + 'step' => array( + 'required' => true, + 'type' => 'string', + ), + 'message' => array( + 'required' => false, + 'type' => 'string', + ), + 'template' => array( + 'required' => false, + 'type' => 'string', + ), + ), + 'callback' => array( $this, 'send' ), + ), + 'status' => array( + 'methods' => \WP_REST_Server::READABLE, + 'args' => array( + 'thread_id' => array( + 'required' => true, + 'type' => 'string', + ), + 'run_id' => array( + 'required' => true, + 'type' => 'string', + ), + ), + 'callback' => array( $this, 'status' ), + ), + 'get' => array( + 'methods' => \WP_REST_Server::READABLE, + 'args' => array( + 'thread_id' => array( + 'required' => true, + 'type' => 'string', + ), + ), + 'callback' => array( $this, 'get' ), + ), + 'images' => array( + 'methods' => \WP_REST_Server::READABLE, + 'args' => array( + 'query' => array( + 'required' => true, + 'type' => 'string', + ), + ), + 'callback' => array( $this, 'images' ), + ), + 'templates' => array( + 'methods' => \WP_REST_Server::READABLE, + 'args' => array( + 'thread_id' => array( + 'required' => true, + 'type' => 'string', + ), + 'images' => array( + 'required' => false, + 'type' => 'array', + ), + ), + 'callback' => array( $this, 'templates' ), + ), + 'homepage' => array( + 'methods' => \WP_REST_Server::READABLE, + 'args' => array( + 'thread_id' => array( + 'required' => true, + 'type' => 'string', + ), + 'template' => array( + 'required' => true, + 'type' => 'string', + ), + ), + 'callback' => array( $this, 'homepage' ), + ), + ); + + foreach ( $routes as $route => $args ) { + $args['permission_callback'] = function () { + return current_user_can( 'manage_options' ); + }; + + register_rest_route( $namespace, '/' . $route, $args ); + } + } + + /** + * Send data to the API. + * + * @param \WP_REST_Request $request Request. + * + * @return \WP_REST_Response + */ + public function send( \WP_REST_Request $request ) { + $data = $request->get_params(); + + $params = array( + 'step' => $data['step'], + 'message' => $data['message'], + ); + + if ( isset( $data['template'] ) ) { + $params['template'] = $data['template']; + } + + $request = wp_remote_post( + QUICKWP_APP_API . 'wizard/send', + array( + 'timeout' => 20, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + 'body' => $params, + ) + ); + + if ( is_wp_error( $request ) ) { + return new \WP_REST_Response( array( 'error' => $request->get_error_message() ), 500 ); + } + + /** + * Holds the response as a standard class object + * + * @var \stdClass $response + */ + $response = json_decode( wp_remote_retrieve_body( $request ) ); + + if ( ! isset( $response->id ) || ! $response->id ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + return new \WP_REST_Response( $response, 200 ); + } + + /** + * Get status. + * + * @param \WP_REST_Request $request Request. + * + * @return \WP_REST_Response + */ + public function status( \WP_REST_Request $request ) { + $data = $request->get_params(); + + $api_url = QUICKWP_APP_API . 'wizard/status'; + + $query_params = array( + 'thread_id' => $data['thread_id'], + 'run_id' => $data['run_id'], + ); + + $request_url = add_query_arg( $query_params, $api_url ); + + $request = wp_safe_remote_get( + $request_url, + array( + 'timeout' => 20, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + ) + ); + + if ( is_wp_error( $request ) ) { + return new \WP_REST_Response( array( 'error' => $request->get_error_message() ), 500 ); + } + + /** + * Holds the response as a standard class object + * + * @var \stdClass $response + */ + $response = json_decode( wp_remote_retrieve_body( $request ) ); + + if ( ! isset( $response->id ) || ! $response->id ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + return new \WP_REST_Response( $response, 200 ); + } + + /** + * Get data. + * + * @param \WP_REST_Request $request Request. + * + * @return \WP_REST_Response + */ + public function get( \WP_REST_Request $request ) { + $data = $request->get_params(); + + $api_url = QUICKWP_APP_API . 'wizard/get'; + + $query_params = array( + 'thread_id' => $data['thread_id'], + ); + + $request_url = add_query_arg( $query_params, $api_url ); + + $request = wp_safe_remote_get( + $request_url, + array( + 'timeout' => 20, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + ) + ); + + if ( is_wp_error( $request ) ) { + return new \WP_REST_Response( array( 'error' => $request->get_error_message() ), 500 ); + } + + /** + * Holds the response as a standard class object + * + * @var \stdClass $response + */ + $response = json_decode( wp_remote_retrieve_body( $request ) ); + + if ( ! isset( $response->data ) || ! $response->data ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + return new \WP_REST_Response( $response, 200 ); + } + + /** + * Get homepage. + * + * @param \WP_REST_Request $request Request. + * + * @return \WP_REST_Response + */ + public function homepage( \WP_REST_Request $request ) { + $data = $request->get_params(); + + $api_url = QUICKWP_APP_API . 'wizard/get'; + + $query_params = array( + 'thread_id' => $data['thread_id'], + ); + + $request_url = add_query_arg( $query_params, $api_url ); + + $request = wp_safe_remote_get( + $request_url, + array( + 'timeout' => 20, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + ) + ); + + if ( is_wp_error( $request ) ) { + return new \WP_REST_Response( array( 'error' => $request->get_error_message() ), 500 ); + } + + /** + * Holds the response as a standard class object + * + * @var \stdClass $response + */ + $response = json_decode( wp_remote_retrieve_body( $request ) ); + + if ( ! isset( $response->data ) || ! $response->data ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + $items = self::process_json_from_response( $response->data ); + + if ( ! $items ) { + return new \WP_REST_Response( array( 'error' => __( 'Error Parsing JSON', 'quickwp' ) ), 500 ); + } + + self::extract_data( $items ); + + $templates = apply_filters( 'quickwp_templates', array() ); + + if ( empty( $templates ) || ! isset( $templates['homepage'] ) ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + $template = $templates['homepage'][ $data['template'] ]; + + $result = array(); + + $theme_path = get_stylesheet_directory(); + + $patterns = file_get_contents( $template ); //phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown + + if ( ! $patterns ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + preg_match_all( '/"slug":"(.*?)"/', $patterns, $matches ); + $slugs = $matches[1]; + + $filtered_patterns = array(); + + foreach ( $slugs as $slug ) { + $slug = str_replace( 'quickwp/', '', $slug ); + $pattern_path = $theme_path . '/patterns/' . $slug . '.php'; + + if ( ! file_exists( $pattern_path ) ) { + continue; + } + + ob_start(); + include $pattern_path; + $pattern_content = ob_get_clean(); + + $filtered_patterns[] = $pattern_content; + } + + return new \WP_REST_Response( + array( + 'status' => 'success', + 'data' => implode( '', $filtered_patterns ), + ), + 200 + ); + } + + /** + * Get templates. + * + * @param \WP_REST_Request $request Request. + * + * @return \WP_REST_Response + */ + public function templates( \WP_REST_Request $request ) { + $data = $request->get_params(); + + $api_url = QUICKWP_APP_API . 'wizard/get'; + + $query_params = array( + 'thread_id' => $data['thread_id'], + ); + + $request_url = add_query_arg( $query_params, $api_url ); + + $request = wp_safe_remote_get( + $request_url, + array( + 'timeout' => 20, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + ) + ); + + if ( is_wp_error( $request ) ) { + return new \WP_REST_Response( array( 'error' => $request->get_error_message() ), 500 ); + } + + /** + * Holds the response as a standard class object + * + * @var \stdClass $response + */ + $response = json_decode( wp_remote_retrieve_body( $request ) ); + + if ( ! isset( $response->data ) || ! $response->data ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + $items = self::process_json_from_response( $response->data ); + + if ( ! $items ) { + return new \WP_REST_Response( array( 'error' => __( 'Error Parsing JSON', 'quickwp' ) ), 500 ); + } + + self::extract_data( $items ); + + $templates = apply_filters( 'quickwp_templates', array() ); + + if ( empty( $templates ) || ! isset( $templates['homepage'] ) ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + $items = $templates['homepage']; + + $result = array(); + + $theme_path = get_stylesheet_directory(); + + foreach ( $items as $item => $path ) { + $pattern = file_get_contents( $path ); //phpcs:ignore WordPressVIPMinimum.Performance.FetchingRemoteData.FileGetContentsUnknown + + if ( ! $pattern ) { + continue; + } + + preg_match_all( '/"slug":"(.*?)"/', $pattern, $matches ); + $slugs = $matches[1]; + + $filtered_patterns = array(); + + foreach ( $slugs as $slug ) { + $slug = str_replace( 'quickwp/', '', $slug ); + $pattern_path = $theme_path . '/patterns/' . $slug . '.php'; + + if ( ! file_exists( $pattern_path ) ) { + continue; + } + + // Check if $data param has images and it counts more than 0. + if ( isset( $data['images'] ) && count( $data['images'] ) > 0 ) { + $images = $data['images']; + + add_filter( + 'quickwp/image', + function () use( $images ) { + // Get a random image from the array. + $image = $images[ array_rand( $images ) ]; + return esc_url( $image['src'] ); + } + ); + } + + ob_start(); + include $pattern_path; + $pattern_content = ob_get_clean(); + + $filtered_patterns[] = $pattern_content; + } + + $result[] = array( + 'slug' => $item, + 'patterns' => implode( '', $filtered_patterns ), + ); + } + + return new \WP_REST_Response( + array( + 'status' => 'success', + 'data' => $result, + ), + 200 + ); + } + + /** + * Get images. + * + * @param \WP_REST_Request $request Request. + * + * @return \WP_REST_Response + */ + public function images( \WP_REST_Request $request ) { + $data = $request->get_params(); + + $api_url = QUICKWP_APP_API . 'wizard/images'; + + $query_params = array( + 'query' => $data['query'], + ); + + $request_url = add_query_arg( $query_params, $api_url ); + + $request = wp_safe_remote_get( + $request_url, + array( + 'timeout' => 20, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + ) + ); + + if ( is_wp_error( $request ) ) { + return new \WP_REST_Response( array( 'error' => $request->get_error_message() ), 500 ); + } + + /** + * Holds the response as a standard class object + * + * @var \stdClass $response + */ + $response = json_decode( wp_remote_retrieve_body( $request ) ); + + if ( ! isset( $response->photos ) ) { + return new \WP_REST_Response( array( 'error' => __( 'Error', 'quickwp' ) ), 500 ); + } + + return new \WP_REST_Response( $response, 200 ); + } + + /** + * Get JSON from response. + * + * @param array $data Response. + * + * @throws \Exception Exception in case of invalid JSON. + * + * @return array|false + */ + private static function process_json_from_response( $data ) { + // Find the target item. + $target = current( $data ); + + if ( false === $target || ! isset( $target->content ) ) { + return false; + } + + // Extract the JSON string. + $json_string = $target->content[0]->text->value; + + try { + $json_object = json_decode( $json_string, true ); + + if ( is_array( $json_object ) ) { + return $json_object; + } + + throw new \Exception( 'Invalid JSON' ); + } catch ( \Exception $e ) { + if ( substr( $json_string, 0, 7 ) === '```json' && substr( trim( $json_string ), -3 ) === '```' ) { + $cleaned_json = trim( str_replace( array( '```json', '```' ), '', $json_string ) ); + $json_object = json_decode( $cleaned_json, true ); + + if ( is_array( $json_object ) ) { + return $json_object; + } + } + } + + return false; + } + + /** + * Extract Data. + * + * @param array $items Items. + * + * @return void + */ + private static function extract_data( $items ) { + foreach ( $items as $item ) { + if ( ! isset( $item['slug'] ) || ! isset( $item['order'] ) || ! isset( $item['strings'] ) ) { + continue; + } + + $strings = $item['strings']; + + foreach ( $strings as $string ) { + add_filter( + 'quickwp/' . $string['slug'], + function () use( $string ) { + return esc_html( $string['value'] ); + } + ); + } + + if ( isset( $item['images'] ) ) { + $images = $item['images']; + + foreach ( $images as $image ) { + add_filter( + 'quickwp/' . $image['slug'], + function ( $value ) use( $image ) { + if ( filter_var( $image['src'], FILTER_VALIDATE_URL ) && ( strpos( $image['src'], 'pexels.com' ) !== false ) ) { + return esc_url( $image['src'] ); + } + + return $value; + } + ); + } + } + } + } +} diff --git a/inc/class-main.php b/inc/class-main.php index 54aec2d..5f5d54a 100644 --- a/inc/class-main.php +++ b/inc/class-main.php @@ -7,16 +7,26 @@ namespace ThemeIsle\QuickWP; +use ThemeIsle\QuickWP\API; + /** * Main class. */ class Main { + /** + * API instance. + * + * @var API + */ + private $api; /** * Constructor. */ public function __construct() { $this->register_hooks(); + + $this->api = new API(); } /** @@ -26,6 +36,14 @@ public function __construct() { */ private function register_hooks() { add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_assets' ) ); + + if ( defined( 'QUICKWP_APP_GUIDED_MODE' ) && QUICKWP_APP_GUIDED_MODE ) { + add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) ); + add_action( 'admin_init', array( $this, 'guided_access' ) ); + add_filter( 'show_admin_bar', '__return_false' ); // phpcs:ignore WordPressVIPMinimum.UserExperience.AdminBarRemoval.RemovalDetected + } + + add_action( 'wp_print_footer_scripts', array( $this, 'print_footer_scripts' ) ); } /** @@ -44,23 +62,132 @@ public function enqueue_assets() { return; } - $asset_file = include QUICKWP_PATH . '/build/index.asset.php'; + $asset_file = include QUICKWP_APP_PATH . '/build/backend/index.asset.php'; wp_enqueue_style( 'quickwp', - QUICKWP_URL . 'build/style-index.css', + QUICKWP_APP_URL . 'build/backend/style-index.css', array( 'wp-components' ), $asset_file['version'] ); wp_enqueue_script( 'quickwp', - QUICKWP_URL . 'build/index.js', + QUICKWP_APP_URL . 'build/backend/index.js', $asset_file['dependencies'], $asset_file['version'], true ); wp_set_script_translations( 'quickwp', 'quickwp' ); + + wp_localize_script( + 'quickwp', + 'quickwp', + array( + 'api' => $this->api->get_endpoint(), + 'siteUrl' => get_site_url(), + 'themeSlug' => get_template(), + 'isGuidedMode' => defined( 'QUICKWP_APP_GUIDED_MODE' ) && QUICKWP_APP_GUIDED_MODE, + ) + ); + } + + /** + * Enqueue frontend assets. + * + * @return void + */ + public function enqueue_frontend_assets() { + $asset_file = include QUICKWP_APP_PATH . '/build/frontend/frontend.asset.php'; + + wp_enqueue_style( + 'quickwp-frontend', + QUICKWP_APP_URL . 'build/frontend/style-index.css', + array(), + $asset_file['version'] + ); + + wp_enqueue_script( + 'quickwp-frontend', + QUICKWP_APP_URL . 'build/frontend/frontend.js', + $asset_file['dependencies'], + $asset_file['version'], + true + ); + } + + /** + * Print footer scripts. + * + * @return void + */ + public function print_footer_scripts() { + if ( ! is_admin() ) { + return; + } + + $current_screen = get_current_screen(); + + if ( + ! current_user_can( 'manage_options' ) || + ! isset( $current_screen->id ) || + 'site-editor' !== $current_screen->id + ) { + return; + } + + ?> + + =1" } }, + "node_modules/@wordpress/element": { + "version": "5.25.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-5.25.0.tgz", + "integrity": "sha512-8FFK1wJ/4n7Y7s3wWRJinoX5WSPbgnJJawYEc6f5Jc7cG+OddHiWZQkU94o6lnRRm0+cCarxMV8K8hNI2Jc7OQ==", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@types/react": "^18.0.21", + "@types/react-dom": "^18.0.6", + "@wordpress/escape-html": "^2.48.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@wordpress/element/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@wordpress/escape-html": { + "version": "2.48.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-2.48.0.tgz", + "integrity": "sha512-zWOJWyRYVddJeZHnAJzV8f58+0GukbIdp+EcbNuRVm+Q2rhv0BpyO1HJB3Z8ba35whtNcGWJibk9WqdMzBAMAw==", + "dependencies": { + "@babel/runtime": "^7.16.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@wordpress/eslint-plugin": { "version": "17.5.0", "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-17.5.0.tgz", @@ -4659,6 +4729,19 @@ "node": ">=12" } }, + "node_modules/@wordpress/icons": { + "version": "9.39.0", + "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-9.39.0.tgz", + "integrity": "sha512-5L0VseLEDtZv1P/weUmwiTLGYCnXuTLtpqLN/CUo7o3TqqrNIK7xubXlFVKd7zeUOpVQAG+S/BgnaWIXru9N5w==", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/element": "^5.25.0", + "@wordpress/primitives": "^3.46.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@wordpress/jest-console": { "version": "7.19.0", "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-7.19.0.tgz", @@ -4745,6 +4828,19 @@ "prettier": ">=3" } }, + "node_modules/@wordpress/primitives": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-3.46.0.tgz", + "integrity": "sha512-KCNr1PqswT9XZoar53HC0e94TF3Sd96DQGvueMZVUE2YPdD96p3EWJjDRhmya+U63yVdHec+35fPDm+aJQQDdQ==", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@wordpress/element": "^5.25.0", + "classnames": "^2.3.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@wordpress/scripts": { "version": "26.19.0", "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-26.19.0.tgz", @@ -5986,7 +6082,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "dev": true, "dependencies": { "pascal-case": "^3.1.2", "tslib": "^2.0.3" @@ -6075,7 +6170,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -6102,7 +6196,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", - "dev": true, "dependencies": { "camel-case": "^4.1.2", "capital-case": "^1.0.4", @@ -6253,6 +6346,11 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "node_modules/clean-webpack-plugin": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz", @@ -6486,7 +6584,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -7087,6 +7184,11 @@ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", "dev": true }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, "node_modules/cwd": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", @@ -7747,7 +7849,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -8151,6 +8252,16 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-config-wordpress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-wordpress/-/eslint-config-wordpress-2.0.0.tgz", + "integrity": "sha512-9ydUZ1zORI3av5EOx4zQml8WpdDx1bAOZC4dLPcYGqVcdBol3dQ2L40e2ill52k/+I+rqUJppGzWK+zP7+lI1w==", + "deprecated": "This package has been deprecated, please use @wordpress/eslint-plugin or @wordpress/scripts", + "dev": true, + "engines": { + "node": ">=4.2.1" + } + }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -9995,7 +10106,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", - "dev": true, "dependencies": { "capital-case": "^1.0.4", "tslib": "^2.0.3" @@ -11850,8 +11960,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "3.14.1", @@ -11938,6 +12047,12 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -12360,6 +12475,52 @@ "uc.micro": "^1.0.1" } }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -12460,7 +12621,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -12472,7 +12632,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, "dependencies": { "tslib": "^2.0.3" } @@ -12742,6 +12901,15 @@ "integrity": "sha512-yywVJy8ctVlN5lNPxsep5urnZ6TTclwPEyigM9M3Bi8vseJBOfqNrGWN/r8NzuIt3PovM323W04blJfGQfQSVg==", "dev": true }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/meow": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", @@ -13155,11 +13323,16 @@ "node": ">= 0.4.0" } }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" @@ -13410,6 +13583,189 @@ "node": ">=10" } }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm-run-all/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/npm-run-all/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/npm-run-all/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/npm-run-all/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/npm-run-all/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", @@ -13812,7 +14168,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "dev": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -13888,7 +14243,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" @@ -13898,7 +14252,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", - "dev": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -13982,6 +14335,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -15345,7 +15710,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -15357,8 +15721,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dev": true, - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -15612,8 +15974,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -16027,8 +16388,6 @@ "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dev": true, - "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -16140,7 +16499,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", - "dev": true, "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", @@ -16440,7 +16798,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dev": true, "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" @@ -16786,6 +17143,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.padend": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.5.tgz", + "integrity": "sha512-DOB27b/2UTTD+4myKUFh+/fXWcu/UDyASIXfg+7VzoCNNGOfWvoyU/x5pvVHr++ztyt/oSYI1BcWBBG/hmlNjA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", @@ -17687,8 +18061,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -17995,7 +18368,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", - "dev": true, "dependencies": { "tslib": "^2.0.3" } @@ -18004,7 +18376,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", - "dev": true, "dependencies": { "tslib": "^2.0.3" } diff --git a/package.json b/package.json index 2f7af84..5f3a736 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "version": "1.0.0", "description": "Build websites quickly.", "scripts": { - "build": "wp-scripts build", + "build": "npm-run-all build:*", + "build:backend": "wp-scripts build src/index.js --output-path=build/backend --output-filename=index.js", + "build:frontend": "wp-scripts build src/frontend/index.js --output-path=build/frontend --output-filename=frontend.js", "check-engines": "wp-scripts check-engines", "check-licenses": "wp-scripts check-licenses", "format": "wp-scripts format", @@ -13,7 +15,9 @@ "lint:pkg-json": "wp-scripts lint-pkg-json", "packages-update": "wp-scripts packages-update", "plugin-zip": "wp-scripts plugin-zip", - "start": "wp-scripts start", + "start": "npm-run-all --parallel start:*", + "start:backend": "wp-scripts start src/index.js --output-path=build/backend --output-filename=index.js", + "start:frontend": "wp-scripts start src/frontend/index.js --output-path=build/frontend --output-filename=frontend.js", "test:e2e": "wp-scripts test-e2e", "test:unit": "wp-scripts test-unit-js", "dist": "bash bin/dist.sh" @@ -30,10 +34,16 @@ "homepage": "https://github.com/Codeinwp/quickwp#readme", "devDependencies": { "@wordpress/scripts": "^26.19.0", + "eslint-config-wordpress": "^2.0.0", + "npm-run-all": "^4.1.5", "simple-git-hooks": "^2.9.0", "tailwindcss": "^3.4.0" }, "simple-git-hooks": { "pre-commit": "npm run lint:js && composer run-script lint && composer run-script phpstan" + }, + "dependencies": { + "@wordpress/icons": "^9.39.0", + "classnames": "^2.3.2" } } diff --git a/phpstan.neon b/phpstan.neon index 743b918..2dbc2a1 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,5 +4,8 @@ parameters: - %currentWorkingDirectory%/inc bootstrapFiles: - %currentWorkingDirectory%/tests/php/static-analysis-stubs/quickwp.php + checkGenericClassInNonGenericObjectType: false + dynamicConstantNames: + - QUICKWP_APP_GUIDED_MODE includes: - %currentWorkingDirectory%/vendor/szepeviktor/phpstan-wordpress/extension.neon \ No newline at end of file diff --git a/postcss.config.js b/postcss.config.js index 16e7961..0e0e5d7 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,3 +1,3 @@ module.exports = { - plugins: [ require( 'tailwindcss' ) ], + plugins: [ require( 'tailwindcss' ) ] }; diff --git a/quickwp.php b/quickwp.php index c935f7e..9c3fd20 100644 --- a/quickwp.php +++ b/quickwp.php @@ -17,12 +17,20 @@ die; } -define( 'QUICKWP_BASEFILE', __FILE__ ); -define( 'QUICKWP_URL', plugins_url( '/', __FILE__ ) ); -define( 'QUICKWP_PATH', __DIR__ ); -define( 'QUICKWP_VERSION', '1.0.0' ); +define( 'QUICKWP_APP_BASEFILE', __FILE__ ); +define( 'QUICKWP_APP_URL', plugins_url( '/', __FILE__ ) ); +define( 'QUICKWP_APP_PATH', __DIR__ ); +define( 'QUICKWP_APP_VERSION', '1.0.0' ); -$vendor_file = QUICKWP_PATH . '/vendor/autoload.php'; +if ( ! defined( 'QUICKWP_APP_API' ) ) { + define( 'QUICKWP_APP_API', 'https://quickwp.com/api/' ); +} + +if ( ! defined( 'QUICKWP_APP_GUIDED_MODE' ) ) { + define( 'QUICKWP_APP_GUIDED_MODE', false ); +} + +$vendor_file = QUICKWP_APP_PATH . '/vendor/autoload.php'; if ( is_readable( $vendor_file ) ) { require_once $vendor_file; diff --git a/src/App.js b/src/App.js index b9ba92b..bb18326 100644 --- a/src/App.js +++ b/src/App.js @@ -1,11 +1,91 @@ /** * WordPress dependencies. */ +import { __ } from '@wordpress/i18n'; + +import { + useDispatch, + useSelect +} from '@wordpress/data'; + +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import { useIsSiteEditorLoading } from './hooks'; +import Loader from './components/Loader'; +import Header from './parts/Header'; +import { recordEvent } from './utils'; + const App = () => { + const isEditorLoading = useIsSiteEditorLoading(); + + const { toggle } = useDispatch( 'core/preferences' ); + + const { + currentStep, + hasError, + hasWelcome + } = useSelect( select => { + const { + getStep, + hasError + } = select( 'quickwp/data' ); + + const { get } = select( 'core/preferences' ); + + return { + currentStep: getStep(), + hasError: hasError(), + hasWelcome: get( 'core/edit-site', 'welcomeGuide' ) + }; + }); + + useEffect( () => { + if ( hasWelcome ) { + toggle( 'core/edit-site', 'welcomeGuide' ); + } + + recordEvent(); + }, [ hasWelcome ]); + + const StepControls = currentStep?.view || null; + + if ( isEditorLoading ) { + return ( +
+ +
+ ); + } + + if ( hasError ) { + return ( +
+

+ { __( + 'There has been an error. Please refresh the page and try again.', + 'quickwp' + ) } +

+
+ ); + } + return ( -
-

QuickWP

-

This is an example starter modal.

+
+
+
); }; diff --git a/src/components/ColorPicker.js b/src/components/ColorPicker.js new file mode 100644 index 0000000..31e979d --- /dev/null +++ b/src/components/ColorPicker.js @@ -0,0 +1,47 @@ +/** + * WordPress dependencies. + */ +import { + ColorIndicator, + ColorPicker as WPColorPicker, + Popover +} from '@wordpress/components'; + +import { useState } from '@wordpress/element'; + +const ColorPicker = ({ + value, + onChange +}) => { + const [ isOpen, setIsOpen ] = useState( false ); + + return ( + { + if ( ! e.target.classList.contains( 'component-color-indicator' ) ) { + return; + } + + setIsOpen( ! isOpen ); + } } + > + { isOpen && ( + setIsOpen( false ) } + > + + + ) } + + ); +}; + +export default ColorPicker; diff --git a/src/components/Loader.js b/src/components/Loader.js new file mode 100644 index 0000000..b291b21 --- /dev/null +++ b/src/components/Loader.js @@ -0,0 +1,53 @@ +/** + * External dependencies. + */ +import classNames from 'classnames'; + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { memo, useEffect, useMemo, useState } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import { PageControlIcon } from '../utils'; + +import STEPS from '../steps'; + +const Loader = () => { + const [ currentPage, setCurrentPage ] = useState( 0 ); + const numberOfPages = useMemo( () => STEPS.length, []); + + useEffect( () => { + const interval = setInterval( () => { + setCurrentPage( ( prevPage ) => + prevPage === numberOfPages - 1 ? 0 : prevPage + 1 + ); + }, 500 ); + + return () => clearInterval( interval ); + }, [ numberOfPages ]); + + return ( +
    + { Array.from({ length: numberOfPages }).map( ( _, page ) => ( +
  • + +
  • + ) ) } +
+ ); +}; + +export default memo( Loader ); diff --git a/src/components/PageControl.js b/src/components/PageControl.js new file mode 100644 index 0000000..504e284 --- /dev/null +++ b/src/components/PageControl.js @@ -0,0 +1,53 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { Button } from '@wordpress/components'; + +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies. + */ +import { PageControlIcon } from '../utils'; +import STEPS from '../steps'; + +const numberOfPages = STEPS.length; + +const PageControl = () => { + const currentStep = useSelect( ( select ) => + select( 'quickwp/data' ).getStep() + ); + + const currentPage = STEPS.findIndex( + ( step ) => step.value === currentStep.value + ); + + return ( +
    + { Array.from({ length: numberOfPages }).map( ( _, page ) => ( +
  • +
  • + ) ) } +
+ ); +}; + +export default PageControl; diff --git a/src/components/TemplatePreview.js b/src/components/TemplatePreview.js new file mode 100644 index 0000000..29bf6e4 --- /dev/null +++ b/src/components/TemplatePreview.js @@ -0,0 +1,79 @@ +/** + * External dependencies. + */ +import classNames from 'classnames'; + +/** + * WordPress dependencies. + */ +import { parse } from '@wordpress/blocks'; + +import { BlockPreview } from '@wordpress/block-editor'; + +import { + useMemo, + useRef +} from '@wordpress/element'; + +const TemplatePreview = ({ + template, + canScroll = false, + isSelected = false, + className = '', + onClick +}) => { + const previewRef = useRef( null ); + + const parsedTemplate = useMemo( () => { + return Array.isArray( template ) ? template : parse( template ); + }, [ template ]); + + const scrollToBottom = () => { + const contentDocument = previewRef.current; + + if ( ! canScroll && contentDocument ) { + contentDocument.scrollTo({ + top: contentDocument.scrollHeight, + behavior: 'smooth' + }); + } + }; + + const scrollToTop = () => { + const contentDocument = previewRef.current; + + if ( ! canScroll && contentDocument ) { + contentDocument.scrollTo({ + top: 0, + behavior: 'smooth' + }); + } + }; + + return ( +
+ +
+ ); +}; + +export default TemplatePreview; diff --git a/src/frontend/index.js b/src/frontend/index.js new file mode 100644 index 0000000..f520333 --- /dev/null +++ b/src/frontend/index.js @@ -0,0 +1,76 @@ +/** + * WordPress dependencies. + */ +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies. + */ +import './style.scss'; + +const downloadBlob = ( filename, content, contentType = '' ) => { + if ( ! filename || ! content ) { + return; + } + + const file = new window.Blob([ content ], { type: contentType }); + const url = window.URL.createObjectURL( file ); + const anchorElement = document.createElement( 'a' ); + anchorElement.href = url; + anchorElement.download = filename; + anchorElement.style.display = 'none'; + document.body.appendChild( anchorElement ); + anchorElement.click(); + document.body.removeChild( anchorElement ); + window.URL.revokeObjectURL( url ); +}; + +const handleExport = async() => { + try { + const response = await apiFetch({ + path: '/wp-block-editor/v1/export', + parse: false, + headers: { + Accept: 'application/zip' + } + }); + const blob = await response.blob(); + const contentDisposition = response.headers.get( + 'content-disposition' + ); + const contentDispositionMatches = + contentDisposition.match( /=(.+)\.zip/ ); + const fileName = contentDispositionMatches[ 1 ] ? + contentDispositionMatches[ 1 ] : + 'edit-site-export'; + + downloadBlob( fileName + '.zip', blob, 'application/zip' ); + } catch ( errorResponse ) { + let error = {}; + try { + error = await errorResponse.json(); + } catch ( e ) {} + const errorMessage = + error.message && 'unknown_error' !== error.code ? + error.message : + __( 'An error occurred while creating the site export.', 'quickwp' ); + + console.error( errorMessage ); + } +}; + +const toolbar = document.createElement( 'div' ); + +toolbar.classList.add( 'quickwp-toolbar' ); + +const paragraph = document.createElement( 'p' ); +paragraph.textContent = 'You can download the ZIP file and install it to your WordPress site.'; +toolbar.appendChild( paragraph ); + +const button = document.createElement( 'button' ); +button.textContent = 'Download'; +toolbar.appendChild( button ); + +button.addEventListener( 'click', handleExport ); + +document.body.appendChild( toolbar ); diff --git a/src/frontend/style.scss b/src/frontend/style.scss new file mode 100644 index 0000000..1b5600d --- /dev/null +++ b/src/frontend/style.scss @@ -0,0 +1,39 @@ +.quickwp-toolbar { + position: fixed; + display: flex; + background: #000; + height: 80px; + bottom: 0; + width: 100%; + box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.75); + align-items: center; + justify-content: space-around; + gap: 12px; + color: #FFF; + + button { + width: fit-content; + cursor: pointer; + border-radius: 0.375rem; + border-width: 1px; + border-style: solid; + border-color: #FFF; + background-color: transparent; + padding-left: 1.5rem; + padding-right: 1.5rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + font-size: 1.125rem; + line-height: 1.75rem; + font-weight: 500; + font-style: normal; + color: #FFF; + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + + &:hover { + background-color: #FFF; + color: #000; + } + } +} \ No newline at end of file diff --git a/src/hooks/index.js b/src/hooks/index.js new file mode 100644 index 0000000..5ac4681 --- /dev/null +++ b/src/hooks/index.js @@ -0,0 +1,111 @@ +/** + * WordPress dependencies + */ +import { useEffect, useState } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { decodeEntities } from '@wordpress/html-entities'; + +const MAX_LOADING_TIME = 10000; // 10 seconds + +// This is a copy of the useEditedEntityRecord function from the Gutenberg plugin. +export const useEditedEntityRecord = ( postType, postId ) => { + const { record, title, description, isLoaded, icon } = useSelect( + ( select ) => { + const { getEditedPostType, getEditedPostId } = + select( 'core/edit-site' ); + const { getEditedEntityRecord, hasFinishedResolution } = + select( 'core' ); + const { __experimentalGetTemplateInfo: getTemplateInfo } = + select( 'core/editor' ); + const usedPostType = postType ?? getEditedPostType(); + const usedPostId = postId ?? getEditedPostId(); + const _record = getEditedEntityRecord( + 'postType', + usedPostType, + usedPostId + ); + const _isLoaded = + usedPostId && + hasFinishedResolution( 'getEditedEntityRecord', [ + 'postType', + usedPostType, + usedPostId + ]); + const templateInfo = getTemplateInfo( _record ); + + return { + record: _record, + title: templateInfo.title, + description: templateInfo.description, + isLoaded: _isLoaded, + icon: templateInfo.icon + }; + }, + [ postType, postId ] + ); + + return { + isLoaded, + icon, + record, + getTitle: () => ( title ? decodeEntities( title ) : null ), + getDescription: () => + description ? decodeEntities( description ) : null + }; +}; + +// This is a copy of the useIsSiteEditorLoading function from the Gutenberg plugin. +export const useIsSiteEditorLoading = () => { + const { isLoaded: hasLoadedPost } = useEditedEntityRecord(); + const [ loaded, setLoaded ] = useState( false ); + const inLoadingPause = useSelect( + ( select ) => { + const hasResolvingSelectors = + select( 'core' ).hasResolvingSelectors(); + return ! loaded && ! hasResolvingSelectors; + }, + [ loaded ] + ); + + /* + * If the maximum expected loading time has passed, we're marking the + * editor as loaded, in order to prevent any failed requests from blocking + * the editor canvas from appearing. + */ + useEffect( () => { + let timeout; + + if ( ! loaded ) { + timeout = setTimeout( () => { + setLoaded( true ); + }, MAX_LOADING_TIME ); + } + + return () => { + clearTimeout( timeout ); + }; + }, [ loaded ]); + + useEffect( () => { + if ( inLoadingPause ) { + + /* + * We're using an arbitrary 1s timeout here to catch brief moments + * without any resolving selectors that would result in displaying + * brief flickers of loading state and loaded state. + * + * It's worth experimenting with different values, since this also + * adds 1s of artificial delay after loading has finished. + */ + const timeout = setTimeout( () => { + setLoaded( true ); + }, 1000 ); + + return () => { + clearTimeout( timeout ); + }; + } + }, [ inLoadingPause ]); + + return ! loaded || ! hasLoadedPost; +}; diff --git a/src/index.js b/src/index.js index 0accca0..ded242e 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ import { registerPlugin } from '@wordpress/plugins'; * Internal dependencies. */ import './style.scss'; +import './store'; import App from './App'; const Render = () => { @@ -22,6 +23,6 @@ const hasFlag = urlParams.get( 'quickwp' ); // If the quickwp query string is present, render the quickwp modal. if ( 'true' === hasFlag ) { registerPlugin( 'quickwp', { - render: Render, - } ); + render: Render + }); } diff --git a/src/parts/ColorPalette.js b/src/parts/ColorPalette.js new file mode 100644 index 0000000..9ef1061 --- /dev/null +++ b/src/parts/ColorPalette.js @@ -0,0 +1,194 @@ +/** + * External dependencies. + */ +import classNames from 'classnames'; + +import { rotateRight } from '@wordpress/icons'; + +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { + Button, + Disabled, + Icon, + Spinner +} from '@wordpress/components'; + +import { + useDispatch, + useSelect +} from '@wordpress/data'; + +import { + useEffect, + useState +} from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import { generateColorPalette } from '../utils'; +import ColorPicker from '../components/ColorPicker'; +import TemplatePreview from '../components/TemplatePreview'; + +const ColorPalette = () => { + const [ isRegenerating, setIsRegenerating ] = useState( false ); + + const { onContinue } = useDispatch( 'quickwp/data' ); + + const { + globalStylesId, + defaultStyles, + palette, + template, + hasLoaded + } = useSelect( ( select ) => { + const { + __experimentalGetCurrentGlobalStylesId, + __experimentalGetCurrentThemeBaseGlobalStyles + } = select( 'core' ); + + const { + getColorPalette, + getProcessStatus, + getHomepage + } = select( 'quickwp/data' ); + + const globalStylesId = __experimentalGetCurrentGlobalStylesId(); + + return { + globalStylesId, + defaultStyles: __experimentalGetCurrentThemeBaseGlobalStyles(), + palette: getColorPalette(), + template: getHomepage() || [], + hasLoaded: true === getProcessStatus( 'color_palette' ) && true === getProcessStatus( 'homepage' ) + }; + }); + + const { editEntityRecord } = useDispatch( 'core' ); + + const { setColorPalette } = useDispatch( 'quickwp/data' ); + + useEffect( () => { + if ( hasLoaded && Boolean( palette.length ) ) { + const colorPalette = palette.map( color => { + const paletteColor = defaultStyles.settings.color.palette.theme.find( paletteColor => paletteColor.slug === color.slug ); + + if ( paletteColor ) { + return { + ...color, + name: paletteColor.name + }; + } + + return color; + }); + + const settings = { + color: { + palette: { + theme: [ + ...colorPalette + ] + } + } + }; + + editEntityRecord( 'root', 'globalStyles', globalStylesId, { + settings + }); + } + }, [ hasLoaded, palette ]); + + const onChangeColor = ( value, slug ) => { + const newPalette = palette.map( color => { + if ( color.slug === slug ) { + return { + ...color, + color: value + }; + } + + return color; + }); + + setColorPalette( newPalette ); + }; + + const onSubmit = async() => { + onContinue(); + }; + + const onRegenerate = async() => { + setIsRegenerating( true ); + await generateColorPalette(); + setIsRegenerating( false ); + }; + + if ( ! hasLoaded ) { + return ( +
+ +
+ ); + } + + return ( +
+ +
+

+ { __( + 'Let\'s give your site a color that fits to your brand.', + 'quickwp' + ) } +

+ +
+ { palette.map( ( color ) => ( + onChangeColor( e, color.slug ) } + /> + ) ) } + + +
+ + +
+
+ + +
+ ); +}; + +export default ColorPalette; diff --git a/src/parts/Header.js b/src/parts/Header.js new file mode 100644 index 0000000..24f6765 --- /dev/null +++ b/src/parts/Header.js @@ -0,0 +1,16 @@ +/** + * Internal dependencies. + */ +import { Logo } from '../utils'; +import PageControl from '../components/PageControl'; + +const Header = () => { + return ( +
+ + +
+ ); +}; + +export default Header; diff --git a/src/parts/ImageSuggestions.js b/src/parts/ImageSuggestions.js new file mode 100644 index 0000000..e5ddf76 --- /dev/null +++ b/src/parts/ImageSuggestions.js @@ -0,0 +1,233 @@ +/** + * External dependencies. + */ +import classNames from 'classnames'; + +import { check } from '@wordpress/icons'; + +/** + * WordPress dependencies. + */ +import { + __, + sprintf +} from '@wordpress/i18n'; + +import { + Button, + Disabled, + Icon, + Spinner, + TextControl +} from '@wordpress/components'; + +import { + useDispatch, + useSelect +} from '@wordpress/data'; + +import { + useEffect, + useState +} from '@wordpress/element'; + +import { ENTER } from '@wordpress/keycodes'; + +/** + * Internal dependencies. + */ +import { requestImages } from '../utils'; + +const ImageSuggestions = () => { + const [ search, setSearch ] = useState( '' ); + const [ isLoading, setIsLoading ] = useState( false ); + const [ images, setImages ] = useState([]); + + const { + queuedImages, + imageKeywords, + activeImageKeyword, + selectedImages, + hasLoaded + } = useSelect( select => { + const { + getImages, + getImageKeywords, + getActiveImageKeyword, + getSelectedImages, + getProcessStatus + } = select( 'quickwp/data' ); + + const activeImageKeyword = getActiveImageKeyword(); + + const queuedImages = getImages( activeImageKeyword ) || []; + + return { + queuedImages, + imageKeywords: getImageKeywords() || [], + activeImageKeyword, + selectedImages: getSelectedImages() || [], + hasLoaded: true === getProcessStatus( 'images' ) + }; + }); + + const { + onContinue, + setActiveImageKeyword, + toggleSelectedImage + } = useDispatch( 'quickwp/data' ); + + const onSearch = async( query = search ) => { + if ( ! query || activeImageKeyword === query ) { + return; + } + + if ( query !== search ) { + setSearch( query ); + } + + setActiveImageKeyword( query ); + + setIsLoading( true ); + await requestImages( query ); + setIsLoading( false ); + }; + + useEffect( () => { + const newImages = [ + ...selectedImages, + ...queuedImages.filter( qImage => ! selectedImages.find( sImage => sImage.id === qImage.id ) ) + ]; + + if ( newImages.length !== images.length || ! newImages.every( ( img, index ) => img.id === images[index]?.id ) ) { + setImages( newImages ); + } + }, [ queuedImages ]); + + if ( ! hasLoaded ) { + return ( +
+ +
+ ); + } + + return ( +
+
+

+ { __( + 'Pick out images that you like, and we\'ll include them in the designs.', + 'quickwp' + ) } +

+ + { + if ( ENTER === e.keyCode ) { + onSearch(); + } + }} + disabled={ isLoading } + className="is-light mt-4" + /> + +
+ { imageKeywords.map( ( keyword ) => { + return ( + + ); + }) } +
+ + +
+ +
+ { ( isLoading ) && ( +
+ +
+ ) } + + +
+ { images.map( image => ( +
toggleSelectedImage( image ) } + > + { selectedImages.includes( image ) && ( +
+ +
+ ) } + + { + +

+ + { sprintf( + __( + 'Photo by %s on Pexels', + 'quickwp' + ), + image.photographer + )} + +

+
+ ) ) } +
+
+
+
+ ); +}; + +export default ImageSuggestions; diff --git a/src/parts/SiteDescription.js b/src/parts/SiteDescription.js new file mode 100644 index 0000000..be89138 --- /dev/null +++ b/src/parts/SiteDescription.js @@ -0,0 +1,68 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { + Button, + TextareaControl +} from '@wordpress/components'; + +import { + useDispatch, + useSelect +} from '@wordpress/data'; + +const SiteDescription = () => { + const { siteDescription } = useSelect( select => { + return { + siteDescription: select( 'quickwp/data' ).getSiteDescription() + }; + }); + + const { + onContinue, + setSiteDescription + } = useDispatch( 'quickwp/data' ); + + const onSubmit = async() => { + if ( 4 > siteDescription?.length ) { + return; + } + + onContinue(); + }; + + return ( +
+

+ { __( + 'Great! We\'d love to learn more about your brand to create a tailor-made website just for you.', + 'quickwp' + ) } +

+ + + + +
+ ); +}; + +export default SiteDescription; diff --git a/src/parts/SiteTopic.js b/src/parts/SiteTopic.js new file mode 100644 index 0000000..82124ac --- /dev/null +++ b/src/parts/SiteTopic.js @@ -0,0 +1,77 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { + Button, + TextControl +} from '@wordpress/components'; + +import { + useDispatch, + useSelect +} from '@wordpress/data'; + +import { ENTER } from '@wordpress/keycodes'; + +const SiteTopic = () => { + const { siteTopic } = useSelect( select => { + return { + siteTopic: select( 'quickwp/data' ).getSiteTopic() + }; + }); + + const { + onContinue, + setSiteTopic + } = useDispatch( 'quickwp/data' ); + + const onSubmit = async() => { + if ( 4 > siteTopic?.length ) { + return; + } + + onContinue(); + }; + + const onEnter = ( e ) => { + if ( ENTER === e.keyCode && !! siteTopic ) { + onSubmit(); + } + }; + + return ( +
+

+ { __( + 'Welcome. What kind of site are you building?', + 'quickwp' + ) } +

+ + + + +
+ ); +}; + +export default SiteTopic; diff --git a/src/parts/Template.js b/src/parts/Template.js new file mode 100644 index 0000000..c756cfd --- /dev/null +++ b/src/parts/Template.js @@ -0,0 +1,92 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import { + Button, + Spinner +} from '@wordpress/components'; + +import { + useDispatch, + useSelect +} from '@wordpress/data'; + +/** + * Internal dependencies. + */ +import TemplatePreview from '../components/TemplatePreview'; + +const Template = () => { + const { + onContinue, + setSelectedTemplate + } = useDispatch( 'quickwp/data' ); + + const { + templates, + selectedTemplate, + hasLoaded + } = useSelect( ( select ) => { + const { + getTemplate, + isSaving, + getProcessStatus, + getSelectedTemplate + } = select( 'quickwp/data' ); + + const templates = getTemplate(); + + return { + templates, + selectedTemplate: getSelectedTemplate(), + hasLoaded: true === getProcessStatus( 'templates' ) + }; + }, []); + + if ( ! hasLoaded ) { + return ( +
+ +
+ ); + } + + return ( +
+
+

+ { __( + 'Pick a layout for your homepage.', + 'quickwp' + ) } +

+ + +
+ +
+ { templates.map( template => { + return ( + setSelectedTemplate( template.slug )} + isSelected={ template.slug === selectedTemplate } + className="aspect-vert" + /> + ); + }) } +
+
+ ); +}; + +export default Template; diff --git a/src/parts/ViewSite.js b/src/parts/ViewSite.js new file mode 100644 index 0000000..1cd18cd --- /dev/null +++ b/src/parts/ViewSite.js @@ -0,0 +1,132 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +import apiFetch from '@wordpress/api-fetch'; + +import { + Button, + Spinner +} from '@wordpress/components'; + +import { + useDispatch, + useSelect +} from '@wordpress/data'; + +const downloadBlob = ( filename, content, contentType = '' ) => { + if ( ! filename || ! content ) { + return; + } + + const file = new window.Blob([ content ], { type: contentType }); + const url = window.URL.createObjectURL( file ); + const anchorElement = document.createElement( 'a' ); + anchorElement.href = url; + anchorElement.download = filename; + anchorElement.style.display = 'none'; + document.body.appendChild( anchorElement ); + anchorElement.click(); + document.body.removeChild( anchorElement ); + window.URL.revokeObjectURL( url ); +}; + +const ViewSite = () => { + const { createErrorNotice } = useDispatch( 'core/notices' ); + + const { + isSaving + } = useSelect( ( select ) => { + const { isSaving } = select( 'quickwp/data' ); + + + return { + isSaving: isSaving() + }; + }, []); + + const handleExport = async() => { + try { + const response = await apiFetch({ + path: '/wp-block-editor/v1/export', + parse: false, + headers: { + Accept: 'application/zip' + } + }); + const blob = await response.blob(); + const contentDisposition = response.headers.get( + 'content-disposition' + ); + const contentDispositionMatches = + contentDisposition.match( /=(.+)\.zip/ ); + const fileName = contentDispositionMatches[ 1 ] ? + contentDispositionMatches[ 1 ] : + 'edit-site-export'; + + downloadBlob( fileName + '.zip', blob, 'application/zip' ); + } catch ( errorResponse ) { + let error = {}; + try { + error = await errorResponse.json(); + } catch ( e ) {} + const errorMessage = + error.message && 'unknown_error' !== error.code ? + error.message : + __( 'An error occurred while creating the site export.', 'quickwp' ); + + createErrorNotice( errorMessage, { type: 'snackbar' }); + } + }; + + if ( isSaving ) { + return ( +
+ +
+ ); + } + + return ( +
+
+

+ { __( + 'Your site is ready. Good job!', + 'quickwp' + ) } +

+ + { Boolean( window.quickwp.isGuidedMode ) && ( +

+ { __( + 'You can download the ZIP file and install it to your WordPress site.', + 'quickwp' + ) } +

+ ) } + +
+ { Boolean( window.quickwp.isGuidedMode ) && ( + + ) } + + +
+
+
+ ); +}; + +export default ViewSite; diff --git a/src/steps.js b/src/steps.js new file mode 100644 index 0000000..e60930d --- /dev/null +++ b/src/steps.js @@ -0,0 +1,49 @@ +/** + * WordPress dependencies. + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import SiteTopic from './parts/SiteTopic'; +import SiteDescription from './parts/SiteDescription'; +import ColorPalette from './parts/ColorPalette'; +import ImageSuggestions from './parts/ImageSuggestions'; +import Template from './parts/Template'; +import ViewSite from './parts/ViewSite'; + +const STEPS = [ + { + value: 'site_topic', + label: __( 'Site Topic', 'quickwp' ), + view: SiteTopic + }, + { + value: 'site_description', + label: __( 'Site Description', 'quickwp' ), + view: SiteDescription + }, + { + value: 'image_suggestions', + label: __( 'Image Suggestions', 'quickwp' ), + view: ImageSuggestions + }, + { + value: 'frontpage_template', + label: __( 'Front Page Template', 'quickwp' ), + view: Template + }, + { + value: 'color_palette', + label: __( 'Color Palette', 'quickwp' ), + view: ColorPalette + }, + { + value: 'view_site', + label: __( 'View Site', 'quickwp' ), + view: ViewSite + } +]; + +export default STEPS; diff --git a/src/store.js b/src/store.js new file mode 100644 index 0000000..3f30dfe --- /dev/null +++ b/src/store.js @@ -0,0 +1,434 @@ +/* eslint-disable camelcase */ + +/** + * WordPress dependencies. + */ +import { + createReduxStore, + register +} from '@wordpress/data'; + +/** + * Internal dependencies. + */ +import STEPS from './steps'; +import { + generateColorPalette, + generateImages, + generateTemplates, + recordEvent, + saveChanges +} from './utils'; + +const DEFAULT_STATE = { + step: 0, + processes: { + 'color_palette': { + 'thread_id': null, + 'run_id': null, + hasLoaded: false + }, + 'images': { + 'thread_id': null, + 'run_id': null, + hasLoaded: false + }, + 'templates': { + 'thread_id': null, + 'run_id': null, + hasLoaded: false + }, + 'homepage': { + 'thread_id': null, + 'run_id': null, + hasLoaded: false + } + }, + colorPalette: [], + images: {}, + imageKeywords: [], + activeImageKeyword: null, + selectedImages: [], + templates: [], + selectedTemplate: null, + homepage: null, + siteTopic: '', + siteDescription: '', + sessionID: '', + isSavimg: false, + hasError: false +}; + +const actions = { + setStep( step ) { + return { + type: 'SET_STEP', + step + }; + }, + nextStep() { + return ({ dispatch, select }) => { + const current = select.getStep(); + const stepIndex = STEPS.findIndex( + ( step ) => current.value === step.value + ); + const isLast = STEPS.length === stepIndex + 1; + const newStep = + STEPS[ isLast ? STEPS.length : stepIndex + 1 ]?.value; + + if ( isLast ) { + return; + } + + dispatch( actions.setStep( newStep ) ); + }; + }, + previousStep() { + return ({ dispatch, select }) => { + const current = select.getStep(); + const stepIndex = STEPS.findIndex( + ( step ) => current.value === step.value + ); + const isFirst = 0 === stepIndex; + const newStep = STEPS[ isFirst ? 0 : stepIndex - 1 ]?.value; + + dispatch( actions.setStep( newStep ) ); + }; + }, + onContinue() { + return ({ dispatch, select }) => { + const current = select.getStep(); + + const stepIndex = STEPS.findIndex( step => current.value === step.value ); + + recordEvent({ + step_id: stepIndex + 1, + step_status: 'completed' + }); + + switch ( current.value ) { + case 'site_topic': + generateColorPalette(); + break; + case 'site_description': + generateImages(); + break; + case 'image_suggestions': + generateTemplates( 'templates' ); + break; + case 'frontpage_template': + generateTemplates( 'homepage' ); + break; + case 'color_palette': + saveChanges(); + break; + } + + dispatch( actions.nextStep() ); + }; + }, + setSiteTopic( siteTopic ) { + return { + type: 'SET_SITE_TOPIC', + siteTopic + }; + }, + setSiteDescription( siteDescription ) { + return { + type: 'SET_SITE_DESCRIPTION', + siteDescription + }; + }, + setColorPalette( colorPalette ) { + return { + type: 'SET_COLOR_PALETTE', + colorPalette + }; + }, + setImages( key, images ) { + return { + type: 'SET_IMAGES', + key, + images + }; + }, + setImageKeywords( imageKeywords ) { + return { + type: 'SET_IMAGE_KEYWORDS', + imageKeywords + }; + }, + setActiveImageKeyword( activeImageKeyword ) { + return { + type: 'SET_ACTIVE_IMAGE_KEYWORD', + activeImageKeyword + }; + }, + toggleSelectedImage( image ) { + return ({ dispatch, select }) => { + const selectedImages = select.getSelectedImages(); + + if ( selectedImages.includes( image ) ) { + dispatch( actions.removeSelectedImage( image ) ); + } else { + dispatch( actions.addSelectedImage( image ) ); + } + }; + }, + addSelectedImage( image ) { + return { + type: 'ADD_SELECTED_IMAGE', + image + }; + }, + removeSelectedImage( image ) { + return { + type: 'REMOVE_SELECTED_IMAGE', + image + }; + }, + setTemplate( templates ) { + return { + type: 'SET_TEMPLATE', + templates + }; + }, + setSelectedTemplate( selectedTemplate ) { + return { + type: 'SET_SELECTED_TEMPLATE', + selectedTemplate + }; + }, + setHomepage( homepage ) { + return { + type: 'SET_HOMEPAGE', + homepage + }; + }, + setSessionID( sessionID ) { + return { + type: 'SET_SESSION_ID', + sessionID + }; + }, + setError( hasError ) { + return { + type: 'SET_ERROR', + hasError + }; + }, + setSaving( isSaving ) { + return { + type: 'SET_SAVING', + isSaving + }; + }, + setThreadID( item, threadID ) { + return { + type: 'SET_THREAD_ID', + item, + threadID + }; + }, + setRunID( item, runID ) { + return { + type: 'SET_RUN_ID', + item, + runID + }; + }, + setProcessStatus( item, hasLoaded ) { + return { + type: 'SET_PROCESS_STATUS', + item, + hasLoaded + }; + } +}; + +const store = createReduxStore( 'quickwp/data', { + reducer( state = DEFAULT_STATE, action ) { + switch ( action.type ) { + case 'SET_STEP': + const step = STEPS.findIndex( + ( step ) => step.value === action.step + ); + + return { + ...state, + step + }; + case 'SET_SITE_TOPIC': + return { + ...state, + siteTopic: action.siteTopic + }; + case 'SET_SITE_DESCRIPTION': + return { + ...state, + siteDescription: action.siteDescription + }; + case 'SET_COLOR_PALETTE': + return { + ...state, + colorPalette: action.colorPalette + }; + case 'SET_ERROR': + return { + ...state, + hasError: action.hasError + }; + case 'SET_SAVING': + return { + ...state, + isSaving: action.isSaving + }; + case 'SET_IMAGES': + return { + ...state, + images: { + ...state.images, + [ action.key ]: action.images + } + }; + case 'SET_IMAGE_KEYWORDS': + return { + ...state, + imageKeywords: action.imageKeywords + }; + case 'SET_ACTIVE_IMAGE_KEYWORD': + return { + ...state, + activeImageKeyword: action.activeImageKeyword + }; + case 'ADD_SELECTED_IMAGE': + return { + ...state, + selectedImages: [ + ...state.selectedImages, + action.image + ] + }; + case 'REMOVE_SELECTED_IMAGE': + return { + ...state, + selectedImages: state.selectedImages.filter( + ( image ) => image !== action.image + ) + }; + case 'SET_TEMPLATE': + return { + ...state, + templates: action.templates + }; + case 'SET_SELECTED_TEMPLATE': + return { + ...state, + selectedTemplate: action.selectedTemplate + }; + case 'SET_HOMEPAGE': + return { + ...state, + homepage: action.homepage + }; + case 'SET_THREAD_ID': + return { + ...state, + processes: { + ...state.processes, + [ action.item ]: { + ...state.processes[ action.item ], + 'thread_id': action.threadID + } + } + }; + case 'SET_RUN_ID': + return { + ...state, + processes: { + ...state.processes, + [ action.item ]: { + ...state.processes[ action.item ], + 'run_id': action.runID + } + } + }; + case 'SET_SESSION_ID': + return { + ...state, + sessionID: action.sessionID + }; + case 'SET_PROCESS_STATUS': + return { + ...state, + processes: { + ...state.processes, + [ action.item ]: { + ...state.processes[ action.item ], + hasLoaded: action.hasLoaded + } + } + }; + } + + return state; + }, + + actions, + + selectors: { + getStep( state ) { + return STEPS[ state.step ]; + }, + getSiteTopic( state ) { + return state.siteTopic; + }, + getSiteDescription( state ) { + return state.siteDescription; + }, + getColorPalette( state ) { + return state.colorPalette; + }, + getImages( state, key ) { + return state.images[ key ]; + }, + getImageKeywords( state ) { + return state.imageKeywords; + }, + getActiveImageKeyword( state ) { + return state.activeImageKeyword; + }, + getSelectedImages( state ) { + return state.selectedImages; + }, + getTemplate( state ) { + return state.templates; + }, + getSelectedTemplate( state ) { + return state.selectedTemplate; + }, + getHomepage( state ) { + return state.homepage; + }, + getSessionID( state ) { + return state.sessionID; + }, + hasError( state ) { + return state.hasError; + }, + isSaving( state ) { + return state.isSaving; + }, + getThreadID( state, item ) { + return state.processes[ item ]?.thread_id; + }, + getRunID( state, item ) { + return state.processes[ item ]?.run_id; + }, + getProcessStatus( state, item ) { + return state.processes[ item ]?.hasLoaded; + } + } +}); + +register( store ); diff --git a/src/style.scss b/src/style.scss index da1a97c..9fd419e 100644 --- a/src/style.scss +++ b/src/style.scss @@ -1,2 +1,78 @@ +@import 'tailwindcss/base'; @import 'tailwindcss/components'; @import 'tailwindcss/utilities'; + +#quickwp { + --wp-admin-theme-color: #ffffff; + + .components-base-control { + .components-text-control__input { + @apply bg-transparent max-w-xl border-0 h-12 text-2xl not-italic font-normal text-fg shadow-none; + } + + .components-textarea-control__input { + @apply bg-transparent max-w-5xl h-24 border-0 text-2xl not-italic font-normal text-fg shadow-none resize-none; + } + + &.is-light { + .components-text-control__input { + @apply text-black bg-white h-10 w-full max-w-full text-xs p-3; + + &::placeholder { + @apply text-black/50; + } + } + } + } + + .components-text-control__input::placeholder, + .components-textarea-control__input::placeholder { + @apply text-fg/50 shadow-none; + } + + .components-button { + &.is-primary { + @apply bg-transparent w-fit h-auto border border-solid border-fg text-fg rounded-md px-6 py-4 transition-all text-lg not-italic font-medium; + + &:hover { + @apply bg-fg text-bg; + } + + &:disabled { + @apply opacity-50; + } + + .components-spinner { + @apply m-0 ml-3; + } + } + + &.is-secondary { + @apply bg-secondary w-fit h-auto text-fg rounded-md px-6 py-4 transition-all text-lg not-italic font-medium shadow-none; + + &:hover { + @apply bg-fg text-bg; + } + + &:disabled { + @apply opacity-50; + } + } + + &.is-token { + @apply text-xs leading-none px-4 py-2; + + &.is-active { + @apply bg-active text-white; + } + } + } + + .block-editor-block-preview__content { + max-height: none !important; + + iframe { + max-height: none !important; + } + } +} diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..2a6f662 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,470 @@ +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + +import { + dispatch, + select +} from '@wordpress/data'; + +import { + Circle, + Path, + Rect, + SVG +} from '@wordpress/primitives'; + +import { addQueryArgs } from '@wordpress/url'; + +export const PageControlIcon = ({ isFilled = false }) => { + return ( + + { isFilled ? ( + + ) : ( + + ) } + + ); +}; + +export const Logo = () => { + return ( + + + + + ); +}; + +/** + * Sometimes OpenAI requests fail, so we try to redo them if that happens. + */ +const retryApiFetch = async( params = [], maxAttempts = 3, delay = 500 ) => { + const { setError } = dispatch( 'quickwp/data' ); + + let attempts = 0; + + const makeRequest = async() => { + try { + + const response = await apiFetch({ + ...params + }); + + return response; + } catch ( error ) { + + attempts++; + + if ( attempts < maxAttempts ) { + await new Promise( resolve => setTimeout( resolve, delay ) ); + return makeRequest(); + } else { + setError( true ); + throw error; + } + } + }; + + return makeRequest(); +}; + +const sendEvent = async( data ) => { + const { + setRunID, + setThreadID + } = dispatch( 'quickwp/data' ); + + const response = await retryApiFetch({ + path: `${ window.quickwp.api }/send`, + method: 'POST', + data: { ...data } + }); + + setThreadID( data.step, response.thread_id ); + setRunID( data.step, response.id ); +}; + +const getEvent = async( type, params = {}) => { + const threadID = select( 'quickwp/data' ).getThreadID( type ); + + let route = ''; + + switch ( type ) { + case 'homepage': + route = 'homepage'; + break; + case 'templates': + route = 'templates'; + break; + default: + route = 'get'; + break; + } + + const response = await retryApiFetch({ + path: addQueryArgs( `${ window.quickwp.api }/${ route }`, { + 'thread_id': threadID, + ...params + }) + }); + + return response; +}; + +const getEventStatus = async( type ) => { + const threadID = select( 'quickwp/data' ).getThreadID( type ); + const runID = select( 'quickwp/data' ).getRunID( type ); + const { setProcessStatus } = dispatch( 'quickwp/data' ); + + const response = await retryApiFetch({ + path: addQueryArgs( `${ window.quickwp.api }/status`, { + 'thread_id': threadID, + 'run_id': runID + }) + }); + + if ( 'completed' !== response.status ) { + return false; + } + + setProcessStatus( type, true ); + + return true; +}; + +const extractPalette = response => { + const runID = select( 'quickwp/data' ).getRunID( 'color_palette' ); + const { setError } = dispatch( 'quickwp/data' ); + + const { data } = response; + + const target = data.find( item => item.run_id === runID ); + + const jsonString = target.content[0].text.value; + + let matches = jsonString.match( /\[(.|\n)*?\]/ ); + + if ( matches && matches[0]) { + let jsonArrayString = matches[0]; + + let jsonObject; + try { + jsonObject = JSON.parse( jsonArrayString ); + } catch ( error ) { + setError( true ); + return false; + } + + return jsonObject; + } else { + setError( true ); + return false; + } +}; + +const fetchImages = async( request ) => { + const runID = select( 'quickwp/data' ).getRunID( 'images' ); + const { + setActiveImageKeyword, + setImageKeywords + } = dispatch( 'quickwp/data' ); + + const { data } = request; + + const target = data.find( item => item.run_id === runID ); + + let queries = target.content[0].text.value; + + queries = queries.split( ',' ); + + queries = queries.map( query => query.trim() ); + + const query = queries[0]; + + setImageKeywords( queries ); + setActiveImageKeyword( query ); + + await requestImages( query ); +}; + +const awaitEvent = async( type, interval = 5000 ) => { + const hasResolved = await getEventStatus( type ); + + if ( ! hasResolved ) { + await new Promise( resolve => setTimeout( resolve, interval ) ); + await awaitEvent( type, interval ); + return; + } +}; + +export const requestImages = async( query ) => { + const { setImages } = dispatch( 'quickwp/data' ); + + const response = await retryApiFetch({ + path: addQueryArgs( `${ window.quickwp.api }/images`, { + query + }) + }); + + setImages( query, response?.photos ); +}; + +export const generateColorPalette = async() => { + const siteTopic = select( 'quickwp/data' ).getSiteTopic(); + + const { + setColorPalette, + setProcessStatus + } = dispatch( 'quickwp/data' ); + + await sendEvent({ + step: 'color_palette', + message: siteTopic + }); + + await awaitEvent( 'color_palette' ); + + const response = await getEvent( 'color_palette' ); + + const palette = extractPalette( response ); + + setColorPalette( palette ); + setProcessStatus( 'color_palette', true ); +}; + +export const generateImages = async() => { + const siteDescription = select( 'quickwp/data' ).getSiteDescription(); + + const { setProcessStatus } = dispatch( 'quickwp/data' ); + + await sendEvent({ + step: 'images', + message: siteDescription + }); + + await awaitEvent( 'images', 10000 ); + + const response = await getEvent( 'images' ); + + await fetchImages( response ); + + setProcessStatus( 'images', true ); +}; + +export const generateTemplates = async( type ) => { + const siteTopic = select( 'quickwp/data' ).getSiteTopic(); + const siteDescription = select( 'quickwp/data' ).getSiteDescription(); + + const images = select( 'quickwp/data' ).getSelectedImages(); + const activeImageKeyword = select( 'quickwp/data' ).getActiveImageKeyword(); + const defaultImages = select( 'quickwp/data' ).getImages( activeImageKeyword ); + const homepage = select( 'quickwp/data' ).getSelectedTemplate(); + + const selectedImages = ( ! images.length && defaultImages.length ) ? defaultImages.slice( 0, 10 ) : images; + + let imagesAr = selectedImages.map( image => ({ + src: image.src.original, + alt: image.alt + }) ); + + const { + setError, + setProcessStatus, + setHomepage, + setTemplate + } = dispatch( 'quickwp/data' ); + + let response; + + if ( 'homepage' === type ) { + await sendEvent({ + step: 'homepage', + message: `Website topic: ${ siteTopic } | Website description: ${ siteDescription } | Images: ${ JSON.stringify( imagesAr ) }`, + template: homepage + }); + + await awaitEvent( 'homepage', 10000 ); + + response = await getEvent( 'homepage', { + template: homepage + }); + } else { + await sendEvent({ + step: 'templates', + message: `Website topic: ${ siteTopic } | Website description: ${ siteDescription }` + }); + + await awaitEvent( 'templates', 10000 ); + + response = await getEvent( 'templates', { + images: imagesAr + }); + } + + if ( 'success' !== response?.status ) { + setError( true ); + return; + } + + if ( 'homepage' === type ) { + const homepageTemplate = formatHomepage( response.data ); + + setHomepage( homepageTemplate ); + setProcessStatus( 'homepage', true ); + } else { + let homepageTemplates = []; + + response.data.forEach( item => { + let homepageTemplate = formatHomepage( item.patterns ); + + const template = { + slug: item.slug, + content: homepageTemplate + }; + + homepageTemplates.push( template ); + }); + + setTemplate( homepageTemplates ); + setProcessStatus( 'templates', true ); + } +}; + +const importTemplate = async() => { + const currentTemplate = select( 'core/edit-site' ).getEditedPostId(); + + const { getHomepage } = select( 'quickwp/data' ); + + const { editEntityRecord } = dispatch( 'core' ); + + const homepage = getHomepage(); + + await editEntityRecord( 'postType', 'wp_template', currentTemplate, { + 'content': homepage + }); +}; + +export const saveChanges = async() => { + const { __experimentalGetDirtyEntityRecords } = select( 'core' ); + + const { saveEditedEntityRecord } = dispatch( 'core' ); + + const { setSaving } = dispatch( 'quickwp/data' ); + + setSaving( true ); + + importTemplate(); + + const edits = __experimentalGetDirtyEntityRecords(); + + await Promise.all( edits.map( async edit => { + await saveEditedEntityRecord( edit.kind, edit.name, edit?.key ); + }) ); + + setSaving( false ); +}; + +const formatHomepage = template => { + const slug = window.quickwp.themeSlug; + let homepageTemplate = ''; + homepageTemplate += ''; + homepageTemplate += template; + homepageTemplate += ''; + + return homepageTemplate; +}; + +export const recordEvent = async( data = {}) => { + if ( ! Boolean( window.quickwp.isGuidedMode ) ) { + return; + } + + const { setSessionID } = dispatch( 'quickwp/data' ); + const { getSessionID } = select( 'quickwp/data' ); + + const trackingId = getSessionID(); + + try { + const response = await fetch( + 'https://api.themeisle.com/tracking/onboarding', + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + _id: trackingId, + data: { + slug: 'quickwp', + // eslint-disable-next-line camelcase + license_id: 'free', + site: '', + ...data + } + }) + } + ); + + if ( ! response.ok ) { + console.error( `HTTP error! Status: ${ response.status }` ); + return false; + } + + const jsonResponse = await response.json(); + + const validCodes = [ 'success', 'invalid' ]; // Add valid codes to this array + + if ( ! validCodes.includes( jsonResponse.code ) ) { + return false; + } + + if ( 'invalid' === jsonResponse.code ) { + console.error( jsonResponse.message ); + return false; + } + const responseData = jsonResponse.data; + + if ( responseData?.id && '' === trackingId ) { + setSessionID( responseData.id ); + } + + return responseData.id || false; + } catch ( error ) { + console.error( error ); + return false; + } +}; diff --git a/tailwind.config.js b/tailwind.config.js index 7eddf58..5cd7db8 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,8 +1,25 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: [ './src/*.js' ], + content: [ './src/style.scss', './src/*.js', './src/**/*.js' ], theme: { - extend: {}, + extend: { + aspectRatio: { + vert: '3/4' + }, + colors: { + bg: '#000000', + fg: '#FFFFFF', + secondary: '#232323', + 'bg-alt': '#2F2F2F', + 'fg-alt': '#E0E0E0', + active: '#4663F8' + }, + maxHeight: { + 'md': '32rem', + '40vh': '40vh', + '80vh': '80vh' + } + } }, - plugins: [], + plugins: [] }; diff --git a/tests/php/static-analysis-stubs/quickwp.php b/tests/php/static-analysis-stubs/quickwp.php index 109fd56..752caee 100644 --- a/tests/php/static-analysis-stubs/quickwp.php +++ b/tests/php/static-analysis-stubs/quickwp.php @@ -5,7 +5,8 @@ * Adds QuickWP constants for PHPStan to use. */ -define( 'QUICKWP_BASEFILE', __FILE__ ); -define( 'QUICKWP_URL', plugins_url( '/', __FILE__ ) ); -define( 'QUICKWP_PATH', dirname( __FILE__ ) ); -define( 'QUICKWP_VERSION', '1.0.0' ); \ No newline at end of file +define( 'QUICKWP_APP_BASEFILE', __FILE__ ); +define( 'QUICKWP_APP_URL', plugins_url( '/', __FILE__ ) ); +define( 'QUICKWP_APP_PATH', dirname( __FILE__ ) ); +define( 'QUICKWP_APP_VERSION', '1.0.0' ); +define( 'QUICKWP_APP_API', 'quickwp/v1' );