import { useState } from 'react'; import { Plus, Trash2, MapPin, ChevronRight, Copy, Crown, } from 'lucide-react'; import { usePostTypes } from '../../hooks/usePostTypes'; import { useTaxonomies } from '../../hooks/useTaxonomies'; import { useProStatus } from '../../hooks/use-pro-status'; // Location rule types interface LocationRule { param: string; operator: string; value: string; } // Location rule group (AND logic within group) type LocationRuleGroup = LocationRule[]; // Location param option type interface LocationParamOption { value: string; label: string; group: string; } // Location param options for Free version const FREE_LOCATION_PARAMS: LocationParamOption[] = [ { value: 'post_type', label: 'Post Type', group: 'Post' }, { value: 'post', label: 'Post', group: 'Post' }, { value: 'post_status', label: 'Post Status', group: 'Post' }, { value: 'taxonomy', label: 'Taxonomy', group: 'Term' }, { value: 'term', label: 'Term', group: 'Term' }, { value: 'user_role', label: 'User Role', group: 'User' }, { value: 'options_page', label: 'Options Page', group: 'Options' }, ]; // Pro location params (shown with PRO badge) const PRO_LOCATION_PARAMS: LocationParamOption[] = [ { value: 'page_template', label: 'Page Template', group: 'Post' }, { value: 'post_parent', label: 'Post Parent', group: 'Post' }, { value: 'post_author', label: 'Post Author', group: 'Post' }, { value: 'post_format', label: 'Post Format', group: 'Post' }, { value: 'post_taxonomy', label: 'Post Taxonomy Term', group: 'Post' }, { value: 'current_user_role', label: 'Current User Role', group: 'User' }, { value: 'current_user_capability', label: 'Current User Capability', group: 'User' }, ]; // Location operators const LOCATION_OPERATORS = [ { value: '==', label: 'is equal to' }, { value: '!=', label: 'is not equal to' }, ]; // Collapsible Section Component function CollapsibleSection({ title, icon, children, defaultOpen = true, badge }: { title: string; icon: React.ReactNode; children: React.ReactNode; defaultOpen?: boolean; badge?: string; }) { const [isOpen, setIsOpen] = useState(defaultOpen); return (
{isOpen && (
{children}
)}
); } // Props interface interface LocationRulesBuilderProps { locations: LocationRuleGroup[]; setLocations: React.Dispatch>; } export function LocationRulesBuilder({ locations, setLocations }: LocationRulesBuilderProps) { const { isPro } = useProStatus(); const { data: postTypes } = usePostTypes(); const { data: taxonomies } = useTaxonomies(); // Get all available post types (built-in + custom) const availablePostTypes = [ { slug: 'post', label: 'Post' }, { slug: 'page', label: 'Page' }, ...(postTypes?.map(pt => ({ slug: pt.slug, label: pt.title })) || []), ]; // Get all available taxonomies (built-in + custom) const availableTaxonomies = [ { slug: 'category', label: 'Category' }, { slug: 'post_tag', label: 'Tag' }, ...(taxonomies?.map(tax => ({ slug: tax.slug, label: tax.title })) || []), ]; // Post status options const postStatuses = [ { value: 'publish', label: 'Published' }, { value: 'pending', label: 'Pending Review' }, { value: 'draft', label: 'Draft' }, { value: 'future', label: 'Scheduled' }, { value: 'private', label: 'Private' }, ]; // User role options const userRoles = [ { value: 'administrator', label: 'Administrator' }, { value: 'editor', label: 'Editor' }, { value: 'author', label: 'Author' }, { value: 'contributor', label: 'Contributor' }, { value: 'subscriber', label: 'Subscriber' }, ]; // Get value options based on param type const getValueOptions = (param: string) => { switch (param) { case 'post_type': return availablePostTypes.map(pt => ({ value: pt.slug, label: pt.label })); case 'taxonomy': return availableTaxonomies.map(tax => ({ value: tax.slug, label: tax.label })); case 'post_status': return postStatuses; case 'user_role': case 'current_user_role': return userRoles; default: return []; } }; // Check if param should have a text input instead of select const isTextInput = (param: string) => { return ['post', 'term', 'post_parent', 'post_author', 'page_template', 'options_page', 'current_user_capability'].includes(param); }; // Check if param is Pro only const isProParam = (param: string) => { return PRO_LOCATION_PARAMS.some((p: LocationParamOption) => p.value === param); }; // Add a new rule to a group const addRule = (groupIndex: number) => { setLocations(prev => prev.map((group: LocationRuleGroup, idx: number) => { if (idx === groupIndex) { return [...group, { param: 'post_type', operator: '==', value: 'post' }]; } return group; })); }; // Update a rule in a group const updateRule = (groupIndex: number, ruleIndex: number, updates: Partial) => { setLocations(prev => prev.map((group: LocationRuleGroup, gIdx: number) => { if (gIdx === groupIndex) { return group.map((rule: LocationRule, rIdx: number) => { if (rIdx === ruleIndex) { // If param changes, reset the value if (updates.param && updates.param !== rule.param) { const defaultOptions = getValueOptions(updates.param); return { ...rule, ...updates, value: defaultOptions.length > 0 ? defaultOptions[0].value : '' }; } return { ...rule, ...updates }; } return rule; }); } return group; })); }; // Remove a rule from a group const removeRule = (groupIndex: number, ruleIndex: number) => { setLocations(prev => prev.map((group: LocationRuleGroup, gIdx: number) => { if (gIdx === groupIndex) { // Don't remove if it's the last rule if (group.length === 1) return group; return group.filter((_: LocationRule, rIdx: number) => rIdx !== ruleIndex); } return group; })); }; // Add a new OR group (Pro feature) const addGroup = () => { if (!isPro) return; setLocations(prev => [...prev, [{ param: 'post_type', operator: '==', value: 'post' }]]); }; // Duplicate a group const duplicateGroup = (groupIndex: number) => { if (!isPro) return; setLocations(prev => { const newGroups = [...prev]; const groupToCopy = prev[groupIndex].map((rule: LocationRule) => ({ ...rule })); newGroups.splice(groupIndex + 1, 0, groupToCopy); return newGroups; }); }; // Remove a group const removeGroup = (groupIndex: number) => { // Don't remove if it's the last group if (locations.length === 1) return; setLocations(prev => prev.filter((_: LocationRuleGroup, idx: number) => idx !== groupIndex)); }; // Generate human-readable summary const getLocationSummary = () => { const allParams = [...FREE_LOCATION_PARAMS, ...PRO_LOCATION_PARAMS]; const groupSummaries = locations.map((group: LocationRuleGroup) => { const ruleSummaries = group.map((rule: LocationRule) => { const paramLabel = allParams.find((p: LocationParamOption) => p.value === rule.param)?.label || rule.param; const operatorLabel = rule.operator === '==' ? 'is' : 'is not'; const valueOptions = getValueOptions(rule.param); const valueLabel = valueOptions.find((v: { value: string; label: string }) => v.value === rule.value)?.label || rule.value; return `${paramLabel} ${operatorLabel} "${valueLabel}"`; }); return ruleSummaries.join(' AND '); }); if (groupSummaries.length === 1) { return groupSummaries[0]; } return groupSummaries.map((s: string, i: number) => `Group ${i + 1}: ${s}`).join(' OR '); }; // Group params by category for the dropdown const groupedParams = () => { const groups: { [key: string]: { value: string; label: string; isPro?: boolean }[] } = { 'Post': [], 'Term': [], 'User': [], 'Options': [], }; FREE_LOCATION_PARAMS.forEach((p: LocationParamOption) => { if (groups[p.group]) { groups[p.group].push({ value: p.value, label: p.label, isPro: false }); } }); PRO_LOCATION_PARAMS.forEach((p: LocationParamOption) => { if (groups[p.group]) { groups[p.group].push({ value: p.value, label: p.label, isPro: true }); } }); return groups; }; const paramGroups = groupedParams(); return ( } badge={locations.length > 1 ? `${locations.length} groups` : `${locations[0]?.length || 0} rules`} > {/* Location Summary */}
Show this field group if
{getLocationSummary() || 'No rules configured'}
{/* Rule Groups */}
{locations.map((group: LocationRuleGroup, groupIndex: number) => (
{/* OR separator between groups */} {groupIndex > 0 && (
or
)} {/* Group Card */}
{/* Group Header */} {locations.length > 1 && (
Rule Group {groupIndex + 1}
)} {/* Rules */}
{group.map((rule: LocationRule, ruleIndex: number) => { const valueOptions = getValueOptions(rule.param); const isText = isTextInput(rule.param); const isParamPro = isProParam(rule.param); return (
{/* AND separator between rules */} {ruleIndex > 0 && (
and
)} {/* Rule Row */}
{/* Param Select */}
{/* Operator Select */}
{/* Value Input/Select */}
{isText ? ( updateRule(groupIndex, ruleIndex, { value: e.target.value })} placeholder={rule.param === 'post' ? 'Enter post ID or slug' : 'Enter value...'} className="rdcfe-h-10 rdcfe-w-full rdcfe-rounded-lg rdcfe-border rdcfe-border-[hsl(var(--rdcfe-border))] rdcfe-bg-white rdcfe-px-3 rdcfe-text-[13px] rdcfe-text-[hsl(var(--rdcfe-foreground))] rdcfe-placeholder-[hsl(var(--rdcfe-muted-foreground))] rdcfe-transition-all focus:rdcfe-border-[hsl(var(--rdcfe-primary))] focus:rdcfe-outline-none focus:rdcfe-ring-2 focus:rdcfe-ring-[hsl(var(--rdcfe-primary)/0.2)]" /> ) : ( )}
{/* Pro Badge for Pro params */} {isParamPro && (
Pro
)} {/* Remove Rule Button */} {group.length > 1 && ( )}
); })} {/* Add Rule Button */}
))}
{/* Add OR Group Button (Pro) */}
{!isPro && (

OR groups allow you to show field groups when any group of rules matches

)}
); }