Compare commits

...

9 commits

Author SHA1 Message Date
Abhijit Bhatnagar
76b5131180 removed all console statements 2025-08-18 01:22:31 +05:30
Abhijit Bhatnagar
310e99b204 updated with pagination 2025-08-18 01:11:45 +05:30
Abhijit Bhatnagar
24008c2a49 posts list and filter working 2025-08-18 01:04:04 +05:30
Abhijit Bhatnagar
1fa23c846e css cleanup 2025-08-17 20:46:48 +05:30
Abhijit Bhatnagar
b2e6a0a21f lint fixes 2025-08-17 02:25:54 +05:30
Abhijit Bhatnagar
2e385d1f9d code refactor 2025-08-17 02:11:49 +05:30
Abhijit Bhatnagar
9dbbd4a93c fixed timezone saving 2025-08-17 02:00:42 +05:30
Abhijit Bhatnagar
9e560f9846 fixing language setting save 2025-08-17 01:32:15 +05:30
Abhijit Bhatnagar
c5a983d24b scroll to top on save or reset 2025-08-16 02:31:33 +05:30
21 changed files with 2544 additions and 275 deletions

View file

@ -21,6 +21,10 @@ add_action( 'rest_api_init', 'helix_register_rest_routes' );
* @since 1.0.0
*/
function helix_register_rest_routes() {
// Get the schemas.
$get_schema = helix_get_settings_schema();
$update_schema = helix_update_settings_schema();
// Settings endpoints.
register_rest_route(
'helix/v1',
@ -30,13 +34,13 @@ function helix_register_rest_routes() {
'methods' => WP_REST_Server::READABLE,
'callback' => 'helix_get_settings',
'permission_callback' => 'helix_settings_permissions_check',
'args' => helix_get_settings_schema(),
'args' => $get_schema,
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => 'helix_update_settings',
'permission_callback' => 'helix_settings_permissions_check',
'args' => helix_update_settings_schema(),
'args' => $update_schema,
),
)
);
@ -113,30 +117,25 @@ function helix_get_settings() {
* @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
*/
function helix_update_settings( $request ) {
$params = $request->get_json_params();
if ( empty( $params ) ) {
return new WP_Error(
'helix_no_settings_data',
__( 'No settings data provided.', 'helix' ),
array( 'status' => 400 )
);
}
$params = $request->get_params();
$allowed_settings = helix_get_allowed_settings();
$updated_settings = array();
$errors = array();
$allowed_settings = helix_get_allowed_settings();
// Process each setting.
foreach ( $params as $setting => $value ) {
// Check if setting is allowed.
if ( ! in_array( $setting, $allowed_settings, true ) ) {
$errors[ $setting ] = sprintf(
$error_msg = sprintf(
/* translators: %s: Setting name */
__( 'Setting "%s" is not allowed to be updated.', 'helix' ),
__( 'Setting "%s" is not allowed.', 'helix' ),
$setting
);
$errors[ $setting ] = $error_msg;
continue;
}
// Validate and sanitize the value.
$sanitized_value = helix_sanitize_setting_value( $setting, $value );
if ( is_wp_error( $sanitized_value ) ) {
@ -144,13 +143,31 @@ function helix_update_settings( $request ) {
continue;
}
// Get the WordPress option name for this setting.
$option_name = helix_get_wp_option_name( $setting );
// Handle special settings that need custom update logic.
$result = helix_update_setting( $setting, $sanitized_value );
// If special handling didn't apply, update normally.
if ( ! $result ) {
$result = update_option( $option_name, $sanitized_value );
}
if ( $result ) {
$updated_settings[ $setting ] = $sanitized_value;
} else {
$errors[ $setting ] = __( 'Failed to update setting.', 'helix' );
// Provide specific error messages for special settings.
if ( 'language' === $setting ) {
$error_msg = sprintf(
/* translators: %s: Language name */
__( 'Language "%s" could not be installed automatically. Please install the language pack manually via WordPress Admin → Settings → General → Site Language.', 'helix' ),
$sanitized_value
);
} else {
$error_msg = __( 'Failed to update setting.', 'helix' );
}
$errors[ $setting ] = $error_msg;
}
}

View file

@ -35,21 +35,28 @@ function helix_get_settings_schema() {
*/
function helix_update_settings_schema() {
$settings_config = helix_get_settings_config();
$schema = array();
foreach ( $settings_config as $category => $settings ) {
foreach ( $settings as $setting_key => $setting_config ) {
$schema[ $setting_key ] = array(
'description' => $setting_config['description'],
'type' => $setting_config['type'],
$schema = array(
'type' => 'object',
'properties' => array(),
);
foreach ( $settings_config as $category => $category_settings ) {
foreach ( $category_settings as $setting_key => $setting_config ) {
$schema['properties'][ $setting_key ] = array(
'type' => get_rest_api_type( $setting_config['type'] ),
);
// Add enum validation if applicable.
if ( isset( $setting_config['enum'] ) ) {
$schema[ $setting_key ]['enum'] = $setting_config['enum'];
$schema['properties'][ $setting_key ]['enum'] = $setting_config['enum'];
}
if ( isset( $setting_config['default'] ) ) {
$schema[ $setting_key ]['default'] = $setting_config['default'];
// Add minimum/maximum validation for numbers.
if ( isset( $setting_config['min'] ) ) {
$schema['properties'][ $setting_key ]['minimum'] = $setting_config['min'];
}
if ( isset( $setting_config['max'] ) ) {
$schema['properties'][ $setting_key ]['maximum'] = $setting_config['max'];
}
}
}
@ -57,6 +64,30 @@ function helix_update_settings_schema() {
return $schema;
}
/**
* Convert Helix setting types to WordPress REST API types.
*
* @since 1.0.0
* @param string $helix_type The Helix setting type.
* @return string|array The WordPress REST API type.
*/
function get_rest_api_type( $helix_type ) {
switch ( $helix_type ) {
case 'string':
case 'email':
case 'url':
return 'string';
case 'integer':
return 'integer';
case 'number':
return 'number';
case 'boolean':
return 'boolean';
default:
return 'string';
}
}
/**
* Get all WordPress settings in organized format.
*
@ -110,12 +141,7 @@ function helix_get_allowed_settings() {
$allowed_settings = array_merge( $allowed_settings, array_keys( $settings ) );
}
/**
* Filter the list of allowed settings.
*
* @since 1.0.0
* @param array $allowed_settings Array of allowed setting keys.
*/
// Filter the list of allowed settings.
return apply_filters( 'helix_allowed_settings', $allowed_settings );
}
@ -226,7 +252,8 @@ function helix_sanitize_setting_value( $setting, $value ) {
// Default sanitization based on type.
switch ( $setting_config['type'] ) {
case 'string':
return sanitize_text_field( $value );
$sanitized = sanitize_text_field( $value );
break;
case 'email':
$sanitized = sanitize_email( $value );
@ -237,46 +264,161 @@ function helix_sanitize_setting_value( $setting, $value ) {
array( 'status' => 400 )
);
}
return $sanitized;
break;
case 'url':
return esc_url_raw( $value );
$sanitized = esc_url_raw( $value );
break;
case 'integer':
return absint( $value );
$sanitized = absint( $value );
break;
case 'number':
return floatval( $value );
$sanitized = floatval( $value );
break;
case 'boolean':
return rest_sanitize_boolean( $value );
$sanitized = rest_sanitize_boolean( $value );
break;
default:
// For enum types, validate against allowed values.
$sanitized = sanitize_text_field( $value );
break;
}
// For any type with enum values, validate against allowed values.
if ( isset( $setting_config['enum'] ) ) {
if ( ! in_array( $value, $setting_config['enum'], true ) ) {
// Extract values from enum options if they are objects with 'value' property.
$enum_values = array();
foreach ( $setting_config['enum'] as $option ) {
if ( is_array( $option ) && isset( $option['value'] ) ) {
$enum_values[] = $option['value'];
} else {
$enum_values[] = $option;
}
}
if ( ! in_array( $sanitized, $enum_values, true ) ) {
return new WP_Error(
'helix_invalid_enum_value',
sprintf(
/* translators: 1: Setting name, 2: Allowed values */
__( 'Invalid value for %1$s. Allowed values: %2$s', 'helix' ),
$setting,
implode( ', ', $setting_config['enum'] )
implode( ', ', $enum_values )
),
array( 'status' => 400 )
);
}
}
return sanitize_text_field( $value );
}
return $sanitized;
}
/**
* Get comprehensive settings configuration.
* Update special settings that require custom logic.
*
* @since 1.0.0
* @return array Settings configuration array.
* @param string $setting The setting key.
* @param mixed $value The sanitized value.
* @return bool|null True if successful, false if failed, null if not a special setting.
*/
function helix_update_setting( $setting, $value ) {
// Handle timezone setting.
if ( 'timezone' === $setting ) {
return helix_update_timezone_setting( $value );
} elseif ( 'language' === $setting ) {
return helix_update_language_setting( $value );
}
// Not a special setting.
return false;
}
/**
* Update language setting with automatic language pack installation.
*
* @since 1.0.0
* @param string $locale The language locale to set.
* @return bool True if successful, false otherwise.
*/
function helix_update_language_setting( $locale ) {
// Try to update the option first.
$result = update_option( 'WPLANG', $locale );
// If it failed, try to install the language pack.
if ( ! $result && function_exists( 'switch_to_locale' ) ) {
// Check if the language file exists.
$lang_dir = WP_CONTENT_DIR . '/languages/';
$lang_file = $lang_dir . $locale . '.po';
// If language file doesn't exist, try to install it.
if ( ! file_exists( $lang_file ) ) {
$install_result = helix_install_language_pack( $locale );
if ( $install_result ) {
// Try updating the option again.
$result = update_option( 'WPLANG', $locale );
}
}
}
return $result;
}
/**
* Update timezone setting by handling both city-based timezones and GMT offsets.
*
* @since 1.0.0
* @param string $timezone_value The timezone value to save.
* @return bool True if update succeeded, false otherwise.
*/
function helix_update_timezone_setting( $timezone_value ) {
// Check if it's a GMT offset (starts with UTC+ or UTC- or is numeric).
if ( preg_match( '/^UTC[+-](\d+(?:\.\d+)?)$/', $timezone_value, $matches ) ) {
// Extract the numeric offset.
$offset = $matches[1];
$is_negative = strpos( $timezone_value, 'UTC-' ) === 0;
// Convert to numeric value (negative if UTC-).
$numeric_offset = $is_negative ? -1 * floatval( $offset ) : floatval( $offset );
// Save to gmt_offset option.
$result = update_option( 'gmt_offset', $numeric_offset );
// Clear the timezone_string option since we're using GMT offset.
if ( $result ) {
update_option( 'timezone_string', '' );
}
return $result;
} elseif ( is_numeric( $timezone_value ) ) {
// Direct numeric offset (like "5.5").
$numeric_offset = floatval( $timezone_value );
// Save to gmt_offset option.
$result = update_option( 'gmt_offset', $numeric_offset );
// Clear the timezone_string option since we're using GMT offset.
if ( $result ) {
update_option( 'timezone_string', '' );
}
return $result;
} else {
// City-based timezone (like "Asia/Kolkata").
// Save to timezone_string option.
$result = update_option( 'timezone_string', $timezone_value );
// Clear the gmt_offset option since we're using city-based timezone.
if ( $result ) {
update_option( 'gmt_offset', '' );
}
return $result;
}
}
/**
* Get available WordPress languages.
*
@ -286,41 +428,131 @@ function helix_sanitize_setting_value( $setting, $value ) {
function helix_get_available_languages() {
$language_options = array();
// Add English (United States) as default
// Add English (United States) as default.
$language_options[] = array(
'value' => '',
'label' => 'English (United States)'
'label' => 'English (United States)',
);
// Try to get installed languages
if ( function_exists( 'get_available_languages' ) || ( file_exists( ABSPATH . 'wp-admin/includes/translation-install.php' ) && require_once ABSPATH . 'wp-admin/includes/translation-install.php' ) ) {
// First, try to get installed languages.
if ( function_exists( 'get_available_languages' ) ) {
$installed_languages = get_available_languages();
} else {
$installed_languages = array();
}
$languages = get_available_languages();
// Always try to include the required file first.
if ( ! function_exists( 'wp_get_available_translations' ) && file_exists( ABSPATH . 'wp-admin/includes/translation-install.php' ) ) {
require_once ABSPATH . 'wp-admin/includes/translation-install.php';
}
if ( ! empty( $languages ) && function_exists( 'wp_get_available_translations' ) ) {
// Get all available translations (including uninstalled ones).
if ( function_exists( 'wp_get_available_translations' ) ) {
$available_translations = wp_get_available_translations();
foreach ( $languages as $language ) {
$language_data = $available_translations[ $language ] ?? null;
if ( $language_data && isset( $language_data['native_name'] ) ) {
// Add all available languages.
foreach ( $available_translations as $locale => $translation_data ) {
$label = isset( $translation_data['native_name'] ) ? $translation_data['native_name'] : $locale;
// Mark installed languages differently.
$is_installed = in_array( $locale, $installed_languages, true );
$display_label = $is_installed ? $label : $label . ' (Not Installed)';
$language_options[] = array(
'value' => $language,
'label' => $language_data['native_name']
'value' => $locale,
'label' => $display_label,
'installed' => $is_installed,
);
}
} else {
// Fallback if translation data is not available
$language_options[] = array(
'value' => $language,
'label' => $language
);
// Fallback to common languages if wp_get_available_translations is still not available.
$language_options = helix_get_fallback_languages( $installed_languages );
}
return $language_options;
}
/**
* Install a language pack using WordPress core functions.
*
* @since 1.0.0
* @param string $locale The language locale to install.
* @return bool True if installation succeeded, false otherwise.
*/
function helix_install_language_pack( $locale ) {
// Make sure we have the required functions.
if ( ! function_exists( 'wp_download_language_pack' ) ) {
if ( file_exists( ABSPATH . 'wp-admin/includes/translation-install.php' ) ) {
require_once ABSPATH . 'wp-admin/includes/translation-install.php';
} else {
return false;
}
}
// If no languages found, add some common ones as fallback
if ( count( $language_options ) === 1 ) {
// 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',
@ -329,15 +561,28 @@ function helix_get_available_languages() {
'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' => $name
'label' => $display_label,
'installed' => $is_installed,
);
}
}
return $language_options;
}
@ -351,92 +596,36 @@ function helix_get_available_languages() {
function helix_get_available_timezones() {
$timezone_options = array();
// UTC and common UTC offsets
$timezone_options[] = array( 'value' => 'UTC', 'label' => 'UTC' );
// Use WordPress core function to get timezone choices.
if ( function_exists( 'wp_timezone_choice' ) ) {
// Get the HTML output from wp_timezone_choice.
$timezone_html = wp_timezone_choice( get_option( 'timezone_string', 'UTC' ) );
// Parse the HTML to extract option values and labels.
if ( preg_match_all( '/<option[^>]*value=["\']([^"\']*)["\'][^>]*>([^<]*)<\/option>/', $timezone_html, $matches, PREG_SET_ORDER ) ) {
foreach ( $matches as $match ) {
$value = $match[1];
$label = trim( $match[2] );
// Positive UTC offsets
for ( $i = 1; $i <= 12; $i++ ) {
$offset = sprintf( '+%d', $i );
$timezone_options[] = array( 'value' => "UTC{$offset}", 'label' => "UTC{$offset}" );
}
// Negative UTC offsets
for ( $i = 1; $i <= 12; $i++ ) {
$offset = sprintf( '-%d', $i );
$timezone_options[] = array( 'value' => "UTC{$offset}", 'label' => "UTC{$offset}" );
}
// Major city-based timezones organized by region
$timezone_regions = array(
'America' => array(
'America/New_York' => 'New York',
'America/Chicago' => 'Chicago',
'America/Denver' => 'Denver',
'America/Los_Angeles' => 'Los Angeles',
'America/Toronto' => 'Toronto',
'America/Vancouver' => 'Vancouver',
'America/Montreal' => 'Montreal',
'America/Mexico_City' => 'Mexico City',
'America/Sao_Paulo' => 'São Paulo',
'America/Buenos_Aires' => 'Buenos Aires',
),
'Europe' => array(
'Europe/London' => 'London',
'Europe/Paris' => 'Paris',
'Europe/Berlin' => 'Berlin',
'Europe/Rome' => 'Rome',
'Europe/Madrid' => 'Madrid',
'Europe/Amsterdam' => 'Amsterdam',
'Europe/Brussels' => 'Brussels',
'Europe/Vienna' => 'Vienna',
'Europe/Stockholm' => 'Stockholm',
'Europe/Moscow' => 'Moscow',
),
'Asia' => array(
'Asia/Tokyo' => 'Tokyo',
'Asia/Shanghai' => 'Shanghai',
'Asia/Hong_Kong' => 'Hong Kong',
'Asia/Singapore' => 'Singapore',
'Asia/Kolkata' => 'Kolkata',
'Asia/Dubai' => 'Dubai',
'Asia/Bangkok' => 'Bangkok',
'Asia/Seoul' => 'Seoul',
'Asia/Manila' => 'Manila',
),
'Australia' => array(
'Australia/Sydney' => 'Sydney',
'Australia/Melbourne' => 'Melbourne',
'Australia/Brisbane' => 'Brisbane',
'Australia/Perth' => 'Perth',
'Australia/Adelaide' => 'Adelaide',
),
'Africa' => array(
'Africa/Cairo' => 'Cairo',
'Africa/Johannesburg' => 'Johannesburg',
'Africa/Lagos' => 'Lagos',
),
'Pacific' => array(
'Pacific/Auckland' => 'Auckland',
'Pacific/Honolulu' => 'Honolulu',
),
);
$timezone_identifiers = timezone_identifiers_list();
foreach ( $timezone_regions as $region => $timezones ) {
foreach ( $timezones as $timezone_id => $city_name ) {
if ( in_array( $timezone_id, $timezone_identifiers ) ) {
// Skip empty values.
if ( ! empty( $value ) || '0' === $value ) {
$timezone_options[] = array(
'value' => $timezone_id,
'label' => "{$city_name} ({$region})"
'value' => $value,
'label' => $label,
);
}
}
}
}
return $timezone_options;
}
/**
* Get comprehensive settings configuration.
*
* @since 1.0.0
* @return array Settings configuration array.
*/
function helix_get_settings_config() {
return array(
'site_information' => array(
@ -611,13 +800,13 @@ function helix_get_settings_config() {
),
'largeSizeW' => array(
'label' => __( 'Large Width', 'helix' ),
'description' => __( 'Maximum width of large-sized images.', 'helix' ),
'description' => __( 'Maximum width of large images.', 'helix' ),
'type' => 'integer',
'default' => 1024,
),
'largeSizeH' => array(
'label' => __( 'Large Height', 'helix' ),
'description' => __( 'Maximum height of large-sized images.', 'helix' ),
'description' => __( 'Maximum height of large images.', 'helix' ),
'type' => 'integer',
'default' => 1024,
),

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
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "eb1f0708b5830b82ea720f047c348230",
"content-hash": "684994342b1eecfad8ef44a86ff89124",
"packages": [],
"packages-dev": [
{
@ -187,16 +187,16 @@
},
{
"name": "phpcsstandards/phpcsutils",
"version": "1.1.0",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/PHPCSStandards/PHPCSUtils.git",
"reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad"
"reference": "f7eb16f2fa4237d5db9e8fed8050239bee17a9bd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/65355670ac17c34cd235cf9d3ceae1b9252c4dad",
"reference": "65355670ac17c34cd235cf9d3ceae1b9252c4dad",
"url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/f7eb16f2fa4237d5db9e8fed8050239bee17a9bd",
"reference": "f7eb16f2fa4237d5db9e8fed8050239bee17a9bd",
"shasum": ""
},
"require": {
@ -276,7 +276,7 @@
"type": "thanks_dev"
}
],
"time": "2025-06-12T04:32:33+00:00"
"time": "2025-08-10T01:04:45+00:00"
},
{
"name": "squizlabs/php_codesniffer",

View file

@ -49,6 +49,7 @@ add_action(
'helixData',
array(
'restUrl' => esc_url_raw( rest_url( 'helix/v1/' ) ),
'wpRestUrl' => esc_url_raw( rest_url( 'wp/v2/' ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
'user' => wp_get_current_user(),
'originalRoute' => $original_route,

View file

@ -2,6 +2,7 @@ import React from 'react';
import { createRoot } from 'react-dom/client';
import Dashboard from './pages/Dashboard/Dashboard';
import Settings from './pages/Settings/Settings';
import Posts from './pages/Posts/Posts';
// Main App component for the dashboard page
export default function App() {
@ -10,12 +11,7 @@ export default function App() {
// Posts page component
function PostsApp() {
return (
<div className="helix-page">
<h1>Posts Management</h1>
<p>Posts management interface will be implemented here.</p>
</div>
);
return <Posts />;
}
// Users page component

View file

@ -0,0 +1,216 @@
## 🚀 **Phase-Wise Implementation Plan**
### **Phase 1: Foundation & Core List View**
**Timeline: 1-2 weeks | Priority: Critical**
#### **1.1 Project Setup & Structure**
- [ ] Create Posts page directory structure
- [ ] Set up routing and navigation integration
- [ ] Create basic Posts page component with placeholder content
- [ ] Integrate with existing Helix navigation system
#### **1.2 Basic Posts List**
- [ ] Create `PostsList` component with table structure
- [ ] Implement `PostRow` component for individual posts
- [ ] Basic data fetching from WordPress REST API (`/wp-json/wp/v2/posts`)
- [ ] Simple pagination (next/previous)
- [ ] Basic loading states and error handling
#### **1.3 Essential CRUD Operations**
- [ ] View post details (read)
- [ ] Basic post creation form
- [ ] Simple post editing (title, content, status)
- [ ] Post deletion with confirmation
- [ ] Status changes (publish, draft, private)
#### **1.4 Basic Search & Filtering**
- [ ] Search by post title
- [ ] Filter by status (published, draft, private)
- [ ] Filter by author
- [ ] Basic date range filtering
**Deliverable**: Functional posts list with basic CRUD operations
---
### **Phase 2: Enhanced List Features & Quick Actions**
**Timeline: 1-2 weeks | Priority: High**
#### **2.1 Advanced Filtering & Search**
- [ ] Enhanced search (title + content + excerpt)
- [ ] Category and tag filtering
- [ ] Advanced date filtering (last week, last month, custom range)
- [ ] Filter combinations and saved filter presets
- [ ] Clear all filters functionality
#### **2.2 Bulk Operations**
- [ ] Multi-select functionality with checkboxes
- [ ] Bulk actions toolbar (publish, draft, delete, move to trash)
- [ ] Bulk category/tag assignment
- [ ] Bulk author reassignment
- [ ] Confirmation dialogs for destructive actions
#### **2.3 Quick Actions & Inline Editing**
- [ ] Quick edit modal for title, excerpt, categories
- [ ] Quick status change dropdown
- [ ] Quick delete with confirmation
- [ ] Quick preview functionality
- [ ] Keyboard shortcuts for common actions
#### **2.4 Enhanced Data Display**
- [ ] Customizable columns (show/hide)
- [ ] Sortable columns (title, date, author, status)
- [ ] Post thumbnails and featured images
- [ ] Comment count display
- [ ] Last modified date
**Deliverable**: Professional-grade posts list with bulk operations and quick actions
---
### **Phase 3: Full Post Editor & Content Management**
**Timeline: 2-3 weeks | Priority: High**
#### **3.1 Advanced Post Editor**
- [ ] Full-screen post editor modal/page
- [ ] Rich text editor integration (TinyMCE or modern alternative)
- [ ] Markdown support option
- [ ] Auto-save functionality
- [ ] Draft preview and comparison
#### **3.2 Media Management Integration**
- [ ] Media library integration
- [ ] Drag & drop image uploads
- [ ] Featured image management
- [ ] Image optimization and resizing
- [ ] Media gallery management
#### **3.3 Content Organization**
- [ ] Category and tag management
- [ ] Custom fields support
- [ ] Post templates and reusable content blocks
- [ ] Content scheduling with timezone support
- [ ] Post revisions and history
#### **3.4 SEO & Publishing Tools**
- [ ] SEO meta fields (title, description, keywords)
- [ ] Social media preview settings
- [ ] Publishing workflow (draft → review → publish)
- [ ] Content validation and quality checks
- [ ] Publishing permissions and approvals
**Deliverable**: Complete post creation and editing experience
---
### **Phase 4: Advanced Features & Workflow**
**Timeline: 2-3 weeks | Priority: Medium**
#### **4.1 Editorial Calendar**
- [ ] Calendar view for content planning
- [ ] Drag & drop post scheduling
- [ ] Content timeline visualization
- [ ] Deadline tracking and reminders
- [ ] Team availability integration
#### **4.2 Collaboration & Workflow**
- [ ] User assignment and notifications
- [ ] Review and approval system
- [ ] Editorial comments and feedback
- [ ] Content submission workflow
- [ ] Team collaboration tools
#### **4.3 Content Analytics**
- [ ] Basic performance metrics
- [ ] Content health scoring
- [ ] Readability analysis
- [ ] SEO scoring
- [ ] Engagement metrics integration
#### **4.4 Advanced Publishing Features**
- [ ] Multi-site publishing
- [ ] Social media auto-posting
- [ ] Email newsletter integration
- [ ] Content syndication
- [ ] A/B testing framework
**Deliverable**: Professional content management workflow system
---
### **Phase 5: Performance & Polish**
**Timeline: 1-2 weeks | Priority: Medium**
#### **5.1 Performance Optimization**
- [ ] Virtual scrolling for large post lists
- [ ] Advanced caching strategies
- [ ] Lazy loading for images and content
- [ ] Optimized API calls and data fetching
- [ ] Bundle size optimization
#### **5.2 User Experience Polish**
- [ ] Advanced keyboard shortcuts
- [ ] Drag & drop reordering
- [ ] Customizable dashboard layouts
- [ ] User preference settings
- [ ] Accessibility improvements (WCAG 2.1 AA)
#### **5.3 Advanced Customization**
- [ ] Custom post type support
- [ ] Extensible plugin architecture
- [ ] Theme customization options
- [ ] Advanced user role permissions
- [ ] API extensibility
**Deliverable**: Production-ready, optimized posts management system
---
## 🛠️ **Technical Implementation Details**
### **Phase 1 Dependencies**
- WordPress REST API endpoints
- Basic React state management
- Existing Helix component library
### **Phase 2 Dependencies**
- Enhanced WordPress REST API queries
- Advanced filtering logic
- Bulk operations API endpoints
### **Phase 3 Dependencies**
- Rich text editor library
- Media management API
- Advanced WordPress hooks and filters
### **Phase 4 Dependencies**
- Calendar component library
- Real-time updates (WebSocket/polling)
- Analytics and metrics APIs
### **Phase 5 Dependencies**
- Performance monitoring tools
- Accessibility testing tools
- Advanced WordPress development hooks
## <20><> **Success Criteria by Phase**
- **Phase 1**: Users can view, create, edit, and delete posts with basic filtering
- **Phase 2**: Users can efficiently manage multiple posts with bulk operations
- **Phase 3**: Users have a complete content creation and editing experience
- **Phase 4**: Teams can collaborate effectively with advanced workflow tools
- **Phase 5**: System is performant, accessible, and production-ready
## 🔄 **Iteration & Testing Strategy**
- **End of each phase**: User testing and feedback collection
- **Continuous**: Code review and quality assurance
- **Phase transitions**: Performance testing and optimization
- **Final phase**: Comprehensive testing across different WordPress setups
This phased approach ensures that:
1. **Each phase delivers immediate value** to users
2. **Development is manageable** and can be completed in realistic timeframes
3. **Testing and feedback** can be incorporated throughout the process
4. **Dependencies are clearly identified** and managed
5. **The system can be deployed** after each phase if needed

179
src/pages/Posts/Posts.css Normal file
View file

@ -0,0 +1,179 @@
/* Posts Page Styles */
.helix-page {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.helix-page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e1e5e9;
}
.helix-page-header h1 {
margin: 0;
font-size: 2rem;
font-weight: 600;
color: #1a1a1a;
}
.helix-page-actions {
display: flex;
gap: 12px;
}
/* Button Styles */
.helix-button {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 8px;
}
.helix-button--primary {
background-color: #007cba;
color: white;
}
.helix-button--primary:hover {
background-color: #005a87;
transform: translateY(-1px);
}
.helix-button--secondary {
background-color: #f0f0f1;
color: #1a1a1a;
border: 1px solid #c3c4c7;
}
.helix-button--secondary:hover {
background-color: #dcdcde;
border-color: #8c8f94;
}
.helix-button--icon {
padding: 8px;
min-width: 36px;
justify-content: center;
background-color: transparent;
color: #50575e;
}
.helix-button--small {
padding: 6px 12px;
font-size: 12px;
min-height: 28px;
}
.helix-button--icon:hover {
background-color: #f0f0f1;
color: #1a1a1a;
}
.helix-button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
/* Error State */
.helix-error {
text-align: center;
padding: 40px 20px;
background-color: #fef7f1;
border: 1px solid #f0b849;
border-radius: 8px;
margin: 20px 0;
}
.helix-error h2 {
color: #d63638;
margin-bottom: 16px;
}
.helix-error p {
color: #50575e;
margin-bottom: 20px;
}
/* Loading State */
.helix-loading {
text-align: center;
padding: 60px 20px;
}
.helix-loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f0f0f1;
border-top: 4px solid #007cba;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Empty State */
.helix-empty-state {
text-align: center;
padding: 60px 20px;
color: #50575e;
}
.helix-empty-state h3 {
margin-bottom: 12px;
font-size: 1.5rem;
color: #1a1a1a;
}
/* Responsive Design */
@media (max-width: 768px) {
.helix-page {
padding: 15px;
}
.helix-page-header {
flex-direction: column;
gap: 20px;
align-items: flex-start;
}
.helix-page-header h1 {
font-size: 1.75rem;
}
.helix-page-actions {
width: 100%;
justify-content: stretch;
}
.helix-button {
flex: 1;
justify-content: center;
}
}
@media (max-width: 480px) {
.helix-page {
padding: 10px;
}
.helix-page-header h1 {
font-size: 1.5rem;
}
}

241
src/pages/Posts/Posts.jsx Normal file
View file

@ -0,0 +1,241 @@
import React, { useState, useEffect, useCallback } from 'react';
import PostsList from './components/PostsList';
import PostFilters from './components/PostFilters';
import './Posts.css';
/**
* Main Posts Management Page Component
* Phase 1: Foundation & Core List View with Pagination
*/
export default function Posts() {
const [ posts, setPosts ] = useState( [] );
const [ loading, setLoading ] = useState( true );
const [ error, setError ] = useState( null );
const [ filters, setFilters ] = useState( {
search: '',
status: 'all',
author: 'all',
dateRange: 'all',
} );
const [ pagination, setPagination ] = useState( {
page: 1,
perPage: 10, // Reduced for proper pagination
total: 0,
totalPages: 0,
} );
/**
* Fetch posts from WordPress REST API
*/
const fetchPosts = useCallback( async () => {
setLoading( true );
setError( null );
try {
const queryParams = new URLSearchParams( {
page: pagination.page,
per_page: pagination.perPage,
} );
// Add all filter parameters to API call
if ( filters.search ) {
queryParams.set( 'search', filters.search );
}
if ( filters.author !== 'all' ) {
queryParams.set( 'author', filters.author );
}
if ( filters.status !== 'all' ) {
queryParams.set( 'status', filters.status );
}
// Note: date filtering will be done client-side
// Try to get the API URL from helixData, fallback to standard WordPress REST API
const apiUrl = `${
window.helixData?.wpRestUrl ||
window.location.origin + '/wp-json/wp/v2/'
}posts?${ queryParams }`;
// Try different authentication methods
const headers = {
'X-WP-Nonce': window.helixData?.nonce || '',
Authorization: `Bearer ${ window.helixData?.nonce || '' }`,
};
// Add nonce as query parameter as well
if ( window.helixData?.nonce ) {
queryParams.set( '_wpnonce', window.helixData.nonce );
}
const response = await fetch( apiUrl, { headers } );
if ( ! response.ok ) {
const errorText = await response.text();
throw new Error(
`HTTP error! status: ${ response.status }, response: ${ errorText }`
);
}
const postsData = await response.json();
// Store posts for current page
setPosts( postsData );
// Get pagination info from API response headers
const total = response.headers.get( 'X-WP-Total' );
const totalPages = response.headers.get( 'X-WP-TotalPages' );
setPagination( ( prev ) => ( {
...prev,
total: parseInt( total ) || postsData.length,
totalPages:
parseInt( totalPages ) ||
Math.ceil(
( parseInt( total ) || postsData.length ) / prev.perPage
),
} ) );
} catch ( err ) {
setError( err.message );
} finally {
setLoading( false );
}
}, [ pagination.page, pagination.perPage, filters ] );
useEffect( () => {
fetchPosts();
}, [ fetchPosts ] );
useEffect( () => {
fetchPosts();
}, [ filters.status, filters.author, filters.search, fetchPosts ] );
/**
* Handle filter changes
*/
const handleFilterChange = ( newFilters ) => {
setFilters( newFilters );
setPagination( ( prev ) => ( { ...prev, page: 1 } ) );
};
/**
* Handle pagination changes
*/
const handlePageChange = ( newPage ) => {
setPagination( ( prev ) => ( { ...prev, page: newPage } ) );
};
/**
* Handle post deletion
*/
const handlePostDelete = async ( postId ) => {
if (
! window.confirm( 'Are you sure you want to delete this post?' )
) {
return;
}
try {
const response = await fetch(
`${
window.helixData?.wpRestUrl ||
window.location.origin + '/wp-json/wp/v2/'
}posts/${ postId }`,
{
method: 'DELETE',
headers: {
'X-WP-Nonce': window.helixData?.nonce || '',
},
}
);
if ( response.ok ) {
// Remove post from local state
setPosts( ( prev ) =>
prev.filter( ( post ) => post.id !== postId )
);
// Refresh posts to update pagination
fetchPosts();
} else {
throw new Error( 'Failed to delete post' );
}
} catch ( err ) {
setError( `Error deleting post: ${ err.message }` );
}
};
/**
* Handle post status change
*/
const handleStatusChange = async ( postId, newStatus ) => {
try {
const response = await fetch(
`${
window.helixData?.wpRestUrl ||
window.location.origin + '/wp-json/wp/v2/'
}posts/${ postId }`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': window.helixData?.nonce || '',
},
body: JSON.stringify( { status: newStatus } ),
}
);
if ( response.ok ) {
// Update post in local state
setPosts( ( prev ) =>
prev.map( ( post ) =>
post.id === postId
? { ...post, status: newStatus }
: post
)
);
} else {
throw new Error( 'Failed to update post status' );
}
} catch ( err ) {
setError( `Error updating post status: ${ err.message }` );
}
};
if ( error ) {
return (
<div className="helix-page">
<div className="helix-error">
<h2>Error Loading Posts</h2>
<p>{ error }</p>
<button onClick={ fetchPosts } className="helix-button">
Try Again
</button>
</div>
</div>
);
}
return (
<div className="helix-page">
<div className="helix-page-header">
<h1>Posts Management</h1>
<div className="helix-page-actions">
<button className="helix-button helix-button--primary">
Add New Post
</button>
</div>
</div>
<PostFilters
filters={ filters }
onFilterChange={ handleFilterChange }
/>
<PostsList
posts={ posts }
loading={ loading }
pagination={ pagination }
onPageChange={ handlePageChange }
onDelete={ handlePostDelete }
onStatusChange={ handleStatusChange }
/>
</div>
);
}

95
src/pages/Posts/README.md Normal file
View file

@ -0,0 +1,95 @@
# Posts Management Page - Phase 1
## Overview
This is the Posts Management page implementation for Phase 1 of the Helix WordPress admin replacement. It provides a modern, React-based interface for managing WordPress posts.
## Features Implemented (Phase 1)
### ✅ Core List View
- **Posts Table**: Clean, responsive table displaying posts with essential information
- **Post Information**: Title, excerpt, author, categories, tags, status, and date
- **Responsive Design**: Mobile-first approach with responsive breakpoints
### ✅ Essential CRUD Operations
- **View Posts**: Display posts with pagination support
- **Delete Posts**: Remove posts with confirmation dialog
- **Status Changes**: Quick status updates (publish, draft, private, etc.)
- **Post Links**: Direct links to view posts and previews
### ✅ Basic Search & Filtering
- **Search**: Search posts by title and content
- **Status Filter**: Filter by post status (published, draft, private, etc.)
- **Author Filter**: Filter by post author
- **Date Filter**: Filter by date ranges (today, week, month, etc.)
### ✅ User Experience Features
- **Loading States**: Spinner and loading indicators
- **Error Handling**: Graceful error display with retry options
- **Empty States**: Helpful messages when no posts are found
- **Pagination**: Navigate through large numbers of posts
- **Action Dropdowns**: Contextual actions for each post
## Component Structure
```
src/pages/Posts/
├── Posts.jsx # Main posts page component
├── components/
│ ├── PostsList.jsx # Posts table and pagination
│ ├── PostRow.jsx # Individual post row with actions
│ └── PostFilters.jsx # Search and filter controls
├── utils/
│ └── postsAPI.js # WordPress REST API integration
├── Posts.css # Main page styles
└── components/
├── PostsList.css # Table and list styles
├── PostRow.css # Row and action styles
└── PostFilters.css # Filter form styles
```
## API Integration
The page integrates with WordPress REST API endpoints:
- `GET /wp-json/wp/v2/posts` - Fetch posts with filters and pagination
- `POST /wp-json/wp/v2/posts/{id}` - Update post status
- `DELETE /wp-json/wp/v2/posts/{id}` - Delete posts
- `GET /wp-json/wp/v2/users` - Fetch authors for filtering
- `GET /wp-json/wp/v2/categories` - Fetch categories for filtering
## Styling
- **Design System**: Consistent with Helix design patterns
- **Responsive**: Mobile-first responsive design
- **Accessibility**: Proper contrast, focus states, and semantic HTML
- **Modern UI**: Clean, professional appearance with subtle shadows and animations
## Browser Support
- Modern browsers with ES6+ support
- Responsive design for mobile and tablet devices
- Graceful degradation for older browsers
## Next Steps (Phase 2)
- [ ] Bulk operations (select multiple posts)
- [ ] Enhanced filtering (category, tag combinations)
- [ ] Quick edit functionality
- [ ] Advanced search options
- [ ] Post creation form
- [ ] Enhanced post editor
## Usage
1. Navigate to the Posts menu in Helix admin
2. Use search and filters to find specific posts
3. Click the action menu (⋮) on any post row for options
4. Use pagination to navigate through large numbers of posts
5. Click post titles to view posts in new tabs
## Technical Notes
- Built with React hooks for state management
- Uses WordPress REST API for data operations
- Implements proper error handling and loading states
- Follows Helix component patterns and styling conventions
- Includes comprehensive responsive design

View file

@ -0,0 +1,133 @@
/* Post Filters Styles */
.helix-post-filters {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 24px;
margin-bottom: 24px;
}
.helix-post-filters__row {
display: flex;
flex-direction: column;
gap: 20px;
margin-bottom: 20px;
}
.helix-post-filters__search {
width: 100%;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 8px;
}
.helix-post-filters__controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
align-items: center;
}
.helix-post-filters__actions {
display: flex;
gap: 12px;
align-items: center;
justify-content: flex-start;
}
/* Filter Group Styles */
.helix-filter-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.helix-filter-label {
font-size: 14px;
font-weight: 500;
color: #1a1a1a;
margin: 0;
}
/* Form Input Styles */
.helix-input,
.helix-select {
padding: 10px 12px;
border: 1px solid #c3c4c7;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
width: 100%;
}
.helix-input:focus,
.helix-select:focus {
outline: none;
border-color: #007cba;
box-shadow: 0 0 0 1px #007cba;
}
.helix-input::placeholder {
color: #8c8f94;
}
.helix-select {
background-color: white;
cursor: pointer;
min-width: 140px;
height: 42px; /* Match input height */
}
.helix-select option {
padding: 8px;
}
/* Responsive Design */
@media (max-width: 768px) {
.helix-post-filters {
padding: 20px;
}
.helix-post-filters__row {
gap: 16px;
}
.helix-post-filters__search {
max-width: 100%;
}
.helix-post-filters__controls {
justify-content: stretch;
}
.helix-post-filters__actions {
justify-content: stretch;
}
.helix-select {
flex: 1;
min-width: auto;
}
}
@media (max-width: 480px) {
.helix-post-filters {
padding: 16px;
}
.helix-post-filters__controls {
flex-direction: column;
gap: 12px;
}
.helix-post-filters__actions {
flex-direction: column;
gap: 8px;
}
.helix-button {
width: 100%;
justify-content: center;
}
}

View file

@ -0,0 +1,194 @@
import React, { useState, useEffect } from 'react';
import './PostFilters.css';
/**
* Post Filters Component - Search and filtering controls
*/
export default function PostFilters( { filters, onFilterChange } ) {
const [ authors, setAuthors ] = useState( [] );
// eslint-disable-next-line no-unused-vars
const [ categories, setCategories ] = useState( [] );
const [ localFilters, setLocalFilters ] = useState( filters );
useEffect( () => {
fetchAuthors();
fetchCategories();
}, [] );
useEffect( () => {
setLocalFilters( filters );
}, [ filters ] );
/**
* Fetch authors for filter dropdown
*/
const fetchAuthors = async () => {
try {
const response = await fetch(
`${
window.helixData?.wpRestUrl ||
window.location.origin + '/wp-json/wp/v2/'
}users?per_page=100`
);
if ( response.ok ) {
const authorsData = await response.json();
setAuthors( authorsData );
}
} catch ( error ) {}
};
/**
* Fetch categories for filter dropdown
*/
const fetchCategories = async () => {
try {
const response = await fetch(
`${
window.helixData?.wpRestUrl ||
window.location.origin + '/wp-json/wp/v2/'
}categories?per_page=100`
);
if ( response.ok ) {
const categoriesData = await response.json();
setCategories( categoriesData );
}
} catch ( error ) {}
};
/**
* Handle filter input changes
*/
const handleFilterChange = ( key, value ) => {
const newFilters = { ...localFilters, [ key ]: value };
setLocalFilters( newFilters );
};
/**
* Apply filters
*/
const handleApplyFilters = () => {
onFilterChange( localFilters );
};
/**
* Clear all filters
*/
const handleClearFilters = () => {
const clearedFilters = {
search: '',
status: 'all',
author: 'all',
dateRange: 'all',
};
setLocalFilters( clearedFilters );
onFilterChange( clearedFilters );
};
/**
* Check if any filters are active
*/
const hasActiveFilters = () => {
return (
localFilters.search ||
localFilters.status !== 'all' ||
localFilters.author !== 'all' ||
localFilters.dateRange !== 'all'
);
};
return (
<div className="helix-post-filters">
<div className="helix-post-filters__row">
<div className="helix-post-filters__search">
<label className="helix-filter-label">Search Posts</label>
<input
type="text"
placeholder="Search posts..."
value={ localFilters.search }
onChange={ ( e ) =>
handleFilterChange( 'search', e.target.value )
}
className="helix-input"
/>
</div>
<div className="helix-post-filters__controls">
<div className="helix-filter-group">
<label className="helix-filter-label">Status</label>
<select
value={ localFilters.status }
onChange={ ( e ) =>
handleFilterChange( 'status', e.target.value )
}
className="helix-select"
>
<option value="all">All Statuses</option>
<option value="publish">Published</option>
<option value="draft">Draft</option>
<option value="pending">Pending</option>
<option value="private">Private</option>
<option value="future">Scheduled</option>
</select>
</div>
<div className="helix-filter-group">
<label className="helix-filter-label">Author</label>
<select
value={ localFilters.author }
onChange={ ( e ) =>
handleFilterChange( 'author', e.target.value )
}
className="helix-select"
>
<option value="all">All Authors</option>
{ authors.map( ( author ) => (
<option key={ author.id } value={ author.id }>
{ author.name }
</option>
) ) }
</select>
</div>
<div className="helix-filter-group">
<label className="helix-filter-label">Date Range</label>
<select
value={ localFilters.dateRange }
onChange={ ( e ) =>
handleFilterChange(
'dateRange',
e.target.value
)
}
className="helix-select"
>
<option value="all">All Dates</option>
<option value="today">Today</option>
<option value="yesterday">Yesterday</option>
<option value="week">This Week</option>
<option value="month">This Month</option>
<option value="quarter">This Quarter</option>
<option value="year">This Year</option>
</select>
</div>
</div>
</div>
<div className="helix-post-filters__actions">
<button
className="helix-button helix-button--primary"
onClick={ handleApplyFilters }
>
Apply Filters
</button>
{ hasActiveFilters() && (
<button
className="helix-button helix-button--secondary"
onClick={ handleClearFilters }
>
Clear Filters
</button>
) }
</div>
</div>
);
}

View file

@ -0,0 +1,103 @@
/* Post Row Styles */
.helix-post-row {
transition: background-color 0.2s ease;
}
.helix-post-row:hover {
background-color: #f8f9fa;
}
/* Post Actions Styles */
.helix-post-actions {
position: relative;
}
/* Quick Actions Styles */
.helix-post-quick-actions {
display: flex;
gap: 8px;
margin-top: 6px;
flex-wrap: wrap;
}
.helix-button--small {
padding: 6px 12px;
font-size: 12px;
min-height: 28px;
}
.helix-post-actions__dropdown {
position: absolute;
top: 100%;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #e1e5e9;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 200px;
padding: 8px 0;
margin-top: 4px;
/* Ensure dropdown is above other elements */
z-index: 9999;
}
.helix-dropdown-item {
display: block;
width: 100%;
padding: 8px 16px;
border: none;
background: none;
text-align: left;
font-size: 14px;
color: #1a1a1a;
cursor: pointer;
transition: background-color 0.2s ease;
}
.helix-dropdown-item:hover {
background-color: #f0f0f1;
}
.helix-dropdown-item:disabled {
color: #8c8f94;
cursor: not-allowed;
}
.helix-dropdown-item:disabled:hover {
background-color: transparent;
}
.helix-dropdown-item--danger {
color: #d63638;
}
.helix-dropdown-item--danger:hover {
background-color: #fef7f1;
}
.helix-dropdown-divider {
margin: 8px 0;
border: none;
border-top: 1px solid #e1e5e9;
}
/* Responsive Design */
@media (max-width: 768px) {
.helix-post-actions__dropdown {
right: auto;
left: 0;
min-width: 160px;
}
}
@media (max-width: 480px) {
.helix-post-actions__dropdown {
min-width: 140px;
}
.helix-dropdown-item {
padding: 6px 12px;
font-size: 13px;
}
}

View file

@ -0,0 +1,307 @@
import React, { useState, useEffect, useRef } from 'react';
import './PostRow.css';
/**
* Individual Post Row Component
*/
export default function PostRow( { post, onDelete, onStatusChange } ) {
const [ showActions, setShowActions ] = useState( false );
const actionsRef = useRef( null );
// Close dropdown when clicking outside
useEffect( () => {
const handleClickOutside = ( event ) => {
if (
actionsRef.current &&
! actionsRef.current.contains( event.target )
) {
setShowActions( false );
}
};
document.addEventListener( 'mousedown', handleClickOutside );
return () => {
document.removeEventListener( 'mousedown', handleClickOutside );
};
}, [] );
/**
* Format date for display
*/
const formatDate = ( dateString ) => {
const date = new Date( dateString );
return date.toLocaleDateString( 'en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
} );
};
/**
* Get status badge styling
*/
const getStatusBadge = ( status ) => {
const statusClasses = {
publish: 'helix-status-badge--publish',
draft: 'helix-status-badge--draft',
private: 'helix-status-badge--private',
pending: 'helix-status-badge--pending',
future: 'helix-status-badge--future',
};
return (
<span
className={ `helix-status-badge ${
statusClasses[ status ] || ''
}` }
>
{ status.charAt( 0 ).toUpperCase() + status.slice( 1 ) }
</span>
);
};
/**
* Handle status change
*/
const handleStatusChange = ( newStatus ) => {
onStatusChange( post.id, newStatus );
setShowActions( false );
};
/**
* Handle post deletion
*/
const handleDelete = () => {
onDelete( post.id );
setShowActions( false );
};
/**
* Handle edit post (open in new tab)
*/
const handleEditPost = ( postData ) => {
// Open WordPress admin edit page in new tab
const editUrl = `${
window.helixData?.adminUrl || '/wp-admin/'
}post.php?post=${ postData.id }&action=edit`;
window.open( editUrl, '_blank' );
setShowActions( false );
};
/**
* Handle quick edit (placeholder for future implementation)
*/
const handleQuickEdit = () => {
// TODO: Implement quick edit modal
setShowActions( false );
};
/**
* Get excerpt from content
*/
const getExcerpt = ( content ) => {
// Remove HTML tags and get first 100 characters
const textContent = content.replace( /<[^>]*>/g, '' );
return textContent.length > 100
? textContent.substring( 0, 100 ) + '...'
: textContent;
};
return (
<tr className="helix-post-row">
<td className="helix-post-row__checkbox">
<input type="checkbox" />
</td>
<td className="helix-post-row__title">
<div className="helix-post-title">
<h4 className="helix-post-title__text">
<a
href={ post.link }
target="_blank"
rel="noopener noreferrer"
>
{ post.title.rendered }
</a>
</h4>
<p className="helix-post-title__excerpt">
{ getExcerpt( post.content.rendered ) }
</p>
<div className="helix-post-quick-actions">
<button
className="helix-button helix-button--small helix-button--secondary"
onClick={ () => handleEditPost( post ) }
title="Edit Post"
>
Edit
</button>
<button
className="helix-button helix-button--small helix-button--secondary"
onClick={ () => window.open( post.link, '_blank' ) }
title="View Post"
>
View
</button>
<button
className="helix-button helix-button--small helix-button--secondary"
onClick={ () =>
window.open(
`${ post.link }?preview=true`,
'_blank'
)
}
title="Preview Post"
>
Preview
</button>
{ post.status !== 'publish' && (
<button
className="helix-button helix-button--small helix-button--primary"
onClick={ () =>
handleStatusChange( 'publish' )
}
title="Publish Post"
>
Publish
</button>
) }
{ post.status === 'publish' && (
<button
className="helix-button helix-button--small helix-button--secondary"
onClick={ () => handleStatusChange( 'draft' ) }
title="Move to Draft"
>
Draft
</button>
) }
</div>
</div>
</td>
<td className="helix-post-row__author">
{ post._embedded?.author?.[ 0 ]?.name || 'Unknown' }
</td>
<td className="helix-post-row__categories">
{ post._embedded?.[ 'wp:term' ]?.[ 0 ]?.map( ( term ) => (
<span key={ term.id } className="helix-category-tag">
{ term.name }
</span>
) ) || 'Uncategorized' }
</td>
<td className="helix-post-row__tags">
{ post._embedded?.[ 'wp:term' ]?.[ 1 ]?.map( ( tag ) => (
<span key={ tag.id } className="helix-tag">
{ tag.name }
</span>
) ) || 'No tags' }
</td>
<td className="helix-post-row__status">
{ getStatusBadge( post.status ) }
</td>
<td className="helix-post-row__date">
{ formatDate( post.date ) }
</td>
<td className="helix-post-row__actions">
<div className="helix-post-actions" ref={ actionsRef }>
<button
className="helix-button helix-button--icon"
onClick={ () => setShowActions( ! showActions ) }
title="More actions"
>
</button>
{ showActions && (
<div className="helix-post-actions__dropdown">
<button
className="helix-dropdown-item"
onClick={ () => handleQuickEdit( post ) }
>
Quick Edit
</button>
<button
className="helix-dropdown-item"
onClick={ () =>
handleStatusChange( 'private' )
}
disabled={ post.status === 'private' }
>
Make Private
</button>
<button
className="helix-dropdown-item"
onClick={ () =>
handleStatusChange( 'pending' )
}
disabled={ post.status === 'pending' }
>
Mark Pending
</button>
<button
className="helix-dropdown-item"
onClick={ () => {
// eslint-disable-next-line no-undef
if ( navigator.clipboard ) {
// eslint-disable-next-line no-undef
navigator.clipboard.writeText(
post.link
);
} else {
// Fallback for older browsers
// eslint-disable-next-line no-undef
const textArea =
// eslint-disable-next-line no-undef
document.createElement(
'textarea'
);
textArea.value = post.link;
// eslint-disable-next-line no-undef
document.body.appendChild( textArea );
textArea.select();
// eslint-disable-next-line no-undef
document.execCommand( 'copy' );
// eslint-disable-next-line no-undef
document.body.removeChild( textArea );
}
setShowActions( false );
} }
>
Copy Link
</button>
<button
className="helix-dropdown-item"
onClick={ () =>
handleStatusChange( 'publish' )
}
disabled={ post.status === 'publish' }
>
Publish
</button>
<button
className="helix-dropdown-item"
onClick={ () => handleStatusChange( 'draft' ) }
disabled={ post.status === 'draft' }
>
Move to Draft
</button>
<button
className="helix-dropdown-item"
onClick={ () =>
handleStatusChange( 'private' )
}
disabled={ post.status === 'private' }
>
Make Private
</button>
<hr className="helix-dropdown-divider" />
<button
className="helix-dropdown-item helix-dropdown-item--danger"
onClick={ handleDelete }
>
Delete
</button>
</div>
) }
</div>
</td>
</tr>
);
}

View file

@ -0,0 +1,224 @@
/* Posts List Styles */
.helix-posts-list {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.helix-posts-table-container {
overflow-x: auto;
}
.helix-posts-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.helix-posts-table th {
background-color: #f8f9fa;
padding: 16px 12px;
text-align: left;
font-weight: 600;
color: #1a1a1a;
border-bottom: 2px solid #e1e5e9;
white-space: nowrap;
}
.helix-posts-table td {
padding: 16px 12px;
border-bottom: 1px solid #f0f0f1;
vertical-align: top;
}
.helix-posts-table tbody tr:hover {
background-color: #f8f9fa;
}
/* Table Column Specific Styles */
.helix-posts-table__checkbox {
width: 40px;
text-align: center;
}
.helix-posts-table__checkbox input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.helix-posts-table__title {
min-width: 300px;
}
.helix-posts-table__author {
min-width: 120px;
}
.helix-posts-table__categories {
min-width: 150px;
}
.helix-posts-table__tags {
min-width: 150px;
}
.helix-posts-table__status {
min-width: 100px;
}
.helix-posts-table__date {
min-width: 100px;
}
.helix-posts-table__actions {
width: 80px;
text-align: center;
}
/* Post Title Styles */
.helix-post-title__text {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
}
.helix-post-title__text a {
color: #007cba;
text-decoration: none;
}
.helix-post-title__text a:hover {
color: #005a87;
text-decoration: underline;
}
.helix-post-title__excerpt {
margin: 0;
font-size: 13px;
color: #646970;
line-height: 1.4;
}
/* Status Badge Styles */
.helix-status-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
text-transform: capitalize;
}
.helix-status-badge--publish {
background-color: #d1e7dd;
color: #0f5132;
}
.helix-status-badge--draft {
background-color: #fff3cd;
color: #856404;
}
.helix-status-badge--private {
background-color: #f8d7da;
color: #721c24;
}
.helix-status-badge--pending {
background-color: #cce5ff;
color: #004085;
}
.helix-status-badge--future {
background-color: #e2e3e5;
color: #383d41;
}
/* Category and Tag Styles */
.helix-category-tag,
.helix-tag {
display: inline-block;
padding: 2px 8px;
margin: 2px 4px 2px 0;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
background-color: #f0f0f1;
color: #50575e;
}
.helix-category-tag {
background-color: #e7f3ff;
color: #007cba;
}
/* Pagination Styles */
.helix-pagination {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background-color: #f8f9fa;
border-top: 1px solid #e1e5e9;
}
.helix-pagination__info {
color: #646970;
font-size: 14px;
}
.helix-pagination__controls {
display: flex;
align-items: center;
gap: 16px;
}
.helix-pagination__current {
font-weight: 500;
color: #1a1a1a;
}
/* Responsive Design */
@media (max-width: 1024px) {
.helix-posts-table__categories,
.helix-posts-table__tags {
display: none;
}
}
@media (max-width: 768px) {
.helix-posts-table__author {
display: none;
}
.helix-posts-table__title {
min-width: 200px;
}
.helix-pagination {
flex-direction: column;
gap: 16px;
text-align: center;
}
}
@media (max-width: 480px) {
.helix-posts-table th,
.helix-posts-table td {
padding: 12px 8px;
font-size: 13px;
}
.helix-posts-table__date {
display: none;
}
.helix-post-title__text {
font-size: 14px;
}
.helix-post-title__excerpt {
font-size: 12px;
}
}

View file

@ -0,0 +1,114 @@
import React from 'react';
import PostRow from './PostRow';
import './PostsList.css';
/**
* Posts List Component - Displays posts in a table format
*/
export default function PostsList( {
posts,
loading,
pagination,
onPageChange,
onDelete,
onStatusChange,
} ) {
if ( loading ) {
return (
<div className="helix-loading">
<div className="helix-loading-spinner"></div>
<p>Loading posts...</p>
</div>
);
}
if ( posts.length === 0 ) {
return (
<div className="helix-empty-state">
<h3>No posts found</h3>
<p>Try adjusting your filters or create a new post.</p>
</div>
);
}
return (
<div className="helix-posts-list">
<div className="helix-posts-table-container">
<table className="helix-posts-table">
<thead>
<tr>
<th className="helix-posts-table__checkbox">
<input type="checkbox" />
</th>
<th className="helix-posts-table__title">Title</th>
<th className="helix-posts-table__author">
Author
</th>
<th className="helix-posts-table__categories">
Categories
</th>
<th className="helix-posts-table__tags">Tags</th>
<th className="helix-posts-table__status">
Status
</th>
<th className="helix-posts-table__date">Date</th>
<th className="helix-posts-table__actions">
Actions
</th>
</tr>
</thead>
<tbody>
{ posts.map( ( post ) => (
<PostRow
key={ post.id }
post={ post }
onDelete={ onDelete }
onStatusChange={ onStatusChange }
/>
) ) }
</tbody>
</table>
</div>
{ pagination.totalPages > 1 && (
<div className="helix-pagination">
<div className="helix-pagination__info">
Showing{ ' ' }
{ ( pagination.page - 1 ) * pagination.perPage + 1 } to{ ' ' }
{ Math.min(
pagination.page * pagination.perPage,
pagination.total
) }{ ' ' }
of { pagination.total } posts
</div>
<div className="helix-pagination__controls">
<button
className="helix-button helix-button--secondary"
disabled={ pagination.page === 1 }
onClick={ () =>
onPageChange( pagination.page - 1 )
}
>
Previous
</button>
<span className="helix-pagination__current">
Page { pagination.page } of{ ' ' }
{ pagination.totalPages }
</span>
<button
className="helix-button helix-button--secondary"
disabled={
pagination.page === pagination.totalPages
}
onClick={ () =>
onPageChange( pagination.page + 1 )
}
>
Next
</button>
</div>
</div>
) }
</div>
);
}

View file

@ -0,0 +1,185 @@
/**
* Posts API Utility Functions
* Centralized API calls for posts management
*/
const API_BASE =
window.helixData?.wpRestUrl || window.location.origin + '/wp-json/wp/v2/';
/**
* Fetch posts with filters and pagination
*/
export const fetchPosts = async ( params = {} ) => {
const queryParams = new URLSearchParams( {
page: 1,
per_page: 20,
...params,
} );
// Remove 'all' values as they're not valid API parameters
[ 'status', 'author', 'dateRange' ].forEach( ( key ) => {
if ( params[ key ] === 'all' ) {
queryParams.delete( key );
}
} );
try {
const response = await fetch( `${ API_BASE }posts?${ queryParams }` );
if ( ! response.ok ) {
throw new Error( `HTTP error! status: ${ response.status }` );
}
const posts = await response.json();
const total = response.headers.get( 'X-WP-Total' );
const totalPages = response.headers.get( 'X-WP-TotalPages' );
return {
posts,
pagination: {
total: parseInt( total ) || 0,
totalPages: parseInt( totalPages ) || 0,
},
};
} catch ( error ) {
throw error;
}
};
/**
* Fetch a single post by ID
*/
export const fetchPost = async ( postId ) => {
try {
const response = await fetch( `${ API_BASE }posts/${ postId }` );
if ( ! response.ok ) {
throw new Error( `HTTP error! status: ${ response.status }` );
}
return await response.json();
} catch ( error ) {
throw error;
}
};
/**
* Create a new post
*/
export const createPost = async ( postData ) => {
try {
const response = await fetch( `${ API_BASE }posts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': window.helixData?.nonce || '',
},
body: JSON.stringify( postData ),
} );
if ( ! response.ok ) {
throw new Error( `HTTP error! status: ${ response.status }` );
}
return await response.json();
} catch ( error ) {
throw error;
}
};
/**
* Update an existing post
*/
export const updatePost = async ( postId, postData ) => {
try {
const response = await fetch( `${ API_BASE }posts/${ postId }`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': window.helixData?.nonce || '',
},
body: JSON.stringify( postData ),
} );
if ( ! response.ok ) {
throw new Error( `HTTP error! status: ${ response.status }` );
}
return await response.json();
} catch ( error ) {
throw error;
}
};
/**
* Delete a post
*/
export const deletePost = async ( postId ) => {
try {
const response = await fetch( `${ API_BASE }posts/${ postId }`, {
method: 'DELETE',
headers: {
'X-WP-Nonce': window.helixData?.nonce || '',
},
} );
if ( ! response.ok ) {
throw new Error( `HTTP error! status: ${ response.status }` );
}
return true;
} catch ( error ) {
throw error;
}
};
/**
* Fetch authors for filter dropdown
*/
export const fetchAuthors = async () => {
try {
const response = await fetch( `${ API_BASE }users?per_page=100` );
if ( ! response.ok ) {
throw new Error( `HTTP error! status: ${ response.status }` );
}
return await response.json();
} catch ( error ) {
throw error;
}
};
/**
* Fetch categories for filter dropdown
*/
export const fetchCategories = async () => {
try {
const response = await fetch( `${ API_BASE }categories?per_page=100` );
if ( ! response.ok ) {
throw new Error( `HTTP error! status: ${ response.status }` );
}
return await response.json();
} catch ( error ) {
throw error;
}
};
/**
* Fetch tags for filter dropdown
*/
export const fetchTags = async () => {
try {
const response = await fetch( `${ API_BASE }tags?per_page=100` );
if ( ! response.ok ) {
throw new Error( `HTTP error! status: ${ response.status }` );
}
return await response.json();
} catch ( error ) {
throw error;
}
};

View file

@ -29,6 +29,16 @@ export default function Settings() {
const [ activeTab, setActiveTab ] = useState( 'site' );
const [ notification, setNotification ] = useState( null );
/**
* Smoothly scroll to the top of the page
*/
const scrollToTop = () => {
window.scrollTo( {
top: 0,
behavior: 'smooth',
} );
};
const handleSave = async () => {
const result = await saveSettings();
@ -45,6 +55,9 @@ export default function Settings() {
'Failed to save settings. Please try again.',
} );
}
// Scroll to top to show the notification
scrollToTop();
};
const handleReset = () => {
@ -53,6 +66,9 @@ export default function Settings() {
type: 'info',
message: 'Changes have been reset to their original values.',
} );
// Scroll to top to show the notification
scrollToTop();
};
const handleTabChange = ( tabId ) => {

View file

@ -32,9 +32,12 @@ const MediaAssetsSettings = ( { settings, updateSetting } ) => {
min={ 0 }
/>
<div className="helix-settings-subsection">
{ /* Image Sizes Section - Header spans full width */ }
<div className="helix-settings-subsection-header">
<h4>Image Sizes</h4>
</div>
{ /* Thumbnail dimensions - side by side */ }
<NumberInput
label="Thumbnail Width"
description="Maximum width of thumbnail images in pixels."
@ -57,6 +60,7 @@ const MediaAssetsSettings = ( { settings, updateSetting } ) => {
max={ 2000 }
/>
{ /* Medium dimensions - side by side */ }
<NumberInput
label="Medium Width"
description="Maximum width of medium-sized images in pixels."
@ -79,6 +83,7 @@ const MediaAssetsSettings = ( { settings, updateSetting } ) => {
max={ 2000 }
/>
{ /* Large dimensions - side by side */ }
<NumberInput
label="Large Width"
description="Maximum width of large-sized images in pixels."
@ -100,7 +105,6 @@ const MediaAssetsSettings = ( { settings, updateSetting } ) => {
min={ 0 }
max={ 4000 }
/>
</div>
<ToggleInput
label="Organize my uploads into month- and year-based folders"

View file

@ -289,13 +289,45 @@
.helix-settings-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
align-items: start;
}
/* Ensure form fields take full width within their grid cell */
.helix-settings-grid .helix-form-field {
width: 100%;
}
/* Handle odd number of fields - make the last field span full width if it's alone */
.helix-settings-grid .helix-form-field:last-child:nth-child(odd) {
grid-column: 1 / -1;
}
/* Ensure subsections don't interfere with grid layout */
.helix-settings-subsection .helix-form-field {
margin-bottom: 16px;
}
/* Ensure consistent spacing between form fields */
.helix-settings-grid .helix-form-field + .helix-form-field {
margin-top: 0;
}
.helix-settings-subsection {
border-top: 1px solid var(--helix-color-3);
padding-top: 24px;
margin-top: 24px;
grid-column: 1 / -1;
}
.helix-settings-subsection-header {
grid-column: 1 / -1;
border-top: 1px solid var(--helix-color-3);
padding-top: 24px;
margin-top: 24px;
}
.helix-settings-subsection h4 {
@ -520,6 +552,18 @@
}
/* Responsive Design */
@media (max-width: 1024px) {
.helix-settings-grid {
grid-template-columns: 1fr;
gap: 20px;
}
.helix-settings-subsection-header {
margin-top: 20px;
padding-top: 20px;
}
}
@media (max-width: 768px) {
.helix-settings-page {
padding: 15px;
@ -551,6 +595,16 @@
padding: 20px;
}
.helix-settings-grid {
grid-template-columns: 1fr;
gap: 20px;
}
.helix-settings-subsection-header {
margin-top: 20px;
padding-top: 20px;
}
.helix-save-buttons {
flex-direction: column;
width: 100%;