diff --git a/ArtisanConnect/api/email.jsx b/ArtisanConnect/api/email.jsx new file mode 100644 index 0000000..1280d24 --- /dev/null +++ b/ArtisanConnect/api/email.jsx @@ -0,0 +1,30 @@ +import { useAuthStore } from "@/store/authStore"; + +const API_URL = "https://hopp.zikor.pl/api/v1"; + +export const sendEmail = async (emailData) => { + const token = useAuthStore.getState().token; + + try { + const response = await fetch(`${API_URL}/email/send`, { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(token && { Authorization: `Bearer ${token}` }), + }, + body: JSON.stringify(emailData), + }); + + if (!response.ok) { + const errorMessage = `HTTP error! Status: ${response.status}`; + console.error("Error przy wysyłaniu maila", errorMessage); + return { success: false, error: errorMessage }; + } + + const result = await response.text(); + return { success: true, result }; + } catch (error) { + console.error("Error przy wysyłaniu maila:", error.message); + return { success: false, error: error.message }; + } +}; \ No newline at end of file diff --git a/ArtisanConnect/app.json b/ArtisanConnect/app.json index bbe332e..27daff2 100644 --- a/ArtisanConnect/app.json +++ b/ArtisanConnect/app.json @@ -5,7 +5,7 @@ "scheme": "com.hamx.artisanconnect", "version": "1.0.0", "orientation": "portrait", - "icon": "./assets/icon.png", + "icon": "./assets/AppIco.png", "userInterfaceStyle": "light", "newArchEnabled": true, "splash": { diff --git a/ArtisanConnect/app/(tabs)/dashboard/userNotices.jsx b/ArtisanConnect/app/(tabs)/dashboard/userNotices.jsx index c131609..084de5c 100644 --- a/ArtisanConnect/app/(tabs)/dashboard/userNotices.jsx +++ b/ArtisanConnect/app/(tabs)/dashboard/userNotices.jsx @@ -63,7 +63,7 @@ export default function UserNotices() { } }; loadNotices(); - }, []); + }, [fetchNotices]); const showNewToast = (title) => { const newId = Math.random(); diff --git a/ArtisanConnect/app/_layout.jsx b/ArtisanConnect/app/_layout.jsx index 599aab1..f6b4b19 100644 --- a/ArtisanConnect/app/_layout.jsx +++ b/ArtisanConnect/app/_layout.jsx @@ -17,6 +17,7 @@ return ( }} > + diff --git a/ArtisanConnect/app/notice/[id].jsx b/ArtisanConnect/app/notice/[id].jsx index 5e7a785..5ad9b2f 100644 --- a/ArtisanConnect/app/notice/[id].jsx +++ b/ArtisanConnect/app/notice/[id].jsx @@ -1,50 +1,107 @@ -import {Link, Stack, useLocalSearchParams} from "expo-router"; -import {Box} from "@/components/ui/box"; -import {Card} from "@/components/ui/card"; -import {Heading} from "@/components/ui/heading"; -import {Image} from "@/components/ui/image"; -import {Text} from "@/components/ui/text"; -import {VStack} from "@/components/ui/vstack"; -import {Ionicons} from "@expo/vector-icons"; +import { Link, Stack, useLocalSearchParams } from "expo-router"; +import { Box } from "@/components/ui/box"; +import { Card } from "@/components/ui/card"; +import { Heading } from "@/components/ui/heading"; +import { Image } from "@/components/ui/image"; +import { Text } from "@/components/ui/text"; +import { VStack } from "@/components/ui/vstack"; +import { Avatar, AvatarImage, AvatarFallbackText } from "@gluestack-ui/themed"; +import { Ionicons } from "@expo/vector-icons"; import { - ActivityIndicator, - Dimensions, - FlatList, - View, - TextInput, + ActivityIndicator, + Dimensions, + FlatList, + View, + TextInput, + SafeAreaView, Alert, } from "react-native"; -import {useEffect, useState, useRef} from "react"; -import {useNoticesStore} from "@/store/noticesStore"; -import {useWishlist} from "@/store/wishlistStore"; -import {Pressable, ScrollView} from "react-native"; -import {getUserById} from "@/api/client"; - -const {width} = Dimensions.get("window"); +import { useEffect, useState, useRef } from "react"; +import { useNoticesStore } from "@/store/noticesStore"; +import { useWishlist } from "@/store/wishlistStore"; +import { Pressable, ScrollView } from "react-native"; +import { getUserById } from "@/api/client"; +import * as ScreenOrientation from "expo-screen-orientation"; +import { useAuthStore } from "@/store/authStore"; +import { sendEmail } from "@/api/email"; export default function NoticeDetails() { - const {id} = useLocalSearchParams(); - const [images, setImages] = useState([]); - const [isImageLoading, setIsImageLoading] = useState(true); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [notice, setNotice] = useState(null); - const [user, setUser] = useState(null); - const [isUserLoading, setIsUserLoading] = useState(true); - const flatListRef = useRef(null); - const [currentIndex, setCurrentIndex] = useState(0); + const { id } = useLocalSearchParams(); + const [images, setImages] = useState([]); + const [isImageLoading, setIsImageLoading] = useState(true); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [notice, setNotice] = useState(null); + const [user, setUser] = useState(null); + const [isUserLoading, setIsUserLoading] = useState(true); + const [isLandscape, setIsLandscape] = useState(false); + const flatListRef = useRef(null); + const [currentIndex, setCurrentIndex] = useState(0); + const [isMessageFormVisible, setIsMessageFormVisible] = useState(false); + const [message, setMessage] = useState(""); + const [isSending, setIsSending] = useState(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); - setIsMessageFormVisible(false); - setMessage(""); - setEmail(""); + const handleSendMessage = async () => { + setIsSending(true); + console.log("Rozpoczynanie procesu wysyłania wiadomości..."); + + const { user_id, token } = useAuthStore.getState(); + console.log("Dane z authStore:", { user_id, token }); + + if (!user_id || !token) { + console.error("Brak danych zalogowanego użytkownika."); + Alert.alert("Błąd", "Zaloguj się, aby wysłać wiadomość."); + setIsSending(false); + return; + } + + let currentUserEmail = ""; + try { + console.log(`Pobieranie danych użytkownika dla user_id: ${user_id}`); + const currentUser = await getUserById(user_id); + console.log("Dane zalogowanego użytkownika:", currentUser); + currentUserEmail = currentUser?.email; + if (!currentUserEmail) { + console.error("Nie znaleziono adresu email zalogowanego użytkownika."); + Alert.alert("Błąd", "Nie znaleziono adresu email zalogowanego użytkownika."); + setIsSending(false); + return; + } + console.log(`Pobrano email zalogowanego użytkownika: ${currentUserEmail}`); + } catch (error) { + console.error("Błąd podczas pobierania danych użytkownika:", error); + Alert.alert("Błąd", "Nie udało się pobrać danych użytkownika. Spróbuj ponownie później."); + setIsSending(false); + return; + } + + const emailData = { + to: user?.email || "", + subject: `Zapytanie ${currentUserEmail} o ogłoszenie ${notice.title}`, + body: message, }; + console.log("Dane emaila do wysyłki:", emailData); + + if (!emailData.to || !emailData.subject || !emailData.body) { + console.error("Walidacja nieudana: brakujące pola w emailData."); + Alert.alert("Błąd", "Wszystkie pola są wymagane!"); + setIsSending(false); + return; + } + + const result = await sendEmail(emailData); + if (result.success) { + console.log("Wiadomość wysłana pomyślnie!", result.result); + setIsMessageFormVisible(false); + setMessage(""); + Alert.alert("Sukces", "Wiadomość została wysłana!"); + } else { + console.error("Błąd podczas wysyłania wiadomości:", result.error); + Alert.alert("Błąd", `Nie udało się wysłać wiadomości: ${result.error}`); + } + setIsSending(false); + console.log("Zakończono proces wysyłania wiadomości."); + }; const formatDate = (dateString) => { const date = new Date(dateString); @@ -55,284 +112,330 @@ export default function NoticeDetails() { }); }; - const {getNoticeById, getAllImagesByNoticeId} = useNoticesStore(); - const toggleNoticeInWishlist = useWishlist( - (state) => state.toggleNoticeInWishlist - ); + const { getNoticeById, getAllImagesByNoticeId } = useNoticesStore(); + const toggleNoticeInWishlist = useWishlist( + (state) => state.toggleNoticeInWishlist + ); - 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 isInWishlist = useWishlist((state) => + id ? state.wishlistNotices.some((item) => item.noticeId == id) : false + ); - 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.`)); - } - } catch (err) { - setError(err); - } finally { - setIsLoading(false); - } - }; - - fetchNotice(); - }, [id]); - - useEffect(() => { - const fetchImage = async () => { - setIsImageLoading(true); - if (notice) { - try { - const fetchedImages = await getAllImagesByNoticeId(notice.noticeId); - setImages( - fetchedImages && fetchedImages.length > 0 - ? fetchedImages - : {uri: "https://http.cat/404.jpg"} - ); - } catch (err) { - console.error("Error while loading images:", err); - setImage({uri: "https://http.cat/404.jpg"}); - } finally { - setIsImageLoading(false); - } - } - }; - - if (notice) { - fetchImage(); - } - }, [notice]); - - 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); - } - } - }; - - fetchUser(); - }, [notice]); - - if (isLoading) { - return ; + const onViewableItemsChanged = useRef(({ viewableItems }) => { + if (viewableItems.length > 0) { + setCurrentIndex(viewableItems[0].index); } - if (error) { - return Błąd, spróbuj ponownie póżniej: {error.message}; + const viewabilityConfig = useRef({ + itemVisiblePercentThreshold: 70, + }).current; + + useEffect(() => { + const unlockOrientation = async () => { + try { + await ScreenOrientation.unlockAsync(); + } catch (err) { + console.error("Error unlocking orientation:", err); + } + }; + + const getInitialOrientation = async () => { + try { + const orientation = await ScreenOrientation.getOrientationAsync(); + setIsLandscape( + orientation === ScreenOrientation.Orientation.LANDSCAPE_LEFT || + orientation === ScreenOrientation.Orientation.LANDSCAPE_RIGHT + ); + } catch (err) { + console.error("Error getting initial orientation:", err); + } + }; + + unlockOrientation(); + getInitialOrientation(); + + const subscription = ScreenOrientation.addOrientationChangeListener( + ({ orientationInfo }) => { + const isLandscapeMode = + orientationInfo.orientation === + ScreenOrientation.Orientation.LANDSCAPE_LEFT || + orientationInfo.orientation === + ScreenOrientation.Orientation.LANDSCAPE_RIGHT; + setIsLandscape(isLandscapeMode); + } + ); + + return () => { + ScreenOrientation.removeOrientationChangeListener(subscription); + ScreenOrientation.lockAsync( + ScreenOrientation.OrientationLock.PORTRAIT_UP + ).catch((err) => + console.error("Error locking orientation on unmount:", err) + ); + }; + }, []); + + 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(() => { + const fetchImage = async () => { + setIsImageLoading(true); + if (notice) { + try { + const fetchedImages = await getAllImagesByNoticeId(notice.noticeId); + console.log("Fetched images:", fetchedImages); + setImages( + fetchedImages && fetchedImages.length > 0 + ? fetchedImages + : ["https://http.cat/404.jpg"] + ); + } catch (err) { + console.error("Error while loading images:", err); + setImages(["https://http.cat/404.jpg"]); + } finally { + setIsImageLoading(false); + } + } + }; + + if (notice) { + fetchImage(); } if (!notice) { return Nie znaleziono ogłoszenia; } + fetchUser(); + }, [notice]); + + if (isLoading) { + return ; + } + + if (error) { + return Błąd, spróbuj ponownie później: {error.message}; + } + + if (!notice) { + return Nie znaleziono ogłoszenia; + } + + const renderImageSection = () => { + if (isImageLoading) { + return ( + + + + ); + } + return ( - - - {isImageLoading ? ( - - - - ) : ( - - ( - - {`Zdjęcie - - )} - keyExtractor={(item, index) => index.toString()} + + ( + + {`Zdjęcie console.error("Image load error:", e.nativeEvent.error)} /> - - {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 - - - - - {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 - - - - - )} - + + )} + keyExtractor={(item, index) => index.toString()} + /> + {images.length > 1 && ( + + {images.map((_, index) => ( + + ))} + + )} + ); + }; + + return ( + + + + + {renderImageSection()} + + + {formatDate(notice.publishDate)} + + + {notice.title} + + + + Cena: + {notice.price} zł + + { + toggleNoticeInWishlist(id); + }} + > + + + + + + Kategoria:{" "} + {notice.category} + + + + Opis ogłoszenia + {notice.description} + + + Użytkownik: + {isUserLoading ? ( + + ) : user ? ( + <> + + + + + {user.firstName?.[0]} + {user.lastName?.[0]} + + + + + + {user.firstName} {user.lastName} + + + Email: {user.email} + + setIsMessageFormVisible(true)} + className="mt-3 bg-blue-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"} + + Temat: + + Zapytanie o ogłoszenie '{notice.title || "Brak nazwy ogłoszenia"}' + + Treść: + + + setIsMessageFormVisible(false)} + className="bg-gray-300 py-2 px-4 rounded-md" + > + Anuluj + + + {isSending ? ( + + ) : ( + Wyślij + )} + + + + + )} + + + ); } diff --git a/ArtisanConnect/app/user/_layout.jsx b/ArtisanConnect/app/user/_layout.jsx new file mode 100644 index 0000000..c384cda --- /dev/null +++ b/ArtisanConnect/app/user/_layout.jsx @@ -0,0 +1,11 @@ +import { Stack } from 'expo-router'; + +export default function UserLayout() { + return ( + + ); +} \ No newline at end of file diff --git a/ArtisanConnect/assets/AppIco.png b/ArtisanConnect/assets/AppIco.png new file mode 100644 index 0000000..9505fb4 Binary files /dev/null and b/ArtisanConnect/assets/AppIco.png differ diff --git a/ArtisanConnect/package-lock.json b/ArtisanConnect/package-lock.json index 92d47f8..9f909aa 100644 --- a/ArtisanConnect/package-lock.json +++ b/ArtisanConnect/package-lock.json @@ -43,6 +43,7 @@ "expo-image-picker": "~16.1.4", "expo-linking": "~7.1.4", "expo-router": "~5.0.5", + "expo-screen-orientation": "~8.1.7", "expo-status-bar": "~2.2.3", "expo-web-browser": "~14.1.6", "form-data": "^4.0.2", @@ -7012,6 +7013,16 @@ "node": ">=10" } }, + "node_modules/expo-screen-orientation": { + "version": "8.1.7", + "resolved": "https://registry.npmjs.org/expo-screen-orientation/-/expo-screen-orientation-8.1.7.tgz", + "integrity": "sha512-nYwadYtdU6mMDk0MCHMPPPQtBoeFYJ2FspLRW+J35CMLqzE4nbpwGeiImfXzkvD94fpOCfI4KgLj5vGauC3pfA==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, "node_modules/expo-status-bar": { "version": "2.2.3", "license": "MIT", diff --git a/ArtisanConnect/package.json b/ArtisanConnect/package.json index 2b538c1..2f66426 100644 --- a/ArtisanConnect/package.json +++ b/ArtisanConnect/package.json @@ -62,7 +62,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-screen-orientation": "~8.1.7" }, "devDependencies": { "@babel/core": "^7.20.0",