mirror of
https://ghproxy.net/https://github.com/abhijitb/helix.git
synced 2025-08-27 20:13:02 +08:00
Compare commits
9 commits
fc331c2e7c
...
76b5131180
Author | SHA1 | Date | |
---|---|---|---|
|
76b5131180 | ||
|
310e99b204 | ||
|
24008c2a49 | ||
|
1fa23c846e | ||
|
b2e6a0a21f | ||
|
2e385d1f9d | ||
|
9dbbd4a93c | ||
|
9e560f9846 | ||
|
c5a983d24b |
21 changed files with 2544 additions and 275 deletions
|
@ -21,6 +21,10 @@ 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',
|
||||
|
@ -30,13 +34,13 @@ function helix_register_rest_routes() {
|
|||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => 'helix_get_settings',
|
||||
'permission_callback' => 'helix_settings_permissions_check',
|
||||
'args' => helix_get_settings_schema(),
|
||||
'args' => $get_schema,
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => 'helix_update_settings',
|
||||
'permission_callback' => 'helix_settings_permissions_check',
|
||||
'args' => helix_update_settings_schema(),
|
||||
'args' => $update_schema,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
@ -113,30 +117,25 @@ 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_json_params();
|
||||
|
||||
if ( empty( $params ) ) {
|
||||
return new WP_Error(
|
||||
'helix_no_settings_data',
|
||||
__( 'No settings data provided.', 'helix' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
$params = $request->get_params();
|
||||
$allowed_settings = helix_get_allowed_settings();
|
||||
$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 ) ) {
|
||||
$errors[ $setting ] = sprintf(
|
||||
$error_msg = sprintf(
|
||||
/* translators: %s: Setting name */
|
||||
__( 'Setting "%s" is not allowed to be updated.', 'helix' ),
|
||||
__( 'Setting "%s" is not allowed.', '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 ) ) {
|
||||
|
@ -144,13 +143,31 @@ function helix_update_settings( $request ) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Get the WordPress option name for this setting.
|
||||
$option_name = helix_get_wp_option_name( $setting );
|
||||
$result = update_option( $option_name, $sanitized_value );
|
||||
|
||||
// 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 );
|
||||
}
|
||||
|
||||
if ( $result ) {
|
||||
$updated_settings[ $setting ] = $sanitized_value;
|
||||
} else {
|
||||
$errors[ $setting ] = __( 'Failed to update setting.', 'helix' );
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,21 +35,28 @@ function helix_get_settings_schema() {
|
|||
*/
|
||||
function helix_update_settings_schema() {
|
||||
$settings_config = helix_get_settings_config();
|
||||
$schema = array();
|
||||
$schema = array(
|
||||
'type' => 'object',
|
||||
'properties' => array(),
|
||||
);
|
||||
|
||||
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'],
|
||||
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'] ),
|
||||
);
|
||||
|
||||
// Add enum validation if applicable.
|
||||
if ( isset( $setting_config['enum'] ) ) {
|
||||
$schema[ $setting_key ]['enum'] = $setting_config['enum'];
|
||||
$schema['properties'][ $setting_key ]['enum'] = $setting_config['enum'];
|
||||
}
|
||||
|
||||
if ( isset( $setting_config['default'] ) ) {
|
||||
$schema[ $setting_key ]['default'] = $setting_config['default'];
|
||||
// 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'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,6 +64,30 @@ 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.
|
||||
*
|
||||
|
@ -110,12 +141,7 @@ function helix_get_allowed_settings() {
|
|||
$allowed_settings = array_merge( $allowed_settings, array_keys( $settings ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the list of allowed settings.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @param array $allowed_settings Array of allowed setting keys.
|
||||
*/
|
||||
// Filter the list of allowed settings.
|
||||
return apply_filters( 'helix_allowed_settings', $allowed_settings );
|
||||
}
|
||||
|
||||
|
@ -226,7 +252,8 @@ function helix_sanitize_setting_value( $setting, $value ) {
|
|||
// Default sanitization based on type.
|
||||
switch ( $setting_config['type'] ) {
|
||||
case 'string':
|
||||
return sanitize_text_field( $value );
|
||||
$sanitized = sanitize_text_field( $value );
|
||||
break;
|
||||
|
||||
case 'email':
|
||||
$sanitized = sanitize_email( $value );
|
||||
|
@ -237,46 +264,161 @@ function helix_sanitize_setting_value( $setting, $value ) {
|
|||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
return $sanitized;
|
||||
break;
|
||||
|
||||
case 'url':
|
||||
return esc_url_raw( $value );
|
||||
$sanitized = esc_url_raw( $value );
|
||||
break;
|
||||
|
||||
case 'integer':
|
||||
return absint( $value );
|
||||
$sanitized = absint( $value );
|
||||
break;
|
||||
|
||||
case 'number':
|
||||
return floatval( $value );
|
||||
$sanitized = floatval( $value );
|
||||
break;
|
||||
|
||||
case 'boolean':
|
||||
return rest_sanitize_boolean( $value );
|
||||
$sanitized = rest_sanitize_boolean( $value );
|
||||
break;
|
||||
|
||||
default:
|
||||
// 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 )
|
||||
);
|
||||
}
|
||||
}
|
||||
return sanitize_text_field( $value );
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
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 $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive settings configuration.
|
||||
* Update special settings that require custom logic.
|
||||
*
|
||||
* @since 1.0.0
|
||||
* @return array Settings configuration array.
|
||||
* @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.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -286,59 +428,162 @@ function helix_sanitize_setting_value( $setting, $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)',
|
||||
);
|
||||
|
||||
// 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' ) ) {
|
||||
// First, try to get installed languages.
|
||||
if ( function_exists( 'get_available_languages' ) ) {
|
||||
$installed_languages = get_available_languages();
|
||||
} else {
|
||||
$installed_languages = array();
|
||||
}
|
||||
|
||||
$languages = get_available_languages();
|
||||
// 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';
|
||||
}
|
||||
|
||||
if ( ! empty( $languages ) && function_exists( 'wp_get_available_translations' ) ) {
|
||||
$available_translations = wp_get_available_translations();
|
||||
// Get all available translations (including uninstalled ones).
|
||||
if ( 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
|
||||
);
|
||||
}
|
||||
}
|
||||
// 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)';
|
||||
|
||||
$language_options[] = array(
|
||||
'value' => $locale,
|
||||
'label' => $display_label,
|
||||
'installed' => $is_installed,
|
||||
);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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' => $code,
|
||||
'label' => $name
|
||||
);
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -351,85 +596,23 @@ function helix_get_available_languages() {
|
|||
function helix_get_available_timezones() {
|
||||
$timezone_options = array();
|
||||
|
||||
// UTC and common UTC offsets
|
||||
$timezone_options[] = array( 'value' => 'UTC', 'label' => 'UTC' );
|
||||
// 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] );
|
||||
|
||||
// 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})"
|
||||
);
|
||||
// Skip empty values.
|
||||
if ( ! empty( $value ) || '0' === $value ) {
|
||||
$timezone_options[] = array(
|
||||
'value' => $value,
|
||||
'label' => $label,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -437,6 +620,12 @@ 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(
|
||||
|
@ -611,13 +800,13 @@ function helix_get_settings_config() {
|
|||
),
|
||||
'largeSizeW' => array(
|
||||
'label' => __( 'Large Width', 'helix' ),
|
||||
'description' => __( 'Maximum width of large-sized images.', 'helix' ),
|
||||
'description' => __( 'Maximum width of large images.', 'helix' ),
|
||||
'type' => 'integer',
|
||||
'default' => 1024,
|
||||
),
|
||||
'largeSizeH' => array(
|
||||
'label' => __( 'Large Height', 'helix' ),
|
||||
'description' => __( 'Maximum height of large-sized images.', 'helix' ),
|
||||
'description' => __( 'Maximum height of large images.', 'helix' ),
|
||||
'type' => 'integer',
|
||||
'default' => 1024,
|
||||
),
|
||||
|
|
1
build/assets/App-BGAsQ9rL.css
Normal file
1
build/assets/App-BGAsQ9rL.css
Normal file
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": "eb1f0708b5830b82ea720f047c348230",
|
||||
"content-hash": "684994342b1eecfad8ef44a86ff89124",
|
||||
"packages": [],
|
||||
"packages-dev": [
|
||||
{
|
||||
|
@ -187,16 +187,16 @@
|
|||
},
|
||||
{
|
||||
"name": "phpcsstandards/phpcsutils",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPCSStandards/PHPCSUtils.git",
|
||||
"reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad"
|
||||
"reference": "f7eb16f2fa4237d5db9e8fed8050239bee17a9bd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/65355670ac17c34cd235cf9d3ceae1b9252c4dad",
|
||||
"reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad",
|
||||
"url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/f7eb16f2fa4237d5db9e8fed8050239bee17a9bd",
|
||||
"reference": "f7eb16f2fa4237d5db9e8fed8050239bee17a9bd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -276,7 +276,7 @@
|
|||
"type": "thanks_dev"
|
||||
}
|
||||
],
|
||||
"time": "2025-06-12T04:32:33+00:00"
|
||||
"time": "2025-08-10T01:04:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "squizlabs/php_codesniffer",
|
||||
|
|
|
@ -49,6 +49,7 @@ 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,6 +2,7 @@ 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() {
|
||||
|
@ -10,12 +11,7 @@ export default function App() {
|
|||
|
||||
// Posts page component
|
||||
function PostsApp() {
|
||||
return (
|
||||
<div className="helix-page">
|
||||
<h1>Posts Management</h1>
|
||||
<p>Posts management interface will be implemented here.</p>
|
||||
</div>
|
||||
);
|
||||
return <Posts />;
|
||||
}
|
||||
|
||||
// Users page component
|
||||
|
|
216
src/pages/Posts/Implementation-Phases.md
Normal file
216
src/pages/Posts/Implementation-Phases.md
Normal file
|
@ -0,0 +1,216 @@
|
|||
## 🚀 **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
|
179
src/pages/Posts/Posts.css
Normal file
179
src/pages/Posts/Posts.css
Normal file
|
@ -0,0 +1,179 @@
|
|||
/* 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;
|
||||
}
|
||||
}
|
241
src/pages/Posts/Posts.jsx
Normal file
241
src/pages/Posts/Posts.jsx
Normal file
|
@ -0,0 +1,241 @@
|
|||
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>
|
||||
);
|
||||
}
|
95
src/pages/Posts/README.md
Normal file
95
src/pages/Posts/README.md
Normal file
|
@ -0,0 +1,95 @@
|
|||
# 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
|
133
src/pages/Posts/components/PostFilters.css
Normal file
133
src/pages/Posts/components/PostFilters.css
Normal file
|
@ -0,0 +1,133 @@
|
|||
/* 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;
|
||||
}
|
||||
}
|
194
src/pages/Posts/components/PostFilters.jsx
Normal file
194
src/pages/Posts/components/PostFilters.jsx
Normal file
|
@ -0,0 +1,194 @@
|
|||
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>
|
||||
);
|
||||
}
|
103
src/pages/Posts/components/PostRow.css
Normal file
103
src/pages/Posts/components/PostRow.css
Normal file
|
@ -0,0 +1,103 @@
|
|||
/* 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;
|
||||
}
|
||||
}
|
307
src/pages/Posts/components/PostRow.jsx
Normal file
307
src/pages/Posts/components/PostRow.jsx
Normal file
|
@ -0,0 +1,307 @@
|
|||
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>
|
||||
);
|
||||
}
|
224
src/pages/Posts/components/PostsList.css
Normal file
224
src/pages/Posts/components/PostsList.css
Normal file
|
@ -0,0 +1,224 @@
|
|||
/* 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;
|
||||
}
|
||||
}
|
114
src/pages/Posts/components/PostsList.jsx
Normal file
114
src/pages/Posts/components/PostsList.jsx
Normal file
|
@ -0,0 +1,114 @@
|
|||
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>
|
||||
);
|
||||
}
|
185
src/pages/Posts/utils/postsAPI.js
Normal file
185
src/pages/Posts/utils/postsAPI.js
Normal file
|
@ -0,0 +1,185 @@
|
|||
/**
|
||||
* 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,6 +29,16 @@ 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();
|
||||
|
||||
|
@ -45,6 +55,9 @@ export default function Settings() {
|
|||
'Failed to save settings. Please try again.',
|
||||
} );
|
||||
}
|
||||
|
||||
// Scroll to top to show the notification
|
||||
scrollToTop();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
|
@ -53,6 +66,9 @@ 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,76 +32,80 @@ const MediaAssetsSettings = ( { settings, updateSetting } ) => {
|
|||
min={ 0 }
|
||||
/>
|
||||
|
||||
<div className="helix-settings-subsection">
|
||||
{ /* Image Sizes Section - Header spans full width */ }
|
||||
<div className="helix-settings-subsection-header">
|
||||
<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,13 +289,45 @@
|
|||
|
||||
.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 {
|
||||
|
@ -520,6 +552,18 @@
|
|||
}
|
||||
|
||||
/* 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;
|
||||
|
@ -551,6 +595,16 @@
|
|||
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