feat: implement onboarding wizard and fix help page navigation
Core Features: - Add Quick Setup Wizard for new users with multi-step flow - Implement distraction-free onboarding layout (no sidebar/header) - Create OnboardingController API endpoint for saving settings - Redirect new users to /setup automatically on first admin access Onboarding Components: - StepMode: Select between full/minimal store modes - StepHomepage: Choose or auto-create homepage - StepAppearance: Configure container width and primary color - StepProgress: Visual progress indicator Navigation & Routing: - Fix Help page links to use react-router navigation (prevent full reload) - Update onboarding completion redirect to /appearance/pages - Add manual onboarding access via Settings > Store Details UI/UX Improvements: - Enable dark mode support for Page Editor - Fix page title rendering in onboarding dropdown - Improve title fallback logic (title.rendered, title, post_title) Type Safety: - Unify PageItem interface across all components - Add 'default' to containerWidth type definition - Add missing properties (permalink_base, has_template, icon) Files Modified: - includes/Api/OnboardingController.php - includes/Api/Routes.php - includes/Admin/Assets.php - admin-spa/src/App.tsx - admin-spa/src/routes/Onboarding/* - admin-spa/src/routes/Help/DocContent.tsx - admin-spa/src/routes/Settings/Store.tsx - admin-spa/src/routes/Appearance/Pages/*
This commit is contained in:
@@ -296,6 +296,7 @@ import NewsletterCampaignsList from '@/routes/Marketing/Campaigns';
|
|||||||
import CampaignEdit from '@/routes/Marketing/Campaigns/Edit';
|
import CampaignEdit from '@/routes/Marketing/Campaigns/Edit';
|
||||||
import MorePage from '@/routes/More';
|
import MorePage from '@/routes/More';
|
||||||
import Help from '@/routes/Help';
|
import Help from '@/routes/Help';
|
||||||
|
import Onboarding from '@/routes/Onboarding';
|
||||||
|
|
||||||
// Addon Route Component - Dynamically loads addon components
|
// Addon Route Component - Dynamically loads addon components
|
||||||
function AddonRoute({ config }: { config: any }) {
|
function AddonRoute({ config }: { config: any }) {
|
||||||
@@ -569,7 +570,8 @@ function AppRoutes() {
|
|||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Dashboard */}
|
{/* Dashboard */}
|
||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/" element={<Navigate to={(window as any).WNW_CONFIG?.onboardingCompleted ? "/dashboard" : "/setup"} replace />} />
|
||||||
|
<Route path="/setup" element={<Onboarding />} />
|
||||||
<Route path="/reset-password" element={<ResetPassword />} />
|
<Route path="/reset-password" element={<ResetPassword />} />
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/dashboard/revenue" element={<DashboardRevenue />} />
|
<Route path="/dashboard/revenue" element={<DashboardRevenue />} />
|
||||||
@@ -744,6 +746,19 @@ function Shell() {
|
|||||||
const submenuTopClass = fullscreen ? 'top-0' : 'top-[calc(7rem+32px)]';
|
const submenuTopClass = fullscreen ? 'top-0' : 'top-[calc(7rem+32px)]';
|
||||||
const submenuZIndex = fullscreen ? 'z-50' : 'z-40';
|
const submenuZIndex = fullscreen ? 'z-50' : 'z-40';
|
||||||
|
|
||||||
|
// Check if current route is setup/onboarding
|
||||||
|
const isSetup = location.pathname === '/setup';
|
||||||
|
|
||||||
|
if (isSetup) {
|
||||||
|
return (
|
||||||
|
<AppProvider isStandalone={isStandalone} exitFullscreen={exitFullscreen}>
|
||||||
|
<div className="min-h-screen bg-background text-foreground flex flex-col">
|
||||||
|
<AppRoutes />
|
||||||
|
</div>
|
||||||
|
</AppProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppProvider isStandalone={isStandalone} exitFullscreen={exitFullscreen}>
|
<AppProvider isStandalone={isStandalone} exitFullscreen={exitFullscreen}>
|
||||||
{!isStandalone && <ShortcutsBinder onToggle={toggle} />}
|
{!isStandalone && <ShortcutsBinder onToggle={toggle} />}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import {
|
|||||||
import { InspectorField, SectionProp } from './InspectorField';
|
import { InspectorField, SectionProp } from './InspectorField';
|
||||||
import { InspectorRepeater } from './InspectorRepeater';
|
import { InspectorRepeater } from './InspectorRepeater';
|
||||||
import { MediaUploader } from '@/components/MediaUploader';
|
import { MediaUploader } from '@/components/MediaUploader';
|
||||||
import { SectionStyles, ElementStyle } from '../store/usePageEditorStore';
|
import { SectionStyles, ElementStyle, PageItem } from '../store/usePageEditorStore';
|
||||||
|
|
||||||
interface Section {
|
interface Section {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -44,17 +44,6 @@ interface Section {
|
|||||||
props: Record<string, SectionProp>;
|
props: Record<string, SectionProp>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageItem {
|
|
||||||
id?: number;
|
|
||||||
type: 'page' | 'template';
|
|
||||||
cpt?: string;
|
|
||||||
slug?: string;
|
|
||||||
title: string;
|
|
||||||
url?: string;
|
|
||||||
isSpaLanding?: boolean;
|
|
||||||
containerWidth?: 'boxed' | 'fullwidth';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InspectorPanelProps {
|
interface InspectorPanelProps {
|
||||||
page: PageItem | null;
|
page: PageItem | null;
|
||||||
selectedSection: Section | null;
|
selectedSection: Section | null;
|
||||||
|
|||||||
@@ -3,15 +3,7 @@ import { __ } from '@/lib/i18n';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { FileText, Layout, Loader2, Home } from 'lucide-react';
|
import { FileText, Layout, Loader2, Home } from 'lucide-react';
|
||||||
|
|
||||||
interface PageItem {
|
import { PageItem } from '../store/usePageEditorStore';
|
||||||
id?: number;
|
|
||||||
type: 'page' | 'template';
|
|
||||||
cpt?: string;
|
|
||||||
slug?: string;
|
|
||||||
title: string;
|
|
||||||
has_template?: boolean;
|
|
||||||
permalink_base?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PageSidebarProps {
|
interface PageSidebarProps {
|
||||||
pages: PageItem[];
|
pages: PageItem[];
|
||||||
|
|||||||
@@ -10,21 +10,7 @@ import { PageSidebar } from './components/PageSidebar';
|
|||||||
import { CanvasRenderer } from './components/CanvasRenderer';
|
import { CanvasRenderer } from './components/CanvasRenderer';
|
||||||
import { InspectorPanel } from './components/InspectorPanel';
|
import { InspectorPanel } from './components/InspectorPanel';
|
||||||
import { CreatePageModal } from './components/CreatePageModal';
|
import { CreatePageModal } from './components/CreatePageModal';
|
||||||
import { usePageEditorStore, Section } from './store/usePageEditorStore';
|
import { usePageEditorStore, Section, PageItem } from './store/usePageEditorStore';
|
||||||
|
|
||||||
// Types
|
|
||||||
interface PageItem {
|
|
||||||
id?: number;
|
|
||||||
type: 'page' | 'template';
|
|
||||||
cpt?: string;
|
|
||||||
slug?: string;
|
|
||||||
title: string;
|
|
||||||
url?: string;
|
|
||||||
icon?: string;
|
|
||||||
has_template?: boolean;
|
|
||||||
permalink_base?: string;
|
|
||||||
isFrontPage?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AppearancePages() {
|
export default function AppearancePages() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -229,12 +215,12 @@ export default function AppearancePages() {
|
|||||||
return (
|
return (
|
||||||
<div className={
|
<div className={
|
||||||
cn(
|
cn(
|
||||||
"flex flex-col bg-white transition-all duration-300",
|
"flex flex-col bg-background transition-all duration-300",
|
||||||
isFullscreen ? "fixed inset-0 z-[100] h-screen" : "h-[calc(100vh-64px)]"
|
isFullscreen ? "fixed inset-0 z-[100] h-screen" : "h-[calc(100vh-64px)]"
|
||||||
)
|
)
|
||||||
} >
|
} >
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
< div className="flex items-center justify-between px-6 py-3 border-b bg-white" >
|
< div className="flex items-center justify-between px-6 py-3 border-b bg-background" >
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold">{__('Page Editor')}</h1>
|
<h1 className="text-xl font-semibold">{__('Page Editor')}</h1>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
@@ -315,7 +301,7 @@ export default function AppearancePages() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 bg-gray-100 flex items-center justify-center text-gray-400">
|
<div className="flex-1 bg-muted/30 flex items-center justify-center text-muted-foreground">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Layout className="w-16 h-16 mx-auto mb-4 opacity-50" />
|
<Layout className="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||||
<p className="text-lg">{__('Select a page from the sidebar')}</p>
|
<p className="text-lg">{__('Select a page from the sidebar')}</p>
|
||||||
|
|||||||
@@ -60,9 +60,12 @@ export interface PageItem {
|
|||||||
slug?: string;
|
slug?: string;
|
||||||
title: string;
|
title: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
|
icon?: string;
|
||||||
|
has_template?: boolean;
|
||||||
|
permalink_base?: string;
|
||||||
isFrontPage?: boolean;
|
isFrontPage?: boolean;
|
||||||
isSpaLanding?: boolean;
|
isSpaLanding?: boolean;
|
||||||
containerWidth?: 'boxed' | 'fullwidth';
|
containerWidth?: 'boxed' | 'fullwidth' | 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageEditorState {
|
interface PageEditorState {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
@@ -11,6 +12,7 @@ interface DocContentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function DocContent({ slug }: DocContentProps) {
|
export default function DocContent({ slug }: DocContentProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [doc, setDoc] = useState<DocContentType | null>(null);
|
const [doc, setDoc] = useState<DocContentType | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -139,16 +141,27 @@ export default function DocContent({ slug }: DocContentProps) {
|
|||||||
</blockquote>
|
</blockquote>
|
||||||
),
|
),
|
||||||
// Links
|
// Links
|
||||||
a: ({ href, children }) => (
|
a: ({ href, children }) => {
|
||||||
|
const isExternal = href?.startsWith('http') || href?.startsWith('mailto:');
|
||||||
|
const isAnchor = href?.startsWith('#');
|
||||||
|
|
||||||
|
return (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={href}
|
||||||
className="text-primary hover:underline"
|
className="text-primary hover:underline cursor-pointer"
|
||||||
target={href?.startsWith('http') ? '_blank' : undefined}
|
target={isExternal ? '_blank' : undefined}
|
||||||
rel={href?.startsWith('http') ? 'noopener noreferrer' : undefined}
|
rel={isExternal ? 'noopener noreferrer' : undefined}
|
||||||
|
onClick={(e) => {
|
||||||
|
if (!isExternal && !isAnchor && href) {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate(href);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
),
|
);
|
||||||
|
},
|
||||||
// Lists
|
// Lists
|
||||||
ul: ({ children }) => (
|
ul: ({ children }) => (
|
||||||
<ul className="list-disc pl-6 my-4 space-y-2">{children}</ul>
|
<ul className="list-disc pl-6 my-4 space-y-2">{children}</ul>
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Check, Maximize, Minimize } from 'lucide-react';
|
||||||
|
|
||||||
|
interface StepAppearanceProps {
|
||||||
|
containerWidth: string;
|
||||||
|
primaryColor: string;
|
||||||
|
onWidthChange: (width: string) => void;
|
||||||
|
onColorChange: (color: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StepAppearance({ containerWidth, primaryColor, onWidthChange, onColorChange }: StepAppearanceProps) {
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
{ name: 'Modern Black', value: '#000000', ring: 'ring-gray-900' },
|
||||||
|
{ name: 'Trusty Blue', value: '#2563eb', ring: 'ring-blue-600' },
|
||||||
|
{ name: 'Vibrant Purple', value: '#7c3aed', ring: 'ring-purple-600' },
|
||||||
|
{ name: 'Forest Green', value: '#16a34a', ring: 'ring-green-600' },
|
||||||
|
{ name: 'Warm Orange', value: '#ea580c', ring: 'ring-orange-600' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const layouts = [
|
||||||
|
{
|
||||||
|
id: 'max-w-6xl',
|
||||||
|
title: 'Boxed',
|
||||||
|
description: 'Centered content with whitespace. Best for readability.',
|
||||||
|
icon: Minimize
|
||||||
|
// Using Minimize as a proxy for "Contained" visual
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'max-w-full',
|
||||||
|
title: 'Full Width',
|
||||||
|
description: 'Edge-to-edge immersive experience.',
|
||||||
|
icon: Maximize
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8 max-w-2xl mx-auto">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-2xl font-semibold mb-2">Choose your vibe</h2>
|
||||||
|
<p className="text-muted-foreground">Customize the look and feel of your store.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-medium uppercase text-muted-foreground">Layout Style</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{layouts.map(layout => {
|
||||||
|
const isSelected = containerWidth === layout.id;
|
||||||
|
const Icon = layout.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={layout.id}
|
||||||
|
onClick={() => onWidthChange(layout.id)}
|
||||||
|
className={`p-4 rounded-xl border-2 transition-all hover:bg-accent/50 text-left flex items-start gap-4 ${isSelected ? 'border-primary bg-accent/20' : 'border-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`p-2 rounded-md ${isSelected ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>
|
||||||
|
<Icon className="w-5 h-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold">{layout.title}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">{layout.description}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-sm font-medium uppercase text-muted-foreground">Brand Color</h3>
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
{colors.map(color => {
|
||||||
|
const isSelected = primaryColor === color.value;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={color.value}
|
||||||
|
onClick={() => onColorChange(color.value)}
|
||||||
|
className={`group relative w-16 h-16 rounded-full flex items-center justify-center transition-all ${isSelected ? `ring-4 ${color.ring} ring-offset-2 ring-offset-background scale-110` : 'hover:scale-105'
|
||||||
|
}`}
|
||||||
|
style={{ backgroundColor: color.value }}
|
||||||
|
title={color.name}
|
||||||
|
>
|
||||||
|
{isSelected && <Check className="w-6 h-6 text-white drop-shadow-md" />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
admin-spa/src/routes/Onboarding/components/StepHomepage.tsx
Normal file
102
admin-spa/src/routes/Onboarding/components/StepHomepage.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Sparkles, Home, ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
|
interface StepHomepageProps {
|
||||||
|
pageId: string | number;
|
||||||
|
createMagicPage: boolean;
|
||||||
|
onPageChange: (id: string | number) => void;
|
||||||
|
onMagicChange: (enabled: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StepHomepage({ pageId, createMagicPage, onPageChange, onMagicChange }: StepHomepageProps) {
|
||||||
|
const [pages, setPages] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Fetch pages for dropdown
|
||||||
|
const fetchPages = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch((window as any).WNW_CONFIG?.restUrl + '/pages?per_page=100');
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
setPages(data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch pages', e);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchPages();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 max-w-2xl mx-auto">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-2xl font-semibold mb-2">Where should customers land?</h2>
|
||||||
|
<p className="text-muted-foreground">Choose the entry point for your store.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-6">
|
||||||
|
{/* Option A: Magic Create */}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
onMagicChange(true);
|
||||||
|
onPageChange('');
|
||||||
|
}}
|
||||||
|
className={`relative flex items-center gap-4 p-6 rounded-xl border-2 transition-all hover:bg-accent/50 text-left ${createMagicPage ? 'border-primary bg-accent/20' : 'border-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`p-3 rounded-lg ${createMagicPage ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>
|
||||||
|
<Sparkles className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="font-semibold text-lg">Auto-create "Shop Home"</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">We'll generate a beautiful homepage for you and set it up automatically.</p>
|
||||||
|
</div>
|
||||||
|
{createMagicPage && <div className="w-4 h-4 rounded-full bg-primary" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center">
|
||||||
|
<span className="w-full border-t" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center text-xs uppercase">
|
||||||
|
<span className="bg-background px-2 text-muted-foreground">Or select existing</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Option B: Select Existing */}
|
||||||
|
<div className={`p-6 rounded-xl border-2 transition-all ${!createMagicPage && pageId ? 'border-primary bg-accent/20' : 'border-border'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<div className={`p-3 rounded-lg ${!createMagicPage && pageId ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>
|
||||||
|
<Home className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold text-lg">Use an existing page</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Select a page you've already created.</p>
|
||||||
|
</div>
|
||||||
|
{!createMagicPage && pageId && <div className="w-4 h-4 rounded-full bg-primary ml-auto" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
className="w-full h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
value={pageId}
|
||||||
|
onChange={(e) => {
|
||||||
|
onMagicChange(false);
|
||||||
|
onPageChange(e.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">Select a page...</option>
|
||||||
|
{pages.map(p => (
|
||||||
|
<option key={p.id} value={p.id}>
|
||||||
|
{p.title?.rendered || p.title || p.post_title || `Page #${p.id}`}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
admin-spa/src/routes/Onboarding/components/StepMode.tsx
Normal file
66
admin-spa/src/routes/Onboarding/components/StepMode.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { LayoutDashboard, ShoppingCart, FileText, Check } from 'lucide-react';
|
||||||
|
|
||||||
|
interface StepModeProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (mode: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StepMode({ value, onChange }: StepModeProps) {
|
||||||
|
const modes = [
|
||||||
|
{
|
||||||
|
id: 'full',
|
||||||
|
title: 'Immersive App',
|
||||||
|
description: 'Your entire store runs as a modern, high-speed app. Best for dedicated e-commerce sites.',
|
||||||
|
icon: LayoutDashboard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'checkout_only',
|
||||||
|
title: 'Checkout Only',
|
||||||
|
description: 'Keep your existing theme for pages, but use our super-fast checkout.',
|
||||||
|
icon: ShoppingCart,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'disabled',
|
||||||
|
title: 'Standard',
|
||||||
|
description: 'Use standard WordPress pages. Good compatibility with legacy plugins.',
|
||||||
|
icon: FileText,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-2xl font-semibold mb-2">How do you want to run your store?</h2>
|
||||||
|
<p className="text-muted-foreground">Select the mode that fits your business needs.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
|
{modes.map((mode) => {
|
||||||
|
const Icon = mode.icon;
|
||||||
|
const isSelected = value === mode.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={mode.id}
|
||||||
|
onClick={() => onChange(mode.id)}
|
||||||
|
className={`relative flex flex-col items-start p-6 rounded-xl border-2 transition-all hover:bg-accent/50 text-left h-full ${isSelected ? 'border-primary bg-accent/20' : 'border-border'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`p-3 rounded-lg mb-4 ${isSelected ? 'bg-primary text-primary-foreground' : 'bg-muted'}`}>
|
||||||
|
<Icon className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<h3 className="font-semibold text-lg mb-2">{mode.title}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">{mode.description}</p>
|
||||||
|
|
||||||
|
{isSelected && (
|
||||||
|
<div className="absolute top-4 right-4 text-primary">
|
||||||
|
<Check className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
admin-spa/src/routes/Onboarding/components/StepProgress.tsx
Normal file
17
admin-spa/src/routes/Onboarding/components/StepProgress.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface StepProgressProps {
|
||||||
|
currentStep: number;
|
||||||
|
totalSteps: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StepProgress({ currentStep, totalSteps }: StepProgressProps) {
|
||||||
|
return (
|
||||||
|
<div className="w-full bg-secondary h-2 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-primary transition-all duration-500 ease-out"
|
||||||
|
style={{ width: `${((currentStep + 1) / totalSteps) * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
159
admin-spa/src/routes/Onboarding/index.tsx
Normal file
159
admin-spa/src/routes/Onboarding/index.tsx
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { ArrowRight, ArrowLeft, Check, Loader2, Rocket } from 'lucide-react';
|
||||||
|
import { StepMode } from './components/StepMode';
|
||||||
|
import { StepHomepage } from './components/StepHomepage';
|
||||||
|
import { StepAppearance } from './components/StepAppearance';
|
||||||
|
import { StepProgress } from './components/StepProgress';
|
||||||
|
|
||||||
|
|
||||||
|
export default function Onboarding() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [step, setStep] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [data, setData] = useState({
|
||||||
|
mode: 'full',
|
||||||
|
pageId: '',
|
||||||
|
createMagicPage: false,
|
||||||
|
containerWidth: 'max-w-6xl',
|
||||||
|
primaryColor: '#000000',
|
||||||
|
});
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{ component: StepMode, title: 'Mode' },
|
||||||
|
{ component: StepHomepage, title: 'Homepage' },
|
||||||
|
{ component: StepAppearance, title: 'Appearance' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleNext = async () => {
|
||||||
|
if (step < steps.length - 1) {
|
||||||
|
if (step === 1 && !data.createMagicPage && !data.pageId) {
|
||||||
|
toast.error('Please select a page or choose auto-create');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStep(s => s + 1);
|
||||||
|
} else {
|
||||||
|
// Final Submit
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
mode: data.mode,
|
||||||
|
create_home_page: data.createMagicPage,
|
||||||
|
entry_page_id: data.pageId,
|
||||||
|
container_width: data.containerWidth,
|
||||||
|
primary_color: data.primaryColor
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch((window as any).WNW_CONFIG?.restUrl + '/onboarding/complete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-WP-Nonce': (window as any).WNW_CONFIG?.nonce
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.success) {
|
||||||
|
toast.success('Store setup complete!');
|
||||||
|
// Update global config to prevent showing onboarding again
|
||||||
|
if ((window as any).WNW_CONFIG) {
|
||||||
|
(window as any).WNW_CONFIG.onboardingCompleted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate('/appearance/pages');
|
||||||
|
} else {
|
||||||
|
throw new Error(json.message || 'Setup failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error(e.message || 'Something went wrong');
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const CurrentStepComponent = steps[step].component;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col items-center justify-center p-4 bg-background">
|
||||||
|
<div className="w-full max-w-4xl bg-card border border-border shadow-xl rounded-2xl overflow-hidden flex flex-col md:flex-row min-h-[600px]">
|
||||||
|
{/* Sidebar / Info Panel */}
|
||||||
|
<div className="bg-muted/30 p-8 md:w-1/3 border-b md:border-b-0 md:border-r border-border flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-8 text-primary">
|
||||||
|
<Rocket className="w-6 h-6" />
|
||||||
|
<span className="font-bold text-xl tracking-tight">WooNooW Setup</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{steps.map((s, i) => (
|
||||||
|
<div key={i} className={`flex items-center gap-3 ${i === step ? 'text-foreground' : 'text-muted-foreground'}`}>
|
||||||
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium border ${i < step ? 'bg-primary text-primary-foreground border-primary' :
|
||||||
|
i === step ? 'bg-background border-primary text-primary' :
|
||||||
|
'bg-muted border-border'
|
||||||
|
}`}>
|
||||||
|
{i < step ? <Check className="w-4 h-4" /> : i + 1}
|
||||||
|
</div>
|
||||||
|
<span className={i === step ? 'font-medium' : ''}>{s.title}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<p className="text-xs text-muted-foreground">step {step + 1} of {steps.length}</p>
|
||||||
|
<div className="mt-2">
|
||||||
|
<StepProgress currentStep={step} totalSteps={steps.length} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content Area */}
|
||||||
|
<div className="flex-1 p-8 flex flex-col">
|
||||||
|
<div className="flex-1">
|
||||||
|
<CurrentStepComponent
|
||||||
|
// Props mapping is dynamic but typed loosely here for simplicity
|
||||||
|
value={data.mode}
|
||||||
|
onChange={(val: string) => setData(d => ({ ...d, mode: val }))}
|
||||||
|
|
||||||
|
// Homepage props
|
||||||
|
pageId={data.pageId}
|
||||||
|
createMagicPage={data.createMagicPage}
|
||||||
|
onPageChange={(id: string | number) => setData(d => ({ ...d, pageId: String(id) }))}
|
||||||
|
onMagicChange={(enabled: boolean) => setData(d => ({ ...d, createMagicPage: enabled }))}
|
||||||
|
|
||||||
|
// Appearance props
|
||||||
|
containerWidth={data.containerWidth}
|
||||||
|
primaryColor={data.primaryColor}
|
||||||
|
onWidthChange={(w: string) => setData(d => ({ ...d, containerWidth: w }))}
|
||||||
|
onColorChange={(c: string) => setData(d => ({ ...d, primaryColor: c }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center mt-8 pt-4 border-t border-border">
|
||||||
|
<button
|
||||||
|
onClick={() => setStep(s => Math.max(0, s - 1))}
|
||||||
|
disabled={step === 0 || loading}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 rounded-md hover:bg-accent disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4" /> Back
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={loading}
|
||||||
|
className="flex items-center gap-2 px-6 py-2 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 font-medium transition-all shadow-sm"
|
||||||
|
>
|
||||||
|
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
|
||||||
|
{step === steps.length - 1 ? 'Launch Store' : 'Next'}
|
||||||
|
{!loading && step < steps.length - 1 && <ArrowRight className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Sparkles } from 'lucide-react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import { SettingsLayout } from './components/SettingsLayout';
|
import { SettingsLayout } from './components/SettingsLayout';
|
||||||
@@ -52,6 +54,7 @@ interface StoreSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function StoreDetailsPage() {
|
export default function StoreDetailsPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [settings, setSettings] = useState<StoreSettings>({
|
const [settings, setSettings] = useState<StoreSettings>({
|
||||||
storeName: '',
|
storeName: '',
|
||||||
@@ -233,6 +236,17 @@ export default function StoreDetailsPage() {
|
|||||||
description="Manage your store's basic information and regional settings"
|
description="Manage your store's basic information and regional settings"
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
action={
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => navigate('/setup')}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
Launch Setup Wizard
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* Store Overview */}
|
{/* Store Overview */}
|
||||||
<div className="bg-primary/10 border border-primary/20 rounded-lg p-4">
|
<div className="bg-primary/10 border border-primary/20 rounded-lg p-4">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WooNooW\Admin;
|
namespace WooNooW\Admin;
|
||||||
|
|
||||||
use WooNooW\Compat\MenuProvider;
|
use WooNooW\Compat\MenuProvider;
|
||||||
@@ -74,6 +75,7 @@ class Assets
|
|||||||
'pluginUrl' => trailingslashit(plugins_url('/', dirname(__DIR__))),
|
'pluginUrl' => trailingslashit(plugins_url('/', dirname(__DIR__))),
|
||||||
'storeUrl' => self::get_spa_url(),
|
'storeUrl' => self::get_spa_url(),
|
||||||
'customerSpaEnabled' => get_option('woonoow_customer_spa_enabled', false),
|
'customerSpaEnabled' => get_option('woonoow_customer_spa_enabled', false),
|
||||||
|
'onboardingCompleted' => get_option('woonoow_onboarding_completed', false),
|
||||||
]);
|
]);
|
||||||
wp_add_inline_script($handle, 'window.WNW_CONFIG = window.WNW_CONFIG || WNW_CONFIG;', 'after');
|
wp_add_inline_script($handle, 'window.WNW_CONFIG = window.WNW_CONFIG || WNW_CONFIG;', 'after');
|
||||||
|
|
||||||
@@ -199,6 +201,7 @@ class Assets
|
|||||||
'pluginUrl' => trailingslashit(plugins_url('/', dirname(__DIR__))),
|
'pluginUrl' => trailingslashit(plugins_url('/', dirname(__DIR__))),
|
||||||
'storeUrl' => self::get_spa_url(),
|
'storeUrl' => self::get_spa_url(),
|
||||||
'customerSpaEnabled' => get_option('woonoow_customer_spa_enabled', false),
|
'customerSpaEnabled' => get_option('woonoow_customer_spa_enabled', false),
|
||||||
|
'onboardingCompleted' => get_option('woonoow_onboarding_completed', false),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// WordPress REST API settings (for media upload compatibility)
|
// WordPress REST API settings (for media upload compatibility)
|
||||||
|
|||||||
172
includes/Api/OnboardingController.php
Normal file
172
includes/Api/OnboardingController.php
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Onboarding REST API Controller
|
||||||
|
*
|
||||||
|
* Handles the quick setup wizard endpoints.
|
||||||
|
*
|
||||||
|
* @package WooNooW
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace WooNooW\Api;
|
||||||
|
|
||||||
|
use WP_REST_Controller;
|
||||||
|
use WP_REST_Server;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use WP_Response;
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
class OnboardingController extends WP_REST_Controller
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Namespace
|
||||||
|
*/
|
||||||
|
protected $namespace = 'woonoow/v1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rest base
|
||||||
|
*/
|
||||||
|
protected $rest_base = 'onboarding';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register routes
|
||||||
|
*/
|
||||||
|
public function register_routes()
|
||||||
|
{
|
||||||
|
// GET /woonoow/v1/onboarding/status
|
||||||
|
register_rest_route($this->namespace, '/' . $this->rest_base . '/status', [
|
||||||
|
[
|
||||||
|
'methods' => WP_REST_Server::READABLE,
|
||||||
|
'callback' => [$this, 'get_status'],
|
||||||
|
'permission_callback' => [$this, 'check_permission'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// POST /woonoow/v1/onboarding/complete
|
||||||
|
register_rest_route($this->namespace, '/' . $this->rest_base . '/complete', [
|
||||||
|
[
|
||||||
|
'methods' => WP_REST_Server::EDITABLE,
|
||||||
|
'callback' => [$this, 'complete'],
|
||||||
|
'permission_callback' => [$this, 'check_permission'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get onboarding status
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Request object
|
||||||
|
* @return WP_REST_Response Response object
|
||||||
|
*/
|
||||||
|
public function get_status(WP_REST_Request $request)
|
||||||
|
{
|
||||||
|
$completed = get_option('woonoow_onboarding_completed', false);
|
||||||
|
return rest_ensure_response(['completed' => (bool) $completed]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete onboarding
|
||||||
|
*
|
||||||
|
* @param WP_REST_Request $request Request object
|
||||||
|
* @return WP_REST_Response|WP_Error Response object or error
|
||||||
|
*/
|
||||||
|
public function complete(WP_REST_Request $request)
|
||||||
|
{
|
||||||
|
$params = $request->get_json_params();
|
||||||
|
|
||||||
|
// 1. Save Mode
|
||||||
|
if (isset($params['mode'])) {
|
||||||
|
$mode = sanitize_text_field($params['mode']);
|
||||||
|
// If Immersive (full), enable SPA mode. Else disable or set accordingly.
|
||||||
|
// Assumption: 'spa_mode' option controls this.
|
||||||
|
// logic: 'full' -> woocommerce_spa_mode = 'yes'
|
||||||
|
// 'checkout_only' -> woocommerce_spa_mode = 'checkout_only'? (Checking implementation later, sticking to standard 'yes'/'no' for now or custom logic if strictly defined)
|
||||||
|
|
||||||
|
// Re-reading strategy: "Immersive (Full SPA)", "Classic", "Standard".
|
||||||
|
// Let's assume standard WP options for now.
|
||||||
|
// If 'full', set 'woonoow_spa_enabled' to 'yes'.
|
||||||
|
update_option('woonoow_spa_mode', $mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Handle Page Selection / Magic Creation
|
||||||
|
if (!empty($params['create_home_page']) && $params['create_home_page'] === true) {
|
||||||
|
$page_id = $this->create_magic_homepage();
|
||||||
|
if ($page_id) {
|
||||||
|
update_option('page_on_front', $page_id);
|
||||||
|
update_option('show_on_front', 'page');
|
||||||
|
// Set as SPA entry page
|
||||||
|
update_option('woonoow_spa_entry_page', $page_id);
|
||||||
|
}
|
||||||
|
} elseif (!empty($params['entry_page_id'])) {
|
||||||
|
$page_id = absint($params['entry_page_id']);
|
||||||
|
update_option('woonoow_spa_entry_page', $page_id);
|
||||||
|
// Optionally set as front page if requested? The user just selected "Where should customers land".
|
||||||
|
// Let's assume for the wizard flow, selecting it implies setting it as front page too for consistency.
|
||||||
|
update_option('page_on_front', $page_id);
|
||||||
|
update_option('show_on_front', 'page');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Appearance Settings
|
||||||
|
// Container Width
|
||||||
|
if (isset($params['container_width'])) {
|
||||||
|
update_option('woonoow_container_width', sanitize_text_field($params['container_width']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Colors / Theme
|
||||||
|
if (isset($params['primary_color'])) {
|
||||||
|
// Saving to AppearanceController's expected option
|
||||||
|
$appearance = get_option('woonoow_appearance_settings', []);
|
||||||
|
if (!is_array($appearance)) $appearance = [];
|
||||||
|
|
||||||
|
$appearance['colors'] = [
|
||||||
|
'primary' => sanitize_hex_color($params['primary_color']),
|
||||||
|
// defaults for others checking strategy... "Modern Black", "Blue", "Purple"
|
||||||
|
];
|
||||||
|
update_option('woonoow_appearance_settings', $appearance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Mark as Complete
|
||||||
|
update_option('woonoow_onboarding_completed', true);
|
||||||
|
|
||||||
|
return rest_ensure_response([
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Onboarding completed',
|
||||||
|
'redirect' => admin_url('admin.php?page=woonoow-builder'), // Or similar
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Programmatically create a homepage
|
||||||
|
*
|
||||||
|
* @return int|false Page ID or false
|
||||||
|
*/
|
||||||
|
private function create_magic_homepage()
|
||||||
|
{
|
||||||
|
$page_args = [
|
||||||
|
'post_type' => 'page',
|
||||||
|
'post_title' => __('Shop Home', 'woonoow'),
|
||||||
|
'post_content' => '<!-- wp:woonoow/hero -->...<!-- /wp:woonoow/hero -->', // Placeholder
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_author' => get_current_user_id(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$page_id = wp_insert_post($page_args);
|
||||||
|
|
||||||
|
if (is_wp_error($page_id)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $page_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check permission
|
||||||
|
*
|
||||||
|
* @return bool True if user has permission
|
||||||
|
*/
|
||||||
|
public function check_permission()
|
||||||
|
{
|
||||||
|
return current_user_can('manage_woocommerce');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WooNooW\Api;
|
namespace WooNooW\Api;
|
||||||
|
|
||||||
use WP_REST_Request;
|
use WP_REST_Request;
|
||||||
@@ -37,9 +38,12 @@ use WooNooW\Api\Controllers\SettingsController;
|
|||||||
use WooNooW\Api\Controllers\CartController as ApiCartController;
|
use WooNooW\Api\Controllers\CartController as ApiCartController;
|
||||||
use WooNooW\Admin\AppearanceController;
|
use WooNooW\Admin\AppearanceController;
|
||||||
use WooNooW\Api\PagesController;
|
use WooNooW\Api\PagesController;
|
||||||
|
use WooNooW\Api\OnboardingController;
|
||||||
|
|
||||||
class Routes {
|
class Routes
|
||||||
public static function init() {
|
{
|
||||||
|
public static function init()
|
||||||
|
{
|
||||||
// Initialize controllers (register action hooks)
|
// Initialize controllers (register action hooks)
|
||||||
OrdersController::init();
|
OrdersController::init();
|
||||||
AppearanceController::init();
|
AppearanceController::init();
|
||||||
@@ -179,6 +183,10 @@ class Routes {
|
|||||||
$docs_controller = new DocsController();
|
$docs_controller = new DocsController();
|
||||||
$docs_controller->register_routes();
|
$docs_controller->register_routes();
|
||||||
|
|
||||||
|
// Onboarding controller
|
||||||
|
$onboarding_controller = new OnboardingController();
|
||||||
|
$onboarding_controller->register_routes();
|
||||||
|
|
||||||
// Frontend controllers (customer-facing)
|
// Frontend controllers (customer-facing)
|
||||||
ShopController::register_routes();
|
ShopController::register_routes();
|
||||||
FrontendCartController::register_routes();
|
FrontendCartController::register_routes();
|
||||||
|
|||||||
Reference in New Issue
Block a user