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
|
* @since 1.0.0
|
||||||
*/
|
*/
|
||||||
function helix_register_rest_routes() {
|
function helix_register_rest_routes() {
|
||||||
// Get the schemas.
|
|
||||||
$get_schema = helix_get_settings_schema();
|
|
||||||
$update_schema = helix_update_settings_schema();
|
|
||||||
|
|
||||||
// Settings endpoints.
|
// Settings endpoints.
|
||||||
register_rest_route(
|
register_rest_route(
|
||||||
'helix/v1',
|
'helix/v1',
|
||||||
|
@ -34,13 +30,13 @@ function helix_register_rest_routes() {
|
||||||
'methods' => WP_REST_Server::READABLE,
|
'methods' => WP_REST_Server::READABLE,
|
||||||
'callback' => 'helix_get_settings',
|
'callback' => 'helix_get_settings',
|
||||||
'permission_callback' => 'helix_settings_permissions_check',
|
'permission_callback' => 'helix_settings_permissions_check',
|
||||||
'args' => $get_schema,
|
'args' => helix_get_settings_schema(),
|
||||||
),
|
),
|
||||||
array(
|
array(
|
||||||
'methods' => WP_REST_Server::EDITABLE,
|
'methods' => WP_REST_Server::EDITABLE,
|
||||||
'callback' => 'helix_update_settings',
|
'callback' => 'helix_update_settings',
|
||||||
'permission_callback' => 'helix_settings_permissions_check',
|
'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.
|
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
|
||||||
*/
|
*/
|
||||||
function helix_update_settings( $request ) {
|
function helix_update_settings( $request ) {
|
||||||
$params = $request->get_params();
|
$params = $request->get_json_params();
|
||||||
$allowed_settings = helix_get_allowed_settings();
|
|
||||||
|
if ( empty( $params ) ) {
|
||||||
|
return new WP_Error(
|
||||||
|
'helix_no_settings_data',
|
||||||
|
__( 'No settings data provided.', 'helix' ),
|
||||||
|
array( 'status' => 400 )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$updated_settings = array();
|
$updated_settings = array();
|
||||||
$errors = array();
|
$errors = array();
|
||||||
|
$allowed_settings = helix_get_allowed_settings();
|
||||||
|
|
||||||
// Process each setting.
|
|
||||||
foreach ( $params as $setting => $value ) {
|
foreach ( $params as $setting => $value ) {
|
||||||
// Check if setting is allowed.
|
|
||||||
if ( ! in_array( $setting, $allowed_settings, true ) ) {
|
if ( ! in_array( $setting, $allowed_settings, true ) ) {
|
||||||
$error_msg = sprintf(
|
$errors[ $setting ] = sprintf(
|
||||||
/* translators: %s: Setting name */
|
/* translators: %s: Setting name */
|
||||||
__( 'Setting "%s" is not allowed.', 'helix' ),
|
__( 'Setting "%s" is not allowed to be updated.', 'helix' ),
|
||||||
$setting
|
$setting
|
||||||
);
|
);
|
||||||
$errors[ $setting ] = $error_msg;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate and sanitize the value.
|
|
||||||
$sanitized_value = helix_sanitize_setting_value( $setting, $value );
|
$sanitized_value = helix_sanitize_setting_value( $setting, $value );
|
||||||
|
|
||||||
if ( is_wp_error( $sanitized_value ) ) {
|
if ( is_wp_error( $sanitized_value ) ) {
|
||||||
|
@ -143,31 +144,13 @@ function helix_update_settings( $request ) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the WordPress option name for this setting.
|
|
||||||
$option_name = helix_get_wp_option_name( $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 ) {
|
if ( $result ) {
|
||||||
$updated_settings[ $setting ] = $sanitized_value;
|
$updated_settings[ $setting ] = $sanitized_value;
|
||||||
} else {
|
} else {
|
||||||
// Provide specific error messages for special settings.
|
$errors[ $setting ] = __( 'Failed to update setting.', 'helix' );
|
||||||
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,28 +35,21 @@ function helix_get_settings_schema() {
|
||||||
*/
|
*/
|
||||||
function helix_update_settings_schema() {
|
function helix_update_settings_schema() {
|
||||||
$settings_config = helix_get_settings_config();
|
$settings_config = helix_get_settings_config();
|
||||||
$schema = array(
|
$schema = array();
|
||||||
'type' => 'object',
|
|
||||||
'properties' => array(),
|
|
||||||
);
|
|
||||||
|
|
||||||
foreach ( $settings_config as $category => $category_settings ) {
|
foreach ( $settings_config as $category => $settings ) {
|
||||||
foreach ( $category_settings as $setting_key => $setting_config ) {
|
foreach ( $settings as $setting_key => $setting_config ) {
|
||||||
$schema['properties'][ $setting_key ] = array(
|
$schema[ $setting_key ] = array(
|
||||||
'type' => get_rest_api_type( $setting_config['type'] ),
|
'description' => $setting_config['description'],
|
||||||
|
'type' => $setting_config['type'],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add enum validation if applicable.
|
|
||||||
if ( isset( $setting_config['enum'] ) ) {
|
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['default'] ) ) {
|
||||||
if ( isset( $setting_config['min'] ) ) {
|
$schema[ $setting_key ]['default'] = $setting_config['default'];
|
||||||
$schema['properties'][ $setting_key ]['minimum'] = $setting_config['min'];
|
|
||||||
}
|
|
||||||
if ( isset( $setting_config['max'] ) ) {
|
|
||||||
$schema['properties'][ $setting_key ]['maximum'] = $setting_config['max'];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,30 +57,6 @@ function helix_update_settings_schema() {
|
||||||
return $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.
|
* 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 ) );
|
$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 );
|
return apply_filters( 'helix_allowed_settings', $allowed_settings );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,8 +226,7 @@ function helix_sanitize_setting_value( $setting, $value ) {
|
||||||
// Default sanitization based on type.
|
// Default sanitization based on type.
|
||||||
switch ( $setting_config['type'] ) {
|
switch ( $setting_config['type'] ) {
|
||||||
case 'string':
|
case 'string':
|
||||||
$sanitized = sanitize_text_field( $value );
|
return sanitize_text_field( $value );
|
||||||
break;
|
|
||||||
|
|
||||||
case 'email':
|
case 'email':
|
||||||
$sanitized = sanitize_email( $value );
|
$sanitized = sanitize_email( $value );
|
||||||
|
@ -264,161 +237,46 @@ function helix_sanitize_setting_value( $setting, $value ) {
|
||||||
array( 'status' => 400 )
|
array( 'status' => 400 )
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
return $sanitized;
|
||||||
|
|
||||||
case 'url':
|
case 'url':
|
||||||
$sanitized = esc_url_raw( $value );
|
return esc_url_raw( $value );
|
||||||
break;
|
|
||||||
|
|
||||||
case 'integer':
|
case 'integer':
|
||||||
$sanitized = absint( $value );
|
return absint( $value );
|
||||||
break;
|
|
||||||
|
|
||||||
case 'number':
|
case 'number':
|
||||||
$sanitized = floatval( $value );
|
return floatval( $value );
|
||||||
break;
|
|
||||||
|
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
$sanitized = rest_sanitize_boolean( $value );
|
return rest_sanitize_boolean( $value );
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
$sanitized = sanitize_text_field( $value );
|
// For enum types, validate against allowed values.
|
||||||
break;
|
if ( isset( $setting_config['enum'] ) ) {
|
||||||
}
|
if ( ! in_array( $value, $setting_config['enum'], true ) ) {
|
||||||
|
return new WP_Error(
|
||||||
// For any type with enum values, validate against allowed values.
|
'helix_invalid_enum_value',
|
||||||
if ( isset( $setting_config['enum'] ) ) {
|
sprintf(
|
||||||
// Extract values from enum options if they are objects with 'value' property.
|
/* translators: 1: Setting name, 2: Allowed values */
|
||||||
$enum_values = array();
|
__( 'Invalid value for %1$s. Allowed values: %2$s', 'helix' ),
|
||||||
foreach ( $setting_config['enum'] as $option ) {
|
$setting,
|
||||||
if ( is_array( $option ) && isset( $option['value'] ) ) {
|
implode( ', ', $setting_config['enum'] )
|
||||||
$enum_values[] = $option['value'];
|
),
|
||||||
} else {
|
array( 'status' => 400 )
|
||||||
$enum_values[] = $option;
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return sanitize_text_field( $value );
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update special settings that require custom logic.
|
* Get comprehensive settings configuration.
|
||||||
*
|
*
|
||||||
* @since 1.0.0
|
* @since 1.0.0
|
||||||
* @param string $setting The setting key.
|
* @return array Settings configuration array.
|
||||||
* @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.
|
* Get available WordPress languages.
|
||||||
*
|
*
|
||||||
|
@ -428,160 +286,57 @@ function helix_update_timezone_setting( $timezone_value ) {
|
||||||
function helix_get_available_languages() {
|
function helix_get_available_languages() {
|
||||||
$language_options = array();
|
$language_options = array();
|
||||||
|
|
||||||
// Add English (United States) as default.
|
// Add English (United States) as default
|
||||||
$language_options[] = array(
|
$language_options[] = array(
|
||||||
'value' => '',
|
'value' => '',
|
||||||
'label' => 'English (United States)',
|
'label' => 'English (United States)'
|
||||||
);
|
);
|
||||||
|
|
||||||
// First, try to get installed languages.
|
// Try to get installed languages
|
||||||
if ( function_exists( 'get_available_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' ) ) {
|
||||||
$installed_languages = get_available_languages();
|
|
||||||
} else {
|
$languages = get_available_languages();
|
||||||
$installed_languages = array();
|
|
||||||
|
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 no languages found, add some common ones as fallback
|
||||||
if ( ! function_exists( 'wp_get_available_translations' ) && file_exists( ABSPATH . 'wp-admin/includes/translation-install.php' ) ) {
|
if ( count( $language_options ) === 1 ) {
|
||||||
require_once ABSPATH . 'wp-admin/includes/translation-install.php';
|
$common_languages = array(
|
||||||
}
|
'es_ES' => 'Español',
|
||||||
|
'fr_FR' => 'Français',
|
||||||
// Get all available translations (including uninstalled ones).
|
'de_DE' => 'Deutsch',
|
||||||
if ( function_exists( 'wp_get_available_translations' ) ) {
|
'it_IT' => 'Italiano',
|
||||||
$available_translations = wp_get_available_translations();
|
'pt_BR' => 'Português do Brasil',
|
||||||
|
'ru_RU' => 'Русский',
|
||||||
// Add all available languages.
|
'ja' => '日本語',
|
||||||
foreach ( $available_translations as $locale => $translation_data ) {
|
'zh_CN' => '简体中文',
|
||||||
$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)';
|
|
||||||
|
|
||||||
|
foreach ( $common_languages as $code => $name ) {
|
||||||
$language_options[] = array(
|
$language_options[] = array(
|
||||||
'value' => $locale,
|
'value' => $code,
|
||||||
'label' => $display_label,
|
'label' => $name
|
||||||
'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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
return $language_options;
|
||||||
|
@ -596,23 +351,85 @@ function helix_get_fallback_languages( $installed_languages = array() ) {
|
||||||
function helix_get_available_timezones() {
|
function helix_get_available_timezones() {
|
||||||
$timezone_options = array();
|
$timezone_options = array();
|
||||||
|
|
||||||
// Use WordPress core function to get timezone choices.
|
// UTC and common UTC offsets
|
||||||
if ( function_exists( 'wp_timezone_choice' ) ) {
|
$timezone_options[] = array( 'value' => 'UTC', 'label' => 'UTC' );
|
||||||
// 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] );
|
|
||||||
|
|
||||||
// Skip empty values.
|
// Positive UTC offsets
|
||||||
if ( ! empty( $value ) || '0' === $value ) {
|
for ( $i = 1; $i <= 12; $i++ ) {
|
||||||
$timezone_options[] = array(
|
$offset = sprintf( '+%d', $i );
|
||||||
'value' => $value,
|
$timezone_options[] = array( 'value' => "UTC{$offset}", 'label' => "UTC{$offset}" );
|
||||||
'label' => $label,
|
}
|
||||||
);
|
|
||||||
}
|
// 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;
|
return $timezone_options;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get comprehensive settings configuration.
|
|
||||||
*
|
|
||||||
* @since 1.0.0
|
|
||||||
* @return array Settings configuration array.
|
|
||||||
*/
|
|
||||||
function helix_get_settings_config() {
|
function helix_get_settings_config() {
|
||||||
return array(
|
return array(
|
||||||
'site_information' => array(
|
'site_information' => array(
|
||||||
|
@ -800,13 +611,13 @@ function helix_get_settings_config() {
|
||||||
),
|
),
|
||||||
'largeSizeW' => array(
|
'largeSizeW' => array(
|
||||||
'label' => __( 'Large Width', 'helix' ),
|
'label' => __( 'Large Width', 'helix' ),
|
||||||
'description' => __( 'Maximum width of large images.', 'helix' ),
|
'description' => __( 'Maximum width of large-sized images.', 'helix' ),
|
||||||
'type' => 'integer',
|
'type' => 'integer',
|
||||||
'default' => 1024,
|
'default' => 1024,
|
||||||
),
|
),
|
||||||
'largeSizeH' => array(
|
'largeSizeH' => array(
|
||||||
'label' => __( 'Large Height', 'helix' ),
|
'label' => __( 'Large Height', 'helix' ),
|
||||||
'description' => __( 'Maximum height of large images.', 'helix' ),
|
'description' => __( 'Maximum height of large-sized images.', 'helix' ),
|
||||||
'type' => 'integer',
|
'type' => 'integer',
|
||||||
'default' => 1024,
|
'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",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "684994342b1eecfad8ef44a86ff89124",
|
"content-hash": "eb1f0708b5830b82ea720f047c348230",
|
||||||
"packages": [],
|
"packages": [],
|
||||||
"packages-dev": [
|
"packages-dev": [
|
||||||
{
|
{
|
||||||
|
@ -187,16 +187,16 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "phpcsstandards/phpcsutils",
|
"name": "phpcsstandards/phpcsutils",
|
||||||
"version": "1.1.1",
|
"version": "1.1.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/PHPCSStandards/PHPCSUtils.git",
|
"url": "https://github.com/PHPCSStandards/PHPCSUtils.git",
|
||||||
"reference": "f7eb16f2fa4237d5db9e8fed8050239bee17a9bd"
|
"reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/f7eb16f2fa4237d5db9e8fed8050239bee17a9bd",
|
"url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/65355670ac17c34cd235cf9d3ceae1b9252c4dad",
|
||||||
"reference": "f7eb16f2fa4237d5db9e8fed8050239bee17a9bd",
|
"reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
|
@ -276,7 +276,7 @@
|
||||||
"type": "thanks_dev"
|
"type": "thanks_dev"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-08-10T01:04:45+00:00"
|
"time": "2025-06-12T04:32:33+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "squizlabs/php_codesniffer",
|
"name": "squizlabs/php_codesniffer",
|
||||||
|
|
|
@ -49,7 +49,6 @@ add_action(
|
||||||
'helixData',
|
'helixData',
|
||||||
array(
|
array(
|
||||||
'restUrl' => esc_url_raw( rest_url( 'helix/v1/' ) ),
|
'restUrl' => esc_url_raw( rest_url( 'helix/v1/' ) ),
|
||||||
'wpRestUrl' => esc_url_raw( rest_url( 'wp/v2/' ) ),
|
|
||||||
'nonce' => wp_create_nonce( 'wp_rest' ),
|
'nonce' => wp_create_nonce( 'wp_rest' ),
|
||||||
'user' => wp_get_current_user(),
|
'user' => wp_get_current_user(),
|
||||||
'originalRoute' => $original_route,
|
'originalRoute' => $original_route,
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import Dashboard from './pages/Dashboard/Dashboard';
|
import Dashboard from './pages/Dashboard/Dashboard';
|
||||||
import Settings from './pages/Settings/Settings';
|
import Settings from './pages/Settings/Settings';
|
||||||
import Posts from './pages/Posts/Posts';
|
|
||||||
|
|
||||||
// Main App component for the dashboard page
|
// Main App component for the dashboard page
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
@ -11,7 +10,12 @@ export default function App() {
|
||||||
|
|
||||||
// Posts page component
|
// Posts page component
|
||||||
function PostsApp() {
|
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
|
// 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 [ activeTab, setActiveTab ] = useState( 'site' );
|
||||||
const [ notification, setNotification ] = useState( null );
|
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 handleSave = async () => {
|
||||||
const result = await saveSettings();
|
const result = await saveSettings();
|
||||||
|
|
||||||
|
@ -55,9 +45,6 @@ export default function Settings() {
|
||||||
'Failed to save settings. Please try again.',
|
'Failed to save settings. Please try again.',
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scroll to top to show the notification
|
|
||||||
scrollToTop();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
|
@ -66,9 +53,6 @@ export default function Settings() {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: 'Changes have been reset to their original values.',
|
message: 'Changes have been reset to their original values.',
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// Scroll to top to show the notification
|
|
||||||
scrollToTop();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTabChange = ( tabId ) => {
|
const handleTabChange = ( tabId ) => {
|
||||||
|
|
|
@ -32,80 +32,76 @@ const MediaAssetsSettings = ( { settings, updateSetting } ) => {
|
||||||
min={ 0 }
|
min={ 0 }
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ /* Image Sizes Section - Header spans full width */ }
|
<div className="helix-settings-subsection">
|
||||||
<div className="helix-settings-subsection-header">
|
|
||||||
<h4>Image Sizes</h4>
|
<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>
|
</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
|
<ToggleInput
|
||||||
label="Organize my uploads into month- and year-based folders"
|
label="Organize my uploads into month- and year-based folders"
|
||||||
description="Organize uploaded files into date-based folder structure."
|
description="Organize uploaded files into date-based folder structure."
|
||||||
|
|
|
@ -289,45 +289,13 @@
|
||||||
|
|
||||||
.helix-settings-grid {
|
.helix-settings-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 24px;
|
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 {
|
.helix-settings-subsection {
|
||||||
border-top: 1px solid var(--helix-color-3);
|
border-top: 1px solid var(--helix-color-3);
|
||||||
padding-top: 24px;
|
padding-top: 24px;
|
||||||
margin-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 {
|
.helix-settings-subsection h4 {
|
||||||
|
@ -552,18 +520,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive Design */
|
/* 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) {
|
@media (max-width: 768px) {
|
||||||
.helix-settings-page {
|
.helix-settings-page {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
@ -595,16 +551,6 @@
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.helix-settings-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.helix-settings-subsection-header {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.helix-save-buttons {
|
.helix-save-buttons {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue