feat: Enhance Store Details with branding features
## 1. Architecture Decisions ✅ Created two comprehensive documents: ### A. ARCHITECTURE_DECISION_CUSTOMER_SPA.md **Decision: Hybrid Approach (Option C)** **WooNooW Plugin ($149/year):** - Admin-SPA (full featured) ✅ - Customer-SPA (basic cart/checkout/account) ✅ - Shortcode mode (works with any theme) ✅ - Full SPA mode (optional) ✅ **Premium Themes ($79/year each):** - Enhanced customer-spa components - Industry-specific designs - Optional upsell **Revenue Analysis:** - Option A (Core): $149K/year - Option B (Separate): $137K/year - **Option C (Hybrid): $164K/year** ✅ Winner! **Benefits:** - 60% users get complete solution - 30% agencies can customize - 10% enterprise have flexibility - Higher revenue potential - Better market positioning ### B. ADDON_REACT_INTEGRATION.md **Clarified addon development approach** **Level 1: Vanilla JS** (No build) - Simple addons use window.WooNooW API - No build process needed - Easy for PHP developers **Level 2: Exposed React** (Recommended) - WooNooW exposes React on window - Addons can use React without bundling it - Build with external React - Best of both worlds **Level 3: Slot-Based** (Advanced) - Full React component integration - Type safety - Modern DX **Implementation:** ```typescript window.WooNooW = { React: React, ReactDOM: ReactDOM, hooks: { addFilter, addAction }, components: { Button, Input, Select }, utils: { api, toast }, }; ``` --- ## 2. Enhanced Store Details Page ✅ ### New Components Created: **A. ImageUpload Component** - Drag & drop support - WordPress media library integration - File validation (type, size) - Preview with remove button - Loading states **B. ColorPicker Component** - Native color picker - Hex input with validation - Preset colors - Live preview - Popover UI ### Store Details Enhancements: **Added to Store Identity Card:** - ✅ Store tagline input - ✅ Store logo upload (2MB max) - ✅ Store icon upload (1MB max) **New Brand Colors Card:** - ✅ Primary color picker - ✅ Accent color picker - ✅ Error color picker - ✅ Reset to default button - ✅ Live preview **Features:** - All branding in one place - No separate Brand & Appearance tab needed - Clean, professional UI - Easy to use - Industry standard --- ## Summary **Architecture:** - ✅ Customer-SPA in core (hybrid approach) - ✅ Addon React integration clarified - ✅ Revenue model optimized **Implementation:** - ✅ ImageUpload component - ✅ ColorPicker component - ✅ Enhanced Store Details page - ✅ Branding features integrated **Result:** - Clean, focused settings - Professional branding tools - Better revenue potential - Clear development path
This commit is contained in:
499
ADDON_REACT_INTEGRATION.md
Normal file
499
ADDON_REACT_INTEGRATION.md
Normal file
@@ -0,0 +1,499 @@
|
||||
# Addon React Integration - How It Works
|
||||
|
||||
## The Question
|
||||
|
||||
**"How can addon developers use React if we only ship built `app.js`?"**
|
||||
|
||||
You're absolutely right to question this! Let me clarify the architecture.
|
||||
|
||||
---
|
||||
|
||||
## Current Misunderstanding
|
||||
|
||||
**What I showed in examples:**
|
||||
```tsx
|
||||
// This WON'T work for external addons!
|
||||
import { addonLoader, addFilter } from '@woonoow/hooks';
|
||||
import { DestinationSearch } from './components/DestinationSearch';
|
||||
|
||||
addonLoader.register({
|
||||
id: 'rajaongkir-bridge',
|
||||
init: () => {
|
||||
addFilter('woonoow_order_form_after_shipping', (content) => {
|
||||
return <DestinationSearch />; // ❌ Can't do this!
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Problem:** External addons can't import React components because:
|
||||
1. They don't have access to our build pipeline
|
||||
2. They only get the compiled `app.js`
|
||||
3. React is bundled, not exposed
|
||||
|
||||
---
|
||||
|
||||
## Solution: Three Integration Levels
|
||||
|
||||
### **Level 1: Vanilla JS/jQuery** (Basic)
|
||||
|
||||
**For simple addons that just need to inject HTML/JS**
|
||||
|
||||
```javascript
|
||||
// addon-bridge.js (vanilla JS, no build needed)
|
||||
(function() {
|
||||
// Wait for WooNooW to load
|
||||
window.addEventListener('woonoow:loaded', function() {
|
||||
// Access WooNooW hooks
|
||||
window.WooNooW.addFilter('woonoow_order_form_after_shipping', function(container, formData) {
|
||||
// Inject HTML
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = `
|
||||
<div class="rajaongkir-destination">
|
||||
<label>Shipping Destination</label>
|
||||
<select id="rajaongkir-dest">
|
||||
<option>Select destination...</option>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
|
||||
// Add event listeners
|
||||
document.getElementById('rajaongkir-dest').addEventListener('change', function(e) {
|
||||
// Update WooNooW state
|
||||
window.WooNooW.updateFormData({
|
||||
shipping: {
|
||||
...formData.shipping,
|
||||
destination_id: e.target.value
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return container;
|
||||
});
|
||||
});
|
||||
})();
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- ✅ No build process needed
|
||||
- ✅ Works immediately
|
||||
- ✅ Easy for PHP developers
|
||||
- ✅ No dependencies
|
||||
|
||||
**Cons:**
|
||||
- ❌ No React benefits
|
||||
- ❌ Manual DOM manipulation
|
||||
- ❌ No type safety
|
||||
|
||||
---
|
||||
|
||||
### **Level 2: Exposed React Runtime** (Recommended)
|
||||
|
||||
**WooNooW exposes React on window for addons to use**
|
||||
|
||||
#### WooNooW Core Setup:
|
||||
|
||||
```typescript
|
||||
// admin-spa/src/main.tsx
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
// Expose React for addons
|
||||
window.WooNooW = {
|
||||
React: React,
|
||||
ReactDOM: ReactDOM,
|
||||
hooks: {
|
||||
addFilter: addFilter,
|
||||
addAction: addAction,
|
||||
// ... other hooks
|
||||
},
|
||||
components: {
|
||||
// Expose common components
|
||||
Button: Button,
|
||||
Input: Input,
|
||||
Select: Select,
|
||||
// ... other UI components
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### Addon Development (with build):
|
||||
|
||||
```javascript
|
||||
// addon-bridge.js (built with Vite/Webpack)
|
||||
const { React, hooks, components } = window.WooNooW;
|
||||
const { addFilter } = hooks;
|
||||
const { Button, Select } = components;
|
||||
|
||||
// Addon can now use React!
|
||||
function DestinationSearch({ value, onChange }) {
|
||||
const [destinations, setDestinations] = React.useState([]);
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
// Fetch destinations
|
||||
fetch('/wp-json/rajaongkir/v1/destinations')
|
||||
.then(res => res.json())
|
||||
.then(data => setDestinations(data));
|
||||
}, []);
|
||||
|
||||
return React.createElement('div', { className: 'rajaongkir-search' },
|
||||
React.createElement('label', null, 'Shipping Destination'),
|
||||
React.createElement(Select, {
|
||||
value: value,
|
||||
onChange: onChange,
|
||||
options: destinations,
|
||||
loading: loading
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Register with WooNooW
|
||||
addFilter('woonoow_order_form_after_shipping', function(container, formData, setFormData) {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(
|
||||
React.createElement(DestinationSearch, {
|
||||
value: formData.shipping?.destination_id,
|
||||
onChange: (value) => setFormData({
|
||||
...formData,
|
||||
shipping: { ...formData.shipping, destination_id: value }
|
||||
})
|
||||
})
|
||||
);
|
||||
return container;
|
||||
});
|
||||
```
|
||||
|
||||
**Addon Build Setup:**
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
export default {
|
||||
build: {
|
||||
lib: {
|
||||
entry: 'src/addon.js',
|
||||
name: 'RajaongkirBridge',
|
||||
fileName: 'addon'
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['react', 'react-dom'], // Don't bundle React
|
||||
output: {
|
||||
globals: {
|
||||
react: 'window.WooNooW.React',
|
||||
'react-dom': 'window.WooNooW.ReactDOM'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- ✅ Can use React
|
||||
- ✅ Access to WooNooW components
|
||||
- ✅ Better DX
|
||||
- ✅ Type safety (with TypeScript)
|
||||
|
||||
**Cons:**
|
||||
- ❌ Requires build process
|
||||
- ❌ More complex setup
|
||||
|
||||
---
|
||||
|
||||
### **Level 3: Slot-Based Rendering** (Advanced)
|
||||
|
||||
**WooNooW renders addon components via slots**
|
||||
|
||||
#### WooNooW Core:
|
||||
|
||||
```typescript
|
||||
// OrderForm.tsx
|
||||
function OrderForm() {
|
||||
// ... form logic
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* ... shipping fields ... */}
|
||||
|
||||
{/* Slot for addons to inject */}
|
||||
<AddonSlot
|
||||
name="order_form_after_shipping"
|
||||
props={{ formData, setFormData }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// AddonSlot.tsx
|
||||
function AddonSlot({ name, props }) {
|
||||
const slots = useAddonSlots(name);
|
||||
|
||||
return (
|
||||
<>
|
||||
{slots.map((slot, index) => (
|
||||
<div key={index} data-addon-slot={slot.id}>
|
||||
{slot.component(props)}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Addon Registration (PHP):
|
||||
|
||||
```php
|
||||
// rajaongkir-bridge.php
|
||||
add_filter('woonoow/addon_slots', function($slots) {
|
||||
$slots['order_form_after_shipping'][] = [
|
||||
'id' => 'rajaongkir-destination',
|
||||
'component' => 'RajaongkirDestination', // Component name
|
||||
'script' => plugin_dir_url(__FILE__) . 'dist/addon.js',
|
||||
'priority' => 10,
|
||||
];
|
||||
return $slots;
|
||||
});
|
||||
```
|
||||
|
||||
#### Addon Component (React with build):
|
||||
|
||||
```typescript
|
||||
// addon/src/DestinationSearch.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export function RajaongkirDestination({ formData, setFormData }) {
|
||||
const [destinations, setDestinations] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/wp-json/rajaongkir/v1/destinations')
|
||||
.then(res => res.json())
|
||||
.then(setDestinations);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="rajaongkir-destination">
|
||||
<label>Shipping Destination</label>
|
||||
<select
|
||||
value={formData.shipping?.destination_id || ''}
|
||||
onChange={(e) => setFormData({
|
||||
...formData,
|
||||
shipping: {
|
||||
...formData.shipping,
|
||||
destination_id: e.target.value
|
||||
}
|
||||
})}
|
||||
>
|
||||
<option value="">Select destination...</option>
|
||||
{destinations.map(dest => (
|
||||
<option key={dest.id} value={dest.id}>
|
||||
{dest.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Export for WooNooW to load
|
||||
window.WooNooWAddons = window.WooNooWAddons || {};
|
||||
window.WooNooWAddons.RajaongkirDestination = RajaongkirDestination;
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- ✅ Full React support
|
||||
- ✅ Type safety
|
||||
- ✅ Modern DX
|
||||
- ✅ Proper component lifecycle
|
||||
|
||||
**Cons:**
|
||||
- ❌ Most complex
|
||||
- ❌ Requires build process
|
||||
- ❌ More WooNooW core complexity
|
||||
|
||||
---
|
||||
|
||||
## Recommended Approach: Level 2 (Exposed React)
|
||||
|
||||
### Implementation in WooNooW Core:
|
||||
|
||||
```typescript
|
||||
// admin-spa/src/main.tsx
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
// UI Components
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select } from '@/components/ui/select';
|
||||
import { Label } from '@/components/ui/label';
|
||||
// ... other components
|
||||
|
||||
// Hooks
|
||||
import { addFilter, addAction, applyFilters, doAction } from '@/lib/hooks';
|
||||
|
||||
// Expose WooNooW API
|
||||
window.WooNooW = {
|
||||
// React runtime
|
||||
React: React,
|
||||
ReactDOM: ReactDOM,
|
||||
|
||||
// Hooks system
|
||||
hooks: {
|
||||
addFilter,
|
||||
addAction,
|
||||
applyFilters,
|
||||
doAction,
|
||||
},
|
||||
|
||||
// UI Components (shadcn/ui)
|
||||
components: {
|
||||
Button,
|
||||
Input,
|
||||
Select,
|
||||
Label,
|
||||
// ... expose commonly used components
|
||||
},
|
||||
|
||||
// Utilities
|
||||
utils: {
|
||||
api: api, // API client
|
||||
toast: toast, // Toast notifications
|
||||
},
|
||||
|
||||
// Version
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
// Emit loaded event
|
||||
window.dispatchEvent(new CustomEvent('woonoow:loaded'));
|
||||
```
|
||||
|
||||
### Addon Developer Experience:
|
||||
|
||||
#### Option 1: Vanilla JS (No Build)
|
||||
|
||||
```javascript
|
||||
// addon.js
|
||||
(function() {
|
||||
const { React, hooks, components } = window.WooNooW;
|
||||
const { addFilter } = hooks;
|
||||
const { Select } = components;
|
||||
|
||||
addFilter('woonoow_order_form_after_shipping', function(container, props) {
|
||||
// Use React.createElement (no JSX)
|
||||
const element = React.createElement(Select, {
|
||||
label: 'Destination',
|
||||
options: [...],
|
||||
value: props.formData.shipping?.destination_id,
|
||||
onChange: (value) => props.setFormData({...})
|
||||
});
|
||||
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(element);
|
||||
|
||||
return container;
|
||||
});
|
||||
})();
|
||||
```
|
||||
|
||||
#### Option 2: With Build (JSX Support)
|
||||
|
||||
```typescript
|
||||
// addon/src/index.tsx
|
||||
const { React, hooks, components } = window.WooNooW;
|
||||
const { addFilter } = hooks;
|
||||
const { Select } = components;
|
||||
|
||||
function DestinationSearch({ formData, setFormData }) {
|
||||
return (
|
||||
<Select
|
||||
label="Destination"
|
||||
options={[...]}
|
||||
value={formData.shipping?.destination_id}
|
||||
onChange={(value) => setFormData({
|
||||
...formData,
|
||||
shipping: { ...formData.shipping, destination_id: value }
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
addFilter('woonoow_order_form_after_shipping', (container, props) => {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(<DestinationSearch {...props} />);
|
||||
return container;
|
||||
});
|
||||
```
|
||||
|
||||
```javascript
|
||||
// vite.config.js
|
||||
export default {
|
||||
build: {
|
||||
lib: {
|
||||
entry: 'src/index.tsx',
|
||||
formats: ['iife'],
|
||||
name: 'RajaongkirAddon'
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['react', 'react-dom'],
|
||||
output: {
|
||||
globals: {
|
||||
react: 'window.WooNooW.React',
|
||||
'react-dom': 'window.WooNooW.ReactDOM'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Documentation for Addon Developers
|
||||
|
||||
### Quick Start Guide:
|
||||
|
||||
```markdown
|
||||
# WooNooW Addon Development
|
||||
|
||||
## Level 1: Vanilla JS (Easiest)
|
||||
|
||||
No build process needed. Just use `window.WooNooW` API.
|
||||
|
||||
## Level 2: React with Build (Recommended)
|
||||
|
||||
1. Setup project:
|
||||
npm init
|
||||
npm install --save-dev vite @types/react
|
||||
|
||||
2. Configure vite.config.js (see example above)
|
||||
|
||||
3. Use WooNooW's React:
|
||||
const { React } = window.WooNooW;
|
||||
|
||||
4. Build:
|
||||
npm run build
|
||||
|
||||
5. Enqueue in WordPress:
|
||||
wp_enqueue_script('my-addon', plugin_dir_url(__FILE__) . 'dist/addon.js', ['woonoow-admin'], '1.0.0', true);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Your concern was valid!** ✅
|
||||
|
||||
**Solution:**
|
||||
1. ✅ Expose React on `window.WooNooW.React`
|
||||
2. ✅ Expose common components on `window.WooNooW.components`
|
||||
3. ✅ Addons can use vanilla JS (no build) or React (with build)
|
||||
4. ✅ Addons don't bundle React (use ours)
|
||||
5. ✅ Proper documentation for developers
|
||||
|
||||
**Result:**
|
||||
- Simple addons: Vanilla JS, no build
|
||||
- Advanced addons: React with build, external React
|
||||
- Best of both worlds!
|
||||
Reference in New Issue
Block a user