feat: Add SPA page selection UI in admin

Complete WooCommerce-style page architecture implementation:

Backend (already committed):
- API endpoint to fetch WordPress pages
- spa_pages field in appearance settings
- is_spa_page() checks in TemplateOverride and Assets

Frontend (this commit):
- Added page selector UI in Appearance > General
- Dropdowns for Shop, Cart, Checkout, Account pages
- Loads available WordPress pages from API
- Saves selected page IDs to settings
- Info alert explaining full-body rendering

UI Features:
- Clean page selection interface
- Shows all published WordPress pages
- '— None —' option to disable
- Integrated into existing General settings tab
- Follows existing design patterns

How it works:
1. Admin selects pages in Appearance > General
2. Page IDs saved to woonoow_appearance_settings
3. Frontend checks if current page matches selected pages
4. If match, renders full SPA to body (no theme interference)
5. Works with ANY theme consistently

Next: Test page selection and verify clean SPA rendering
This commit is contained in:
Dwindi Ramadhana
2025-12-30 20:19:46 +07:00
parent 012effd11d
commit f054a78c5d
2 changed files with 153 additions and 1 deletions

View File

@@ -12,9 +12,22 @@ import { Alert, AlertDescription } from '@/components/ui/alert';
import { toast } from 'sonner';
import { api } from '@/lib/api';
interface WordPressPage {
id: number;
title: string;
slug: string;
}
export default function AppearanceGeneral() {
const [loading, setLoading] = useState(true);
const [spaMode, setSpaMode] = useState<'disabled' | 'checkout_only' | 'full'>('full');
const [spaPages, setSpaPages] = useState({
shop: 0,
cart: 0,
checkout: 0,
account: 0,
});
const [availablePages, setAvailablePages] = useState<WordPressPage[]>([]);
const [toastPosition, setToastPosition] = useState('top-right');
const [typographyMode, setTypographyMode] = useState<'predefined' | 'custom_google'>('predefined');
const [predefinedPair, setPredefinedPair] = useState('modern');
@@ -40,11 +53,20 @@ export default function AppearanceGeneral() {
useEffect(() => {
const loadSettings = async () => {
try {
// Load appearance settings
const response = await api.get('/appearance/settings');
const general = response.data?.general;
if (general) {
if (general.spa_mode) setSpaMode(general.spa_mode);
if (general.spa_pages) {
setSpaPages({
shop: general.spa_pages.shop || 0,
cart: general.spa_pages.cart || 0,
checkout: general.spa_pages.checkout || 0,
account: general.spa_pages.account || 0,
});
}
if (general.toast_position) setToastPosition(general.toast_position);
if (general.typography) {
setTypographyMode(general.typography.mode || 'predefined');
@@ -63,6 +85,12 @@ export default function AppearanceGeneral() {
});
}
}
// Load available pages
const pagesResponse = await api.get('/pages/list');
if (pagesResponse.data?.data) {
setAvailablePages(pagesResponse.data.data);
}
} catch (error) {
console.error('Failed to load settings:', error);
} finally {
@@ -76,7 +104,8 @@ export default function AppearanceGeneral() {
const handleSave = async () => {
try {
await api.post('/appearance/general', {
spa_mode: spaMode,
spaMode,
spaPages,
toastPosition,
typography: {
mode: typographyMode,
@@ -144,6 +173,98 @@ export default function AppearanceGeneral() {
</RadioGroup>
</SettingsCard>
{/* SPA Pages */}
<SettingsCard
title="SPA Pages"
description="Select which pages should render as full-page SPA (like WooCommerce settings)"
>
<div className="space-y-4">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
These pages will render directly to the body element with no theme interference.
This is the recommended approach for a clean SPA experience.
</AlertDescription>
</Alert>
<SettingsSection label="Shop Page" htmlFor="spa-page-shop">
<Select
value={spaPages.shop.toString()}
onValueChange={(value) => setSpaPages({ ...spaPages, shop: parseInt(value) })}
>
<SelectTrigger id="spa-page-shop">
<SelectValue placeholder="Select a page..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="0"> None </SelectItem>
{availablePages.map((page) => (
<SelectItem key={page.id} value={page.id.toString()}>
{page.title}
</SelectItem>
))}
</SelectContent>
</Select>
</SettingsSection>
<SettingsSection label="Cart Page" htmlFor="spa-page-cart">
<Select
value={spaPages.cart.toString()}
onValueChange={(value) => setSpaPages({ ...spaPages, cart: parseInt(value) })}
>
<SelectTrigger id="spa-page-cart">
<SelectValue placeholder="Select a page..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="0"> None </SelectItem>
{availablePages.map((page) => (
<SelectItem key={page.id} value={page.id.toString()}>
{page.title}
</SelectItem>
))}
</SelectContent>
</Select>
</SettingsSection>
<SettingsSection label="Checkout Page" htmlFor="spa-page-checkout">
<Select
value={spaPages.checkout.toString()}
onValueChange={(value) => setSpaPages({ ...spaPages, checkout: parseInt(value) })}
>
<SelectTrigger id="spa-page-checkout">
<SelectValue placeholder="Select a page..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="0"> None </SelectItem>
{availablePages.map((page) => (
<SelectItem key={page.id} value={page.id.toString()}>
{page.title}
</SelectItem>
))}
</SelectContent>
</Select>
</SettingsSection>
<SettingsSection label="My Account Page" htmlFor="spa-page-account">
<Select
value={spaPages.account.toString()}
onValueChange={(value) => setSpaPages({ ...spaPages, account: parseInt(value) })}
>
<SelectTrigger id="spa-page-account">
<SelectValue placeholder="Select a page..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="0"> None </SelectItem>
{availablePages.map((page) => (
<SelectItem key={page.id} value={page.id.toString()}>
{page.title}
</SelectItem>
))}
</SelectContent>
</Select>
</SettingsSection>
</div>
</SettingsCard>
{/* Toast Notifications */}
<SettingsCard
title="Toast Notifications"