diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx index d5b7a94..f9b0fb3 100644 --- a/app/sites/[id]/page.tsx +++ b/app/sites/[id]/page.tsx @@ -460,7 +460,7 @@ export default function SiteDashboardPage() { {/* Dimension Filters */} -
+
diff --git a/components/dashboard/AddFilterDropdown.tsx b/components/dashboard/AddFilterDropdown.tsx index 6599be3..e0068ce 100644 --- a/components/dashboard/AddFilterDropdown.tsx +++ b/components/dashboard/AddFilterDropdown.tsx @@ -18,38 +18,26 @@ interface AddFilterDropdownProps { suggestions?: FilterSuggestions } -// Which dimensions show as always-visible chips vs hidden in "More" -const PRIMARY_DIMS = ['page', 'referrer', 'country', 'browser', 'os', 'device'] -const SECONDARY_DIMS = ['region', 'city', 'utm_source', 'utm_medium', 'utm_campaign'] +const ALL_DIMS = ['page', 'referrer', 'country', 'region', 'city', 'browser', 'os', 'device', 'utm_source', 'utm_medium', 'utm_campaign'] -function DimensionPopover({ - dimension, - suggestions, - onApply, - onClose, -}: { - dimension: string - suggestions: FilterSuggestion[] - onApply: (filter: DimensionFilter) => void - onClose: () => void -}) { +export default function AddFilterDropdown({ onAdd, suggestions = {} }: AddFilterDropdownProps) { + const [isOpen, setIsOpen] = useState(false) + const [selectedDim, setSelectedDim] = useState(null) const [operator, setOperator] = useState('is') const [search, setSearch] = useState('') const ref = useRef(null) const inputRef = useRef(null) + // Close on outside click or Escape useEffect(() => { - inputRef.current?.focus() - }, []) - - useEffect(() => { + if (!isOpen) return function handleClick(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) { - onClose() + handleClose() } } function handleEsc(e: KeyboardEvent) { - if (e.key === 'Escape') onClose() + if (e.key === 'Escape') handleClose() } document.addEventListener('mousedown', handleClick) document.addEventListener('keydown', handleEsc) @@ -57,197 +45,163 @@ function DimensionPopover({ document.removeEventListener('mousedown', handleClick) document.removeEventListener('keydown', handleEsc) } - }, [onClose]) + }, [isOpen]) - const filtered = suggestions.filter(s => + // Focus search input when a dimension is selected + useEffect(() => { + if (selectedDim) inputRef.current?.focus() + }, [selectedDim]) + + function handleClose() { + setIsOpen(false) + setSelectedDim(null) + setOperator('is') + setSearch('') + } + + function handleSelectValue(value: string) { + onAdd({ dimension: selectedDim!, operator, values: [value] }) + handleClose() + } + + function handleSubmitCustom() { + if (!search.trim() || !selectedDim) return + onAdd({ dimension: selectedDim, operator, values: [search.trim()] }) + handleClose() + } + + const dimSuggestions = selectedDim ? (suggestions[selectedDim] || []) : [] + const filtered = dimSuggestions.filter(s => s.label.toLowerCase().includes(search.toLowerCase()) || s.value.toLowerCase().includes(search.toLowerCase()) ) - function handleSelectValue(value: string) { - onApply({ dimension, operator, values: [value] }) - onClose() - } - - function handleSubmitCustom() { - if (!search.trim()) return - onApply({ dimension, operator, values: [search.trim()] }) - onClose() - } - return ( -
- {/* Operator pills */} -
- {OPERATORS.map(op => ( - - ))} -
+
+ - {/* Search input */} -
- setSearch(e.target.value)} - onKeyDown={e => { - if (e.key === 'Enter') { - if (filtered.length === 1) { - handleSelectValue(filtered[0].value) - } else { - handleSubmitCustom() - } - } - }} - placeholder={`Search or type ${DIMENSION_LABELS[dimension]?.toLowerCase()}...`} - className="w-full px-3 py-2 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-brand-orange/40 focus:border-brand-orange transition-colors" - /> -
- - {/* Suggestions list */} - {filtered.length > 0 && ( -
- {filtered.map(s => ( - + ))} +
+ ) : ( + /* Step 2: Operator + search + values */ + <> + {/* Header with back button */} +
+ + + {DIMENSION_LABELS[selectedDim]} +
+ + {/* Operator pills */} +
+ {OPERATORS.map(op => ( + + ))} +
+ + {/* Search input */} +
+ setSearch(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter') { + if (filtered.length === 1) { + handleSelectValue(filtered[0].value) + } else { + handleSubmitCustom() + } + } + }} + placeholder={`Search ${DIMENSION_LABELS[selectedDim]?.toLowerCase()}...`} + className="w-full px-3 py-2 text-sm bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-neutral-900 dark:text-white placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-brand-orange/40 focus:border-brand-orange transition-colors" + /> +
+ + {/* Values list */} + {filtered.length > 0 && ( +
+ {filtered.map(s => ( + + ))} +
)} - - ))} -
- )} - {/* Custom value apply when no matches */} - {search.trim() && filtered.length === 0 && ( -
- + {/* Custom value apply */} + {search.trim() && filtered.length === 0 && ( +
+ +
+ )} + + )}
)}
) -} - -export default function AddFilterDropdown({ onAdd, suggestions = {} }: AddFilterDropdownProps) { - const [openDim, setOpenDim] = useState(null) - const [showMore, setShowMore] = useState(false) - const moreRef = useRef(null) - - useEffect(() => { - if (!showMore) return - function handleClick(e: MouseEvent) { - if (moreRef.current && !moreRef.current.contains(e.target as Node)) { - setShowMore(false) - } - } - document.addEventListener('mousedown', handleClick) - return () => document.removeEventListener('mousedown', handleClick) - }, [showMore]) - - function renderChip(dim: string) { - const isOpen = openDim === dim - return ( -
- - {isOpen && ( - setOpenDim(null)} - /> - )} -
- ) - } - - return ( -
- {PRIMARY_DIMS.map(renderChip)} - - {/* More dropdown for secondary dimensions */} -
- - {showMore && ( -
- {SECONDARY_DIMS.map(dim => ( - - ))} -
- )} - {/* Render popover for secondary dims inline here */} - {openDim && SECONDARY_DIMS.includes(openDim) && ( - setOpenDim(null)} - /> - )} -
-
- ) -} +} \ No newline at end of file