project setup
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { ReactNode } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import { SearchBar } from "@/components/ui/SearchBar";
|
||||
import { ItemsPerPageMenu } from "@/components/shared/ItemsPerPageMenu";
|
||||
import { SizeEnum } from "@/constants/sizeEnum";
|
||||
|
||||
interface DesktopTableControlsProps {
|
||||
searchQuery: string;
|
||||
menuVisible: boolean;
|
||||
menuAnchor: { x: number; y: number };
|
||||
searchPlaceholder?: string;
|
||||
onSearch: (query: string) => void;
|
||||
onMenuDismiss: () => void;
|
||||
onItemsPerPageChange: (value: number) => void;
|
||||
itemsPerPage?: number;
|
||||
renderFilterComponent?: () => ReactNode;
|
||||
renderFilterChips?: () => ReactNode;
|
||||
renderActionButtons?: () => ReactNode;
|
||||
itemsPerPageMenuOptions?: number[];
|
||||
}
|
||||
|
||||
export const DesktopTableControls = ({
|
||||
searchQuery,
|
||||
menuVisible,
|
||||
menuAnchor,
|
||||
searchPlaceholder = "Search",
|
||||
onSearch,
|
||||
onMenuDismiss,
|
||||
onItemsPerPageChange,
|
||||
itemsPerPage = 25,
|
||||
renderFilterComponent,
|
||||
renderFilterChips,
|
||||
renderActionButtons,
|
||||
itemsPerPageMenuOptions,
|
||||
}: DesktopTableControlsProps) => {
|
||||
return (
|
||||
<>
|
||||
<View className="flex-row items-center justify-between mb-2" style={{ height: 40 }}>
|
||||
<View className="flex-row items-center flex-1">
|
||||
<View className="flex-1 max-w-[250px]">
|
||||
<SearchBar
|
||||
placeholderText={searchPlaceholder}
|
||||
searchQuery={searchQuery}
|
||||
onChange={onSearch}
|
||||
size={SizeEnum.Small}
|
||||
/>
|
||||
</View>
|
||||
{renderFilterComponent && (
|
||||
<View className="ml-2">{renderFilterComponent()}</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View className="flex-row items-center ml-4">
|
||||
{renderActionButtons && <View>{renderActionButtons()}</View>}
|
||||
<ItemsPerPageMenu
|
||||
menuVisible={menuVisible}
|
||||
menuAnchor={menuAnchor}
|
||||
onDismiss={onMenuDismiss}
|
||||
onItemsPerPageChange={onItemsPerPageChange}
|
||||
totalItems={itemsPerPage}
|
||||
itemsPerPageMenuOptions={itemsPerPageMenuOptions || [25, 50, 100]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{renderFilterChips && <View className="">{renderFilterChips()}</View>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { View, TouchableOpacity } from "react-native";
|
||||
import { Menu, Text, IconButton, Divider } from "react-native-paper";
|
||||
|
||||
import { MultiSelectGlyph } from "./filters/MultiSelectGlyph";
|
||||
|
||||
export type FilterOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
interface FilterMultiSelectProps {
|
||||
label: string;
|
||||
options: FilterOption[];
|
||||
selected: string[];
|
||||
onChange: (values: string[]) => void;
|
||||
iconName?: string;
|
||||
className?: string;
|
||||
menuWidth?: number;
|
||||
showClearAll?: boolean;
|
||||
}
|
||||
|
||||
export const FilterMultiSelect = ({
|
||||
label,
|
||||
options,
|
||||
selected,
|
||||
onChange,
|
||||
iconName = undefined,
|
||||
className,
|
||||
menuWidth,
|
||||
showClearAll = true,
|
||||
}: FilterMultiSelectProps) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const selectedSet = useMemo(() => new Set(selected), [selected]);
|
||||
|
||||
const toggle = (value: string) => {
|
||||
const next = new Set(selectedSet);
|
||||
if (next.has(value)) {
|
||||
next.delete(value);
|
||||
} else {
|
||||
next.add(value);
|
||||
}
|
||||
onChange(Array.from(next));
|
||||
};
|
||||
|
||||
const anchor = (
|
||||
<TouchableOpacity
|
||||
onPress={() => setVisible(true)}
|
||||
className={`flex-row items-center px-2 h-8 rounded ${className || ""}`}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={{ fontSize: 12, color: "#000" }}>{label}</Text>
|
||||
{iconName ? (
|
||||
<IconButton
|
||||
icon={iconName}
|
||||
size={18}
|
||||
onPress={() => setVisible(true)}
|
||||
/>
|
||||
) : (
|
||||
<MultiSelectGlyph />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
visible={visible}
|
||||
onDismiss={() => setVisible(false)}
|
||||
anchor={anchor}
|
||||
contentStyle={menuWidth ? { width: menuWidth } : undefined}
|
||||
>
|
||||
{options.map((opt) => {
|
||||
const isSelected = selectedSet.has(opt.value);
|
||||
return (
|
||||
<Menu.Item
|
||||
key={opt.value}
|
||||
onPress={() => toggle(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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{showClearAll && (
|
||||
<>
|
||||
<Divider />
|
||||
<Menu.Item
|
||||
onPress={() => onChange([])}
|
||||
leadingIcon="close-circle-outline"
|
||||
disabled={selectedSet.size === 0}
|
||||
title="Clear all"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Menu.Item
|
||||
onPress={() => setVisible(false)}
|
||||
leadingIcon="check"
|
||||
title="Done"
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Menu } from "react-native-paper";
|
||||
|
||||
interface ItemsPerPageMenuProps {
|
||||
menuVisible: boolean;
|
||||
menuAnchor: { x: number; y: number };
|
||||
onDismiss: () => void;
|
||||
onItemsPerPageChange: (value: number) => void;
|
||||
totalItems: number;
|
||||
itemsPerPageMenuOptions: number[];
|
||||
}
|
||||
|
||||
export const ItemsPerPageMenu = ({
|
||||
menuVisible,
|
||||
menuAnchor,
|
||||
onDismiss,
|
||||
onItemsPerPageChange,
|
||||
itemsPerPageMenuOptions,
|
||||
}: ItemsPerPageMenuProps) => {
|
||||
return (
|
||||
<Menu visible={menuVisible} onDismiss={onDismiss} anchor={menuAnchor}>
|
||||
{itemsPerPageMenuOptions.map((option) => (
|
||||
<Menu.Item
|
||||
key={option}
|
||||
onPress={() => {
|
||||
onItemsPerPageChange(option);
|
||||
onDismiss();
|
||||
}}
|
||||
title={`${option} per page`}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { View } from "react-native";
|
||||
|
||||
import { SearchBar } from "@/components/ui/SearchBar";
|
||||
import { ItemsPerPageMenu } from "@/components/shared/ItemsPerPageMenu";
|
||||
|
||||
interface MobileTableControlsProps {
|
||||
searchQuery: string;
|
||||
menuVisible: boolean;
|
||||
menuAnchor: { x: number; y: number };
|
||||
searchPlaceholder?: string;
|
||||
onSearch: (query: string) => void;
|
||||
onMenuDismiss: () => void;
|
||||
onItemsPerPageChange: (value: number) => void;
|
||||
itemsPerPage?: number;
|
||||
renderFilterComponent?: () => React.ReactNode;
|
||||
renderFilterChips?: () => React.ReactNode;
|
||||
renderActionButtons?: () => React.ReactNode;
|
||||
itemsPerPageMenuOptions?: number[];
|
||||
}
|
||||
|
||||
export const MobileTableControls = ({
|
||||
searchQuery,
|
||||
menuVisible,
|
||||
menuAnchor,
|
||||
searchPlaceholder = "Search",
|
||||
onSearch,
|
||||
onMenuDismiss,
|
||||
onItemsPerPageChange,
|
||||
itemsPerPage = 25,
|
||||
renderFilterComponent,
|
||||
renderFilterChips,
|
||||
renderActionButtons,
|
||||
itemsPerPageMenuOptions,
|
||||
}: MobileTableControlsProps) => {
|
||||
return (
|
||||
<>
|
||||
<View className="flex-1">
|
||||
<SearchBar
|
||||
placeholderText={searchPlaceholder}
|
||||
searchQuery={searchQuery}
|
||||
onChange={onSearch}
|
||||
/>
|
||||
</View>
|
||||
{renderFilterComponent && (
|
||||
<View className="flex-row flex-wrap items-center mb-2 mt-2">
|
||||
<View className="flex-row flex-wrap items-center flex-1">
|
||||
{renderFilterComponent()}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Filter Chips Row */}
|
||||
{renderFilterChips && <View className="mb-2">{renderFilterChips()}</View>}
|
||||
|
||||
{/* Action Buttons and Items Per Page Row */}
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View className="flex-row items-center flex-1">
|
||||
{renderActionButtons && renderActionButtons()}
|
||||
</View>
|
||||
<View className="ml-2">
|
||||
<ItemsPerPageMenu
|
||||
menuVisible={menuVisible}
|
||||
menuAnchor={menuAnchor}
|
||||
onDismiss={onMenuDismiss}
|
||||
onItemsPerPageChange={onItemsPerPageChange}
|
||||
totalItems={itemsPerPage}
|
||||
itemsPerPageMenuOptions={itemsPerPageMenuOptions || [25, 50, 100]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { View } from "react-native";
|
||||
import { DataTable, Text } from "react-native-paper";
|
||||
|
||||
import { TableSortDirection } from "@/constants/tableSortDirectionEnum";
|
||||
|
||||
interface SortableHeaderProps {
|
||||
title: string;
|
||||
column: string;
|
||||
sortColumn: string | null;
|
||||
sortDirection: TableSortDirection;
|
||||
onSort: (column: string) => void;
|
||||
centerAlign?: boolean;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
export const SortableHeader = ({
|
||||
title,
|
||||
column,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
onSort,
|
||||
centerAlign = false,
|
||||
style,
|
||||
}: SortableHeaderProps) => {
|
||||
return (
|
||||
<DataTable.Title
|
||||
style={{
|
||||
justifyContent: centerAlign ? "center" : "flex-start",
|
||||
...style,
|
||||
}}
|
||||
onPress={() => onSort(column)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: centerAlign ? "center" : "flex-start",
|
||||
}}
|
||||
>
|
||||
<Text style={{ marginRight: 4, fontSize: 14 }}>
|
||||
{sortColumn === column ? (
|
||||
sortDirection === TableSortDirection.ASCENDING ? (
|
||||
<Ionicons name="arrow-up" size={20} color="black" />
|
||||
) : (
|
||||
<Ionicons name="arrow-down" size={20} color="black" />
|
||||
)
|
||||
) : (
|
||||
<Ionicons name="swap-vertical-outline" size={20} color="black" />
|
||||
)}
|
||||
</Text>
|
||||
<Text>{title}</Text>
|
||||
</View>
|
||||
</DataTable.Title>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { View } from "react-native";
|
||||
|
||||
import { MobileTableControls } from "./MobileTableControls";
|
||||
import { DesktopTableControls } from "./DesktopTableControls";
|
||||
|
||||
interface TableControlsProps {
|
||||
searchQuery: string;
|
||||
menuVisible: boolean;
|
||||
menuAnchor: { x: number; y: number };
|
||||
searchPlaceholder?: string;
|
||||
onSearch: (query: string) => void;
|
||||
onMenuDismiss: () => void;
|
||||
onItemsPerPageChange: (value: number) => void;
|
||||
itemsPerPage?: number;
|
||||
renderFilterComponent?: () => React.ReactNode;
|
||||
renderFilterChips?: () => React.ReactNode;
|
||||
renderActionButtons?: () => React.ReactNode;
|
||||
itemsPerPageMenuOptions?: number[];
|
||||
}
|
||||
|
||||
export const TableControls = ({
|
||||
searchQuery,
|
||||
menuVisible,
|
||||
menuAnchor,
|
||||
searchPlaceholder = "Search",
|
||||
onSearch,
|
||||
onMenuDismiss,
|
||||
onItemsPerPageChange,
|
||||
itemsPerPage = 25,
|
||||
renderFilterComponent,
|
||||
renderFilterChips,
|
||||
renderActionButtons,
|
||||
itemsPerPageMenuOptions,
|
||||
}: TableControlsProps) => {
|
||||
return (
|
||||
<>
|
||||
<View className="hidden md:flex flex-col mb-4">
|
||||
<DesktopTableControls
|
||||
searchQuery={searchQuery}
|
||||
menuVisible={menuVisible}
|
||||
menuAnchor={menuAnchor}
|
||||
onSearch={onSearch}
|
||||
onMenuDismiss={onMenuDismiss}
|
||||
onItemsPerPageChange={onItemsPerPageChange}
|
||||
itemsPerPage={itemsPerPage}
|
||||
renderFilterComponent={renderFilterComponent}
|
||||
renderFilterChips={renderFilterChips}
|
||||
renderActionButtons={renderActionButtons}
|
||||
itemsPerPageMenuOptions={itemsPerPageMenuOptions}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View className="flex md:hidden flex-col mb-4">
|
||||
<MobileTableControls
|
||||
searchQuery={searchQuery}
|
||||
menuVisible={menuVisible}
|
||||
menuAnchor={menuAnchor}
|
||||
onSearch={onSearch}
|
||||
onMenuDismiss={onMenuDismiss}
|
||||
onItemsPerPageChange={onItemsPerPageChange}
|
||||
itemsPerPage={itemsPerPage}
|
||||
renderFilterComponent={renderFilterComponent}
|
||||
renderFilterChips={renderFilterChips}
|
||||
renderActionButtons={renderActionButtons}
|
||||
itemsPerPageMenuOptions={itemsPerPageMenuOptions}
|
||||
searchPlaceholder={searchPlaceholder}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
import { View, Text, GestureResponderEvent } from "react-native";
|
||||
import { IconButton } from "react-native-paper";
|
||||
|
||||
import { useAppSelector } from "@/store/hooks";
|
||||
import { selectIsMobile } from "@/store/screen/screenSizeSlice";
|
||||
|
||||
interface TablePaginationProps {
|
||||
page: number;
|
||||
itemsPerPage: number;
|
||||
totalItems: number;
|
||||
from: number;
|
||||
to: number;
|
||||
onPageChange: (page: number) => void;
|
||||
onItemsPerPageButtonPress: (event: GestureResponderEvent) => void;
|
||||
}
|
||||
|
||||
export const TablePagination = ({
|
||||
page,
|
||||
itemsPerPage,
|
||||
totalItems,
|
||||
from,
|
||||
to,
|
||||
onPageChange,
|
||||
onItemsPerPageButtonPress,
|
||||
}: TablePaginationProps) => {
|
||||
const isMobile = useAppSelector(selectIsMobile);
|
||||
const maxPage = Math.ceil(totalItems / itemsPerPage) - 1;
|
||||
|
||||
const desktopNavigationButtons = (
|
||||
<>
|
||||
<Text className="text-sm mr-2">{`${from}-${to} of ${totalItems}`}</Text>
|
||||
<IconButton
|
||||
icon="chevron-double-left"
|
||||
size={20}
|
||||
disabled={page === 0}
|
||||
onPress={() => onPageChange(0)}
|
||||
/>
|
||||
<IconButton
|
||||
icon="chevron-left"
|
||||
size={20}
|
||||
disabled={page === 0}
|
||||
onPress={() => onPageChange(Math.max(0, page - 1))}
|
||||
/>
|
||||
<IconButton
|
||||
icon="chevron-right"
|
||||
size={20}
|
||||
disabled={page >= maxPage}
|
||||
onPress={() => onPageChange(Math.min(maxPage, page + 1))}
|
||||
/>
|
||||
<IconButton
|
||||
icon="chevron-double-right"
|
||||
size={20}
|
||||
disabled={page >= maxPage}
|
||||
onPress={() => onPageChange(maxPage)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const mobileNavigationButtons = (
|
||||
<>
|
||||
<IconButton
|
||||
icon="chevron-double-left"
|
||||
size={16}
|
||||
style={{ margin: 0 }}
|
||||
disabled={page === 0}
|
||||
onPress={() => onPageChange(0)}
|
||||
/>
|
||||
<IconButton
|
||||
icon="chevron-left"
|
||||
size={16}
|
||||
style={{ margin: 0 }}
|
||||
disabled={page === 0}
|
||||
onPress={() => onPageChange(Math.max(0, page - 1))}
|
||||
/>
|
||||
<IconButton
|
||||
icon="chevron-right"
|
||||
size={16}
|
||||
style={{ margin: 0 }}
|
||||
disabled={page >= maxPage}
|
||||
onPress={() => onPageChange(Math.min(maxPage, page + 1))}
|
||||
/>
|
||||
<IconButton
|
||||
icon="chevron-double-right"
|
||||
size={16}
|
||||
style={{ margin: 0 }}
|
||||
disabled={page >= maxPage}
|
||||
onPress={() => onPageChange(maxPage)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const itemsPerPageControls = (
|
||||
<>
|
||||
<IconButton icon="menu" size={20} onPress={onItemsPerPageButtonPress} />
|
||||
<Text className="text-sm">{`${itemsPerPage} per page`}</Text>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
className="mt-4 flex items-center"
|
||||
style={{
|
||||
marginLeft: isMobile ? 0 : "auto",
|
||||
width: isMobile ? "100%" : "auto",
|
||||
}}
|
||||
>
|
||||
{isMobile ? (
|
||||
<View className="bg-white py-1 px-2 rounded-md w-full">
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View className="flex-row items-center">{mobileNavigationButtons}</View>
|
||||
<Text className="text-xs text-gray-600">{`${from}-${to} / ${totalItems}`}</Text>
|
||||
<View className="flex-row items-center">
|
||||
<IconButton
|
||||
icon="menu"
|
||||
size={16}
|
||||
style={{ margin: 0 }}
|
||||
onPress={onItemsPerPageButtonPress}
|
||||
/>
|
||||
<Text className="text-xs text-gray-600">{`${itemsPerPage}/page`}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View className="flex-row items-center bg-white py-0.5 px-3 rounded-md">
|
||||
{desktopNavigationButtons}
|
||||
<View className="h-4 w-px bg-gray-300 mx-2" />
|
||||
{itemsPerPageControls}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -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