project setup

This commit is contained in:
Sone
2025-11-02 10:08:56 +01:00
commit 568838003e
174 changed files with 27100 additions and 0 deletions
@@ -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>
);
};