mirror of
https://ghproxy.net/https://github.com/abhijitb/helix.git
synced 2025-08-27 20:13:02 +08:00
posts list and filter working
This commit is contained in:
parent
1fa23c846e
commit
24008c2a49
16 changed files with 2146 additions and 18 deletions
1
build/assets/App-BGAsQ9rL.css
Normal file
1
build/assets/App-BGAsQ9rL.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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,
|
||||
|
|
17
src/App.jsx
17
src/App.jsx
|
@ -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
|
||||
|
|
216
src/pages/Posts/Implementation-Phases.md
Normal file
216
src/pages/Posts/Implementation-Phases.md
Normal file
|
@ -0,0 +1,216 @@
|
|||
## 🚀 **Phase-Wise Implementation Plan**
|
||||
|
||||
### **Phase 1: Foundation & Core List View**
|
||||
**Timeline: 1-2 weeks | Priority: Critical**
|
||||
|
||||
#### **1.1 Project Setup & Structure**
|
||||
- [ ] Create Posts page directory structure
|
||||
- [ ] Set up routing and navigation integration
|
||||
- [ ] Create basic Posts page component with placeholder content
|
||||
- [ ] Integrate with existing Helix navigation system
|
||||
|
||||
#### **1.2 Basic Posts List**
|
||||
- [ ] Create `PostsList` component with table structure
|
||||
- [ ] Implement `PostRow` component for individual posts
|
||||
- [ ] Basic data fetching from WordPress REST API (`/wp-json/wp/v2/posts`)
|
||||
- [ ] Simple pagination (next/previous)
|
||||
- [ ] Basic loading states and error handling
|
||||
|
||||
#### **1.3 Essential CRUD Operations**
|
||||
- [ ] View post details (read)
|
||||
- [ ] Basic post creation form
|
||||
- [ ] Simple post editing (title, content, status)
|
||||
- [ ] Post deletion with confirmation
|
||||
- [ ] Status changes (publish, draft, private)
|
||||
|
||||
#### **1.4 Basic Search & Filtering**
|
||||
- [ ] Search by post title
|
||||
- [ ] Filter by status (published, draft, private)
|
||||
- [ ] Filter by author
|
||||
- [ ] Basic date range filtering
|
||||
|
||||
**Deliverable**: Functional posts list with basic CRUD operations
|
||||
|
||||
---
|
||||
|
||||
### **Phase 2: Enhanced List Features & Quick Actions**
|
||||
**Timeline: 1-2 weeks | Priority: High**
|
||||
|
||||
#### **2.1 Advanced Filtering & Search**
|
||||
- [ ] Enhanced search (title + content + excerpt)
|
||||
- [ ] Category and tag filtering
|
||||
- [ ] Advanced date filtering (last week, last month, custom range)
|
||||
- [ ] Filter combinations and saved filter presets
|
||||
- [ ] Clear all filters functionality
|
||||
|
||||
#### **2.2 Bulk Operations**
|
||||
- [ ] Multi-select functionality with checkboxes
|
||||
- [ ] Bulk actions toolbar (publish, draft, delete, move to trash)
|
||||
- [ ] Bulk category/tag assignment
|
||||
- [ ] Bulk author reassignment
|
||||
- [ ] Confirmation dialogs for destructive actions
|
||||
|
||||
#### **2.3 Quick Actions & Inline Editing**
|
||||
- [ ] Quick edit modal for title, excerpt, categories
|
||||
- [ ] Quick status change dropdown
|
||||
- [ ] Quick delete with confirmation
|
||||
- [ ] Quick preview functionality
|
||||
- [ ] Keyboard shortcuts for common actions
|
||||
|
||||
#### **2.4 Enhanced Data Display**
|
||||
- [ ] Customizable columns (show/hide)
|
||||
- [ ] Sortable columns (title, date, author, status)
|
||||
- [ ] Post thumbnails and featured images
|
||||
- [ ] Comment count display
|
||||
- [ ] Last modified date
|
||||
|
||||
**Deliverable**: Professional-grade posts list with bulk operations and quick actions
|
||||
|
||||
---
|
||||
|
||||
### **Phase 3: Full Post Editor & Content Management**
|
||||
**Timeline: 2-3 weeks | Priority: High**
|
||||
|
||||
#### **3.1 Advanced Post Editor**
|
||||
- [ ] Full-screen post editor modal/page
|
||||
- [ ] Rich text editor integration (TinyMCE or modern alternative)
|
||||
- [ ] Markdown support option
|
||||
- [ ] Auto-save functionality
|
||||
- [ ] Draft preview and comparison
|
||||
|
||||
#### **3.2 Media Management Integration**
|
||||
- [ ] Media library integration
|
||||
- [ ] Drag & drop image uploads
|
||||
- [ ] Featured image management
|
||||
- [ ] Image optimization and resizing
|
||||
- [ ] Media gallery management
|
||||
|
||||
#### **3.3 Content Organization**
|
||||
- [ ] Category and tag management
|
||||
- [ ] Custom fields support
|
||||
- [ ] Post templates and reusable content blocks
|
||||
- [ ] Content scheduling with timezone support
|
||||
- [ ] Post revisions and history
|
||||
|
||||
#### **3.4 SEO & Publishing Tools**
|
||||
- [ ] SEO meta fields (title, description, keywords)
|
||||
- [ ] Social media preview settings
|
||||
- [ ] Publishing workflow (draft → review → publish)
|
||||
- [ ] Content validation and quality checks
|
||||
- [ ] Publishing permissions and approvals
|
||||
|
||||
**Deliverable**: Complete post creation and editing experience
|
||||
|
||||
---
|
||||
|
||||
### **Phase 4: Advanced Features & Workflow**
|
||||
**Timeline: 2-3 weeks | Priority: Medium**
|
||||
|
||||
#### **4.1 Editorial Calendar**
|
||||
- [ ] Calendar view for content planning
|
||||
- [ ] Drag & drop post scheduling
|
||||
- [ ] Content timeline visualization
|
||||
- [ ] Deadline tracking and reminders
|
||||
- [ ] Team availability integration
|
||||
|
||||
#### **4.2 Collaboration & Workflow**
|
||||
- [ ] User assignment and notifications
|
||||
- [ ] Review and approval system
|
||||
- [ ] Editorial comments and feedback
|
||||
- [ ] Content submission workflow
|
||||
- [ ] Team collaboration tools
|
||||
|
||||
#### **4.3 Content Analytics**
|
||||
- [ ] Basic performance metrics
|
||||
- [ ] Content health scoring
|
||||
- [ ] Readability analysis
|
||||
- [ ] SEO scoring
|
||||
- [ ] Engagement metrics integration
|
||||
|
||||
#### **4.4 Advanced Publishing Features**
|
||||
- [ ] Multi-site publishing
|
||||
- [ ] Social media auto-posting
|
||||
- [ ] Email newsletter integration
|
||||
- [ ] Content syndication
|
||||
- [ ] A/B testing framework
|
||||
|
||||
**Deliverable**: Professional content management workflow system
|
||||
|
||||
---
|
||||
|
||||
### **Phase 5: Performance & Polish**
|
||||
**Timeline: 1-2 weeks | Priority: Medium**
|
||||
|
||||
#### **5.1 Performance Optimization**
|
||||
- [ ] Virtual scrolling for large post lists
|
||||
- [ ] Advanced caching strategies
|
||||
- [ ] Lazy loading for images and content
|
||||
- [ ] Optimized API calls and data fetching
|
||||
- [ ] Bundle size optimization
|
||||
|
||||
#### **5.2 User Experience Polish**
|
||||
- [ ] Advanced keyboard shortcuts
|
||||
- [ ] Drag & drop reordering
|
||||
- [ ] Customizable dashboard layouts
|
||||
- [ ] User preference settings
|
||||
- [ ] Accessibility improvements (WCAG 2.1 AA)
|
||||
|
||||
#### **5.3 Advanced Customization**
|
||||
- [ ] Custom post type support
|
||||
- [ ] Extensible plugin architecture
|
||||
- [ ] Theme customization options
|
||||
- [ ] Advanced user role permissions
|
||||
- [ ] API extensibility
|
||||
|
||||
**Deliverable**: Production-ready, optimized posts management system
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ **Technical Implementation Details**
|
||||
|
||||
### **Phase 1 Dependencies**
|
||||
- WordPress REST API endpoints
|
||||
- Basic React state management
|
||||
- Existing Helix component library
|
||||
|
||||
### **Phase 2 Dependencies**
|
||||
- Enhanced WordPress REST API queries
|
||||
- Advanced filtering logic
|
||||
- Bulk operations API endpoints
|
||||
|
||||
### **Phase 3 Dependencies**
|
||||
- Rich text editor library
|
||||
- Media management API
|
||||
- Advanced WordPress hooks and filters
|
||||
|
||||
### **Phase 4 Dependencies**
|
||||
- Calendar component library
|
||||
- Real-time updates (WebSocket/polling)
|
||||
- Analytics and metrics APIs
|
||||
|
||||
### **Phase 5 Dependencies**
|
||||
- Performance monitoring tools
|
||||
- Accessibility testing tools
|
||||
- Advanced WordPress development hooks
|
||||
|
||||
## <20><> **Success Criteria by Phase**
|
||||
|
||||
- **Phase 1**: Users can view, create, edit, and delete posts with basic filtering
|
||||
- **Phase 2**: Users can efficiently manage multiple posts with bulk operations
|
||||
- **Phase 3**: Users have a complete content creation and editing experience
|
||||
- **Phase 4**: Teams can collaborate effectively with advanced workflow tools
|
||||
- **Phase 5**: System is performant, accessible, and production-ready
|
||||
|
||||
## 🔄 **Iteration & Testing Strategy**
|
||||
|
||||
- **End of each phase**: User testing and feedback collection
|
||||
- **Continuous**: Code review and quality assurance
|
||||
- **Phase transitions**: Performance testing and optimization
|
||||
- **Final phase**: Comprehensive testing across different WordPress setups
|
||||
|
||||
This phased approach ensures that:
|
||||
1. **Each phase delivers immediate value** to users
|
||||
2. **Development is manageable** and can be completed in realistic timeframes
|
||||
3. **Testing and feedback** can be incorporated throughout the process
|
||||
4. **Dependencies are clearly identified** and managed
|
||||
5. **The system can be deployed** after each phase if needed
|
179
src/pages/Posts/Posts.css
Normal file
179
src/pages/Posts/Posts.css
Normal file
|
@ -0,0 +1,179 @@
|
|||
/* Posts Page Styles */
|
||||
.helix-page {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.helix-page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 2px solid #e1e5e9;
|
||||
}
|
||||
|
||||
.helix-page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.helix-page-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Button Styles */
|
||||
.helix-button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.helix-button--primary {
|
||||
background-color: #007cba;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.helix-button--primary:hover {
|
||||
background-color: #005a87;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.helix-button--secondary {
|
||||
background-color: #f0f0f1;
|
||||
color: #1a1a1a;
|
||||
border: 1px solid #c3c4c7;
|
||||
}
|
||||
|
||||
.helix-button--secondary:hover {
|
||||
background-color: #dcdcde;
|
||||
border-color: #8c8f94;
|
||||
}
|
||||
|
||||
.helix-button--icon {
|
||||
padding: 8px;
|
||||
min-width: 36px;
|
||||
justify-content: center;
|
||||
background-color: transparent;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.helix-button--small {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.helix-button--icon:hover {
|
||||
background-color: #f0f0f1;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.helix-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* Error State */
|
||||
.helix-error {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background-color: #fef7f1;
|
||||
border: 1px solid #f0b849;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.helix-error h2 {
|
||||
color: #d63638;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.helix-error p {
|
||||
color: #50575e;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.helix-loading {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
}
|
||||
|
||||
.helix-loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f0f0f1;
|
||||
border-top: 4px solid #007cba;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.helix-empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.helix-empty-state h3 {
|
||||
margin-bottom: 12px;
|
||||
font-size: 1.5rem;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.helix-page {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.helix-page-header {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.helix-page-header h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.helix-page-actions {
|
||||
width: 100%;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.helix-button {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.helix-page {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.helix-page-header h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
348
src/pages/Posts/Posts.jsx
Normal file
348
src/pages/Posts/Posts.jsx
Normal 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
95
src/pages/Posts/README.md
Normal file
|
@ -0,0 +1,95 @@
|
|||
# Posts Management Page - Phase 1
|
||||
|
||||
## Overview
|
||||
This is the Posts Management page implementation for Phase 1 of the Helix WordPress admin replacement. It provides a modern, React-based interface for managing WordPress posts.
|
||||
|
||||
## Features Implemented (Phase 1)
|
||||
|
||||
### ✅ Core List View
|
||||
- **Posts Table**: Clean, responsive table displaying posts with essential information
|
||||
- **Post Information**: Title, excerpt, author, categories, tags, status, and date
|
||||
- **Responsive Design**: Mobile-first approach with responsive breakpoints
|
||||
|
||||
### ✅ Essential CRUD Operations
|
||||
- **View Posts**: Display posts with pagination support
|
||||
- **Delete Posts**: Remove posts with confirmation dialog
|
||||
- **Status Changes**: Quick status updates (publish, draft, private, etc.)
|
||||
- **Post Links**: Direct links to view posts and previews
|
||||
|
||||
### ✅ Basic Search & Filtering
|
||||
- **Search**: Search posts by title and content
|
||||
- **Status Filter**: Filter by post status (published, draft, private, etc.)
|
||||
- **Author Filter**: Filter by post author
|
||||
- **Date Filter**: Filter by date ranges (today, week, month, etc.)
|
||||
|
||||
### ✅ User Experience Features
|
||||
- **Loading States**: Spinner and loading indicators
|
||||
- **Error Handling**: Graceful error display with retry options
|
||||
- **Empty States**: Helpful messages when no posts are found
|
||||
- **Pagination**: Navigate through large numbers of posts
|
||||
- **Action Dropdowns**: Contextual actions for each post
|
||||
|
||||
## Component Structure
|
||||
|
||||
```
|
||||
src/pages/Posts/
|
||||
├── Posts.jsx # Main posts page component
|
||||
├── components/
|
||||
│ ├── PostsList.jsx # Posts table and pagination
|
||||
│ ├── PostRow.jsx # Individual post row with actions
|
||||
│ └── PostFilters.jsx # Search and filter controls
|
||||
├── utils/
|
||||
│ └── postsAPI.js # WordPress REST API integration
|
||||
├── Posts.css # Main page styles
|
||||
└── components/
|
||||
├── PostsList.css # Table and list styles
|
||||
├── PostRow.css # Row and action styles
|
||||
└── PostFilters.css # Filter form styles
|
||||
```
|
||||
|
||||
## API Integration
|
||||
|
||||
The page integrates with WordPress REST API endpoints:
|
||||
- `GET /wp-json/wp/v2/posts` - Fetch posts with filters and pagination
|
||||
- `POST /wp-json/wp/v2/posts/{id}` - Update post status
|
||||
- `DELETE /wp-json/wp/v2/posts/{id}` - Delete posts
|
||||
- `GET /wp-json/wp/v2/users` - Fetch authors for filtering
|
||||
- `GET /wp-json/wp/v2/categories` - Fetch categories for filtering
|
||||
|
||||
## Styling
|
||||
|
||||
- **Design System**: Consistent with Helix design patterns
|
||||
- **Responsive**: Mobile-first responsive design
|
||||
- **Accessibility**: Proper contrast, focus states, and semantic HTML
|
||||
- **Modern UI**: Clean, professional appearance with subtle shadows and animations
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Modern browsers with ES6+ support
|
||||
- Responsive design for mobile and tablet devices
|
||||
- Graceful degradation for older browsers
|
||||
|
||||
## Next Steps (Phase 2)
|
||||
|
||||
- [ ] Bulk operations (select multiple posts)
|
||||
- [ ] Enhanced filtering (category, tag combinations)
|
||||
- [ ] Quick edit functionality
|
||||
- [ ] Advanced search options
|
||||
- [ ] Post creation form
|
||||
- [ ] Enhanced post editor
|
||||
|
||||
## Usage
|
||||
|
||||
1. Navigate to the Posts menu in Helix admin
|
||||
2. Use search and filters to find specific posts
|
||||
3. Click the action menu (⋮) on any post row for options
|
||||
4. Use pagination to navigate through large numbers of posts
|
||||
5. Click post titles to view posts in new tabs
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- Built with React hooks for state management
|
||||
- Uses WordPress REST API for data operations
|
||||
- Implements proper error handling and loading states
|
||||
- Follows Helix component patterns and styling conventions
|
||||
- Includes comprehensive responsive design
|
133
src/pages/Posts/components/PostFilters.css
Normal file
133
src/pages/Posts/components/PostFilters.css
Normal file
|
@ -0,0 +1,133 @@
|
|||
/* Post Filters Styles */
|
||||
.helix-post-filters {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.helix-post-filters__row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.helix-post-filters__search {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.helix-post-filters__controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.helix-post-filters__actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
/* Filter Group Styles */
|
||||
.helix-filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.helix-filter-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Form Input Styles */
|
||||
.helix-input,
|
||||
.helix-select {
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #c3c4c7;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.helix-input:focus,
|
||||
.helix-select:focus {
|
||||
outline: none;
|
||||
border-color: #007cba;
|
||||
box-shadow: 0 0 0 1px #007cba;
|
||||
}
|
||||
|
||||
.helix-input::placeholder {
|
||||
color: #8c8f94;
|
||||
}
|
||||
|
||||
.helix-select {
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
min-width: 140px;
|
||||
height: 42px; /* Match input height */
|
||||
}
|
||||
|
||||
.helix-select option {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.helix-post-filters {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.helix-post-filters__row {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.helix-post-filters__search {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.helix-post-filters__controls {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.helix-post-filters__actions {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.helix-select {
|
||||
flex: 1;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.helix-post-filters {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.helix-post-filters__controls {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.helix-post-filters__actions {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.helix-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
200
src/pages/Posts/components/PostFilters.jsx
Normal file
200
src/pages/Posts/components/PostFilters.jsx
Normal 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>
|
||||
);
|
||||
}
|
103
src/pages/Posts/components/PostRow.css
Normal file
103
src/pages/Posts/components/PostRow.css
Normal file
|
@ -0,0 +1,103 @@
|
|||
/* Post Row Styles */
|
||||
.helix-post-row {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.helix-post-row:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Post Actions Styles */
|
||||
.helix-post-actions {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Quick Actions Styles */
|
||||
.helix-post-quick-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.helix-button--small {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.helix-post-actions__dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 1px solid #e1e5e9;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
min-width: 200px;
|
||||
padding: 8px 0;
|
||||
margin-top: 4px;
|
||||
/* Ensure dropdown is above other elements */
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.helix-dropdown-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.helix-dropdown-item:hover {
|
||||
background-color: #f0f0f1;
|
||||
}
|
||||
|
||||
.helix-dropdown-item:disabled {
|
||||
color: #8c8f94;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.helix-dropdown-item:disabled:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.helix-dropdown-item--danger {
|
||||
color: #d63638;
|
||||
}
|
||||
|
||||
.helix-dropdown-item--danger:hover {
|
||||
background-color: #fef7f1;
|
||||
}
|
||||
|
||||
.helix-dropdown-divider {
|
||||
margin: 8px 0;
|
||||
border: none;
|
||||
border-top: 1px solid #e1e5e9;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.helix-post-actions__dropdown {
|
||||
right: auto;
|
||||
left: 0;
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.helix-post-actions__dropdown {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.helix-dropdown-item {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
309
src/pages/Posts/components/PostRow.jsx
Normal file
309
src/pages/Posts/components/PostRow.jsx
Normal 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>
|
||||
);
|
||||
}
|
224
src/pages/Posts/components/PostsList.css
Normal file
224
src/pages/Posts/components/PostsList.css
Normal file
|
@ -0,0 +1,224 @@
|
|||
/* Posts List Styles */
|
||||
.helix-posts-list {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.helix-posts-table-container {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.helix-posts-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.helix-posts-table th {
|
||||
background-color: #f8f9fa;
|
||||
padding: 16px 12px;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
border-bottom: 2px solid #e1e5e9;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.helix-posts-table td {
|
||||
padding: 16px 12px;
|
||||
border-bottom: 1px solid #f0f0f1;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.helix-posts-table tbody tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Table Column Specific Styles */
|
||||
.helix-posts-table__checkbox {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.helix-posts-table__checkbox input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.helix-posts-table__title {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.helix-posts-table__author {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.helix-posts-table__categories {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.helix-posts-table__tags {
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.helix-posts-table__status {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.helix-posts-table__date {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.helix-posts-table__actions {
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Post Title Styles */
|
||||
.helix-post-title__text {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.helix-post-title__text a {
|
||||
color: #007cba;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.helix-post-title__text a:hover {
|
||||
color: #005a87;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.helix-post-title__excerpt {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #646970;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Status Badge Styles */
|
||||
.helix-status-badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.helix-status-badge--publish {
|
||||
background-color: #d1e7dd;
|
||||
color: #0f5132;
|
||||
}
|
||||
|
||||
.helix-status-badge--draft {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.helix-status-badge--private {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.helix-status-badge--pending {
|
||||
background-color: #cce5ff;
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
.helix-status-badge--future {
|
||||
background-color: #e2e3e5;
|
||||
color: #383d41;
|
||||
}
|
||||
|
||||
/* Category and Tag Styles */
|
||||
.helix-category-tag,
|
||||
.helix-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
margin: 2px 4px 2px 0;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
background-color: #f0f0f1;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.helix-category-tag {
|
||||
background-color: #e7f3ff;
|
||||
color: #007cba;
|
||||
}
|
||||
|
||||
/* Pagination Styles */
|
||||
.helix-pagination {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid #e1e5e9;
|
||||
}
|
||||
|
||||
.helix-pagination__info {
|
||||
color: #646970;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.helix-pagination__controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.helix-pagination__current {
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 1024px) {
|
||||
.helix-posts-table__categories,
|
||||
.helix-posts-table__tags {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.helix-posts-table__author {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.helix-posts-table__title {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.helix-pagination {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.helix-posts-table th,
|
||||
.helix-posts-table td {
|
||||
padding: 12px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.helix-posts-table__date {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.helix-post-title__text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.helix-post-title__excerpt {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
114
src/pages/Posts/components/PostsList.jsx
Normal file
114
src/pages/Posts/components/PostsList.jsx
Normal file
|
@ -0,0 +1,114 @@
|
|||
import React from 'react';
|
||||
import PostRow from './PostRow';
|
||||
import './PostsList.css';
|
||||
|
||||
/**
|
||||
* Posts List Component - Displays posts in a table format
|
||||
*/
|
||||
export default function PostsList( {
|
||||
posts,
|
||||
loading,
|
||||
pagination,
|
||||
onPageChange,
|
||||
onDelete,
|
||||
onStatusChange,
|
||||
} ) {
|
||||
if ( loading ) {
|
||||
return (
|
||||
<div className="helix-loading">
|
||||
<div className="helix-loading-spinner"></div>
|
||||
<p>Loading posts...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( posts.length === 0 ) {
|
||||
return (
|
||||
<div className="helix-empty-state">
|
||||
<h3>No posts found</h3>
|
||||
<p>Try adjusting your filters or create a new post.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="helix-posts-list">
|
||||
<div className="helix-posts-table-container">
|
||||
<table className="helix-posts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="helix-posts-table__checkbox">
|
||||
<input type="checkbox" />
|
||||
</th>
|
||||
<th className="helix-posts-table__title">Title</th>
|
||||
<th className="helix-posts-table__author">
|
||||
Author
|
||||
</th>
|
||||
<th className="helix-posts-table__categories">
|
||||
Categories
|
||||
</th>
|
||||
<th className="helix-posts-table__tags">Tags</th>
|
||||
<th className="helix-posts-table__status">
|
||||
Status
|
||||
</th>
|
||||
<th className="helix-posts-table__date">Date</th>
|
||||
<th className="helix-posts-table__actions">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ posts.map( ( post ) => (
|
||||
<PostRow
|
||||
key={ post.id }
|
||||
post={ post }
|
||||
onDelete={ onDelete }
|
||||
onStatusChange={ onStatusChange }
|
||||
/>
|
||||
) ) }
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{ pagination.totalPages > 1 && (
|
||||
<div className="helix-pagination">
|
||||
<div className="helix-pagination__info">
|
||||
Showing{ ' ' }
|
||||
{ ( pagination.page - 1 ) * pagination.perPage + 1 } to{ ' ' }
|
||||
{ Math.min(
|
||||
pagination.page * pagination.perPage,
|
||||
pagination.total
|
||||
) }{ ' ' }
|
||||
of { pagination.total } posts
|
||||
</div>
|
||||
<div className="helix-pagination__controls">
|
||||
<button
|
||||
className="helix-button helix-button--secondary"
|
||||
disabled={ pagination.page === 1 }
|
||||
onClick={ () =>
|
||||
onPageChange( pagination.page - 1 )
|
||||
}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span className="helix-pagination__current">
|
||||
Page { pagination.page } of{ ' ' }
|
||||
{ pagination.totalPages }
|
||||
</span>
|
||||
<button
|
||||
className="helix-button helix-button--secondary"
|
||||
disabled={
|
||||
pagination.page === pagination.totalPages
|
||||
}
|
||||
onClick={ () =>
|
||||
onPageChange( pagination.page + 1 )
|
||||
}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) }
|
||||
</div>
|
||||
);
|
||||
}
|
201
src/pages/Posts/utils/postsAPI.js
Normal file
201
src/pages/Posts/utils/postsAPI.js
Normal 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;
|
||||
}
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue