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
This commit is contained in:
Dwindi Ramadhana
2026-02-05 22:51:44 +07:00
parent 5f08c18ec7
commit 7da4f0a167
11 changed files with 221 additions and 22 deletions

View File

@@ -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.

View File

@@ -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 (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 ml-2 text-muted-foreground hover:text-primary"
asChild
>
<a href={docUrl} target="_blank" rel="noopener noreferrer" title="View Documentation">
<BookOpen className="h-4 w-4" />
<span className="sr-only">View Documentation</span>
</a>
</Button>
);
}

View File

@@ -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 (
<div className={`sticky top-0 z-20 border-b bg-background ${hideOnDesktop ? 'md:hidden' : ''}`}>
<div className={`${containerClass} px-4 py-3 flex items-center justify-between min-w-0`}>
<div className="min-w-0 flex-1">
<div className="min-w-0 flex-1 flex items-center">
<h1 className="text-lg font-semibold truncate">{title}</h1>
<DocLink />
</div>
{action && <div className="flex-shrink-0 ml-4">{action}</div>}
</div>

View File

@@ -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<string, string> = {
// 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;
};

View File

@@ -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;

View File

@@ -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 (
<div className="w-full space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">{__('Newsletter')}</h1>
<div className="flex items-center">
<h1 className="text-2xl font-bold tracking-tight">{__('Newsletter')}</h1>
<DocLink />
</div>
<p className="text-muted-foreground mt-2">{__('Newsletter module is disabled')}</p>
</div>
<div className="bg-yellow-50 dark:bg-yellow-950/20 border border-yellow-200 dark:border-yellow-900 rounded-lg p-6 text-center">
@@ -53,7 +58,10 @@ export default function NewsletterLayout() {
return (
<div className="w-full space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">{__('Newsletter')}</h1>
<div className="flex items-center">
<h1 className="text-2xl font-bold tracking-tight">{__('Newsletter')}</h1>
<DocLink />
</div>
<p className="text-muted-foreground mt-2">{__('Manage subscribers and send email campaigns')}</p>
</div>

View File

@@ -24,13 +24,18 @@ const cards: MarketingCard[] = [
},
];
import { DocLink } from '@/components/DocLink';
export default function Marketing() {
const navigate = useNavigate();
return (
<div className="w-full space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">{__('Marketing')}</h1>
<div className="flex items-center">
<h1 className="text-2xl font-bold tracking-tight">{__('Marketing')}</h1>
<DocLink />
</div>
<p className="text-muted-foreground mt-2">{__('Newsletter, campaigns, and promotions')}</p>
</div>

View File

@@ -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({
<div className="mb-8">
<div className="flex items-start justify-between gap-4 min-w-0">
<div className="min-w-0 flex-1">
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
<div className="flex items-center">
<h1 className="text-2xl font-bold tracking-tight">{title}</h1>
<DocLink />
</div>
{description && (
<p className="text-muted-foreground mt-2">{description}</p>
)}

View File

@@ -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
}

View File

@@ -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 <ClassicLayout>{children}</ClassicLayout>;
case 'centered':
return <ModernLayout>{children}</ModernLayout>;
case 'minimal':
return <LaunchLayout>{children}</LaunchLayout>;
case 'split':
return <BoutiqueLayout>{children}</BoutiqueLayout>;
default:
return <ClassicLayout>{children}</ClassicLayout>;
}
return (
<>
<CouponURLHandler />
{/* Map header styles to layouts */}
{headerSettings.style === 'classic' && <ClassicLayout>{children}</ClassicLayout>}
{headerSettings.style === 'centered' && <ModernLayout>{children}</ModernLayout>}
{headerSettings.style === 'minimal' && <LaunchLayout>{children}</LaunchLayout>}
{headerSettings.style === 'split' && <BoutiqueLayout>{children}</BoutiqueLayout>}
</>
);
}
// Temporary internal switch function removed to allow fragment wrapping above.
// Re-implementing logic directly in return for cleaner wrapping.
/**
* Classic Layout - Traditional ecommerce
*/

View File

@@ -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;
}