Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 52 additions & 22 deletions src/components/dashboard/DashboardFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
'use client';

import React, { useState, useCallback, useMemo } from 'react';
import { Filter, X, RotateCcw } from 'lucide-react';
import { Filter, X, RotateCcw, LifeBuoy } from 'lucide-react';
import { TimeRange, AggregationType, CHART_COLOR_PALETTE } from '@/utils/visualizationUtils';
import type { DashboardFiltersState } from '@/hooks/useDashboardData';
import { useInternationalization } from '@/hooks/useInternationalization';
Expand All @@ -18,6 +18,9 @@ import {
getDashboardTimeRangeLabel,
translateWithFallback,
} from './dashboardI18n';
import { FilterHelpPopover } from '@/components/search/FilterHelpPopover';
import { FilterSupportGuide } from '@/components/search/FilterSupportGuide';
import { useFilterCustomerSupport } from '@/hooks/useFilterCustomerSupport';

// ─── Types ────────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -52,6 +55,7 @@ export const DashboardFilters = React.memo<DashboardFiltersProps>(
}) => {
const { t } = useInternationalization();
const [isOpen, setIsOpen] = useState(false);
const support = useFilterCustomerSupport();

const timeRangeOptions = useMemo(
() =>
Expand Down Expand Up @@ -184,20 +188,32 @@ export const DashboardFilters = React.memo<DashboardFiltersProps>(
</div>
</div>

{activeFilterCount > 0 && (
<div className="flex items-center gap-2">
{activeFilterCount > 0 && (
<button
onClick={onReset}
aria-label={translateWithFallback(
t,
'dashboard.analytics.filters.resetAllAria',
'Reset all filters',
)}
className="flex items-center gap-1 px-3 py-1.5 text-sm text-gray-500 dark:text-gray-400 hover:text-red-500 dark:hover:text-red-400 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-400 rounded-lg"
>
<RotateCcw className="w-3.5 h-3.5" aria-hidden="true" />
{translateWithFallback(t, 'dashboard.analytics.filters.resetAll', 'Reset All')}
</button>
)}

<button
onClick={onReset}
aria-label={translateWithFallback(
t,
'dashboard.analytics.filters.resetAllAria',
'Reset all filters',
)}
className="flex items-center gap-1 px-3 py-1.5 text-sm text-gray-500 dark:text-gray-400 hover:text-red-500 dark:hover:text-red-400 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-400 rounded-lg"
type="button"
onClick={support.openGuide}
aria-label="Open filter help guide"
className="flex items-center gap-1 px-3 py-1.5 text-sm text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-400 rounded-lg"
>
<RotateCcw className="w-3.5 h-3.5" aria-hidden="true" />
{translateWithFallback(t, 'dashboard.analytics.filters.resetAll', 'Reset All')}
<LifeBuoy className="w-3.5 h-3.5" aria-hidden="true" />
Help
</button>
)}
</div>
</div>

{/* Expandable panel */}
Expand All @@ -208,16 +224,24 @@ export const DashboardFilters = React.memo<DashboardFiltersProps>(
>
{/* Time Range */}
<div>
<label
htmlFor="filter-time-range"
className="block text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2"
>
{translateWithFallback(
t,
'dashboard.analytics.filters.timeRange',
'Time Range',
)}
</label>
<div className="flex items-center gap-1 mb-2">
<label
htmlFor="filter-time-range"
className="block text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide"
>
{translateWithFallback(
t,
'dashboard.analytics.filters.timeRange',
'Time Range',
)}
</label>
<FilterHelpPopover
content={support.FILTER_HELP_CONTENT.duration}
isOpen={support.activeHelpId === 'duration'}
onToggle={() => support.toggleHelp('duration')}
onClose={support.closeHelp}
/>
</div>
<select
id="filter-time-range"
value={filters.timeRange}
Expand Down Expand Up @@ -336,6 +360,12 @@ export const DashboardFilters = React.memo<DashboardFiltersProps>(
</div>
</div>
)}

<FilterSupportGuide
isOpen={support.guideOpen}
onClose={support.closeGuide}
helpContent={support.FILTER_HELP_CONTENT}
/>
</div>
);
},
Expand Down
52 changes: 51 additions & 1 deletion src/components/search/FacetedFilterSystem.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
'use client';

import React, { useCallback } from 'react';
import { BarChart, DollarSign, Tag as TagIcon, Star, Filter, RotateCcw, Check } from 'lucide-react';
import { BarChart, DollarSign, Tag as TagIcon, Star, Filter, RotateCcw, Check, LifeBuoy } from 'lucide-react';
import { SearchFilters, SearchContentType } from '../../utils/searchUtils';
import { MultiSelect } from '../ui/MultiSelect';
import { RangeSlider } from '../ui/RangeSlider';
import { FilterHelpPopover } from './FilterHelpPopover';
import { FilterSupportGuide } from './FilterSupportGuide';
import { useFilterCustomerSupport } from '../../hooks/useFilterCustomerSupport';

interface FacetedFilterSystemProps {
filters: SearchFilters;
Expand Down Expand Up @@ -39,6 +42,8 @@ const DIFFICULTY_LEVELS = [

export const FacetedFilterSystem = React.memo<FacetedFilterSystemProps>(
({ filters, onFilterChange, onReset }) => {
const support = useFilterCustomerSupport();

const toggleContentType = useCallback(
(type: SearchContentType) => {
let newTypes: SearchContentType[];
Expand Down Expand Up @@ -76,6 +81,12 @@ export const FacetedFilterSystem = React.memo<FacetedFilterSystemProps>(
<div className="flex items-center justify-between mb-4">
<h3 className="text-xs font-mono font-bold text-slate-400 uppercase tracking-widest flex items-center gap-2">
<Filter className="w-3 h-3" /> Content Type
<FilterHelpPopover
content={support.FILTER_HELP_CONTENT['content-type']}
isOpen={support.activeHelpId === 'content-type'}
onToggle={() => support.toggleHelp('content-type')}
onClose={support.closeHelp}
/>
</h3>
<button
onClick={onReset}
Expand Down Expand Up @@ -109,6 +120,12 @@ export const FacetedFilterSystem = React.memo<FacetedFilterSystemProps>(
<div className="glass-panel p-6 rounded-2xl">
<h3 className="text-xs font-mono font-bold text-slate-400 uppercase tracking-widest flex items-center gap-2 mb-4">
<TagIcon className="w-3 h-3" /> Targeted Topics
<FilterHelpPopover
content={support.FILTER_HELP_CONTENT.topics}
isOpen={support.activeHelpId === 'topics'}
onToggle={() => support.toggleHelp('topics')}
onClose={support.closeHelp}
/>
</h3>
<MultiSelect
options={TOPIC_OPTIONS}
Expand All @@ -122,6 +139,12 @@ export const FacetedFilterSystem = React.memo<FacetedFilterSystemProps>(
<div className="glass-panel p-6 rounded-2xl">
<h3 className="text-xs font-mono font-bold text-slate-400 uppercase tracking-widest flex items-center gap-2 mb-4">
<BarChart className="w-3 h-3" /> Complexity Level
<FilterHelpPopover
content={support.FILTER_HELP_CONTENT.difficulty}
isOpen={support.activeHelpId === 'difficulty'}
onToggle={() => support.toggleHelp('difficulty')}
onClose={support.closeHelp}
/>
</h3>
<div className="flex flex-col gap-2">
{DIFFICULTY_LEVELS.map((level) => {
Expand Down Expand Up @@ -153,6 +176,12 @@ export const FacetedFilterSystem = React.memo<FacetedFilterSystemProps>(
<div className="glass-panel p-6 rounded-2xl md:col-span-2">
<h3 className="text-xs font-mono font-bold text-slate-400 uppercase tracking-widest flex items-center gap-2 mb-6">
<DollarSign className="w-3 h-3" /> Price Ceiling (USD)
<FilterHelpPopover
content={support.FILTER_HELP_CONTENT.price}
isOpen={support.activeHelpId === 'price'}
onToggle={() => support.toggleHelp('price')}
onClose={support.closeHelp}
/>
</h3>
<RangeSlider
min={0}
Expand Down Expand Up @@ -181,6 +210,12 @@ export const FacetedFilterSystem = React.memo<FacetedFilterSystemProps>(
<div className="glass-panel p-6 rounded-2xl">
<h3 className="text-xs font-mono font-bold text-slate-400 uppercase tracking-widest flex items-center gap-2 mb-4">
<Star className="w-3 h-3" /> Min Rating
<FilterHelpPopover
content={support.FILTER_HELP_CONTENT.rating}
isOpen={support.activeHelpId === 'rating'}
onToggle={() => support.toggleHelp('rating')}
onClose={support.closeHelp}
/>
</h3>
<div className="flex flex-col gap-2">
{[4, 3, 2].map((min) => {
Expand Down Expand Up @@ -210,6 +245,21 @@ export const FacetedFilterSystem = React.memo<FacetedFilterSystemProps>(
</div>
</div>
</div>

<button
type="button"
onClick={support.openGuide}
className="w-full py-3 px-4 bg-blue-50 border border-blue-200 text-blue-700 font-mono text-xs uppercase tracking-wider rounded-lg hover:bg-blue-100 hover:border-blue-300 transition-all duration-300 flex items-center justify-center gap-2 group cursor-pointer"
>
<LifeBuoy className="w-4 h-4" />
Need Help?
</button>

<FilterSupportGuide
isOpen={support.guideOpen}
onClose={support.closeGuide}
helpContent={support.FILTER_HELP_CONTENT}
/>
</div>
);
},
Expand Down
136 changes: 136 additions & 0 deletions src/components/search/FilterHelpPopover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
'use client';

import { useEffect, useRef } from 'react';
import { HelpCircle, X, Lightbulb, MessageCircle } from 'lucide-react';
import type { FilterHelpContent } from '@/hooks/useFilterCustomerSupport';

interface FilterHelpPopoverProps {
content: FilterHelpContent;
isOpen: boolean;
onToggle: () => void;
onClose: () => void;
}

export function FilterHelpPopover({
content,
isOpen,
onToggle,
onClose,
}: FilterHelpPopoverProps) {
const popoverRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);

useEffect(() => {
if (!isOpen) return undefined;

const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
buttonRef.current?.focus();
}
};

const handleClickOutside = (e: MouseEvent) => {
if (
popoverRef.current &&
!popoverRef.current.contains(e.target as Node) &&
buttonRef.current &&
!buttonRef.current.contains(e.target as Node)
) {
onClose();
}
};

document.addEventListener('keydown', handleKeyDown);
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen, onClose]);

return (
<div className="relative inline-flex">
<button
ref={buttonRef}
type="button"
onClick={onToggle}
aria-label={`Help: ${content.title}`}
aria-expanded={isOpen}
aria-controls={`help-popover-${content.id}`}
className="ml-1.5 inline-flex items-center justify-center rounded-full p-0.5 text-slate-400 hover:text-blue-500 hover:bg-blue-50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
>
<HelpCircle size={14} aria-hidden="true" />
</button>

{isOpen && (
<div
ref={popoverRef}
id={`help-popover-${content.id}`}
role="dialog"
aria-label={`${content.title} help`}
className="absolute left-0 top-6 z-30 w-80 rounded-xl border border-slate-200 bg-white p-4 shadow-lg dark:border-slate-700 dark:bg-slate-900"
>
<div className="flex items-start justify-between mb-3">
<h4 className="text-sm font-semibold text-slate-900 dark:text-slate-100">
{content.title}
</h4>
<button
type="button"
onClick={onClose}
aria-label="Close help"
className="rounded p-0.5 text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition-colors"
>
<X size={14} aria-hidden="true" />
</button>
</div>

<p className="text-xs text-slate-600 dark:text-slate-400 leading-relaxed mb-3">
{content.description}
</p>

{content.tips.length > 0 && (
<div className="mb-3">
<h5 className="flex items-center gap-1 text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
<Lightbulb size={12} aria-hidden="true" />
Tips
</h5>
<ul className="space-y-1">
{content.tips.map((tip, i) => (
<li
key={i}
className="text-xs text-slate-500 dark:text-slate-400 pl-4 -indent-3"
>
<span className="mr-1 text-blue-400">{'>'}</span>
{tip}
</li>
))}
</ul>
</div>
)}

{content.faqs.length > 0 && (
<div>
<h5 className="flex items-center gap-1 text-xs font-semibold text-slate-700 dark:text-slate-300 mb-1.5">
<MessageCircle size={12} aria-hidden="true" />
FAQ
</h5>
<div className="space-y-2">
{content.faqs.slice(0, 2).map((faq, i) => (
<div key={i}>
<p className="text-xs font-medium text-slate-700 dark:text-slate-300">
Q: {faq.question}
</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
A: {faq.answer}
</p>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
);
}
Loading
Loading