init filter
This commit is contained in:
@@ -1,38 +1,105 @@
|
||||
// import React from "react"
|
||||
import {FlatList, Text, ActivityIndicator, RefreshControl} from "react-native";
|
||||
import {useState, useEffect} from "react";
|
||||
import { FlatList, Text, ActivityIndicator, RefreshControl, Dimensions } from "react-native";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import {useNoticesStore} from "@/store/noticesStore";
|
||||
import {NoticeCard} from "@/components/NoticeCard";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { HStack } from "@/components/ui/hstack"
|
||||
import { Box } from "@/components/ui/box"
|
||||
import { Button, ButtonText, ButtonIcon } from "@/components/ui/button";
|
||||
import { useNoticesStore } from "@/store/noticesStore";
|
||||
import { NoticeCard } from "@/components/NoticeCard";
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { Box } from "@/components/ui/box";
|
||||
import { Button, ButtonText } 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 {
|
||||
Actionsheet,
|
||||
ActionsheetContent,
|
||||
ActionsheetItem,
|
||||
ActionsheetItemText,
|
||||
ActionsheetDragIndicator,
|
||||
ActionsheetDragIndicatorWrapper,
|
||||
ActionsheetBackdrop,
|
||||
} from "@/components/ui/actionsheet"
|
||||
import { useRouter } from "expo-router";
|
||||
Actionsheet,
|
||||
ActionsheetContent,
|
||||
ActionsheetDragIndicator,
|
||||
ActionsheetDragIndicatorWrapper,
|
||||
ActionsheetBackdrop,
|
||||
} from "@/components/ui/actionsheet";
|
||||
import {
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectInput,
|
||||
SelectIcon,
|
||||
SelectPortal,
|
||||
SelectBackdrop,
|
||||
SelectContent,
|
||||
SelectDragIndicator,
|
||||
SelectDragIndicatorWrapper,
|
||||
SelectItem,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
export default function Notices() {
|
||||
const {notices, fetchNotices} = useNoticesStore();
|
||||
// Hooks
|
||||
const { notices, fetchNotices } = useNoticesStore();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [showActionsheet, setShowActionsheet] = useState(false)
|
||||
const handleClose = () => setShowActionsheet(false)
|
||||
|
||||
const [showActionsheet, setShowActionsheet] = useState(false);
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [filteredNotices, setFilteredNotices] = useState([]);
|
||||
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(() => {
|
||||
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 () => {
|
||||
@@ -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;
|
||||
let filterActive = false;
|
||||
if (params.sort) {
|
||||
if( params.sort === "latest") {
|
||||
filteredNotices = [...filteredNotices].sort(
|
||||
(a, b) => new Date(b.publishDate) - new Date(a.publishDate)
|
||||
);
|
||||
}
|
||||
const handlePriceFrom = (value) => {
|
||||
router.replace({
|
||||
pathname: "/notices",
|
||||
params: { ...params, priceFrom: value }
|
||||
});
|
||||
}
|
||||
|
||||
if(params.category) {
|
||||
filteredNotices = filteredNotices.filter(
|
||||
(notice) => notice.category === params.category
|
||||
);
|
||||
filterActive = true;
|
||||
const handlePriceTo = (value) => {
|
||||
router.replace({
|
||||
pathname: "/notices",
|
||||
params: { ...params, priceTo: value }
|
||||
});
|
||||
}
|
||||
|
||||
if(params.attribute) {
|
||||
filterActive = true;
|
||||
}
|
||||
|
||||
const handleClose = () => setShowActionsheet(false);
|
||||
|
||||
const onRefresh = async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
@@ -82,52 +149,118 @@ export default function Notices() {
|
||||
};
|
||||
|
||||
if (isLoading && !refreshing) {
|
||||
return <ActivityIndicator/>;
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Box className="flex-row p-2 pt-4 pb-4 bg-white items-center justify-between">
|
||||
<Button variant="outline" onPress={() => setShowActionsheet(true)}>
|
||||
<ButtonText>Filtry</ButtonText>
|
||||
<Ionicons name="filter-outline" size={20} color="black" />
|
||||
</Button>
|
||||
{filterActive && (
|
||||
<Button variant="link" onPress={() => router.replace("/notices")}>
|
||||
<ButtonText>Wyczyść</ButtonText>
|
||||
</Button>)}
|
||||
</Box>
|
||||
<Actionsheet isOpen={showActionsheet} onClose={handleClose}>
|
||||
<ActionsheetBackdrop />
|
||||
<ActionsheetContent>
|
||||
<ActionsheetDragIndicatorWrapper>
|
||||
<ActionsheetDragIndicator />
|
||||
</ActionsheetDragIndicatorWrapper>
|
||||
<ActionsheetItem>
|
||||
<ActionsheetItemText>Kategoria</ActionsheetItemText>
|
||||
</ActionsheetItem>
|
||||
</ActionsheetContent>
|
||||
</Actionsheet>
|
||||
<FlatList
|
||||
key={2}
|
||||
data={filteredNotices}
|
||||
numColumns={2}
|
||||
columnContainerClassName="m-2"
|
||||
columnWrapperClassName="gap-2 m-2"
|
||||
renderItem={({item}) => <NoticeCard notice={item}/>}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
colors={["#3b82f6"]}
|
||||
tintColor="#3b82f6"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<Box style={{ flexDirection: "row", padding: 8, paddingTop: 16, paddingBottom: 16, backgroundColor: "white", alignItems: "center", justifyContent: "space-between" }}>
|
||||
<Button variant="outline" onPress={() => setShowActionsheet(true)}>
|
||||
<ButtonText>Filtry</ButtonText>
|
||||
<Ionicons name="filter-outline" size={20} color="black" />
|
||||
</Button>
|
||||
{filterActive && (
|
||||
<Button variant="link" onPress={() => router.replace("/notices")}>
|
||||
<ButtonText>Wyczyść</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<Actionsheet isOpen={showActionsheet} onClose={handleClose}>
|
||||
<ActionsheetBackdrop />
|
||||
<ActionsheetContent>
|
||||
<ActionsheetDragIndicatorWrapper>
|
||||
<ActionsheetDragIndicator />
|
||||
</ActionsheetDragIndicatorWrapper>
|
||||
<Box className="mb-4">
|
||||
<HStack space="md" style={{ width: "100%"}}>
|
||||
<FormControl
|
||||
style={{ flex: 1 }}>
|
||||
<Input>
|
||||
<InputField
|
||||
keyboardType="numeric"
|
||||
placeholder="Od:"
|
||||
value={params.priceFrom || ''}
|
||||
onChangeText={handlePriceFrom}
|
||||
/>
|
||||
</Input>
|
||||
</FormControl>
|
||||
<FormControl
|
||||
style={{ flex: 1 }}>
|
||||
<Input>
|
||||
<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"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
0
ArtisanConnect/components/FilterSection.jsx
Normal file
0
ArtisanConnect/components/FilterSection.jsx
Normal file
264
ArtisanConnect/components/ui/slider/index.tsx
Normal file
264
ArtisanConnect/components/ui/slider/index.tsx
Normal 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 };
|
||||
3
ArtisanConnect/package-lock.json
generated
3
ArtisanConnect/package-lock.json
generated
@@ -23,6 +23,7 @@
|
||||
"@gluestack-ui/overlay": "^0.1.22",
|
||||
"@gluestack-ui/pressable": "^0.1.23",
|
||||
"@gluestack-ui/select": "^0.1.31",
|
||||
"@gluestack-ui/slider": "^0.1.32",
|
||||
"@gluestack-ui/textarea": "^0.1.25",
|
||||
"@gluestack-ui/themed": "^1.1.73",
|
||||
"@gluestack-ui/toast": "^1.0.9",
|
||||
@@ -2329,6 +2330,8 @@
|
||||
},
|
||||
"node_modules/@gluestack-ui/slider": {
|
||||
"version": "0.1.32",
|
||||
"resolved": "https://registry.npmjs.org/@gluestack-ui/slider/-/slider-0.1.32.tgz",
|
||||
"integrity": "sha512-g0e7dAGOYYARlL3cdHe3mhN71j85TnqUgK/xOYWjVDE0U+atIXxxTVEXeO0ZPGJ3YUOUUAInIVGaa0xvnjEkYg==",
|
||||
"dependencies": {
|
||||
"@gluestack-ui/form-control": "^0.1.19",
|
||||
"@gluestack-ui/hooks": "0.1.13",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@gluestack-ui/overlay": "^0.1.22",
|
||||
"@gluestack-ui/pressable": "^0.1.23",
|
||||
"@gluestack-ui/select": "^0.1.31",
|
||||
"@gluestack-ui/slider": "^0.1.32",
|
||||
"@gluestack-ui/textarea": "^0.1.25",
|
||||
"@gluestack-ui/themed": "^1.1.73",
|
||||
"@gluestack-ui/toast": "^1.0.9",
|
||||
|
||||
Reference in New Issue
Block a user