*/ 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 */ $appname = strtolower(sanitize_file_name($_POST['appname'] ?? '')); $app_options_list = $this->get_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'], $app_options_list); if (in_array($appname, $existing_appnames) || $this->does_pageslug_exist($pageslug)) { echo wp_json_encode([ 'status' => 0, 'message' => "App name or page slug already exists.", ]); } else { $this->create_new_app($app_options_list, $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 === "edit_url_slug" && $appname && $pageslug) { $app_option = $this->get_app_options($app_options_list, $appname); if (($app_option && $app_option['pageslug'])) { $this->update_react_page($app_option['pageslug'], $pageslug); $this->change_pageslug_option($app_options_list, $appname, $pageslug); $this->add_build_path($appname); $this->write_index_html($appname, $this->get_index_html_content($pageslug)); echo wp_json_encode([ 'status' => 1, 'message' => 'Url Slug changed.' ]); } elseif (!$this->does_pageslug_exist($pageslug) && $pageslug) { $this->insert_react_page($appname, $pageslug); $this->add_app_options($app_options_list, $appname, $pageslug); $this->add_build_path($appname); $this->write_index_html($appname, $this->get_index_html_content($pageslug)); echo wp_json_encode([ 'status' => 1, 'message' => 'Url Slug created.' ]); } else { echo wp_json_encode([ 'status' => 0, 'message' => "There exists another page that already has this page slug.", ]); } } elseif ($param === "update_index_html" && $appname && $pageslug) { $this->write_index_html($appname, $this->get_index_html_content($pageslug)); echo wp_json_encode([ 'status' => 1, 'message' => 'Index.html updated.', ]); } 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_appdir_removed) { echo wp_json_encode([ 'status' => 1, 'message' => 'App deleted.', ]); } else { echo wp_json_encode([ 'status' => 1, 'message' => "Couldn't remove files. Please remove directory by hand.", ]); } } 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' => 0, '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(); } } /** * Checks if the given string is already used as a pageslug of any * post in the current WP site. * @param string $pageslug * @return bool * @since 1.2.0 */ public function does_pageslug_exist(string $pageslug) { global $wpdb; return !empty($wpdb->get_row( $wpdb->prepare("SELECT * FROM " . $wpdb->prefix . "posts WHERE post_name = %s", $pageslug) )); } /** * 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 $app_options_list * @param string $appname * @param string $pageslug * @param string $template * @param string $type * @return void * @since 1.0.0 */ function create_new_app( $app_options_list, string $appname, string $pageslug, string $template, string $type ) { $this->add_app_options($app_options_list, $appname, $pageslug); 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); $this->add_build_path($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 int 0 if no success * @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); $relative_apppath = $relative_apppath ? $relative_apppath : "/wp-content/plugins/reactpress/apps/{$appname}/"; $homepage = "{$relative_apppath}/build"; $path_package_json = "{$apppath}/package.json"; $package_json_contents = file_get_contents($path_package_json); if (stripos($package_json_contents, $homepage)) { chdir($apppath); shell_exec("npm run build"); return 1; } else { // Add a homepage attribute during the build process and remove it again, // that the developer can build with the public/index.html without WP. file_put_contents( $path_package_json, str_replace("react-scripts build", "PUBLIC_URL={$homepage} react-scripts build", $package_json_contents) ); chdir($apppath); shell_exec("npm run build"); return 2; } return 0; } /** * Add the right build path to package.json * * @param string $appname * @return int 0 if no success * @since 1.2.0 */ function add_build_path($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); $relative_apppath = $relative_apppath ? $relative_apppath : "/wp-content/plugins/reactpress/apps/{$appname}/"; $homepage = "{$relative_apppath}/build"; $path_package_json = "{$apppath}/package.json"; $package_json_contents = file_get_contents($path_package_json); if (!$package_json_contents) { return 0; } elseif (stripos($package_json_contents, $homepage)) { return 1; } else { // Add a homepage attribute during the build process and remove it again, // that the developer can build with the public/index.html without WP. file_put_contents( $path_package_json, str_replace("react-scripts build", "PUBLIC_URL={$homepage} react-scripts build", $package_json_contents) ); return 2; } return 0; } /** * 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"]; } } } /** * Change the slug of a page to the new one. * * @param string $oldSlug * @param string $newSlug * @return void * @since 1.0.0 */ public function update_react_page(string $oldSlug, string $newSlug) { 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", $oldSlug ) ); if (empty($get_data)) { // alreade we have data with this post name return ['status' => 'false', 'message' => 'Couldn\'t find page.']; } else { $result = wp_update_post( array( 'ID' => $get_data->ID, 'post_name' => $newSlug, ) ); if ($result) { return ['status' => 'true', 'message' => 'Page updated.']; } else { return ['status' => 'false', 'message' => "Couldn't update 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 WordPress installation needs access to npm 6 or higher to create React apps from the admin interface. However you can go to the app directory and use create-react-app from there
  • "; } if (!$is_posix) { $message .= "
  • Right now Windows is not supported for developing apps with ReactPress. Windows users can use WSL. You can also go to the app directory and try Create React App from there
  • "; } return $message; } /** * Get all folders in the apps directory and return them as an array * * @return array * @since 1.2.0 */ public function get_app_names() { chdir(REPR_PLUGIN_PATH . 'apps'); $appnames = scandir(REPR_PLUGIN_PATH . 'apps'); return array_values(array_filter($appnames, fn ($el) => $el[0] !== '.' && is_dir($el))); } /** * Return all apps as an array * * @return array * @since 1.2.0 */ public function get_apps() { $appnames = $this->get_app_names(); $app_options = get_option('repr_apps') ?? []; $apps = array_map(function ($el) use ($app_options) { $app_option = array_reduce( $app_options ? $app_options : [], fn ($carry, $item) => $item['appname'] === $el ? $item : $carry, [] ); return [ 'appname' => $el, 'pageslug' => $app_option['pageslug'] ?? '', 'type' => is_file(REPR_PLUGIN_PATH . 'apps/' . $el . '/package.json') ? 'development' : 'deployment' ]; }, $appnames); return $apps; } public function add_app_options(array $app_options_list, string $appname, string $pageslug) { if (!is_array($app_options_list) && $appname && $pageslug) { add_option('repr_apps', [[ 'appname' => $appname, 'pageslug' => $pageslug, ]]); } elseif ($appname && $pageslug) { update_option('repr_apps', $this->array_add($app_options_list, [ 'appname' => $appname, 'pageslug' => $pageslug, ])); } } /** * Get the option for the given app name. * @param string $appname * @return mixed */ public function change_pageslug_option(array $app_options_list, string $appname, string $pageslug) { foreach ($app_options_list as $key => $val) { if ($val['appname'] === $appname) { $app_options_list[$key]['pageslug'] = $pageslug; } } update_option('repr_apps', $app_options_list); return $app_options_list; } /** * Get the option for the given app name. * @param string $appname * @return mixed */ public function get_app_options(array $app_options_list, string $appname) { $app_options = null; foreach ($app_options_list as $key => $val) { if ($val['appname'] === $appname) { $app_options = $val; } } return $app_options; } } /** * 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 { $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', '');