project setup
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { TouchableOpacity } from "react-native";
|
||||
import { IconButton, Text } from "react-native-paper";
|
||||
|
||||
export const CombinedFilterAnchor = ({
|
||||
setVisible,
|
||||
}: {
|
||||
setVisible: (visible: boolean) => void;
|
||||
}) => (
|
||||
<TouchableOpacity
|
||||
onPress={() => setVisible(true)}
|
||||
className="flex-row items-center px-2 h-8 rounded bg-white"
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={{ fontSize: 12, color: "black" }}>Filter</Text>
|
||||
<IconButton
|
||||
icon="tune-vertical"
|
||||
size={18}
|
||||
onPress={() => setVisible(true)}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useMemo, useState, ReactNode } from "react";
|
||||
import { View, useWindowDimensions } from "react-native";
|
||||
import { Divider, Menu, Text } from "react-native-paper";
|
||||
|
||||
import { CombinedFilterAnchor } from "@/components/shared/filters/CombinedFilterAnchor";
|
||||
import { CombinedFilterResetButton } from "@/components/shared/filters/CombinedFilterResetButton";
|
||||
|
||||
type CombinedFilterOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
icon?: ReactNode;
|
||||
};
|
||||
|
||||
export type CombinedFilterSection = {
|
||||
key: string;
|
||||
title: string;
|
||||
options: CombinedFilterOption[];
|
||||
selected: string[];
|
||||
onChange: (values: string[]) => void;
|
||||
};
|
||||
|
||||
interface CombinedFilterMenuProps {
|
||||
sections: CombinedFilterSection[];
|
||||
onResetAll?: () => void;
|
||||
showResetButton?: boolean;
|
||||
}
|
||||
|
||||
export const CombinedFilterMenu = ({
|
||||
sections,
|
||||
onResetAll,
|
||||
showResetButton = false,
|
||||
}: CombinedFilterMenuProps) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { width } = useWindowDimensions();
|
||||
|
||||
const totalSelected = useMemo(
|
||||
() => sections.reduce((sum, s) => sum + (s.selected?.length || 0), 0),
|
||||
[sections]
|
||||
);
|
||||
|
||||
// Determine if mobile or desktop
|
||||
const isMobile = width < 768;
|
||||
|
||||
const toggle = (sectionKey: string, value: string) => {
|
||||
const section = sections.find((s) => s.key === sectionKey);
|
||||
if (!section) return;
|
||||
const set = new Set(section.selected);
|
||||
if (set.has(value)) {
|
||||
set.delete(value);
|
||||
} else {
|
||||
set.add(value);
|
||||
}
|
||||
section.onChange(Array.from(set));
|
||||
};
|
||||
|
||||
const clearAll = () => {
|
||||
sections.forEach((s) => s.onChange([]));
|
||||
if (onResetAll) onResetAll();
|
||||
};
|
||||
|
||||
const columnSections = useMemo(() => {
|
||||
if (isMobile) {
|
||||
return [sections];
|
||||
}
|
||||
|
||||
return sections.map((section) => [section]);
|
||||
}, [sections, isMobile]);
|
||||
|
||||
const renderSection = (
|
||||
section: CombinedFilterSection
|
||||
) => (
|
||||
<View key={section.key} className="mb-2">
|
||||
<Menu.Item
|
||||
title={<Text style={{ fontWeight: "bold" }}>{section.title}</Text>}
|
||||
disabled
|
||||
/>
|
||||
{section.options.map((opt) => {
|
||||
const isSelected = section.selected.includes(opt.value);
|
||||
return (
|
||||
<Menu.Item
|
||||
key={`${section.key}-${opt.value}`}
|
||||
onPress={() => toggle(section.key, opt.value)}
|
||||
leadingIcon={
|
||||
isSelected ? "checkbox-marked" : "checkbox-blank-outline"
|
||||
}
|
||||
title={
|
||||
<View className="flex-row items-center">
|
||||
{opt.icon ? <View className="mr-2">{opt.icon}</View> : null}
|
||||
<Text>{opt.label}</Text>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View className="flex-row items-center">
|
||||
<Menu
|
||||
visible={visible}
|
||||
onDismiss={() => setVisible(false)}
|
||||
anchor={<CombinedFilterAnchor setVisible={setVisible} />}
|
||||
contentStyle={{
|
||||
minWidth: isMobile
|
||||
? width - 32
|
||||
: Math.min(sections.length * 250, 800),
|
||||
maxWidth: isMobile ? width - 32 : undefined,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
className="flex-row"
|
||||
style={{ flexWrap: isMobile ? "nowrap" : "wrap" }}
|
||||
>
|
||||
{columnSections.map((columnSects, colIndex) => (
|
||||
<View
|
||||
key={`column-${colIndex}`}
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: isMobile ? undefined : 200,
|
||||
paddingRight: colIndex < columnSections.length - 1 ? 8 : 0,
|
||||
}}
|
||||
>
|
||||
{columnSects.map((section) => renderSection(section))}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
<Divider />
|
||||
<Menu.Item
|
||||
onPress={clearAll}
|
||||
leadingIcon="close-circle-outline"
|
||||
disabled={totalSelected <= 1}
|
||||
title="Clear all"
|
||||
/>
|
||||
<Menu.Item
|
||||
onPress={() => setVisible(false)}
|
||||
leadingIcon="check"
|
||||
title="Done"
|
||||
/>
|
||||
</Menu>
|
||||
<CombinedFilterResetButton
|
||||
showResetButton={showResetButton}
|
||||
totalSelected={totalSelected}
|
||||
clearAll={clearAll}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { TouchableOpacity } from "react-native";
|
||||
import { IconButton, Text } from "react-native-paper";
|
||||
|
||||
interface CombinedFilterResetButtonProps {
|
||||
showResetButton: boolean;
|
||||
totalSelected: number;
|
||||
clearAll: () => void;
|
||||
}
|
||||
|
||||
export const CombinedFilterResetButton = ({
|
||||
showResetButton,
|
||||
totalSelected,
|
||||
clearAll,
|
||||
}: CombinedFilterResetButtonProps) =>
|
||||
showResetButton &&
|
||||
totalSelected > 1 && (
|
||||
<TouchableOpacity
|
||||
onPress={clearAll}
|
||||
className="flex-row items-center px-2 h-8 rounded bg-white ml-2"
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={{ fontSize: 12, color: "black" }}>Reset all</Text>
|
||||
<IconButton icon="close-circle-outline" size={18} onPress={clearAll} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
import { View } from "react-native";
|
||||
|
||||
export const MultiSelectGlyph = () => (
|
||||
<View className="ml-1 flex-row items-center">
|
||||
<View className="w-1 h-4 bg-gray-500 rounded" />
|
||||
<View className="ml-1 w-2 h-2 bg-gray-500 rounded-full" />
|
||||
<View className="ml-1 w-2 h-2 bg-gray-500 rounded-full" />
|
||||
</View>
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { View } from "react-native";
|
||||
import { Chip } from "react-native-paper";
|
||||
|
||||
export type SelectedChip = {
|
||||
key: string;
|
||||
label: string;
|
||||
onRemove: () => void;
|
||||
};
|
||||
|
||||
interface SelectedFilterChipsProps {
|
||||
chips: SelectedChip[];
|
||||
}
|
||||
|
||||
export const SelectedFilterChips = ({ chips }: SelectedFilterChipsProps) => {
|
||||
if (chips.length === 0) return null;
|
||||
|
||||
return (
|
||||
<View className="flex-row flex-wrap items-center" style={{ width: '100%' }}>
|
||||
{chips.map((chip) => (
|
||||
<View key={chip.key} className="mr-1 mb-1">
|
||||
<Chip onClose={chip.onRemove}>{chip.label}</Chip>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user