init filter

This commit is contained in:
2025-06-02 23:20:36 +02:00
parent d6fd6a225b
commit 0bae3bf212
5 changed files with 482 additions and 81 deletions

View File

@@ -1,38 +1,105 @@
// import React from "react" import { FlatList, Text, ActivityIndicator, RefreshControl, Dimensions } from "react-native";
import {FlatList, Text, ActivityIndicator, RefreshControl} from "react-native"; import { useState, useEffect } from "react";
import {useState, useEffect} from "react";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import {useNoticesStore} from "@/store/noticesStore"; import { useNoticesStore } from "@/store/noticesStore";
import {NoticeCard} from "@/components/NoticeCard"; import { NoticeCard } from "@/components/NoticeCard";
import { useLocalSearchParams } from "expo-router"; import { useLocalSearchParams, useRouter } from "expo-router";
import { HStack } from "@/components/ui/hstack" import { Box } from "@/components/ui/box";
import { Box } from "@/components/ui/box" import { Button, ButtonText } from "@/components/ui/button";
import { Button, ButtonText, ButtonIcon } from "@/components/ui/button"; import { ChevronDownIcon } from "@/components/ui/icon";
import { listCategories } from "@/api/categories";
import { FormControl, FormControlLabel } from "@/components/ui/form-control";
import { Input, InputField } from "@/components/ui/input";
import { HStack } from "@/components/ui/hstack";
import { import {
Actionsheet, Actionsheet,
ActionsheetContent, ActionsheetContent,
ActionsheetItem, ActionsheetDragIndicator,
ActionsheetItemText, ActionsheetDragIndicatorWrapper,
ActionsheetDragIndicator, ActionsheetBackdrop,
ActionsheetDragIndicatorWrapper, } from "@/components/ui/actionsheet";
ActionsheetBackdrop, import {
} from "@/components/ui/actionsheet" Select,
import { useRouter } from "expo-router"; SelectTrigger,
SelectInput,
SelectIcon,
SelectPortal,
SelectBackdrop,
SelectContent,
SelectDragIndicator,
SelectDragIndicatorWrapper,
SelectItem,
} from "@/components/ui/select";
export default function Notices() { export default function Notices() {
const {notices, fetchNotices} = useNoticesStore(); // Hooks
const { notices, fetchNotices } = useNoticesStore();
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [showActionsheet, setShowActionsheet] = useState(false) const [showActionsheet, setShowActionsheet] = useState(false);
const handleClose = () => setShowActionsheet(false) const [categories, setCategories] = useState([]);
const [filteredNotices, setFilteredNotices] = useState([]);
const params = useLocalSearchParams(); const params = useLocalSearchParams();
console.log("GET params:", params); const router = useRouter();
useEffect(() => {
const fetchSelectItems = async () => {
try {
const data = await listCategories();
if (Array.isArray(data)) {
setCategories(data);
} else {
console.error('listCategories did not return an array:', data);
setError(new Error('Invalid categories data'));
}
} catch (error) {
console.error('Error fetching select items:', error);
setError(error);
}
};
fetchSelectItems();
}, []);
useEffect(() => { useEffect(() => {
loadData(); loadData();
}, []); }, []);
useEffect(() => {
let result = notices;
if (params.category) {
result = result.filter(notice => notice.category === params.category);
}
if (params.sort === "latest") {
result = [...result].sort(
(a, b) => new Date(b.publishDate) - new Date(a.publishDate)
);
}
if(params.priceFrom) {
result = result.filter(notice => {
const price = parseFloat(notice.price);
const priceFrom = parseFloat(params.priceFrom);
return !isNaN(price) && price >= priceFrom;
});
}
if (params.priceTo) {
result = result.filter(notice => {
const price = parseFloat(notice.price);
const priceTo = parseFloat(params.priceTo);
return !isNaN(price) && price <= priceTo;
});
}
setFilteredNotices(result);
}, [notices, params.category, params.sort, params.priceFrom, params.priceTo]);
let filterActive = !!params.category || params.sort === "latest";
const loadData = async () => { const loadData = async () => {
@@ -47,29 +114,29 @@ export default function Notices() {
} }
}; };
const router = useRouter(); const handleCategorySelect = (value) => {
router.replace({
pathname: "/notices",
params: { ...params, category: value }
});
};
let filteredNotices = notices; const handlePriceFrom = (value) => {
let filterActive = false; router.replace({
if (params.sort) { pathname: "/notices",
if( params.sort === "latest") { params: { ...params, priceFrom: value }
filteredNotices = [...filteredNotices].sort( });
(a, b) => new Date(b.publishDate) - new Date(a.publishDate)
);
}
} }
if(params.category) { const handlePriceTo = (value) => {
filteredNotices = filteredNotices.filter( router.replace({
(notice) => notice.category === params.category pathname: "/notices",
); params: { ...params, priceTo: value }
filterActive = true; });
} }
if(params.attribute) { const handleClose = () => setShowActionsheet(false);
filterActive = true;
}
const onRefresh = async () => { const onRefresh = async () => {
setRefreshing(true); setRefreshing(true);
try { try {
@@ -82,52 +149,118 @@ export default function Notices() {
}; };
if (isLoading && !refreshing) { if (isLoading && !refreshing) {
return <ActivityIndicator/>; return <ActivityIndicator />;
} }
if (error) { if (error) {
return <Text>Nie udało sie pobrać listy. {error.message}</Text>; return <Text>Nie udało się pobrać listy. {error.message}</Text>;
} }
const SCREEN_HEIGHT = Dimensions.get('window').height;
const selectedCategory = params.category && categories?.find(
(cat) => cat.value === params.category
) || null;
return ( return (
<> <>
<Box className="flex-row p-2 pt-4 pb-4 bg-white items-center justify-between"> <Box style={{ flexDirection: "row", padding: 8, paddingTop: 16, paddingBottom: 16, backgroundColor: "white", alignItems: "center", justifyContent: "space-between" }}>
<Button variant="outline" onPress={() => setShowActionsheet(true)}> <Button variant="outline" onPress={() => setShowActionsheet(true)}>
<ButtonText>Filtry</ButtonText> <ButtonText>Filtry</ButtonText>
<Ionicons name="filter-outline" size={20} color="black" /> <Ionicons name="filter-outline" size={20} color="black" />
</Button> </Button>
{filterActive && ( {filterActive && (
<Button variant="link" onPress={() => router.replace("/notices")}> <Button variant="link" onPress={() => router.replace("/notices")}>
<ButtonText>Wyczyść</ButtonText> <ButtonText>Wyczyść</ButtonText>
</Button>)} </Button>
</Box> )}
<Actionsheet isOpen={showActionsheet} onClose={handleClose}> </Box>
<ActionsheetBackdrop /> <Actionsheet isOpen={showActionsheet} onClose={handleClose}>
<ActionsheetContent> <ActionsheetBackdrop />
<ActionsheetDragIndicatorWrapper> <ActionsheetContent>
<ActionsheetDragIndicator /> <ActionsheetDragIndicatorWrapper>
</ActionsheetDragIndicatorWrapper> <ActionsheetDragIndicator />
<ActionsheetItem> </ActionsheetDragIndicatorWrapper>
<ActionsheetItemText>Kategoria</ActionsheetItemText> <Box className="mb-4">
</ActionsheetItem> <HStack space="md" style={{ width: "100%"}}>
</ActionsheetContent> <FormControl
</Actionsheet> style={{ flex: 1 }}>
<FlatList <Input>
key={2} <InputField
data={filteredNotices} keyboardType="numeric"
numColumns={2} placeholder="Od:"
columnContainerClassName="m-2" value={params.priceFrom || ''}
columnWrapperClassName="gap-2 m-2" onChangeText={handlePriceFrom}
renderItem={({item}) => <NoticeCard notice={item}/>} />
refreshControl={ </Input>
<RefreshControl </FormControl>
refreshing={refreshing} <FormControl
onRefresh={onRefresh} style={{ flex: 1 }}>
colors={["#3b82f6"]} <Input>
tintColor="#3b82f6" <InputField
/> keyboardType="numeric"
} placeholder="Do:"
/> value={params.priceTo || ''}
onChangeText={handlePriceTo}
/>
</Input>
</FormControl>
</HStack>
</Box>
<Box className="mb-4 w-full">
<Select
style={{ width: '100%' }}
selectedValue={params.category || ''}
onValueChange={handleCategorySelect}
>
<SelectTrigger variant="outline" size="md">
<SelectInput
placeholder="Wybierz kategorię"
value={selectedCategory ? selectedCategory.label : ""}
/>
<SelectIcon style={{ marginRight: 12 }} as={ChevronDownIcon} />
</SelectTrigger>
<SelectPortal>
<SelectBackdrop />
<SelectContent
style={{ maxHeight: SCREEN_HEIGHT * 0.6, width: '100%' }}
>
<SelectDragIndicatorWrapper>
<SelectDragIndicator />
</SelectDragIndicatorWrapper>
<FlatList
style={{ width: '100%' }}
data={categories}
keyExtractor={(item) => item.value?.toString() || item.id?.toString() || Math.random().toString()}
renderItem={({ item }) => (
<SelectItem
label={item.label}
value={item.value}
/>
)}
/>
</SelectContent>
</SelectPortal>
</Select>
</Box>
</ActionsheetContent>
</Actionsheet>
<FlatList
data={filteredNotices}
numColumns={2}
columnWrapperStyle={{ gap: 8, marginHorizontal: 8 }}
contentContainerStyle={{ paddingBottom: 16 }}
renderItem={({ item }) => <NoticeCard notice={item} />}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={["#3b82f6"]}
tintColor="#3b82f6"
/>
}
/>
</> </>
); );
} }

View File

@@ -0,0 +1,264 @@
'use client';
import { createSlider } from '@gluestack-ui/slider';
import { Pressable } from 'react-native';
import { View } from 'react-native';
import React from 'react';
import { tva } from '@gluestack-ui/nativewind-utils/tva';
import {
withStyleContext,
useStyleContext,
} from '@gluestack-ui/nativewind-utils/withStyleContext';
import type { VariantProps } from '@gluestack-ui/nativewind-utils';
import { cssInterop } from 'nativewind';
const SCOPE = 'SLIDER';
const Root = withStyleContext(View, SCOPE);
export const UISlider = createSlider({
Root: Root,
Thumb: View,
Track: Pressable,
FilledTrack: View,
ThumbInteraction: View,
});
cssInterop(UISlider.Track, { className: 'style' });
const sliderStyle = tva({
base: 'justify-center items-center data-[disabled=true]:opacity-40 data-[disabled=true]:web:pointer-events-none',
variants: {
orientation: {
horizontal: 'w-full',
vertical: 'h-full',
},
size: {
sm: '',
md: '',
lg: '',
},
isReversed: {
true: '',
false: '',
},
},
});
const sliderThumbStyle = tva({
base: 'bg-primary-500 absolute rounded-full data-[focus=true]:bg-primary-600 data-[active=true]:bg-primary-600 data-[hover=true]:bg-primary-600 data-[disabled=true]:bg-primary-500 web:cursor-pointer web:data-[active=true]:outline web:data-[active=true]:outline-4 web:data-[active=true]:outline-primary-400 shadow-hard-1',
parentVariants: {
size: {
sm: 'h-4 w-4',
md: 'h-5 w-5',
lg: 'h-6 w-6',
},
},
});
const sliderTrackStyle = tva({
base: 'bg-background-300 rounded-lg overflow-hidden',
parentVariants: {
orientation: {
horizontal: 'w-full',
vertical: 'h-full',
},
isReversed: {
true: '',
false: '',
},
size: {
sm: '',
md: '',
lg: '',
},
},
parentCompoundVariants: [
{
orientation: 'horizontal',
size: 'sm',
class: 'h-1 flex-row',
},
{
orientation: 'horizontal',
size: 'sm',
isReversed: true,
class: 'h-1 flex-row-reverse',
},
{
orientation: 'horizontal',
size: 'md',
class: 'h-1 flex-row',
},
{
orientation: 'horizontal',
size: 'md',
isReversed: true,
class: 'h-[5px] flex-row-reverse',
},
{
orientation: 'horizontal',
size: 'lg',
class: 'h-1.5 flex-row',
},
{
orientation: 'horizontal',
size: 'lg',
isReversed: true,
class: 'h-1.5 flex-row-reverse',
},
{
orientation: 'vertical',
size: 'sm',
class: 'w-1 flex-col-reverse',
},
{
orientation: 'vertical',
size: 'sm',
isReversed: true,
class: 'w-1 flex-col',
},
{
orientation: 'vertical',
size: 'md',
class: 'w-[5px] flex-col-reverse',
},
{
orientation: 'vertical',
size: 'md',
isReversed: true,
class: 'w-[5px] flex-col',
},
{
orientation: 'vertical',
size: 'lg',
class: 'w-1.5 flex-col-reverse',
},
{
orientation: 'vertical',
size: 'lg',
isReversed: true,
class: 'w-1.5 flex-col',
},
],
});
const sliderFilledTrackStyle = tva({
base: 'bg-primary-500 data-[focus=true]:bg-primary-600 data-[active=true]:bg-primary-600 data-[hover=true]:bg-primary-600',
parentVariants: {
orientation: {
horizontal: 'h-full',
vertical: 'w-full',
},
},
});
type ISliderProps = React.ComponentProps<typeof UISlider> &
VariantProps<typeof sliderStyle>;
const Slider = React.forwardRef<
React.ComponentRef<typeof UISlider>,
ISliderProps
>(function Slider(
{
className,
size = 'md',
orientation = 'horizontal',
isReversed = false,
...props
},
ref
) {
return (
<UISlider
ref={ref}
isReversed={isReversed}
orientation={orientation}
{...props}
className={sliderStyle({
orientation,
isReversed,
class: className,
})}
context={{ size, orientation, isReversed }}
/>
);
});
type ISliderThumbProps = React.ComponentProps<typeof UISlider.Thumb> &
VariantProps<typeof sliderThumbStyle>;
const SliderThumb = React.forwardRef<
React.ComponentRef<typeof UISlider.Thumb>,
ISliderThumbProps
>(function SliderThumb({ className, size, ...props }, ref) {
const { size: parentSize } = useStyleContext(SCOPE);
return (
<UISlider.Thumb
ref={ref}
{...props}
className={sliderThumbStyle({
parentVariants: {
size: parentSize,
},
size,
class: className,
})}
/>
);
});
type ISliderTrackProps = React.ComponentProps<typeof UISlider.Track> &
VariantProps<typeof sliderTrackStyle>;
const SliderTrack = React.forwardRef<
React.ComponentRef<typeof UISlider.Track>,
ISliderTrackProps
>(function SliderTrack({ className, ...props }, ref) {
const {
orientation: parentOrientation,
size: parentSize,
isReversed,
} = useStyleContext(SCOPE);
return (
<UISlider.Track
ref={ref}
{...props}
className={sliderTrackStyle({
parentVariants: {
orientation: parentOrientation,
size: parentSize,
isReversed,
},
class: className,
})}
/>
);
});
type ISliderFilledTrackProps = React.ComponentProps<
typeof UISlider.FilledTrack
> &
VariantProps<typeof sliderFilledTrackStyle>;
const SliderFilledTrack = React.forwardRef<
React.ComponentRef<typeof UISlider.FilledTrack>,
ISliderFilledTrackProps
>(function SliderFilledTrack({ className, ...props }, ref) {
const { orientation: parentOrientation } = useStyleContext(SCOPE);
return (
<UISlider.FilledTrack
ref={ref}
{...props}
className={sliderFilledTrackStyle({
parentVariants: {
orientation: parentOrientation,
},
class: className,
})}
/>
);
});
export { Slider, SliderThumb, SliderTrack, SliderFilledTrack };

View File

@@ -23,6 +23,7 @@
"@gluestack-ui/overlay": "^0.1.22", "@gluestack-ui/overlay": "^0.1.22",
"@gluestack-ui/pressable": "^0.1.23", "@gluestack-ui/pressable": "^0.1.23",
"@gluestack-ui/select": "^0.1.31", "@gluestack-ui/select": "^0.1.31",
"@gluestack-ui/slider": "^0.1.32",
"@gluestack-ui/textarea": "^0.1.25", "@gluestack-ui/textarea": "^0.1.25",
"@gluestack-ui/themed": "^1.1.73", "@gluestack-ui/themed": "^1.1.73",
"@gluestack-ui/toast": "^1.0.9", "@gluestack-ui/toast": "^1.0.9",
@@ -2329,6 +2330,8 @@
}, },
"node_modules/@gluestack-ui/slider": { "node_modules/@gluestack-ui/slider": {
"version": "0.1.32", "version": "0.1.32",
"resolved": "https://registry.npmjs.org/@gluestack-ui/slider/-/slider-0.1.32.tgz",
"integrity": "sha512-g0e7dAGOYYARlL3cdHe3mhN71j85TnqUgK/xOYWjVDE0U+atIXxxTVEXeO0ZPGJ3YUOUUAInIVGaa0xvnjEkYg==",
"dependencies": { "dependencies": {
"@gluestack-ui/form-control": "^0.1.19", "@gluestack-ui/form-control": "^0.1.19",
"@gluestack-ui/hooks": "0.1.13", "@gluestack-ui/hooks": "0.1.13",

View File

@@ -24,6 +24,7 @@
"@gluestack-ui/overlay": "^0.1.22", "@gluestack-ui/overlay": "^0.1.22",
"@gluestack-ui/pressable": "^0.1.23", "@gluestack-ui/pressable": "^0.1.23",
"@gluestack-ui/select": "^0.1.31", "@gluestack-ui/select": "^0.1.31",
"@gluestack-ui/slider": "^0.1.32",
"@gluestack-ui/textarea": "^0.1.25", "@gluestack-ui/textarea": "^0.1.25",
"@gluestack-ui/themed": "^1.1.73", "@gluestack-ui/themed": "^1.1.73",
"@gluestack-ui/toast": "^1.0.9", "@gluestack-ui/toast": "^1.0.9",