refactor: Simplify to single SPA entry page architecture
User feedback: 'SPA means Single Page, why 4 pages?' Correct architecture: - 1 SPA entry page (e.g., /store) - SPA Mode determines initial route: * Full SPA → starts at shop page * Checkout Only → starts at cart page * Disabled → never loads - React Router handles rest via /#/ routing Changes: - Admin UI: Changed from 4 page selectors to 1 SPA entry page - Backend: spa_pages array → spa_page integer - Template: Initial route based on spa_mode setting - Simplified is_spa_page() checks (single ID comparison) Benefits: - User can set /store as homepage (Settings → Reading) - Landing page → CTA → direct to cart/checkout - Clean single entry point - Mode controls behavior, not multiple pages Example flow: - Visit https://site.com/store - Full SPA: loads shop, navigate via /#/product/123 - Checkout Only: loads cart, navigate via /#/checkout - Homepage: set /store as homepage, SPA loads on site root Next: Add direct-to-cart CTA with product parameter
This commit is contained in:
@@ -21,12 +21,7 @@ interface WordPressPage {
|
|||||||
export default function AppearanceGeneral() {
|
export default function AppearanceGeneral() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [spaMode, setSpaMode] = useState<'disabled' | 'checkout_only' | 'full'>('full');
|
const [spaMode, setSpaMode] = useState<'disabled' | 'checkout_only' | 'full'>('full');
|
||||||
const [spaPages, setSpaPages] = useState({
|
const [spaPage, setSpaPage] = useState(0);
|
||||||
shop: 0,
|
|
||||||
cart: 0,
|
|
||||||
checkout: 0,
|
|
||||||
account: 0,
|
|
||||||
});
|
|
||||||
const [availablePages, setAvailablePages] = useState<WordPressPage[]>([]);
|
const [availablePages, setAvailablePages] = useState<WordPressPage[]>([]);
|
||||||
const [toastPosition, setToastPosition] = useState('top-right');
|
const [toastPosition, setToastPosition] = useState('top-right');
|
||||||
const [typographyMode, setTypographyMode] = useState<'predefined' | 'custom_google'>('predefined');
|
const [typographyMode, setTypographyMode] = useState<'predefined' | 'custom_google'>('predefined');
|
||||||
@@ -59,14 +54,7 @@ export default function AppearanceGeneral() {
|
|||||||
|
|
||||||
if (general) {
|
if (general) {
|
||||||
if (general.spa_mode) setSpaMode(general.spa_mode);
|
if (general.spa_mode) setSpaMode(general.spa_mode);
|
||||||
if (general.spa_pages) {
|
if (general.spa_page) setSpaPage(general.spa_page || 0);
|
||||||
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.toast_position) setToastPosition(general.toast_position);
|
||||||
if (general.typography) {
|
if (general.typography) {
|
||||||
setTypographyMode(general.typography.mode || 'predefined');
|
setTypographyMode(general.typography.mode || 'predefined');
|
||||||
@@ -105,7 +93,7 @@ export default function AppearanceGeneral() {
|
|||||||
try {
|
try {
|
||||||
await api.post('/appearance/general', {
|
await api.post('/appearance/general', {
|
||||||
spaMode,
|
spaMode,
|
||||||
spaPages,
|
spaPage,
|
||||||
toastPosition,
|
toastPosition,
|
||||||
typography: {
|
typography: {
|
||||||
mode: typographyMode,
|
mode: typographyMode,
|
||||||
@@ -142,7 +130,7 @@ export default function AppearanceGeneral() {
|
|||||||
Disabled
|
Disabled
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Use WordPress default pages (no SPA functionality)
|
SPA never loads (use WordPress default pages)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,7 +142,7 @@ export default function AppearanceGeneral() {
|
|||||||
Checkout Only
|
Checkout Only
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
SPA for checkout flow only (cart, checkout, thank you)
|
SPA starts at cart page (cart → checkout → thank you → account)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,90 +154,33 @@ export default function AppearanceGeneral() {
|
|||||||
Full SPA
|
Full SPA
|
||||||
</Label>
|
</Label>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Entire customer-facing site uses SPA (recommended)
|
SPA starts at shop page (shop → product → cart → checkout → account)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|
||||||
{/* SPA Pages */}
|
{/* SPA Page */}
|
||||||
<SettingsCard
|
<SettingsCard
|
||||||
title="SPA Pages"
|
title="SPA Page"
|
||||||
description="Select which pages should render as full-page SPA (like WooCommerce settings)"
|
description="Select the page where the SPA will load (e.g., /store)"
|
||||||
>
|
>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
These pages will render directly to the body element with no theme interference.
|
This page will render the full SPA to the body element with no theme interference.
|
||||||
This is the recommended approach for a clean SPA experience.
|
The SPA Mode above determines the initial route (shop or cart). React Router handles navigation via /#/ routing.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<SettingsSection label="Shop Page" htmlFor="spa-page-shop">
|
<SettingsSection label="SPA Entry Page" htmlFor="spa-page">
|
||||||
<Select
|
<Select
|
||||||
value={spaPages.shop.toString()}
|
value={spaPage.toString()}
|
||||||
onValueChange={(value) => setSpaPages({ ...spaPages, shop: parseInt(value) })}
|
onValueChange={(value) => setSpaPage(parseInt(value))}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="spa-page-shop">
|
<SelectTrigger id="spa-page">
|
||||||
<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..." />
|
<SelectValue placeholder="Select a page..." />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -261,6 +192,11 @@ export default function AppearanceGeneral() {
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
|
<strong>Full SPA:</strong> Loads shop page initially<br />
|
||||||
|
<strong>Checkout Only:</strong> Loads cart page initially<br />
|
||||||
|
<strong>Tip:</strong> You can set this page as your homepage in Settings → Reading
|
||||||
|
</p>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</div>
|
</div>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|||||||
@@ -89,12 +89,7 @@ class AppearanceController {
|
|||||||
|
|
||||||
$general_data = [
|
$general_data = [
|
||||||
'spa_mode' => sanitize_text_field($request->get_param('spaMode')),
|
'spa_mode' => sanitize_text_field($request->get_param('spaMode')),
|
||||||
'spa_pages' => [
|
'spa_page' => absint($request->get_param('spaPage') ?? 0),
|
||||||
'shop' => absint($request->get_param('spaPages')['shop'] ?? 0),
|
|
||||||
'cart' => absint($request->get_param('spaPages')['cart'] ?? 0),
|
|
||||||
'checkout' => absint($request->get_param('spaPages')['checkout'] ?? 0),
|
|
||||||
'account' => absint($request->get_param('spaPages')['account'] ?? 0),
|
|
||||||
],
|
|
||||||
'toast_position' => sanitize_text_field($request->get_param('toastPosition') ?? 'top-right'),
|
'toast_position' => sanitize_text_field($request->get_param('toastPosition') ?? 'top-right'),
|
||||||
'typography' => [
|
'typography' => [
|
||||||
'mode' => sanitize_text_field($request->get_param('typography')['mode'] ?? 'predefined'),
|
'mode' => sanitize_text_field($request->get_param('typography')['mode'] ?? 'predefined'),
|
||||||
@@ -415,12 +410,7 @@ class AppearanceController {
|
|||||||
return [
|
return [
|
||||||
'general' => [
|
'general' => [
|
||||||
'spa_mode' => 'full',
|
'spa_mode' => 'full',
|
||||||
'spa_pages' => [
|
'spa_page' => 0,
|
||||||
'shop' => 0,
|
|
||||||
'cart' => 0,
|
|
||||||
'checkout' => 0,
|
|
||||||
'account' => 0,
|
|
||||||
],
|
|
||||||
'toast_position' => 'top-right',
|
'toast_position' => 'top-right',
|
||||||
'typography' => [
|
'typography' => [
|
||||||
'mode' => 'predefined',
|
'mode' => 'predefined',
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ class Assets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if current page is a designated SPA page
|
* Check if current page is the designated SPA page
|
||||||
*/
|
*/
|
||||||
private static function is_spa_page() {
|
private static function is_spa_page() {
|
||||||
global $post;
|
global $post;
|
||||||
@@ -371,18 +371,14 @@ class Assets {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get SPA page IDs from appearance settings
|
// Get SPA page ID from appearance settings
|
||||||
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||||
$spa_pages = isset($appearance_settings['general']['spa_pages']) ? $appearance_settings['general']['spa_pages'] : [];
|
$spa_page_id = isset($appearance_settings['general']['spa_page']) ? $appearance_settings['general']['spa_page'] : 0;
|
||||||
|
|
||||||
// Check if current page matches any SPA page
|
// Check if current page matches the SPA page
|
||||||
$current_page_id = $post->ID;
|
if ($spa_page_id && $post->ID == $spa_page_id) {
|
||||||
|
|
||||||
foreach ($spa_pages as $page_type => $page_id) {
|
|
||||||
if ($page_id && $current_page_id == $page_id) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ class TemplateOverride {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if current page is a designated SPA page
|
* Check if current page is the designated SPA page
|
||||||
*/
|
*/
|
||||||
private static function is_spa_page() {
|
private static function is_spa_page() {
|
||||||
global $post;
|
global $post;
|
||||||
@@ -279,18 +279,14 @@ class TemplateOverride {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get SPA page IDs from appearance settings
|
// Get SPA page ID from appearance settings
|
||||||
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||||
$spa_pages = isset($appearance_settings['general']['spa_pages']) ? $appearance_settings['general']['spa_pages'] : [];
|
$spa_page_id = isset($appearance_settings['general']['spa_page']) ? $appearance_settings['general']['spa_page'] : 0;
|
||||||
|
|
||||||
// Check if current page matches any SPA page
|
// Check if current page matches the SPA page
|
||||||
$current_page_id = $post->ID;
|
if ($spa_page_id && $post->ID == $spa_page_id) {
|
||||||
|
|
||||||
foreach ($spa_pages as $page_type => $page_id) {
|
|
||||||
if ($page_id && $current_page_id == $page_id) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,23 +8,19 @@
|
|||||||
</head>
|
</head>
|
||||||
<body <?php body_class('woonoow-spa-page'); ?>>
|
<body <?php body_class('woonoow-spa-page'); ?>>
|
||||||
<?php
|
<?php
|
||||||
// Determine page type and data attributes
|
// Determine initial route based on SPA mode
|
||||||
$page_type = 'shop';
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||||
$data_attrs = 'data-page="shop"';
|
$spa_mode = isset($appearance_settings['general']['spa_mode']) ? $appearance_settings['general']['spa_mode'] : 'full';
|
||||||
|
|
||||||
if (is_product()) {
|
// Set initial page based on mode
|
||||||
$page_type = 'product';
|
if ($spa_mode === 'checkout_only') {
|
||||||
global $post;
|
// Checkout Only mode starts at cart
|
||||||
$data_attrs = 'data-page="product" data-product-id="' . esc_attr($post->ID) . '"';
|
|
||||||
} elseif (is_cart()) {
|
|
||||||
$page_type = 'cart';
|
$page_type = 'cart';
|
||||||
$data_attrs = 'data-page="cart"';
|
$data_attrs = 'data-page="cart" data-initial-route="/cart"';
|
||||||
} elseif (is_checkout()) {
|
} else {
|
||||||
$page_type = 'checkout';
|
// Full SPA mode starts at shop
|
||||||
$data_attrs = 'data-page="checkout"';
|
$page_type = 'shop';
|
||||||
} elseif (is_account_page()) {
|
$data_attrs = 'data-page="shop" data-initial-route="/shop"';
|
||||||
$page_type = 'account';
|
|
||||||
$data_attrs = 'data-page="account"';
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user