feat: product page layout toggle (flat/card), fix email shortcode rendering
- Add layout_style setting (flat default) to product appearance
- AppearanceController: sanitize & persist layout_style, add to default settings
- Admin SPA: Layout Style select in Appearance > Product
- Customer SPA: useEffect targets <main> bg-white in flat mode (full-width),
card mode uses per-section white floating cards on gray background
- Accordion sections styled per mode: flat=border-t dividers, card=white cards
- Fix email shortcode gaps (EmailRenderer, EmailManager)
- Add missing variables: return_url, contact_url, account_url (alias),
payment_error_reason, order_items_list (alias for order_items_table)
- Fix customer_note extra_data key mismatch (note → customer_note)
- Pass low_stock_threshold via extra_data in low_stock email send
This commit is contained in:
@@ -6,6 +6,7 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { AlertCircle } from 'lucide-react';
|
import { AlertCircle } from 'lucide-react';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
@@ -31,6 +32,7 @@ export default function AppearanceGeneral() {
|
|||||||
const [customBody, setCustomBody] = useState('');
|
const [customBody, setCustomBody] = useState('');
|
||||||
const [fontScale, setFontScale] = useState([1.0]);
|
const [fontScale, setFontScale] = useState([1.0]);
|
||||||
const [containerWidth, setContainerWidth] = useState<'boxed' | 'fullwidth'>('boxed');
|
const [containerWidth, setContainerWidth] = useState<'boxed' | 'fullwidth'>('boxed');
|
||||||
|
const [hideAdminBar, setHideAdminBar] = useState(true);
|
||||||
|
|
||||||
const fontPairs = {
|
const fontPairs = {
|
||||||
modern: { name: 'Modern & Clean', fonts: 'Inter' },
|
modern: { name: 'Modern & Clean', fonts: 'Inter' },
|
||||||
@@ -70,6 +72,9 @@ export default function AppearanceGeneral() {
|
|||||||
if (general.container_width) {
|
if (general.container_width) {
|
||||||
setContainerWidth(general.container_width);
|
setContainerWidth(general.container_width);
|
||||||
}
|
}
|
||||||
|
if (general.hide_admin_bar !== undefined) {
|
||||||
|
setHideAdminBar(!!general.hide_admin_bar);
|
||||||
|
}
|
||||||
if (general.colors) {
|
if (general.colors) {
|
||||||
setColors({
|
setColors({
|
||||||
primary: general.colors.primary || '#1a1a1a',
|
primary: general.colors.primary || '#1a1a1a',
|
||||||
@@ -116,6 +121,7 @@ export default function AppearanceGeneral() {
|
|||||||
scale: fontScale[0],
|
scale: fontScale[0],
|
||||||
},
|
},
|
||||||
containerWidth,
|
containerWidth,
|
||||||
|
hideAdminBar,
|
||||||
colors,
|
colors,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -176,6 +182,28 @@ export default function AppearanceGeneral() {
|
|||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|
||||||
|
{/* Admin Bar */}
|
||||||
|
<SettingsCard
|
||||||
|
title="Admin Bar"
|
||||||
|
description="Control visibility of the WordPress admin bar for logged-in users"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label htmlFor="hide-admin-bar" className="font-medium cursor-pointer">
|
||||||
|
Hide Admin Bar on frontend
|
||||||
|
</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Hides the WordPress admin bar for all users when visiting your store
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
id="hide-admin-bar"
|
||||||
|
checked={hideAdminBar}
|
||||||
|
onCheckedChange={setHideAdminBar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsCard>
|
||||||
|
|
||||||
{/* SPA Page */}
|
{/* SPA Page */}
|
||||||
<SettingsCard
|
<SettingsCard
|
||||||
title="SPA Page"
|
title="SPA Page"
|
||||||
|
|||||||
@@ -81,7 +81,10 @@ export function CanvasSection({
|
|||||||
>
|
>
|
||||||
{/* Section content with Styles */}
|
{/* Section content with Styles */}
|
||||||
<div
|
<div
|
||||||
className={cn("relative overflow-hidden rounded-lg", !section.styles?.backgroundColor && !section.styles?.backgroundType && "bg-white/50")}
|
className={cn(
|
||||||
|
"relative overflow-hidden rounded-lg",
|
||||||
|
!section.styles?.backgroundColor && !section.styles?.backgroundType && "bg-white/50"
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
...(section.styles?.backgroundType === 'gradient'
|
...(section.styles?.backgroundType === 'gradient'
|
||||||
? { background: `linear-gradient(${section.styles?.gradientAngle ?? 135}deg, ${section.styles?.gradientFrom || '#9333ea'}, ${section.styles?.gradientTo || '#3b82f6'})` }
|
? { background: `linear-gradient(${section.styles?.gradientAngle ?? 135}deg, ${section.styles?.gradientFrom || '#9333ea'}, ${section.styles?.gradientTo || '#3b82f6'})` }
|
||||||
@@ -118,13 +121,50 @@ export function CanvasSection({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Dynamic background placeholder (Featured Image) */}
|
||||||
|
{section.styles?.backgroundType === 'image'
|
||||||
|
&& section.styles?.dynamicBackground === 'post_featured_image'
|
||||||
|
&& !section.styles?.backgroundImage && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-0"
|
||||||
|
style={{
|
||||||
|
backgroundColor: '#e2e8f0',
|
||||||
|
backgroundImage: 'repeating-linear-gradient(45deg, transparent, transparent 10px, rgba(148,163,184,0.15) 10px, rgba(148,163,184,0.15) 20px)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 z-0 flex items-center justify-center pointer-events-none">
|
||||||
|
<div className="flex flex-col items-center gap-1 text-gray-400">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M6.827 6.175A2.31 2.31 0 015.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 00-1.134-.175 2.31 2.31 0 01-1.64-1.055l-.822-1.316a2.192 2.192 0 00-1.736-1.039 48.774 48.774 0 00-5.232 0 2.192 2.192 0 00-1.736 1.039l-.821 1.316z" />
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M16.5 12.75a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0z" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-xs font-medium">Featured Image</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Overlay preview */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-0 bg-black"
|
||||||
|
style={{ opacity: (section.styles.backgroundOverlay || 0) / 100 }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Content Wrapper */}
|
{/* Content Wrapper */}
|
||||||
|
{section.styles?.contentWidth === 'boxed' ? (
|
||||||
|
<div className="relative z-10 container mx-auto px-4 max-w-5xl">
|
||||||
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"relative z-10",
|
"relative z-10",
|
||||||
section.styles?.contentWidth === 'contained' ? 'container mx-auto px-4 max-w-6xl' : 'w-full'
|
section.styles?.contentWidth === 'contained' ? 'container mx-auto px-4 max-w-6xl' : 'w-full'
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Floating Toolbar (Standard Interaction) */}
|
{/* Floating Toolbar (Standard Interaction) */}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { __ } from '@/lib/i18n';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -14,8 +13,9 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { FileText, Layout, Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
interface PageItem {
|
interface PageItem {
|
||||||
id?: number;
|
id?: number;
|
||||||
@@ -23,83 +23,119 @@ interface PageItem {
|
|||||||
cpt?: string;
|
cpt?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
has_template?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CreatePageModalProps {
|
interface CreatePageModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
|
cptList?: PageItem[];
|
||||||
onCreated: (page: PageItem) => void;
|
onCreated: (page: PageItem) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CreatePageModal({ open, onOpenChange, onCreated }: CreatePageModalProps) {
|
export function CreatePageModal({ open, onOpenChange, cptList = [], onCreated }: CreatePageModalProps) {
|
||||||
const [pageType, setPageType] = useState<'page' | 'template'>('page');
|
const [mode, setMode] = useState<'page' | 'template'>('page');
|
||||||
|
|
||||||
|
// Structural page state
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [slug, setSlug] = useState('');
|
const [slug, setSlug] = useState('');
|
||||||
const [selectedTemplateId, setSelectedTemplateId] = useState<string>('blank');
|
const [selectedTemplateId, setSelectedTemplateId] = useState<string>('blank');
|
||||||
|
|
||||||
|
// CPT template state
|
||||||
|
const [selectedCpt, setSelectedCpt] = useState<string>('');
|
||||||
|
const [selectedCptPreset, setSelectedCptPreset] = useState<string>('single-post');
|
||||||
|
|
||||||
// Prevent double submission
|
// Prevent double submission
|
||||||
const isSubmittingRef = useRef(false);
|
const isSubmittingRef = useRef(false);
|
||||||
|
|
||||||
// Get site URL from WordPress config
|
|
||||||
const siteUrl = window.WNW_CONFIG?.siteUrl?.replace(/\/$/, '') || window.location.origin;
|
const siteUrl = window.WNW_CONFIG?.siteUrl?.replace(/\/$/, '') || window.location.origin;
|
||||||
|
|
||||||
// Fetch templates
|
// Fetch template presets
|
||||||
const { data: templates = [] } = useQuery({
|
const { data: templates = [] } = useQuery({
|
||||||
queryKey: ['templates-presets'],
|
queryKey: ['templates-presets'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await api.get('/templates/presets');
|
const res = await api.get('/templates/presets');
|
||||||
return res as { id: string; label: string; description: string; icon: string }[];
|
return res as { id: string; label: string; description: string; icon: string; sections?: any }[];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create page mutation
|
// CPTs that don't have a template yet
|
||||||
const createMutation = useMutation({
|
const availableCpts = cptList.filter(p => p.type === 'template' && !p.has_template);
|
||||||
mutationFn: async (data: { title: string; slug: string; templateId?: string }) => {
|
|
||||||
// Guard against double submission
|
|
||||||
if (isSubmittingRef.current) {
|
|
||||||
throw new Error('Request already in progress');
|
|
||||||
}
|
|
||||||
isSubmittingRef.current = true;
|
|
||||||
|
|
||||||
|
// Set default CPT when list loads
|
||||||
|
useEffect(() => {
|
||||||
|
if (availableCpts.length > 0 && !selectedCpt) {
|
||||||
|
setSelectedCpt(availableCpts[0].cpt || '');
|
||||||
|
}
|
||||||
|
}, [availableCpts, selectedCpt]);
|
||||||
|
|
||||||
|
// Create structural page mutation
|
||||||
|
const createPageMutation = useMutation({
|
||||||
|
mutationFn: async (data: { title: string; slug: string; templateId: string }) => {
|
||||||
|
if (isSubmittingRef.current) throw new Error('Request already in progress');
|
||||||
|
isSubmittingRef.current = true;
|
||||||
try {
|
try {
|
||||||
// api.post returns JSON directly (not wrapped in { data: ... })
|
|
||||||
const response = await api.post('/pages', {
|
const response = await api.post('/pages', {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
slug: data.slug,
|
slug: data.slug,
|
||||||
templateId: data.templateId
|
templateId: data.templateId,
|
||||||
});
|
});
|
||||||
return response; // Return response directly, not response.data
|
return { type: 'page' as const, data: response };
|
||||||
} finally {
|
} finally {
|
||||||
// Reset after a delay to prevent race conditions
|
setTimeout(() => { isSubmittingRef.current = false; }, 500);
|
||||||
setTimeout(() => {
|
|
||||||
isSubmittingRef.current = false;
|
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (result) => {
|
||||||
if (data?.page) {
|
if (result?.data?.page) {
|
||||||
toast.success(__('Page created successfully'));
|
toast.success(__('Page created successfully'));
|
||||||
onCreated({
|
onCreated({
|
||||||
id: data.page.id,
|
id: result.data.page.id,
|
||||||
type: 'page',
|
type: result.type,
|
||||||
slug: data.page.slug,
|
slug: result.data.page.slug,
|
||||||
title: data.page.title,
|
title: result.data.page.title,
|
||||||
});
|
});
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
setTitle('');
|
|
||||||
setSlug('');
|
|
||||||
setSelectedTemplateId('blank');
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error: any) => {
|
onError: (error: any) => {
|
||||||
// Don't show error for duplicate prevention
|
if (error?.message === 'Request already in progress') return;
|
||||||
if (error?.message === 'Request already in progress') {
|
const message = error?.response?.data?.message || error?.message || __('Failed to create page');
|
||||||
return;
|
toast.error(message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create CPT template mutation
|
||||||
|
const createTemplateMutation = useMutation({
|
||||||
|
mutationFn: async (data: { cpt: string; presetId: string }) => {
|
||||||
|
if (isSubmittingRef.current) throw new Error('Request already in progress');
|
||||||
|
isSubmittingRef.current = true;
|
||||||
|
try {
|
||||||
|
// Get preset sections
|
||||||
|
const presets = templates as any[];
|
||||||
|
const preset = presets.find((t: any) => t.id === data.presetId);
|
||||||
|
const sections = preset?.sections || [];
|
||||||
|
|
||||||
|
const response = await api.post(`/templates/${data.cpt}`, { sections });
|
||||||
|
return { cpt: data.cpt, data: response };
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => { isSubmittingRef.current = false; }, 500);
|
||||||
}
|
}
|
||||||
// Extract error message from the response
|
},
|
||||||
const message = error?.response?.data?.message ||
|
onSuccess: (result) => {
|
||||||
error?.message ||
|
toast.success(__('Template created successfully'));
|
||||||
__('Failed to create page');
|
// Find the CPT item from the list to pass back
|
||||||
|
const cptItem = cptList.find(p => p.cpt === result.cpt);
|
||||||
|
onCreated({
|
||||||
|
type: 'template',
|
||||||
|
cpt: result.cpt,
|
||||||
|
title: cptItem?.title || `${result.cpt} Template`,
|
||||||
|
has_template: true,
|
||||||
|
});
|
||||||
|
onOpenChange(false);
|
||||||
|
},
|
||||||
|
onError: (error: any) => {
|
||||||
|
if (error?.message === 'Request already in progress') return;
|
||||||
|
const message = error?.response?.data?.message || error?.message || __('Failed to create template');
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -107,35 +143,48 @@ export function CreatePageModal({ open, onOpenChange, onCreated }: CreatePageMod
|
|||||||
// Auto-generate slug from title
|
// Auto-generate slug from title
|
||||||
const handleTitleChange = (value: string) => {
|
const handleTitleChange = (value: string) => {
|
||||||
setTitle(value);
|
setTitle(value);
|
||||||
// Auto-generate slug only if slug matches the previously auto-generated value
|
|
||||||
const autoSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
const autoSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
|
||||||
if (!slug || slug === autoSlug) {
|
if (!slug || slug === autoSlug) {
|
||||||
setSlug(value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''));
|
setSlug(value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const handleSubmit = () => {
|
|
||||||
if (createMutation.isPending || isSubmittingRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pageType === 'page' && title && slug) {
|
|
||||||
createMutation.mutate({ title, slug, templateId: selectedTemplateId });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reset form when modal closes
|
// Reset form when modal closes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
|
setMode('page');
|
||||||
setTitle('');
|
setTitle('');
|
||||||
setSlug('');
|
setSlug('');
|
||||||
setPageType('page');
|
|
||||||
setSelectedTemplateId('blank');
|
setSelectedTemplateId('blank');
|
||||||
|
setSelectedCpt('');
|
||||||
|
setSelectedCptPreset('single-post');
|
||||||
isSubmittingRef.current = false;
|
isSubmittingRef.current = false;
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
const isDisabled = pageType === 'page' && (!title || !slug) || createMutation.isPending || isSubmittingRef.current;
|
const handleSubmit = () => {
|
||||||
|
if (isSubmittingRef.current) return;
|
||||||
|
|
||||||
|
if (mode === 'page') {
|
||||||
|
if (title && slug) {
|
||||||
|
createPageMutation.mutate({ title, slug, templateId: selectedTemplateId });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (selectedCpt) {
|
||||||
|
createTemplateMutation.mutate({ cpt: selectedCpt, presetId: selectedCptPreset });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPending = createPageMutation.isPending || createTemplateMutation.isPending;
|
||||||
|
const isPageDisabled = !title || !slug || isPending;
|
||||||
|
const isTemplateDisabled = !selectedCpt || isPending;
|
||||||
|
const isDisabled = mode === 'page' ? isPageDisabled : isTemplateDisabled;
|
||||||
|
|
||||||
|
// Page layout presets (exclude single-post — it's for CPT)
|
||||||
|
const pagePresets = templates.filter((tpl: any) => tpl.id !== 'single-post');
|
||||||
|
// CPT presets (include ALL — user can pick any layout)
|
||||||
|
const cptPresets = templates as any[];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -147,42 +196,15 @@ export function CreatePageModal({ open, onOpenChange, onCreated }: CreatePageMod
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-6 px-6 py-4">
|
<div className="px-6 py-4">
|
||||||
{/* Page Type Selection */}
|
<Tabs value={mode} onValueChange={(v) => setMode(v as 'page' | 'template')}>
|
||||||
<RadioGroup value={pageType} onValueChange={(v) => setPageType(v as 'page' | 'template')} className="grid grid-cols-2 gap-4">
|
<TabsList className="w-full grid grid-cols-2 mb-6">
|
||||||
<div
|
<TabsTrigger value="page">{__('Structural Page')}</TabsTrigger>
|
||||||
className={`flex items-start space-x-3 p-4 border rounded-lg cursor-pointer transition-colors ${pageType === 'page' ? 'border-primary bg-primary/5 ring-1 ring-primary' : 'hover:bg-accent/50'}`}
|
<TabsTrigger value="template">{__('CPT Template')}</TabsTrigger>
|
||||||
onClick={() => setPageType('page')}
|
</TabsList>
|
||||||
>
|
|
||||||
<RadioGroupItem value="page" id="page" className="mt-1" />
|
|
||||||
<div className="flex-1">
|
|
||||||
<Label htmlFor="page" className="flex items-center gap-2 cursor-pointer font-medium">
|
|
||||||
<FileText className="w-4 h-4" />
|
|
||||||
{__('Structural Page')}
|
|
||||||
</Label>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
|
||||||
{__('Static content like About, Contact, Terms')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start space-x-3 p-4 border rounded-lg cursor-pointer opacity-50 relative">
|
{/* ── Structural Page Tab ── */}
|
||||||
<RadioGroupItem value="template" id="template" className="mt-1" disabled />
|
<TabsContent value="page" className="space-y-6 mt-0">
|
||||||
<div className="flex-1">
|
|
||||||
<Label htmlFor="template" className="flex items-center gap-2 cursor-pointer font-medium">
|
|
||||||
<Layout className="w-4 h-4" />
|
|
||||||
{__('CPT Template')}
|
|
||||||
</Label>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
|
||||||
{__('Templates are auto-created for each post type')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
|
|
||||||
{/* Page Details */}
|
|
||||||
{pageType === 'page' && (
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="title">{__('Page Title')}</Label>
|
<Label htmlFor="title">{__('Page Title')}</Label>
|
||||||
@@ -191,7 +213,7 @@ export function CreatePageModal({ open, onOpenChange, onCreated }: CreatePageMod
|
|||||||
value={title}
|
value={title}
|
||||||
onChange={(e) => handleTitleChange(e.target.value)}
|
onChange={(e) => handleTitleChange(e.target.value)}
|
||||||
placeholder={__('e.g., About Us')}
|
placeholder={__('e.g., About Us')}
|
||||||
disabled={createMutation.isPending}
|
disabled={isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -201,7 +223,7 @@ export function CreatePageModal({ open, onOpenChange, onCreated }: CreatePageMod
|
|||||||
value={slug}
|
value={slug}
|
||||||
onChange={(e) => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))}
|
onChange={(e) => setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))}
|
||||||
placeholder={__('e.g., about-us')}
|
placeholder={__('e.g., about-us')}
|
||||||
disabled={createMutation.isPending}
|
disabled={isPending}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground truncate">
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
<span className="font-mono text-primary">{siteUrl}/{slug || 'page'}</span>
|
<span className="font-mono text-primary">{siteUrl}/{slug || 'page'}</span>
|
||||||
@@ -210,9 +232,9 @@ export function CreatePageModal({ open, onOpenChange, onCreated }: CreatePageMod
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<Label>{__('Choose a Template')}</Label>
|
<Label>{__('Choose a Layout')}</Label>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
{templates.map((tpl) => (
|
{pagePresets.map((tpl: any) => (
|
||||||
<div
|
<div
|
||||||
key={tpl.id}
|
key={tpl.id}
|
||||||
className={`
|
className={`
|
||||||
@@ -221,40 +243,80 @@ export function CreatePageModal({ open, onOpenChange, onCreated }: CreatePageMod
|
|||||||
`}
|
`}
|
||||||
onClick={() => setSelectedTemplateId(tpl.id)}
|
onClick={() => setSelectedTemplateId(tpl.id)}
|
||||||
>
|
>
|
||||||
<div className="mb-2 font-medium text-sm flex items-center gap-2">
|
<div className="mb-2 font-medium text-sm">{tpl.label}</div>
|
||||||
{tpl.label}
|
<p className="text-xs text-muted-foreground line-clamp-2">{tpl.description}</p>
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground line-clamp-2">
|
|
||||||
{tpl.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{templates.length === 0 && (
|
|
||||||
<div className="col-span-4 text-center py-4 text-muted-foreground text-sm">
|
|
||||||
{__('Loading templates...')}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* ── CPT Template Tab ── */}
|
||||||
|
<TabsContent value="template" className="space-y-6 mt-0">
|
||||||
|
{availableCpts.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-muted-foreground text-sm space-y-2">
|
||||||
|
<p className="font-medium">{__('All post types already have a template.')}</p>
|
||||||
|
<p className="text-xs">{__('Abort an existing template first to create a new one.')}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>{__('Select Post Type')}</Label>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
|
{availableCpts.map((cpt) => (
|
||||||
|
<div
|
||||||
|
key={cpt.cpt}
|
||||||
|
className={`
|
||||||
|
p-3 border rounded-lg cursor-pointer transition-all hover:bg-accent
|
||||||
|
${selectedCpt === cpt.cpt ? 'border-primary ring-1 ring-primary bg-primary/5' : 'border-border'}
|
||||||
|
`}
|
||||||
|
onClick={() => setSelectedCpt(cpt.cpt || '')}
|
||||||
|
>
|
||||||
|
<div className="font-medium text-sm">{cpt.title}</div>
|
||||||
|
{cpt.cpt && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1 font-mono">/{cpt.cpt}/</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label>{__('Starting Layout')}</Label>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
|
{cptPresets.map((tpl: any) => (
|
||||||
|
<div
|
||||||
|
key={tpl.id}
|
||||||
|
className={`
|
||||||
|
p-3 border rounded-lg cursor-pointer transition-all hover:bg-accent
|
||||||
|
${selectedCptPreset === tpl.id ? 'border-primary ring-1 ring-primary bg-primary/5' : 'border-border'}
|
||||||
|
`}
|
||||||
|
onClick={() => setSelectedCptPreset(tpl.id)}
|
||||||
|
>
|
||||||
|
<div className="font-medium text-sm">{tpl.label}</div>
|
||||||
|
<p className="text-xs text-muted-foreground line-clamp-2 mt-1">{tpl.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={createMutation.isPending}>
|
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={isPending}>
|
||||||
{__('Cancel')}
|
{__('Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button onClick={handleSubmit} disabled={isDisabled}>
|
||||||
onClick={handleSubmit}
|
{isPending ? (
|
||||||
disabled={isDisabled}
|
|
||||||
>
|
|
||||||
{createMutation.isPending ? (
|
|
||||||
<>
|
<>
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
{__('Creating...')}
|
{__('Creating...')}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
__('Create Page')
|
mode === 'page' ? __('Create Page') : __('Create Template')
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ export function InspectorField({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelectChange = (val: string) => {
|
||||||
|
onChange({ type: 'dynamic', source: val });
|
||||||
|
};
|
||||||
|
|
||||||
const handleTypeToggle = (dynamic: boolean) => {
|
const handleTypeToggle = (dynamic: boolean) => {
|
||||||
if (dynamic) {
|
if (dynamic) {
|
||||||
onChange({ type: 'dynamic', source: availableSources[0]?.value || 'post_title' });
|
onChange({ type: 'dynamic', source: availableSources[0]?.value || 'post_title' });
|
||||||
@@ -85,7 +89,8 @@ export function InspectorField({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isDynamic && supportsDynamic ? (
|
{isDynamic && supportsDynamic ? (
|
||||||
<Select value={currentValue} onValueChange={handleValueChange}>
|
<div className="space-y-2">
|
||||||
|
<Select value={currentValue} onValueChange={handleSelectChange}>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select data source" />
|
<SelectValue placeholder="Select data source" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -97,6 +102,7 @@ export function InspectorField({
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
</div>
|
||||||
) : fieldType === 'rte' ? (
|
) : fieldType === 'rte' ? (
|
||||||
<RichTextEditor
|
<RichTextEditor
|
||||||
content={currentValue}
|
content={currentValue}
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ interface InspectorPanelProps {
|
|||||||
onSetAsSpaLanding?: () => void;
|
onSetAsSpaLanding?: () => void;
|
||||||
onUnsetSpaLanding?: () => void;
|
onUnsetSpaLanding?: () => void;
|
||||||
onDeletePage?: () => void;
|
onDeletePage?: () => void;
|
||||||
|
onDeleteTemplate?: () => void;
|
||||||
onContainerWidthChange?: (width: 'boxed' | 'fullwidth') => void;
|
onContainerWidthChange?: (width: 'boxed' | 'fullwidth') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +128,6 @@ const COLOR_SCHEMES = [
|
|||||||
{ value: 'primary', label: 'Primary' },
|
{ value: 'primary', label: 'Primary' },
|
||||||
{ value: 'secondary', label: 'Secondary' },
|
{ value: 'secondary', label: 'Secondary' },
|
||||||
{ value: 'muted', label: 'Muted' },
|
{ value: 'muted', label: 'Muted' },
|
||||||
{ value: 'gradient', label: 'Gradient' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const STYLABLE_ELEMENTS: Record<string, { name: string; label: string; type: 'text' | 'image' }[]> = {
|
const STYLABLE_ELEMENTS: Record<string, { name: string; label: string; type: 'text' | 'image' }[]> = {
|
||||||
@@ -183,6 +183,7 @@ export function InspectorPanel({
|
|||||||
onSetAsSpaLanding,
|
onSetAsSpaLanding,
|
||||||
onUnsetSpaLanding,
|
onUnsetSpaLanding,
|
||||||
onDeletePage,
|
onDeletePage,
|
||||||
|
onDeleteTemplate,
|
||||||
onContainerWidthChange,
|
onContainerWidthChange,
|
||||||
}: InspectorPanelProps) {
|
}: InspectorPanelProps) {
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
@@ -306,6 +307,25 @@ export function InspectorPanel({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Danger Zone - Templates */}
|
||||||
|
{isTemplate && page && onDeleteTemplate && (
|
||||||
|
<div className="pt-2 border-t mt-2">
|
||||||
|
<Label className="text-xs text-red-600 uppercase tracking-wider block mb-2">{__('Danger Zone')}</Label>
|
||||||
|
<p className="text-xs text-gray-500 mb-3">
|
||||||
|
{__('Deleting this template will disable SPA rendering for this post type. WordPress will handle it natively.')}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-full justify-start text-red-600 hover:text-red-700 hover:bg-red-50 border-red-200"
|
||||||
|
onClick={onDeleteTemplate}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
|
{__('Abort SPA Template')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-blue-50 text-blue-800 p-3 rounded text-xs leading-relaxed">
|
<div className="bg-blue-50 text-blue-800 p-3 rounded text-xs leading-relaxed">
|
||||||
{__('Select any section on the canvas to edit its content and design.')}
|
{__('Select any section on the canvas to edit its content and design.')}
|
||||||
@@ -433,21 +453,32 @@ export function InspectorPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Feature Grid Repeater */}
|
{/* Feature Grid Repeater */}
|
||||||
{selectedSection.type === 'feature-grid' && (
|
{selectedSection.type === 'feature-grid' && (() => {
|
||||||
|
const featuresProp = selectedSection.props.features;
|
||||||
|
const isDynamicFeatures = featuresProp?.type === 'dynamic' && !!featuresProp?.source;
|
||||||
|
const items = Array.isArray(featuresProp?.value) ? featuresProp.value : [];
|
||||||
|
return (
|
||||||
<div className="pt-4 border-t">
|
<div className="pt-4 border-t">
|
||||||
<InspectorRepeater
|
<InspectorRepeater
|
||||||
label={__('Features')}
|
label={__('Features')}
|
||||||
items={Array.isArray(selectedSection.props.features?.value) ? selectedSection.props.features.value : []}
|
items={items}
|
||||||
onChange={(items) => onSectionPropChange('features', { type: 'static', value: items })}
|
onChange={(newItems) => onSectionPropChange('features', { type: 'static', value: newItems })}
|
||||||
fields={[
|
fields={[
|
||||||
{ name: 'title', label: 'Title', type: 'text' },
|
{ name: 'title', label: 'Title', type: 'text' },
|
||||||
{ name: 'description', label: 'Description', type: 'textarea' },
|
{ name: 'description', label: 'Description', type: 'textarea' },
|
||||||
{ name: 'icon', label: 'Icon', type: 'icon' },
|
{ name: 'icon', label: 'Icon', type: 'icon' },
|
||||||
]}
|
]}
|
||||||
itemLabelKey="title"
|
itemLabelKey="title"
|
||||||
|
isDynamic={isDynamicFeatures}
|
||||||
|
dynamicLabel={
|
||||||
|
isDynamicFeatures
|
||||||
|
? `⚡ Auto-populated from "${featuresProp.source}" at runtime`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
})()}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* Design Tab */}
|
{/* Design Tab */}
|
||||||
@@ -571,10 +602,42 @@ export function InspectorPanel({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Image Background */}
|
{/* Image Background */}
|
||||||
{selectedSection.styles?.backgroundType === 'image' && (
|
{selectedSection.styles?.backgroundType === 'image' && (() => {
|
||||||
|
const isDynamicBg = selectedSection.styles?.dynamicBackground === 'post_featured_image';
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Source toggle: Upload vs Featured Image */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="text-xs">{__('Background Image')}</Label>
|
<Label className="text-xs">{__('Background Image')}</Label>
|
||||||
|
<div className="flex gap-1 p-0.5 bg-gray-100 rounded-md">
|
||||||
|
<button
|
||||||
|
onClick={() => onSectionStylesChange({ dynamicBackground: undefined })}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 text-xs py-1.5 px-2 rounded transition-colors',
|
||||||
|
!isDynamicBg
|
||||||
|
? 'bg-white shadow-sm font-medium text-gray-900'
|
||||||
|
: 'text-gray-500 hover:text-gray-700'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Upload Image
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onSectionStylesChange({ dynamicBackground: 'post_featured_image', backgroundImage: '' })}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 text-xs py-1.5 px-2 rounded transition-colors',
|
||||||
|
isDynamicBg
|
||||||
|
? 'bg-white shadow-sm font-medium text-gray-900'
|
||||||
|
: 'text-gray-500 hover:text-gray-700'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Featured Image
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Static upload */}
|
||||||
|
{!isDynamicBg && (
|
||||||
|
<div className="space-y-2">
|
||||||
<MediaUploader type="image" onSelect={(url) => onSectionStylesChange({ backgroundImage: url })}>
|
<MediaUploader type="image" onSelect={(url) => onSectionStylesChange({ backgroundImage: url })}>
|
||||||
{selectedSection.styles?.backgroundImage ? (
|
{selectedSection.styles?.backgroundImage ? (
|
||||||
<div className="relative group cursor-pointer border rounded overflow-hidden h-24 bg-gray-50">
|
<div className="relative group cursor-pointer border rounded overflow-hidden h-24 bg-gray-50">
|
||||||
@@ -597,6 +660,15 @@ export function InspectorPanel({
|
|||||||
)}
|
)}
|
||||||
</MediaUploader>
|
</MediaUploader>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Dynamic source info */}
|
||||||
|
{isDynamicBg && (
|
||||||
|
<div className="flex items-start gap-2 text-xs bg-blue-50 border border-blue-200 rounded-md p-2.5 text-blue-700">
|
||||||
|
<span className="mt-0.5">⚡</span>
|
||||||
|
<span>At runtime, the background will use this post's featured image. Falls back to no background if no featured image is set.</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-1 pt-2">
|
<div className="space-y-1 pt-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -612,7 +684,8 @@ export function InspectorPanel({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Spacing Controls */}
|
{/* Spacing Controls */}
|
||||||
<div className="grid grid-cols-2 gap-2 pt-2 border-t mt-4">
|
<div className="grid grid-cols-2 gap-2 pt-2 border-t mt-4">
|
||||||
@@ -643,7 +716,7 @@ export function InspectorPanel({
|
|||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={selectedSection.styles?.contentWidth || 'full'}
|
value={selectedSection.styles?.contentWidth || 'full'}
|
||||||
onValueChange={(val: any) => onSectionStylesChange({ contentWidth: val })}
|
onValueChange={(val: any) => onSectionStylesChange({ contentWidth: val })}
|
||||||
className="flex gap-4"
|
className="flex flex-wrap gap-4"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<RadioGroupItem value="full" id="width-full" />
|
<RadioGroupItem value="full" id="width-full" />
|
||||||
@@ -653,6 +726,10 @@ export function InspectorPanel({
|
|||||||
<RadioGroupItem value="contained" id="width-contained" />
|
<RadioGroupItem value="contained" id="width-contained" />
|
||||||
<Label htmlFor="width-contained" className="text-sm font-normal cursor-pointer">{__('Contained')}</Label>
|
<Label htmlFor="width-contained" className="text-sm font-normal cursor-pointer">{__('Contained')}</Label>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="boxed" id="width-boxed" />
|
||||||
|
<Label htmlFor="width-boxed" className="text-sm font-normal cursor-pointer">{__('Boxed (Card)')}</Label>
|
||||||
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ interface InspectorRepeaterProps {
|
|||||||
fields: RepeaterFieldDef[];
|
fields: RepeaterFieldDef[];
|
||||||
onChange: (items: any[]) => void;
|
onChange: (items: any[]) => void;
|
||||||
itemLabelKey?: string; // Key to use for the accordion header (e.g., 'title')
|
itemLabelKey?: string; // Key to use for the accordion header (e.g., 'title')
|
||||||
|
isDynamic?: boolean; // If true, items come from a dynamic source — hide Add Item
|
||||||
|
dynamicLabel?: string; // Custom label for the dynamic placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sortable Item Component
|
// Sortable Item Component
|
||||||
@@ -148,7 +150,7 @@ function SortableItem({ id, item, index, fields, itemLabelKey, onChange, onDelet
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InspectorRepeater({ label, items = [], fields, onChange, itemLabelKey = 'title' }: InspectorRepeaterProps) {
|
export function InspectorRepeater({ label, items = [], fields, onChange, itemLabelKey = 'title', isDynamic = false, dynamicLabel }: InspectorRepeaterProps) {
|
||||||
// Generate simple stable IDs for sorting if items don't have them
|
// Generate simple stable IDs for sorting if items don't have them
|
||||||
const itemIds = items.map((_, i) => `item-${i}`);
|
const itemIds = items.map((_, i) => `item-${i}`);
|
||||||
|
|
||||||
@@ -191,10 +193,12 @@ export function InspectorRepeater({ label, items = [], fields, onChange, itemLab
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-xs font-semibold text-gray-500 uppercase tracking-wider">{label}</Label>
|
<Label className="text-xs font-semibold text-gray-500 uppercase tracking-wider">{label}</Label>
|
||||||
|
{!isDynamic && (
|
||||||
<Button variant="outline" size="sm" className="h-6 text-xs" onClick={handleAddItem}>
|
<Button variant="outline" size="sm" className="h-6 text-xs" onClick={handleAddItem}>
|
||||||
<Plus className="w-3 h-3 mr-1" />
|
<Plus className="w-3 h-3 mr-1" />
|
||||||
Add Item
|
Add Item
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Accordion type="single" collapsible className="w-full">
|
<Accordion type="single" collapsible className="w-full">
|
||||||
@@ -224,8 +228,15 @@ export function InspectorRepeater({ label, items = [], fields, onChange, itemLab
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
{items.length === 0 && (
|
{items.length === 0 && (
|
||||||
<div className="text-xs text-gray-400 text-center py-4 border border-dashed rounded-md bg-gray-50">
|
<div className={cn(
|
||||||
No items yet. Click "Add Item" to start.
|
"text-xs text-center py-4 border rounded-md",
|
||||||
|
isDynamic
|
||||||
|
? "text-blue-600 border-blue-200 bg-blue-50"
|
||||||
|
: "text-gray-400 border-dashed bg-gray-50"
|
||||||
|
)}>
|
||||||
|
{isDynamic
|
||||||
|
? (dynamicLabel || '⚡ Auto-populated from related posts at runtime')
|
||||||
|
: 'No items yet. Click "Add Item" to start.'}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ interface PageSidebarProps {
|
|||||||
|
|
||||||
export function PageSidebar({ pages, selectedPage, onSelectPage, isLoading }: PageSidebarProps) {
|
export function PageSidebar({ pages, selectedPage, onSelectPage, isLoading }: PageSidebarProps) {
|
||||||
const structuralPages = pages.filter(p => p.type === 'page');
|
const structuralPages = pages.filter(p => p.type === 'page');
|
||||||
const templates = pages.filter(p => p.type === 'template');
|
const templates = pages.filter(p => p.type === 'template' && p.has_template);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -69,7 +69,10 @@ export function PageSidebar({ pages, selectedPage, onSelectPage, isLoading }: Pa
|
|||||||
{__('Templates')}
|
{__('Templates')}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{templates.map((template) => (
|
{templates.length === 0 ? (
|
||||||
|
<p className="text-sm text-gray-400 italic">{__('No templates yet')}</p>
|
||||||
|
) : (
|
||||||
|
templates.map((template) => (
|
||||||
<button
|
<button
|
||||||
key={`template-${template.cpt}`}
|
key={`template-${template.cpt}`}
|
||||||
onClick={() => onSelectPage(template)}
|
onClick={() => onSelectPage(template)}
|
||||||
@@ -86,7 +89,8 @@ export function PageSidebar({ pages, selectedPage, onSelectPage, isLoading }: Pa
|
|||||||
<span className="text-xs text-gray-400">{template.permalink_base}*</span>
|
<span className="text-xs text-gray-400">{template.permalink_base}*</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface Section {
|
|||||||
colorScheme?: string;
|
colorScheme?: string;
|
||||||
props: Record<string, { type: 'static' | 'dynamic'; value?: string; source?: string }>;
|
props: Record<string, { type: 'static' | 'dynamic'; value?: string; source?: string }>;
|
||||||
elementStyles?: Record<string, any>;
|
elementStyles?: Record<string, any>;
|
||||||
|
styles?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CTABannerRendererProps {
|
interface CTABannerRendererProps {
|
||||||
@@ -21,17 +22,26 @@ const COLOR_SCHEMES: Record<string, { bg: string; text: string; btnBg: string; b
|
|||||||
primary: { bg: 'bg-blue-600', text: 'text-white', btnBg: 'bg-white', btnText: 'text-blue-600' },
|
primary: { bg: 'bg-blue-600', text: 'text-white', btnBg: 'bg-white', btnText: 'text-blue-600' },
|
||||||
secondary: { bg: 'bg-gray-800', text: 'text-white', btnBg: 'bg-white', btnText: 'text-gray-800' },
|
secondary: { bg: 'bg-gray-800', text: 'text-white', btnBg: 'bg-white', btnText: 'text-gray-800' },
|
||||||
muted: { bg: 'bg-gray-50', text: 'text-gray-700', btnBg: 'bg-gray-800', btnText: 'text-white' },
|
muted: { bg: 'bg-gray-50', text: 'text-gray-700', btnBg: 'bg-gray-800', btnText: 'text-white' },
|
||||||
gradient: { bg: 'bg-gradient-to-r from-purple-600 to-blue-500', text: 'text-white', btnBg: 'bg-white', btnText: 'text-purple-600' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CTABannerRenderer({ section, className }: CTABannerRendererProps) {
|
export function CTABannerRenderer({ section, className }: CTABannerRendererProps) {
|
||||||
const scheme = COLOR_SCHEMES[section.colorScheme || 'primary'];
|
const scheme = COLOR_SCHEMES[section.colorScheme || 'primary'] ?? COLOR_SCHEMES['primary'];
|
||||||
|
|
||||||
const title = section.props?.title?.value || 'Ready to get started?';
|
const title = section.props?.title?.value || 'Ready to get started?';
|
||||||
const text = section.props?.text?.value || 'Join thousands of happy customers today.';
|
const text = section.props?.text?.value || 'Join thousands of happy customers today.';
|
||||||
const buttonText = section.props?.button_text?.value || 'Get Started';
|
const buttonText = section.props?.button_text?.value || 'Get Started';
|
||||||
const buttonUrl = section.props?.button_url?.value || '#';
|
const buttonUrl = section.props?.button_url?.value || '#';
|
||||||
|
|
||||||
|
const heightMap: Record<string, string> = {
|
||||||
|
'default': 'py-12 md:py-20',
|
||||||
|
'small': 'py-8 md:py-12',
|
||||||
|
'medium': 'py-16 md:py-24',
|
||||||
|
'large': 'py-24 md:py-36',
|
||||||
|
'fullscreen': 'min-h-[50vh] flex flex-col justify-center',
|
||||||
|
};
|
||||||
|
const customPadding = section.styles?.paddingTop || section.styles?.paddingBottom;
|
||||||
|
const heightClasses = customPadding ? '' : (heightMap[section.styles?.heightPreset || 'default'] || 'py-12 md:py-20');
|
||||||
|
|
||||||
// Helper to get text styles (including font family)
|
// Helper to get text styles (including font family)
|
||||||
const getTextStyles = (elementName: string) => {
|
const getTextStyles = (elementName: string) => {
|
||||||
const styles = section.elementStyles?.[elementName] || {};
|
const styles = section.elementStyles?.[elementName] || {};
|
||||||
@@ -56,8 +66,10 @@ export function CTABannerRenderer({ section, className }: CTABannerRendererProps
|
|||||||
const textStyle = getTextStyles('text');
|
const textStyle = getTextStyles('text');
|
||||||
const btnStyle = getTextStyles('button_text');
|
const btnStyle = getTextStyles('button_text');
|
||||||
|
|
||||||
|
const hasCustomBackground = !!section.styles?.backgroundColor || !!section.styles?.backgroundImage || section.styles?.backgroundType === 'gradient';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('py-12 px-4 md:py-20 md:px-8', scheme.bg, scheme.text, className)}>
|
<div className={cn(heightClasses, 'px-4 md:px-8', !hasCustomBackground && scheme.bg, scheme.text, className)}>
|
||||||
<div className="max-w-4xl mx-auto text-center space-y-6">
|
<div className="max-w-4xl mx-auto text-center space-y-6">
|
||||||
<h2
|
<h2
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface Section {
|
|||||||
colorScheme?: string;
|
colorScheme?: string;
|
||||||
props: Record<string, { type: 'static' | 'dynamic'; value?: string; source?: string }>;
|
props: Record<string, { type: 'static' | 'dynamic'; value?: string; source?: string }>;
|
||||||
elementStyles?: Record<string, any>;
|
elementStyles?: Record<string, any>;
|
||||||
|
styles?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ContactFormRendererProps {
|
interface ContactFormRendererProps {
|
||||||
@@ -21,11 +22,10 @@ const COLOR_SCHEMES: Record<string, { bg: string; text: string; inputBg: string;
|
|||||||
primary: { bg: 'wn-primary-bg', text: 'text-white', inputBg: 'bg-white', btnBg: 'bg-white' },
|
primary: { bg: 'wn-primary-bg', text: 'text-white', inputBg: 'bg-white', btnBg: 'bg-white' },
|
||||||
secondary: { bg: 'wn-secondary-bg', text: 'text-white', inputBg: 'bg-gray-700', btnBg: 'bg-blue-500' },
|
secondary: { bg: 'wn-secondary-bg', text: 'text-white', inputBg: 'bg-gray-700', btnBg: 'bg-blue-500' },
|
||||||
muted: { bg: 'bg-gray-50', text: 'text-gray-700', inputBg: 'bg-white', btnBg: 'bg-gray-800' },
|
muted: { bg: 'bg-gray-50', text: 'text-gray-700', inputBg: 'bg-white', btnBg: 'bg-gray-800' },
|
||||||
gradient: { bg: 'wn-gradient-bg', text: 'text-white', inputBg: 'bg-white/90', btnBg: 'bg-white' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ContactFormRenderer({ section, className }: ContactFormRendererProps) {
|
export function ContactFormRenderer({ section, className }: ContactFormRendererProps) {
|
||||||
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'];
|
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'] ?? COLOR_SCHEMES['default'];
|
||||||
|
|
||||||
const title = section.props?.title?.value || 'Contact Us';
|
const title = section.props?.title?.value || 'Contact Us';
|
||||||
|
|
||||||
@@ -69,10 +69,22 @@ export function ContactFormRenderer({ section, className }: ContactFormRendererP
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const heightMap: Record<string, string> = {
|
||||||
|
'default': 'py-12 md:py-20',
|
||||||
|
'small': 'py-8 md:py-12',
|
||||||
|
'medium': 'py-16 md:py-24',
|
||||||
|
'large': 'py-24 md:py-36',
|
||||||
|
'fullscreen': 'min-h-[50vh] flex flex-col justify-center',
|
||||||
|
};
|
||||||
|
const customPadding = section.styles?.paddingTop || section.styles?.paddingBottom;
|
||||||
|
const heightClasses = customPadding ? '' : (heightMap[section.styles?.heightPreset || 'default'] || 'py-12 md:py-20');
|
||||||
|
|
||||||
|
const hasCustomBackground = !!section.styles?.backgroundColor || !!section.styles?.backgroundImage || section.styles?.backgroundType === 'gradient';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('py-12 px-4 md:py-20 md:px-8', !scheme.bg.startsWith('wn-') && scheme.bg, scheme.text, className)}
|
className={cn(heightClasses, 'px-4 md:px-8', !hasCustomBackground && !scheme.bg.startsWith('wn-') && scheme.bg, scheme.text, className)}
|
||||||
style={getBackgroundStyle()}
|
style={hasCustomBackground ? {} : getBackgroundStyle()}
|
||||||
>
|
>
|
||||||
<div className="max-w-xl mx-auto">
|
<div className="max-w-xl mx-auto">
|
||||||
<h2
|
<h2
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ const COLOR_SCHEMES: Record<string, { bg: string; text: string }> = {
|
|||||||
primary: { bg: 'wn-primary-bg', text: 'text-white' },
|
primary: { bg: 'wn-primary-bg', text: 'text-white' },
|
||||||
secondary: { bg: 'wn-secondary-bg', text: 'text-white' },
|
secondary: { bg: 'wn-secondary-bg', text: 'text-white' },
|
||||||
muted: { bg: 'bg-gray-50', text: 'text-gray-700' },
|
muted: { bg: 'bg-gray-50', text: 'text-gray-700' },
|
||||||
gradient: { bg: 'wn-gradient-bg', text: 'text-white' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const WIDTH_CLASSES: Record<string, string> = {
|
const WIDTH_CLASSES: Record<string, string> = {
|
||||||
@@ -152,7 +151,7 @@ const generateScopedStyles = (sectionId: string, elementStyles: Record<string, a
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ContentRenderer({ section, className }: ContentRendererProps) {
|
export function ContentRenderer({ section, className }: ContentRendererProps) {
|
||||||
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'];
|
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'] ?? COLOR_SCHEMES['default'];
|
||||||
const layout = section.layoutVariant || 'default';
|
const layout = section.layoutVariant || 'default';
|
||||||
const widthClass = WIDTH_CLASSES[layout] || WIDTH_CLASSES.default;
|
const widthClass = WIDTH_CLASSES[layout] || WIDTH_CLASSES.default;
|
||||||
|
|
||||||
@@ -211,17 +210,20 @@ export function ContentRenderer({ section, className }: ContentRendererProps) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasCustomBackground = !!section.styles?.backgroundColor || !!section.styles?.backgroundImage || section.styles?.backgroundType === 'gradient';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={`section-${section.id}`}
|
id={`section-${section.id}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
'relative w-full overflow-hidden',
|
||||||
'px-4 md:px-8',
|
'px-4 md:px-8',
|
||||||
heightClasses,
|
heightClasses,
|
||||||
!scheme.bg.startsWith('wn-') && scheme.bg,
|
!hasCustomBackground && !scheme.bg.startsWith('wn-') && scheme.bg,
|
||||||
scheme.text,
|
scheme.text,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
style={getBackgroundStyle()}
|
style={hasCustomBackground ? {} : getBackgroundStyle()}
|
||||||
>
|
>
|
||||||
<style dangerouslySetInnerHTML={{ __html: generateScopedStyles(section.id, section.elementStyles || {}) }} />
|
<style dangerouslySetInnerHTML={{ __html: generateScopedStyles(section.id, section.elementStyles || {}) }} />
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as LucideIcons from 'lucide-react';
|
import * as LucideIcons from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Image, Calendar, User } from 'lucide-react';
|
||||||
|
|
||||||
interface Section {
|
interface Section {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -8,6 +9,7 @@ interface Section {
|
|||||||
colorScheme?: string;
|
colorScheme?: string;
|
||||||
props: Record<string, any>;
|
props: Record<string, any>;
|
||||||
elementStyles?: Record<string, any>;
|
elementStyles?: Record<string, any>;
|
||||||
|
styles?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FeatureGridRendererProps {
|
interface FeatureGridRendererProps {
|
||||||
@@ -20,7 +22,6 @@ const COLOR_SCHEMES: Record<string, { bg: string; text: string; cardBg: string }
|
|||||||
primary: { bg: 'wn-primary-bg', text: 'text-white', cardBg: 'bg-white/10' },
|
primary: { bg: 'wn-primary-bg', text: 'text-white', cardBg: 'bg-white/10' },
|
||||||
secondary: { bg: 'wn-secondary-bg', text: 'text-white', cardBg: 'bg-white/10' },
|
secondary: { bg: 'wn-secondary-bg', text: 'text-white', cardBg: 'bg-white/10' },
|
||||||
muted: { bg: 'bg-gray-50', text: 'text-gray-700', cardBg: 'bg-white' },
|
muted: { bg: 'bg-gray-50', text: 'text-gray-700', cardBg: 'bg-white' },
|
||||||
gradient: { bg: 'wn-gradient-bg', text: 'text-white', cardBg: 'bg-white/10' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const GRID_CLASSES: Record<string, string> = {
|
const GRID_CLASSES: Record<string, string> = {
|
||||||
@@ -29,20 +30,57 @@ const GRID_CLASSES: Record<string, string> = {
|
|||||||
'grid-4': 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
|
'grid-4': 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Default features for demo
|
// Default features for static demo
|
||||||
const DEFAULT_FEATURES = [
|
const DEFAULT_FEATURES = [
|
||||||
{ title: 'Fast Delivery', description: 'Quick shipping to your doorstep', icon: 'Truck' },
|
{ title: 'Fast Delivery', description: 'Quick shipping to your doorstep', icon: 'Truck' },
|
||||||
{ title: 'Secure Payment', description: 'Your data is always protected', icon: 'Shield' },
|
{ title: 'Secure Payment', description: 'Your data is always protected', icon: 'Shield' },
|
||||||
{ title: 'Quality Products', description: 'Only the best for our customers', icon: 'Star' },
|
{ title: 'Quality Products', description: 'Only the best for our customers', icon: 'Star' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Placeholder post-card skeleton shown when features are dynamic (related_posts)
|
||||||
|
function PostCardPlaceholder({ index, cardBg }: { index: number; cardBg: string }) {
|
||||||
|
const widths = ['w-3/4', 'w-2/3', 'w-4/5'];
|
||||||
|
const titleWidth = widths[index % widths.length];
|
||||||
|
return (
|
||||||
|
<div className={cn('rounded-xl overflow-hidden', cardBg, 'shadow-sm border border-dashed border-gray-300')}>
|
||||||
|
{/* Thumbnail placeholder */}
|
||||||
|
<div className="aspect-[16/9] bg-gray-200 flex items-center justify-center">
|
||||||
|
<Image className="w-8 h-8 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<div className="p-4 space-y-2">
|
||||||
|
{/* Meta row */}
|
||||||
|
<div className="flex items-center gap-3 text-xs text-gray-400">
|
||||||
|
<span className="flex items-center gap-1"><Calendar className="w-3 h-3" /> Jan 1, 2025</span>
|
||||||
|
<span className="flex items-center gap-1"><User className="w-3 h-3" /> Author</span>
|
||||||
|
</div>
|
||||||
|
{/* Title skeleton */}
|
||||||
|
<div className={cn('h-4 bg-gray-300 rounded animate-pulse', titleWidth)} />
|
||||||
|
{/* Excerpt skeleton */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="h-3 bg-gray-200 rounded animate-pulse w-full" />
|
||||||
|
<div className="h-3 bg-gray-200 rounded animate-pulse w-5/6" />
|
||||||
|
</div>
|
||||||
|
{/* "Read more" chip */}
|
||||||
|
<div className="pt-1">
|
||||||
|
<div className="inline-block h-3 w-16 bg-blue-200 rounded animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function FeatureGridRenderer({ section, className }: FeatureGridRendererProps) {
|
export function FeatureGridRenderer({ section, className }: FeatureGridRendererProps) {
|
||||||
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'];
|
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'] ?? COLOR_SCHEMES['default'];
|
||||||
const layout = section.layoutVariant || 'grid-3';
|
const layout = section.layoutVariant || 'grid-3';
|
||||||
const gridClass = GRID_CLASSES[layout] || GRID_CLASSES['grid-3'];
|
const gridClass = GRID_CLASSES[layout] || GRID_CLASSES['grid-3'];
|
||||||
|
|
||||||
const heading = section.props?.heading?.value || 'Our Features';
|
const heading = section.props?.heading?.value || 'Our Features';
|
||||||
const features = section.props?.features?.value || DEFAULT_FEATURES;
|
const featuresProp = section.props?.features;
|
||||||
|
const isDynamic = featuresProp?.type === 'dynamic' && !!featuresProp?.source;
|
||||||
|
const features = isDynamic ? [] : (featuresProp?.value || DEFAULT_FEATURES);
|
||||||
|
|
||||||
|
// Determine how many placeholder post-cards to show (match grid columns)
|
||||||
|
const placeholderCount = layout === 'grid-4' ? 4 : layout === 'grid-2' ? 2 : 3;
|
||||||
|
|
||||||
// Helper to get text styles (including font family)
|
// Helper to get text styles (including font family)
|
||||||
const getTextStyles = (elementName: string) => {
|
const getTextStyles = (elementName: string) => {
|
||||||
@@ -81,10 +119,22 @@ export function FeatureGridRenderer({ section, className }: FeatureGridRendererP
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const heightMap: Record<string, string> = {
|
||||||
|
'default': 'py-12 md:py-20',
|
||||||
|
'small': 'py-8 md:py-12',
|
||||||
|
'medium': 'py-16 md:py-24',
|
||||||
|
'large': 'py-24 md:py-36',
|
||||||
|
'fullscreen': 'min-h-[50vh] flex flex-col justify-center',
|
||||||
|
};
|
||||||
|
const customPadding = section.styles?.paddingTop || section.styles?.paddingBottom;
|
||||||
|
const heightClasses = customPadding ? '' : (heightMap[section.styles?.heightPreset || 'default'] || 'py-12 md:py-20');
|
||||||
|
|
||||||
|
const hasCustomBackground = !!section.styles?.backgroundColor || !!section.styles?.backgroundImage || section.styles?.backgroundType === 'gradient';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('py-12 px-4 md:py-20 md:px-8', !scheme.bg.startsWith('wn-') && scheme.bg, scheme.text, className)}
|
className={cn(heightClasses, 'px-4 md:px-8', !hasCustomBackground && !scheme.bg.startsWith('wn-') && scheme.bg, scheme.text, className)}
|
||||||
style={getBackgroundStyle()}
|
style={hasCustomBackground ? {} : getBackgroundStyle()}
|
||||||
>
|
>
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
{heading && (
|
{heading && (
|
||||||
@@ -99,11 +149,18 @@ export function FeatureGridRenderer({ section, className }: FeatureGridRendererP
|
|||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Dynamic (related posts) — show post-card skeleton placeholders */}
|
||||||
|
{isDynamic ? (
|
||||||
|
<div className={cn('grid gap-8', gridClass)}>
|
||||||
|
{Array.from({ length: placeholderCount }).map((_, i) => (
|
||||||
|
<PostCardPlaceholder key={i} index={i} cardBg={scheme.cardBg} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Static items — regular icon feature cards */
|
||||||
<div className={cn('grid gap-8', gridClass)}>
|
<div className={cn('grid gap-8', gridClass)}>
|
||||||
{(Array.isArray(features) ? features : DEFAULT_FEATURES).map((feature: any, index: number) => {
|
{(Array.isArray(features) ? features : DEFAULT_FEATURES).map((feature: any, index: number) => {
|
||||||
// Resolve icon from name, fallback to Star
|
|
||||||
const IconComponent = (LucideIcons as any)[feature.icon] || LucideIcons.Star;
|
const IconComponent = (LucideIcons as any)[feature.icon] || LucideIcons.Star;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
@@ -139,7 +196,9 @@ export function FeatureGridRenderer({ section, className }: FeatureGridRendererP
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface Section {
|
|||||||
colorScheme?: string;
|
colorScheme?: string;
|
||||||
props: Record<string, { type: 'static' | 'dynamic'; value?: string; source?: string }>;
|
props: Record<string, { type: 'static' | 'dynamic'; value?: string; source?: string }>;
|
||||||
elementStyles?: Record<string, any>;
|
elementStyles?: Record<string, any>;
|
||||||
|
styles?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HeroRendererProps {
|
interface HeroRendererProps {
|
||||||
@@ -20,13 +21,22 @@ const COLOR_SCHEMES: Record<string, { bg: string; text: string }> = {
|
|||||||
primary: { bg: 'wn-primary-bg', text: 'text-white' },
|
primary: { bg: 'wn-primary-bg', text: 'text-white' },
|
||||||
secondary: { bg: 'wn-secondary-bg', text: 'text-white' },
|
secondary: { bg: 'wn-secondary-bg', text: 'text-white' },
|
||||||
muted: { bg: 'bg-gray-50', text: 'text-gray-700' },
|
muted: { bg: 'bg-gray-50', text: 'text-gray-700' },
|
||||||
gradient: { bg: 'wn-gradient-bg', text: 'text-white' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function HeroRenderer({ section, className }: HeroRendererProps) {
|
export function HeroRenderer({ section, className }: HeroRendererProps) {
|
||||||
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'];
|
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'] ?? COLOR_SCHEMES['default'];
|
||||||
const layout = section.layoutVariant || 'default';
|
const layout = section.layoutVariant || 'default';
|
||||||
|
|
||||||
|
const heightMap: Record<string, string> = {
|
||||||
|
'default': 'py-12 md:py-20',
|
||||||
|
'small': 'py-8 md:py-12',
|
||||||
|
'medium': 'py-16 md:py-24',
|
||||||
|
'large': 'py-24 md:py-36',
|
||||||
|
'fullscreen': 'min-h-[50vh] flex flex-col justify-center',
|
||||||
|
};
|
||||||
|
const customPadding = section.styles?.paddingTop || section.styles?.paddingBottom;
|
||||||
|
const heightClasses = customPadding ? '' : (heightMap[section.styles?.heightPreset || 'default'] || 'py-12 md:py-20');
|
||||||
|
|
||||||
const title = section.props?.title?.value || 'Hero Title';
|
const title = section.props?.title?.value || 'Hero Title';
|
||||||
const subtitle = section.props?.subtitle?.value || 'Your amazing subtitle here';
|
const subtitle = section.props?.subtitle?.value || 'Your amazing subtitle here';
|
||||||
const image = section.props?.image?.value;
|
const image = section.props?.image?.value;
|
||||||
@@ -66,12 +76,12 @@ export function HeroRenderer({ section, className }: HeroRendererProps) {
|
|||||||
// Helper for image styles
|
// Helper for image styles
|
||||||
const imageStyle = section.elementStyles?.['image'] || {};
|
const imageStyle = section.elementStyles?.['image'] || {};
|
||||||
|
|
||||||
|
const hasCustomBackground = !!section.styles?.backgroundColor || !!section.styles?.backgroundImage || section.styles?.backgroundType === 'gradient';
|
||||||
|
|
||||||
if (layout === 'hero-left-image' || layout === 'hero-right-image') {
|
if (layout === 'hero-left-image' || layout === 'hero-right-image') {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('w-full', !scheme.bg.startsWith('wn-') && scheme.bg, scheme.text, className)}
|
className={cn(heightClasses, 'px-4 md:px-8', !hasCustomBackground && !scheme.bg.startsWith('wn-') && scheme.bg, scheme.text, className)}
|
||||||
>
|
>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'max-w-6xl mx-auto flex items-center gap-12',
|
'max-w-6xl mx-auto flex items-center gap-12',
|
||||||
@@ -146,7 +156,7 @@ export function HeroRenderer({ section, className }: HeroRendererProps) {
|
|||||||
// Default centered layout
|
// Default centered layout
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('w-full text-center', !scheme.bg.startsWith('wn-') && scheme.bg, scheme.text, className)}
|
className={cn(heightClasses, 'px-4 md:px-8 text-center', !hasCustomBackground && !scheme.bg.startsWith('wn-') && scheme.bg, scheme.text, className)}
|
||||||
>
|
>
|
||||||
<div className="max-w-4xl mx-auto space-y-6">
|
<div className="max-w-4xl mx-auto space-y-6">
|
||||||
<h1
|
<h1
|
||||||
|
|||||||
@@ -14,11 +14,10 @@ const COLOR_SCHEMES: Record<string, { bg: string; text: string }> = {
|
|||||||
primary: { bg: 'wn-primary-bg', text: 'text-white' },
|
primary: { bg: 'wn-primary-bg', text: 'text-white' },
|
||||||
secondary: { bg: 'wn-secondary-bg', text: 'text-white' },
|
secondary: { bg: 'wn-secondary-bg', text: 'text-white' },
|
||||||
muted: { bg: 'bg-gray-50', text: 'text-gray-700' },
|
muted: { bg: 'bg-gray-50', text: 'text-gray-700' },
|
||||||
gradient: { bg: 'wn-gradient-bg', text: 'text-white' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ImageTextRenderer({ section, className }: ImageTextRendererProps) {
|
export function ImageTextRenderer({ section, className }: ImageTextRendererProps) {
|
||||||
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'];
|
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'] ?? COLOR_SCHEMES['default'];
|
||||||
const layout = section.layoutVariant || 'image-left';
|
const layout = section.layoutVariant || 'image-left';
|
||||||
const isImageRight = layout === 'image-right';
|
const isImageRight = layout === 'image-right';
|
||||||
|
|
||||||
@@ -73,10 +72,22 @@ export function ImageTextRenderer({ section, className }: ImageTextRendererProps
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const heightMap: Record<string, string> = {
|
||||||
|
'default': 'py-12 md:py-20',
|
||||||
|
'small': 'py-8 md:py-12',
|
||||||
|
'medium': 'py-16 md:py-24',
|
||||||
|
'large': 'py-24 md:py-36',
|
||||||
|
'fullscreen': 'min-h-[50vh] flex flex-col justify-center',
|
||||||
|
};
|
||||||
|
const customPadding = section.styles?.paddingTop || section.styles?.paddingBottom;
|
||||||
|
const heightClasses = customPadding ? '' : (heightMap[section.styles?.heightPreset || 'default'] || 'py-12 md:py-20');
|
||||||
|
|
||||||
|
const hasCustomBackground = !!section.styles?.backgroundColor || !!section.styles?.backgroundImage || section.styles?.backgroundType === 'gradient';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('py-12 px-4 md:py-20 md:px-8', !scheme.bg.startsWith('wn-') && scheme.bg, scheme.text, className)}
|
className={cn(heightClasses, 'px-4 md:px-8', !hasCustomBackground && !scheme.bg.startsWith('wn-') && scheme.bg, scheme.text, className)}
|
||||||
style={getBackgroundStyle()}
|
style={hasCustomBackground ? {} : getBackgroundStyle()}
|
||||||
>
|
>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'max-w-6xl mx-auto flex items-center gap-12',
|
'max-w-6xl mx-auto flex items-center gap-12',
|
||||||
|
|||||||
@@ -145,6 +145,22 @@ export default function AppearancePages() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Delete template mutation (abort SPA for this CPT)
|
||||||
|
const deleteTemplateMutation = useMutation({
|
||||||
|
mutationFn: async (cpt: string) => {
|
||||||
|
return api.del(`/templates/${cpt}`);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(__('CPT template deleted. WordPress will handle this post type natively.'));
|
||||||
|
markAsSaved();
|
||||||
|
setCurrentPage(null);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['pages'] });
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error(__('Failed to delete template'));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Set as SPA Landing mutation
|
// Set as SPA Landing mutation
|
||||||
const setSpaLandingMutation = useMutation({
|
const setSpaLandingMutation = useMutation({
|
||||||
mutationFn: async (id: number) => {
|
mutationFn: async (id: number) => {
|
||||||
@@ -212,6 +228,14 @@ export default function AppearancePages() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteTemplate = () => {
|
||||||
|
if (!currentPage || currentPage.type !== 'template' || !currentPage.cpt) return;
|
||||||
|
|
||||||
|
if (confirm(__('Are you sure? This will delete the SPA template and WordPress will handle this post type natively. This cannot be undone.'))) {
|
||||||
|
deleteTemplateMutation.mutate(currentPage.cpt);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={
|
<div className={
|
||||||
cn(
|
cn(
|
||||||
@@ -358,6 +382,7 @@ export default function AppearancePages() {
|
|||||||
}}
|
}}
|
||||||
onUnsetSpaLanding={() => unsetSpaLandingMutation.mutate()}
|
onUnsetSpaLanding={() => unsetSpaLandingMutation.mutate()}
|
||||||
onDeletePage={handleDeletePage}
|
onDeletePage={handleDeletePage}
|
||||||
|
onDeleteTemplate={handleDeleteTemplate}
|
||||||
onContainerWidthChange={(width) => {
|
onContainerWidthChange={(width) => {
|
||||||
if (currentPage) {
|
if (currentPage) {
|
||||||
setCurrentPage({ ...currentPage, containerWidth: width });
|
setCurrentPage({ ...currentPage, containerWidth: width });
|
||||||
@@ -373,6 +398,7 @@ export default function AppearancePages() {
|
|||||||
< CreatePageModal
|
< CreatePageModal
|
||||||
open={showCreateModal}
|
open={showCreateModal}
|
||||||
onOpenChange={setShowCreateModal}
|
onOpenChange={setShowCreateModal}
|
||||||
|
cptList={pages.filter((p: PageItem) => p.type === 'template')}
|
||||||
onCreated={(newPage) => {
|
onCreated={(newPage) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['pages'] });
|
queryClient.invalidateQueries({ queryKey: ['pages'] });
|
||||||
setCurrentPage(newPage);
|
setCurrentPage(newPage);
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ export interface SectionStyles {
|
|||||||
backgroundOverlay?: number; // 0-100 opacity
|
backgroundOverlay?: number; // 0-100 opacity
|
||||||
paddingTop?: string;
|
paddingTop?: string;
|
||||||
paddingBottom?: string;
|
paddingBottom?: string;
|
||||||
contentWidth?: 'full' | 'contained';
|
contentWidth?: 'full' | 'contained' | 'boxed';
|
||||||
heightPreset?: string;
|
heightPreset?: string;
|
||||||
|
dynamicBackground?: string; // e.g. 'post_featured_image'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ElementStyle {
|
export interface ElementStyle {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export default function AppearanceProduct() {
|
|||||||
const [imagePosition, setImagePosition] = useState('left');
|
const [imagePosition, setImagePosition] = useState('left');
|
||||||
const [galleryStyle, setGalleryStyle] = useState('thumbnails');
|
const [galleryStyle, setGalleryStyle] = useState('thumbnails');
|
||||||
const [stickyAddToCart, setStickyAddToCart] = useState(false);
|
const [stickyAddToCart, setStickyAddToCart] = useState(false);
|
||||||
|
const [layoutStyle, setLayoutStyle] = useState('flat');
|
||||||
|
|
||||||
const [elements, setElements] = useState({
|
const [elements, setElements] = useState({
|
||||||
breadcrumbs: true,
|
breadcrumbs: true,
|
||||||
@@ -40,6 +41,7 @@ export default function AppearanceProduct() {
|
|||||||
if (product.layout.image_position) setImagePosition(product.layout.image_position);
|
if (product.layout.image_position) setImagePosition(product.layout.image_position);
|
||||||
if (product.layout.gallery_style) setGalleryStyle(product.layout.gallery_style);
|
if (product.layout.gallery_style) setGalleryStyle(product.layout.gallery_style);
|
||||||
if (product.layout.sticky_add_to_cart !== undefined) setStickyAddToCart(product.layout.sticky_add_to_cart);
|
if (product.layout.sticky_add_to_cart !== undefined) setStickyAddToCart(product.layout.sticky_add_to_cart);
|
||||||
|
if (product.layout.layout_style) setLayoutStyle(product.layout.layout_style);
|
||||||
}
|
}
|
||||||
if (product.elements) {
|
if (product.elements) {
|
||||||
setElements({
|
setElements({
|
||||||
@@ -80,7 +82,8 @@ export default function AppearanceProduct() {
|
|||||||
layout: {
|
layout: {
|
||||||
image_position: imagePosition,
|
image_position: imagePosition,
|
||||||
gallery_style: galleryStyle,
|
gallery_style: galleryStyle,
|
||||||
sticky_add_to_cart: stickyAddToCart
|
sticky_add_to_cart: stickyAddToCart,
|
||||||
|
layout_style: layoutStyle,
|
||||||
},
|
},
|
||||||
elements,
|
elements,
|
||||||
related_products: {
|
related_products: {
|
||||||
@@ -106,6 +109,23 @@ export default function AppearanceProduct() {
|
|||||||
title="Layout"
|
title="Layout"
|
||||||
description="Configure product page layout and gallery"
|
description="Configure product page layout and gallery"
|
||||||
>
|
>
|
||||||
|
<SettingsSection label="Layout Style" htmlFor="layout-style">
|
||||||
|
<Select value={layoutStyle} onValueChange={setLayoutStyle}>
|
||||||
|
<SelectTrigger id="layout-style">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="flat">Flat — content floats on page background</SelectItem>
|
||||||
|
<SelectItem value="card">Card — content inside a white elevated card</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-sm text-muted-foreground mt-2">
|
||||||
|
{layoutStyle === 'flat'
|
||||||
|
? 'Clean, minimal look. Product sections blend with the page background.'
|
||||||
|
: 'Each product section is wrapped in a white card, elevated from the background.'}
|
||||||
|
</p>
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection label="Image Position" htmlFor="image-position">
|
<SettingsSection label="Image Position" htmlFor="image-position">
|
||||||
<Select value={imagePosition} onValueChange={setImagePosition}>
|
<Select value={imagePosition} onValueChange={setImagePosition}>
|
||||||
<SelectTrigger id="image-position">
|
<SelectTrigger id="image-position">
|
||||||
|
|||||||
100
customer-spa/package-lock.json
generated
100
customer-spa/package-lock.json
generated
@@ -22,6 +22,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tanstack/react-query": "^5.90.5",
|
"@tanstack/react-query": "^5.90.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -62,7 +63,6 @@
|
|||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||||
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@@ -1057,7 +1057,6 @@
|
|||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
@@ -1079,7 +1078,6 @@
|
|||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
@@ -1089,14 +1087,12 @@
|
|||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.31",
|
"version": "0.3.31",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/resolve-uri": "^3.1.0",
|
"@jridgewell/resolve-uri": "^3.1.0",
|
||||||
@@ -1107,7 +1103,6 @@
|
|||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.stat": "2.0.5",
|
"@nodelib/fs.stat": "2.0.5",
|
||||||
@@ -1121,7 +1116,6 @@
|
|||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
|
||||||
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
@@ -1131,7 +1125,6 @@
|
|||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
|
||||||
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.scandir": "2.1.5",
|
"@nodelib/fs.scandir": "2.1.5",
|
||||||
@@ -2660,6 +2653,31 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/typography": {
|
||||||
|
"version": "0.5.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
|
||||||
|
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"postcss-selector-parser": "6.0.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
||||||
|
"version": "6.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||||
|
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cssesc": "^3.0.0",
|
||||||
|
"util-deprecate": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tanstack/query-core": {
|
"node_modules/@tanstack/query-core": {
|
||||||
"version": "5.90.10",
|
"version": "5.90.10",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.10.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.10.tgz",
|
||||||
@@ -3099,14 +3117,12 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||||
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/anymatch": {
|
"node_modules/anymatch": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
@@ -3120,7 +3136,6 @@
|
|||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
@@ -3365,7 +3380,6 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -3388,7 +3402,6 @@
|
|||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fill-range": "^7.1.1"
|
"fill-range": "^7.1.1"
|
||||||
@@ -3495,7 +3508,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
|
||||||
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -3543,7 +3555,6 @@
|
|||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"anymatch": "~3.1.2",
|
"anymatch": "~3.1.2",
|
||||||
@@ -3568,7 +3579,6 @@
|
|||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.1"
|
"is-glob": "^4.0.1"
|
||||||
@@ -3638,7 +3648,6 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -3686,7 +3695,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"cssesc": "bin/cssesc"
|
"cssesc": "bin/cssesc"
|
||||||
@@ -3827,14 +3835,12 @@
|
|||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/dlv": {
|
"node_modules/dlv": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
|
||||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/doctrine": {
|
"node_modules/doctrine": {
|
||||||
@@ -4433,7 +4439,6 @@
|
|||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||||
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
"integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nodelib/fs.stat": "^2.0.2",
|
"@nodelib/fs.stat": "^2.0.2",
|
||||||
@@ -4450,7 +4455,6 @@
|
|||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.1"
|
"is-glob": "^4.0.1"
|
||||||
@@ -4477,7 +4481,6 @@
|
|||||||
"version": "1.19.1",
|
"version": "1.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
|
||||||
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
|
"integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"reusify": "^1.0.4"
|
"reusify": "^1.0.4"
|
||||||
@@ -4500,7 +4503,6 @@
|
|||||||
"version": "7.1.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
@@ -4581,7 +4583,6 @@
|
|||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
"dev": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -4596,7 +4597,6 @@
|
|||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@@ -4723,7 +4723,6 @@
|
|||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-glob": "^4.0.3"
|
"is-glob": "^4.0.3"
|
||||||
@@ -4867,7 +4866,6 @@
|
|||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
@@ -5012,7 +5010,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"binary-extensions": "^2.0.0"
|
"binary-extensions": "^2.0.0"
|
||||||
@@ -5055,7 +5052,6 @@
|
|||||||
"version": "2.16.1",
|
"version": "2.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hasown": "^2.0.2"
|
"hasown": "^2.0.2"
|
||||||
@@ -5106,7 +5102,6 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -5152,7 +5147,6 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-extglob": "^2.1.1"
|
"is-extglob": "^2.1.1"
|
||||||
@@ -5191,7 +5185,6 @@
|
|||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
@@ -5395,7 +5388,6 @@
|
|||||||
"version": "1.21.7",
|
"version": "1.21.7",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
@@ -5511,7 +5503,6 @@
|
|||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||||
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
@@ -5524,7 +5515,6 @@
|
|||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
@@ -5595,7 +5585,6 @@
|
|||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
@@ -5605,7 +5594,6 @@
|
|||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"braces": "^3.0.3",
|
"braces": "^3.0.3",
|
||||||
@@ -5642,7 +5630,6 @@
|
|||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
|
||||||
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"any-promise": "^1.0.0",
|
"any-promise": "^1.0.0",
|
||||||
@@ -5654,7 +5641,6 @@
|
|||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -5687,7 +5673,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -5707,7 +5692,6 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -5717,7 +5701,6 @@
|
|||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
|
||||||
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -5926,21 +5909,18 @@
|
|||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
@@ -5953,7 +5933,6 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
|
||||||
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -5963,7 +5942,6 @@
|
|||||||
"version": "4.0.7",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
||||||
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
|
"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -5983,7 +5961,6 @@
|
|||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -6012,7 +5989,6 @@
|
|||||||
"version": "15.1.0",
|
"version": "15.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
|
||||||
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"postcss-value-parser": "^4.0.0",
|
"postcss-value-parser": "^4.0.0",
|
||||||
@@ -6030,7 +6006,6 @@
|
|||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-core-module": "^2.16.1",
|
"is-core-module": "^2.16.1",
|
||||||
@@ -6051,7 +6026,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
|
||||||
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
|
"integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -6077,7 +6051,6 @@
|
|||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
|
||||||
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
|
"integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -6120,7 +6093,6 @@
|
|||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
|
||||||
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -6146,7 +6118,6 @@
|
|||||||
"version": "6.1.2",
|
"version": "6.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||||
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssesc": "^3.0.0",
|
"cssesc": "^3.0.0",
|
||||||
@@ -6160,7 +6131,6 @@
|
|||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
@@ -6199,7 +6169,6 @@
|
|||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -6405,7 +6374,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pify": "^2.3.0"
|
"pify": "^2.3.0"
|
||||||
@@ -6415,7 +6383,6 @@
|
|||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"picomatch": "^2.2.1"
|
"picomatch": "^2.2.1"
|
||||||
@@ -6500,7 +6467,6 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||||
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
"integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"iojs": ">=1.0.0",
|
"iojs": ">=1.0.0",
|
||||||
@@ -6553,7 +6519,6 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -6824,7 +6789,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -6959,7 +6923,6 @@
|
|||||||
"version": "3.35.1",
|
"version": "3.35.1",
|
||||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
|
||||||
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
|
"integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.2",
|
"@jridgewell/gen-mapping": "^0.3.2",
|
||||||
@@ -6995,7 +6958,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -7018,7 +6980,6 @@
|
|||||||
"version": "3.4.18",
|
"version": "3.4.18",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz",
|
||||||
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
|
"integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
@@ -7066,7 +7027,6 @@
|
|||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-core-module": "^2.16.1",
|
"is-core-module": "^2.16.1",
|
||||||
@@ -7087,7 +7047,6 @@
|
|||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||||
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"any-promise": "^1.0.0"
|
"any-promise": "^1.0.0"
|
||||||
@@ -7097,7 +7056,6 @@
|
|||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
|
||||||
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"thenify": ">= 3.1.0 < 4"
|
"thenify": ">= 3.1.0 < 4"
|
||||||
@@ -7110,7 +7068,6 @@
|
|||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -7127,7 +7084,6 @@
|
|||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
@@ -7145,7 +7101,6 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
@@ -7158,7 +7113,6 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-number": "^7.0.0"
|
"is-number": "^7.0.0"
|
||||||
@@ -7184,7 +7138,6 @@
|
|||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
|
||||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
@@ -7421,7 +7374,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vaul": {
|
"node_modules/vaul": {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tanstack/react-query": "^5.90.5",
|
"@tanstack/react-query": "^5.90.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import Login from './pages/Login';
|
|||||||
import ForgotPassword from './pages/ForgotPassword';
|
import ForgotPassword from './pages/ForgotPassword';
|
||||||
import ResetPassword from './pages/ResetPassword';
|
import ResetPassword from './pages/ResetPassword';
|
||||||
import OrderPay from './pages/OrderPay';
|
import OrderPay from './pages/OrderPay';
|
||||||
|
import Subscribe from './pages/Subscribe';
|
||||||
import { DynamicPageRenderer } from './pages/DynamicPage';
|
import { DynamicPageRenderer } from './pages/DynamicPage';
|
||||||
|
|
||||||
// Create QueryClient instance
|
// Create QueryClient instance
|
||||||
@@ -116,6 +117,9 @@ function AppRoutes() {
|
|||||||
{/* Wishlist - Public route accessible to guests */}
|
{/* Wishlist - Public route accessible to guests */}
|
||||||
<Route path="/wishlist" element={<Wishlist />} />
|
<Route path="/wishlist" element={<Wishlist />} />
|
||||||
|
|
||||||
|
{/* Newsletter / Notifications */}
|
||||||
|
<Route path="/subscribe" element={<Subscribe />} />
|
||||||
|
|
||||||
{/* Login & Auth */}
|
{/* Login & Auth */}
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface SharedContentProps {
|
|||||||
imagePosition?: 'left' | 'right' | 'top' | 'bottom';
|
imagePosition?: 'left' | 'right' | 'top' | 'bottom';
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
containerWidth?: 'full' | 'contained';
|
containerWidth?: 'full' | 'contained' | 'boxed';
|
||||||
|
|
||||||
// Styles
|
// Styles
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -53,15 +53,19 @@ export const SharedContentLayout: React.FC<SharedContentProps> = ({
|
|||||||
const isImageTop = imagePosition === 'top';
|
const isImageTop = imagePosition === 'top';
|
||||||
const isImageBottom = imagePosition === 'bottom';
|
const isImageBottom = imagePosition === 'bottom';
|
||||||
|
|
||||||
// Wrapper classes
|
// Wrapper classes — full = edge-to-edge, contained = narrow readable column, boxed = card at max-w-5xl
|
||||||
const containerClasses = cn(
|
const containerClasses = cn(
|
||||||
'w-full mx-auto px-4 sm:px-6 lg:px-8',
|
'w-full mx-auto px-4 sm:px-6 lg:px-8',
|
||||||
containerWidth === 'contained' ? 'max-w-7xl' : ''
|
containerWidth === 'contained' ? 'max-w-4xl'
|
||||||
|
: containerWidth === 'boxed' ? 'max-w-5xl'
|
||||||
|
: '' // full = no max-width cap
|
||||||
);
|
);
|
||||||
|
|
||||||
const gridClasses = cn(
|
const gridClasses = cn(
|
||||||
'mx-auto',
|
'mx-auto',
|
||||||
hasImage && (isImageLeft || isImageRight) ? 'grid grid-cols-1 lg:grid-cols-2 gap-12 items-center' : 'max-w-4xl'
|
hasImage && (isImageLeft || isImageRight)
|
||||||
|
? 'grid grid-cols-1 lg:grid-cols-2 gap-12 items-center'
|
||||||
|
: containerWidth === 'full' ? 'w-full' : '' // no extra constraint for contained — outer already limits it
|
||||||
);
|
);
|
||||||
|
|
||||||
const imageWrapperOrder = isImageRight ? 'lg:order-last' : 'lg:order-first';
|
const imageWrapperOrder = isImageRight ? 'lg:order-last' : 'lg:order-first';
|
||||||
@@ -74,6 +78,8 @@ export const SharedContentLayout: React.FC<SharedContentProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses}>
|
||||||
|
{containerWidth === 'boxed' ? (
|
||||||
|
<div className="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden px-6 md:px-10 py-10">
|
||||||
<div className={gridClasses}>
|
<div className={gridClasses}>
|
||||||
{/* Image Side */}
|
{/* Image Side */}
|
||||||
{hasImage && (
|
{hasImage && (
|
||||||
@@ -96,7 +102,7 @@ export const SharedContentLayout: React.FC<SharedContentProps> = ({
|
|||||||
<h2
|
<h2
|
||||||
className={cn(
|
className={cn(
|
||||||
"tracking-tight text-current mb-6",
|
"tracking-tight text-current mb-6",
|
||||||
!titleClassName && "text-3xl font-bold sm:text-4xl",
|
!titleClassName && "text-3xl font-bold sm:text-4xl lg:text-5xl",
|
||||||
titleClassName
|
titleClassName
|
||||||
)}
|
)}
|
||||||
style={titleStyle}
|
style={titleStyle}
|
||||||
@@ -111,8 +117,9 @@ export const SharedContentLayout: React.FC<SharedContentProps> = ({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'prose prose-lg max-w-none',
|
'prose prose-lg max-w-none',
|
||||||
'prose-h1:text-3xl prose-h1:font-bold prose-h1:mt-4 prose-h1:mb-2',
|
'prose-h1:text-3xl md:prose-h1:text-4xl lg:prose-h1:text-5xl prose-h1:font-bold prose-h1:mt-6 prose-h1:mb-4',
|
||||||
'prose-h2:text-2xl prose-h2:font-bold prose-h2:mt-3 prose-h2:mb-2',
|
'prose-h2:text-2xl md:prose-h2:text-3xl lg:prose-h2:text-4xl prose-h2:font-bold prose-h2:mt-5 prose-h2:mb-3',
|
||||||
|
'prose-h3:text-xl md:prose-h3:text-2xl lg:prose-h3:text-3xl prose-h3:font-bold prose-h3:mt-4 prose-h3:mb-2',
|
||||||
'prose-headings:text-[var(--tw-prose-headings)]',
|
'prose-headings:text-[var(--tw-prose-headings)]',
|
||||||
'prose-p:text-[var(--tw-prose-body)]',
|
'prose-p:text-[var(--tw-prose-body)]',
|
||||||
'text-[var(--tw-prose-body)]',
|
'text-[var(--tw-prose-body)]',
|
||||||
@@ -151,5 +158,81 @@ export const SharedContentLayout: React.FC<SharedContentProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={gridClasses}>
|
||||||
|
{/* Image Side */}
|
||||||
|
{hasImage && (
|
||||||
|
<div className={cn(
|
||||||
|
'relative w-full aspect-[4/3] rounded-2xl overflow-hidden shadow-lg',
|
||||||
|
imageWrapperOrder,
|
||||||
|
(isImageTop || isImageBottom) && 'mb-8'
|
||||||
|
)} style={imageStyle}>
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={title || 'Section Image'}
|
||||||
|
className="absolute inset-0 w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Content Side */}
|
||||||
|
<div className={cn('flex flex-col', hasImage ? 'bg-transparent' : '')}>
|
||||||
|
{title && (
|
||||||
|
<h2
|
||||||
|
className={cn(
|
||||||
|
"tracking-tight text-current mb-6",
|
||||||
|
!titleClassName && "text-3xl font-bold sm:text-4xl lg:text-5xl",
|
||||||
|
titleClassName
|
||||||
|
)}
|
||||||
|
style={titleStyle}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{text && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'prose prose-lg max-w-none',
|
||||||
|
'prose-h1:text-3xl md:prose-h1:text-4xl lg:prose-h1:text-5xl prose-h1:font-bold prose-h1:mt-6 prose-h1:mb-4',
|
||||||
|
'prose-h2:text-2xl md:prose-h2:text-3xl lg:prose-h2:text-4xl prose-h2:font-bold prose-h2:mt-5 prose-h2:mb-3',
|
||||||
|
'prose-h3:text-xl md:prose-h3:text-2xl lg:prose-h3:text-3xl prose-h3:font-bold prose-h3:mt-4 prose-h3:mb-2',
|
||||||
|
'prose-headings:text-[var(--tw-prose-headings)]',
|
||||||
|
'prose-p:text-[var(--tw-prose-body)]',
|
||||||
|
'text-[var(--tw-prose-body)]',
|
||||||
|
className,
|
||||||
|
textClassName
|
||||||
|
)}
|
||||||
|
style={proseStyle}
|
||||||
|
dangerouslySetInnerHTML={{ __html: text }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
{buttons && buttons.length > 0 && (
|
||||||
|
<div className="mt-8 flex flex-wrap gap-4">
|
||||||
|
{buttons.map((btn, idx) => (
|
||||||
|
btn.text && btn.url && (
|
||||||
|
<a
|
||||||
|
key={idx}
|
||||||
|
href={btn.url}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 px-4 py-2",
|
||||||
|
!buttonStyle?.style?.backgroundColor && "bg-primary",
|
||||||
|
!buttonStyle?.style?.color && "text-primary-foreground hover:bg-primary/90",
|
||||||
|
buttonStyle?.classNames
|
||||||
|
)}
|
||||||
|
style={buttonStyle?.style}
|
||||||
|
>
|
||||||
|
{btn.text}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ export function useProductSettings() {
|
|||||||
image_position: 'left' as string,
|
image_position: 'left' as string,
|
||||||
gallery_style: 'thumbnails' as string,
|
gallery_style: 'thumbnails' as string,
|
||||||
sticky_add_to_cart: false,
|
sticky_add_to_cart: false,
|
||||||
|
layout_style: 'flat' as string,
|
||||||
},
|
},
|
||||||
elements: {
|
elements: {
|
||||||
breadcrumbs: true,
|
breadcrumbs: true,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export interface SectionStyleResult {
|
|||||||
*/
|
*/
|
||||||
export function getSectionBackground(styles?: Record<string, any>): SectionStyleResult {
|
export function getSectionBackground(styles?: Record<string, any>): SectionStyleResult {
|
||||||
if (!styles) {
|
if (!styles) {
|
||||||
return { style: {}, hasOverlay: false, overlayStyle: undefined };
|
return { style: {}, hasOverlay: false, overlayOpacity: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const bgType = styles.backgroundType || 'solid';
|
const bgType = styles.backgroundType || 'solid';
|
||||||
@@ -56,3 +56,30 @@ export function getSectionBackground(styles?: Record<string, any>): SectionStyle
|
|||||||
|
|
||||||
return { style, hasOverlay, overlayOpacity, backgroundImage };
|
return { style, hasOverlay, overlayOpacity, backgroundImage };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns inner container class names for the three content width modes:
|
||||||
|
* - full: edge-to-edge, no max-width
|
||||||
|
* - contained: centered max-w-6xl (matches Product page / SPA default)
|
||||||
|
* - boxed: centered max-w-5xl, wrapped in a white rounded-2xl card (matches product accordion cards)
|
||||||
|
*
|
||||||
|
* For 'boxed', apply this to the inner container div; no extra wrapper needed.
|
||||||
|
*/
|
||||||
|
export function getContentWidthClasses(contentWidth?: string): string {
|
||||||
|
switch (contentWidth) {
|
||||||
|
case 'full':
|
||||||
|
return 'w-full px-4 md:px-8';
|
||||||
|
case 'boxed':
|
||||||
|
return 'container mx-auto px-4 max-w-5xl';
|
||||||
|
case 'contained':
|
||||||
|
default:
|
||||||
|
return 'container mx-auto px-4';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the section uses the boxed (card) layout.
|
||||||
|
*/
|
||||||
|
export function isBoxedLayout(contentWidth?: string): boolean {
|
||||||
|
return contentWidth === 'boxed';
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { api } from '@/lib/api/client';
|
import { api } from '@/lib/api/client';
|
||||||
import { Helmet } from 'react-helmet-async';
|
import { Helmet } from 'react-helmet-async';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
// Section Components
|
// Section Components
|
||||||
import { HeroSection } from './sections/HeroSection';
|
import { HeroSection } from './sections/HeroSection';
|
||||||
@@ -121,14 +122,25 @@ export function DynamicPageRenderer({ slug: propSlug }: DynamicPageRendererProps
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [notFound, setNotFound] = useState(false);
|
const [notFound, setNotFound] = useState(false);
|
||||||
|
|
||||||
// Use prop slug if provided, otherwise use param slug
|
// Get page type from DOM (injected by TemplateOverride.php)
|
||||||
const effectiveSlug = propSlug || paramSlug;
|
const appEl = document.getElementById('woonoow-customer-app');
|
||||||
|
const dataPageType = appEl?.getAttribute('data-page');
|
||||||
|
const dataCptType = appEl?.getAttribute('data-cpt-type'); // e.g. 'post', 'portfolio'
|
||||||
|
const dataCptSlug = appEl?.getAttribute('data-cpt-slug'); // e.g. 'my-post-slug'
|
||||||
|
|
||||||
|
// Determine content type:
|
||||||
|
// Priority: pathBase from router > data-cpt-type from DOM > fallback
|
||||||
|
const contentType = pathBase
|
||||||
|
? (pathBase === 'blog' ? 'post' : pathBase)
|
||||||
|
: (dataPageType === 'cpt' && dataCptType ? dataCptType : undefined);
|
||||||
|
|
||||||
|
// Effective slug: prefer router param, then DOM cpt-slug
|
||||||
|
const effectiveSlug = propSlug || paramSlug || (dataPageType === 'cpt' ? dataCptSlug : undefined) || '';
|
||||||
|
|
||||||
// Determine if this is a page or CPT content
|
// Determine if this is a page or CPT content
|
||||||
// If propSlug is provided, it's treated as a structural page (pathBase is undefined)
|
const isStructuralPage = dataPageType === 'page' || dataPageType === 'shop' || contentType === undefined;
|
||||||
const isStructuralPage = !pathBase || !!propSlug;
|
|
||||||
const contentType = pathBase === 'blog' ? 'post' : pathBase;
|
const contentSlug = effectiveSlug;
|
||||||
const contentSlug = effectiveSlug || '';
|
|
||||||
|
|
||||||
// Fetch page/content data
|
// Fetch page/content data
|
||||||
const { data: pageData, isLoading, error } = useQuery<PageData>({
|
const { data: pageData, isLoading, error } = useQuery<PageData>({
|
||||||
@@ -138,11 +150,12 @@ export function DynamicPageRenderer({ slug: propSlug }: DynamicPageRendererProps
|
|||||||
// Fetch structural page - api.get returns JSON directly
|
// Fetch structural page - api.get returns JSON directly
|
||||||
const response = await api.get<PageData>(`/pages/${contentSlug}`);
|
const response = await api.get<PageData>(`/pages/${contentSlug}`);
|
||||||
return response;
|
return response;
|
||||||
} else {
|
} else if (contentType) {
|
||||||
// Fetch CPT content with template
|
// Fetch CPT content with template
|
||||||
const response = await api.get<PageData>(`/content/${contentType}/${contentSlug}`);
|
const response = await api.get<PageData>(`/content/${contentType}/${contentSlug}`);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
throw new Error("Unable to determine content type");
|
||||||
},
|
},
|
||||||
retry: false,
|
retry: false,
|
||||||
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
|
||||||
@@ -175,6 +188,16 @@ export function DynamicPageRenderer({ slug: propSlug }: DynamicPageRendererProps
|
|||||||
<div className="flex flex-col items-center justify-center min-h-[50vh] text-center">
|
<div className="flex flex-col items-center justify-center min-h-[50vh] text-center">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">404</h1>
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">404</h1>
|
||||||
<p className="text-gray-600 mb-8">Page not found</p>
|
<p className="text-gray-600 mb-8">Page not found</p>
|
||||||
|
<div className="bg-gray-100 p-4 rounded text-left mb-8 text-sm">
|
||||||
|
<strong>DEBUG INFO:</strong><br />
|
||||||
|
pathBase: {pathBase ?? 'undefined'}<br />
|
||||||
|
propSlug: {propSlug ?? 'undefined'}<br />
|
||||||
|
paramSlug: {paramSlug ?? 'undefined'}<br />
|
||||||
|
effectiveSlug: {effectiveSlug ?? 'undefined'}<br />
|
||||||
|
dataPageType: {dataPageType ?? 'undefined'}<br />
|
||||||
|
contentType: {contentType ?? 'undefined'}<br />
|
||||||
|
isStructuralPage: {isStructuralPage ? 'true' : 'false'}<br />
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/')}
|
onClick={() => navigate('/')}
|
||||||
className="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90"
|
className="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90"
|
||||||
@@ -226,15 +249,15 @@ export function DynamicPageRenderer({ slug: propSlug }: DynamicPageRendererProps
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={section.id}
|
key={section.id}
|
||||||
className={`relative overflow-hidden ${!section.styles?.backgroundColor ? '' : ''}`}
|
className="relative overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: section.styles?.backgroundColor,
|
// Only explicit custom padding overrides from the padding fields
|
||||||
paddingTop: section.styles?.paddingTop,
|
paddingTop: section.styles?.paddingTop,
|
||||||
paddingBottom: section.styles?.paddingBottom,
|
paddingBottom: section.styles?.paddingBottom,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Background Image & Overlay */}
|
{/* Full-bleed background image & overlay */}
|
||||||
{section.styles?.backgroundImage && (
|
{section.styles?.backgroundImage && (section.styles.backgroundType === 'image' || !section.styles.backgroundType) && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 z-0 bg-cover bg-center bg-no-repeat"
|
className="absolute inset-0 z-0 bg-cover bg-center bg-no-repeat"
|
||||||
@@ -247,11 +270,11 @@ export function DynamicPageRenderer({ slug: propSlug }: DynamicPageRendererProps
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Content Wrapper */}
|
{/* Section component — manages its own background, height, and inner content width */}
|
||||||
<div className={`relative z-10 ${section.styles?.contentWidth === 'contained' ? 'container mx-auto px-4 max-w-6xl' : 'w-full'}`}>
|
<div className="relative z-10 w-full">
|
||||||
<SectionComponent
|
<SectionComponent
|
||||||
id={section.id}
|
id={section.id}
|
||||||
section={section} // Pass full section object for components that need raw data
|
section={section}
|
||||||
layout={section.layoutVariant || 'default'}
|
layout={section.layoutVariant || 'default'}
|
||||||
colorScheme={section.colorScheme || 'default'}
|
colorScheme={section.colorScheme || 'default'}
|
||||||
styles={section.styles}
|
styles={section.styles}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||||
|
|
||||||
interface CTABannerSectionProps {
|
interface CTABannerSectionProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -22,26 +23,34 @@ export function CTABannerSection({
|
|||||||
elementStyles,
|
elementStyles,
|
||||||
styles,
|
styles,
|
||||||
}: CTABannerSectionProps & { styles?: Record<string, any> }) {
|
}: CTABannerSectionProps & { styles?: Record<string, any> }) {
|
||||||
|
const heightMap: Record<string, string> = {
|
||||||
|
'default': 'py-12 md:py-20',
|
||||||
|
'small': 'py-8 md:py-12',
|
||||||
|
'medium': 'py-16 md:py-24',
|
||||||
|
'large': 'py-24 md:py-36',
|
||||||
|
'fullscreen': 'min-h-screen flex flex-col justify-center',
|
||||||
|
};
|
||||||
|
const customPadding = styles?.paddingTop || styles?.paddingBottom;
|
||||||
|
const heightClasses = customPadding ? '' : (heightMap[styles?.heightPreset || 'default'] || 'py-12 md:py-20');
|
||||||
|
|
||||||
// Helper to get text styles (including font family)
|
|
||||||
const getTextStyles = (elementName: string) => {
|
const getTextStyles = (elementName: string) => {
|
||||||
const styles = elementStyles?.[elementName] || {};
|
const es = elementStyles?.[elementName] || {};
|
||||||
return {
|
return {
|
||||||
classNames: cn(
|
classNames: cn(
|
||||||
styles.fontSize,
|
es.fontSize,
|
||||||
styles.fontWeight,
|
es.fontWeight,
|
||||||
{
|
{
|
||||||
'font-sans': styles.fontFamily === 'secondary',
|
'font-sans': es.fontFamily === 'secondary',
|
||||||
'font-serif': styles.fontFamily === 'primary',
|
'font-serif': es.fontFamily === 'primary',
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
style: {
|
style: {
|
||||||
color: styles.color,
|
color: es.color,
|
||||||
textAlign: styles.textAlign,
|
textAlign: es.textAlign,
|
||||||
backgroundColor: styles.backgroundColor,
|
backgroundColor: es.backgroundColor,
|
||||||
borderColor: styles.borderColor,
|
borderColor: es.borderColor,
|
||||||
borderWidth: styles.borderWidth,
|
borderWidth: es.borderWidth,
|
||||||
borderRadius: styles.borderRadius,
|
borderRadius: es.borderRadius,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -49,31 +58,15 @@ export function CTABannerSection({
|
|||||||
const titleStyle = getTextStyles('title');
|
const titleStyle = getTextStyles('title');
|
||||||
const textStyle = getTextStyles('text');
|
const textStyle = getTextStyles('text');
|
||||||
const btnStyle = getTextStyles('button_text');
|
const btnStyle = getTextStyles('button_text');
|
||||||
return (
|
|
||||||
<section
|
// Shared inner content — same markup used in boxed and non-boxed
|
||||||
id={id}
|
const innerContent = (
|
||||||
className={cn(
|
<>
|
||||||
'wn-section wn-cta-banner',
|
|
||||||
`wn-cta-banner--${layout}`,
|
|
||||||
`wn-scheme--${colorScheme}`,
|
|
||||||
'py-12 md:py-20',
|
|
||||||
{
|
|
||||||
'bg-primary text-primary-foreground': colorScheme === 'primary',
|
|
||||||
'bg-secondary text-secondary-foreground': colorScheme === 'secondary',
|
|
||||||
'bg-gradient-to-r from-primary to-secondary text-white': colorScheme === 'gradient',
|
|
||||||
'bg-muted': colorScheme === 'muted',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={cn(
|
|
||||||
"mx-auto px-4 text-center",
|
|
||||||
styles?.contentWidth === 'full' ? 'w-full' : 'container'
|
|
||||||
)}>
|
|
||||||
{title && (
|
{title && (
|
||||||
<h2
|
<h2
|
||||||
className={cn(
|
className={cn(
|
||||||
"wn-cta__title mb-6",
|
"wn-cta__title mb-6",
|
||||||
!elementStyles?.title?.fontSize && "text-3xl md:text-4xl",
|
!elementStyles?.title?.fontSize && "text-3xl md:text-4xl lg:text-5xl",
|
||||||
!elementStyles?.title?.fontWeight && "font-bold",
|
!elementStyles?.title?.fontWeight && "font-bold",
|
||||||
titleStyle.classNames
|
titleStyle.classNames
|
||||||
)}
|
)}
|
||||||
@@ -87,10 +80,11 @@ export function CTABannerSection({
|
|||||||
<p className={cn(
|
<p className={cn(
|
||||||
'wn-cta-banner__text mb-8 max-w-2xl mx-auto',
|
'wn-cta-banner__text mb-8 max-w-2xl mx-auto',
|
||||||
!elementStyles?.text?.fontSize && "text-lg md:text-xl",
|
!elementStyles?.text?.fontSize && "text-lg md:text-xl",
|
||||||
{
|
styles?.contentWidth !== 'boxed' && {
|
||||||
'text-white/90': colorScheme === 'primary' || colorScheme === 'gradient',
|
'text-white/90': colorScheme === 'primary',
|
||||||
'text-gray-600': colorScheme === 'muted',
|
'text-gray-600': colorScheme === 'muted',
|
||||||
},
|
},
|
||||||
|
styles?.contentWidth === 'boxed' && 'text-gray-600',
|
||||||
textStyle.classNames
|
textStyle.classNames
|
||||||
)}
|
)}
|
||||||
style={textStyle.style}
|
style={textStyle.style}
|
||||||
@@ -104,14 +98,18 @@ export function CTABannerSection({
|
|||||||
href={button_url}
|
href={button_url}
|
||||||
className={cn(
|
className={cn(
|
||||||
'wn-cta-banner__button inline-block px-8 py-3 rounded-lg font-semibold transition-all hover:opacity-90',
|
'wn-cta-banner__button inline-block px-8 py-3 rounded-lg font-semibold transition-all hover:opacity-90',
|
||||||
!btnStyle.style?.backgroundColor && {
|
!btnStyle.style?.backgroundColor && (styles?.contentWidth === 'boxed'
|
||||||
'bg-white': colorScheme === 'primary' || colorScheme === 'gradient',
|
? 'bg-primary'
|
||||||
|
: {
|
||||||
|
'bg-white': colorScheme === 'primary',
|
||||||
'bg-primary': colorScheme === 'muted' || colorScheme === 'secondary',
|
'bg-primary': colorScheme === 'muted' || colorScheme === 'secondary',
|
||||||
},
|
}),
|
||||||
!btnStyle.style?.color && {
|
!btnStyle.style?.color && (styles?.contentWidth === 'boxed'
|
||||||
'text-primary': colorScheme === 'primary' || colorScheme === 'gradient',
|
? 'text-primary-foreground'
|
||||||
|
: {
|
||||||
|
'text-primary': colorScheme === 'primary',
|
||||||
'text-white': colorScheme === 'muted' || colorScheme === 'secondary',
|
'text-white': colorScheme === 'muted' || colorScheme === 'secondary',
|
||||||
},
|
}),
|
||||||
btnStyle.classNames
|
btnStyle.classNames
|
||||||
)}
|
)}
|
||||||
style={btnStyle.style}
|
style={btnStyle.style}
|
||||||
@@ -119,7 +117,54 @@ export function CTABannerSection({
|
|||||||
{button_text}
|
{button_text}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasCustomBackground = !!styles?.backgroundColor || !!styles?.backgroundImage || styles?.backgroundType === 'gradient';
|
||||||
|
const sectionBg = getSectionBackground(styles);
|
||||||
|
|
||||||
|
// Helper to get background style for dynamic schemes
|
||||||
|
const getBackgroundStyle = (): React.CSSProperties | undefined => {
|
||||||
|
if (hasCustomBackground) return sectionBg.style;
|
||||||
|
if (colorScheme === 'primary') {
|
||||||
|
return { backgroundColor: 'var(--wn-primary, #1a1a1a)' };
|
||||||
|
}
|
||||||
|
if (colorScheme === 'secondary') {
|
||||||
|
return { backgroundColor: 'var(--wn-secondary, #6b7280)' };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id={id}
|
||||||
|
className={cn(
|
||||||
|
'wn-section wn-cta-banner',
|
||||||
|
`wn-cta-banner--${layout}`,
|
||||||
|
`wn-scheme--${colorScheme}`,
|
||||||
|
heightClasses,
|
||||||
|
{
|
||||||
|
'bg-primary text-primary-foreground': colorScheme === 'primary' && !hasCustomBackground,
|
||||||
|
'bg-secondary text-secondary-foreground': colorScheme === 'secondary' && !hasCustomBackground,
|
||||||
|
'bg-muted': colorScheme === 'muted' && !hasCustomBackground,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
style={getBackgroundStyle()}
|
||||||
|
>
|
||||||
|
{styles?.contentWidth === 'boxed' ? (
|
||||||
|
<div className="container mx-auto px-4 max-w-5xl">
|
||||||
|
<div className="bg-white text-gray-900 rounded-2xl shadow-sm border border-gray-200 overflow-hidden px-6 md:px-10 py-10 text-center">
|
||||||
|
{innerContent}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={cn(
|
||||||
|
"mx-auto px-4 text-center",
|
||||||
|
styles?.contentWidth === 'full' ? 'w-full' : 'container'
|
||||||
|
)}>
|
||||||
|
{innerContent}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||||
|
|
||||||
interface ContactFormSectionProps {
|
interface ContactFormSectionProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -23,6 +24,15 @@ export function ContactFormSection({
|
|||||||
elementStyles,
|
elementStyles,
|
||||||
styles,
|
styles,
|
||||||
}: ContactFormSectionProps & { styles?: Record<string, any> }) {
|
}: ContactFormSectionProps & { styles?: Record<string, any> }) {
|
||||||
|
const heightMap: Record<string, string> = {
|
||||||
|
'default': 'py-12 md:py-20',
|
||||||
|
'small': 'py-8 md:py-12',
|
||||||
|
'medium': 'py-16 md:py-24',
|
||||||
|
'large': 'py-24 md:py-36',
|
||||||
|
'fullscreen': 'min-h-screen flex flex-col justify-center',
|
||||||
|
};
|
||||||
|
const customPadding = styles?.paddingTop || styles?.paddingBottom;
|
||||||
|
const heightClasses = customPadding ? '' : (heightMap[styles?.heightPreset || 'default'] || 'py-12 md:py-20');
|
||||||
const [formData, setFormData] = useState<Record<string, string>>({});
|
const [formData, setFormData] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// Helper to get text styles (including font family)
|
// Helper to get text styles (including font family)
|
||||||
@@ -87,6 +97,19 @@ export function ContactFormSection({
|
|||||||
} finally {
|
} finally {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
|
}; const hasCustomBackground = !!styles?.backgroundColor || !!styles?.backgroundImage || styles?.backgroundType === 'gradient';
|
||||||
|
const sectionBg = getSectionBackground(styles);
|
||||||
|
|
||||||
|
// Helper to get background style for dynamic schemes
|
||||||
|
const getBackgroundStyle = (): React.CSSProperties | undefined => {
|
||||||
|
if (hasCustomBackground) return sectionBg.style;
|
||||||
|
if (colorScheme === 'primary') {
|
||||||
|
return { backgroundColor: 'var(--wn-primary, #1a1a1a)' };
|
||||||
|
}
|
||||||
|
if (colorScheme === 'secondary') {
|
||||||
|
return { backgroundColor: 'var(--wn-secondary, #6b7280)' };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -95,17 +118,18 @@ export function ContactFormSection({
|
|||||||
className={cn(
|
className={cn(
|
||||||
'wn-section wn-contact-form',
|
'wn-section wn-contact-form',
|
||||||
`wn-scheme--${colorScheme}`,
|
`wn-scheme--${colorScheme}`,
|
||||||
`wn-scheme--${colorScheme}`,
|
heightClasses,
|
||||||
'py-12 md:py-20',
|
|
||||||
{
|
{
|
||||||
// 'bg-white': colorScheme === 'default', // Removed for global styling
|
'bg-muted': colorScheme === 'muted' && !hasCustomBackground,
|
||||||
'bg-muted': colorScheme === 'muted',
|
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
style={getBackgroundStyle()}
|
||||||
>
|
>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"mx-auto px-4",
|
"mx-auto px-4",
|
||||||
styles?.contentWidth === 'full' ? 'w-full' : 'container'
|
styles?.contentWidth === 'full' ? 'w-full'
|
||||||
|
: styles?.contentWidth === 'boxed' ? 'max-w-5xl'
|
||||||
|
: 'container'
|
||||||
)}>
|
)}>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'max-w-xl mx-auto',
|
'max-w-xl mx-auto',
|
||||||
@@ -116,7 +140,7 @@ export function ContactFormSection({
|
|||||||
{title && (
|
{title && (
|
||||||
<h2 className={cn(
|
<h2 className={cn(
|
||||||
"wn-contact__title text-center mb-12",
|
"wn-contact__title text-center mb-12",
|
||||||
!elementStyles?.title?.fontSize && "text-3xl md:text-4xl",
|
!elementStyles?.title?.fontSize && "text-3xl md:text-4xl lg:text-5xl",
|
||||||
!elementStyles?.title?.fontWeight && "font-bold",
|
!elementStyles?.title?.fontWeight && "font-bold",
|
||||||
titleStyle.classNames
|
titleStyle.classNames
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { SharedContentLayout } from '@/components/SharedContentLayout';
|
import { SharedContentLayout } from '@/components/SharedContentLayout';
|
||||||
|
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||||
|
|
||||||
interface ContentSectionProps {
|
interface ContentSectionProps {
|
||||||
|
id?: string;
|
||||||
section: {
|
section: {
|
||||||
id: string;
|
id: string;
|
||||||
layoutVariant?: string;
|
layoutVariant?: string;
|
||||||
colorScheme?: string;
|
colorScheme?: string;
|
||||||
props?: {
|
|
||||||
content?: { value: string };
|
|
||||||
cta_text?: { value: string };
|
|
||||||
cta_url?: { value: string };
|
|
||||||
};
|
|
||||||
elementStyles?: Record<string, any>;
|
elementStyles?: Record<string, any>;
|
||||||
styles?: Record<string, any>;
|
styles?: Record<string, any>;
|
||||||
|
props?: any;
|
||||||
};
|
};
|
||||||
|
content?: string;
|
||||||
|
cta_text?: string;
|
||||||
|
cta_url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLOR_SCHEMES: Record<string, { bg: string; text: string }> = {
|
const COLOR_SCHEMES: Record<string, { bg: string; text: string }> = {
|
||||||
@@ -25,7 +26,6 @@ const COLOR_SCHEMES: Record<string, { bg: string; text: string }> = {
|
|||||||
primary: { bg: 'wn-primary-bg', text: 'text-white' },
|
primary: { bg: 'wn-primary-bg', text: 'text-white' },
|
||||||
secondary: { bg: 'wn-secondary-bg', text: 'text-white' },
|
secondary: { bg: 'wn-secondary-bg', text: 'text-white' },
|
||||||
muted: { bg: 'bg-gray-50', text: 'text-gray-700' },
|
muted: { bg: 'bg-gray-50', text: 'text-gray-700' },
|
||||||
gradient: { bg: 'wn-gradient-bg', text: 'text-white' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const WIDTH_CLASSES: Record<string, string> = {
|
const WIDTH_CLASSES: Record<string, string> = {
|
||||||
@@ -164,11 +164,10 @@ const generateScopedStyles = (sectionId: string, elementStyles: Record<string, a
|
|||||||
return styles.join('\n');
|
return styles.join('\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ContentSection({ section }: ContentSectionProps) {
|
export function ContentSection({ section, content: propContent, cta_text: propCtaText, cta_url: propCtaUrl, outerPadding = false }: ContentSectionProps & { outerPadding?: boolean }) {
|
||||||
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'];
|
const scheme = COLOR_SCHEMES[section.colorScheme || 'default'] ?? COLOR_SCHEMES['default'];
|
||||||
// Default to 'default' width if not specified
|
// Default to 'default' width if not specified
|
||||||
const layout = section.layoutVariant || 'default';
|
const layout = section.layoutVariant || 'default';
|
||||||
const widthClass = section.styles?.contentWidth === 'full' ? WIDTH_CLASSES.full : (WIDTH_CLASSES[layout] || WIDTH_CLASSES.default);
|
|
||||||
|
|
||||||
const heightPreset = section.styles?.heightPreset || 'default';
|
const heightPreset = section.styles?.heightPreset || 'default';
|
||||||
|
|
||||||
@@ -182,7 +181,7 @@ export function ContentSection({ section }: ContentSectionProps) {
|
|||||||
|
|
||||||
const heightClasses = heightMap[heightPreset] || 'py-12 md:py-20';
|
const heightClasses = heightMap[heightPreset] || 'py-12 md:py-20';
|
||||||
|
|
||||||
const content = section.props?.content?.value || '';
|
const content = propContent || section.props?.content?.value || section.props?.content || '';
|
||||||
|
|
||||||
// Helper to get text styles
|
// Helper to get text styles
|
||||||
const getTextStyles = (elementName: string) => {
|
const getTextStyles = (elementName: string) => {
|
||||||
@@ -209,15 +208,16 @@ export function ContentSection({ section }: ContentSectionProps) {
|
|||||||
const textStyle = getTextStyles('text');
|
const textStyle = getTextStyles('text');
|
||||||
const buttonStyle = getTextStyles('button');
|
const buttonStyle = getTextStyles('button');
|
||||||
|
|
||||||
const containerWidth = section.styles?.contentWidth || 'contained';
|
const containerWidth = section.styles?.contentWidth ?? 'contained';
|
||||||
const cta_text = section.props?.cta_text?.value;
|
const cta_text = propCtaText || section.props?.cta_text?.value || section.props?.cta_text;
|
||||||
const cta_url = section.props?.cta_url?.value;
|
const cta_url = propCtaUrl || section.props?.cta_url?.value || section.props?.cta_url;
|
||||||
|
|
||||||
|
const hasCustomBackground = !!section.styles?.backgroundColor || !!section.styles?.backgroundImage || section.styles?.backgroundType === 'gradient';
|
||||||
|
const sectionBg = getSectionBackground(section.styles);
|
||||||
|
|
||||||
// Helper to get background style for dynamic schemes
|
// Helper to get background style for dynamic schemes
|
||||||
const getBackgroundStyle = () => {
|
const getBackgroundStyle = (): React.CSSProperties | undefined => {
|
||||||
if (scheme.bg === 'wn-gradient-bg') {
|
if (hasCustomBackground) return sectionBg.style;
|
||||||
return { backgroundImage: 'linear-gradient(135deg, var(--wn-gradient-start, #9333ea), var(--wn-gradient-end, #3b82f6))' };
|
|
||||||
}
|
|
||||||
if (scheme.bg === 'wn-primary-bg') {
|
if (scheme.bg === 'wn-primary-bg') {
|
||||||
return { backgroundColor: 'var(--wn-primary, #1a1a1a)' };
|
return { backgroundColor: 'var(--wn-primary, #1a1a1a)' };
|
||||||
}
|
}
|
||||||
@@ -234,9 +234,8 @@ export function ContentSection({ section }: ContentSectionProps) {
|
|||||||
id={section.id}
|
id={section.id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'wn-content',
|
'wn-content',
|
||||||
'px-4 md:px-8',
|
|
||||||
heightClasses,
|
heightClasses,
|
||||||
!scheme.bg.startsWith('wn-') && scheme.bg,
|
!hasCustomBackground && !scheme.bg.startsWith('wn-') && scheme.bg,
|
||||||
scheme.text
|
scheme.text
|
||||||
)}
|
)}
|
||||||
style={getBackgroundStyle()}
|
style={getBackgroundStyle()}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import * as LucideIcons from 'lucide-react';
|
import * as LucideIcons from 'lucide-react';
|
||||||
|
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||||
|
|
||||||
interface FeatureItem {
|
interface FeatureItem {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
|
// Post-card fields (from related_posts dynamic source)
|
||||||
|
url?: string;
|
||||||
|
featured_image?: string;
|
||||||
|
excerpt?: string;
|
||||||
|
date?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FeatureGridSectionProps {
|
interface FeatureGridSectionProps {
|
||||||
@@ -26,15 +32,26 @@ export function FeatureGridSection({
|
|||||||
elementStyles,
|
elementStyles,
|
||||||
styles,
|
styles,
|
||||||
}: FeatureGridSectionProps & { features?: FeatureItem[], styles?: Record<string, any> }) {
|
}: FeatureGridSectionProps & { features?: FeatureItem[], styles?: Record<string, any> }) {
|
||||||
// Use items or features (priority to items if both exist, but usually only one comes from props)
|
const heightMap: Record<string, string> = {
|
||||||
|
'default': 'py-12 md:py-20',
|
||||||
|
'small': 'py-8 md:py-12',
|
||||||
|
'medium': 'py-16 md:py-24',
|
||||||
|
'large': 'py-24 md:py-36',
|
||||||
|
'fullscreen': 'min-h-screen flex flex-col justify-center',
|
||||||
|
};
|
||||||
|
const customPadding = styles?.paddingTop || styles?.paddingBottom;
|
||||||
|
const heightClasses = customPadding ? '' : (heightMap[styles?.heightPreset || 'default'] || 'py-12 md:py-20');
|
||||||
const listItems = items.length > 0 ? items : features;
|
const listItems = items.length > 0 ? items : features;
|
||||||
|
|
||||||
const gridCols = {
|
const gridCols = {
|
||||||
'grid-2': 'md:grid-cols-2',
|
'grid-2': 'md:grid-cols-2',
|
||||||
'grid-3': 'md:grid-cols-3',
|
'grid-3': 'md:grid-cols-3',
|
||||||
'grid-4': 'md:grid-cols-2 lg:grid-cols-4',
|
'grid-4': 'md:grid-cols-2 lg:grid-cols-4',
|
||||||
}[layout] || 'md:grid-cols-3';
|
}[layout] || 'md:grid-cols-3';
|
||||||
|
|
||||||
// Helper to get text styles (including font family)
|
// Detect if these are post-cards (from related_posts) — they have a url field
|
||||||
|
const isPostCards = listItems.some(item => !!item.url);
|
||||||
|
|
||||||
const getTextStyles = (elementName: string) => {
|
const getTextStyles = (elementName: string) => {
|
||||||
const styles = elementStyles?.[elementName] || {};
|
const styles = elementStyles?.[elementName] || {};
|
||||||
return {
|
return {
|
||||||
@@ -60,6 +77,21 @@ export function FeatureGridSection({
|
|||||||
const headingStyle = getTextStyles('heading');
|
const headingStyle = getTextStyles('heading');
|
||||||
const featureItemStyle = getTextStyles('feature_item');
|
const featureItemStyle = getTextStyles('feature_item');
|
||||||
|
|
||||||
|
const hasCustomBackground = !!styles?.backgroundColor || !!styles?.backgroundImage || styles?.backgroundType === 'gradient';
|
||||||
|
const sectionBg = getSectionBackground(styles);
|
||||||
|
|
||||||
|
// Helper to get background style for dynamic schemes
|
||||||
|
const getBackgroundStyle = (): React.CSSProperties | undefined => {
|
||||||
|
if (hasCustomBackground) return sectionBg.style;
|
||||||
|
if (colorScheme === 'primary') {
|
||||||
|
return { backgroundColor: 'var(--wn-primary, #1a1a1a)' };
|
||||||
|
}
|
||||||
|
if (colorScheme === 'secondary') {
|
||||||
|
return { backgroundColor: 'var(--wn-secondary, #6b7280)' };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id={id}
|
id={id}
|
||||||
@@ -67,23 +99,25 @@ export function FeatureGridSection({
|
|||||||
'wn-section wn-feature-grid',
|
'wn-section wn-feature-grid',
|
||||||
`wn-feature-grid--${layout}`,
|
`wn-feature-grid--${layout}`,
|
||||||
`wn-scheme--${colorScheme}`,
|
`wn-scheme--${colorScheme}`,
|
||||||
'py-12 md:py-24',
|
heightClasses,
|
||||||
{
|
{
|
||||||
// 'bg-white': colorScheme === 'default', // Removed for global styling
|
'bg-muted': colorScheme === 'muted' && !hasCustomBackground,
|
||||||
'bg-muted': colorScheme === 'muted',
|
'text-primary-foreground': colorScheme === 'primary' && !hasCustomBackground,
|
||||||
'bg-primary text-primary-foreground': colorScheme === 'primary',
|
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
style={getBackgroundStyle()}
|
||||||
>
|
>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"mx-auto px-4",
|
"mx-auto px-4",
|
||||||
styles?.contentWidth === 'full' ? 'w-full' : 'container'
|
styles?.contentWidth === 'full' ? 'w-full'
|
||||||
|
: styles?.contentWidth === 'boxed' ? 'max-w-5xl'
|
||||||
|
: 'container'
|
||||||
)}>
|
)}>
|
||||||
{heading && (
|
{heading && (
|
||||||
<h2
|
<h2
|
||||||
className={cn(
|
className={cn(
|
||||||
"wn-features__heading text-center mb-12",
|
"wn-features__heading text-center mb-10",
|
||||||
!elementStyles?.heading?.fontSize && "text-3xl md:text-4xl",
|
!elementStyles?.heading?.fontSize && "text-2xl md:text-3xl lg:text-4xl",
|
||||||
!elementStyles?.heading?.fontWeight && "font-bold",
|
!elementStyles?.heading?.fontWeight && "font-bold",
|
||||||
headingStyle.classNames
|
headingStyle.classNames
|
||||||
)}
|
)}
|
||||||
@@ -93,8 +127,66 @@ export function FeatureGridSection({
|
|||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={cn('grid gap-8', gridCols)}>
|
<div className={cn('grid gap-6', gridCols)}>
|
||||||
{listItems.map((item, index) => (
|
{listItems.map((item, index) => {
|
||||||
|
// ── Post Card (from related_posts) ──────────────────────────
|
||||||
|
if (isPostCards) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={index}
|
||||||
|
href={item.url || '#'}
|
||||||
|
className={cn(
|
||||||
|
'wn-post-card group block rounded-xl overflow-hidden transition-all duration-200',
|
||||||
|
'bg-white shadow-md hover:shadow-xl hover:-translate-y-1',
|
||||||
|
featureItemStyle.classNames
|
||||||
|
)}
|
||||||
|
style={featureItemStyle.style}
|
||||||
|
>
|
||||||
|
{/* Thumbnail */}
|
||||||
|
{item.featured_image ? (
|
||||||
|
<div className="aspect-[16/9] overflow-hidden bg-gray-100">
|
||||||
|
<img
|
||||||
|
src={item.featured_image}
|
||||||
|
alt={item.title || ''}
|
||||||
|
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="aspect-[16/9] bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center">
|
||||||
|
<svg className="w-10 h-10 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Card Body */}
|
||||||
|
<div className="p-5">
|
||||||
|
{item.date && (
|
||||||
|
<p className="text-xs text-gray-400 mb-2 uppercase tracking-wider">{item.date}</p>
|
||||||
|
)}
|
||||||
|
{item.title && (
|
||||||
|
<h3 className="font-semibold text-gray-900 text-base leading-snug mb-2 group-hover:text-primary transition-colors line-clamp-2">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
|
{(item.excerpt || item.description) && (
|
||||||
|
<p className="text-sm text-gray-500 line-clamp-3 mb-4">
|
||||||
|
{item.excerpt || item.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<span className="inline-flex items-center gap-1 text-sm font-medium text-primary">
|
||||||
|
Read more
|
||||||
|
<svg className="w-4 h-4 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Feature Card (icon + title + desc) ─────────────────────
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -117,7 +209,6 @@ export function FeatureGridSection({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{item.title && (
|
{item.title && (
|
||||||
<h3
|
<h3
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -129,9 +220,9 @@ export function FeatureGridSection({
|
|||||||
{item.title}
|
{item.title}
|
||||||
</h3>
|
</h3>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{item.description && (
|
{item.description && (
|
||||||
<p className={cn(
|
<p
|
||||||
|
className={cn(
|
||||||
'wn-feature-grid__item-desc',
|
'wn-feature-grid__item-desc',
|
||||||
!featureItemStyle.style?.color && {
|
!featureItemStyle.style?.color && {
|
||||||
'text-gray-600': colorScheme !== 'primary',
|
'text-gray-600': colorScheme !== 'primary',
|
||||||
@@ -144,8 +235,14 @@ export function FeatureGridSection({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Empty state for related posts */}
|
||||||
|
{isPostCards && listItems.length === 0 && (
|
||||||
|
<p className="text-center text-gray-400 text-sm py-8">No related articles found.</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ export function HeroSection({
|
|||||||
elementStyles,
|
elementStyles,
|
||||||
styles,
|
styles,
|
||||||
}: HeroSectionProps & { styles?: Record<string, any> }) {
|
}: HeroSectionProps & { styles?: Record<string, any> }) {
|
||||||
|
const heightMap: Record<string, string> = {
|
||||||
|
'default': 'py-16 md:py-28',
|
||||||
|
'small': 'py-8 md:py-12',
|
||||||
|
'medium': 'py-16 md:py-24',
|
||||||
|
'large': 'py-24 md:py-36',
|
||||||
|
'fullscreen': 'min-h-screen flex flex-col justify-center',
|
||||||
|
};
|
||||||
|
const customPadding = styles?.paddingTop || styles?.paddingBottom;
|
||||||
|
const heightClasses = customPadding ? '' : (heightMap[styles?.heightPreset || 'default'] || 'py-16 md:py-28');
|
||||||
const isImageLeft = layout === 'hero-left-image' || layout === 'image-left';
|
const isImageLeft = layout === 'hero-left-image' || layout === 'image-left';
|
||||||
const isImageRight = layout === 'hero-right-image' || layout === 'image-right';
|
const isImageRight = layout === 'hero-right-image' || layout === 'image-right';
|
||||||
const isCentered = layout === 'centered' || layout === 'default';
|
const isCentered = layout === 'centered' || layout === 'default';
|
||||||
@@ -67,9 +76,6 @@ export function HeroSection({
|
|||||||
const getBackgroundStyle = (): React.CSSProperties | undefined => {
|
const getBackgroundStyle = (): React.CSSProperties | undefined => {
|
||||||
// If user set custom bg via Design tab, use that
|
// If user set custom bg via Design tab, use that
|
||||||
if (hasCustomBackground) return sectionBg.style;
|
if (hasCustomBackground) return sectionBg.style;
|
||||||
if (colorScheme === 'gradient') {
|
|
||||||
return { backgroundImage: 'linear-gradient(135deg, var(--wn-gradient-start, #9333ea), var(--wn-gradient-end, #3b82f6))' };
|
|
||||||
}
|
|
||||||
if (colorScheme === 'primary') {
|
if (colorScheme === 'primary') {
|
||||||
return { backgroundColor: 'var(--wn-primary, #1a1a1a)' };
|
return { backgroundColor: 'var(--wn-primary, #1a1a1a)' };
|
||||||
}
|
}
|
||||||
@@ -79,7 +85,7 @@ export function HeroSection({
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isDynamicScheme = ['primary', 'secondary', 'gradient'].includes(colorScheme) && !hasCustomBackground;
|
const isDynamicScheme = ['primary', 'secondary'].includes(colorScheme) && !hasCustomBackground;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
@@ -88,12 +94,15 @@ export function HeroSection({
|
|||||||
'wn-section wn-hero',
|
'wn-section wn-hero',
|
||||||
`wn-hero--${layout}`,
|
`wn-hero--${layout}`,
|
||||||
'relative overflow-hidden',
|
'relative overflow-hidden',
|
||||||
|
heightClasses,
|
||||||
)}
|
)}
|
||||||
style={sectionBg.style}
|
style={sectionBg.style}
|
||||||
>
|
>
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'mx-auto px-4 z-10 relative flex w-full',
|
'mx-auto px-4 z-10 relative flex w-full',
|
||||||
styles?.contentWidth === 'full' ? 'w-full' : 'container max-w-7xl',
|
styles?.contentWidth === 'full' ? 'w-full'
|
||||||
|
: styles?.contentWidth === 'boxed' ? 'max-w-5xl'
|
||||||
|
: 'container max-w-7xl',
|
||||||
{
|
{
|
||||||
'flex flex-col md:flex-row items-center gap-8': isImageLeft || isImageRight,
|
'flex flex-col md:flex-row items-center gap-8': isImageLeft || isImageRight,
|
||||||
'text-center': isCentered,
|
'text-center': isCentered,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { SharedContentLayout } from '@/components/SharedContentLayout';
|
import { SharedContentLayout } from '@/components/SharedContentLayout';
|
||||||
|
import { getSectionBackground } from '@/lib/sectionStyles';
|
||||||
|
|
||||||
interface ImageTextSectionProps {
|
interface ImageTextSectionProps {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -66,25 +67,40 @@ export function ImageTextSection({
|
|||||||
};
|
};
|
||||||
const heightClasses = heightMap[heightPreset] || 'py-12 md:py-24';
|
const heightClasses = heightMap[heightPreset] || 'py-12 md:py-24';
|
||||||
|
|
||||||
|
const hasCustomBackground = !!styles?.backgroundColor || !!styles?.backgroundImage || styles?.backgroundType === 'gradient';
|
||||||
|
const sectionBg = getSectionBackground(styles);
|
||||||
|
|
||||||
|
// Helper to get background style for dynamic schemes
|
||||||
|
const getBackgroundStyle = (): React.CSSProperties | undefined => {
|
||||||
|
if (hasCustomBackground) return sectionBg.style;
|
||||||
|
if (colorScheme === 'primary') {
|
||||||
|
return { backgroundColor: 'var(--wn-primary, #1a1a1a)' };
|
||||||
|
}
|
||||||
|
if (colorScheme === 'secondary') {
|
||||||
|
return { backgroundColor: 'var(--wn-secondary, #6b7280)' };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id={id}
|
id={id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'wn-section wn-image-text',
|
'wn-section wn-image-text',
|
||||||
`wn-scheme--${colorScheme}`,
|
`wn-scheme--${colorScheme}`,
|
||||||
heightClasses,
|
!styles?.paddingTop && !styles?.paddingBottom && heightClasses,
|
||||||
{
|
{
|
||||||
'bg-muted': colorScheme === 'muted',
|
'bg-muted': colorScheme === 'muted' && !hasCustomBackground,
|
||||||
'bg-primary/5': colorScheme === 'primary',
|
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
style={getBackgroundStyle()}
|
||||||
>
|
>
|
||||||
<SharedContentLayout
|
<SharedContentLayout
|
||||||
title={title}
|
title={title}
|
||||||
text={text}
|
text={text}
|
||||||
image={image}
|
image={image}
|
||||||
imagePosition={isImageRight ? 'right' : 'left'}
|
imagePosition={isImageRight ? 'right' : 'left'}
|
||||||
containerWidth={styles?.contentWidth === 'full' ? 'full' : 'contained'}
|
containerWidth={styles?.contentWidth === 'full' ? 'full' : styles?.contentWidth === 'boxed' ? 'boxed' : 'contained'}
|
||||||
titleStyle={titleStyle.style}
|
titleStyle={titleStyle.style}
|
||||||
titleClassName={titleStyle.classNames}
|
titleClassName={titleStyle.classNames}
|
||||||
textStyle={textStyle.style}
|
textStyle={textStyle.style}
|
||||||
|
|||||||
@@ -29,6 +29,20 @@ export default function Product() {
|
|||||||
const { isEnabled: wishlistEnabled, isInWishlist, toggleWishlist, isLoggedIn } = useWishlist();
|
const { isEnabled: wishlistEnabled, isInWishlist, toggleWishlist, isLoggedIn } = useWishlist();
|
||||||
const { isEnabled: isModuleEnabled } = useModules();
|
const { isEnabled: isModuleEnabled } = useModules();
|
||||||
|
|
||||||
|
// Apply white background to <main> in flat mode so the full viewport width is white
|
||||||
|
useEffect(() => {
|
||||||
|
const main = document.querySelector('main');
|
||||||
|
if (!main) return;
|
||||||
|
if (layout.layout_style === 'flat') {
|
||||||
|
(main as HTMLElement).style.backgroundColor = '#ffffff';
|
||||||
|
} else {
|
||||||
|
(main as HTMLElement).style.backgroundColor = '';
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
(main as HTMLElement).style.backgroundColor = '';
|
||||||
|
};
|
||||||
|
}, [layout.layout_style]);
|
||||||
|
|
||||||
// Fetch product details by slug
|
// Fetch product details by slug
|
||||||
const { data: product, isLoading, error } = useQuery<ProductType | null>({
|
const { data: product, isLoading, error } = useQuery<ProductType | null>({
|
||||||
queryKey: ['product', slug],
|
queryKey: ['product', slug],
|
||||||
@@ -94,10 +108,16 @@ export default function Product() {
|
|||||||
// Find matching variation when attributes change
|
// Find matching variation when attributes change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (product?.type === 'variable' && product.variations && Object.keys(selectedAttributes).length > 0) {
|
if (product?.type === 'variable' && product.variations && Object.keys(selectedAttributes).length > 0) {
|
||||||
const variation = (product.variations as any[]).find(v => {
|
let bestMatch: any = null;
|
||||||
if (!v.attributes) return false;
|
let highestScore = -1;
|
||||||
|
|
||||||
return Object.entries(selectedAttributes).every(([attrName, attrValue]) => {
|
(product.variations as any[]).forEach(v => {
|
||||||
|
if (!v.attributes) return;
|
||||||
|
|
||||||
|
let isMatch = true;
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
const attributesMatch = Object.entries(selectedAttributes).every(([attrName, attrValue]) => {
|
||||||
const normalizedSelectedValue = attrValue.toLowerCase().trim();
|
const normalizedSelectedValue = attrValue.toLowerCase().trim();
|
||||||
const attrNameLower = attrName.toLowerCase();
|
const attrNameLower = attrName.toLowerCase();
|
||||||
|
|
||||||
@@ -108,17 +128,11 @@ export default function Product() {
|
|||||||
// Try to find a matching key in the variation attributes
|
// Try to find a matching key in the variation attributes
|
||||||
let variationValue: string | undefined = undefined;
|
let variationValue: string | undefined = undefined;
|
||||||
|
|
||||||
// Check for common WooCommerce attribute key formats
|
|
||||||
// 1. Check strict slug format (attribute_7-days-...)
|
|
||||||
if (`attribute_${attrSlug}` in v.attributes) {
|
if (`attribute_${attrSlug}` in v.attributes) {
|
||||||
variationValue = v.attributes[`attribute_${attrSlug}`];
|
variationValue = v.attributes[`attribute_${attrSlug}`];
|
||||||
}
|
} else if (`attribute_pa_${attrSlug}` in v.attributes) {
|
||||||
// 2. Check pa_ format (attribute_pa_color)
|
|
||||||
else if (`attribute_pa_${attrSlug}` in v.attributes) {
|
|
||||||
variationValue = v.attributes[`attribute_pa_${attrSlug}`];
|
variationValue = v.attributes[`attribute_pa_${attrSlug}`];
|
||||||
}
|
} else if (`attribute_${attrNameLower}` in v.attributes) {
|
||||||
// 3. Fallback to name-based checks (legacy)
|
|
||||||
else if (`attribute_${attrNameLower}` in v.attributes) {
|
|
||||||
variationValue = v.attributes[`attribute_${attrNameLower}`];
|
variationValue = v.attributes[`attribute_${attrNameLower}`];
|
||||||
} else if (`attribute_pa_${attrNameLower}` in v.attributes) {
|
} else if (`attribute_pa_${attrNameLower}` in v.attributes) {
|
||||||
variationValue = v.attributes[`attribute_pa_${attrNameLower}`];
|
variationValue = v.attributes[`attribute_pa_${attrNameLower}`];
|
||||||
@@ -126,23 +140,34 @@ export default function Product() {
|
|||||||
variationValue = v.attributes[attrNameLower];
|
variationValue = v.attributes[attrNameLower];
|
||||||
}
|
}
|
||||||
|
|
||||||
// If key is undefined/missing in variation, it means "Any" -> Match
|
// If key is undefined/missing in variation, it means "Any" -> Match with score 0
|
||||||
if (variationValue === undefined || variationValue === null) {
|
if (variationValue === undefined || variationValue === null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If empty string, it also means "Any" -> Match
|
// If empty string, it also means "Any" -> Match with score 0
|
||||||
const normalizedVarValue = String(variationValue).toLowerCase().trim();
|
const normalizedVarValue = String(variationValue).toLowerCase().trim();
|
||||||
if (normalizedVarValue === '') {
|
if (normalizedVarValue === '') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, values must match
|
// Exact match gets a higher score
|
||||||
return normalizedVarValue === normalizedSelectedValue;
|
if (normalizedVarValue === normalizedSelectedValue) {
|
||||||
});
|
score += 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value mismatch
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
setSelectedVariation(variation || null);
|
if (attributesMatch && score > highestScore) {
|
||||||
|
highestScore = score;
|
||||||
|
bestMatch = v;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setSelectedVariation(bestMatch || null);
|
||||||
} else if (product?.type !== 'variable') {
|
} else if (product?.type !== 'variable') {
|
||||||
setSelectedVariation(null);
|
setSelectedVariation(null);
|
||||||
}
|
}
|
||||||
@@ -317,7 +342,10 @@ export default function Product() {
|
|||||||
availability: stockStatus === 'instock' ? 'in stock' : 'out of stock',
|
availability: stockStatus === 'instock' ? 'in stock' : 'out of stock',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="max-w-6xl mx-auto py-8">
|
{/* Flat: entire Container is bg-white. Card: per-section white cards on gray. */}
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
{/* Top section: flat = no card wrapper, card = white card */}
|
||||||
|
<div className={layout.layout_style === 'card' ? 'bg-white rounded-2xl shadow-sm border border-gray-100 p-6 lg:p-8 xl:p-10 mb-8' : 'mb-8'}>
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
{elements.breadcrumbs && (
|
{elements.breadcrumbs && (
|
||||||
<nav className="mb-6 text-sm">
|
<nav className="mb-6 text-sm">
|
||||||
@@ -329,7 +357,7 @@ export default function Product() {
|
|||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`grid gap-6 lg:gap-12 ${layout.image_position === 'right' ? 'lg:grid-cols-[42%_58%]' : 'lg:grid-cols-[58%_42%]'}`}>
|
<div className={`grid gap-6 lg:gap-8 ${layout.image_position === 'right' ? 'lg:grid-cols-[5fr_7fr]' : 'lg:grid-cols-[7fr_5fr]'}`}>
|
||||||
{/* Product Images */}
|
{/* Product Images */}
|
||||||
<div className={`lg:sticky lg:top-8 lg:self-start ${layout.image_position === 'right' ? 'lg:order-2' : ''}`}>
|
<div className={`lg:sticky lg:top-8 lg:self-start ${layout.image_position === 'right' ? 'lg:order-2' : ''}`}>
|
||||||
{/* Main Image - ENHANCED */}
|
{/* Main Image - ENHANCED */}
|
||||||
@@ -660,14 +688,18 @@ export default function Product() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Product Information - VERTICAL SECTIONS (Research: 27% overlook tabs) */}
|
{/* Product Information - VERTICAL SECTIONS (Research: 27% overlook tabs) */}
|
||||||
<div className="mt-12 space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Description Section */}
|
{/* Description Section */}
|
||||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
<div className={layout.layout_style === 'card'
|
||||||
|
? 'bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden'
|
||||||
|
: 'border-t border-gray-200 overflow-hidden'
|
||||||
|
}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab(activeTab === 'description' ? '' : 'description')}
|
onClick={() => setActiveTab(activeTab === 'description' ? '' : 'description')}
|
||||||
className="w-full flex items-center justify-between p-5 bg-gray-50 hover:bg-gray-100 transition-colors"
|
className="w-full flex items-center justify-between p-5 hover:bg-gray-50 transition-colors bg-transparent"
|
||||||
>
|
>
|
||||||
<h2 className="text-xl font-bold text-gray-900">Product Description</h2>
|
<h2 className="text-xl font-bold text-gray-900">Product Description</h2>
|
||||||
<svg
|
<svg
|
||||||
@@ -694,10 +726,13 @@ export default function Product() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Specifications Section - SCANNABLE TABLE */}
|
{/* Specifications Section - SCANNABLE TABLE */}
|
||||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
<div className={layout.layout_style === 'card'
|
||||||
|
? 'bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden'
|
||||||
|
: 'border-t border-gray-200 overflow-hidden'
|
||||||
|
}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab(activeTab === 'additional' ? '' : 'additional')}
|
onClick={() => setActiveTab(activeTab === 'additional' ? '' : 'additional')}
|
||||||
className="w-full flex items-center justify-between p-5 bg-gray-50 hover:bg-gray-100 transition-colors"
|
className="w-full flex items-center justify-between p-5 hover:bg-gray-50 transition-colors bg-transparent"
|
||||||
>
|
>
|
||||||
<h2 className="text-xl font-bold text-gray-900">Specifications</h2>
|
<h2 className="text-xl font-bold text-gray-900">Specifications</h2>
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
39
customer-spa/src/pages/Subscribe/index.tsx
Normal file
39
customer-spa/src/pages/Subscribe/index.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Helmet } from 'react-helmet-async';
|
||||||
|
import { NewsletterForm } from '@/components/NewsletterForm';
|
||||||
|
|
||||||
|
export default function Subscribe() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[70vh] flex flex-col items-center justify-center py-20 px-4 bg-gray-50/50">
|
||||||
|
<Helmet>
|
||||||
|
<title>Subscribe | WooNooW</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<div className="max-w-md w-full bg-white p-8 md:p-10 rounded-2xl shadow-sm border border-gray-100 text-center space-y-6">
|
||||||
|
<div className="w-16 h-16 bg-primary/10 text-primary rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-2xl md:text-3xl font-bold text-gray-900 tracking-tight">
|
||||||
|
Subscribe to our Newsletter
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 leading-relaxed">
|
||||||
|
Get the latest updates, articles, and exclusive offers straight to your inbox. No spam, ever.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="pt-4 mt-8 text-left">
|
||||||
|
<NewsletterForm
|
||||||
|
gdprRequired={true}
|
||||||
|
consentText="I agree to receive marketing emails and understand I can unsubscribe at any time."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-gray-400 mt-6 pt-6 border-t border-gray-100">
|
||||||
|
By subscribing, you agree to our Terms of Service and Privacy Policy.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,5 +25,5 @@ module.exports = {
|
|||||||
borderRadius: { lg: "12px", md: "10px", sm: "8px" }
|
borderRadius: { lg: "12px", md: "10px", sm: "8px" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")]
|
plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")]
|
||||||
};
|
};
|
||||||
@@ -381,6 +381,7 @@ class AppearanceController
|
|||||||
'image_position' => sanitize_text_field($data['layout']['image_position'] ?? 'left'),
|
'image_position' => sanitize_text_field($data['layout']['image_position'] ?? 'left'),
|
||||||
'gallery_style' => sanitize_text_field($data['layout']['gallery_style'] ?? 'thumbnails'),
|
'gallery_style' => sanitize_text_field($data['layout']['gallery_style'] ?? 'thumbnails'),
|
||||||
'sticky_add_to_cart' => (bool) ($data['layout']['sticky_add_to_cart'] ?? false),
|
'sticky_add_to_cart' => (bool) ($data['layout']['sticky_add_to_cart'] ?? false),
|
||||||
|
'layout_style' => sanitize_text_field($data['layout']['layout_style'] ?? 'flat'),
|
||||||
],
|
],
|
||||||
'elements' => [
|
'elements' => [
|
||||||
'breadcrumbs' => (bool) ($data['elements']['breadcrumbs'] ?? true),
|
'breadcrumbs' => (bool) ($data['elements']['breadcrumbs'] ?? true),
|
||||||
@@ -601,7 +602,11 @@ class AppearanceController
|
|||||||
'show_icon' => true,
|
'show_icon' => true,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'product' => [],
|
'product' => [
|
||||||
|
'layout' => [
|
||||||
|
'layout_style' => 'flat',
|
||||||
|
],
|
||||||
|
],
|
||||||
'cart' => [],
|
'cart' => [],
|
||||||
'checkout' => [],
|
'checkout' => [],
|
||||||
'thankyou' => [],
|
'thankyou' => [],
|
||||||
|
|||||||
@@ -75,7 +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),
|
'onboardingCompleted' => (get_option('woonoow_onboarding_completed', false) === 'yes' || get_option('woonoow_onboarding_completed') == 1),
|
||||||
]);
|
]);
|
||||||
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');
|
||||||
|
|
||||||
@@ -201,7 +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),
|
'onboardingCompleted' => (get_option('woonoow_onboarding_completed', false) === 'yes' || get_option('woonoow_onboarding_completed') == 1),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// WordPress REST API settings (for media upload compatibility)
|
// WordPress REST API settings (for media upload compatibility)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace WooNooW\Admin;
|
namespace WooNooW\Admin;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -9,28 +10,31 @@ namespace WooNooW\Admin;
|
|||||||
*
|
*
|
||||||
* @package WooNooW\Admin
|
* @package WooNooW\Admin
|
||||||
*/
|
*/
|
||||||
class StandaloneAdmin {
|
class StandaloneAdmin
|
||||||
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize standalone admin handler
|
* Initialize standalone admin handler
|
||||||
*/
|
*/
|
||||||
public static function init() {
|
public static function init()
|
||||||
|
{
|
||||||
// Catch /admin requests very early (before WordPress routing)
|
// Catch /admin requests very early (before WordPress routing)
|
||||||
add_action( 'parse_request', [ __CLASS__, 'handle_admin_request' ], 1 );
|
add_action('parse_request', [__CLASS__, 'handle_admin_request'], 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle /admin requests
|
* Handle /admin requests
|
||||||
*/
|
*/
|
||||||
public static function handle_admin_request() {
|
public static function handle_admin_request()
|
||||||
|
{
|
||||||
// Check if this is an /admin request
|
// Check if this is an /admin request
|
||||||
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
|
$request_uri = $_SERVER['REQUEST_URI'] ?? '';
|
||||||
|
|
||||||
// Remove query string
|
// Remove query string
|
||||||
$path = strtok( $request_uri, '?' );
|
$path = strtok($request_uri, '?');
|
||||||
|
|
||||||
// Only handle exact /admin or /admin/ paths (not asset files)
|
// Only handle exact /admin or /admin/ paths (not asset files)
|
||||||
if ( $path !== '/admin' && $path !== '/admin/' ) {
|
if ($path !== '/admin' && $path !== '/admin/') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,33 +46,34 @@ class StandaloneAdmin {
|
|||||||
/**
|
/**
|
||||||
* Render standalone admin interface
|
* Render standalone admin interface
|
||||||
*/
|
*/
|
||||||
private static function render_standalone_admin() {
|
private static function render_standalone_admin()
|
||||||
|
{
|
||||||
// Enqueue WordPress media library (needed for image uploads)
|
// Enqueue WordPress media library (needed for image uploads)
|
||||||
wp_enqueue_media();
|
wp_enqueue_media();
|
||||||
|
|
||||||
// Check if user is logged in and has permissions
|
// Check if user is logged in and has permissions
|
||||||
$is_logged_in = is_user_logged_in();
|
$is_logged_in = is_user_logged_in();
|
||||||
$has_permission = $is_logged_in && current_user_can( 'manage_woocommerce' );
|
$has_permission = $is_logged_in && current_user_can('manage_woocommerce');
|
||||||
$is_authenticated = $is_logged_in && $has_permission;
|
$is_authenticated = $is_logged_in && $has_permission;
|
||||||
|
|
||||||
// Debug logging (only in WP_DEBUG mode)
|
// Debug logging (only in WP_DEBUG mode)
|
||||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get nonce for REST API
|
// Get nonce for REST API
|
||||||
$nonce = wp_create_nonce( 'wp_rest' );
|
$nonce = wp_create_nonce('wp_rest');
|
||||||
$rest_url = untrailingslashit( rest_url( 'woonoow/v1' ) );
|
$rest_url = untrailingslashit(rest_url('woonoow/v1'));
|
||||||
$wp_admin_url = admin_url( 'admin.php?page=woonoow' );
|
$wp_admin_url = admin_url('admin.php?page=woonoow');
|
||||||
|
|
||||||
// Get current user data if authenticated
|
// Get current user data if authenticated
|
||||||
$current_user = null;
|
$current_user = null;
|
||||||
if ( $is_authenticated ) {
|
if ($is_authenticated) {
|
||||||
$user = wp_get_current_user();
|
$user = wp_get_current_user();
|
||||||
$current_user = [
|
$current_user = [
|
||||||
'id' => $user->ID,
|
'id' => $user->ID,
|
||||||
'name' => $user->display_name,
|
'name' => $user->display_name,
|
||||||
'email' => $user->user_email,
|
'email' => $user->user_email,
|
||||||
'avatar' => get_avatar_url( $user->ID ),
|
'avatar' => get_avatar_url($user->ID),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,101 +81,105 @@ class StandaloneAdmin {
|
|||||||
$store_settings = self::get_store_settings();
|
$store_settings = self::get_store_settings();
|
||||||
|
|
||||||
// Get asset URLs
|
// Get asset URLs
|
||||||
$plugin_url = plugins_url( '', dirname( dirname( __FILE__ ) ) );
|
$plugin_url = plugins_url('', dirname(dirname(__FILE__)));
|
||||||
$asset_url = $plugin_url . '/admin-spa/dist';
|
$asset_url = $plugin_url . '/admin-spa/dist';
|
||||||
|
|
||||||
// Cache busting
|
// Cache busting
|
||||||
$version = defined( 'WP_DEBUG' ) && WP_DEBUG ? time() : '1.0.0';
|
$version = defined('WP_DEBUG') && WP_DEBUG ? time() : '1.0.0';
|
||||||
$css_url = $asset_url . '/app.css?ver=' . $version;
|
$css_url = $asset_url . '/app.css?ver=' . $version;
|
||||||
$js_url = $asset_url . '/app.js?ver=' . $version;
|
$js_url = $asset_url . '/app.js?ver=' . $version;
|
||||||
|
|
||||||
// Render HTML
|
// Render HTML
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="<?php echo esc_attr( get_locale() ); ?>">
|
<html lang="<?php echo esc_attr(get_locale()); ?>">
|
||||||
<head>
|
|
||||||
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
<title><?php echo esc_html( get_option( 'blogname', 'WooNooW' ) ); ?> Admin</title>
|
<title><?php echo esc_html(get_option('blogname', 'WooNooW')); ?> Admin</title>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
// Favicon
|
// Favicon
|
||||||
$icon = get_option( 'woonoow_store_icon', '' );
|
$icon = get_option('woonoow_store_icon', '');
|
||||||
if ( ! empty( $icon ) ) {
|
if (! empty($icon)) {
|
||||||
?>
|
?>
|
||||||
<link rel="icon" type="image/png" href="<?php echo esc_url( $icon ); ?>" />
|
<link rel="icon" type="image/png" href="<?php echo esc_url($icon); ?>" />
|
||||||
<link rel="apple-touch-icon" href="<?php echo esc_url( $icon ); ?>" />
|
<link rel="apple-touch-icon" href="<?php echo esc_url($icon); ?>" />
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
// Print WordPress media library styles (complete set for proper modal)
|
// Print WordPress media library styles (complete set for proper modal)
|
||||||
wp_print_styles( 'media-views' );
|
wp_print_styles('media-views');
|
||||||
wp_print_styles( 'imgareaselect' );
|
wp_print_styles('imgareaselect');
|
||||||
wp_print_styles( 'buttons' );
|
wp_print_styles('buttons');
|
||||||
wp_print_styles( 'dashicons' );
|
wp_print_styles('dashicons');
|
||||||
wp_print_styles( 'wp-admin' );
|
wp_print_styles('wp-admin');
|
||||||
wp_print_styles( 'common' );
|
wp_print_styles('common');
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- WooNooW Assets -->
|
<!-- WooNooW Assets -->
|
||||||
<link rel="stylesheet" href="<?php echo esc_url( $css_url ); ?>">
|
<link rel="stylesheet" href="<?php echo esc_url($css_url); ?>">
|
||||||
</head>
|
</head>
|
||||||
<body class="woonoow-standalone">
|
|
||||||
|
<body class="woonoow-standalone">
|
||||||
<div id="woonoow-admin-app"></div>
|
<div id="woonoow-admin-app"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Minimal config - no WordPress bloat
|
// Minimal config - no WordPress bloat
|
||||||
window.WNW_CONFIG = {
|
window.WNW_CONFIG = {
|
||||||
restUrl: <?php echo wp_json_encode( $rest_url ); ?>,
|
restUrl: <?php echo wp_json_encode($rest_url); ?>,
|
||||||
nonce: <?php echo wp_json_encode( $nonce ); ?>,
|
nonce: <?php echo wp_json_encode($nonce); ?>,
|
||||||
standaloneMode: true,
|
standaloneMode: true,
|
||||||
wpAdminUrl: <?php echo wp_json_encode( $wp_admin_url ); ?>,
|
wpAdminUrl: <?php echo wp_json_encode($wp_admin_url); ?>,
|
||||||
isAuthenticated: <?php echo $is_authenticated ? 'true' : 'false'; ?>,
|
isAuthenticated: <?php echo $is_authenticated ? 'true' : 'false'; ?>,
|
||||||
currentUser: <?php echo wp_json_encode( $current_user ); ?>,
|
currentUser: <?php echo wp_json_encode($current_user); ?>,
|
||||||
locale: <?php echo wp_json_encode( get_locale() ); ?>,
|
locale: <?php echo wp_json_encode(get_locale()); ?>,
|
||||||
siteUrl: <?php echo wp_json_encode( home_url() ); ?>,
|
siteUrl: <?php echo wp_json_encode(home_url()); ?>,
|
||||||
siteName: <?php echo wp_json_encode( get_bloginfo( 'name' ) ); ?>,
|
siteName: <?php echo wp_json_encode(get_bloginfo('name')); ?>,
|
||||||
storeUrl: <?php echo wp_json_encode( self::get_spa_url() ); ?>,
|
storeUrl: <?php echo wp_json_encode(self::get_spa_url()); ?>,
|
||||||
customerSpaEnabled: <?php echo get_option( 'woonoow_customer_spa_enabled', false ) ? 'true' : 'false'; ?>
|
customerSpaEnabled: <?php echo get_option('woonoow_customer_spa_enabled', false) ? 'true' : 'false'; ?>,
|
||||||
|
onboardingCompleted: <?php echo (get_option('woonoow_onboarding_completed', false) === 'yes' || get_option('woonoow_onboarding_completed') == 1) ? 'true' : 'false'; ?>
|
||||||
};
|
};
|
||||||
|
|
||||||
// Also set WNW_API for API compatibility
|
// Also set WNW_API for API compatibility
|
||||||
window.WNW_API = {
|
window.WNW_API = {
|
||||||
root: <?php echo wp_json_encode( $rest_url ); ?>,
|
root: <?php echo wp_json_encode($rest_url); ?>,
|
||||||
nonce: <?php echo wp_json_encode( $nonce ); ?>,
|
nonce: <?php echo wp_json_encode($nonce); ?>,
|
||||||
isDev: <?php echo ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? 'true' : 'false'; ?>
|
isDev: <?php echo (defined('WP_DEBUG') && WP_DEBUG) ? 'true' : 'false'; ?>
|
||||||
};
|
};
|
||||||
|
|
||||||
// WooCommerce store settings (currency, formatting, etc.)
|
// WooCommerce store settings (currency, formatting, etc.)
|
||||||
window.WNW_STORE = <?php echo wp_json_encode( $store_settings ); ?>;
|
window.WNW_STORE = <?php echo wp_json_encode($store_settings); ?>;
|
||||||
|
|
||||||
// Navigation tree (single source of truth from PHP)
|
// Navigation tree (single source of truth from PHP)
|
||||||
window.WNW_NAV_TREE = <?php echo wp_json_encode( \WooNooW\Compat\NavigationRegistry::get_frontend_nav_tree() ); ?>;
|
window.WNW_NAV_TREE = <?php echo wp_json_encode(\WooNooW\Compat\NavigationRegistry::get_frontend_nav_tree()); ?>;
|
||||||
|
|
||||||
// WordPress REST API settings (for media upload compatibility)
|
// WordPress REST API settings (for media upload compatibility)
|
||||||
window.wpApiSettings = {
|
window.wpApiSettings = {
|
||||||
root: <?php echo wp_json_encode( untrailingslashit( rest_url() ) ); ?>,
|
root: <?php echo wp_json_encode(untrailingslashit(rest_url())); ?>,
|
||||||
nonce: <?php echo wp_json_encode( $nonce ); ?>,
|
nonce: <?php echo wp_json_encode($nonce); ?>,
|
||||||
versionString: 'wp/v2/'
|
versionString: 'wp/v2/'
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
// Print WordPress media library scripts (needed for wp.media)
|
// Print WordPress media library scripts (needed for wp.media)
|
||||||
wp_print_scripts( 'media-editor' );
|
wp_print_scripts('media-editor');
|
||||||
wp_print_scripts( 'media-audiovideo' );
|
wp_print_scripts('media-audiovideo');
|
||||||
|
|
||||||
// Print media templates (required for media modal to work)
|
// Print media templates (required for media modal to work)
|
||||||
wp_print_media_templates();
|
wp_print_media_templates();
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<script type="module" src="<?php echo esc_url( $js_url ); ?>"></script>
|
<script type="module" src="<?php echo esc_url($js_url); ?>"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
<?php
|
</html>
|
||||||
|
<?php
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -178,14 +187,15 @@ class StandaloneAdmin {
|
|||||||
*
|
*
|
||||||
* @return array Store settings (currency, decimals, separators, etc.)
|
* @return array Store settings (currency, decimals, separators, etc.)
|
||||||
*/
|
*/
|
||||||
private static function get_store_settings(): array {
|
private static function get_store_settings(): array
|
||||||
|
{
|
||||||
// Get WooCommerce settings with fallbacks
|
// Get WooCommerce settings with fallbacks
|
||||||
$currency = function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : 'USD';
|
$currency = function_exists('get_woocommerce_currency') ? get_woocommerce_currency() : 'USD';
|
||||||
$currency_sym = function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol( $currency ) : '$';
|
$currency_sym = function_exists('get_woocommerce_currency_symbol') ? get_woocommerce_currency_symbol($currency) : '$';
|
||||||
$decimals = function_exists( 'wc_get_price_decimals' ) ? wc_get_price_decimals() : 2;
|
$decimals = function_exists('wc_get_price_decimals') ? wc_get_price_decimals() : 2;
|
||||||
$thousand_sep = function_exists( 'wc_get_price_thousand_separator' ) ? wc_get_price_thousand_separator() : ',';
|
$thousand_sep = function_exists('wc_get_price_thousand_separator') ? wc_get_price_thousand_separator() : ',';
|
||||||
$decimal_sep = function_exists( 'wc_get_price_decimal_separator' ) ? wc_get_price_decimal_separator() : '.';
|
$decimal_sep = function_exists('wc_get_price_decimal_separator') ? wc_get_price_decimal_separator() : '.';
|
||||||
$currency_pos = get_option( 'woocommerce_currency_pos', 'left' );
|
$currency_pos = get_option('woocommerce_currency_pos', 'left');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'currency' => $currency,
|
'currency' => $currency,
|
||||||
@@ -200,17 +210,17 @@ class StandaloneAdmin {
|
|||||||
/** Get the SPA page URL from appearance settings (dynamic slug) */
|
/** Get the SPA page URL from appearance settings (dynamic slug) */
|
||||||
private static function get_spa_url(): string
|
private static function get_spa_url(): string
|
||||||
{
|
{
|
||||||
$appearance_settings = get_option( 'woonoow_appearance_settings', [] );
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||||
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
|
$spa_page_id = $appearance_settings['general']['spa_page'] ?? 0;
|
||||||
|
|
||||||
if ( $spa_page_id ) {
|
if ($spa_page_id) {
|
||||||
$spa_url = get_permalink( $spa_page_id );
|
$spa_url = get_permalink($spa_page_id);
|
||||||
if ( $spa_url ) {
|
if ($spa_url) {
|
||||||
return trailingslashit( $spa_url );
|
return trailingslashit($spa_url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to /store/ if no SPA page configured
|
// Fallback to /store/ if no SPA page configured
|
||||||
return home_url( '/store/' );
|
return home_url('/store/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class OnboardingController extends WP_REST_Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Mark as Complete
|
// 4. Mark as Complete
|
||||||
update_option('woonoow_onboarding_completed', true);
|
update_option('woonoow_onboarding_completed', 'yes');
|
||||||
|
|
||||||
return rest_ensure_response([
|
return rest_ensure_response([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class PagesController
|
|||||||
'permission_callback' => '__return_true',
|
'permission_callback' => '__return_true',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Get/Save CPT templates
|
// Get/Save/Delete CPT templates
|
||||||
register_rest_route($namespace, '/templates/(?P<cpt>[a-zA-Z0-9_-]+)', [
|
register_rest_route($namespace, '/templates/(?P<cpt>[a-zA-Z0-9_-]+)', [
|
||||||
[
|
[
|
||||||
'methods' => 'GET',
|
'methods' => 'GET',
|
||||||
@@ -70,6 +70,11 @@ class PagesController
|
|||||||
'callback' => [__CLASS__, 'save_template'],
|
'callback' => [__CLASS__, 'save_template'],
|
||||||
'permission_callback' => [__CLASS__, 'check_admin_permission'],
|
'permission_callback' => [__CLASS__, 'check_admin_permission'],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'methods' => 'DELETE',
|
||||||
|
'callback' => [__CLASS__, 'delete_template'],
|
||||||
|
'permission_callback' => [__CLASS__, 'check_admin_permission'],
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Get post with template applied (for SPA rendering)
|
// Get post with template applied (for SPA rendering)
|
||||||
@@ -337,6 +342,34 @@ class PagesController
|
|||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete CPT template (abort SPA handling for this post type)
|
||||||
|
*/
|
||||||
|
public static function delete_template(WP_REST_Request $request)
|
||||||
|
{
|
||||||
|
$cpt = $request->get_param('cpt');
|
||||||
|
|
||||||
|
// Validate CPT exists
|
||||||
|
if (!post_type_exists($cpt) && $cpt !== 'post') {
|
||||||
|
return new WP_Error('invalid_cpt', 'Invalid post type', ['status' => 400]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$option_key = "wn_template_{$cpt}";
|
||||||
|
$exists = get_option($option_key, null);
|
||||||
|
|
||||||
|
if ($exists === null) {
|
||||||
|
return new WP_Error('not_found', 'No template found for this post type', ['status' => 404]);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete_option($option_key);
|
||||||
|
|
||||||
|
return new WP_REST_Response([
|
||||||
|
'success' => true,
|
||||||
|
'cpt' => $cpt,
|
||||||
|
'message' => 'Template deleted. WordPress will now handle this post type natively.',
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get content with template applied (for SPA rendering)
|
* Get content with template applied (for SPA rendering)
|
||||||
*/
|
*/
|
||||||
@@ -378,7 +411,37 @@ class PagesController
|
|||||||
if ($template && !empty($template['sections'])) {
|
if ($template && !empty($template['sections'])) {
|
||||||
foreach ($template['sections'] as $section) {
|
foreach ($template['sections'] as $section) {
|
||||||
$resolved_section = $section;
|
$resolved_section = $section;
|
||||||
$resolved_section['props'] = PageSSR::resolve_props($section['props'] ?? [], $post_data);
|
|
||||||
|
// Pre-resolve special dynamic sources that produce arrays before PageSSR::resolve_props
|
||||||
|
$props = $section['props'] ?? [];
|
||||||
|
foreach ($props as $key => $prop) {
|
||||||
|
if (is_array($prop) && ($prop['type'] ?? '') === 'dynamic' && ($prop['source'] ?? '') === 'related_posts') {
|
||||||
|
$props[$key] = [
|
||||||
|
'type' => 'static',
|
||||||
|
'value' => PlaceholderRenderer::get_related_posts($post->ID, 3, $type),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolved_section['props'] = PageSSR::resolve_props($props, $post_data);
|
||||||
|
|
||||||
|
// Resolve dynamicBackground in styles
|
||||||
|
// If styles.dynamicBackground === 'post_featured_image', set styles.backgroundImage from post data
|
||||||
|
$styles = $resolved_section['styles'] ?? [];
|
||||||
|
if (!empty($styles['dynamicBackground']) && (empty($styles['backgroundType']) || $styles['backgroundType'] === 'image')) {
|
||||||
|
$dyn_source = $styles['dynamicBackground'];
|
||||||
|
if ($dyn_source === 'post_featured_image' || $dyn_source === 'featured_image') {
|
||||||
|
$featured_url = $post_data['featured_image'] ?? '';
|
||||||
|
if (!empty($featured_url)) {
|
||||||
|
$styles['backgroundImage'] = $featured_url;
|
||||||
|
$styles['backgroundType'] = 'image';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Remove the internal marker from the rendered output
|
||||||
|
unset($styles['dynamicBackground']);
|
||||||
|
$resolved_section['styles'] = $styles;
|
||||||
|
}
|
||||||
|
|
||||||
$rendered_sections[] = $resolved_section;
|
$rendered_sections[] = $resolved_section;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,18 @@ class ProductsController
|
|||||||
return trim($sanitized);
|
return trim($sanitized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize rich text (allows HTML tags)
|
||||||
|
*/
|
||||||
|
private static function sanitize_rich_text($value)
|
||||||
|
{
|
||||||
|
if (!isset($value) || $value === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$sanitized = wp_kses_post($value);
|
||||||
|
return trim($sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitize numeric value
|
* Sanitize numeric value
|
||||||
*/
|
*/
|
||||||
@@ -335,8 +347,12 @@ class ProductsController
|
|||||||
$product->set_slug(self::sanitize_slug($data['slug']));
|
$product->set_slug(self::sanitize_slug($data['slug']));
|
||||||
}
|
}
|
||||||
$product->set_status(sanitize_key($data['status'] ?? 'publish'));
|
$product->set_status(sanitize_key($data['status'] ?? 'publish'));
|
||||||
$product->set_description(self::sanitize_textarea($data['description'] ?? ''));
|
if (isset($data['description'])) {
|
||||||
|
$product->set_description(self::sanitize_rich_text($data['description'] ?? ''));
|
||||||
|
}
|
||||||
|
if (isset($data['short_description'])) {
|
||||||
$product->set_short_description(self::sanitize_textarea($data['short_description'] ?? ''));
|
$product->set_short_description(self::sanitize_textarea($data['short_description'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
if (!empty($data['sku'])) {
|
if (!empty($data['sku'])) {
|
||||||
$product->set_sku(self::sanitize_text($data['sku']));
|
$product->set_sku(self::sanitize_text($data['sku']));
|
||||||
@@ -489,7 +505,7 @@ class ProductsController
|
|||||||
if (isset($data['name'])) $product->set_name(self::sanitize_text($data['name']));
|
if (isset($data['name'])) $product->set_name(self::sanitize_text($data['name']));
|
||||||
if (isset($data['slug'])) $product->set_slug(self::sanitize_slug($data['slug']));
|
if (isset($data['slug'])) $product->set_slug(self::sanitize_slug($data['slug']));
|
||||||
if (isset($data['status'])) $product->set_status(sanitize_key($data['status']));
|
if (isset($data['status'])) $product->set_status(sanitize_key($data['status']));
|
||||||
if (isset($data['description'])) $product->set_description(self::sanitize_textarea($data['description']));
|
if (isset($data['description'])) $product->set_description(self::sanitize_rich_text($data['description']));
|
||||||
if (isset($data['short_description'])) $product->set_short_description(self::sanitize_textarea($data['short_description']));
|
if (isset($data['short_description'])) $product->set_short_description(self::sanitize_textarea($data['short_description']));
|
||||||
if (isset($data['sku'])) $product->set_sku(self::sanitize_text($data['sku']));
|
if (isset($data['sku'])) $product->set_sku(self::sanitize_text($data['sku']));
|
||||||
|
|
||||||
@@ -942,10 +958,17 @@ class ProductsController
|
|||||||
$value = $term ? $term->name : $value;
|
$value = $term ? $term->name : $value;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Custom attribute - stored as lowercase in meta
|
// Custom attribute - stored as sanitize_title in meta
|
||||||
$meta_key = 'attribute_' . strtolower($attr_name);
|
$sanitized_name = sanitize_title($attr_name);
|
||||||
|
$meta_key = 'attribute_' . $sanitized_name;
|
||||||
$value = get_post_meta($variation_id, $meta_key, true);
|
$value = get_post_meta($variation_id, $meta_key, true);
|
||||||
|
|
||||||
|
// Fallback to legacy lowercase if not found
|
||||||
|
if ($value === '') {
|
||||||
|
$meta_key_legacy = 'attribute_' . strtolower($attr_name);
|
||||||
|
$value = get_post_meta($variation_id, $meta_key_legacy, true);
|
||||||
|
}
|
||||||
|
|
||||||
// Capitalize the attribute name for display to match admin SPA
|
// Capitalize the attribute name for display to match admin SPA
|
||||||
$clean_name = ucfirst($attr_name);
|
$clean_name = ucfirst($attr_name);
|
||||||
}
|
}
|
||||||
@@ -1029,8 +1052,27 @@ class ProductsController
|
|||||||
|
|
||||||
foreach ($parent_attributes as $attr_name => $parent_attr) {
|
foreach ($parent_attributes as $attr_name => $parent_attr) {
|
||||||
if (!$parent_attr->get_variation()) continue;
|
if (!$parent_attr->get_variation()) continue;
|
||||||
if (strcasecmp($display_name, $attr_name) === 0 || strcasecmp($display_name, ucfirst($attr_name)) === 0) {
|
|
||||||
$wc_attributes[strtolower($attr_name)] = strtolower($value);
|
$is_match = false;
|
||||||
|
if (strpos($attr_name, 'pa_') === 0) {
|
||||||
|
$label = wc_attribute_label($attr_name);
|
||||||
|
if (strcasecmp($display_name, $label) === 0 || strcasecmp($display_name, $attr_name) === 0) {
|
||||||
|
$is_match = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Custom attribute: Check exact name, or sanitized version
|
||||||
|
if (
|
||||||
|
strcasecmp($display_name, $attr_name) === 0 ||
|
||||||
|
strcasecmp($display_name, $parent_attr->get_name()) === 0 ||
|
||||||
|
sanitize_title($display_name) === sanitize_title($attr_name)
|
||||||
|
) {
|
||||||
|
$is_match = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($is_match) {
|
||||||
|
// WooCommerce expects the exact attribute slug as the key
|
||||||
|
$wc_attributes[$attr_name] = $value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1095,7 +1137,7 @@ class ProductsController
|
|||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
foreach ($wc_attributes as $attr_name => $attr_value) {
|
foreach ($wc_attributes as $attr_name => $attr_value) {
|
||||||
$meta_key = 'attribute_' . $attr_name;
|
$meta_key = 'attribute_' . sanitize_title($attr_name);
|
||||||
|
|
||||||
$wpdb->delete(
|
$wpdb->delete(
|
||||||
$wpdb->postmeta,
|
$wpdb->postmeta,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Email Manager
|
* Email Manager
|
||||||
*
|
*
|
||||||
@@ -9,7 +10,8 @@
|
|||||||
|
|
||||||
namespace WooNooW\Core\Notifications;
|
namespace WooNooW\Core\Notifications;
|
||||||
|
|
||||||
class EmailManager {
|
class EmailManager
|
||||||
|
{
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instance
|
* Instance
|
||||||
@@ -19,7 +21,8 @@ class EmailManager {
|
|||||||
/**
|
/**
|
||||||
* Get instance
|
* Get instance
|
||||||
*/
|
*/
|
||||||
public static function instance() {
|
public static function instance()
|
||||||
|
{
|
||||||
if (null === self::$instance) {
|
if (null === self::$instance) {
|
||||||
self::$instance = new self();
|
self::$instance = new self();
|
||||||
}
|
}
|
||||||
@@ -29,14 +32,16 @@ class EmailManager {
|
|||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
*/
|
*/
|
||||||
private function __construct() {
|
private function __construct()
|
||||||
|
{
|
||||||
$this->init_hooks();
|
$this->init_hooks();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize hooks
|
* Initialize hooks
|
||||||
*/
|
*/
|
||||||
private function init_hooks() {
|
private function init_hooks()
|
||||||
|
{
|
||||||
// Disable WooCommerce emails to prevent duplicates
|
// Disable WooCommerce emails to prevent duplicates
|
||||||
add_action('woocommerce_email', [$this, 'disable_wc_emails'], 1);
|
add_action('woocommerce_email', [$this, 'disable_wc_emails'], 1);
|
||||||
|
|
||||||
@@ -74,7 +79,8 @@ class EmailManager {
|
|||||||
*
|
*
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public static function is_enabled() {
|
public static function is_enabled()
|
||||||
|
{
|
||||||
// Check global notification system mode
|
// Check global notification system mode
|
||||||
$system_mode = get_option('woonoow_notification_system_mode', 'woonoow');
|
$system_mode = get_option('woonoow_notification_system_mode', 'woonoow');
|
||||||
return $system_mode === 'woonoow';
|
return $system_mode === 'woonoow';
|
||||||
@@ -85,7 +91,8 @@ class EmailManager {
|
|||||||
*
|
*
|
||||||
* @param WC_Emails $email_class
|
* @param WC_Emails $email_class
|
||||||
*/
|
*/
|
||||||
public function disable_wc_emails($email_class) {
|
public function disable_wc_emails($email_class)
|
||||||
|
{
|
||||||
// Only disable WC emails if WooNooW system is enabled
|
// Only disable WC emails if WooNooW system is enabled
|
||||||
if (!self::is_enabled()) {
|
if (!self::is_enabled()) {
|
||||||
return; // Keep WC emails if WooNooW system disabled
|
return; // Keep WC emails if WooNooW system disabled
|
||||||
@@ -117,7 +124,8 @@ class EmailManager {
|
|||||||
* @param int $order_id
|
* @param int $order_id
|
||||||
* @param WC_Order $order
|
* @param WC_Order $order
|
||||||
*/
|
*/
|
||||||
public function send_order_processing_email($order_id, $order = null) {
|
public function send_order_processing_email($order_id, $order = null)
|
||||||
|
{
|
||||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +159,8 @@ class EmailManager {
|
|||||||
* @param int $order_id
|
* @param int $order_id
|
||||||
* @param WC_Order $order
|
* @param WC_Order $order
|
||||||
*/
|
*/
|
||||||
public function send_order_completed_email($order_id, $order = null) {
|
public function send_order_completed_email($order_id, $order = null)
|
||||||
|
{
|
||||||
if (!$order) {
|
if (!$order) {
|
||||||
$order = wc_get_order($order_id);
|
$order = wc_get_order($order_id);
|
||||||
}
|
}
|
||||||
@@ -175,7 +184,8 @@ class EmailManager {
|
|||||||
* @param int $order_id
|
* @param int $order_id
|
||||||
* @param WC_Order $order
|
* @param WC_Order $order
|
||||||
*/
|
*/
|
||||||
public function send_order_on_hold_email($order_id, $order = null) {
|
public function send_order_on_hold_email($order_id, $order = null)
|
||||||
|
{
|
||||||
if (!$order) {
|
if (!$order) {
|
||||||
$order = wc_get_order($order_id);
|
$order = wc_get_order($order_id);
|
||||||
}
|
}
|
||||||
@@ -199,7 +209,8 @@ class EmailManager {
|
|||||||
* @param int $order_id
|
* @param int $order_id
|
||||||
* @param WC_Order $order
|
* @param WC_Order $order
|
||||||
*/
|
*/
|
||||||
public function send_order_cancelled_email($order_id, $order = null) {
|
public function send_order_cancelled_email($order_id, $order = null)
|
||||||
|
{
|
||||||
if (!$order) {
|
if (!$order) {
|
||||||
$order = wc_get_order($order_id);
|
$order = wc_get_order($order_id);
|
||||||
}
|
}
|
||||||
@@ -220,7 +231,8 @@ class EmailManager {
|
|||||||
* @param int $order_id
|
* @param int $order_id
|
||||||
* @param WC_Order $order
|
* @param WC_Order $order
|
||||||
*/
|
*/
|
||||||
public function send_order_refunded_email($order_id, $order = null) {
|
public function send_order_refunded_email($order_id, $order = null)
|
||||||
|
{
|
||||||
if (!$order) {
|
if (!$order) {
|
||||||
$order = wc_get_order($order_id);
|
$order = wc_get_order($order_id);
|
||||||
}
|
}
|
||||||
@@ -243,7 +255,8 @@ class EmailManager {
|
|||||||
*
|
*
|
||||||
* @param int $order_id
|
* @param int $order_id
|
||||||
*/
|
*/
|
||||||
public function send_new_order_admin_email($order_id) {
|
public function send_new_order_admin_email($order_id)
|
||||||
|
{
|
||||||
$order = wc_get_order($order_id);
|
$order = wc_get_order($order_id);
|
||||||
|
|
||||||
if (!$order) {
|
if (!$order) {
|
||||||
@@ -264,7 +277,8 @@ class EmailManager {
|
|||||||
*
|
*
|
||||||
* @param array $args
|
* @param array $args
|
||||||
*/
|
*/
|
||||||
public function send_customer_note_email($args) {
|
public function send_customer_note_email($args)
|
||||||
|
{
|
||||||
$order = wc_get_order($args['order_id']);
|
$order = wc_get_order($args['order_id']);
|
||||||
|
|
||||||
if (!$order) {
|
if (!$order) {
|
||||||
@@ -276,8 +290,8 @@ class EmailManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send email with note data
|
// Send email with note data — key must match {customer_note} variable in template
|
||||||
$this->send_email('customer_note', 'customer', $order, ['note' => $args['customer_note']]);
|
$this->send_email('customer_note', 'customer', $order, ['customer_note' => $args['customer_note']]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -287,7 +301,8 @@ class EmailManager {
|
|||||||
* @param array $new_customer_data
|
* @param array $new_customer_data
|
||||||
* @param bool $password_generated
|
* @param bool $password_generated
|
||||||
*/
|
*/
|
||||||
public function send_new_customer_email($customer_id, $new_customer_data = [], $password_generated = false) {
|
public function send_new_customer_email($customer_id, $new_customer_data = [], $password_generated = false)
|
||||||
|
{
|
||||||
// Check if event is enabled
|
// Check if event is enabled
|
||||||
if (!$this->is_event_enabled('new_customer', 'email', 'customer')) {
|
if (!$this->is_event_enabled('new_customer', 'email', 'customer')) {
|
||||||
return;
|
return;
|
||||||
@@ -312,7 +327,8 @@ class EmailManager {
|
|||||||
* @param WP_User $user_data User object
|
* @param WP_User $user_data User object
|
||||||
* @return string Empty string to prevent WordPress sending default email
|
* @return string Empty string to prevent WordPress sending default email
|
||||||
*/
|
*/
|
||||||
public function handle_password_reset_email($message, $key, $user_login, $user_data) {
|
public function handle_password_reset_email($message, $key, $user_login, $user_data)
|
||||||
|
{
|
||||||
// Check if WooNooW notification system is enabled
|
// Check if WooNooW notification system is enabled
|
||||||
if (!self::is_enabled()) {
|
if (!self::is_enabled()) {
|
||||||
return $message; // Use WordPress default
|
return $message; // Use WordPress default
|
||||||
@@ -371,7 +387,8 @@ class EmailManager {
|
|||||||
* @param string $reset_link Full reset link URL
|
* @param string $reset_link Full reset link URL
|
||||||
* @param WC_Customer|null $customer WooCommerce customer object if available
|
* @param WC_Customer|null $customer WooCommerce customer object if available
|
||||||
*/
|
*/
|
||||||
private function send_password_reset_email($user, $key, $reset_link, $customer = null) {
|
private function send_password_reset_email($user, $key, $reset_link, $customer = null)
|
||||||
|
{
|
||||||
// Get email renderer
|
// Get email renderer
|
||||||
$renderer = EmailRenderer::instance();
|
$renderer = EmailRenderer::instance();
|
||||||
|
|
||||||
@@ -417,14 +434,18 @@ class EmailManager {
|
|||||||
*
|
*
|
||||||
* @param WC_Product $product
|
* @param WC_Product $product
|
||||||
*/
|
*/
|
||||||
public function send_low_stock_email($product) {
|
public function send_low_stock_email($product)
|
||||||
|
{
|
||||||
// Check if event is enabled
|
// Check if event is enabled
|
||||||
if (!$this->is_event_enabled('low_stock', 'email', 'staff')) {
|
if (!$this->is_event_enabled('low_stock', 'email', 'staff')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send email
|
// Pass low_stock_threshold so template can display it
|
||||||
$this->send_email('low_stock', 'staff', $product);
|
$low_stock_threshold = get_option('woocommerce_notify_low_stock_amount', 2);
|
||||||
|
$this->send_email('low_stock', 'staff', $product, [
|
||||||
|
'low_stock_threshold' => $low_stock_threshold,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -432,7 +453,8 @@ class EmailManager {
|
|||||||
*
|
*
|
||||||
* @param WC_Product $product
|
* @param WC_Product $product
|
||||||
*/
|
*/
|
||||||
public function send_out_of_stock_email($product) {
|
public function send_out_of_stock_email($product)
|
||||||
|
{
|
||||||
// Check if event is enabled
|
// Check if event is enabled
|
||||||
if (!$this->is_event_enabled('out_of_stock', 'email', 'staff')) {
|
if (!$this->is_event_enabled('out_of_stock', 'email', 'staff')) {
|
||||||
return;
|
return;
|
||||||
@@ -447,7 +469,8 @@ class EmailManager {
|
|||||||
*
|
*
|
||||||
* @param WC_Product $product
|
* @param WC_Product $product
|
||||||
*/
|
*/
|
||||||
public function check_stock_levels($product) {
|
public function check_stock_levels($product)
|
||||||
|
{
|
||||||
$stock = $product->get_stock_quantity();
|
$stock = $product->get_stock_quantity();
|
||||||
$low_stock_threshold = get_option('woocommerce_notify_low_stock_amount', 2);
|
$low_stock_threshold = get_option('woocommerce_notify_low_stock_amount', 2);
|
||||||
|
|
||||||
@@ -466,7 +489,8 @@ class EmailManager {
|
|||||||
* @param string $recipient_type
|
* @param string $recipient_type
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
private function is_event_enabled($event_id, $channel_id, $recipient_type) {
|
private function is_event_enabled($event_id, $channel_id, $recipient_type)
|
||||||
|
{
|
||||||
$settings = get_option('woonoow_notification_settings', []);
|
$settings = get_option('woonoow_notification_settings', []);
|
||||||
|
|
||||||
// Check if event exists and channel is configured
|
// Check if event exists and channel is configured
|
||||||
@@ -493,7 +517,8 @@ class EmailManager {
|
|||||||
* @param mixed $data
|
* @param mixed $data
|
||||||
* @param array $extra_data
|
* @param array $extra_data
|
||||||
*/
|
*/
|
||||||
private function send_email($event_id, $recipient_type, $data, $extra_data = []) {
|
private function send_email($event_id, $recipient_type, $data, $extra_data = [])
|
||||||
|
{
|
||||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ class EmailRenderer
|
|||||||
'payment_method' => $data->get_payment_method_title(),
|
'payment_method' => $data->get_payment_method_title(),
|
||||||
'payment_status' => $data->get_status(),
|
'payment_status' => $data->get_status(),
|
||||||
'payment_date' => $payment_date,
|
'payment_date' => $payment_date,
|
||||||
|
'payment_error_reason' => $data->get_meta('_payment_error_reason') ?: 'Payment declined',
|
||||||
'transaction_id' => $data->get_transaction_id() ?: 'N/A',
|
'transaction_id' => $data->get_transaction_id() ?: 'N/A',
|
||||||
'shipping_method' => $data->get_shipping_method(),
|
'shipping_method' => $data->get_shipping_method(),
|
||||||
'estimated_delivery' => $estimated_delivery,
|
'estimated_delivery' => $estimated_delivery,
|
||||||
@@ -239,9 +240,12 @@ class EmailRenderer
|
|||||||
'billing_address' => $data->get_formatted_billing_address(),
|
'billing_address' => $data->get_formatted_billing_address(),
|
||||||
'shipping_address' => $data->get_formatted_shipping_address(),
|
'shipping_address' => $data->get_formatted_shipping_address(),
|
||||||
// URLs
|
// URLs
|
||||||
'review_url' => $data->get_view_order_url(), // Can be customized later
|
'review_url' => $data->get_view_order_url(),
|
||||||
|
'return_url' => $data->get_view_order_url(), // Customers click to initiate return
|
||||||
|
'contact_url' => home_url('/contact'),
|
||||||
'shop_url' => get_permalink(wc_get_page_id('shop')),
|
'shop_url' => get_permalink(wc_get_page_id('shop')),
|
||||||
'my_account_url' => get_permalink(wc_get_page_id('myaccount')),
|
'my_account_url' => get_permalink(wc_get_page_id('myaccount')),
|
||||||
|
'account_url' => get_permalink(wc_get_page_id('myaccount')), // Alias for my_account_url
|
||||||
'payment_retry_url' => $data->get_checkout_payment_url(),
|
'payment_retry_url' => $data->get_checkout_payment_url(),
|
||||||
// Tracking (if available from meta)
|
// Tracking (if available from meta)
|
||||||
'tracking_number' => $data->get_meta('_tracking_number') ?: 'N/A',
|
'tracking_number' => $data->get_meta('_tracking_number') ?: 'N/A',
|
||||||
@@ -249,6 +253,7 @@ class EmailRenderer
|
|||||||
'shipping_carrier' => $data->get_meta('_shipping_carrier') ?: 'Standard Shipping',
|
'shipping_carrier' => $data->get_meta('_shipping_carrier') ?: 'Standard Shipping',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
// Order items table
|
// Order items table
|
||||||
$items_html = '<table class="order-details" style="width: 100%; border-collapse: collapse;">';
|
$items_html = '<table class="order-details" style="width: 100%; border-collapse: collapse;">';
|
||||||
$items_html .= '<thead><tr>';
|
$items_html .= '<thead><tr>';
|
||||||
@@ -277,9 +282,10 @@ class EmailRenderer
|
|||||||
|
|
||||||
$items_html .= '</tbody></table>';
|
$items_html .= '</tbody></table>';
|
||||||
|
|
||||||
// Both naming conventions for compatibility
|
// All naming conventions for compatibility
|
||||||
$variables['order_items'] = $items_html;
|
$variables['order_items'] = $items_html;
|
||||||
$variables['order_items_table'] = $items_html;
|
$variables['order_items_table'] = $items_html;
|
||||||
|
$variables['order_items_list'] = $items_html; // Alias used in some templates
|
||||||
}
|
}
|
||||||
|
|
||||||
// Product variables
|
// Product variables
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ class Assets
|
|||||||
add_action('wp_enqueue_scripts', [self::class, 'dequeue_conflicting_scripts'], 100);
|
add_action('wp_enqueue_scripts', [self::class, 'dequeue_conflicting_scripts'], 100);
|
||||||
add_filter('script_loader_tag', [self::class, 'add_module_type'], 10, 3);
|
add_filter('script_loader_tag', [self::class, 'add_module_type'], 10, 3);
|
||||||
add_action('woocommerce_before_main_content', [self::class, 'inject_spa_mount_point'], 5);
|
add_action('woocommerce_before_main_content', [self::class, 'inject_spa_mount_point'], 5);
|
||||||
|
|
||||||
|
// Hide admin bar if configured
|
||||||
|
$settings = get_option('woonoow_appearance_settings', []);
|
||||||
|
if (!empty($settings['general']['hide_admin_bar'])) {
|
||||||
|
add_filter('show_admin_bar', '__return_false');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -115,13 +121,15 @@ class Assets
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're in full mode and not on a page with shortcode
|
// Get appearance settings for unified spa_mode check
|
||||||
$spa_settings = get_option('woonoow_customer_spa_settings', []);
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||||
$mode = isset($spa_settings['mode']) ? $spa_settings['mode'] : 'disabled';
|
$spa_mode = $appearance_settings['general']['spa_mode'] ?? 'full';
|
||||||
|
|
||||||
if ($mode === 'full') {
|
if ($spa_mode === 'full') {
|
||||||
// Only inject if the mount point doesn't already exist (from shortcode)
|
// Only inject if the mount point doesn't already exist (from shortcode)
|
||||||
echo '<div id="woonoow-customer-app" data-page="shop"><div class="woonoow-loading"><p>Loading...</p></div></div>';
|
$request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
|
||||||
|
$current_path = parse_url($request_uri, PHP_URL_PATH);
|
||||||
|
echo '<div id="woonoow-customer-app" data-page="shop" data-initial-route="' . esc_attr($current_path) . '"><div class="woonoow-loading"><p>Loading...</p></div></div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +235,14 @@ class Assets
|
|||||||
// If SPA Entry Page is WP frontpage, base path is /, otherwise use Entry Page slug
|
// If SPA Entry Page is WP frontpage, base path is /, otherwise use Entry Page slug
|
||||||
$base_path = $is_spa_wp_frontpage ? '' : ($spa_page ? '/' . $spa_page->post_name : '/store');
|
$base_path = $is_spa_wp_frontpage ? '' : ($spa_page ? '/' . $spa_page->post_name : '/store');
|
||||||
|
|
||||||
|
// When injecting into a CPT or structural page, the URL does not start with the SPA base path.
|
||||||
|
// E.g., /desain-mockup... instead of /store/desain-mockup...
|
||||||
|
// For these pages, we must force the base path to empty so BrowserRouter starts from the root.
|
||||||
|
if (is_singular() && (!isset($spa_page) || get_queried_object_id() !== $spa_page->ID)) {
|
||||||
|
// If we're on a singular page that isn't the SPA Entry Page, it's a structural page or CPT
|
||||||
|
$base_path = '';
|
||||||
|
}
|
||||||
|
|
||||||
// Check if BrowserRouter is enabled (default: true for SEO)
|
// Check if BrowserRouter is enabled (default: true for SEO)
|
||||||
$use_browser_router = $appearance_settings['general']['use_browser_router'] ?? true;
|
$use_browser_router = $appearance_settings['general']['use_browser_router'] ?? true;
|
||||||
|
|
||||||
@@ -275,20 +291,27 @@ class Assets
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if we should load customer-spa assets
|
* Check if we should load assets on this page
|
||||||
*/
|
*/
|
||||||
private static function should_load_assets()
|
public static function should_load_assets()
|
||||||
{
|
{
|
||||||
global $post;
|
// Don't load on admin pages
|
||||||
|
if (is_admin()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we're serving SPA directly (set by serve_spa_for_frontpage_routes)
|
// Force load if constant is defined (e.g. for preview)
|
||||||
if (defined('WOONOOW_SERVE_SPA') && WOONOOW_SERVE_SPA) {
|
if (defined('WOONOOW_SERVE_SPA') && WOONOOW_SERVE_SPA) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're on a frontpage SPA route (by URL detection)
|
// Get SPA mode from appearance settings
|
||||||
if (self::is_frontpage_spa_route()) {
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||||
return true;
|
$mode = $appearance_settings['general']['spa_mode'] ?? 'full';
|
||||||
|
|
||||||
|
// Check if SPA is completely disabled
|
||||||
|
if ($mode === 'disabled') {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// First check: Is this a designated SPA page?
|
// First check: Is this a designated SPA page?
|
||||||
@@ -296,39 +319,29 @@ class Assets
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get SPA mode from appearance settings (the correct source)
|
// Check if we're on a frontpage SPA route
|
||||||
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
if (self::is_frontpage_spa_route()) {
|
||||||
$mode = $appearance_settings['general']['spa_mode'] ?? 'full';
|
|
||||||
|
|
||||||
// If disabled, only load for pages with shortcodes
|
|
||||||
if ($mode === 'disabled') {
|
|
||||||
// Special handling for WooCommerce Shop page (it's an archive, not a regular post)
|
|
||||||
if (function_exists('is_shop') && is_shop()) {
|
|
||||||
$shop_page_id = get_option('woocommerce_shop_page_id');
|
|
||||||
if ($shop_page_id) {
|
|
||||||
$shop_page = get_post($shop_page_id);
|
|
||||||
if ($shop_page && has_shortcode($shop_page->post_content, 'woonoow_shop')) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For structural pages (is_singular('page'))
|
||||||
|
if (is_singular('page')) {
|
||||||
|
$page_id = get_queried_object_id();
|
||||||
|
$structure = get_post_meta($page_id, '_wn_page_structure', true);
|
||||||
|
if (!empty($structure) && !empty($structure['sections'])) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for shortcodes on regular pages
|
// For CPTs with WooNooW templates
|
||||||
if ($post) {
|
if (is_singular() && !is_singular('page')) {
|
||||||
if (has_shortcode($post->post_content, 'woonoow_shop')) {
|
$post_type = get_post_type();
|
||||||
return true;
|
if (!in_array($post_type, ['product', 'shop_order', 'shop_coupon'])) {
|
||||||
}
|
$wn_template = get_option("wn_template_{$post_type}", null);
|
||||||
if (has_shortcode($post->post_content, 'woonoow_cart')) {
|
if (!empty($wn_template) && !empty($wn_template['sections'])) {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (has_shortcode($post->post_content, 'woonoow_checkout')) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (has_shortcode($post->post_content, 'woonoow_account')) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Full SPA mode - load on all WooCommerce pages
|
// Full SPA mode - load on all WooCommerce pages
|
||||||
@@ -353,6 +366,7 @@ class Assets
|
|||||||
|
|
||||||
// Checkout-Only mode - load only on specific pages
|
// Checkout-Only mode - load only on specific pages
|
||||||
if ($mode === 'checkout_only') {
|
if ($mode === 'checkout_only') {
|
||||||
|
$spa_settings = get_option('woonoow_customer_spa_settings', []);
|
||||||
$checkout_pages = isset($spa_settings['checkoutPages']) ? $spa_settings['checkoutPages'] : [];
|
$checkout_pages = isset($spa_settings['checkoutPages']) ? $spa_settings['checkoutPages'] : [];
|
||||||
|
|
||||||
if (!empty($checkout_pages['checkout']) && function_exists('is_checkout') && is_checkout() && !is_order_received_page()) {
|
if (!empty($checkout_pages['checkout']) && function_exists('is_checkout') && is_checkout() && !is_order_received_page()) {
|
||||||
@@ -370,6 +384,7 @@ class Assets
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
global $post;
|
||||||
// Check if current page has WooNooW shortcodes
|
// Check if current page has WooNooW shortcodes
|
||||||
if ($post && has_shortcode($post->post_content, 'woonoow_shop')) {
|
if ($post && has_shortcode($post->post_content, 'woonoow_shop')) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -283,8 +283,8 @@ class ShopController
|
|||||||
|
|
||||||
// Add detailed info if requested
|
// Add detailed info if requested
|
||||||
if ($detailed) {
|
if ($detailed) {
|
||||||
$data['description'] = $product->get_description();
|
$data['description'] = wpautop($product->get_description());
|
||||||
$data['short_description'] = $product->get_short_description();
|
$data['short_description'] = wpautop($product->get_short_description());
|
||||||
$data['sku'] = $product->get_sku();
|
$data['sku'] = $product->get_sku();
|
||||||
$data['tags'] = wp_get_post_terms($product->get_id(), 'product_tag', ['fields' => 'names']);
|
$data['tags'] = wp_get_post_terms($product->get_id(), 'product_tag', ['fields' => 'names']);
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,11 @@ class TemplateOverride
|
|||||||
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=reset-password',
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=reset-password',
|
||||||
'top'
|
'top'
|
||||||
);
|
);
|
||||||
|
add_rewrite_rule(
|
||||||
|
'^subscribe/?$',
|
||||||
|
'index.php?page_id=' . $spa_page_id . '&woonoow_spa_path=subscribe',
|
||||||
|
'top'
|
||||||
|
);
|
||||||
|
|
||||||
// /order-pay/* → SPA page
|
// /order-pay/* → SPA page
|
||||||
add_rewrite_rule(
|
add_rewrite_rule(
|
||||||
@@ -352,7 +357,6 @@ class TemplateOverride
|
|||||||
if ($post->ID == $spa_page_id || $post->ID == $frontpage_id) {
|
if ($post->ID == $spa_page_id || $post->ID == $frontpage_id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if page has WooNooW structure
|
// Check if page has WooNooW structure
|
||||||
$structure = get_post_meta($post->ID, '_wn_page_structure', true);
|
$structure = get_post_meta($post->ID, '_wn_page_structure', true);
|
||||||
if (!empty($structure) && !empty($structure['sections'])) {
|
if (!empty($structure) && !empty($structure['sections'])) {
|
||||||
@@ -364,11 +368,6 @@ class TemplateOverride
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Serve SPA template directly for frontpage SPA routes
|
|
||||||
* When SPA page is set as WordPress frontpage, intercept known routes
|
|
||||||
* and serve the SPA template directly (bypasses WooCommerce templates)
|
|
||||||
*/
|
|
||||||
/**
|
/**
|
||||||
* Serve SPA template directly for frontpage SPA routes
|
* Serve SPA template directly for frontpage SPA routes
|
||||||
* When SPA page is set as WordPress frontpage, intercept known routes
|
* When SPA page is set as WordPress frontpage, intercept known routes
|
||||||
@@ -417,8 +416,8 @@ class TemplateOverride
|
|||||||
'/my-account', // Account page
|
'/my-account', // Account page
|
||||||
'/login', // Login page
|
'/login', // Login page
|
||||||
'/register', // Register page
|
'/register', // Register page
|
||||||
'/register', // Register page
|
|
||||||
'/reset-password', // Password reset
|
'/reset-password', // Password reset
|
||||||
|
'/subscribe', // Subscribe page
|
||||||
'/order-pay', // Order pay page
|
'/order-pay', // Order pay page
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -535,6 +534,32 @@ class TemplateOverride
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if it's a structural page with WooNooW sections
|
||||||
|
if (is_singular('page')) {
|
||||||
|
$page_id = get_queried_object_id();
|
||||||
|
$structure = get_post_meta($page_id, '_wn_page_structure', true);
|
||||||
|
if (!empty($structure) && !empty($structure['sections'])) {
|
||||||
|
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
|
||||||
|
if (file_exists($spa_template)) {
|
||||||
|
return $spa_template;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a CPT singular with a WooNooW template
|
||||||
|
if (is_singular() && !is_singular('page')) {
|
||||||
|
$post_type = get_post_type();
|
||||||
|
if ($post_type) {
|
||||||
|
$cpt_template = get_option("wn_template_{$post_type}", null);
|
||||||
|
if (!empty($cpt_template) && !empty($cpt_template['sections'])) {
|
||||||
|
$spa_template = plugin_dir_path(dirname(dirname(__FILE__))) . 'templates/spa-full-page.php';
|
||||||
|
if (file_exists($spa_template)) {
|
||||||
|
return $spa_template;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// For spa_mode = 'full', override WooCommerce pages
|
// For spa_mode = 'full', override WooCommerce pages
|
||||||
if ($spa_mode === 'full') {
|
if ($spa_mode === 'full') {
|
||||||
// Override all WooCommerce pages
|
// Override all WooCommerce pages
|
||||||
@@ -569,23 +594,30 @@ class TemplateOverride
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine page type
|
// Determine page type and route
|
||||||
$page_type = 'shop';
|
|
||||||
$data_attrs = 'data-page="shop"';
|
$data_attrs = 'data-page="shop"';
|
||||||
|
|
||||||
|
// Pass current request URI as initial route so router doesn't fallback to /shop
|
||||||
|
$request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
|
||||||
|
$current_path = parse_url($request_uri, PHP_URL_PATH);
|
||||||
|
$data_attrs .= ' data-initial-route="' . esc_attr($current_path) . '"';
|
||||||
|
|
||||||
if (is_product()) {
|
if (is_product()) {
|
||||||
$page_type = 'product';
|
|
||||||
global $post;
|
global $post;
|
||||||
$data_attrs = 'data-page="product" data-product-id="' . esc_attr($post->ID) . '"';
|
$data_attrs .= ' data-page="product" data-product-id="' . esc_attr($post->ID) . '"';
|
||||||
} elseif (is_cart()) {
|
} elseif (is_cart()) {
|
||||||
$page_type = 'cart';
|
$data_attrs .= ' data-page="cart"';
|
||||||
$data_attrs = 'data-page="cart"';
|
|
||||||
} elseif (is_checkout()) {
|
} elseif (is_checkout()) {
|
||||||
$page_type = 'checkout';
|
$data_attrs .= ' data-page="checkout"';
|
||||||
$data_attrs = 'data-page="checkout"';
|
|
||||||
} elseif (is_account_page()) {
|
} elseif (is_account_page()) {
|
||||||
$page_type = 'account';
|
$data_attrs .= ' data-page="account"';
|
||||||
$data_attrs = 'data-page="account"';
|
} elseif (is_singular('page')) {
|
||||||
|
$data_attrs .= ' data-page="page"';
|
||||||
|
} elseif (is_singular() && !is_singular('page')) {
|
||||||
|
// CPT single item with a WooNooW template
|
||||||
|
global $post;
|
||||||
|
$post_type = get_post_type();
|
||||||
|
$data_attrs .= ' data-page="cpt" data-cpt-type="' . esc_attr($post_type) . '" data-cpt-slug="' . esc_attr($post->post_name) . '"';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output SPA mount point
|
// Output SPA mount point
|
||||||
@@ -631,6 +663,26 @@ class TemplateOverride
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For structural pages (is_singular('page'))
|
||||||
|
if (is_singular('page')) {
|
||||||
|
$page_id = get_queried_object_id();
|
||||||
|
$structure = get_post_meta($page_id, '_wn_page_structure', true);
|
||||||
|
if (!empty($structure) && !empty($structure['sections'])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For CPT singular items with a WooNooW template
|
||||||
|
if (is_singular() && !is_singular('page')) {
|
||||||
|
$post_type = get_post_type();
|
||||||
|
if ($post_type) {
|
||||||
|
$cpt_template = get_option("wn_template_{$post_type}", null);
|
||||||
|
if (!empty($cpt_template) && !empty($cpt_template['sections'])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ class TemplateRegistry
|
|||||||
'description' => 'Simple contact page with a form and address details.',
|
'description' => 'Simple contact page with a form and address details.',
|
||||||
'icon' => 'mail',
|
'icon' => 'mail',
|
||||||
'sections' => self::get_contact_structure()
|
'sections' => self::get_contact_structure()
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 'single-post',
|
||||||
|
'label' => 'Single Post / CPT',
|
||||||
|
'description' => 'A dynamic layout for blog posts or custom post types with a hero, featured image, and body content.',
|
||||||
|
'icon' => 'layout',
|
||||||
|
'sections' => self::get_single_post_structure()
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -166,4 +173,73 @@ class TemplateRegistry
|
|||||||
]
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function get_single_post_structure()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
// ── Section 1: Article Hero ─────────────────────────────────────
|
||||||
|
[
|
||||||
|
'id' => self::generate_id(),
|
||||||
|
'type' => 'hero',
|
||||||
|
'layoutVariant' => 'centered',
|
||||||
|
'colorScheme' => 'default',
|
||||||
|
'props' => [
|
||||||
|
'title' => ['type' => 'dynamic', 'source' => 'post_title'],
|
||||||
|
'subtitle' => ['type' => 'dynamic', 'source' => 'post_author'],
|
||||||
|
'image' => ['type' => 'static', 'value' => ''],
|
||||||
|
'cta_text' => ['type' => 'static', 'value' => ''],
|
||||||
|
'cta_url' => ['type' => 'static', 'value' => ''],
|
||||||
|
],
|
||||||
|
// dynamicBackground tells the API to resolve styles.backgroundImage
|
||||||
|
// from 'post_featured_image' at render time (falls back to '' if no featured image)
|
||||||
|
'styles' => [
|
||||||
|
'contentWidth' => 'contained',
|
||||||
|
'heightPreset' => 'medium',
|
||||||
|
'dynamicBackground' => 'post_featured_image',
|
||||||
|
'backgroundOverlay' => 50,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ── Section 2: Article Body ─────────────────────────────────────
|
||||||
|
[
|
||||||
|
'id' => self::generate_id(),
|
||||||
|
'type' => 'content',
|
||||||
|
'layoutVariant' => 'narrow',
|
||||||
|
'colorScheme' => 'default',
|
||||||
|
'props' => [
|
||||||
|
'content' => ['type' => 'dynamic', 'source' => 'post_content'],
|
||||||
|
'cta_text' => ['type' => 'static', 'value' => ''],
|
||||||
|
'cta_url' => ['type' => 'static', 'value' => ''],
|
||||||
|
],
|
||||||
|
'styles' => ['contentWidth' => 'contained', 'heightPreset' => 'default'],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ── Section 3: Related Posts ────────────────────────────────────
|
||||||
|
[
|
||||||
|
'id' => self::generate_id(),
|
||||||
|
'type' => 'feature-grid',
|
||||||
|
'layoutVariant' => 'grid-3',
|
||||||
|
'colorScheme' => 'muted',
|
||||||
|
'props' => [
|
||||||
|
'heading' => ['type' => 'static', 'value' => 'Related Articles'],
|
||||||
|
'features' => ['type' => 'dynamic', 'source' => 'related_posts'],
|
||||||
|
],
|
||||||
|
'styles' => ['contentWidth' => 'contained', 'heightPreset' => 'default'],
|
||||||
|
],
|
||||||
|
|
||||||
|
// ── Section 4: CTA Banner ───────────────────────────────────────
|
||||||
|
[
|
||||||
|
'id' => self::generate_id(),
|
||||||
|
'type' => 'cta-banner',
|
||||||
|
'colorScheme' => 'gradient',
|
||||||
|
'props' => [
|
||||||
|
'title' => ['type' => 'static', 'value' => 'Enjoyed this article?'],
|
||||||
|
'text' => ['type' => 'static', 'value' => 'Subscribe to our newsletter and never miss an update.'],
|
||||||
|
'button_text' => ['type' => 'static', 'value' => 'Subscribe Now'],
|
||||||
|
'button_url' => ['type' => 'static', 'value' => '/subscribe'],
|
||||||
|
],
|
||||||
|
'styles' => ['contentWidth' => 'contained', 'heightPreset' => 'medium'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,54 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html <?php language_attributes(); ?>>
|
<html <?php language_attributes(); ?>>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="<?php bloginfo('charset'); ?>">
|
<meta charset="<?php bloginfo('charset'); ?>">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title><?php wp_title('|', true, 'right'); ?><?php bloginfo('name'); ?></title>
|
<title><?php wp_title('|', true, 'right'); ?><?php bloginfo('name'); ?></title>
|
||||||
<?php wp_head(); ?>
|
<?php wp_head(); ?>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body <?php body_class('woonoow-spa-page'); ?>>
|
<body <?php body_class('woonoow-spa-page'); ?>>
|
||||||
<?php
|
<?php
|
||||||
// Determine initial route based on SPA mode
|
// Determine initial route based on SPA mode
|
||||||
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
$appearance_settings = get_option('woonoow_appearance_settings', []);
|
||||||
$spa_mode = isset($appearance_settings['general']['spa_mode']) ? $appearance_settings['general']['spa_mode'] : 'full';
|
$spa_mode = isset($appearance_settings['general']['spa_mode']) ? $appearance_settings['general']['spa_mode'] : 'full';
|
||||||
|
|
||||||
|
// Get actual request path for accurate routing
|
||||||
|
$request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
|
||||||
|
$current_path = parse_url($request_uri, PHP_URL_PATH);
|
||||||
|
|
||||||
// Set initial page based on mode
|
// Set initial page based on mode
|
||||||
if ($spa_mode === 'checkout_only') {
|
if ($spa_mode === 'checkout_only') {
|
||||||
// Checkout Only mode starts at cart
|
// Checkout Only mode starts at cart
|
||||||
$page_type = 'cart';
|
$page_type = 'cart';
|
||||||
$data_attrs = 'data-page="cart" data-initial-route="/cart"';
|
$data_attrs = 'data-page="cart" data-initial-route="/cart"';
|
||||||
} else {
|
} else {
|
||||||
// Full SPA mode starts at shop
|
// Evaluate WordPress page type to pass to React App
|
||||||
$page_type = 'shop';
|
if (is_product()) {
|
||||||
|
global $post;
|
||||||
|
$data_attrs = 'data-page="product" data-product-id="' . esc_attr($post->ID) . '"';
|
||||||
|
} elseif (is_cart()) {
|
||||||
|
$data_attrs = 'data-page="cart"';
|
||||||
|
} elseif (is_checkout()) {
|
||||||
|
$data_attrs = 'data-page="checkout"';
|
||||||
|
} elseif (is_account_page()) {
|
||||||
|
$data_attrs = 'data-page="account"';
|
||||||
|
} elseif (is_singular('page')) {
|
||||||
|
$data_attrs = 'data-page="page"';
|
||||||
|
} elseif (is_singular() && !is_singular('page')) {
|
||||||
|
global $post;
|
||||||
|
$post_type = get_post_type();
|
||||||
|
$data_attrs = 'data-page="cpt" data-cpt-type="' . esc_attr($post_type) . '" data-cpt-slug="' . esc_attr($post->post_name) . '"';
|
||||||
|
} else {
|
||||||
|
$data_attrs = 'data-page="shop"';
|
||||||
|
}
|
||||||
|
|
||||||
// If this is the front page, route to /
|
// If this is the front page, route to /
|
||||||
if (is_front_page()) {
|
if (is_front_page()) {
|
||||||
$data_attrs = 'data-page="shop" data-initial-route="/"';
|
$data_attrs .= ' data-initial-route="/"';
|
||||||
} else {
|
} else {
|
||||||
$data_attrs = 'data-page="shop" data-initial-route="/shop"';
|
$data_attrs .= ' data-initial-route="' . esc_attr($current_path) . '"';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -38,4 +61,5 @@
|
|||||||
|
|
||||||
<?php wp_footer(); ?>
|
<?php wp_footer(); ?>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
5
test_onboarding_val.php
Normal file
5
test_onboarding_val.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
require_once dirname(dirname(dirname(__DIR__))) . '/wp-load.php';
|
||||||
|
$val = get_option('woonoow_onboarding_completed', "NOT_FOUND");
|
||||||
|
echo "Type: " . gettype($val) . "\n";
|
||||||
|
echo "Value: " . var_export($val, true) . "\n";
|
||||||
Reference in New Issue
Block a user