From 7da4f0a16749eaf7c4b2fce1dd25bcab12415266 Mon Sep 17 00:00:00 2001 From: Dwindi Ramadhana Date: Thu, 5 Feb 2026 22:51:44 +0700 Subject: [PATCH] feat: integrate contextual links and fix coupons navigation - Added DocLink component and mapped routes - Fixed Coupons nav link to /marketing/coupons - Updated Settings pages to show inline documentation links --- .agent/plans/onboarding_strategy.md | 39 +++++++++++++ admin-spa/src/components/DocLink.tsx | 26 +++++++++ admin-spa/src/components/PageHeader.tsx | 5 +- admin-spa/src/config/docRoutes.ts | 58 +++++++++++++++++++ .../src/routes/Marketing/Coupons/index.tsx | 7 +++ .../src/routes/Marketing/Newsletter/index.tsx | 12 +++- admin-spa/src/routes/Marketing/index.tsx | 7 ++- .../Settings/components/SettingsLayout.tsx | 9 ++- .../src/components/CouponURLHandler.tsx | 49 ++++++++++++++++ customer-spa/src/layouts/BaseLayout.tsx | 27 +++++---- includes/Compat/NavigationRegistry.php | 4 +- 11 files changed, 221 insertions(+), 22 deletions(-) create mode 100644 .agent/plans/onboarding_strategy.md create mode 100644 admin-spa/src/components/DocLink.tsx create mode 100644 admin-spa/src/config/docRoutes.ts create mode 100644 customer-spa/src/components/CouponURLHandler.tsx diff --git a/.agent/plans/onboarding_strategy.md b/.agent/plans/onboarding_strategy.md new file mode 100644 index 0000000..d560c3c --- /dev/null +++ b/.agent/plans/onboarding_strategy.md @@ -0,0 +1,39 @@ +# User Onboarding & Simplification Strategy + +## The Problem +The current "General Settings" screen presents too many technical decisions (SPA Mode, Entry Page, Container Width, Typography, Colors) at once. This creates analysis paralysis for new users who just want to "get the store running." + +## Recommended Solution: The "Quick Setup" Wizard +Instead of dumping users into the Settings screen, we implement a **Linear Onboarding Flow** that launches automatically on the first visit (or manually via "Setup Wizard" button). + +### Tech Stack +* **No new libraries** needed. We can build this using your existing `@radix-ui` components (Dialog, Cards, Button). +* **State**: Managed via simple React state or Zustand store. + +### The Flow (4 Steps) + +#### 1. Welcome & Mode (The "What"?) +* **Question**: "How do you want to run your store?" +* **Options**: + * **Immersive (Full SPA)**: "Modern, app-like experience. Best for dedicated stores." (Selects 'full') + * **Classic (Checkout Only)**: "Keep your current theme, but use our super-fast checkout." (Selects 'checkout_only') + * **Standard**: "Use standard WordPress pages." (Selects 'disabled') + +#### 2. The Homepage (The "Where"?) +* **Question**: "Where should customers land?" +* **Action**: Dropdown to select a page. +* **Magic Button**: "Auto-create 'Shop' Page" (Creates a page, sets it as SPA Entry, and sets WP Frontpage setting automatically). **<-- This solves the redirect bug confusion.** + +#### 3. Styling (The "Look") +* **Question**: "Choose your vibe." +* **Design**: + * **Layout**: Simple visual toggle between "Boxed" (Focus) vs "Full Width" (Immersive). + * **Theme**: Clickable color swatches (Modern Black, Trusty Blue, Vibrant Purple). + +#### 4. The Finish Line +* **Action**: "Save & Launch Builder". +* **Result**: Redirects the user directly to the Visual Builder for their home page. + +## Ancillary Improvements +1. **Contextual Hints**: Use the already installed `HoverCard` or `Popover` to add "?" icons next to complex settings (like "SPA Entry Page") explaining them in plain English. +2. **Smart Defaults**: Pre-select "Boxed", "Full SPA", and "Modern" font pair so users can just click "Next -> Next -> Next" if they don't care. diff --git a/admin-spa/src/components/DocLink.tsx b/admin-spa/src/components/DocLink.tsx new file mode 100644 index 0000000..1306dc1 --- /dev/null +++ b/admin-spa/src/components/DocLink.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { useLocation } from 'react-router-dom'; +import { BookOpen } from 'lucide-react'; +import { getDocUrl } from '@/config/docRoutes'; +import { Button } from '@/components/ui/button'; + +export function DocLink() { + const location = useLocation(); + const docUrl = getDocUrl(location.pathname); + + if (!docUrl) return null; + + return ( + + ); +} diff --git a/admin-spa/src/components/PageHeader.tsx b/admin-spa/src/components/PageHeader.tsx index b586325..8925a06 100644 --- a/admin-spa/src/components/PageHeader.tsx +++ b/admin-spa/src/components/PageHeader.tsx @@ -7,6 +7,8 @@ interface PageHeaderProps { hideOnDesktop?: boolean; } +import { DocLink } from '@/components/DocLink'; + export function PageHeader({ fullscreen = false, hideOnDesktop = false }: PageHeaderProps) { const { title, action } = usePageHeader(); const location = useLocation(); @@ -24,8 +26,9 @@ export function PageHeader({ fullscreen = false, hideOnDesktop = false }: PageHe return (
-
+

{title}

+
{action &&
{action}
}
diff --git a/admin-spa/src/config/docRoutes.ts b/admin-spa/src/config/docRoutes.ts new file mode 100644 index 0000000..11e1cee --- /dev/null +++ b/admin-spa/src/config/docRoutes.ts @@ -0,0 +1,58 @@ +/** + * docRoutes.ts + * + * Maps Admin SPA routes to external documentation URLs. + * Used by the DocLink component to provide contextual help. + */ +export const docRoutes: Record = { + // Marketing Suite + // '/marketing': 'https://docs.woonoow.com/docs/marketing', // No general marketing doc yet + '/marketing/coupons': 'https://docs.woonoow.com/docs/marketing/coupons', + '/marketing/newsletter': 'https://docs.woonoow.com/docs/marketing/newsletter', + '/marketing/wishlist': 'https://docs.woonoow.com/docs/marketing/wishlist', + + // Settings - Modules + '/settings/modules/wishlist': 'https://docs.woonoow.com/docs/marketing/wishlist', + '/settings/modules/newsletter': 'https://docs.woonoow.com/docs/marketing/newsletter', + + // Builder + '/appearance/header': 'https://docs.woonoow.com/docs/builder/header-footer#header', + '/appearance/footer': 'https://docs.woonoow.com/docs/builder/header-footer#footer', + + // Store Management + '/products': 'https://docs.woonoow.com/docs/store/products', + '/orders': 'https://docs.woonoow.com/docs/store/orders', + '/customers': 'https://docs.woonoow.com/docs/store/customers', + + // Configuration + '/settings': 'https://docs.woonoow.com/docs/configuration/general', + '/settings/store': 'https://docs.woonoow.com/docs/configuration/general', + '/settings/payments': 'https://docs.woonoow.com/docs/configuration/payment-shipping', + '/settings/shipping': 'https://docs.woonoow.com/docs/configuration/payment-shipping', + '/settings/tax': 'https://docs.woonoow.com/docs/configuration/general', // Fallback + '/settings/customers': 'https://docs.woonoow.com/docs/store/customers', + '/settings/security': 'https://docs.woonoow.com/docs/configuration/security', + '/settings/notifications': 'https://docs.woonoow.com/docs/configuration/email', + '/settings/modules': 'https://docs.woonoow.com/docs/configuration/modules', + '/appearance/themes': 'https://docs.woonoow.com/docs/configuration/appearance', +}; + +/** + * Helper to get doc URL for a specific path + * + * Can be enhanced with regex matching if needed + */ +export const getDocUrl = (path: string): string | null => { + // 1. Direct match + if (docRoutes[path]) return docRoutes[path]; + + // 2. Partial match (longest match first) + const sortedKeys = Object.keys(docRoutes).sort((a, b) => b.length - a.length); + for (const key of sortedKeys) { + if (path.startsWith(key)) { + return docRoutes[key]; + } + } + + return null; +}; diff --git a/admin-spa/src/routes/Marketing/Coupons/index.tsx b/admin-spa/src/routes/Marketing/Coupons/index.tsx index 964d822..0db074c 100644 --- a/admin-spa/src/routes/Marketing/Coupons/index.tsx +++ b/admin-spa/src/routes/Marketing/Coupons/index.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { usePageHeader } from '@/contexts/PageHeaderContext'; import { Link, useNavigate } from 'react-router-dom'; import { __ } from '@/lib/i18n'; import { CouponsApi, type Coupon } from '@/lib/api/coupons'; @@ -36,6 +37,12 @@ export default function CouponsIndex() { // Configure FAB to navigate to new coupon page useFABConfig('coupons'); + // Set page header for contextual link + const { setPageHeader } = usePageHeader(); + React.useEffect(() => { + setPageHeader(__('Coupons')); + }, [setPageHeader]); + // Count active filters const activeFiltersCount = discountType && discountType !== 'all' ? 1 : 0; diff --git a/admin-spa/src/routes/Marketing/Newsletter/index.tsx b/admin-spa/src/routes/Marketing/Newsletter/index.tsx index bde9c0d..506f860 100644 --- a/admin-spa/src/routes/Marketing/Newsletter/index.tsx +++ b/admin-spa/src/routes/Marketing/Newsletter/index.tsx @@ -6,6 +6,8 @@ import { __ } from '@/lib/i18n'; import { useModules } from '@/hooks/useModules'; import { cn } from '@/lib/utils'; // Assuming cn exists, widely used in ShadCN +import { DocLink } from '@/components/DocLink'; + export default function NewsletterLayout() { const navigate = useNavigate(); const location = useLocation(); @@ -16,7 +18,10 @@ export default function NewsletterLayout() { return (
-

{__('Newsletter')}

+
+

{__('Newsletter')}

+ +

{__('Newsletter module is disabled')}

@@ -53,7 +58,10 @@ export default function NewsletterLayout() { return (
-

{__('Newsletter')}

+
+

{__('Newsletter')}

+ +

{__('Manage subscribers and send email campaigns')}

diff --git a/admin-spa/src/routes/Marketing/index.tsx b/admin-spa/src/routes/Marketing/index.tsx index 772fde4..2217575 100644 --- a/admin-spa/src/routes/Marketing/index.tsx +++ b/admin-spa/src/routes/Marketing/index.tsx @@ -24,13 +24,18 @@ const cards: MarketingCard[] = [ }, ]; +import { DocLink } from '@/components/DocLink'; + export default function Marketing() { const navigate = useNavigate(); return (
-

{__('Marketing')}

+
+

{__('Marketing')}

+ +

{__('Newsletter, campaigns, and promotions')}

diff --git a/admin-spa/src/routes/Settings/components/SettingsLayout.tsx b/admin-spa/src/routes/Settings/components/SettingsLayout.tsx index 80bde16..7943985 100644 --- a/admin-spa/src/routes/Settings/components/SettingsLayout.tsx +++ b/admin-spa/src/routes/Settings/components/SettingsLayout.tsx @@ -3,6 +3,8 @@ import { Button } from '@/components/ui/button'; import { Loader2 } from 'lucide-react'; import { usePageHeader } from '@/contexts/PageHeaderContext'; +import { DocLink } from '@/components/DocLink'; + interface SettingsLayoutProps { title: string | React.ReactNode; description?: string; @@ -40,7 +42,7 @@ export function SettingsLayout({ useEffect(() => { // Extract string title if it's a ReactNode const titleString = typeof title === 'string' ? title : ''; - + if (onSave) { // Combine custom action with save button const headerAction = ( @@ -84,7 +86,10 @@ export function SettingsLayout({
-

{title}

+
+

{title}

+ +
{description && (

{description}

)} diff --git a/customer-spa/src/components/CouponURLHandler.tsx b/customer-spa/src/components/CouponURLHandler.tsx new file mode 100644 index 0000000..2652cc7 --- /dev/null +++ b/customer-spa/src/components/CouponURLHandler.tsx @@ -0,0 +1,49 @@ +import React, { useEffect, useRef } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { applyCoupon } from '@/lib/cart/api'; +import { useCartStore } from '@/lib/cart/store'; +import { toast } from 'sonner'; + +/** + * CouponURLHandler + * + * Global component that listens for 'coupon' or 'apply_coupon' query parameters + * and automatically applies them to the cart session. + */ +export function CouponURLHandler() { + const [searchParams, setSearchParams] = useSearchParams(); + const { setCart } = useCartStore(); + const processedRef = useRef(false); + + useEffect(() => { + const couponCode = searchParams.get('coupon') || searchParams.get('apply_coupon'); + + if (couponCode && !processedRef.current) { + processedRef.current = true; // Prevent double firing in StrictMode + + const apply = async () => { + const toastId = toast.loading(`Applying coupon: ${couponCode}...`); + + try { + const updatedCart = await applyCoupon(couponCode); + setCart(updatedCart); + toast.success(`Coupon "${couponCode}" applied successfully!`, { id: toastId }); + } catch (error: any) { + console.error('Failed to apply URL coupon:', error); + toast.error(error.message || `Failed to apply coupon "${couponCode}"`, { id: toastId }); + } finally { + // Remove the coupon param from URL to prevent re-application on refresh + // Use a new URLSearchParams object to avoid direct mutation issues + const newParams = new URLSearchParams(searchParams); + newParams.delete('coupon'); + newParams.delete('apply_coupon'); + setSearchParams(newParams, { replace: true }); + } + }; + + apply(); + } + }, [searchParams, setSearchParams, setCart]); + + return null; // This component renders nothing +} diff --git a/customer-spa/src/layouts/BaseLayout.tsx b/customer-spa/src/layouts/BaseLayout.tsx index ad69580..ea0e13b 100644 --- a/customer-spa/src/layouts/BaseLayout.tsx +++ b/customer-spa/src/layouts/BaseLayout.tsx @@ -9,6 +9,7 @@ import { NewsletterForm } from '../components/NewsletterForm'; import { LayoutWrapper } from './LayoutWrapper'; import { useModules } from '../hooks/useModules'; import { useModuleSettings } from '../hooks/useModuleSettings'; +import { CouponURLHandler } from '../components/CouponURLHandler'; interface BaseLayoutProps { children: ReactNode; @@ -22,22 +23,20 @@ interface BaseLayoutProps { export function BaseLayout({ children }: BaseLayoutProps) { const headerSettings = useHeaderSettings(); - // Map header styles to layouts - // classic -> ClassicLayout, centered -> ModernLayout, minimal -> LaunchLayout, split -> BoutiqueLayout - switch (headerSettings.style) { - case 'classic': - return {children}; - case 'centered': - return {children}; - case 'minimal': - return {children}; - case 'split': - return {children}; - default: - return {children}; - } + return ( + <> + + {/* Map header styles to layouts */} + {headerSettings.style === 'classic' && {children}} + {headerSettings.style === 'centered' && {children}} + {headerSettings.style === 'minimal' && {children}} + {headerSettings.style === 'split' && {children}} + + ); } +// Temporary internal switch function removed to allow fragment wrapping above. +// Re-implementing logic directly in return for cleaner wrapping. /** * Classic Layout - Traditional ecommerce */ diff --git a/includes/Compat/NavigationRegistry.php b/includes/Compat/NavigationRegistry.php index b1cc4c1..6bf1a6a 100644 --- a/includes/Compat/NavigationRegistry.php +++ b/includes/Compat/NavigationRegistry.php @@ -15,7 +15,7 @@ if (! defined('ABSPATH')) exit; class NavigationRegistry { const NAV_OPTION = 'wnw_nav_tree'; - const NAV_VERSION = '1.3.0'; // Added Subscriptions section + const NAV_VERSION = '1.3.1'; // Updated Coupons link /** * Initialize hooks @@ -222,7 +222,7 @@ class NavigationRegistry } // Coupons - always available - $children[] = ['label' => __('Coupons', 'woonoow'), 'mode' => 'spa', 'path' => '/coupons']; + $children[] = ['label' => __('Coupons', 'woonoow'), 'mode' => 'spa', 'path' => '/marketing/coupons']; return $children; }