diff --git a/ArtisanConnect/api/categories.jsx b/ArtisanConnect/api/categories.jsx index ae3d001..ed76375 100644 --- a/ArtisanConnect/api/categories.jsx +++ b/ArtisanConnect/api/categories.jsx @@ -1,12 +1,16 @@ import axios from "axios"; +import { useAuthStore } from "@/store/authStore"; -const API_URL = "https://testowe.zikor.pl/api/v1"; +const API_URL = "https://hopp.zikor.pl/api/v1"; export async function listCategories() { - try { - const response = await axios.get(`${API_URL}/vars/categories`); - return response.data; - } catch (err) { - console.error("Nie udało się pobrać listy kategorii.", err.response.status); - } -} \ No newline at end of file + const { token } = useAuthStore.getState(); + const headers = token ? { Authorization: `Bearer ${token}` } : {}; + + try { + const response = await axios.get(`${API_URL}/vars/categories`, { headers }); + return response.data; + } catch (err) { + console.error("Nie udało się pobrać listy kategorii.", err.response.status); + } +} diff --git a/ArtisanConnect/api/client.jsx b/ArtisanConnect/api/client.jsx new file mode 100644 index 0000000..6268c0e --- /dev/null +++ b/ArtisanConnect/api/client.jsx @@ -0,0 +1,16 @@ +import axios from "axios"; + +const API_URL = "https://hopp.zikor.pl/api/v1"; + +export async function getUserById(userId) { + try { + const response = await axios.get(`${API_URL}/clients/get/${userId}`); + return response.data; + } catch (err) { + console.error( + `Nie udało się pobrać danych użytkownika o ID ${userId}.`, + err.response.status + ); + throw err; + } +} diff --git a/ArtisanConnect/api/notices.jsx b/ArtisanConnect/api/notices.jsx index 6b786f7..aa53d8b 100644 --- a/ArtisanConnect/api/notices.jsx +++ b/ArtisanConnect/api/notices.jsx @@ -1,129 +1,129 @@ import axios from "axios"; -import FormData from 'form-data' -import {useAuthStore} from "@/store/authStore"; +import FormData from "form-data"; +import { useAuthStore } from "@/store/authStore"; const API_URL = "https://testowe.zikor.pl/api/v1"; -// const API_URL = "http://10.0.2.2:8080/api/v1"; +const API_URL = "https://hopp.zikor.pl/api/v1"; export async function listNotices() { - const { token } = useAuthStore.getState(); - const headers = token ? { 'Authorization': `Bearer ${token}` } : {}; + const { token } = useAuthStore.getState(); + const headers = token ? { Authorization: `Bearer ${token}` } : {}; - console.log(token); - - const response = await fetch(`${API_URL}/notices/get/all`, { - headers: headers - }); - console.log(response); - const data = await response.json(); - if (!response.ok) { - throw new Error(response.toString()); - } - return data; + const response = await fetch(`${API_URL}/notices/get/all`, { + headers: headers, + }); + const data = await response.json(); + if (!response.ok) { + throw new Error(response.toString()); + } + return data; } export async function getNoticeById(noticeId) { - const response = await fetch(`${API_URL}/notices/get/${noticeId}`); + const response = await fetch(`${API_URL}/notices/get/${noticeId}`); - const data = await response.json(); - if (!response.ok) { - throw new Error("Error"); - } - return data; + const data = await response.json(); + if (!response.ok) { + throw new Error("Error"); + } + return data; } export async function createNotice(notice) { - try { - const response = await axios.post(`${API_URL}/notices/add`, notice, { - headers: { - "Content-Type": "application/json", - }, - }); + try { + const response = await axios.post(`${API_URL}/notices/add`, notice, { + headers: { + "Content-Type": "application/json", + }, + }); - if (response.data.noticeId !== null) { - for (const imageUri of notice.image) { - await uploadImage(response.data.noticeId, imageUri); - } - } - - return response.data; - } catch (error) { - console.log("Error", error.response.data, error.response.status); - return null; + if (response.data.noticeId !== null) { + for (const imageUri of notice.image) { + await uploadImage(response.data.noticeId, imageUri); + } } + + return response.data; + } catch (error) { + console.log("Error", error.response.data, error.response.status); + return null; + } } export async function getImageByNoticeId(noticeId) { - let imageUrl; - try { - const listResponse = await axios.get(`${API_URL}/images/list/${noticeId}`); + let imageUrl; + try { + const listResponse = await axios.get(`${API_URL}/images/list/${noticeId}`); - const imageName = listResponse.data[0]; - imageUrl = `${API_URL}/images/get/${imageName}`; + const imageName = listResponse.data[0]; + imageUrl = `${API_URL}/images/get/${imageName}`; - console.log(`Pobrano zdjęcie o nazwie: ${imageName}`); - - return imageUrl; - } catch (err) { - console.log(`Zdjęcie nie istnieje dla notice o id: ${noticeId}`); - imageUrl = "https://http.cat/404.jpg"; - return imageUrl; - } + return imageUrl; + } catch (err) { + console.log(`Zdjęcie nie istnieje dla notice o id: ${noticeId}`); + imageUrl = "https://http.cat/404.jpg"; + return imageUrl; + } } export async function getAllImagesByNoticeId(noticeId) { - try { - const listResponse = await axios.get(`${API_URL}/images/list/${noticeId}`); + try { + const listResponse = await axios.get(`${API_URL}/images/list/${noticeId}`); - if (listResponse.data && listResponse.data.length > 0) { - const imageUrls = listResponse.data.map(imageName => - `${API_URL}/images/get/${imageName}` - ); - - // console.log(`Pobrano ${imageUrls.length} zdjęć dla ogłoszenia o id: ${noticeId}`); - return imageUrls; - } - - // console.log(`Brak zdjęć dla ogłoszenia o id: ${noticeId}`); - return ["https://http.cat/404.jpg"]; - } catch (err) { - // console.log(`Błąd podczas pobierania listy zdjęć dla ogłoszenia o id: ${noticeId}`, err); - return ["https://http.cat/404.jpg"]; + if (listResponse.data && listResponse.data.length > 0) { + return listResponse.data.map( + (imageName) => `${API_URL}/images/get/${imageName}` + ); } + + return ["https://http.cat/404.jpg"]; + } catch (err) { + if (err.response.status === 404) { + // console.info(`Ogłoszenie o id: ${noticeId} nie posiada zdjęć.`); + return ["https://http.cat/404.jpg"]; + } + console.warn( + `Nie udało się pobrać listy zdjęć dla ogłoszenia o id: ${noticeId}`, + err + ); + return ["https://http.cat/404.jpg"]; + } } export const uploadImage = async (noticeId, imageUri) => { - console.log(imageUri); + const formData = new FormData(); - const formData = new FormData(); + const filename = imageUri.split("/").pop(); - const filename = imageUri.split('/').pop(); + const match = /\.(\w+)$/.exec(filename); + const type = match ? `image/${match[1]}` : "image/jpeg"; - const match = /\.(\w+)$/.exec(filename); - const type = match ? `image/${match[1]}` : 'image/jpeg'; + formData.append("file", { + uri: imageUri, + name: filename, + type: type, + }); - formData.append('file', { - uri: imageUri, - name: filename, - type: type, - }); - - try { - const response = await axios.post( - `${API_URL}/images/upload/${noticeId}`, - formData, - { - headers: { - 'Content-Type': 'multipart/form-data', - }, - } - ); - console.info('Upload successful:', response.data); - return response.data; - } catch (error) { - console.log("imageURI:", imageUri); - console.error('Error uploading image:', error.response.data, error.response.status); - throw error; - } -} \ No newline at end of file + try { + const response = await axios.post( + `${API_URL}/images/upload/${noticeId}`, + formData, + { + headers: { + "Content-Type": "multipart/form-data", + }, + } + ); + console.info("Upload successful:", response.data); + return response.data; + } catch (error) { + console.log("imageURI:", imageUri); + console.error( + "Error uploading image:", + error.response.data, + error.response.status + ); + throw error; + } +}; diff --git a/ArtisanConnect/api/wishlist.jsx b/ArtisanConnect/api/wishlist.jsx index 9e5996f..265fc71 100644 --- a/ArtisanConnect/api/wishlist.jsx +++ b/ArtisanConnect/api/wishlist.jsx @@ -1,29 +1,38 @@ import axios from "axios"; +import { useAuthStore } from "@/store/authStore"; // import FormData from 'form-data' -const API_URL = "https://testowe.zikor.pl/api/v1/wishlist"; +const API_URL = "https://hopp.zikor.pl/api/v1/wishlist"; export async function toggleNoticeStatus(noticeId) { - try { - const response = await axios.post(`${API_URL}/toggle/${noticeId}`, null, { - headers: { - "Content-Type": "application/json", - }, - }); - return response.data; - } catch (error) { - console.error("Error toggling wishlist item:", error); - throw error; - } + const { token } = useAuthStore.getState(); + const headers = token ? { Authorization: `Bearer ${token}` } : {}; + + try { + const response = await axios.post( + `${API_URL}/toggle/${noticeId}`, + {}, + { + headers, + } + ); + return response.data; + } catch (error) { + console.error("Error toggling wishlist item:", error); + throw error; + } } export async function getWishlist() { - try { - const response = await axios.get(`${API_URL}/`); - console.log("Wishlist response:", response.data); - return response.data; - } catch (error) { - console.error("Error fetching wishlist:", error); - throw error; - } + const { token } = useAuthStore.getState(); + const headers = token ? { Authorization: `Bearer ${token}` } : {}; + + try { + const response = await axios.get(`${API_URL}/`, { headers }); + console.log("Wishlist response:", response.data); + return response.data; + } catch (error) { + console.error("Error fetching wishlist:", error); + throw error; + } } diff --git a/ArtisanConnect/app.json b/ArtisanConnect/app.json index 71e8089..bbe332e 100644 --- a/ArtisanConnect/app.json +++ b/ArtisanConnect/app.json @@ -2,7 +2,7 @@ "expo": { "name": "ArtisanConnect", "slug": "ArtisanConnect", - "scheme": "Artisanconnect", + "scheme": "com.hamx.artisanconnect", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", @@ -14,13 +14,19 @@ "backgroundColor": "#ffffff" }, "ios": { - "supportsTablet": true + "supportsTablet": true, + "bundleIdentifier": "com.hamx.artisanconnect" }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" - } + }, + "permissions": [ + "android.permission.RECORD_AUDIO", + "android.permission.CAMERA" + ], + "package": "com.hamx.artisanconnect" }, "web": { "favicon": "./assets/favicon.png" @@ -40,6 +46,12 @@ } ], "expo-web-browser" - ] + ], + "extra": { + "router": {}, + "eas": { + "projectId": "7a0d8bc8-938f-4d2a-babb-945faee13429" + } + } } } diff --git a/ArtisanConnect/app/(auth)/login.jsx b/ArtisanConnect/app/(auth)/login.jsx index a494ec1..ec33970 100644 --- a/ArtisanConnect/app/(auth)/login.jsx +++ b/ArtisanConnect/app/(auth)/login.jsx @@ -1,5 +1,5 @@ -import React, {useState} from 'react'; -import {StyleSheet, ActivityIndicator, SafeAreaView, View} from 'react-native'; +import React, {useEffect, useState} from 'react'; +import {StyleSheet, ActivityIndicator, SafeAreaView, View, Platform} from 'react-native'; import {useAuthStore} from '@/store/authStore'; import {useRouter, Link} from 'expo-router'; @@ -15,12 +15,37 @@ import {ArrowRightIcon} from "@/components/ui/icon" import {Divider} from '@/components/ui/divider'; import {Ionicons} from "@expo/vector-icons"; +import * as WebBrowser from 'expo-web-browser'; +import * as Google from "expo-auth-session/providers/google"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import {makeRedirectUri} from "expo-auth-session"; + +import Constants from 'expo-constants'; + +WebBrowser.maybeCompleteAuthSession(); + +// client_id ios 936418008320-ohefdfcebd41f6oa2o8phh1mgj9s49sl.apps.googleusercontent.com +// android 936418008320-d8dfjph5e4r28fcm1rbdfbh5phmbg03d.apps.googleusercontent.com + export default function Login() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); - const {signIn, isLoading} = useAuthStore(); + const {signIn, isLoading, signInWithGoogle} = useAuthStore(); const router = useRouter(); + const [request, response, promptAsync] = Google.useAuthRequest({ + androidClientId: "936418008320-d8dfjph5e4r28fcm1rbdfbh5phmbg03d.apps.googleusercontent.com", + iosClientId: "936418008320-ohefdfcebd41f6oa2o8phh1mgj9s49sl.apps.googleusercontent.com", + webClientId: "936418008320-btdngtlfnjac1p67guje72m9el5q59a7.apps.googleusercontent.com", + redirectUri: + Platform.OS === 'android' + ? makeRedirectUri({ + scheme: Constants.expoConfig.android.package, + path: '/', + }) + : undefined, + }) + const handleInternalLogin = async () => { if (!email || !password) { alert('Proszę wprowadzić email i hasło.'); @@ -36,6 +61,47 @@ export default function Login() { } } + useEffect(() => { + handleGoogleLogin(); + }, [response]); + + const handleGoogleLogin = async () => { + // const user = await AsyncStorage.getItem("@user"); + let user = null; + if (!user) { + if(response.type === "success") { + user = await getUserInfo(response.authentication.accessToken) + await signInWithGoogle(response.authentication.accessToken); + alert(`Zalogowano jako ${user.email}`); + } + + } else { + console.info("Pobrano użytkownika z AsyncStorage:", JSON.parse(user)); + alert(`Zalogowano jako ${user.email}`); + } + }; + + const getUserInfo = async (token) => { + if(!token) { + return + } + try { + const response = await fetch("https://www.googleapis.com/userinfo/v2/me", + { + headers: { + Authorization: `Bearer ${token}` + }, + } + ); + const user = await response.json(); + await AsyncStorage.setItem("@user", JSON.stringify(user)); + return user; + } catch (error) { + console.error("Błąd podczas pobierania informacji o użytkowniku:", error); + throw error; + } + } + if (isLoading) { return ( @@ -82,7 +148,7 @@ export default function Login() { - diff --git a/ArtisanConnect/app/(tabs)/_layout.jsx b/ArtisanConnect/app/(tabs)/_layout.jsx index 7f9d212..4a42cf7 100644 --- a/ArtisanConnect/app/(tabs)/_layout.jsx +++ b/ArtisanConnect/app/(tabs)/_layout.jsx @@ -1,64 +1,70 @@ -import {Tabs} from "expo-router"; -import {Ionicons} from "@expo/vector-icons"; +import { Tabs, Redirect } from "expo-router"; +import { Ionicons } from "@expo/vector-icons"; +import { useAuthStore } from "@/store/authStore"; export default function TabLayout() { - return ( - - ( - - ), - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - - ); + const token = useAuthStore((state) => state.token); + + if (!token) { + return ; + } + return ( + + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + + ); } diff --git a/ArtisanConnect/app/(tabs)/dashboard/_layout.jsx b/ArtisanConnect/app/(tabs)/dashboard/_layout.jsx index df44641..980668e 100644 --- a/ArtisanConnect/app/(tabs)/dashboard/_layout.jsx +++ b/ArtisanConnect/app/(tabs)/dashboard/_layout.jsx @@ -1,6 +1,15 @@ +import { DrawerItem } from "@react-navigation/drawer"; import { Drawer } from "expo-router/drawer"; +import { useAuthStore } from "@/store/authStore"; + +import { + DrawerContentScrollView, + DrawerItemList, +} from "@react-navigation/drawer"; export default function AccountDrawerLayout() { + const signOut = useAuthStore((state) => state.signOut); + return ( ( + + + + + )} > Użytkownik; +import { VStack } from "@/components/ui/vstack"; +import { Image } from "@/components/ui/image"; +import { ActivityIndicator } from "react-native"; +import { useEffect, useState } from "react"; +import { getUserById } from "@/api/client"; +import { HStack } from "@gluestack-ui/themed"; + +export default function Account() { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const currentUserId = 2; // Tymczasowo, do czasu zaimplementowania logowania bo nie moge pobrac usera + + useEffect(() => { + const fetchUser = async () => { + setIsLoading(true); + try { + const userData = await getUserById(currentUserId); + setUser(userData); + } catch (err) { + console.error("Błąd podczas pobierania danych użytkownika:", err); + } finally { + setIsLoading(false); + } + }; + fetchUser(); + }, []); + + if (isLoading) { + return ; + } + + if (!user) { + return Nie udało się pobrać danych użytkownika.; + } + + return ( + + + + Zdjęcie profilowe + + + + {user.firstName} {user.lastName} + + + + + Moje dane + + + E-mail + {user.email || "brak danych"} + + + console.log("Edytuj dane użytkownika")} + > + Edytuj profil + + + + + Moje konto + + + + Moje ogłoszenia + + + + + {/*Tak dodałem, można zmienić na coś innego*/} + + + Historia płatności + + + + + {/* + Ustawienia powiadomień + + */} + + + {/* + Wyloguj się + */} + + ); } diff --git a/ArtisanConnect/app/(tabs)/dashboard/userNotices.jsx b/ArtisanConnect/app/(tabs)/dashboard/userNotices.jsx index 9f5ef2f..7fe0f2b 100644 --- a/ArtisanConnect/app/(tabs)/dashboard/userNotices.jsx +++ b/ArtisanConnect/app/(tabs)/dashboard/userNotices.jsx @@ -1,4 +1,75 @@ -import { Text } from "@/components/ui/text"; +import { useNoticesStore } from "@/store/noticesStore"; +import { NoticeCard } from "@/components/NoticeCard"; +import {Button} from "react-native"; +import {Box} from "@/components/ui/box"; +import {Text} from "@/components/ui/text"; +import {VStack} from "@/components/ui/vstack"; +import {ActivityIndicator, FlatList } from "react-native"; +import {useEffect, useState} from "react"; + export default function UserNotices() { - return Użytkownik; -} + const { notices, fetchNotices } = useNoticesStore(); + const [isLoading, setIsLoading] = useState(true); + const currentUserId = 1; // Tymczasowo, do czasu zaimplementowania logowania bo nie moge pobrac usera + + useEffect(() => { + const loadNotices = async () => { + setIsLoading(true); + try { + await fetchNotices(); + } catch (err) { + console.error("Błąd podczas pobierania ogłoszeń:", err); + } finally { + setIsLoading(false); + } + }; + loadNotices(); + }, []); + + const userNotices = notices.filter(notice => notice.clientId === currentUserId); + + if (isLoading) { + return ; + } + + return ( + + Moje ogłoszenia + {userNotices.length > 0 ? ( + ( + + + + + + + + )} + keyExtractor={(item) => item.noticeId.toString()} + /> + ) : ( + Nie masz żadnych ogłoszeń. + )} + + ); +} \ No newline at end of file diff --git a/ArtisanConnect/app/(tabs)/index.jsx b/ArtisanConnect/app/(tabs)/index.jsx index f8d6111..674865e 100644 --- a/ArtisanConnect/app/(tabs)/index.jsx +++ b/ArtisanConnect/app/(tabs)/index.jsx @@ -1,17 +1,20 @@ import { ScrollView, View } from "react-native"; -import { useNoticesStore } from '@/store/noticesStore'; +import { useNoticesStore } from "@/store/noticesStore"; import { CategorySection } from "@/components/CategorySection"; import { NoticeSection } from "@/components/NoticeSection"; import { UserSection } from "@/components/UserSection"; import { SearchSection } from "@/components/SearchSection"; -import { FlatList } from 'react-native'; +import { FlatList } from "react-native"; import { useAuthStore } from "@/store/authStore"; import { useRouter } from "expo-router"; import { useEffect, useState } from "react"; +import { SafeAreaView } from "react-native"; + export default function Home() { -const token = useAuthStore((state) => state.token); + const token = useAuthStore((state) => state.token); const router = useRouter(); const [isReady, setIsReady] = useState(false); + const fetchNotices = useNoticesStore((state) => state.fetchNotices); useEffect(() => { setIsReady(true); @@ -23,23 +26,41 @@ const token = useAuthStore((state) => state.token); } }, [isReady, token, router]); + useEffect(() => { + if (token) { + fetchNotices(); + } + }, [token, fetchNotices]); + const notices = useNoticesStore((state) => state.notices); - const latestNotices = [...notices] + // console.log("Notices:", notices); + + const latestNotices = [...notices] .sort((a, b) => new Date(b.publishDate) - new Date(a.publishDate)) .slice(0, 6); - const recomendedNotices = [...notices] + const recomendedNotices = [...notices] .sort(() => Math.random() - 0.5) .slice(0, 6); return ( - - - + + {/* */} + + - + - - - + + + {/* */} + ); } diff --git a/ArtisanConnect/app/(tabs)/wishlist.jsx b/ArtisanConnect/app/(tabs)/wishlist.jsx index d6595c8..60c8f5d 100644 --- a/ArtisanConnect/app/(tabs)/wishlist.jsx +++ b/ArtisanConnect/app/(tabs)/wishlist.jsx @@ -7,12 +7,14 @@ import { Text } from "@/components/ui/text"; import { useEffect } from "react"; export default function Wishlist() { - const wishlistNotices = useWishlist((state) => state.wishlistNotices); + const wishlistNotices = useWishlist((state) => state.wishlistNotices); const fetchWishlist = useWishlist((state) => state.fetchWishlist); useEffect(() => { fetchWishlist(); }, []); + + // console.log("Wishlist notices:", wishlistNotices); if (wishlistNotices.length === 0) { return ( diff --git a/ArtisanConnect/app/_layout.jsx b/ArtisanConnect/app/_layout.jsx index 4569df8..599aab1 100644 --- a/ArtisanConnect/app/_layout.jsx +++ b/ArtisanConnect/app/_layout.jsx @@ -2,21 +2,13 @@ import { Stack, Redirect } from "expo-router"; import "@/global.css"; import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { useEffect, useState } from "react"; -import { useNoticesStore } from "@/store/noticesStore"; const queryClient = new QueryClient(); export default function RootLayout() { - const fetchNotices = useNoticesStore((state) => state.fetchNotices); - - useEffect(() => { - fetchNotices(); - }, []); return ( - state.addNoticeToWishlist); - const removeNoticeFromWishlist = useWishlist((state) => state.removeNoticeFromWishlist); - const isInWishlist = useWishlist((state) => - notice ? state.wishlistNotices.some((item) => item.noticeId === notice.noticeId) : false - ); + const [isMessageFormVisible, setIsMessageFormVisible] = useState(false); + const [message, setMessage] = useState(""); + const [Email, setEmail] = useState(""); + const handleSendMessage = () => { + console.log("Wiadomość do:", user?.email); + console.log("Email nadawcy:", Email); + console.log("Treść:", message); - 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); - } - }; + setIsMessageFormVisible(false); + setMessage(""); + setEmail(""); + }; - fetchNotice(); - }, [id]); + const formatDate = (dateString) => { + const date = new Date(dateString); + return date.toLocaleDateString("pl-PL", { + year: "numeric", + month: "2-digit", + day: "2-digit", + }); + }; - useEffect(() => { - const fetchImage = async () => { - setIsImageLoading(true); - if (notice) { - try { - const images = await getAllImagesByNoticeId(notice.noticeId); - setImage(images && images.length > 0 ? images[0] : "https://http.cat/404.jpg"); - } catch (err) { - console.error("Error while loading images:", err); - setImage("https://http.cat/404.jpg"); - } finally { - setIsImageLoading(false); - } - } - }; + const { getNoticeById, getAllImagesByNoticeId } = useNoticesStore(); + const toggleNoticeInWishlist = useWishlist( + (state) => state.toggleNoticeInWishlist + ); - if (notice) { - fetchImage(); + const isInWishlist = useWishlist((state) => + id ? state.wishlistNotices.some((item) => item.noticeId == id) : false + ); + const onViewableItemsChanged = useRef(({ viewableItems }) => { + if (viewableItems.length > 0) { + setCurrentIndex(viewableItems[0].index); + } + }).current; + + const viewabilityConfig = useRef({ + itemVisiblePercentThreshold: 70, + }).current; + + 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.`)); } - }, [notice]); + } catch (err) { + setError(err); + } finally { + setIsLoading(false); + } + }; - if (isLoading) { - return ; + fetchNotice(); + }, [id]); + + useEffect(() => { + const fetchImage = async () => { + setIsImageLoading(true); + if (notice) { + try { + const fetchedImages = await getAllImagesByNoticeId(notice.noticeId); + setImages( + fetchedImages && fetchedImages.length > 0 + ? fetchedImages + : ["https://http.cat/404.jpg"] + ); + } catch (err) { + console.error("Error while loading images:", err); + setImage("https://http.cat/404.jpg"); + } finally { + setIsImageLoading(false); + } + } + }; + + if (notice) { + fetchImage(); } + }, [notice]); - if (error) { - return Błąd, spróbuj ponownie póżniej: {error.message}; - } + useEffect(() => { + const fetchUser = async () => { + if (notice && notice.clientId) { + setIsUserLoading(true); + try { + const userData = await getUserById(notice.clientId); + setUser(userData); + } catch (err) { + console.error("Nie udało się pobrać danych użytkownika:", err); + } finally { + setIsUserLoading(false); + } + } + }; - if (!notice) { - return Nie znaleziono ogłoszenia; - } + fetchUser(); + }, [notice]); - return ( - - - {isImageLoading ? ( - - - - ) : ( + if (isLoading) { + return ; + } + + if (error) { + return Błąd, spróbuj ponownie póżniej: {error.message}; + } + + if (!notice) { + return Nie znaleziono ogłoszenia; + } + + return ( + + + {isImageLoading ? ( + + + + ) : ( + + ( + image + )} + keyExtractor={(item, index) => index.toString()} + /> - - - {notice.title} - - - - {notice.price}zł - - { - if (isInWishlist) { - removeNoticeFromWishlist(notice.noticeId); - } else { - addNoticeToWishlist(notice); - } - }} - > - - + {images.length > 1 && ( + + {images.map((_, index) => ( + + ))} + + )} + + )} + + + + + {formatDate(notice.publishDate)} + + + {notice.title} + + + + + Cena: + {notice.price} zł + + + { + toggleNoticeInWishlist(id); + }} + > + + + + + + Kategoria:{" "} + {notice.category} + + + + Opis ogloszenia + + {notice.description} + + + + + Uzytkownik: + {isUserLoading ? ( + + ) : user ? ( + <> + + Zdjęcie profilowe - - - ); -} \ No newline at end of file + + + + {user.firstName} {user.lastName} + + + Email: {user.email} + + setIsMessageFormVisible(true)} + className="mt-3 bg-primary-500 py-2 px-4 rounded-md" + > + + Wyślij wiadomość + + + + + Zobacz więcej ogłoszeń od {user.firstName} + + + + + ) : ( + Błąd podczas ładowania danych użytkownika + )} + + + + {isMessageFormVisible && ( + + + + Wyślij wiadomość do {user?.firstName} + + + Do: + + {user?.email || "Brak adresu e-mail"} + + Twój e-mail: + + + + + + setIsMessageFormVisible(false)} + className="bg-gray-300 py-2 px-4 rounded-md" + > + Anuluj + + + + Wyślij + + + + + )} + + ); +} diff --git a/ArtisanConnect/app/user/[userId].jsx b/ArtisanConnect/app/user/[userId].jsx new file mode 100644 index 0000000..0f85231 --- /dev/null +++ b/ArtisanConnect/app/user/[userId].jsx @@ -0,0 +1,69 @@ +import { useLocalSearchParams } from "expo-router"; +import { useState, useEffect } from "react"; +import { FlatList, ActivityIndicator, Text } from "react-native"; +import { Box } from "@/components/ui/box"; +import { Image } from "@/components/ui/image"; +import { VStack } from "@/components/ui/vstack"; +import { Heading } from "@/components/ui/heading"; +import { getUserById } from "@/api/client"; +import { useNoticesStore } from "@/store/noticesStore"; +import { NoticeCard } from "@/components/NoticeCard"; + +export default function UserProfile() { + const { userId } = useLocalSearchParams(); + const [user, setUser] = useState(null); + const [isUserLoading, setIsUserLoading] = useState(true); + const { notices } = useNoticesStore(); + + useEffect(() => { + const fetchUser = async () => { + setIsUserLoading(true); + try { + const userData = await getUserById(Number(userId)); + setUser(userData); + } catch (err) { + console.error("Błąd podczas pobierania danych użytkownika:", err); + setUser(null); + } finally { + setIsUserLoading(false); + } + }; + fetchUser(); + }, [userId]); + + if (isUserLoading) { + return ; + } + + if (!user) { + return Nie znaleziono użytkownika; + } + + const userNotices = notices.filter(notice => notice.clientId === Number(userId)); + + return ( + + + Zdjęcie profilowe + + {user.firstName} {user.lastName} + + + {userNotices.length > 0 ? ( + } + keyExtractor={(item) => item.noticeId.toString()} + /> + ) : ( + Ten użytkownik nie ma żadnych ogłoszeń. + )} + + ); +} \ No newline at end of file diff --git a/ArtisanConnect/components/CategorySection.jsx b/ArtisanConnect/components/CategorySection.jsx index ed6de5f..2d9321e 100644 --- a/ArtisanConnect/components/CategorySection.jsx +++ b/ArtisanConnect/components/CategorySection.jsx @@ -1,31 +1,38 @@ -import { View, FlatList} from 'react-native'; -import { useEffect, useState } from 'react' -import { Heading } from '@/components/ui/heading'; -import { Text } from '@/components/ui/text'; -import { Link } from 'expo-router'; -import { Pressable } from '@/components/ui/pressable'; -import axios from 'axios'; - -export function CategorySection({notices, title}) { -// const notices = useNoticesStore((state) => state.notices); +import { View, FlatList } from "react-native"; +import { useEffect, useState } from "react"; +import { useAuthStore } from "@/store/authStore"; +import { Heading } from "@/components/ui/heading"; +import { Text } from "@/components/ui/text"; +import { Link } from "expo-router"; +import { Pressable } from "@/components/ui/pressable"; +import { listCategories } from "@/api/categories"; +export function CategorySection({ notices, title }) { const [categoryMap, setCategoryMap] = useState({}); useEffect(() => { - axios.get('https://testowe.zikor.pl/api/v1/vars/categories') - .then(res => setCategoryMap(res.data)) - .catch(() => setCategoryMap({})); - }, []); - -const categories = Array.from( - new Set(notices.map((notice) => notice.category)) -).filter(Boolean); + const fetchCategories = async () => { + let data = await listCategories(); + if (Array.isArray(data)) { + setCategoryMap(data); + } + }; + fetchCategories(); + }); + + const categories = Array.from( + new Set(notices.map((notice) => notice.category)) + ).filter(Boolean); const getCount = (category) => notices.filter((notice) => notice.category === category).length; -return ( + if (!categoryMap || Object.keys(categoryMap).length === 0) { + return null; + } + + return ( {title} cat.value === item); return ( - - - {categoryObj ? categoryObj.label : item} ({getCount(item)}) - - + + + {categoryObj ? categoryObj.label : item} ({getCount(item)}) + + ); }} /> ); - -} \ No newline at end of file +} diff --git a/ArtisanConnect/components/SearchSection.jsx b/ArtisanConnect/components/SearchSection.jsx index c0aa15e..97d0077 100644 --- a/ArtisanConnect/components/SearchSection.jsx +++ b/ArtisanConnect/components/SearchSection.jsx @@ -2,6 +2,7 @@ import { Input, InputField, InputIcon, InputSlot } from "@/components/ui/input" import { SearchIcon } from "@/components/ui/icon" import { Box } from "@/components/ui/box" import { useRouter } from "expo-router"; +import { View } from "react-native"; export function SearchSection({ searchQuery, setSearchQuery }) { const router = useRouter(); @@ -14,18 +15,17 @@ export function SearchSection({ searchQuery, setSearchQuery }) { }); }; return ( - - + + - - ) } \ No newline at end of file diff --git a/ArtisanConnect/components/UserSection.jsx b/ArtisanConnect/components/UserSection.jsx index db42d59..7cd6c61 100644 --- a/ArtisanConnect/components/UserSection.jsx +++ b/ArtisanConnect/components/UserSection.jsx @@ -1,22 +1,27 @@ -import { View} from 'react-native'; -import { useEffect, useState } from 'react' -import { Heading } from '@/components/ui/heading'; -import { FlatList } from 'react-native'; -import axios from 'axios'; -import UserBlock from '@/components/UserBlock'; +import { View } from "react-native"; +import { useEffect, useState } from "react"; +import { Heading } from "@/components/ui/heading"; +import { FlatList } from "react-native"; +import axios from "axios"; +import UserBlock from "@/components/UserBlock"; +import { useAuthStore } from "@/store/authStore"; -export function UserSection({notices, title}) { - - const [users, setUsers] = useState([]); +export function UserSection({ notices, title }) { + const token = useAuthStore((state) => state.token); + const headers = token ? { Authorization: `Bearer ${token}` } : {}; + const [users, setUsers] = useState([]); useEffect(() => { - axios.get('https://testowe.zikor.pl/api/v1/clients/get/all') - .then(res => setUsers(res.data)) - .catch(() => setUsers([])); - }, []); - - const usersWithNoticeCount = users.map(user => { - const count = notices.filter(n => n.clientId === user.id).length; + if (token) { + axios + .get("https://hopp.zikor.pl/api/v1/clients/get/all", { headers }) + .then((res) => setUsers(res.data)) + .catch(() => setUsers([])); + } + }, [token]); + + const usersWithNoticeCount = users.map((user) => { + const count = notices.filter((n) => n.clientId === user.id).length; return { ...user, noticeCount: count }; }); @@ -24,7 +29,7 @@ export function UserSection({notices, title}) { .sort((a, b) => b.noticeCount - a.noticeCount) .slice(0, 5); -return ( + return ( {title} { - return ( - - ); + return ; }} /> ); - -} \ No newline at end of file +} diff --git a/ArtisanConnect/eas.json b/ArtisanConnect/eas.json new file mode 100644 index 0000000..a614f19 --- /dev/null +++ b/ArtisanConnect/eas.json @@ -0,0 +1,21 @@ +{ + "cli": { + "version": ">= 16.8.0", + "appVersionSource": "remote" + }, + "build": { + "development": { + "developmentClient": true, + "distribution": "internal" + }, + "preview": { + "distribution": "internal" + }, + "production": { + "autoIncrement": true + } + }, + "submit": { + "production": {} + } +} diff --git a/ArtisanConnect/package.json b/ArtisanConnect/package.json index e578f2b..75e2ac2 100644 --- a/ArtisanConnect/package.json +++ b/ArtisanConnect/package.json @@ -61,7 +61,8 @@ "react-native-svg": "15.11.2", "react-native-web": "~0.20.0", "tailwindcss": "^3.4.17", - "zustand": "^5.0.3" + "zustand": "^5.0.3", + "expo-crypto": "~14.1.4" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/ArtisanConnect/store/authStore.jsx b/ArtisanConnect/store/authStore.jsx index bc9f92c..a5d14d3 100644 --- a/ArtisanConnect/store/authStore.jsx +++ b/ArtisanConnect/store/authStore.jsx @@ -1,108 +1,141 @@ -import {create} from "zustand"; -import {createJSONStorage, persist} from "zustand/middleware"; +import { create } from "zustand"; +import { createJSONStorage, persist } from "zustand/middleware"; import AsyncStorage from "@react-native-async-storage/async-storage"; import axios from "axios"; -const API_URL = "https://testowe.zikor.pl/api/v1"; +const API_URL = "https://hopp.zikor.pl/api/v1"; export const useAuthStore = create( - persist( - (set) => ({ - user: null, - token: null, - isLoading: false, - error: null, + persist( + (set, get) => { + // Dodaj interceptor tylko raz + if (!axios.interceptors.response.handlers.length) { + axios.interceptors.response.use( + (response) => response, + (error) => { + if ( + (error.response && error.response.status === 401) || + error.response.status === 403 + ) { + set({ user: null, token: null, isLoading: false }); + delete axios.defaults.headers.common["Authorization"]; + } + return Promise.reject(error); + } + ); + } - signIn: async (email, password) => { - set({isLoading: true, error: null}); - try { - const response = await axios.post(`${API_URL}/auth/login`, { - email, - password - }); + return { + user_id: null, + token: null, + isLoading: false, + error: null, - const user = response.data.user; - const token = response.data.token; - set({user, token, isLoading: false}); - } catch (error) { - set({error: error.response?.data?.message || error.message, isLoading: false}); - throw error; - } - }, + signIn: async (email, password) => { + set({ isLoading: true, error: null }); + try { + const response = await axios.post(`${API_URL}/auth/login`, { + email, + password, + }); - signUp: async (userData) => { - set({isLoading: true, error: null}); - try { - console.log(userData); + const user_id = response.data.user_id; + const token = response.data.token; + set({ user_id: user_id, token: token, isLoading: false }); - const response = await axios.post(`${API_URL}/auth/register`, userData, { - headers: {'Content-Type': 'application/json'} - }); + axios.defaults.headers.common["Authorization"] = `Bearer ${token}`; + } catch (error) { + set({ + error: error.response?.data?.message || error.message, + isLoading: false, + }); + throw error; + } + }, - console.log(response.data); + signUp: async (userData) => { + set({ isLoading: true, error: null }); + try { + const response = await axios.post( + `${API_URL}/auth/register`, + userData, + { + headers: { "Content-Type": "application/json" }, + } + ); - const user = response.data.user; - const token = response.data.token; - set({user, token, isLoading: false}); + const user_id = response.data.user_id; + const token = response.data.token; + set({ user_id: user_id, token: token, isLoading: false }); - return user; - } catch (error) { - set({error: error.response?.data?.message || error.message, isLoading: false}); - throw error; - } - }, + axios.defaults.headers.common["Authorization"] = `Bearer ${token}`; + } catch (error) { + set({ + error: error.response?.data?.message || error.message, + isLoading: false, + }); + throw error; + } + }, - signInWithGoogle: async (googleToken) => { - set({isLoading: true, error: null}); - try { - const response = await axios.post(`${API_URL}/auth/google`, {token: googleToken}); + signInWithGoogle: async (googleToken) => { + set({ isLoading: true, error: null }); + try { + const response = await axios.post( + `${API_URL}/auth/google`, + { googleToken: googleToken }, + { + headers: { "Content-Type": "application/json" }, + } + ); + const user_id = response.data.user_id; + const token = response.data.token; + set({ user_id: user_id, token: token, isLoading: false }); - const {user, token} = response.data; - set({user, token, isLoading: false}); + axios.defaults.headers.common["Authorization"] = `Bearer ${token}`; + } catch (error) { + set({ + error: error.response?.data?.message || error.message, + isLoading: false, + }); + throw error; + } + }, - axios.defaults.headers.common["Authorization"] = `Bearer ${token}`; + signOut: async () => { + try { + await axios.post(`${API_URL}/auth/logout`); + } catch (error) { + console.error("Logout error:", error); + } finally { + delete axios.defaults.headers.common["Authorization"]; + set({ user_id: null, token: null }); + } + }, - return user; - } catch (error) { - set({error: error.response?.data?.message || error.message, isLoading: false}); - throw error; - } - }, + // checkAuth: async () => { + // const { token } = useAuthStore.getState(); + // if (!token) return null; - signOut: async () => { - try { - // Можно отправить запрос на бэкенд для инвалидации токена - await axios.post(`${API_URL}/auth/logout`); - } catch (error) { - console.error("Logout error:", error); - } finally { - delete axios.defaults.headers.common["Authorization"]; - set({user: null, token: null}); - } - }, + // set({ isLoading: true }); + // try { + // axios.defaults.headers.common["Authorization"] = `Bearer ${token}`; - checkAuth: async () => { - const {token} = useAuthStore.getState(); - if (!token) return null; + // const response = await axios.get(`${API_URL}/auth/me`); - set({isLoading: true}); - try { - axios.defaults.headers.common["Authorization"] = `Bearer ${token}`; - - const response = await axios.get(`${API_URL}/auth/me`); - - set({user: response.data, isLoading: false}); - return response.data; - } catch (error) { - delete axios.defaults.headers.common["Authorization"]; - set({user: null, token: null, isLoading: false}); - return null; - } - }, - }), - { - name: "auth-storage", - storage: createJSONStorage(() => AsyncStorage), - } - ) -); \ No newline at end of file + // set({ user_id: response.data, isLoading: false }); + // return response.data; + // } catch (error) { + // delete axios.defaults.headers.common["Authorization"]; + // set({ user_id: null, token: null, isLoading: false }); + // return null; + // } + // }, + }; + }, + { + name: "auth-storage", + storage: createJSONStorage(() => AsyncStorage), + } + ) +);