*/ class Reactpress_Admin { /** * The ID of this plugin. * * @since 1.0.0 * @access private * @var string $plugin_name The ID of this plugin. */ private $plugin_name; /** * The version of this plugin. * * @since 1.0.0 * @access private * @var string $version The current version of this plugin. */ private $version; /** * Initialize the class and set its properties. * * @since 1.0.0 * @param string $plugin_name The name of this plugin. * @param string $version The version of this plugin. */ public function __construct($plugin_name, $version) { $this->plugin_name = $plugin_name; $this->version = $version; } /** * Register the stylesheets for the admin area. * * @since 1.0.0 */ public function enqueue_styles() { $valid_pages = ['reactpress']; $page = sanitize_title($_REQUEST['page'] ?? ""); if (in_array($page, $valid_pages)) { wp_enqueue_style($this->plugin_name, plugin_dir_url(__FILE__) . 'css/reactpress-admin.css', array(), $this->version, 'all'); } } /** * Register the JavaScript for the admin area. * * @since 1.0.0 */ public function enqueue_scripts() { $valid_pages = ['reactpress']; $page = sanitize_title($_REQUEST['page'] ?? ""); if (in_array($page, $valid_pages)) { wp_enqueue_script('rp-jquery-validate', plugin_dir_url(__FILE__) . 'js/jquery.validate.min.js', array('jquery'), $this->version, false); wp_enqueue_script($this->plugin_name, plugin_dir_url(__FILE__) . 'js/reactpress-admin.js', array('jquery'), $this->version, false); wp_localize_script($this->plugin_name, "rp", array( 'ajaxurl' => admin_url('admin-ajax.php'), 'apps' => get_option(('repr_apps')) )); } } /** * Add admin menu. * * @return void * @since 1.0.0 */ public function add_admin_menu() { add_menu_page( 'ReactPress', 'ReactPress', 'manage_options', 'reactpress', fn () => require_once('partials/reactpress-admin-display.php'), REPR_MENU_ICON ); } /** * Add page template. * (C) https://www.pradipdebnath.com/2019/08/17/how-to-add-page-template-from-plugin-in-wordpress/ * * @param array $templates The list of page templates * @return array $templates The modified list of page templates* * @since 1.0.0 */ public function repr_add_page_template($templates) { $templates[REPR_PLUGIN_PATH . 'templates/react-page-template.php'] = __('Page Template From ReactPress', 'text-domain'); return $templates; } public function repr_handle_admin_ajax_request() { /** * Handles all request from the admin frontend. * * @since 1.0.0 */ global $wpdb; $appname = strtolower(sanitize_file_name($_POST['appname'] ?? '')); $repr_apps = get_option('repr_apps') ?? []; $pageslug = sanitize_title_for_query($_POST['pageslug'] ?? ''); $param = sanitize_file_name($_REQUEST['param'] ?? ""); $template = sanitize_file_name($_POST['template'] ?? ''); $type = sanitize_file_name($_POST['type'] ?? 'development'); try { if (!empty($param)) { if ($param === "create_react_app" && $appname && $pageslug) { $existing_appnames = array_map(fn ($el) => $el['appname'], $repr_apps); $does_pageslug_exist = !empty($wpdb->get_row( $wpdb->prepare("SELECT * FROM " . $wpdb->prefix . "posts WHERE post_name = %s", $pageslug) )); if (in_array($appname, $existing_appnames) || $does_pageslug_exist) { echo wp_json_encode([ 'status' => 0, 'message' => "App name or page slug already exists.", ]); } else { $this->create_new_app($repr_apps, $appname, $pageslug, $template, $type); $this->insert_react_page($appname, $pageslug); if ($type === 'development') { $this->write_index_html($appname, $this->get_index_html_content($pageslug)); } echo wp_json_encode([ 'status' => 1, 'message' => "React app created.", 'appname' => $appname, 'pageslug' => $pageslug ]); } } elseif ($param === "start_react_app" && $appname && $pageslug) { $this->write_index_html($appname, $this->get_index_html_content($pageslug)); $parts = $this->start_react_app($appname); $parts; if (empty($parts)) { echo wp_json_encode(['status' => 0, 'message' => "Couldn't load app."]); } else { echo wp_json_encode([ 'status' => 1, 'ip' => substr($parts[1], 2), 'message' => 'App started.', 'port' => $parts[2], 'protocol' => $parts[0], ]); } } elseif ($param === 'stop_react_app' && $appname) { $this->stop_react_app($appname); echo wp_json_encode([ 'status' => 1, 'message' => 'App stopped.', ]); } elseif ($param === 'is_react_app_running' && $appname) { if ($this->is_react_app_running($appname)) { $parts = $this->get_app_uri($this->app_path($appname), 1); echo wp_json_encode([ 'status' => 1, 'ip' => substr($parts[1], 2), 'message' => 'App is running.', 'port' => $parts[2], 'protocol' => $parts[0], ]); } } elseif ($param === 'delete_react_app' && $appname) { if ($this->is_react_app_running($appname)) { $this->stop_react_app($appname); } $options = get_option('repr_apps'); $is_option_deleted = update_option('repr_apps', array_filter( $options, fn ($el) => $el['appname'] !== $appname )); $is_appdir_removed = repr_delete_directory($this->app_path($appname)); if ($is_option_deleted && $is_option_deleted) { echo wp_json_encode([ 'status' => 1, 'message' => 'App deleted.', ]); } elseif ($is_option_deleted) { echo wp_json_encode([ 'status' => 1, 'message' => "Couldn't remove files. Please remove directory by hand.", ]); } elseif ($is_appdir_removed) { echo wp_json_encode([ 'status' => 0, 'message' => "Couldn't remove app from database. Please try again later.", ]); } else { echo wp_json_encode([ 'status' => 0, 'message' => 'App not running.', ]); } } elseif ($param === "build_react_app" && $appname && $pageslug) { if ($this->build_react_app($appname)) { echo wp_json_encode([ 'status' => 1, 'message' => "App {$appname} build. View", ]); } else { echo wp_json_encode([ 'status' => 0, 'message' => "Build failed.", ]); } } else { echo wp_json_encode([ 'status' => -1, 'message' => "Request unknown.", ]); } } } catch (Exception $e) { echo (wp_json_encode([ 'status' => 0, 'message' => $e->getMessage(), 'code' => $e->getCode(), // User-defined Exception code 'filename' => $e->getFile(), // Source filename 'line' => $e->getLine(), // Source line 'trace' => $e->getTraceAsString(), // Formated string of trace ])); } finally { wp_die(); } } /** * Downloads the content of the page with the given pageslug and removes the * the react assets of the build page, that we can use the content for our * development server. * * @param string $pageslug * @return string * @since 1.0.0 */ public function get_index_html_content(string $pageslug) { $file_contents = file_get_contents(site_url() . '/' . $pageslug); $file_contents_arr = explode(PHP_EOL, $file_contents); // filter all build assets out of the file, that they don't conflict // with the dev assets. $filtered_arr = array_filter($file_contents_arr, fn ($el) => !strpos($el, "id='rp-react-app-asset-")); $filtered_contents = implode(PHP_EOL, $filtered_arr); return $filtered_contents; } /** * Writes the given to the index.html file in the app directory of * the given appname. * * @param string $appname * @param string $content * @return bool if the writing of the file succeded or not * @since 1.0.0 */ public function write_index_html(string $appname, string $content) { $index_html_path = sprintf("%sapps/%s/public/index.html", REPR_PLUGIN_PATH, $appname); return file_put_contents($index_html_path, $content); } /** * Writes a new app to the database and starts the creation of the app * in the filesystem. * * @param mixed $repr_apps * @param string $appname * @param string $pageslug * @param string $template * @param string $type * @return void * @since 1.0.0 */ function create_new_app( $repr_apps, string $appname, string $pageslug, string $template, string $type ) { if (!is_array($repr_apps) && $appname && $pageslug && $type) { add_option('repr_apps', [[ 'appname' => $appname, 'pageslug' => $pageslug, 'type' => $type, ]]); } elseif ($appname && $pageslug) { update_option('repr_apps', $this->array_add($repr_apps, [ 'appname' => $appname, 'pageslug' => $pageslug, 'type' => $type, ])); } if ($appname && $pageslug && $type) { return $this->create_react_app($appname, $type, $template,); } else { return true; } } /** * Start create-react-app or just creates the directory for the given * appname. * * @param string $appname * @param string $type * @param string $template * @return void * @since 1.0.0 */ function create_react_app(string $appname, string $type, string $template = '') { $output = []; $retval = ''; $template_option = $template ? " --template {$template}" : ''; chdir(REPR_PLUGIN_PATH); if ($type === 'deployment') { return mkdir("apps/{$appname}", 0777, true); } else { exec("npx create-react-app apps/{$appname}{$template_option}", $output, $retval); $this->fix_hot_reloading($appname); return end($output) === 'Happy hacking!' ? true : false; } } /** * Add .env file to fix hot reloading in VM. * See https://stackoverflow.com/questions/43274925/development-server-of-create-react-app-does-not-auto-refresh#answer-43281575 * * @param string $appname * @return void */ function fix_hot_reloading(string $appname) { $index_html_path = sprintf("%sapps/%s/.env", REPR_PLUGIN_PATH, $appname); $content = "# Fix hot reloading in VMs\nCHOKIDAR_USEPOLLING=true"; return file_put_contents($index_html_path, $content); } /** * Reads the React app uri out of the out.log file of the * app. Which is a splash screen of create-react-app. * * @param string $apppath the directory path of the app * @param integer $max_trys the number of trys to read the uri. * Important when out.log file is still being created. * @return array contains the protocol, the ip and the * port of the uri. * @since 1.0.0 */ function get_app_uri(string $apppath, int $max_trys = 1) { $regex = '/http:\/\/\d+\.\d+\.\d+\.\d+:\d*/'; $matches = []; $trys = 0; while (empty($matches) && $trys < $max_trys) { try { $file_content = file_get_contents("{$apppath}/out.log"); } catch (Exception $e) { return []; } preg_match($regex, $file_content, $matches); $trys += 1; if ($trys < $max_trys) { sleep(1); } } if (empty($matches)) { return []; } else { return explode(':', $matches[0]); } } /** * Builds the given react app, adds the right source path to * package.json, that react loads assets in the right way. * * @param string $appname * @return void * @since 1.0.0 */ function build_react_app($appname) { $apppath = $this->app_path($appname); // We need the relative path, that we can deploy our // build app to another server later. $relative_apppath = $this->app_path($appname, true); $homepage = "{$relative_apppath}/build"; $path_package_json = "{$apppath}/package.json"; $package_json_contents = file_get_contents($path_package_json); $package_json_arr_VAR = explode(PHP_EOL, $package_json_contents); // Add a homepage attribute during the build process and remove it again, // that the developer can build with the public/index.html without WP. array_splice($package_json_arr_VAR, 1, 0, ' "homepage": "' . $homepage . '",'); file_put_contents( $path_package_json, implode(PHP_EOL, $package_json_arr_VAR) ); chdir($apppath); shell_exec("npm run build"); array_splice($package_json_arr_VAR, 1, 1); file_put_contents( $path_package_json, implode(PHP_EOL, $package_json_arr_VAR) ); return true; } /** * Get the pid of the processes start by npm start. * * @param string $appname * @return array of the pids * @since 1.0.0 */ function get_node_pids($appname) { $apppath = $this->app_path($appname); $command = "ps aux | grep {$apppath} | grep -v grep"; $processes = []; exec($command, $processes); return array_map(fn ($el) => preg_split("/\ + /", $el)[1], $processes); } /** * Checks if the given app is running * * @param string $appname * @return boolean * @since 1.0.0 */ function is_react_app_running(string $appname) { return !empty($this->get_node_pids($appname)); } /** * Creates the path the app, beginning from root of filesystem * or htdocs. * * @param string $appname * @param boolean $relative_to_home_path * @return string * @since 1.0.0 */ function app_path(string $appname, $relative_to_home_path = false): string { $apppath = escapeshellcmd(REPR_PLUGIN_PATH . "apps/{$appname}"); $document_root = $_SERVER['DOCUMENT_ROOT'] ?? ''; if ($relative_to_home_path) { return explode($document_root, $apppath)[1]; } else { return $apppath; } } /** * Start the react app and extracts the right url auf the development * server. * * @param string $appname * @return void * @since 1.0.0 */ function start_react_app(string $appname) { $apppath = $this->app_path($appname); chdir($apppath); shell_exec("npm start > out.log 2>&1 & echo $! > pid.log"); return $this->get_app_uri($apppath, 100); } /** * Stops all processes, that were started for the given app * * @param string $appname * @return void * @since 1.0.0 */ function stop_react_app($appname) { // end node processes based on the app path $SIGHUP = 1; $apppath = $this->app_path($appname); foreach ($this->get_node_pids($appname) as $pid) { posix_kill((int)$pid, $SIGHUP); } // cancel npm start $npm_pid = file_get_contents("{$apppath}/pid.log"); posix_kill((int)$npm_pid, $SIGHUP); // cleanup remove out.log and pid.log unlink("{$apppath}/pid.log"); unlink("{$apppath}/out.log"); } /** * Helper function to add an element to an array * without mutationg the original array. * * @param array $array * @param [type] $entry * @return void * @since 1.0.0 */ function array_add(array $array, $entry) { return array_merge($array, [$entry]); } /** * Creates a new page with the given name and slug. Rejects if the slug is already * taken * * @param string $appname * @param string $pageslug * @return void * @since 1.0.0 */ public function insert_react_page(string $appname, string $pageslug) { global $wpdb; // check if post_name (which is the slug and should be unique) exist $get_data = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM " . $wpdb->prefix . "posts WHERE post_name = %s", $pageslug ) ); if (!empty($get_data)) { // alreade we have data with this post name return ['status' => 'false', 'message' => 'Page already exists.']; } else { $result = wp_insert_post( array( 'post_title' => $appname, 'post_name' => $pageslug, 'post_status' => 'publish', 'post_author' => '1', 'post_content' => REPR_REACT_ROOT_TAG, 'post_type' => "page", // Assign page template 'page_template' => REPR_PLUGIN_PATH . 'templates/react-page-template.php', ) ); if ($result) { return ['status' => 'true', 'message' => 'Page created.']; } else { return ['status' => 'false', 'message' => "Couldn't create page"]; } } } /** * Checks if the dev environment is suitable. If not * produces a message for the user. * * @return string * @since 1.0.0 */ public function environment_message(): string { $has_exec = function_exists('exec'); $has_shell_exec = function_exists('shell_exec'); $is_posix = strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN'; $has_npm_v6 = false; if ($has_exec && $has_shell_exec) { $has_npm_v6 = (int)shell_exec('npm -v') >= 6; } $message = ''; if (!($has_exec && $has_shell_exec)) { $message .= "
  • Your WordPress installation needs access to the php functions exec and shell_exec.
  • "; } if (!$has_npm_v6) { $message .= "
  • Your dev server needs access to npm 6 or higher to develop React apps."; } if (!$is_posix) { $message .= "
  • Right now Windows is not supported for developing apps with ReactPress. Windows users can use WSL.
  • "; } if ($message) { $message .= "

    For convience we provide a VirtualBox image that works well with ReactPress.

    "; } return $message; } } /** * Deletes directory recursively * (C) Paulund https://paulund.co.uk/php-delete-directory-and-files-in-directory * * @param string $dirname * @return bool true if directory deleted * @since 1.0.0 */ function repr_delete_directory(string $dirname): bool { repr_log($dirname); $dir_handle = ''; if (is_dir($dirname)) $dir_handle = opendir($dirname); if (!$dir_handle) return false; while ($file = readdir($dir_handle)) { if ($file != "." && $file != "..") { if (!is_dir($dirname . "/" . $file)) unlink($dirname . "/" . $file); else repr_delete_directory($dirname . '/' . $file); } } closedir($dir_handle); return rmdir($dirname); } function repr_response(mixed $value, int $options = 0, int $depth = 512) { echo (wp_json_encode($value, $options, $depth)); } /** * Write error to a log file named debug.log in wp-content. * * @param mixed $log The thing you want to log. * @since 1.0.0 */ function repr_log($log) { if (true === WP_DEBUG) { if (is_array($log) || is_object($log)) { error_log(print_r($log, true)); } else { error_log($log); } } return $log; } function repr_thread($operator, $first, ...$args) { $isThreadFirst = false; switch ($operator) { case '->>': $isThreadFirst = false; break; case '->': $isThreadFirst = true; break; default: throw new Exception('Operator not supported'); break; } return array_reduce($args, function ($prev, $next) use ($isThreadFirst) { if (is_array($next)) { $head = $next[0]; $tail = array_slice($next, 1); return $isThreadFirst ? call_user_func_array($head, [$prev, ...$tail]) : call_user_func_array($head, [...$tail, $prev]); } else { return call_user_func_array($next, $prev); } }, $first); } /** * The html block, the react apps hook to. */ define("REPR_REACT_ROOT_TAG", "\n\n\n
    \n"); define("REPR_MENU_ICON", "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgd2lkdGg9IjI0IgogICBoZWlnaHQ9IjI0IgogICB2aWV3Qm94PSIwIDAgMjQgMjQiCiAgIHZlcnNpb249IjEuMSIKICAgaWQ9InN2Zzg5NyIKICAgc29kaXBvZGk6ZG9jbmFtZT0iYnhsLXJlYWN0LnN2ZyIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMS4wLjEgKDA3NjdmODMwMmEsIDIwMjAtMTAtMTcpIj4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGE5MDMiPgogICAgPHJkZjpSREY+CiAgICAgIDxjYzpXb3JrCiAgICAgICAgIHJkZjphYm91dD0iIj4KICAgICAgICA8ZGM6Zm9ybWF0PmltYWdlL3N2Zyt4bWw8L2RjOmZvcm1hdD4KICAgICAgICA8ZGM6dHlwZQogICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+CiAgICAgIDwvY2M6V29yaz4KICAgIDwvcmRmOlJERj4KICA8L21ldGFkYXRhPgogIDxkZWZzCiAgICAgaWQ9ImRlZnM5MDEiIC8+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxIgogICAgIG9iamVjdHRvbGVyYW5jZT0iMTAiCiAgICAgZ3JpZHRvbGVyYW5jZT0iMTAiCiAgICAgZ3VpZGV0b2xlcmFuY2U9IjEwIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIgogICAgIGlua3NjYXBlOnBhZ2VzaGFkb3c9IjIiCiAgICAgaW5rc2NhcGU6d2luZG93LXdpZHRoPSIxOTIwIgogICAgIGlua3NjYXBlOndpbmRvdy1oZWlnaHQ9IjEwMTUiCiAgICAgaWQ9Im5hbWVkdmlldzg5OSIKICAgICBzaG93Z3JpZD0iZmFsc2UiCiAgICAgaW5rc2NhcGU6em9vbT0iMzYuMTI1IgogICAgIGlua3NjYXBlOmN4PSIxMiIKICAgICBpbmtzY2FwZTpjeT0iMTIiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9IjE5MjAiCiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjI4IgogICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjEiCiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnODk3IiAvPgogIDxjaXJjbGUKICAgICBjeD0iMTIiCiAgICAgY3k9IjExLjI0NSIKICAgICByPSIxLjc4NSIKICAgICBpZD0iY2lyY2xlODg3IgogICAgIHN0eWxlPSJmaWxsOiNhMGE1YWE7ZmlsbC1vcGFjaXR5OjEiIC8+CiAgPHBhdGgKICAgICBkPSJNNy4wMDIsMTQuNzk0bC0wLjM5NS0wLjEwMWMtMi45MzQtMC43NDEtNC42MTctMi4wMDEtNC42MTctMy40NTJjMC0xLjQ1MiwxLjY4NC0yLjcxMSw0LjYxNy0zLjQ1MmwwLjM5NS0wLjFMNy4xMTMsOC4wOCBjMC4yOTcsMS4wMjMsMC42NzYsMi4wMjIsMS4xMzYsMi45ODNsMC4wODUsMC4xNzhsLTAuMDg1LDAuMTc4Yy0wLjQ2LDAuOTYzLTAuODQxLDEuOTYxLTEuMTM2LDIuOTg1TDcuMDAyLDE0Ljc5NEw3LjAwMiwxNC43OTR6IE02LjQyNSw4LjY5OWMtMi4yMjksMC42MjgtMy41OTgsMS41ODYtMy41OTgsMi41NDJjMCwwLjk1NCwxLjM2OCwxLjkxMywzLjU5OCwyLjU0YzAuMjczLTAuODY4LDAuNjAzLTEuNzE3LDAuOTg1LTIuNTQgQzcuMDI1LDEwLjQxNiw2LjY5Niw5LjU2Nyw2LjQyNSw4LjY5OXogTTE2Ljk5NywxNC43OTRsLTAuMTEtMC4zOTJjLTAuMjk4LTEuMDI0LTAuNjc3LTIuMDIyLTEuMTM3LTIuOTg0bC0wLjA4NS0wLjE3NyBsMC4wODUtMC4xNzljMC40Ni0wLjk2MSwwLjgzOS0xLjk2LDEuMTM3LTIuOTg0bDAuMTEtMC4zOWwwLjM5NSwwLjFjMi45MzUsMC43NDEsNC42MTcsMiw0LjYxNywzLjQ1MyBjMCwxLjQ1Mi0xLjY4MywyLjcxMS00LjYxNywzLjQ1MkwxNi45OTcsMTQuNzk0eiBNMTYuNTg3LDExLjI0MWMwLjQsMC44NjYsMC43MzMsMS43MTgsMC45ODcsMi41NCBjMi4yMy0wLjYyNywzLjU5OS0xLjU4NiwzLjU5OS0yLjU0YzAtMC45NTYtMS4zNjgtMS45MTMtMy41OTktMi41NDJDMTcuMzAxLDkuNTY3LDE2Ljk3MiwxMC40MTYsMTYuNTg3LDExLjI0MUwxNi41ODcsMTEuMjQxeiIKICAgICBpZD0icGF0aDg4OSIKICAgICBzdHlsZT0iZmlsbDojYTBhNWFhO2ZpbGwtb3BhY2l0eToxIiAvPgogIDxwYXRoCiAgICAgZD0iTTYuNDE5LDguNjk1bC0wLjExLTAuMzlDNS40ODMsNS4zOTcsNS43MzMsMy4zMTQsNi45OTYsMi41ODhjMS4yMzUtMC43MTUsMy4yMjIsMC4xMyw1LjMwMywyLjI2NWwwLjI4NCwwLjI5MiBsLTAuMjg0LDAuMjkxYy0wLjczOSwwLjc2OS0xLjQxNSwxLjU5Ni0yLjAyLDIuNDc0bC0wLjExMywwLjE2Mkw5Ljk3LDguMDg4QzguOTA3LDguMTcxLDcuODUxLDguMzQyLDYuODEzLDguNTk3TDYuNDE5LDguNjk1IEw2LjQxOSw4LjY5NXogTTguMDAxLDMuMTY2Yy0wLjIyNCwwLTAuNDIyLDAuMDQ5LTAuNTg5LDAuMTQ1QzYuNTg0LDMuNzg4LDYuNDM4LDUuNDQ5LDcuMDA4LDcuNjkxIGMwLjg5MS0wLjE5NywxLjc5LTAuMzM4LDIuNjk2LTAuNDE3YzAuNTI1LTAuNzQ1LDEuMDk3LTEuNDUzLDEuNzEzLTIuMTIzQzEwLjExNCwzLjg4NCw4Ljg4NCwzLjE2Niw4LjAwMSwzLjE2Nkw4LjAwMSwzLjE2NnogTTE1Ljk5OCwyMC4xNUwxNS45OTgsMjAuMTVjLTEuMTg4LDAtMi43MTQtMC44OTYtNC4yOTgtMi41MjJsLTAuMjgzLTAuMjkxbDAuMjgzLTAuMjljMC43MzktMC43NywxLjQxNi0xLjU5OSwyLjAyMS0yLjQ3NyBsMC4xMTItMC4xNmwwLjE5NC0wLjAxOWMxLjA2NS0wLjA4MiwyLjEyMi0wLjI1MiwzLjE1OC0wLjUwN2wwLjM5NS0wLjFsMC4xMTEsMC4zOTFjMC44MjIsMi45MDYsMC41NzMsNC45OTItMC42ODgsNS43MTggQzE2LjY5OCwyMC4wNjYsMTYuMzUyLDIwLjE1NSwxNS45OTgsMjAuMTVMMTUuOTk4LDIwLjE1eiBNMTIuNTgzLDE3LjMzYzEuMzAyLDEuMjY3LDIuNTMzLDEuOTg2LDMuNDE1LDEuOTg2bDAsMCBjMC4yMjUsMCwwLjQyMy0wLjA1LDAuNTg5LTAuMTQ1YzAuODI5LTAuNDc4LDAuOTc2LTIuMTQyLDAuNDA0LTQuMzg0Yy0wLjg5LDAuMTk4LTEuNzksMC4zNC0yLjY5OCwwLjQxOSBDMTMuNzcxLDE1Ljk1MSwxMy4xOTksMTYuNjYxLDEyLjU4MywxNy4zM3oiCiAgICAgaWQ9InBhdGg4OTEiCiAgICAgc3R5bGU9ImZpbGw6I2EwYTVhYTtmaWxsLW9wYWNpdHk6MSIgLz4KICA8cGF0aAogICAgIGQ9Ik0xNy41OCw4LjY5NWwtMC4zOTUtMC4wOTljLTEuMDM2LTAuMjU2LTIuMDkzLTAuNDI2LTMuMTU4LTAuNTA5bC0wLjE5NC0wLjAxN2wtMC4xMTItMC4xNjIgYy0wLjYwNC0wLjg3OC0xLjI4MS0xLjcwNS0yLjAyMS0yLjQ3NGwtMC4yODMtMC4yOTFMMTEuNyw0Ljg1M2MyLjA4LTIuMTM0LDQuMDY2LTIuOTc5LDUuMzAzLTIuMjY1IGMxLjI2MiwwLjcyNywxLjUxMywyLjgxLDAuNjg4LDUuNzE3TDE3LjU4LDguNjk1TDE3LjU4LDguNjk1eiBNMTQuMjkzLDcuMjc0YzAuOTU0LDAuMDg1LDEuODU4LDAuMjI4LDIuNjk4LDAuNDE3IGMwLjU3MS0yLjI0MiwwLjQyNS0zLjkwMy0wLjQwNC00LjM4MWMtMC44MjQtMC40NzctMi4zNzUsMC4yNTMtNC4wMDQsMS44NDFDMTMuMTk5LDUuODIxLDEzLjc3MSw2LjUyOSwxNC4yOTMsNy4yNzR6IE04LjAwMSwyMC4xNSBjLTAuMzUzLDAuMDA1LTAuNjk5LTAuMDg0LTEuMDA1LTAuMjU3Yy0xLjI2My0wLjcyNi0xLjUxMy0yLjgxMS0wLjY4OC01LjcxOGwwLjEwOC0wLjM5MWwwLjM5NSwwLjEgYzAuOTY0LDAuMjQzLDIuMDI2LDAuNDE0LDMuMTU4LDAuNTA3bDAuMTk0LDAuMDE5bDAuMTEzLDAuMTZjMC42MDQsMC44NzgsMS4yOCwxLjcwNywyLjAyLDIuNDc3bDAuMjg0LDAuMjlsLTAuMjg0LDAuMjkxIEMxMC43MTMsMTkuMjU1LDkuMTg3LDIwLjE1LDguMDAxLDIwLjE1TDguMDAxLDIwLjE1eiBNNy4wMDgsMTQuNzg4Yy0wLjU3LDIuMjQyLTAuNDI0LDMuOTA2LDAuNDA0LDQuMzg0IGMwLjgyNSwwLjQ3LDIuMzcxLTAuMjU1LDQuMDA1LTEuODQyYy0wLjYxNi0wLjY3LTEuMTg4LTEuMzc5LTEuNzEzLTIuMTIzQzguNzk4LDE1LjEyOCw3Ljg5OCwxNC45ODYsNy4wMDgsMTQuNzg4TDcuMDA4LDE0Ljc4OHoiCiAgICAgaWQ9InBhdGg4OTMiCiAgICAgc3R5bGU9ImZpbGw6I2EwYTVhYTtmaWxsLW9wYWNpdHk6MSIgLz4KICA8cGF0aAogICAgIGQ9Ik0xMiwxNS4zMTNjLTAuNjg3LDAtMS4zOTItMC4wMjktMi4xLTAuMDg4bC0wLjE5Ni0wLjAxN2wtMC4xMTMtMC4xNjJjLTAuMzk4LTAuNTcyLTAuNzc0LTEuMTYzLTEuMTI2LTEuNzY5IGMtMC4zNDktMC42MDctMC42NzItMS4yMjYtMC45NzEtMS44NTlMNy40MSwxMS4yNDFsMC4wODQtMC4xNzljMC4yOTktMC42MzIsMC42MjItMS4yNTIsMC45NzEtMS44NTggYzAuMzQ3LTAuNTk2LDAuNzI2LTEuMTkyLDEuMTI2LTEuNzdsMC4xMTMtMC4xNkw5LjksNy4yNTZjMS4zOTctMC4xMTcsMi44MDEtMC4xMTcsNC4xOTgsMGwwLjE5NCwwLjAxOWwwLjExMywwLjE2IGMwLjc5OSwxLjE0OSwxLjUwMywyLjM2MiwyLjEsMy42MjhsMC4wODMsMC4xNzlsLTAuMDgzLDAuMTc3Yy0wLjU5NywxLjI2OC0xLjI5OSwyLjQ4MS0yLjEsMy42MjhsLTAuMTEzLDAuMTYybC0wLjE5NCwwLjAxNyBDMTMuMzkyLDE1LjI4MywxMi42ODYsMTUuMzEzLDEyLDE1LjMxM0wxMiwxNS4zMTN6IE0xMC4xNjYsMTQuNDA5YzEuMjM1LDAuMDkzLDIuNDMzLDAuMDkzLDMuNjY3LDAgYzAuNjktMS4wMSwxLjMwMS0yLjA2OCwxLjgzMi0zLjE2OGMtMC41MjktMS4xMDItMS4xNDItMi4xNjEtMS44MzItMy4xNjhjLTEuMjIxLTAuMDk0LTIuNDQ2LTAuMDk0LTMuNjY3LDAgYy0wLjY4OSwxLjAwNy0xLjMwNSwyLjA2NS0xLjgzMiwzLjE2OEM4Ljg2NSwxMi4zNDEsOS40NzksMTMuMzk5LDEwLjE2NiwxNC40MDlMMTAuMTY2LDE0LjQwOXoiCiAgICAgaWQ9InBhdGg4OTUiCiAgICAgc3R5bGU9ImZpbGw6I2EwYTVhYTtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZTtzdHJva2Utb3BhY2l0eToxIiAvPgo8L3N2Zz4K"); define('REPR_REACT_ICON_SVG', ''); define('REPR_LOGO', '');