mirror of
https://ghproxy.net/https://github.com/abhijitb/helix.git
synced 2025-08-28 06:26:00 +08:00
Compare commits
No commits in common. "76b51311803087fd1d4c7e138e5a146a21ae6551" and "fc331c2e7ca3cd59dfc7b2d82c89691963bffd74" have entirely different histories.
76b5131180
...
fc331c2e7c
21 changed files with 274 additions and 2543 deletions
|
@ -21,10 +21,6 @@ add_action( 'rest_api_init', 'helix_register_rest_routes' );
|
|||
* @since 1.0.0
|
||||
*/
|
||||
function helix_register_rest_routes() {
|
||||
// Get the schemas.
|
||||
$get_schema = helix_get_settings_schema();
|
||||
$update_schema = helix_update_settings_schema();
|
||||
|
||||
// Settings endpoints.
|
||||
register_rest_route(
|
||||
'helix/v1',
|
||||
|
@ -34,13 +30,13 @@ function helix_register_rest_routes() {
|
|||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => 'helix_get_settings',
|
||||
'permission_callback' => 'helix_settings_permissions_check',
|
||||
'args' => $get_schema,
|
||||
'args' => helix_get_settings_schema(),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => 'helix_update_settings',
|
||||
'permission_callback' => 'helix_settings_permissions_check',
|
||||
'args' => $update_schema,
|
||||
'args' => helix_update_settings_schema(),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
@ -117,25 +113,30 @@ function helix_get_settings() {
|
|||
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
function helix_update_settings( $request ) {
|
||||
$params = $request->get_params();
|
||||
$allowed_settings = helix_get_allowed_settings();
|
||||
$params = $request->get_json_params();
|
||||
|
||||
if ( empty( $params ) ) {
|
||||
return new WP_Error(
|
||||
'helix_no_settings_data',
|
||||
__( 'No settings data provided.', 'helix' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
$updated_settings = array();
|
||||
$errors = array();
|
||||
$allowed_settings = helix_get_allowed_settings();
|
||||
|
||||
// Process each setting.
|
||||
foreach ( $params as $setting => $value ) {
|
||||
// Check if setting is allowed.
|
||||
if ( ! in_array( $setting, $allowed_settings, true ) ) {
|
||||
$error_msg = sprintf(
|
||||
$errors[ $setting ] = sprintf(
|
||||
/* translators: %s: Setting name */
|
||||
__( 'Setting "%s" is not allowed.', 'helix' ),
|
||||
__( 'Setting "%s" is not allowed to be updated.', 'helix' ),
|
||||
$setting
|
||||
);
|
||||
$errors[ $setting ] = $error_msg;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate and sanitize the value.
|
||||
$sanitized_value = helix_sanitize_setting_value( $setting, $value );
|
||||
|
||||
if ( is_wp_error( $sanitized_value ) ) {
|
||||
|
@ -143,31 +144,13 @@ function helix_update_settings( $request ) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Get the WordPress option name for this setting.
|
||||
$option_name = helix_get_wp_option_name( $setting );
|
||||
|
||||
// Handle special settings that need custom update logic.
|
||||
$result = helix_update_setting( $setting, $sanitized_value );
|
||||
|
||||
// If special handling didn't apply, update normally.
|
||||
if ( ! $result ) {
|
||||
$result = update_option( $option_name, $sanitized_value );
|
||||
}
|
||||
$result = update_option( $option_name, $sanitized_value );
|
||||
|
||||
if ( $result ) {
|
||||
$updated_settings[ $setting ] = $sanitized_value;
|
||||
} else {
|
||||
// Provide specific error messages for special settings.
|
||||
if ( 'language' === $setting ) {
|
||||
$error_msg = sprintf(
|
||||
/* translators: %s: Language name */
|
||||
__( 'Language "%s" could not be installed automatically. Please install the language pack manually via WordPress Admin → Settings → General → Site Language.', 'helix' ),
|
||||
$sanitized_value
|
||||
);
|
||||
} else {
|
||||
$error_msg = __( 'Failed to update setting.', 'helix' );
|
||||
}
|
||||
$errors[ $setting ] = $error_msg;
|
||||
$errors[ $setting ] = __( 'Failed to update setting.', 'helix' );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,28 +35,21 @@ function helix_get_settings_schema() {
|
|||
*/
|
||||
function helix_update_settings_schema() {
|
||||
$settings_config = helix_get_settings_config();
|
||||
$schema = array(
|
||||
'type' => 'object',
|
||||
'properties' => array(),
|
||||
);
|
||||
$schema = array();
|
||||
|
||||
foreach ( $settings_config as $category => $category_settings ) {
|
||||
foreach ( $category_settings as $setting_key => $setting_config ) {
|
||||
$schema['properties'][ $setting_key ] = array(
|
||||
'type' => get_rest_api_type( $setting_config['type'] ),
|
||||
foreach ( $settings_config as $category => $settings ) {
|
||||
foreach ( $settings as $setting_key => $setting_config ) {
|
||||
$schema[ $setting_key ] = array(
|
||||
'description' => $setting_config['description'],
|
||||
'type' => $setting_config['type'],
|
||||
);
|
||||
|
||||
// Add enum validation if applicable.
|
||||
if ( isset( $setting_config['enum'] ) ) {
|
||||
$schema['properties'][ $setting_key ]['enum'] = $setting_config['enum'];
|
||||
$schema[ $setting_key ]['enum'] = $setting_config['enum'];
|
||||
}
|
||||
|
||||
// Add minimum/maximum validation for numbers.
|
||||
if ( isset( $setting_config['min'] ) ) {
|
||||
$schema['properties'][ $setting_key ]['minimum'] = $setting_config['min'];
|
||||
}
|
||||
if ( isset( $setting_config['max'] ) ) {
|
||||
$schema['properties'][ $setting_key ]['maximum'] = $setting_config['max'];
|
||||
if ( isset( $setting_config['default'] ) ) {
|
||||
$schema[ $setting_key ]['default'] = $setting_config['default'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,30 +57,6 @@ function helix_update_settings_schema() {
|
|||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Helix setting types to WordPress REST API types.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @param string $helix_type The Helix setting type.
|
||||
* @return string|array The WordPress REST API type.
|
||||
*/
|
||||
function get_rest_api_type( $helix_type ) {
|
||||
switch ( $helix_type ) {
|
||||
case 'string':
|
||||
case 'email':
|
||||
case 'url':
|
||||
return 'string';
|
||||
case 'integer':
|
||||
return 'integer';
|
||||
case 'number':
|
||||
return 'number';
|
||||
case 'boolean':
|
||||
return 'boolean';
|
||||
default:
|
||||
return 'string';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all WordPress settings in organized format.
|
||||
*
|
||||
|
@ -141,7 +110,12 @@ function helix_get_allowed_settings() {
|
|||
$allowed_settings = array_merge( $allowed_settings, array_keys( $settings ) );
|
||||
}
|
||||
|
||||
// Filter the list of allowed settings.
|
||||
/**
|
||||
* Filter the list of allowed settings.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @param array $allowed_settings Array of allowed setting keys.
|
||||
*/
|
||||
return apply_filters( 'helix_allowed_settings', $allowed_settings );
|
||||
}
|
||||
|
||||
|
@ -252,8 +226,7 @@ function helix_sanitize_setting_value( $setting, $value ) {
|
|||
// Default sanitization based on type.
|
||||
switch ( $setting_config['type'] ) {
|
||||
case 'string':
|
||||
$sanitized = sanitize_text_field( $value );
|
||||
break;
|
||||
return sanitize_text_field( $value );
|
||||
|
||||
case 'email':
|
||||
$sanitized = sanitize_email( $value );
|
||||
|
@ -264,161 +237,46 @@ function helix_sanitize_setting_value( $setting, $value ) {
|
|||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
break;
|
||||
return $sanitized;
|
||||
|
||||
case 'url':
|
||||
$sanitized = esc_url_raw( $value );
|
||||
break;
|
||||
return esc_url_raw( $value );
|
||||
|
||||
case 'integer':
|
||||
$sanitized = absint( $value );
|
||||
break;
|
||||
return absint( $value );
|
||||
|
||||
case 'number':
|
||||
$sanitized = floatval( $value );
|
||||
break;
|
||||
return floatval( $value );
|
||||
|
||||
case 'boolean':
|
||||
$sanitized = rest_sanitize_boolean( $value );
|
||||
break;
|
||||
return rest_sanitize_boolean( $value );
|
||||
|
||||
default:
|
||||
$sanitized = sanitize_text_field( $value );
|
||||
break;
|
||||
}
|
||||
|
||||
// For any type with enum values, validate against allowed values.
|
||||
if ( isset( $setting_config['enum'] ) ) {
|
||||
// Extract values from enum options if they are objects with 'value' property.
|
||||
$enum_values = array();
|
||||
foreach ( $setting_config['enum'] as $option ) {
|
||||
if ( is_array( $option ) && isset( $option['value'] ) ) {
|
||||
$enum_values[] = $option['value'];
|
||||
} else {
|
||||
$enum_values[] = $option;
|
||||
// For enum types, validate against allowed values.
|
||||
if ( isset( $setting_config['enum'] ) ) {
|
||||
if ( ! in_array( $value, $setting_config['enum'], true ) ) {
|
||||
return new WP_Error(
|
||||
'helix_invalid_enum_value',
|
||||
sprintf(
|
||||
/* translators: 1: Setting name, 2: Allowed values */
|
||||
__( 'Invalid value for %1$s. Allowed values: %2$s', 'helix' ),
|
||||
$setting,
|
||||
implode( ', ', $setting_config['enum'] )
|
||||
),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! in_array( $sanitized, $enum_values, true ) ) {
|
||||
return new WP_Error(
|
||||
'helix_invalid_enum_value',
|
||||
sprintf(
|
||||
/* translators: 1: Setting name, 2: Allowed values */
|
||||
__( 'Invalid value for %1$s. Allowed values: %2$s', 'helix' ),
|
||||
$setting,
|
||||
implode( ', ', $enum_values )
|
||||
),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
return sanitize_text_field( $value );
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update special settings that require custom logic.
|
||||
* Get comprehensive settings configuration.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @param string $setting The setting key.
|
||||
* @param mixed $value The sanitized value.
|
||||
* @return bool|null True if successful, false if failed, null if not a special setting.
|
||||
* @return array Settings configuration array.
|
||||
*/
|
||||
function helix_update_setting( $setting, $value ) {
|
||||
// Handle timezone setting.
|
||||
if ( 'timezone' === $setting ) {
|
||||
return helix_update_timezone_setting( $value );
|
||||
} elseif ( 'language' === $setting ) {
|
||||
return helix_update_language_setting( $value );
|
||||
}
|
||||
// Not a special setting.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update language setting with automatic language pack installation.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @param string $locale The language locale to set.
|
||||
* @return bool True if successful, false otherwise.
|
||||
*/
|
||||
function helix_update_language_setting( $locale ) {
|
||||
// Try to update the option first.
|
||||
$result = update_option( 'WPLANG', $locale );
|
||||
|
||||
// If it failed, try to install the language pack.
|
||||
if ( ! $result && function_exists( 'switch_to_locale' ) ) {
|
||||
// Check if the language file exists.
|
||||
$lang_dir = WP_CONTENT_DIR . '/languages/';
|
||||
$lang_file = $lang_dir . $locale . '.po';
|
||||
|
||||
// If language file doesn't exist, try to install it.
|
||||
if ( ! file_exists( $lang_file ) ) {
|
||||
$install_result = helix_install_language_pack( $locale );
|
||||
|
||||
if ( $install_result ) {
|
||||
// Try updating the option again.
|
||||
$result = update_option( 'WPLANG', $locale );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update timezone setting by handling both city-based timezones and GMT offsets.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @param string $timezone_value The timezone value to save.
|
||||
* @return bool True if update succeeded, false otherwise.
|
||||
*/
|
||||
function helix_update_timezone_setting( $timezone_value ) {
|
||||
// Check if it's a GMT offset (starts with UTC+ or UTC- or is numeric).
|
||||
if ( preg_match( '/^UTC[+-](\d+(?:\.\d+)?)$/', $timezone_value, $matches ) ) {
|
||||
// Extract the numeric offset.
|
||||
$offset = $matches[1];
|
||||
$is_negative = strpos( $timezone_value, 'UTC-' ) === 0;
|
||||
|
||||
// Convert to numeric value (negative if UTC-).
|
||||
$numeric_offset = $is_negative ? -1 * floatval( $offset ) : floatval( $offset );
|
||||
|
||||
// Save to gmt_offset option.
|
||||
$result = update_option( 'gmt_offset', $numeric_offset );
|
||||
|
||||
// Clear the timezone_string option since we're using GMT offset.
|
||||
if ( $result ) {
|
||||
update_option( 'timezone_string', '' );
|
||||
}
|
||||
|
||||
return $result;
|
||||
} elseif ( is_numeric( $timezone_value ) ) {
|
||||
// Direct numeric offset (like "5.5").
|
||||
$numeric_offset = floatval( $timezone_value );
|
||||
|
||||
// Save to gmt_offset option.
|
||||
$result = update_option( 'gmt_offset', $numeric_offset );
|
||||
|
||||
// Clear the timezone_string option since we're using GMT offset.
|
||||
if ( $result ) {
|
||||
update_option( 'timezone_string', '' );
|
||||
}
|
||||
|
||||
return $result;
|
||||
} else {
|
||||
// City-based timezone (like "Asia/Kolkata").
|
||||
// Save to timezone_string option.
|
||||
$result = update_option( 'timezone_string', $timezone_value );
|
||||
|
||||
// Clear the gmt_offset option since we're using city-based timezone.
|
||||
if ( $result ) {
|
||||
update_option( 'gmt_offset', '' );
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available WordPress languages.
|
||||
*
|
||||
|
@ -428,160 +286,57 @@ function helix_update_timezone_setting( $timezone_value ) {
|
|||
function helix_get_available_languages() {
|
||||
$language_options = array();
|
||||
|
||||
// Add English (United States) as default.
|
||||
// Add English (United States) as default
|
||||
$language_options[] = array(
|
||||
'value' => '',
|
||||
'label' => 'English (United States)',
|
||||
'label' => 'English (United States)'
|
||||
);
|
||||
|
||||
// First, try to get installed languages.
|
||||
if ( function_exists( 'get_available_languages' ) ) {
|
||||
$installed_languages = get_available_languages();
|
||||
} else {
|
||||
$installed_languages = array();
|
||||
// Try to get installed languages
|
||||
if ( function_exists( 'get_available_languages' ) || ( file_exists( ABSPATH . 'wp-admin/includes/translation-install.php' ) && require_once ABSPATH . 'wp-admin/includes/translation-install.php' ) ) {
|
||||
|
||||
$languages = get_available_languages();
|
||||
|
||||
if ( ! empty( $languages ) && function_exists( 'wp_get_available_translations' ) ) {
|
||||
$available_translations = wp_get_available_translations();
|
||||
|
||||
foreach ( $languages as $language ) {
|
||||
$language_data = $available_translations[ $language ] ?? null;
|
||||
if ( $language_data && isset( $language_data['native_name'] ) ) {
|
||||
$language_options[] = array(
|
||||
'value' => $language,
|
||||
'label' => $language_data['native_name']
|
||||
);
|
||||
} else {
|
||||
// Fallback if translation data is not available
|
||||
$language_options[] = array(
|
||||
'value' => $language,
|
||||
'label' => $language
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always try to include the required file first.
|
||||
if ( ! function_exists( 'wp_get_available_translations' ) && file_exists( ABSPATH . 'wp-admin/includes/translation-install.php' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/translation-install.php';
|
||||
}
|
||||
|
||||
// Get all available translations (including uninstalled ones).
|
||||
if ( function_exists( 'wp_get_available_translations' ) ) {
|
||||
$available_translations = wp_get_available_translations();
|
||||
|
||||
// Add all available languages.
|
||||
foreach ( $available_translations as $locale => $translation_data ) {
|
||||
$label = isset( $translation_data['native_name'] ) ? $translation_data['native_name'] : $locale;
|
||||
|
||||
// Mark installed languages differently.
|
||||
$is_installed = in_array( $locale, $installed_languages, true );
|
||||
$display_label = $is_installed ? $label : $label . ' (Not Installed)';
|
||||
// If no languages found, add some common ones as fallback
|
||||
if ( count( $language_options ) === 1 ) {
|
||||
$common_languages = array(
|
||||
'es_ES' => 'Español',
|
||||
'fr_FR' => 'Français',
|
||||
'de_DE' => 'Deutsch',
|
||||
'it_IT' => 'Italiano',
|
||||
'pt_BR' => 'Português do Brasil',
|
||||
'ru_RU' => 'Русский',
|
||||
'ja' => '日本語',
|
||||
'zh_CN' => '简体中文',
|
||||
);
|
||||
|
||||
foreach ( $common_languages as $code => $name ) {
|
||||
$language_options[] = array(
|
||||
'value' => $locale,
|
||||
'label' => $display_label,
|
||||
'installed' => $is_installed,
|
||||
'value' => $code,
|
||||
'label' => $name
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Fallback to common languages if wp_get_available_translations is still not available.
|
||||
$language_options = helix_get_fallback_languages( $installed_languages );
|
||||
}
|
||||
|
||||
return $language_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a language pack using WordPress core functions.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @param string $locale The language locale to install.
|
||||
* @return bool True if installation succeeded, false otherwise.
|
||||
*/
|
||||
function helix_install_language_pack( $locale ) {
|
||||
// Make sure we have the required functions.
|
||||
if ( ! function_exists( 'wp_download_language_pack' ) ) {
|
||||
if ( file_exists( ABSPATH . 'wp-admin/includes/translation-install.php' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/translation-install.php';
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we have the filesystem API.
|
||||
if ( ! function_exists( 'request_filesystem_credentials' ) ) {
|
||||
if ( file_exists( ABSPATH . 'wp-admin/includes/file.php' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/file.php';
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the function is now available.
|
||||
if ( ! function_exists( 'wp_download_language_pack' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if filesystem API is available.
|
||||
if ( ! function_exists( 'request_filesystem_credentials' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get available translations to verify the language exists.
|
||||
if ( function_exists( 'wp_get_available_translations' ) ) {
|
||||
$available_translations = wp_get_available_translations();
|
||||
|
||||
if ( ! isset( $available_translations[ $locale ] ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to download and install the language pack.
|
||||
try {
|
||||
|
||||
$download_result = wp_download_language_pack( $locale );
|
||||
|
||||
if ( is_wp_error( $download_result ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify the language file now exists.
|
||||
$lang_dir = WP_CONTENT_DIR . '/languages/';
|
||||
$lang_file = $lang_dir . $locale . '.po';
|
||||
|
||||
if ( file_exists( $lang_file ) ) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch ( Exception $e ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fallback language options when wp_get_available_translations is not available.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @param array $installed_languages Array of installed language codes.
|
||||
* @return array Array of fallback language options.
|
||||
*/
|
||||
function helix_get_fallback_languages( $installed_languages = array() ) {
|
||||
$language_options = array();
|
||||
|
||||
// Common languages as fallback.
|
||||
$common_languages = array(
|
||||
'en_GB' => 'English (United Kingdom)',
|
||||
'es_ES' => 'Español',
|
||||
'fr_FR' => 'Français',
|
||||
'de_DE' => 'Deutsch',
|
||||
'it_IT' => 'Italiano',
|
||||
'pt_BR' => 'Português do Brasil',
|
||||
'ru_RU' => 'Русский',
|
||||
'ja' => '日本語',
|
||||
'zh_CN' => '简体中文',
|
||||
'ar' => 'العربية',
|
||||
'hi_IN' => 'हिन्दी',
|
||||
'ko_KR' => '한국어',
|
||||
'nl_NL' => 'Nederlands',
|
||||
'sv_SE' => 'Svenska',
|
||||
'da_DK' => 'Dansk',
|
||||
'fi' => 'Suomi',
|
||||
'no' => 'Norsk',
|
||||
'pl_PL' => 'Polski',
|
||||
'tr_TR' => 'Türkçe',
|
||||
);
|
||||
|
||||
foreach ( $common_languages as $code => $name ) {
|
||||
$is_installed = in_array( $code, $installed_languages, true );
|
||||
$display_label = $is_installed ? $name : $name . ' (Not Installed)';
|
||||
|
||||
$language_options[] = array(
|
||||
'value' => $code,
|
||||
'label' => $display_label,
|
||||
'installed' => $is_installed,
|
||||
);
|
||||
}
|
||||
|
||||
return $language_options;
|
||||
|
@ -596,23 +351,85 @@ function helix_get_fallback_languages( $installed_languages = array() ) {
|
|||
function helix_get_available_timezones() {
|
||||
$timezone_options = array();
|
||||
|
||||
// Use WordPress core function to get timezone choices.
|
||||
if ( function_exists( 'wp_timezone_choice' ) ) {
|
||||
// Get the HTML output from wp_timezone_choice.
|
||||
$timezone_html = wp_timezone_choice( get_option( 'timezone_string', 'UTC' ) );
|
||||
// Parse the HTML to extract option values and labels.
|
||||
if ( preg_match_all( '/<option[^>]*value=["\']([^"\']*)["\'][^>]*>([^<]*)<\/option>/', $timezone_html, $matches, PREG_SET_ORDER ) ) {
|
||||
foreach ( $matches as $match ) {
|
||||
$value = $match[1];
|
||||
$label = trim( $match[2] );
|
||||
// UTC and common UTC offsets
|
||||
$timezone_options[] = array( 'value' => 'UTC', 'label' => 'UTC' );
|
||||
|
||||
// Skip empty values.
|
||||
if ( ! empty( $value ) || '0' === $value ) {
|
||||
$timezone_options[] = array(
|
||||
'value' => $value,
|
||||
'label' => $label,
|
||||
);
|
||||
}
|
||||
// Positive UTC offsets
|
||||
for ( $i = 1; $i <= 12; $i++ ) {
|
||||
$offset = sprintf( '+%d', $i );
|
||||
$timezone_options[] = array( 'value' => "UTC{$offset}", 'label' => "UTC{$offset}" );
|
||||
}
|
||||
|
||||
// Negative UTC offsets
|
||||
for ( $i = 1; $i <= 12; $i++ ) {
|
||||
$offset = sprintf( '-%d', $i );
|
||||
$timezone_options[] = array( 'value' => "UTC{$offset}", 'label' => "UTC{$offset}" );
|
||||
}
|
||||
|
||||
// Major city-based timezones organized by region
|
||||
$timezone_regions = array(
|
||||
'America' => array(
|
||||
'America/New_York' => 'New York',
|
||||
'America/Chicago' => 'Chicago',
|
||||
'America/Denver' => 'Denver',
|
||||
'America/Los_Angeles' => 'Los Angeles',
|
||||
'America/Toronto' => 'Toronto',
|
||||
'America/Vancouver' => 'Vancouver',
|
||||
'America/Montreal' => 'Montreal',
|
||||
'America/Mexico_City' => 'Mexico City',
|
||||
'America/Sao_Paulo' => 'São Paulo',
|
||||
'America/Buenos_Aires' => 'Buenos Aires',
|
||||
),
|
||||
'Europe' => array(
|
||||
'Europe/London' => 'London',
|
||||
'Europe/Paris' => 'Paris',
|
||||
'Europe/Berlin' => 'Berlin',
|
||||
'Europe/Rome' => 'Rome',
|
||||
'Europe/Madrid' => 'Madrid',
|
||||
'Europe/Amsterdam' => 'Amsterdam',
|
||||
'Europe/Brussels' => 'Brussels',
|
||||
'Europe/Vienna' => 'Vienna',
|
||||
'Europe/Stockholm' => 'Stockholm',
|
||||
'Europe/Moscow' => 'Moscow',
|
||||
),
|
||||
'Asia' => array(
|
||||
'Asia/Tokyo' => 'Tokyo',
|
||||
'Asia/Shanghai' => 'Shanghai',
|
||||
'Asia/Hong_Kong' => 'Hong Kong',
|
||||
'Asia/Singapore' => 'Singapore',
|
||||
'Asia/Kolkata' => 'Kolkata',
|
||||
'Asia/Dubai' => 'Dubai',
|
||||
'Asia/Bangkok' => 'Bangkok',
|
||||
'Asia/Seoul' => 'Seoul',
|
||||
'Asia/Manila' => 'Manila',
|
||||
),
|
||||
'Australia' => array(
|
||||
'Australia/Sydney' => 'Sydney',
|
||||
'Australia/Melbourne' => 'Melbourne',
|
||||
'Australia/Brisbane' => 'Brisbane',
|
||||
'Australia/Perth' => 'Perth',
|
||||
'Australia/Adelaide' => 'Adelaide',
|
||||
),
|
||||
'Africa' => array(
|
||||
'Africa/Cairo' => 'Cairo',
|
||||
'Africa/Johannesburg' => 'Johannesburg',
|
||||
'Africa/Lagos' => 'Lagos',
|
||||
),
|
||||
'Pacific' => array(
|
||||
'Pacific/Auckland' => 'Auckland',
|
||||
'Pacific/Honolulu' => 'Honolulu',
|
||||
),
|
||||
);
|
||||
|
||||
$timezone_identifiers = timezone_identifiers_list();
|
||||
|
||||
foreach ( $timezone_regions as $region => $timezones ) {
|
||||
foreach ( $timezones as $timezone_id => $city_name ) {
|
||||
if ( in_array( $timezone_id, $timezone_identifiers ) ) {
|
||||
$timezone_options[] = array(
|
||||
'value' => $timezone_id,
|
||||
'label' => "{$city_name} ({$region})"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -620,12 +437,6 @@ function helix_get_available_timezones() {
|
|||
return $timezone_options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive settings configuration.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @return array Settings configuration array.
|
||||
*/
|
||||
function helix_get_settings_config() {
|
||||
return array(
|
||||
'site_information' => array(
|
||||
|
@ -800,13 +611,13 @@ function helix_get_settings_config() {
|
|||
),
|
||||
'largeSizeW' => array(
|
||||
'label' => __( 'Large Width', 'helix' ),
|
||||
'description' => __( 'Maximum width of large images.', 'helix' ),
|
||||
'description' => __( 'Maximum width of large-sized images.', 'helix' ),
|
||||
'type' => 'integer',
|
||||
'default' => 1024,
|
||||
),
|
||||
'largeSizeH' => array(
|
||||
'label' => __( 'Large Height', 'helix' ),
|
||||
'description' => __( 'Maximum height of large images.', 'helix' ),
|
||||
'description' => __( 'Maximum height of large-sized images.', 'helix' ),
|
||||
'type' => 'integer',
|
||||
'default' => 1024,
|
||||
),
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
12
composer.lock
generated
12
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "684994342b1eecfad8ef44a86ff89124",
|
||||
"content-hash": "eb1f0708b5830b82ea720f047c348230",
|
||||
"packages": [],
|
||||
"packages-dev": [
|
||||
{
|
||||
|
@ -187,16 +187,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpcsstandards/phpcsutils",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPCSStandards/PHPCSUtils.git",
|
||||
"reference": "f7eb16f2fa4237d5db9e8fed8050239bee17a9bd"
|
||||
"reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/f7eb16f2fa4237d5db9e8fed8050239bee17a9bd",
|
||||
"reference": "f7eb16f2fa4237d5db9e8fed8050239bee17a9bd",
|
||||
"url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/65355670ac17c34cd235cf9d3ceae1b9252c4dad",
|
||||
"reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -276,7 +276,7 @@
|
|||
"type": "thanks_dev"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-10T01:04:45+00:00"
|
||||
"time": "2025-06-12T04:32:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "squizlabs/php_codesniffer",
|
||||
|
|
|
@ -49,7 +49,6 @@ add_action(
|
|||
'helixData',
|
||||
array(
|
||||
'restUrl' => esc_url_raw( rest_url( 'helix/v1/' ) ),
|
||||
'wpRestUrl' => esc_url_raw( rest_url( 'wp/v2/' ) ),
|
||||
'nonce' => wp_create_nonce( 'wp_rest' ),
|
||||
'user' => wp_get_current_user(),
|
||||
'originalRoute' => $original_route,
|
||||
|
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
import { createRoot } from 'react-dom/client';
|
||||
import Dashboard from './pages/Dashboard/Dashboard';
|
||||
import Settings from './pages/Settings/Settings';
|
||||
import Posts from './pages/Posts/Posts';
|
||||
|
||||
// Main App component for the dashboard page
|
||||
export default function App() {
|
||||
|
@ -11,7 +10,12 @@ export default function App() {
|
|||
|
||||
// Posts page component
|
||||
function PostsApp() {
|
||||
return <Posts />;
|
||||
return (
|
||||
<div className="helix-page">
|
||||
<h1>Posts Management</h1>
|
||||
<p>Posts management interface will be implemented here.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Users page component
|
||||
|
|
|
@ -1,216 +0,0 @@
|
|||
## 🚀 **Phase-Wise Implementation Plan**
|
||||
|
||||
### **Phase 1: Foundation & Core List View**
|
||||
**Timeline: 1-2 weeks | Priority: Critical**
|
||||
|
||||
#### **1.1 Project Setup & Structure**
|
||||
- [ ] Create Posts page directory structure
|
||||
- [ ] Set up routing and navigation integration
|
||||
- [ ] Create basic Posts page component with placeholder content
|
||||
- [ ] Integrate with existing Helix navigation system
|
||||
|
||||
#### **1.2 Basic Posts List**
|
||||
- [ ] Create `PostsList` component with table structure
|
||||
- [ ] Implement `PostRow` component for individual posts
|
||||
- [ ] Basic data fetching from WordPress REST API (`/wp-json/wp/v2/posts`)
|
||||
- [ ] Simple pagination (next/previous)
|
||||
- [ ] Basic loading states and error handling
|
||||
|
||||
#### **1.3 Essential CRUD Operations**
|
||||
- [ ] View post details (read)
|
||||
- [ ] Basic post creation form
|
||||
- [ ] Simple post editing (title, content, status)
|
||||
- [ ] Post deletion with confirmation
|
||||
- [ ] Status changes (publish, draft, private)
|
||||
|
||||
#### **1.4 Basic Search & Filtering**
|
||||
- [ ] Search by post title
|
||||
- [ ] Filter by status (published, draft, private)
|
||||
- [ ] Filter by author
|
||||
- [ ] Basic date range filtering
|
||||
|
||||
**Deliverable**: Functional posts list with basic CRUD operations
|
||||
|
||||
---
|
||||
|
||||
### **Phase 2: Enhanced List Features & Quick Actions**
|
||||
**Timeline: 1-2 weeks | Priority: High**
|
||||
|
||||
#### **2.1 Advanced Filtering & Search**
|
||||
- [ ] Enhanced search (title + content + excerpt)
|
||||
- [ ] Category and tag filtering
|
||||
- [ ] Advanced date filtering (last week, last month, custom range)
|
||||
- [ ] Filter combinations and saved filter presets
|
||||
- [ ] Clear all filters functionality
|
||||
|
||||
#### **2.2 Bulk Operations**
|
||||
- [ ] Multi-select functionality with checkboxes
|
||||
- [ ] Bulk actions toolbar (publish, draft, delete, move to trash)
|
||||
- [ ] Bulk category/tag assignment
|
||||
- [ ] Bulk author reassignment
|
||||
- [ ] Confirmation dialogs for destructive actions
|
||||
|
||||
#### **2.3 Quick Actions & Inline Editing**
|
||||
- [ ] Quick edit modal for title, excerpt, categories
|
||||
- [ ] Quick status change dropdown
|
||||
- [ ] Quick delete with confirmation
|
||||
- [ ] Quick preview functionality
|
||||
- [ ] Keyboard shortcuts for common actions
|
||||
|
||||
#### **2.4 Enhanced Data Display**
|
||||
- [ ] Customizable columns (show/hide)
|
||||
- [ ] Sortable columns (title, date, author, status)
|
||||
- [ ] Post thumbnails and featured images
|
||||
- [ ] Comment count display
|
||||
- [ ] Last modified date
|
||||
|
||||
**Deliverable**: Professional-grade posts list with bulk operations and quick actions
|
||||
|
||||
---
|
||||
|
||||
### **Phase 3: Full Post Editor & Content Management**
|
||||
**Timeline: 2-3 weeks | Priority: High**
|
||||
|
||||
#### **3.1 Advanced Post Editor**
|
||||
- [ ] Full-screen post editor modal/page
|
||||
- [ ] Rich text editor integration (TinyMCE or modern alternative)
|
||||
- [ ] Markdown support option
|
||||
- [ ] Auto-save functionality
|
||||
- [ ] Draft preview and comparison
|
||||
|
||||
#### **3.2 Media Management Integration**
|
||||
- [ ] Media library integration
|
||||
- [ ] Drag & drop image uploads
|
||||
- [ ] Featured image management
|
||||
- [ ] Image optimization and resizing
|
||||
- [ ] Media gallery management
|
||||
|
||||
#### **3.3 Content Organization**
|
||||
- [ ] Category and tag management
|
||||
- [ ] Custom fields support
|
||||
- [ ] Post templates and reusable content blocks
|
||||
- [ ] Content scheduling with timezone support
|
||||
- [ ] Post revisions and history
|
||||
|
||||
#### **3.4 SEO & Publishing Tools**
|
||||
- [ ] SEO meta fields (title, description, keywords)
|
||||
- [ ] Social media preview settings
|
||||
- [ ] Publishing workflow (draft → review → publish)
|
||||
- [ ] Content validation and quality checks
|
||||
- [ ] Publishing permissions and approvals
|
||||
|
||||
**Deliverable**: Complete post creation and editing experience
|
||||
|
||||
---
|
||||
|
||||
### **Phase 4: Advanced Features & Workflow**
|
||||
**Timeline: 2-3 weeks | Priority: Medium**
|
||||
|
||||
#### **4.1 Editorial Calendar**
|
||||
- [ ] Calendar view for content planning
|
||||
- [ ] Drag & drop post scheduling
|
||||
- [ ] Content timeline visualization
|
||||
- [ ] Deadline tracking and reminders
|
||||
- [ ] Team availability integration
|
||||
|
||||
#### **4.2 Collaboration & Workflow**
|
||||
- [ ] User assignment and notifications
|
||||
- [ ] Review and approval system
|
||||
- [ ] Editorial comments and feedback
|
||||
- [ ] Content submission workflow
|
||||
- [ ] Team collaboration tools
|
||||
|
||||
#### **4.3 Content Analytics**
|
||||
- [ ] Basic performance metrics
|
||||
- [ ] Content health scoring
|
||||
- [ ] Readability analysis
|
||||
- [ ] SEO scoring
|
||||
- [ ] Engagement metrics integration
|
||||
|
||||
#### **4.4 Advanced Publishing Features**
|
||||
- [ ] Multi-site publishing
|
||||
- [ ] Social media auto-posting
|
||||
- [ ] Email newsletter integration
|
||||
- [ ] Content syndication
|
||||
- [ ] A/B testing framework
|
||||
|
||||
**Deliverable**: Professional content management workflow system
|
||||
|
||||
---
|
||||
|
||||
### **Phase 5: Performance & Polish**
|
||||
**Timeline: 1-2 weeks | Priority: Medium**
|
||||
|
||||
#### **5.1 Performance Optimization**
|
||||
- [ ] Virtual scrolling for large post lists
|
||||
- [ ] Advanced caching strategies
|
||||
- [ ] Lazy loading for images and content
|
||||
- [ ] Optimized API calls and data fetching
|
||||
- [ ] Bundle size optimization
|
||||
|
||||
#### **5.2 User Experience Polish**
|
||||
- [ ] Advanced keyboard shortcuts
|
||||
- [ ] Drag & drop reordering
|
||||
- [ ] Customizable dashboard layouts
|
||||
- [ ] User preference settings
|
||||
- [ ] Accessibility improvements (WCAG 2.1 AA)
|
||||
|
||||
#### **5.3 Advanced Customization**
|
||||
- [ ] Custom post type support
|
||||
- [ ] Extensible plugin architecture
|
||||
- [ ] Theme customization options
|
||||
- [ ] Advanced user role permissions
|
||||
- [ ] API extensibility
|
||||
|
||||
**Deliverable**: Production-ready, optimized posts management system
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ **Technical Implementation Details**
|
||||
|
||||
### **Phase 1 Dependencies**
|
||||
- WordPress REST API endpoints
|
||||
- Basic React state management
|
||||
- Existing Helix component library
|
||||
|
||||
### **Phase 2 Dependencies**
|
||||
- Enhanced WordPress REST API queries
|
||||
- Advanced filtering logic
|
||||
- Bulk operations API endpoints
|
||||
|
||||
### **Phase 3 Dependencies**
|
||||
- Rich text editor library
|
||||
- Media management API
|
||||
- Advanced WordPress hooks and filters
|
||||
|
||||
### **Phase 4 Dependencies**
|
||||
- Calendar component library
|
||||
- Real-time updates (WebSocket/polling)
|
||||
- Analytics and metrics APIs
|
||||
|
||||
### **Phase 5 Dependencies**
|
||||
- Performance monitoring tools
|
||||
- Accessibility testing tools
|
||||
- Advanced WordPress development hooks
|
||||
|
||||
## <20><> **Success Criteria by Phase**
|
||||
|
||||
- **Phase 1**: Users can view, create, edit, and delete posts with basic filtering
|
||||
- **Phase 2**: Users can efficiently manage multiple posts with bulk operations
|
||||
- **Phase 3**: Users have a complete content creation and editing experience
|
||||
- **Phase 4**: Teams can collaborate effectively with advanced workflow tools
|
||||
- **Phase 5**: System is performant, accessible, and production-ready
|
||||
|
||||
## 🔄 **Iteration & Testing Strategy**
|
||||
|
||||
- **End of each phase**: User testing and feedback collection
|
||||
- **Continuous**: Code review and quality assurance
|
||||
- **Phase transitions**: Performance testing and optimization
|
||||
- **Final phase**: Comprehensive testing across different WordPress setups
|
||||
|
||||
This phased approach ensures that:
|
||||
1. **Each phase delivers immediate value** to users
|
||||
2. **Development is manageable** and can be completed in realistic timeframes
|
||||
3. **Testing and feedback** can be incorporated throughout the process
|
||||
4. **Dependencies are clearly identified** and managed
|
||||
5. **The system can be deployed** after each phase if needed
|
|
@ -1,179 +0,0 @@
|
|||
/* Posts Page Styles */
|
||||
.helix-page {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.helix-page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e1e5e9;
|
||||
}
|
||||
|
||||
.helix-page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.helix-page-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.helix-button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.helix-button--primary {
|
||||
background-color: #007cba;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.helix-button--primary:hover {
|
||||
background-color: #005a87;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.helix-button--secondary {
|
||||
background-color: #f0f0f1;
|
||||
color: #1a1a1a;
|
||||
border: 1px solid #c3c4c7;
|
||||
}
|
||||
|
||||
.helix-button--secondary:hover {
|
||||
background-color: #dcdcde;
|
||||
border-color: #8c8f94;
|
||||
}
|
||||
|
||||
.helix-button--icon {
|
||||
padding: 8px;
|
||||
min-width: 36px;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.helix-button--small {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.helix-button--icon:hover {
|
||||
background-color: #f0f0f1;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.helix-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Error State */
|
||||
.helix-error {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background-color: #fef7f1;
|
||||
border: 1px solid #f0b849;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.helix-error h2 {
|
||||
color: #d63638;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.helix-error p {
|
||||
color: #50575e;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.helix-loading {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.helix-loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f0f0f1;
|
||||
border-top: 4px solid #007cba;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.helix-empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.helix-empty-state h3 {
|
||||
margin-bottom: 12px;
|
||||
font-size: 1.5rem;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.helix-page {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.helix-page-header {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.helix-page-header h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.helix-page-actions {
|
||||
width: 100%;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.helix-button {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.helix-page {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.helix-page-header h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
|
@ -1,241 +0,0 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import PostsList from './components/PostsList';
|
||||
import PostFilters from './components/PostFilters';
|
||||
import './Posts.css';
|
||||
|
||||
/**
|
||||
* Main Posts Management Page Component
|
||||
* Phase 1: Foundation & Core List View with Pagination
|
||||
*/
|
||||
export default function Posts() {
|
||||
const [ posts, setPosts ] = useState( [] );
|
||||
const [ loading, setLoading ] = useState( true );
|
||||
const [ error, setError ] = useState( null );
|
||||
const [ filters, setFilters ] = useState( {
|
||||
search: '',
|
||||
status: 'all',
|
||||
author: 'all',
|
||||
dateRange: 'all',
|
||||
} );
|
||||
const [ pagination, setPagination ] = useState( {
|
||||
page: 1,
|
||||
perPage: 10, // Reduced for proper pagination
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
} );
|
||||
|
||||
/**
|
||||
* Fetch posts from WordPress REST API
|
||||
*/
|
||||
const fetchPosts = useCallback( async () => {
|
||||
setLoading( true );
|
||||
setError( null );
|
||||
|
||||
try {
|
||||
const queryParams = new URLSearchParams( {
|
||||
page: pagination.page,
|
||||
per_page: pagination.perPage,
|
||||
} );
|
||||
|
||||
// Add all filter parameters to API call
|
||||
if ( filters.search ) {
|
||||
queryParams.set( 'search', filters.search );
|
||||
}
|
||||
if ( filters.author !== 'all' ) {
|
||||
queryParams.set( 'author', filters.author );
|
||||
}
|
||||
if ( filters.status !== 'all' ) {
|
||||
queryParams.set( 'status', filters.status );
|
||||
}
|
||||
// Note: date filtering will be done client-side
|
||||
|
||||
// Try to get the API URL from helixData, fallback to standard WordPress REST API
|
||||
const apiUrl = `${
|
||||
window.helixData?.wpRestUrl ||
|
||||
window.location.origin + '/wp-json/wp/v2/'
|
||||
}posts?${ queryParams }`;
|
||||
|
||||
// Try different authentication methods
|
||||
const headers = {
|
||||
'X-WP-Nonce': window.helixData?.nonce || '',
|
||||
Authorization: `Bearer ${ window.helixData?.nonce || '' }`,
|
||||
};
|
||||
|
||||
// Add nonce as query parameter as well
|
||||
if ( window.helixData?.nonce ) {
|
||||
queryParams.set( '_wpnonce', window.helixData.nonce );
|
||||
}
|
||||
|
||||
const response = await fetch( apiUrl, { headers } );
|
||||
|
||||
if ( ! response.ok ) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(
|
||||
`HTTP error! status: ${ response.status }, response: ${ errorText }`
|
||||
);
|
||||
}
|
||||
|
||||
const postsData = await response.json();
|
||||
// Store posts for current page
|
||||
setPosts( postsData );
|
||||
|
||||
// Get pagination info from API response headers
|
||||
const total = response.headers.get( 'X-WP-Total' );
|
||||
const totalPages = response.headers.get( 'X-WP-TotalPages' );
|
||||
|
||||
setPagination( ( prev ) => ( {
|
||||
...prev,
|
||||
total: parseInt( total ) || postsData.length,
|
||||
totalPages:
|
||||
parseInt( totalPages ) ||
|
||||
Math.ceil(
|
||||
( parseInt( total ) || postsData.length ) / prev.perPage
|
||||
),
|
||||
} ) );
|
||||
} catch ( err ) {
|
||||
setError( err.message );
|
||||
} finally {
|
||||
setLoading( false );
|
||||
}
|
||||
}, [ pagination.page, pagination.perPage, filters ] );
|
||||
|
||||
useEffect( () => {
|
||||
fetchPosts();
|
||||
}, [ fetchPosts ] );
|
||||
|
||||
useEffect( () => {
|
||||
fetchPosts();
|
||||
}, [ filters.status, filters.author, filters.search, fetchPosts ] );
|
||||
|
||||
/**
|
||||
* Handle filter changes
|
||||
*/
|
||||
const handleFilterChange = ( newFilters ) => {
|
||||
setFilters( newFilters );
|
||||
setPagination( ( prev ) => ( { ...prev, page: 1 } ) );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle pagination changes
|
||||
*/
|
||||
const handlePageChange = ( newPage ) => {
|
||||
setPagination( ( prev ) => ( { ...prev, page: newPage } ) );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle post deletion
|
||||
*/
|
||||
const handlePostDelete = async ( postId ) => {
|
||||
if (
|
||||
! window.confirm( 'Are you sure you want to delete this post?' )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${
|
||||
window.helixData?.wpRestUrl ||
|
||||
window.location.origin + '/wp-json/wp/v2/'
|
||||
}posts/${ postId }`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-WP-Nonce': window.helixData?.nonce || '',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if ( response.ok ) {
|
||||
// Remove post from local state
|
||||
setPosts( ( prev ) =>
|
||||
prev.filter( ( post ) => post.id !== postId )
|
||||
);
|
||||
// Refresh posts to update pagination
|
||||
fetchPosts();
|
||||
} else {
|
||||
throw new Error( 'Failed to delete post' );
|
||||
}
|
||||
} catch ( err ) {
|
||||
setError( `Error deleting post: ${ err.message }` );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle post status change
|
||||
*/
|
||||
const handleStatusChange = async ( postId, newStatus ) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${
|
||||
window.helixData?.wpRestUrl ||
|
||||
window.location.origin + '/wp-json/wp/v2/'
|
||||
}posts/${ postId }`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': window.helixData?.nonce || '',
|
||||
},
|
||||
body: JSON.stringify( { status: newStatus } ),
|
||||
}
|
||||
);
|
||||
|
||||
if ( response.ok ) {
|
||||
// Update post in local state
|
||||
setPosts( ( prev ) =>
|
||||
prev.map( ( post ) =>
|
||||
post.id === postId
|
||||
? { ...post, status: newStatus }
|
||||
: post
|
||||
)
|
||||
);
|
||||
} else {
|
||||
throw new Error( 'Failed to update post status' );
|
||||
}
|
||||
} catch ( err ) {
|
||||
setError( `Error updating post status: ${ err.message }` );
|
||||
}
|
||||
};
|
||||
|
||||
if ( error ) {
|
||||
return (
|
||||
<div className="helix-page">
|
||||
<div className="helix-error">
|
||||
<h2>Error Loading Posts</h2>
|
||||
<p>{ error }</p>
|
||||
<button onClick={ fetchPosts } className="helix-button">
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="helix-page">
|
||||
<div className="helix-page-header">
|
||||
<h1>Posts Management</h1>
|
||||
<div className="helix-page-actions">
|
||||
<button className="helix-button helix-button--primary">
|
||||
Add New Post
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PostFilters
|
||||
filters={ filters }
|
||||
onFilterChange={ handleFilterChange }
|
||||
/>
|
||||
|
||||
<PostsList
|
||||
posts={ posts }
|
||||
loading={ loading }
|
||||
pagination={ pagination }
|
||||
onPageChange={ handlePageChange }
|
||||
onDelete={ handlePostDelete }
|
||||
onStatusChange={ handleStatusChange }
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
# Posts Management Page - Phase 1
|
||||
|
||||
## Overview
|
||||
This is the Posts Management page implementation for Phase 1 of the Helix WordPress admin replacement. It provides a modern, React-based interface for managing WordPress posts.
|
||||
|
||||
## Features Implemented (Phase 1)
|
||||
|
||||
### ✅ Core List View
|
||||
- **Posts Table**: Clean, responsive table displaying posts with essential information
|
||||
- **Post Information**: Title, excerpt, author, categories, tags, status, and date
|
||||
- **Responsive Design**: Mobile-first approach with responsive breakpoints
|
||||
|
||||
### ✅ Essential CRUD Operations
|
||||
- **View Posts**: Display posts with pagination support
|
||||
- **Delete Posts**: Remove posts with confirmation dialog
|
||||
- **Status Changes**: Quick status updates (publish, draft, private, etc.)
|
||||
- **Post Links**: Direct links to view posts and previews
|
||||
|
||||
### ✅ Basic Search & Filtering
|
||||
- **Search**: Search posts by title and content
|
||||
- **Status Filter**: Filter by post status (published, draft, private, etc.)
|
||||
- **Author Filter**: Filter by post author
|
||||
- **Date Filter**: Filter by date ranges (today, week, month, etc.)
|
||||
|
||||
### ✅ User Experience Features
|
||||
- **Loading States**: Spinner and loading indicators
|
||||
- **Error Handling**: Graceful error display with retry options
|
||||
- **Empty States**: Helpful messages when no posts are found
|
||||
- **Pagination**: Navigate through large numbers of posts
|
||||
- **Action Dropdowns**: Contextual actions for each post
|
||||
|
||||
## Component Structure
|
||||
|
||||
```
|
||||
src/pages/Posts/
|
||||
├── Posts.jsx # Main posts page component
|
||||
├── components/
|
||||
│ ├── PostsList.jsx # Posts table and pagination
|
||||
│ ├── PostRow.jsx # Individual post row with actions
|
||||
│ └── PostFilters.jsx # Search and filter controls
|
||||
├── utils/
|
||||
│ └── postsAPI.js # WordPress REST API integration
|
||||
├── Posts.css # Main page styles
|
||||
└── components/
|
||||
├── PostsList.css # Table and list styles
|
||||
├── PostRow.css # Row and action styles
|
||||
└── PostFilters.css # Filter form styles
|
||||
```
|
||||
|
||||
## API Integration
|
||||
|
||||
The page integrates with WordPress REST API endpoints:
|
||||
- `GET /wp-json/wp/v2/posts` - Fetch posts with filters and pagination
|
||||
- `POST /wp-json/wp/v2/posts/{id}` - Update post status
|
||||
- `DELETE /wp-json/wp/v2/posts/{id}` - Delete posts
|
||||
- `GET /wp-json/wp/v2/users` - Fetch authors for filtering
|
||||
- `GET /wp-json/wp/v2/categories` - Fetch categories for filtering
|
||||
|
||||
## Styling
|
||||
|
||||
- **Design System**: Consistent with Helix design patterns
|
||||
- **Responsive**: Mobile-first responsive design
|
||||
- **Accessibility**: Proper contrast, focus states, and semantic HTML
|
||||
- **Modern UI**: Clean, professional appearance with subtle shadows and animations
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Modern browsers with ES6+ support
|
||||
- Responsive design for mobile and tablet devices
|
||||
- Graceful degradation for older browsers
|
||||
|
||||
## Next Steps (Phase 2)
|
||||
|
||||
- [ ] Bulk operations (select multiple posts)
|
||||
- [ ] Enhanced filtering (category, tag combinations)
|
||||
- [ ] Quick edit functionality
|
||||
- [ ] Advanced search options
|
||||
- [ ] Post creation form
|
||||
- [ ] Enhanced post editor
|
||||
|
||||
## Usage
|
||||
|
||||
1. Navigate to the Posts menu in Helix admin
|
||||
2. Use search and filters to find specific posts
|
||||
3. Click the action menu (⋮) on any post row for options
|
||||
4. Use pagination to navigate through large numbers of posts
|
||||
5. Click post titles to view posts in new tabs
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- Built with React hooks for state management
|
||||
- Uses WordPress REST API for data operations
|
||||
- Implements proper error handling and loading states
|
||||
- Follows Helix component patterns and styling conventions
|
||||
- Includes comprehensive responsive design
|
|
@ -1,133 +0,0 @@
|
|||
/* Post Filters Styles */
|
||||
.helix-post-filters {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.helix-post-filters__row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.helix-post-filters__search {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.helix-post-filters__controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.helix-post-filters__actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Filter Group Styles */
|
||||
.helix-filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.helix-filter-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Form Input Styles */
|
||||
.helix-input,
|
||||
.helix-select {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #c3c4c7;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.helix-input:focus,
|
||||
.helix-select:focus {
|
||||
outline: none;
|
||||
border-color: #007cba;
|
||||
box-shadow: 0 0 0 1px #007cba;
|
||||
}
|
||||
|
||||
.helix-input::placeholder {
|
||||
color: #8c8f94;
|
||||
}
|
||||
|
||||
.helix-select {
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
min-width: 140px;
|
||||
height: 42px; /* Match input height */
|
||||
}
|
||||
|
||||
.helix-select option {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.helix-post-filters {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.helix-post-filters__row {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.helix-post-filters__search {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.helix-post-filters__controls {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.helix-post-filters__actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.helix-select {
|
||||
flex: 1;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.helix-post-filters {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.helix-post-filters__controls {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.helix-post-filters__actions {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.helix-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import './PostFilters.css';
|
||||
|
||||
/**
|
||||
* Post Filters Component - Search and filtering controls
|
||||
*/
|
||||
export default function PostFilters( { filters, onFilterChange } ) {
|
||||
const [ authors, setAuthors ] = useState( [] );
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [ categories, setCategories ] = useState( [] );
|
||||
const [ localFilters, setLocalFilters ] = useState( filters );
|
||||
|
||||
useEffect( () => {
|
||||
fetchAuthors();
|
||||
fetchCategories();
|
||||
}, [] );
|
||||
|
||||
useEffect( () => {
|
||||
setLocalFilters( filters );
|
||||
}, [ filters ] );
|
||||
|
||||
/**
|
||||
* Fetch authors for filter dropdown
|
||||
*/
|
||||
const fetchAuthors = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${
|
||||
window.helixData?.wpRestUrl ||
|
||||
window.location.origin + '/wp-json/wp/v2/'
|
||||
}users?per_page=100`
|
||||
);
|
||||
if ( response.ok ) {
|
||||
const authorsData = await response.json();
|
||||
setAuthors( authorsData );
|
||||
}
|
||||
} catch ( error ) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch categories for filter dropdown
|
||||
*/
|
||||
const fetchCategories = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`${
|
||||
window.helixData?.wpRestUrl ||
|
||||
window.location.origin + '/wp-json/wp/v2/'
|
||||
}categories?per_page=100`
|
||||
);
|
||||
if ( response.ok ) {
|
||||
const categoriesData = await response.json();
|
||||
setCategories( categoriesData );
|
||||
}
|
||||
} catch ( error ) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle filter input changes
|
||||
*/
|
||||
const handleFilterChange = ( key, value ) => {
|
||||
const newFilters = { ...localFilters, [ key ]: value };
|
||||
setLocalFilters( newFilters );
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply filters
|
||||
*/
|
||||
const handleApplyFilters = () => {
|
||||
onFilterChange( localFilters );
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
const handleClearFilters = () => {
|
||||
const clearedFilters = {
|
||||
search: '',
|
||||
status: 'all',
|
||||
author: 'all',
|
||||
dateRange: 'all',
|
||||
};
|
||||
setLocalFilters( clearedFilters );
|
||||
onFilterChange( clearedFilters );
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if any filters are active
|
||||
*/
|
||||
const hasActiveFilters = () => {
|
||||
return (
|
||||
localFilters.search ||
|
||||
localFilters.status !== 'all' ||
|
||||
localFilters.author !== 'all' ||
|
||||
localFilters.dateRange !== 'all'
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="helix-post-filters">
|
||||
<div className="helix-post-filters__row">
|
||||
<div className="helix-post-filters__search">
|
||||
<label className="helix-filter-label">Search Posts</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search posts..."
|
||||
value={ localFilters.search }
|
||||
onChange={ ( e ) =>
|
||||
handleFilterChange( 'search', e.target.value )
|
||||
}
|
||||
className="helix-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="helix-post-filters__controls">
|
||||
<div className="helix-filter-group">
|
||||
<label className="helix-filter-label">Status</label>
|
||||
<select
|
||||
value={ localFilters.status }
|
||||
onChange={ ( e ) =>
|
||||
handleFilterChange( 'status', e.target.value )
|
||||
}
|
||||
className="helix-select"
|
||||
>
|
||||
<option value="all">All Statuses</option>
|
||||
<option value="publish">Published</option>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="private">Private</option>
|
||||
<option value="future">Scheduled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="helix-filter-group">
|
||||
<label className="helix-filter-label">Author</label>
|
||||
<select
|
||||
value={ localFilters.author }
|
||||
onChange={ ( e ) =>
|
||||
handleFilterChange( 'author', e.target.value )
|
||||
}
|
||||
className="helix-select"
|
||||
>
|
||||
<option value="all">All Authors</option>
|
||||
{ authors.map( ( author ) => (
|
||||
<option key={ author.id } value={ author.id }>
|
||||
{ author.name }
|
||||
</option>
|
||||
) ) }
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="helix-filter-group">
|
||||
<label className="helix-filter-label">Date Range</label>
|
||||
<select
|
||||
value={ localFilters.dateRange }
|
||||
onChange={ ( e ) =>
|
||||
handleFilterChange(
|
||||
'dateRange',
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
className="helix-select"
|
||||
>
|
||||
<option value="all">All Dates</option>
|
||||
<option value="today">Today</option>
|
||||
<option value="yesterday">Yesterday</option>
|
||||
<option value="week">This Week</option>
|
||||
<option value="month">This Month</option>
|
||||
<option value="quarter">This Quarter</option>
|
||||
<option value="year">This Year</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="helix-post-filters__actions">
|
||||
<button
|
||||
className="helix-button helix-button--primary"
|
||||
onClick={ handleApplyFilters }
|
||||
>
|
||||
Apply Filters
|
||||
</button>
|
||||
{ hasActiveFilters() && (
|
||||
<button
|
||||
className="helix-button helix-button--secondary"
|
||||
onClick={ handleClearFilters }
|
||||
>
|
||||
Clear Filters
|
||||
</button>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
/* Post Row Styles */
|
||||
.helix-post-row {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.helix-post-row:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Post Actions Styles */
|
||||
.helix-post-actions {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Quick Actions Styles */
|
||||
.helix-post-quick-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.helix-button--small {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.helix-post-actions__dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #e1e5e9;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
min-width: 200px;
|
||||
padding: 8px 0;
|
||||
margin-top: 4px;
|
||||
/* Ensure dropdown is above other elements */
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.helix-dropdown-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.helix-dropdown-item:hover {
|
||||
background-color: #f0f0f1;
|
||||
}
|
||||
|
||||
.helix-dropdown-item:disabled {
|
||||
color: #8c8f94;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.helix-dropdown-item:disabled:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.helix-dropdown-item--danger {
|
||||
color: #d63638;
|
||||
}
|
||||
|
||||
.helix-dropdown-item--danger:hover {
|
||||
background-color: #fef7f1;
|
||||
}
|
||||
|
||||
.helix-dropdown-divider {
|
||||
margin: 8px 0;
|
||||
border: none;
|
||||
border-top: 1px solid #e1e5e9;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.helix-post-actions__dropdown {
|
||||
right: auto;
|
||||
left: 0;
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.helix-post-actions__dropdown {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.helix-dropdown-item {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
|
@ -1,307 +0,0 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import './PostRow.css';
|
||||
|
||||
/**
|
||||
* Individual Post Row Component
|
||||
*/
|
||||
export default function PostRow( { post, onDelete, onStatusChange } ) {
|
||||
const [ showActions, setShowActions ] = useState( false );
|
||||
const actionsRef = useRef( null );
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect( () => {
|
||||
const handleClickOutside = ( event ) => {
|
||||
if (
|
||||
actionsRef.current &&
|
||||
! actionsRef.current.contains( event.target )
|
||||
) {
|
||||
setShowActions( false );
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener( 'mousedown', handleClickOutside );
|
||||
return () => {
|
||||
document.removeEventListener( 'mousedown', handleClickOutside );
|
||||
};
|
||||
}, [] );
|
||||
|
||||
/**
|
||||
* Format date for display
|
||||
*/
|
||||
const formatDate = ( dateString ) => {
|
||||
const date = new Date( dateString );
|
||||
return date.toLocaleDateString( 'en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get status badge styling
|
||||
*/
|
||||
const getStatusBadge = ( status ) => {
|
||||
const statusClasses = {
|
||||
publish: 'helix-status-badge--publish',
|
||||
draft: 'helix-status-badge--draft',
|
||||
private: 'helix-status-badge--private',
|
||||
pending: 'helix-status-badge--pending',
|
||||
future: 'helix-status-badge--future',
|
||||
};
|
||||
|
||||
return (
|
||||
<span
|
||||
className={ `helix-status-badge ${
|
||||
statusClasses[ status ] || ''
|
||||
}` }
|
||||
>
|
||||
{ status.charAt( 0 ).toUpperCase() + status.slice( 1 ) }
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle status change
|
||||
*/
|
||||
const handleStatusChange = ( newStatus ) => {
|
||||
onStatusChange( post.id, newStatus );
|
||||
setShowActions( false );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle post deletion
|
||||
*/
|
||||
const handleDelete = () => {
|
||||
onDelete( post.id );
|
||||
setShowActions( false );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle edit post (open in new tab)
|
||||
*/
|
||||
const handleEditPost = ( postData ) => {
|
||||
// Open WordPress admin edit page in new tab
|
||||
const editUrl = `${
|
||||
window.helixData?.adminUrl || '/wp-admin/'
|
||||
}post.php?post=${ postData.id }&action=edit`;
|
||||
window.open( editUrl, '_blank' );
|
||||
setShowActions( false );
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle quick edit (placeholder for future implementation)
|
||||
*/
|
||||
const handleQuickEdit = () => {
|
||||
// TODO: Implement quick edit modal
|
||||
setShowActions( false );
|
||||
};
|
||||
|
||||
/**
|
||||
* Get excerpt from content
|
||||
*/
|
||||
const getExcerpt = ( content ) => {
|
||||
// Remove HTML tags and get first 100 characters
|
||||
const textContent = content.replace( /<[^>]*>/g, '' );
|
||||
return textContent.length > 100
|
||||
? textContent.substring( 0, 100 ) + '...'
|
||||
: textContent;
|
||||
};
|
||||
|
||||
return (
|
||||
<tr className="helix-post-row">
|
||||
<td className="helix-post-row__checkbox">
|
||||
<input type="checkbox" />
|
||||
</td>
|
||||
<td className="helix-post-row__title">
|
||||
<div className="helix-post-title">
|
||||
<h4 className="helix-post-title__text">
|
||||
<a
|
||||
href={ post.link }
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{ post.title.rendered }
|
||||
</a>
|
||||
</h4>
|
||||
<p className="helix-post-title__excerpt">
|
||||
{ getExcerpt( post.content.rendered ) }
|
||||
</p>
|
||||
<div className="helix-post-quick-actions">
|
||||
<button
|
||||
className="helix-button helix-button--small helix-button--secondary"
|
||||
onClick={ () => handleEditPost( post ) }
|
||||
title="Edit Post"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="helix-button helix-button--small helix-button--secondary"
|
||||
onClick={ () => window.open( post.link, '_blank' ) }
|
||||
title="View Post"
|
||||
>
|
||||
View
|
||||
</button>
|
||||
<button
|
||||
className="helix-button helix-button--small helix-button--secondary"
|
||||
onClick={ () =>
|
||||
window.open(
|
||||
`${ post.link }?preview=true`,
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
title="Preview Post"
|
||||
>
|
||||
Preview
|
||||
</button>
|
||||
{ post.status !== 'publish' && (
|
||||
<button
|
||||
className="helix-button helix-button--small helix-button--primary"
|
||||
onClick={ () =>
|
||||
handleStatusChange( 'publish' )
|
||||
}
|
||||
title="Publish Post"
|
||||
>
|
||||
Publish
|
||||
</button>
|
||||
) }
|
||||
{ post.status === 'publish' && (
|
||||
<button
|
||||
className="helix-button helix-button--small helix-button--secondary"
|
||||
onClick={ () => handleStatusChange( 'draft' ) }
|
||||
title="Move to Draft"
|
||||
>
|
||||
Draft
|
||||
</button>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="helix-post-row__author">
|
||||
{ post._embedded?.author?.[ 0 ]?.name || 'Unknown' }
|
||||
</td>
|
||||
<td className="helix-post-row__categories">
|
||||
{ post._embedded?.[ 'wp:term' ]?.[ 0 ]?.map( ( term ) => (
|
||||
<span key={ term.id } className="helix-category-tag">
|
||||
{ term.name }
|
||||
</span>
|
||||
) ) || 'Uncategorized' }
|
||||
</td>
|
||||
<td className="helix-post-row__tags">
|
||||
{ post._embedded?.[ 'wp:term' ]?.[ 1 ]?.map( ( tag ) => (
|
||||
<span key={ tag.id } className="helix-tag">
|
||||
{ tag.name }
|
||||
</span>
|
||||
) ) || 'No tags' }
|
||||
</td>
|
||||
<td className="helix-post-row__status">
|
||||
{ getStatusBadge( post.status ) }
|
||||
</td>
|
||||
<td className="helix-post-row__date">
|
||||
{ formatDate( post.date ) }
|
||||
</td>
|
||||
<td className="helix-post-row__actions">
|
||||
<div className="helix-post-actions" ref={ actionsRef }>
|
||||
<button
|
||||
className="helix-button helix-button--icon"
|
||||
onClick={ () => setShowActions( ! showActions ) }
|
||||
title="More actions"
|
||||
>
|
||||
⋮
|
||||
</button>
|
||||
|
||||
{ showActions && (
|
||||
<div className="helix-post-actions__dropdown">
|
||||
<button
|
||||
className="helix-dropdown-item"
|
||||
onClick={ () => handleQuickEdit( post ) }
|
||||
>
|
||||
Quick Edit
|
||||
</button>
|
||||
<button
|
||||
className="helix-dropdown-item"
|
||||
onClick={ () =>
|
||||
handleStatusChange( 'private' )
|
||||
}
|
||||
disabled={ post.status === 'private' }
|
||||
>
|
||||
Make Private
|
||||
</button>
|
||||
<button
|
||||
className="helix-dropdown-item"
|
||||
onClick={ () =>
|
||||
handleStatusChange( 'pending' )
|
||||
}
|
||||
disabled={ post.status === 'pending' }
|
||||
>
|
||||
Mark Pending
|
||||
</button>
|
||||
<button
|
||||
className="helix-dropdown-item"
|
||||
onClick={ () => {
|
||||
// eslint-disable-next-line no-undef
|
||||
if ( navigator.clipboard ) {
|
||||
// eslint-disable-next-line no-undef
|
||||
navigator.clipboard.writeText(
|
||||
post.link
|
||||
);
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
// eslint-disable-next-line no-undef
|
||||
const textArea =
|
||||
// eslint-disable-next-line no-undef
|
||||
document.createElement(
|
||||
'textarea'
|
||||
);
|
||||
textArea.value = post.link;
|
||||
// eslint-disable-next-line no-undef
|
||||
document.body.appendChild( textArea );
|
||||
textArea.select();
|
||||
// eslint-disable-next-line no-undef
|
||||
document.execCommand( 'copy' );
|
||||
// eslint-disable-next-line no-undef
|
||||
document.body.removeChild( textArea );
|
||||
}
|
||||
setShowActions( false );
|
||||
} }
|
||||
>
|
||||
Copy Link
|
||||
</button>
|
||||
<button
|
||||
className="helix-dropdown-item"
|
||||
onClick={ () =>
|
||||
handleStatusChange( 'publish' )
|
||||
}
|
||||
disabled={ post.status === 'publish' }
|
||||
>
|
||||
Publish
|
||||
</button>
|
||||
<button
|
||||
className="helix-dropdown-item"
|
||||
onClick={ () => handleStatusChange( 'draft' ) }
|
||||
disabled={ post.status === 'draft' }
|
||||
>
|
||||
Move to Draft
|
||||
</button>
|
||||
<button
|
||||
className="helix-dropdown-item"
|
||||
onClick={ () =>
|
||||
handleStatusChange( 'private' )
|
||||
}
|
||||
disabled={ post.status === 'private' }
|
||||
>
|
||||
Make Private
|
||||
</button>
|
||||
<hr className="helix-dropdown-divider" />
|
||||
<button
|
||||
className="helix-dropdown-item helix-dropdown-item--danger"
|
||||
onClick={ handleDelete }
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
|
@ -1,224 +0,0 @@
|
|||
/* Posts List Styles */
|
||||
.helix-posts-list {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.helix-posts-table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.helix-posts-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.helix-posts-table th {
|
||||
background-color: #f8f9fa;
|
||||
padding: 16px 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
border-bottom: 2px solid #e1e5e9;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.helix-posts-table td {
|
||||
padding: 16px 12px;
|
||||
border-bottom: 1px solid #f0f0f1;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.helix-posts-table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Table Column Specific Styles */
|
||||
.helix-posts-table__checkbox {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.helix-posts-table__checkbox input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.helix-posts-table__title {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.helix-posts-table__author {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.helix-posts-table__categories {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.helix-posts-table__tags {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.helix-posts-table__status {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.helix-posts-table__date {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.helix-posts-table__actions {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Post Title Styles */
|
||||
.helix-post-title__text {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.helix-post-title__text a {
|
||||
color: #007cba;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.helix-post-title__text a:hover {
|
||||
color: #005a87;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.helix-post-title__excerpt {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #646970;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Status Badge Styles */
|
||||
.helix-status-badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.helix-status-badge--publish {
|
||||
background-color: #d1e7dd;
|
||||
color: #0f5132;
|
||||
}
|
||||
|
||||
.helix-status-badge--draft {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.helix-status-badge--private {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.helix-status-badge--pending {
|
||||
background-color: #cce5ff;
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
.helix-status-badge--future {
|
||||
background-color: #e2e3e5;
|
||||
color: #383d41;
|
||||
}
|
||||
|
||||
/* Category and Tag Styles */
|
||||
.helix-category-tag,
|
||||
.helix-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
margin: 2px 4px 2px 0;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background-color: #f0f0f1;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.helix-category-tag {
|
||||
background-color: #e7f3ff;
|
||||
color: #007cba;
|
||||
}
|
||||
|
||||
/* Pagination Styles */
|
||||
.helix-pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid #e1e5e9;
|
||||
}
|
||||
|
||||
.helix-pagination__info {
|
||||
color: #646970;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.helix-pagination__controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.helix-pagination__current {
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.helix-posts-table__categories,
|
||||
.helix-posts-table__tags {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.helix-posts-table__author {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.helix-posts-table__title {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.helix-pagination {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.helix-posts-table th,
|
||||
.helix-posts-table td {
|
||||
padding: 12px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.helix-posts-table__date {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.helix-post-title__text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.helix-post-title__excerpt {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
import React from 'react';
|
||||
import PostRow from './PostRow';
|
||||
import './PostsList.css';
|
||||
|
||||
/**
|
||||
* Posts List Component - Displays posts in a table format
|
||||
*/
|
||||
export default function PostsList( {
|
||||
posts,
|
||||
loading,
|
||||
pagination,
|
||||
onPageChange,
|
||||
onDelete,
|
||||
onStatusChange,
|
||||
} ) {
|
||||
if ( loading ) {
|
||||
return (
|
||||
<div className="helix-loading">
|
||||
<div className="helix-loading-spinner"></div>
|
||||
<p>Loading posts...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( posts.length === 0 ) {
|
||||
return (
|
||||
<div className="helix-empty-state">
|
||||
<h3>No posts found</h3>
|
||||
<p>Try adjusting your filters or create a new post.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="helix-posts-list">
|
||||
<div className="helix-posts-table-container">
|
||||
<table className="helix-posts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="helix-posts-table__checkbox">
|
||||
<input type="checkbox" />
|
||||
</th>
|
||||
<th className="helix-posts-table__title">Title</th>
|
||||
<th className="helix-posts-table__author">
|
||||
Author
|
||||
</th>
|
||||
<th className="helix-posts-table__categories">
|
||||
Categories
|
||||
</th>
|
||||
<th className="helix-posts-table__tags">Tags</th>
|
||||
<th className="helix-posts-table__status">
|
||||
Status
|
||||
</th>
|
||||
<th className="helix-posts-table__date">Date</th>
|
||||
<th className="helix-posts-table__actions">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ posts.map( ( post ) => (
|
||||
<PostRow
|
||||
key={ post.id }
|
||||
post={ post }
|
||||
onDelete={ onDelete }
|
||||
onStatusChange={ onStatusChange }
|
||||
/>
|
||||
) ) }
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{ pagination.totalPages > 1 && (
|
||||
<div className="helix-pagination">
|
||||
<div className="helix-pagination__info">
|
||||
Showing{ ' ' }
|
||||
{ ( pagination.page - 1 ) * pagination.perPage + 1 } to{ ' ' }
|
||||
{ Math.min(
|
||||
pagination.page * pagination.perPage,
|
||||
pagination.total
|
||||
) }{ ' ' }
|
||||
of { pagination.total } posts
|
||||
</div>
|
||||
<div className="helix-pagination__controls">
|
||||
<button
|
||||
className="helix-button helix-button--secondary"
|
||||
disabled={ pagination.page === 1 }
|
||||
onClick={ () =>
|
||||
onPageChange( pagination.page - 1 )
|
||||
}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span className="helix-pagination__current">
|
||||
Page { pagination.page } of{ ' ' }
|
||||
{ pagination.totalPages }
|
||||
</span>
|
||||
<button
|
||||
className="helix-button helix-button--secondary"
|
||||
disabled={
|
||||
pagination.page === pagination.totalPages
|
||||
}
|
||||
onClick={ () =>
|
||||
onPageChange( pagination.page + 1 )
|
||||
}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,185 +0,0 @@
|
|||
/**
|
||||
* Posts API Utility Functions
|
||||
* Centralized API calls for posts management
|
||||
*/
|
||||
|
||||
const API_BASE =
|
||||
window.helixData?.wpRestUrl || window.location.origin + '/wp-json/wp/v2/';
|
||||
|
||||
/**
|
||||
* Fetch posts with filters and pagination
|
||||
*/
|
||||
export const fetchPosts = async ( params = {} ) => {
|
||||
const queryParams = new URLSearchParams( {
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
...params,
|
||||
} );
|
||||
|
||||
// Remove 'all' values as they're not valid API parameters
|
||||
[ 'status', 'author', 'dateRange' ].forEach( ( key ) => {
|
||||
if ( params[ key ] === 'all' ) {
|
||||
queryParams.delete( key );
|
||||
}
|
||||
} );
|
||||
|
||||
try {
|
||||
const response = await fetch( `${ API_BASE }posts?${ queryParams }` );
|
||||
|
||||
if ( ! response.ok ) {
|
||||
throw new Error( `HTTP error! status: ${ response.status }` );
|
||||
}
|
||||
|
||||
const posts = await response.json();
|
||||
const total = response.headers.get( 'X-WP-Total' );
|
||||
const totalPages = response.headers.get( 'X-WP-TotalPages' );
|
||||
|
||||
return {
|
||||
posts,
|
||||
pagination: {
|
||||
total: parseInt( total ) || 0,
|
||||
totalPages: parseInt( totalPages ) || 0,
|
||||
},
|
||||
};
|
||||
} catch ( error ) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch a single post by ID
|
||||
*/
|
||||
export const fetchPost = async ( postId ) => {
|
||||
try {
|
||||
const response = await fetch( `${ API_BASE }posts/${ postId }` );
|
||||
|
||||
if ( ! response.ok ) {
|
||||
throw new Error( `HTTP error! status: ${ response.status }` );
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch ( error ) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new post
|
||||
*/
|
||||
export const createPost = async ( postData ) => {
|
||||
try {
|
||||
const response = await fetch( `${ API_BASE }posts`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': window.helixData?.nonce || '',
|
||||
},
|
||||
body: JSON.stringify( postData ),
|
||||
} );
|
||||
|
||||
if ( ! response.ok ) {
|
||||
throw new Error( `HTTP error! status: ${ response.status }` );
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch ( error ) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update an existing post
|
||||
*/
|
||||
export const updatePost = async ( postId, postData ) => {
|
||||
try {
|
||||
const response = await fetch( `${ API_BASE }posts/${ postId }`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-WP-Nonce': window.helixData?.nonce || '',
|
||||
},
|
||||
body: JSON.stringify( postData ),
|
||||
} );
|
||||
|
||||
if ( ! response.ok ) {
|
||||
throw new Error( `HTTP error! status: ${ response.status }` );
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch ( error ) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete a post
|
||||
*/
|
||||
export const deletePost = async ( postId ) => {
|
||||
try {
|
||||
const response = await fetch( `${ API_BASE }posts/${ postId }`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-WP-Nonce': window.helixData?.nonce || '',
|
||||
},
|
||||
} );
|
||||
|
||||
if ( ! response.ok ) {
|
||||
throw new Error( `HTTP error! status: ${ response.status }` );
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch ( error ) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch authors for filter dropdown
|
||||
*/
|
||||
export const fetchAuthors = async () => {
|
||||
try {
|
||||
const response = await fetch( `${ API_BASE }users?per_page=100` );
|
||||
|
||||
if ( ! response.ok ) {
|
||||
throw new Error( `HTTP error! status: ${ response.status }` );
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch ( error ) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch categories for filter dropdown
|
||||
*/
|
||||
export const fetchCategories = async () => {
|
||||
try {
|
||||
const response = await fetch( `${ API_BASE }categories?per_page=100` );
|
||||
|
||||
if ( ! response.ok ) {
|
||||
throw new Error( `HTTP error! status: ${ response.status }` );
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch ( error ) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch tags for filter dropdown
|
||||
*/
|
||||
export const fetchTags = async () => {
|
||||
try {
|
||||
const response = await fetch( `${ API_BASE }tags?per_page=100` );
|
||||
|
||||
if ( ! response.ok ) {
|
||||
throw new Error( `HTTP error! status: ${ response.status }` );
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch ( error ) {
|
||||
throw error;
|
||||
}
|
||||
};
|
|
@ -29,16 +29,6 @@ export default function Settings() {
|
|||
const [ activeTab, setActiveTab ] = useState( 'site' );
|
||||
const [ notification, setNotification ] = useState( null );
|
||||
|
||||
/**
|
||||
* Smoothly scroll to the top of the page
|
||||
*/
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo( {
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
} );
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const result = await saveSettings();
|
||||
|
||||
|
@ -55,9 +45,6 @@ export default function Settings() {
|
|||
'Failed to save settings. Please try again.',
|
||||
} );
|
||||
}
|
||||
|
||||
// Scroll to top to show the notification
|
||||
scrollToTop();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
|
@ -66,9 +53,6 @@ export default function Settings() {
|
|||
type: 'info',
|
||||
message: 'Changes have been reset to their original values.',
|
||||
} );
|
||||
|
||||
// Scroll to top to show the notification
|
||||
scrollToTop();
|
||||
};
|
||||
|
||||
const handleTabChange = ( tabId ) => {
|
||||
|
|
|
@ -32,80 +32,76 @@ const MediaAssetsSettings = ( { settings, updateSetting } ) => {
|
|||
min={ 0 }
|
||||
/>
|
||||
|
||||
{ /* Image Sizes Section - Header spans full width */ }
|
||||
<div className="helix-settings-subsection-header">
|
||||
<div className="helix-settings-subsection">
|
||||
<h4>Image Sizes</h4>
|
||||
|
||||
<NumberInput
|
||||
label="Thumbnail Width"
|
||||
description="Maximum width of thumbnail images in pixels."
|
||||
value={ settings.thumbnailSizeW }
|
||||
onChange={ ( value ) =>
|
||||
updateSetting( 'thumbnailSizeW', value )
|
||||
}
|
||||
min={ 0 }
|
||||
max={ 2000 }
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Thumbnail Height"
|
||||
description="Maximum height of thumbnail images in pixels."
|
||||
value={ settings.thumbnailSizeH }
|
||||
onChange={ ( value ) =>
|
||||
updateSetting( 'thumbnailSizeH', value )
|
||||
}
|
||||
min={ 0 }
|
||||
max={ 2000 }
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Medium Width"
|
||||
description="Maximum width of medium-sized images in pixels."
|
||||
value={ settings.mediumSizeW }
|
||||
onChange={ ( value ) =>
|
||||
updateSetting( 'mediumSizeW', value )
|
||||
}
|
||||
min={ 0 }
|
||||
max={ 2000 }
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Medium Height"
|
||||
description="Maximum height of medium-sized images in pixels."
|
||||
value={ settings.mediumSizeH }
|
||||
onChange={ ( value ) =>
|
||||
updateSetting( 'mediumSizeH', value )
|
||||
}
|
||||
min={ 0 }
|
||||
max={ 2000 }
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Large Width"
|
||||
description="Maximum width of large-sized images in pixels."
|
||||
value={ settings.largeSizeW }
|
||||
onChange={ ( value ) =>
|
||||
updateSetting( 'largeSizeW', value )
|
||||
}
|
||||
min={ 0 }
|
||||
max={ 4000 }
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Large Height"
|
||||
description="Maximum height of large-sized images in pixels."
|
||||
value={ settings.largeSizeH }
|
||||
onChange={ ( value ) =>
|
||||
updateSetting( 'largeSizeH', value )
|
||||
}
|
||||
min={ 0 }
|
||||
max={ 4000 }
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ /* Thumbnail dimensions - side by side */ }
|
||||
<NumberInput
|
||||
label="Thumbnail Width"
|
||||
description="Maximum width of thumbnail images in pixels."
|
||||
value={ settings.thumbnailSizeW }
|
||||
onChange={ ( value ) =>
|
||||
updateSetting( 'thumbnailSizeW', value )
|
||||
}
|
||||
min={ 0 }
|
||||
max={ 2000 }
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Thumbnail Height"
|
||||
description="Maximum height of thumbnail images in pixels."
|
||||
value={ settings.thumbnailSizeH }
|
||||
onChange={ ( value ) =>
|
||||
updateSetting( 'thumbnailSizeH', value )
|
||||
}
|
||||
min={ 0 }
|
||||
max={ 2000 }
|
||||
/>
|
||||
|
||||
{ /* Medium dimensions - side by side */ }
|
||||
<NumberInput
|
||||
label="Medium Width"
|
||||
description="Maximum width of medium-sized images in pixels."
|
||||
value={ settings.mediumSizeW }
|
||||
onChange={ ( value ) =>
|
||||
updateSetting( 'mediumSizeW', value )
|
||||
}
|
||||
min={ 0 }
|
||||
max={ 2000 }
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Medium Height"
|
||||
description="Maximum height of medium-sized images in pixels."
|
||||
value={ settings.mediumSizeH }
|
||||
onChange={ ( value ) =>
|
||||
updateSetting( 'mediumSizeH', value )
|
||||
}
|
||||
min={ 0 }
|
||||
max={ 2000 }
|
||||
/>
|
||||
|
||||
{ /* Large dimensions - side by side */ }
|
||||
<NumberInput
|
||||
label="Large Width"
|
||||
description="Maximum width of large-sized images in pixels."
|
||||
value={ settings.largeSizeW }
|
||||
onChange={ ( value ) =>
|
||||
updateSetting( 'largeSizeW', value )
|
||||
}
|
||||
min={ 0 }
|
||||
max={ 4000 }
|
||||
/>
|
||||
|
||||
<NumberInput
|
||||
label="Large Height"
|
||||
description="Maximum height of large-sized images in pixels."
|
||||
value={ settings.largeSizeH }
|
||||
onChange={ ( value ) =>
|
||||
updateSetting( 'largeSizeH', value )
|
||||
}
|
||||
min={ 0 }
|
||||
max={ 4000 }
|
||||
/>
|
||||
|
||||
<ToggleInput
|
||||
label="Organize my uploads into month- and year-based folders"
|
||||
description="Organize uploaded files into date-based folder structure."
|
||||
|
|
|
@ -289,45 +289,13 @@
|
|||
|
||||
.helix-settings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* Ensure form fields take full width within their grid cell */
|
||||
.helix-settings-grid .helix-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Handle odd number of fields - make the last field span full width if it's alone */
|
||||
.helix-settings-grid .helix-form-field:last-child:nth-child(odd) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* Ensure subsections don't interfere with grid layout */
|
||||
.helix-settings-subsection .helix-form-field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Ensure consistent spacing between form fields */
|
||||
.helix-settings-grid .helix-form-field + .helix-form-field {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.helix-settings-subsection {
|
||||
border-top: 1px solid var(--helix-color-3);
|
||||
padding-top: 24px;
|
||||
margin-top: 24px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.helix-settings-subsection-header {
|
||||
grid-column: 1 / -1;
|
||||
border-top: 1px solid var(--helix-color-3);
|
||||
padding-top: 24px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.helix-settings-subsection h4 {
|
||||
|
@ -552,18 +520,6 @@
|
|||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.helix-settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.helix-settings-subsection-header {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.helix-settings-page {
|
||||
padding: 15px;
|
||||
|
@ -595,16 +551,6 @@
|
|||
padding: 20px;
|
||||
}
|
||||
|
||||
.helix-settings-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.helix-settings-subsection-header {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.helix-save-buttons {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue