Add, list, getbyid

working via zustand now
This commit is contained in:
2025-05-20 15:19:29 +02:00
parent 0295386dac
commit a7cf31900b
10 changed files with 1199 additions and 865 deletions

View File

@@ -31,19 +31,17 @@ export async function createNotice(notice) {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
}); });
// console.log("Response", response.data, "status code: ", response.status);
// console.log("New notice id: ", response.data.noticeId);
// console.log("Image url: ", notice.image)
if (response.data.noticeId !== null) { if (response.data.noticeId !== null) {
notice.image.forEach(imageUri => { notice.image.forEach(imageUri => {
uploadImage(response.data.noticeId, imageUri); uploadImage(response.data.noticeId, imageUri);
}); });
// uploadImage(response.data.noticeId, notice.image);
} }
return response.data;
} catch (error) { } catch (error) {
console.log("Error", error.response.data, error.response.status); console.log("Error", error.response.data, error.response.status);
return null;
} }
} }
@@ -87,7 +85,6 @@ export async function getAllImagesByNoticeId(noticeId) {
} }
export const uploadImage = async (noticeId, imageUri) => { export const uploadImage = async (noticeId, imageUri) => {
console.log("Started upload image");
console.log(imageUri); console.log(imageUri);
const formData = new FormData(); const formData = new FormData();

View File

@@ -32,7 +32,13 @@
{ {
"photosPermission": "The app accesses your photos to let you share them with your friends." "photosPermission": "The app accesses your photos to let you share them with your friends."
} }
],
[
"expo-camera",
{
"cameraPermission": "Please allow $(PRODUCT_NAME) to access your camera"
}
] ]
] ]
} }
} }

View File

@@ -21,17 +21,20 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import {ChevronDownIcon} from "@/components/ui/icon"; import {ChevronDownIcon} from "@/components/ui/icon";
import {useMutation} from "@tanstack/react-query"; import {useNoticesStore} from "@/store/noticesStore";
import {createNotice} from "@/api/notices";
import {listCategories} from "@/api/categories"; import {listCategories} from "@/api/categories";
import {useRouter} from "expo-router";
export default function CreateNotice() { export default function CreateNotice() {
const router = useRouter();
const {addNotice} = useNoticesStore();
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [price, setPrice] = useState(""); const [price, setPrice] = useState("");
const [category, setCategory] = useState(""); const [category, setCategory] = useState("");
const [image, setImage] = useState([]); const [image, setImage] = useState([]);
const [selectItems, setSelectItems] = useState([]); const [selectItems, setSelectItems] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
@@ -73,26 +76,7 @@ export default function CreateNotice() {
}, },
}); });
const noticeMutation = useMutation({ const handleAddNotice = async () => {
mutationFn: () =>
createNotice({
title: title,
clientId: 1,
description: description,
price: parseFloat(price),
category: category,
status: "ACTIVE",
image: image,
}),
onSuccess: () => {
console.log("Notice created successfully");
},
onError: (error) => {
console.error("Error creating notice. Erroe message: ", error.message);
},
});
const addNotice = () => {
setError({ setError({
title: !title, title: !title,
description: !description, description: !description,
@@ -104,13 +88,47 @@ export default function CreateNotice() {
console.log("Error in form"); console.log("Error in form");
return; return;
} }
noticeMutation.mutate();
setIsLoading(true);
try {
const result = await addNotice({
title: title,
clientId: 1,
description: description,
price: price,
category: category,
status: "ACTIVE",
image: image
});
if (result) {
console.log("Notice created successfully with ID: ", result.noticeId);
router.push("/(tabs)/notices");
}
} catch (error) {
console.error("Error creating notice. Error message: ", error.message);
} finally {
setIsLoading(false);
}
}; };
const takePicture = async () => {
const {status} = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') {
return;
}
const result = await ImagePicker.launchCameraAsync({
allowsEditing: false,
});
if (!result.canceled && result.assets) {
setImage(result.assets.map(asset => asset.uri));
}
}
const pickImage = async () => { const pickImage = async () => {
// No permissions request is necessary for launching the image library
let result = await ImagePicker.launchImageLibraryAsync({ let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'], mediaTypes: 'images',
selectionLimit: 8, selectionLimit: 8,
allowsEditing: false, allowsEditing: false,
allowsMultipleSelection: true, allowsMultipleSelection: true,
@@ -119,7 +137,6 @@ export default function CreateNotice() {
}); });
if (!result.canceled) { if (!result.canceled) {
// await uploadImage(1, result.assets[0].uri);
setImage(result.assets.map(asset => asset.uri)); setImage(result.assets.map(asset => asset.uri));
} }
}; };
@@ -135,10 +152,13 @@ export default function CreateNotice() {
Wybierz zdjęcia Wybierz zdjęcia
</ButtonText> </ButtonText>
</Button> </Button>
<Text size="sm" <Button onPress={takePicture}>
bold="true" <ButtonText>Zrób zdjęcie</ButtonText>
> </Button>
Pierwsze zdjęcie będzie zdjęciem głównym</Text> <Text size="sm"
bold="true"
>
Pierwsze zdjęcie będzie zdjęciem głównym</Text>
{image && image.length > 0 && ( {image && image.length > 0 && (
<VStack space="xs" className="flex-row flex-wrap"> <VStack space="xs" className="flex-row flex-wrap">
{image.map((img, index) => ( {image.map((img, index) => (
@@ -208,8 +228,8 @@ export default function CreateNotice() {
</VStack> </VStack>
<Button <Button
className="mt-5 w-full" className="mt-5 w-full"
onPress={() => addNotice()} onPress={handleAddNotice}
disabled={noticeMutation.isLoading} disabled={isLoading}
> >
<ButtonText className="text-typography-0">Dodaj</ButtonText> <ButtonText className="text-typography-0">Dodaj</ButtonText>
</Button> </Button>
@@ -217,4 +237,4 @@ export default function CreateNotice() {
</FormControl> </FormControl>
</ScrollView> </ScrollView>
); );
} }

View File

@@ -1,47 +1,65 @@
import { FlatList, Text, ActivityIndicator, RefreshControl } from "react-native"; import {FlatList, Text, ActivityIndicator, RefreshControl} from "react-native";
import { useState } from "react"; import {useState, useEffect} from "react";
import { listNotices } from "@/api/notices"; import {useNoticesStore} from "@/store/noticesStore";
import { useQuery } from "@tanstack/react-query"; import {NoticeCard} from "@/components/NoticeCard";
import { NoticeCard } from "@/components/NoticeCard";
export default function Notices() { export default function Notices() {
const [refreshing, setRefreshing] = useState(false); const {notices, fetchNotices} = useNoticesStore();
const { data, isLoading, error, refetch } = useQuery({ const [refreshing, setRefreshing] = useState(false);
queryKey: ["notices"], const [isLoading, setIsLoading] = useState(true);
queryFn: listNotices, const [error, setError] = useState(null);
});
if (isLoading) { useEffect(() => {
return <ActivityIndicator />; loadData();
} }, []);
if (error) { const loadData = async () => {
console.log(error.message); setIsLoading(true);
return <Text>Nie udało sie pobrać listy. {error.message}</Text>; try {
} await fetchNotices();
setError(null);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
};
const onRefresh = async () => { const onRefresh = async () => {
setRefreshing(true); setRefreshing(true);
await refetch(); try {
setRefreshing(false); await fetchNotices();
}; } catch (err) {
setError(err);
} finally {
setRefreshing(false);
}
};
return ( if (isLoading && !refreshing) {
<FlatList return <ActivityIndicator/>;
key={2} }
data={data}
numColumns={2} if (error) {
columnContainerClassName="m-2" return <Text>Nie udało sie pobrać listy. {error.message}</Text>;
columnWrapperClassName="gap-2 m-2" }
renderItem={({ item }) => <NoticeCard notice={item} />}
refreshControl={ return (
<RefreshControl <FlatList
refreshing={refreshing || isLoading} key={2}
onRefresh={onRefresh} data={notices}
colors={["#3b82f6"]} numColumns={2}
tintColor="#3b82f6" columnContainerClassName="m-2"
columnWrapperClassName="gap-2 m-2"
renderItem={({item}) => <NoticeCard notice={item}/>}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={["#3b82f6"]}
tintColor="#3b82f6"
/>
}
/> />
} );
/> }
);
}

View File

@@ -1,47 +1,72 @@
import {Stack, useLocalSearchParams} from "expo-router"; import {Stack, useLocalSearchParams} from "expo-router";
import {Box} from "@/components/ui/box"; import {Box} from "@/components/ui/box";
import {Button, ButtonText} from "@/components/ui/button";
import {Card} from "@/components/ui/card"; import {Card} from "@/components/ui/card";
import {Heading} from "@/components/ui/heading"; import {Heading} from "@/components/ui/heading";
import {Image} from "@/components/ui/image"; import {Image} from "@/components/ui/image";
import {Text} from "@/components/ui/text"; import {Text} from "@/components/ui/text";
import {VStack} from "@/components/ui/vstack"; import {VStack} from "@/components/ui/vstack";
import {Icon, FavouriteIcon} from "@/components/ui/icon"; import {Ionicons} from "@expo/vector-icons";
import {useQuery} from "@tanstack/react-query";
import {getImageByNoticeId, getNoticeById} from "@/api/notices";
import {ActivityIndicator} from "react-native"; import {ActivityIndicator} from "react-native";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {useNoticesStore} from "@/store/noticesStore";
import {useWishlist} from "@/store/wishlistStore";
import {Pressable} from "react-native";
export default function NoticeDetails() { export default function NoticeDetails() {
const {id} = useLocalSearchParams(); const {id} = useLocalSearchParams();
const [image, setImage] = useState(null); const [image, setImage] = useState(null);
const [isImageLoading, setIsImageLoading] = useState(true); const [isImageLoading, setIsImageLoading] = useState(true);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [notice, setNotice] = useState(null);
const { const {getNoticeById, getAllImagesByNoticeId} = useNoticesStore();
data: notice, const addNoticeToWishlist = useWishlist((state) => state.addNoticeToWishlist);
isLoading, const removeNoticeFromWishlist = useWishlist((state) => state.removeNoticeFromWishlist);
error, const isInWishlist = useWishlist((state) =>
} = useQuery({ notice ? state.wishlistNotices.some((item) => item.noticeId === notice.noticeId) : false
queryKey: ["notices", id], );
queryFn: () => getNoticeById(Number(id)),
}); useEffect(() => {
const fetchNotice = async () => {
setIsLoading(true);
try {
const noticeData = getNoticeById(Number(id));
if (noticeData) {
setNotice(noticeData);
setError(null);
} else {
setError(new Error(`Notice with ID ${id} not found.`));
}
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
};
fetchNotice();
}, [id]);
useEffect(() => { useEffect(() => {
const fetchImage = async () => { const fetchImage = async () => {
setIsImageLoading(true); setIsImageLoading(true);
if (notice) { if (notice) {
try { try {
const imageData = await getImageByNoticeId(notice.noticeId); const images = await getAllImagesByNoticeId(notice.noticeId);
setImage(imageData); setImage(images && images.length > 0 ? images[0] : "https://http.cat/404.jpg");
} catch (err) { } catch (err) {
console.error("Błąd przy pobieraniu obrazu:", err); console.error("Error while loading images:", err);
setImage("https://http.cat/404.jpg");
} finally { } finally {
setIsImageLoading(false); setIsImageLoading(false);
} }
} }
}; };
fetchImage(); if (notice) {
fetchImage();
}
}, [notice]); }, [notice]);
if (isLoading) { if (isLoading) {
@@ -49,11 +74,15 @@ export default function NoticeDetails() {
} }
if (error) { if (error) {
return <Text>Błąd, spróbuj ponownie póżniej</Text>; return <Text>Błąd, spróbuj ponownie póżniej: {error.message}</Text>;
}
if (!notice) {
return <Text>Nie znaleziono ogłoszenia</Text>;
} }
return ( return (
<Card className="p-0 rounded-lg m-3 flex-1"> <Card className="p-0 rounded-lg m-3 flex-1">
<Stack.Screen <Stack.Screen
options={{ options={{
title: notice.title, title: notice.title,
@@ -66,7 +95,7 @@ export default function NoticeDetails() {
) : ( ) : (
<Image <Image
source={{ source={{
uri: image || "https://http.cat/404.jpg", uri: image,
}} }}
className="h-auto w-full rounded-md aspect-[1/1]" className="h-auto w-full rounded-md aspect-[1/1]"
alt="image" alt="image"
@@ -82,11 +111,21 @@ export default function NoticeDetails() {
<Heading size="md" className="flex-1"> <Heading size="md" className="flex-1">
{notice.price} {notice.price}
</Heading> </Heading>
<Icon <Pressable
as={FavouriteIcon} onPress={() => {
size="sm" if (isInWishlist) {
className="text-primary-500 w-6 h-6" removeNoticeFromWishlist(notice.noticeId);
/> } else {
addNoticeToWishlist(notice);
}
}}
>
<Ionicons
name={isInWishlist ? "heart" : "heart-outline"}
size={24}
color={"primary-heading-500"}
/>
</Pressable>
</Box> </Box>
</VStack> </VStack>
</Card> </Card>

View File

@@ -5,43 +5,71 @@ import {Image} from "@/components/ui/image";
import {Text} from "@/components/ui/text"; import {Text} from "@/components/ui/text";
import {VStack} from "@/components/ui/vstack"; import {VStack} from "@/components/ui/vstack";
import {Link} from "expo-router"; import {Link} from "expo-router";
import {Pressable, ActivityIndicator} from "react-native"; import {Pressable, ActivityIndicator, View} from "react-native";
import {useWishlist} from "@/store/wishlistStore"; import {useWishlist} from "@/store/wishlistStore";
import {useNoticesStore} from "@/store/noticesStore";
import {Ionicons} from "@expo/vector-icons"; import {Ionicons} from "@expo/vector-icons";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {getImageByNoticeId} from "@/api/notices";
export function NoticeCard({notice}) { export function NoticeCard({notice}) {
const noticeId = notice?.noticeId;
const addNoticeToWishlist = useWishlist((state) => state.addNoticeToWishlist); const addNoticeToWishlist = useWishlist((state) => state.addNoticeToWishlist);
const removeNoticeFromWishlist = useWishlist( const removeNoticeFromWishlist = useWishlist((state) => state.removeNoticeFromWishlist);
(state) => state.removeNoticeFromWishlist
);
const isInWishlist = useWishlist((state) => const isInWishlist = useWishlist((state) =>
state.wishlistNotices.some((item) => item.noticeId === notice.noticeId) noticeId ? state.wishlistNotices.some((item) => item.noticeId === noticeId) : false
); );
const [image, setImage] = useState(null); const [image, setImage] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const {getAllImagesByNoticeId} = useNoticesStore();
useEffect(() => { useEffect(() => {
let isMounted = true;
const fetchImage = async () => { const fetchImage = async () => {
if (!noticeId) {
if (isMounted) {
setImage("https://http.cat/404.jpg");
setIsLoading(false);
}
return;
}
setIsLoading(true); setIsLoading(true);
try { try {
let imageUrl = await getImageByNoticeId(notice.noticeId); const images = await getAllImagesByNoticeId(noticeId);
setImage(imageUrl); if (isMounted) {
setImage(images && images.length > 0 ? images[0] : "https://http.cat/404.jpg");
}
} catch (error) { } catch (error) {
console.error("Błąd podczas pobierania obrazu:", error); console.error(`Error while loading image: ${error}`);
if (isMounted) {
setImage("https://http.cat/404.jpg");
}
} finally { } finally {
setIsLoading(false); if (isMounted) {
setIsLoading(false);
}
} }
}; };
fetchImage(); fetchImage();
}, [notice.noticeId]);
return () => {
isMounted = false;
};
}, [noticeId]);
if (!notice) {
return <View style={{flex: 1}} />;
}
return ( return (
<Link href={`/notice/${notice.noticeId}`} asChild> <Link href={`/notice/${noticeId}`} asChild>
<Pressable className="flex-1"> <Pressable className="flex-1">
<Card className="p-0 rounded-lg max-w-[460px] flex-1"> <Card className="p-0 rounded-lg max-w-[460px] flex-1">
{isLoading ? ( {isLoading ? (
<Box className="h-auto w-full rounded-md aspect-[1/1] bg-gray-100 items-center justify-center"> <Box className="h-auto w-full rounded-md aspect-[1/1] bg-gray-100 items-center justify-center">
<ActivityIndicator size="large" color="#3b82f6" /> <ActivityIndicator size="large" color="#3b82f6" />
@@ -49,7 +77,7 @@ export function NoticeCard({notice}) {
) : ( ) : (
<Image <Image
source={{ source={{
uri: image || "https://http.cat/404.jpg", uri: image,
}} }}
className="h-auto w-full rounded-md aspect-[1/1]" className="h-auto w-full rounded-md aspect-[1/1]"
alt="image" alt="image"
@@ -67,16 +95,16 @@ export function NoticeCard({notice}) {
<Pressable <Pressable
onPress={() => { onPress={() => {
if (isInWishlist) { if (isInWishlist) {
removeNoticeFromWishlist(notice.noticeId); // Usuń z ulubionych removeNoticeFromWishlist(noticeId);
} else { } else {
addNoticeToWishlist(notice); // Dodaj do ulubionych addNoticeToWishlist(notice);
} }
}} }}
> >
<Ionicons <Ionicons
name={isInWishlist ? "heart" : "heart-outline"} // Dynamiczna ikona name={isInWishlist ? "heart" : "heart-outline"}
size={24} // Rozmiar ikony size={24}
color={"primary-heading-500"} // Kolor ikony color={"primary-heading-500"}
/> />
</Pressable> </Pressable>
</Box> </Box>

File diff suppressed because it is too large Load Diff

View File

@@ -50,7 +50,8 @@
"react-native-svg": "15.11.2", "react-native-svg": "15.11.2",
"react-native-web": "~0.20.0", "react-native-web": "~0.20.0",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"zustand": "^5.0.3" "zustand": "^5.0.3",
"expo-camera": "~16.1.6"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.20.0", "@babel/core": "^7.20.0",

View File

@@ -0,0 +1,41 @@
import {create} from "zustand";
import * as api from "@/api/notices";
export const useNoticesStore = create((set, get) => ({
notices: [],
fetchNotices: async () => {
set({error: null});
try {
const data = await api.listNotices();
set({notices: data});
} catch (error) {
set(error);
}
},
addNotice: async (notice) => {
try {
const newNotice = await api.createNotice(notice);
set((state) => ({
notices: [...state.notices, newNotice],
}));
return newNotice;
} catch (error) {
set({ error });
return null;
}
},
getNoticeById: (noticeId) => {
return get().notices.find((notice) => String(notice.noticeId) === String(noticeId));
},
getAllImagesByNoticeId: async (noticeId) => {
try {
return await api.getAllImagesByNoticeId(noticeId);
} catch (error) {
console.error("Error while getting images:", error);
return ["https://http.cat/404.jpg"];
}
}
}));

View File

@@ -9,7 +9,7 @@ export const useWishlist = create((set) => ({
removeNoticeFromWishlist: (noticeId) => removeNoticeFromWishlist: (noticeId) =>
set((state) => ({ set((state) => ({
wishlistNotices: state.wishlistNotices.filter( wishlistNotices: state.wishlistNotices.filter(
(item) => item.noticeId != noticeId (item) => item.noticeId !== noticeId
), ),
})), })),
})); }));