/** * FeaturePicker Component * * A searchable picker for selecting features to include in a plan or version. * Features are grouped by type (boolean capabilities vs integer limits). * Features are loaded dynamically from the billing API. */ import React, { useState, useMemo } from 'react'; import { Check, Sliders, Search, X } from 'lucide-react'; import type { Feature, PlanFeatureWrite } from '../../hooks/useBillingAdmin'; export interface FeaturePickerProps { /** Available features from the API */ features: Feature[]; /** Currently selected features with their values */ selectedFeatures: PlanFeatureWrite[]; /** Callback when selection changes */ onChange: (features: PlanFeatureWrite[]) => void; /** Optional: Show compact view */ compact?: boolean; } export const FeaturePicker: React.FC = ({ features, selectedFeatures, onChange, compact = false, }) => { const [searchTerm, setSearchTerm] = useState(''); // Group features by type const { booleanFeatures, integerFeatures } = useMemo(() => { const boolean = features.filter((f) => f.feature_type === 'boolean'); const integer = features.filter((f) => f.feature_type === 'integer'); return { booleanFeatures: boolean, integerFeatures: integer }; }, [features]); // Filter by search term const filteredBooleanFeatures = useMemo(() => { if (!searchTerm) return booleanFeatures; const term = searchTerm.toLowerCase(); return booleanFeatures.filter( (f) => f.name.toLowerCase().includes(term) || f.code.toLowerCase().includes(term) || f.description?.toLowerCase().includes(term) ); }, [booleanFeatures, searchTerm]); const filteredIntegerFeatures = useMemo(() => { if (!searchTerm) return integerFeatures; const term = searchTerm.toLowerCase(); return integerFeatures.filter( (f) => f.name.toLowerCase().includes(term) || f.code.toLowerCase().includes(term) || f.description?.toLowerCase().includes(term) ); }, [integerFeatures, searchTerm]); const hasNoResults = searchTerm && filteredBooleanFeatures.length === 0 && filteredIntegerFeatures.length === 0; // Check if a feature is selected const isSelected = (code: string): boolean => { return selectedFeatures.some((f) => f.feature_code === code); }; // Get selected feature data const getSelectedFeature = (code: string): PlanFeatureWrite | undefined => { return selectedFeatures.find((f) => f.feature_code === code); }; // Toggle boolean feature selection const toggleBooleanFeature = (code: string) => { if (isSelected(code)) { onChange(selectedFeatures.filter((f) => f.feature_code !== code)); } else { onChange([ ...selectedFeatures, { feature_code: code, bool_value: true, int_value: null }, ]); } }; // Toggle integer feature selection const toggleIntegerFeature = (code: string) => { if (isSelected(code)) { onChange(selectedFeatures.filter((f) => f.feature_code !== code)); } else { onChange([ ...selectedFeatures, { feature_code: code, bool_value: null, int_value: 0 }, ]); } }; // Update integer feature value const updateIntegerValue = (code: string, value: number) => { onChange( selectedFeatures.map((f) => f.feature_code === code ? { ...f, int_value: value } : f ) ); }; const clearSearch = () => { setSearchTerm(''); }; return (
{/* Search Box */}
setSearchTerm(e.target.value)} placeholder="Search features..." className="w-full pl-10 pr-10 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm" /> {searchTerm && ( )}
{/* No Results Message */} {hasNoResults && (

No features found matching "{searchTerm}"

)} {/* Boolean Features (Capabilities) */} {filteredBooleanFeatures.length > 0 && (

Capabilities

{filteredBooleanFeatures.map((feature) => { const selected = isSelected(feature.code); return ( ); })}
)} {/* Integer Features (Limits & Quotas) */} {filteredIntegerFeatures.length > 0 && (

Limits & Quotas

Set to 0 for unlimited. Uncheck to exclude from plan.

{filteredIntegerFeatures.map((feature) => { const selectedFeature = getSelectedFeature(feature.code); const selected = !!selectedFeature; return (
{selected && ( updateIntegerValue(feature.code, parseInt(e.target.value) || 0) } aria-label={`${feature.name} limit value`} className="w-20 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white" placeholder="0" /> )}
); })}
)} {/* Empty State */} {!searchTerm && features.length === 0 && (

No features defined yet.

Add features in the Features Library tab first.

)}
); };