Skip to content

Commit f97b6a6

Browse files
committed
feat(toolbar): add Next.js toolbar demo example
Add comprehensive Next.js App Router example demonstrating the toolbar with real WordPress integration. - Complete Next.js example with App Router - Hash-based port calculation for deterministic ports - Template-based configuration (mu-plugin, .htaccess, .wp-env.json) - One-command startup with concurrently - Real WordPress data integration (no mocks) - WPGraphQL for post fetching - Responsive toolbar with real user data
1 parent 03d58ba commit f97b6a6

File tree

15 files changed

+724
-0
lines changed

15 files changed

+724
-0
lines changed
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Toolbar Demo - React Hooks
2+
3+
React hooks example with Next.js App Router demonstrating the Headless WordPress Toolbar.
4+
5+
## Features
6+
7+
- React hooks (`useToolbar`, `useToolbarState`, `useToolbarNodes`)
8+
- Next.js App Router (App Directory)
9+
- TypeScript
10+
- Framework-agnostic state management
11+
- WordPress context integration
12+
13+
## Quick Start
14+
15+
```bash
16+
# Install dependencies (from monorepo root)
17+
pnpm install
18+
19+
# Start WordPress (from this directory)
20+
npx wp-env start
21+
22+
# Start Next.js dev server (from example-app directory)
23+
cd example-app
24+
pnpm dev
25+
```
26+
27+
Open:
28+
- Next.js App: [http://localhost:3001](http://localhost:3001)
29+
- WordPress Admin: [http://localhost:8001/wp-admin](http://localhost:8001/wp-admin)
30+
- Username: `admin`
31+
- Password: `password`
32+
33+
## Key Files
34+
35+
- `lib/toolbar.ts` - Singleton toolbar instance
36+
- `lib/wordpress.ts` - WordPress REST API integration
37+
- `app/components/Toolbar.tsx` - Toolbar component using React hooks
38+
- `app/page.tsx` - Demo page with WordPress integration
39+
40+
## Usage Pattern
41+
42+
```tsx
43+
import { toolbar } from '@/lib/toolbar';
44+
import { useToolbar } from '@wpengine/hwp-toolbar/react';
45+
46+
function MyComponent() {
47+
const { state, nodes } = useToolbar(toolbar);
48+
49+
return (
50+
<div>
51+
{nodes.map(node => (
52+
<button key={node.id} onClick={node.onClick}>
53+
{typeof node.label === 'function' ? node.label() : node.label}
54+
</button>
55+
))}
56+
</div>
57+
);
58+
}
59+
```
60+
61+
## State Management
62+
63+
The toolbar follows modern state management patterns (TanStack/Zustand):
64+
65+
1. **Framework-agnostic core** - `Toolbar` class manages state
66+
2. **React integration** - Hooks subscribe to state changes
67+
3. **Full UI control** - You render the toolbar however you want
68+
69+
## WordPress Integration
70+
71+
The demo integrates with a local WordPress instance via REST API.
72+
73+
### Demo Authentication
74+
75+
By default, the demo uses **mock authentication** to simplify the setup:
76+
77+
```ts
78+
// Demo user (no actual WordPress login required)
79+
const user = await getCurrentUser(); // Returns mock user
80+
81+
// Public posts endpoint (no authentication needed)
82+
const posts = await getPosts(); // Fetches from /wp/v2/posts
83+
```
84+
85+
This lets you test the toolbar immediately without WordPress login complexity.
86+
87+
### Production Authentication
88+
89+
For production use, implement proper authentication using **WordPress Application Passwords**:
90+
91+
1. **Generate Application Password**:
92+
- Go to WordPress Admin → Users → Profile
93+
- Scroll to "Application Passwords"
94+
- Create a new password
95+
96+
2. **Configure Environment**:
97+
```bash
98+
cp .env.local.example .env.local
99+
# Add your credentials to .env.local
100+
```
101+
102+
3. **Update `lib/wordpress.ts`**:
103+
```ts
104+
const auth = btoa(`${process.env.WP_USERNAME}:${process.env.WP_APP_PASSWORD}`);
105+
106+
export async function fetchFromWordPress(endpoint: string) {
107+
const response = await fetch(`${WP_API_URL}/wp-json${endpoint}`, {
108+
headers: {
109+
'Authorization': `Basic ${auth}`
110+
}
111+
});
112+
// ...
113+
}
114+
```
115+
116+
### Features Demonstrated
117+
118+
- **WordPress Connection**: Fetch data from WordPress REST API
119+
- **Post Management**: Load and display WordPress posts
120+
- **Dynamic Toolbar**: Nodes appear/disappear based on WordPress context
121+
- **Error Handling**: Clear error messages for connection issues
122+
123+
### Troubleshooting
124+
125+
**CORS Errors**
126+
- Make sure wp-env is running: `npx wp-env start`
127+
- Check WordPress is accessible at http://localhost:8001
128+
- MU plugin should enable CORS headers automatically
129+
130+
**Connection Failed**
131+
- Verify wp-env is running: `npx wp-env start`
132+
- Check the port matches `.wp-env.json` (default: 8001)
133+
- Try accessing http://localhost:8001 in your browser
134+
135+
**No Posts Available**
136+
- Create sample posts in WordPress Admin
137+
- Or run: `npx wp-env run cli wp post generate --count=5`
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# WordPress Configuration
2+
# Copy this file to .env.local and update with your values
3+
4+
# WordPress Site URL
5+
NEXT_PUBLIC_WP_URL=http://localhost:8891
6+
7+
# Production Authentication (optional)
8+
# For real authentication, generate Application Passwords in WordPress:
9+
# 1. Go to WordPress Admin → Users → Profile
10+
# 2. Scroll to "Application Passwords"
11+
# 3. Create a new application password
12+
# 4. Add credentials here:
13+
# WP_USERNAME=admin
14+
# WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx
15+
16+
# Note: This demo uses mock authentication by default.
17+
# To enable real authentication:
18+
# 1. Uncomment the credentials above
19+
# 2. Update lib/wordpress.ts to use Application Password authentication
20+
# 3. Add Authorization header: `Basic ${btoa(username:password)}`
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
'use client';
2+
3+
import { useToolbar } from '@wpengine/hwp-toolbar/react';
4+
import { toolbar } from '@/lib/toolbar';
5+
import '@wpengine/hwp-toolbar/styles';
6+
import { useState, useEffect } from 'react';
7+
8+
export function Toolbar() {
9+
const { state, nodes } = useToolbar(toolbar);
10+
const [position, setPosition] = useState<'top' | 'bottom'>('bottom');
11+
12+
useEffect(() => {
13+
const unsubscribe = toolbar.subscribe(() => {
14+
const config = (toolbar as any).config;
15+
setPosition(config?.position || 'bottom');
16+
});
17+
return unsubscribe;
18+
}, []);
19+
20+
return (
21+
<div className={`hwp-toolbar hwp-toolbar-${position}`}>
22+
<div className="hwp-toolbar-section hwp-toolbar-left">
23+
{nodes
24+
.filter((node) => node.position !== 'right')
25+
.map((node) => {
26+
const label = typeof node.label === 'function' ? node.label() : node.label;
27+
28+
if (node.type === 'divider') {
29+
return <div key={node.id} className="hwp-toolbar-divider" />;
30+
}
31+
32+
if (node.type === 'link' && node.href) {
33+
return (
34+
<a
35+
key={node.id}
36+
href={node.href}
37+
target={node.target}
38+
className="hwp-toolbar-link"
39+
>
40+
{label}
41+
</a>
42+
);
43+
}
44+
45+
return (
46+
<button
47+
key={node.id}
48+
onClick={node.onClick}
49+
className={`hwp-toolbar-button ${
50+
node.id === 'preview' && state.preview ? 'hwp-toolbar-button-active' : ''
51+
}`}
52+
>
53+
{label}
54+
</button>
55+
);
56+
})}
57+
</div>
58+
59+
<div className="hwp-toolbar-section hwp-toolbar-center"></div>
60+
61+
<div className="hwp-toolbar-section hwp-toolbar-right">
62+
{state.user && (
63+
<button
64+
className="hwp-toolbar-button"
65+
onClick={() => {
66+
if (confirm(`Logged in as: ${state.user!.name}\n\nLogout?`)) {
67+
toolbar.setWordPressContext({
68+
user: null,
69+
post: null,
70+
site: null,
71+
});
72+
}
73+
}}
74+
>
75+
User: {state.user.name}
76+
</button>
77+
)}
78+
</div>
79+
</div>
80+
);
81+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
* {
2+
box-sizing: border-box;
3+
margin: 0;
4+
padding: 0;
5+
}
6+
7+
body {
8+
font-family: system-ui, -apple-system, sans-serif;
9+
line-height: 1.5;
10+
color: #333;
11+
background: #f9f9f9;
12+
padding-bottom: 60px;
13+
}
14+
15+
.container {
16+
max-width: 800px;
17+
margin: 0 auto;
18+
padding: 16px;
19+
}
20+
21+
h1 {
22+
font-size: 1.25rem;
23+
margin-bottom: 0.25rem;
24+
font-weight: 600;
25+
color: #222;
26+
}
27+
28+
h2 {
29+
font-size: 0.75rem;
30+
margin: 1rem 0 0.5rem;
31+
text-transform: uppercase;
32+
letter-spacing: 0.5px;
33+
color: #888;
34+
font-weight: 500;
35+
}
36+
37+
h3 {
38+
font-size: 0.875rem;
39+
margin: 0.5rem 0;
40+
font-weight: 500;
41+
}
42+
43+
p {
44+
font-size: 0.875rem;
45+
color: #666;
46+
margin-bottom: 1rem;
47+
}
48+
49+
.controls,
50+
.state,
51+
.info {
52+
margin-bottom: 1rem;
53+
padding: 0.75rem;
54+
background: white;
55+
border: 1px solid #e8e8e8;
56+
border-radius: 2px;
57+
}
58+
59+
.controls button {
60+
padding: 4px 10px;
61+
margin: 3px 3px 3px 0;
62+
border: 1px solid #d0d0d0;
63+
background: white;
64+
color: #555;
65+
cursor: pointer;
66+
font-size: 12px;
67+
border-radius: 2px;
68+
}
69+
70+
.controls button:hover {
71+
background: #fafafa;
72+
border-color: #999;
73+
}
74+
75+
#wordpress-posts button {
76+
padding: 4px 10px;
77+
margin: 3px 3px 3px 0;
78+
border: 1px solid #d0d0d0;
79+
background: white;
80+
color: #555;
81+
cursor: pointer;
82+
font-size: 12px;
83+
border-radius: 2px;
84+
}
85+
86+
#wordpress-posts button:hover {
87+
background: #fafafa;
88+
border-color: #999;
89+
}
90+
91+
.state pre {
92+
background: #fafafa;
93+
color: #444;
94+
padding: 0.5rem;
95+
font-size: 0.75rem;
96+
font-family: monospace;
97+
overflow-x: auto;
98+
border: 1px solid #eee;
99+
border-radius: 2px;
100+
}
101+
102+
.info ol {
103+
margin-left: 1.25rem;
104+
font-size: 0.8125rem;
105+
}
106+
107+
.info li {
108+
margin: 0.25rem 0;
109+
color: #666;
110+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Metadata } from 'next';
2+
import { Toolbar } from './components/Toolbar';
3+
import './globals.css';
4+
5+
export const metadata: Metadata = {
6+
title: 'Toolbar Demo - React Hooks',
7+
description: 'Headless WordPress Toolbar with React hooks',
8+
};
9+
10+
export default function RootLayout({
11+
children,
12+
}: Readonly<{
13+
children: React.ReactNode;
14+
}>) {
15+
return (
16+
<html lang="en">
17+
<body>
18+
{children}
19+
<Toolbar />
20+
</body>
21+
</html>
22+
);
23+
}

0 commit comments

Comments
 (0)