posts list and filter working

This commit is contained in:
Abhijit Bhatnagar 2025-08-18 01:04:04 +05:30
parent 1fa23c846e
commit 24008c2a49
16 changed files with 2146 additions and 18 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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
@ -46,9 +42,18 @@ document.addEventListener( 'DOMContentLoaded', function () {
// Posts page
const postsRoot = document.getElementById( 'helix-posts-root' );
// eslint-disable-next-line no-console
console.log( 'Posts root element:', postsRoot );
if ( postsRoot ) {
// eslint-disable-next-line no-console
console.log( 'Creating Posts app root' );
const root = createRoot( postsRoot );
root.render( <PostsApp /> );
// eslint-disable-next-line no-console
console.log( 'Posts app rendered' );
} else {
// eslint-disable-next-line no-console
console.log( 'Posts root element not found' );
}
// Users page

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;
}
}

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

@ -0,0 +1,348 @@
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
*/
export default function Posts() {
// Debug: Log when component mounts
// eslint-disable-next-line no-console
console.log( 'Posts component mounted' );
const [ posts, setPosts ] = useState( [] );
const [ allPosts, setAllPosts ] = useState( [] ); // Store all posts for client-side filtering
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: 50, // Increased to get more posts for better filtering
total: 0,
totalPages: 0,
} );
/**
* Apply client-side filtering for status and date ranges
*/
const applyClientSideFilters = ( postsData, currentFilters ) => {
let filtered = postsData;
// Filter by status
if ( currentFilters.status !== 'all' ) {
filtered = filtered.filter(
( post ) => post.status === currentFilters.status
);
}
// Filter by date range
if ( currentFilters.dateRange !== 'all' ) {
const now = new Date();
const today = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate()
);
filtered = filtered.filter( ( post ) => {
const postDate = new Date( post.date );
switch ( currentFilters.dateRange ) {
case 'today':
return postDate >= today;
case 'yesterday':
const yesterday = new Date( today );
yesterday.setDate( yesterday.getDate() - 1 );
return postDate >= yesterday && postDate < today;
case 'week':
const weekAgo = new Date( today );
weekAgo.setDate( weekAgo.getDate() - 7 );
return postDate >= weekAgo;
case 'month':
const monthAgo = new Date( today );
monthAgo.setMonth( monthAgo.getMonth() - 1 );
return postDate >= monthAgo;
case 'quarter':
const quarterAgo = new Date( today );
quarterAgo.setMonth( quarterAgo.getMonth() - 3 );
return postDate >= quarterAgo;
case 'year':
const yearAgo = new Date( today );
yearAgo.setFullYear( yearAgo.getFullYear() - 1 );
return postDate >= yearAgo;
default:
return true;
}
} );
}
return filtered;
};
/**
* Fetch posts from WordPress REST API
*/
const fetchPosts = useCallback( async () => {
setLoading( true );
setError( null );
// Debug: Log function start
// eslint-disable-next-line no-console
console.log( 'fetchPosts function called' );
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 }`;
// Debug: Log the API URL and nonce
// eslint-disable-next-line no-console
console.log( 'API URL:', apiUrl );
// eslint-disable-next-line no-console
console.log( 'Nonce:', window.helixData?.nonce );
// 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();
// Debug: Log what posts we're getting
// eslint-disable-next-line no-console
console.log(
'Fetched posts:',
postsData.map( ( p ) => ( {
id: p.id,
title: p.title.rendered,
status: p.status,
} ) )
);
// Store all posts for client-side filtering
setAllPosts( postsData );
// Apply client-side filtering
const filteredPosts = applyClientSideFilters( postsData, filters );
setPosts( filteredPosts );
setPagination( ( prev ) => ( {
...prev,
total: filteredPosts.length,
totalPages: Math.ceil(
filteredPosts.length / pagination.perPage
),
} ) );
} catch ( err ) {
setError( err.message );
// eslint-disable-next-line no-console
console.error( 'Error fetching posts:', err );
} finally {
setLoading( false );
}
}, [ pagination.page, pagination.perPage, filters ] );
// useEffect must come after fetchPosts is defined
useEffect( () => {
// Debug: Log when useEffect runs
// eslint-disable-next-line no-console
console.log( 'useEffect triggered, calling fetchPosts' );
fetchPosts();
}, [ fetchPosts ] ); // fetchPosts is now stable with useCallback
// Trigger fetch when filters change (for server-side filtering)
useEffect( () => {
if ( allPosts.length > 0 ) {
// eslint-disable-next-line no-console
console.log( 'Filters changed, triggering new fetch' );
fetchPosts();
}
}, [ filters.status, filters.author, filters.search, fetchPosts ] );
/**
* Handle filter changes
*/
const handleFilterChange = ( newFilters ) => {
setFilters( newFilters );
setPagination( ( prev ) => ( { ...prev, page: 1 } ) ); // Reset to first page
// Apply client-side filtering to existing posts
if ( allPosts.length > 0 ) {
const filteredPosts = applyClientSideFilters(
allPosts,
newFilters
);
setPosts( filteredPosts );
setPagination( ( prev ) => ( {
...prev,
total: filteredPosts.length,
totalPages: Math.ceil( filteredPosts.length / prev.perPage ),
} ) );
}
};
/**
* 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,200 @@
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 ) {
// eslint-disable-next-line no-console
console.error( 'Error fetching authors:', 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 ) {
// eslint-disable-next-line no-console
console.error( 'Error fetching categories:', 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,309 @@
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 = ( postData ) => {
// TODO: Implement quick edit modal
// eslint-disable-next-line no-console
console.log( 'Quick edit for post:', postData.id );
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,201 @@
/**
* 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 ) {
// eslint-disable-next-line no-console
console.error( 'Error fetching posts:', 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 ) {
// eslint-disable-next-line no-console
console.error( 'Error fetching post:', 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 ) {
// eslint-disable-next-line no-console
console.error( 'Error creating post:', 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 ) {
// eslint-disable-next-line no-console
console.error( 'Error updating post:', 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 ) {
// eslint-disable-next-line no-console
console.error( 'Error deleting post:', 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 ) {
// eslint-disable-next-line no-console
console.error( 'Error fetching authors:', 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 ) {
// eslint-disable-next-line no-console
console.error( 'Error fetching categories:', 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 ) {
// eslint-disable-next-line no-console
console.error( 'Error fetching tags:', error );
throw error;
}
};