10 Commits

Author SHA1 Message Date
d4133f28bd autoryzacja działa zarówno na android jak i ios 2025-06-06 15:49:08 +02:00
b6f2225148 scheme fix 2025-06-04 22:25:31 +02:00
8dc51fafdf AUTORYZACJA DO GOOOOOGLA DZIALA 2025-06-04 22:09:03 +02:00
1862b6b79e Ekran logowania i rejestracja działa! Tylko wewnętrzne logowanie, bez google 2025-06-04 15:41:40 +02:00
9962dc1e55 clear form after adding (problem with category clearing) 2025-05-20 18:26:50 +02:00
00fbe8b655 Merge remote-tracking branch 'origin/zustand' into zustand 2025-05-20 15:20:34 +02:00
532183b305 Add, list, getbyid
working via zustand now + adding image from camera
2025-05-20 15:20:21 +02:00
a7cf31900b Add, list, getbyid
working via zustand now
2025-05-20 15:19:29 +02:00
Patryk
0295386dac add css styles to drawer 2025-05-08 22:12:33 +02:00
Patryk
6598faf5e8 init drawer 2025-05-06 22:17:51 +02:00
21 changed files with 847 additions and 13040 deletions

View File

View File

@@ -1,6 +1,6 @@
import axios from "axios";
const API_URL = "https://testowe.zikor.pl/api/v1";
const API_URL = "https://hopp.zikor.pl/api/v1";
export async function listCategories() {
try {

View File

@@ -1,12 +1,18 @@
import axios from "axios";
import FormData from 'form-data'
import {useAuthStore} from "@/store/authStore";
const API_URL = "https://testowe.zikor.pl/api/v1";
// const API_URL = "https://testowe.zikor.pl/api/v1";
// const API_URL = "http://172.20.10.2:8080/api/v1";
const API_URL = "https://hopp.zikor.pl/api/v1";
export async function listNotices() {
const response = await fetch(`${API_URL}/notices/get/all`);
const { token } = useAuthStore.getState();
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
const response = await fetch(`${API_URL}/notices/get/all`, {
headers: headers
});
const data = await response.json();
if (!response.ok) {
throw new Error(response.toString());
@@ -17,11 +23,11 @@ export async function listNotices() {
export async function getNoticeById(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) {
@@ -31,19 +37,17 @@ export async function createNotice(notice) {
"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) {
notice.image.forEach(imageUri => {
uploadImage(response.data.noticeId, imageUri);
});
// uploadImage(response.data.noticeId, notice.image);
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;
}
}
@@ -55,8 +59,6 @@ export async function getImageByNoticeId(noticeId) {
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}`);
@@ -70,26 +72,23 @@ export async function getAllImagesByNoticeId(noticeId) {
const listResponse = await axios.get(`${API_URL}/images/list/${noticeId}`);
if (listResponse.data && listResponse.data.length > 0) {
const imageUrls = listResponse.data.map(imageName =>
return 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);
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("Started upload image");
console.log(imageUri);
const formData = new FormData();
const filename = imageUri.split('/').pop();

View File

@@ -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"
@@ -32,7 +38,20 @@
{
"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"
}
],
"expo-web-browser"
],
"extra": {
"router": {},
"eas": {
"projectId": "7a0d8bc8-938f-4d2a-babb-945faee13429"
}
}
}
}

View File

@@ -1,62 +1,75 @@
import { Tabs } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
import {Tabs} from "expo-router";
import {Ionicons} from "@expo/vector-icons";
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: "rgb(var(--color-primary-500))",
}}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarLabel: "Home",
tabBarIcon: ({ color, size }) => (
<Ionicons name="home-outline" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="notices"
options={{
title: "Ogłoszenia",
tabBarLabel: "Ogłoszenia",
tabBarIcon: ({ color, size }) => (
<Ionicons name="list-outline" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="notice/create"
options={{
title: "Dodaj",
tabBarLabel: "Dodaj",
tabBarIcon: ({ color, size }) => (
<Ionicons name="add-circle-outline" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="wishlist"
options={{
title: "Ulubione",
tabBarLabel: "Ulubione",
tabBarIcon: ({ color, size }) => (
<Ionicons name="heart-outline" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="account"
options={{
title: "Konto",
tabBarLabel: "Konto",
tabBarIcon: ({ color, size }) => (
<Ionicons name="person-outline" size={size} color={color} />
),
}}
/>
</Tabs>
);
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: "rgb(var(--color-primary-500))",
}}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarLabel: "Home",
tabBarIcon: ({color, size}) => (
<Ionicons name="home-outline" size={size} color={color}/>
),
}}
/>
<Tabs.Screen
name="notices"
options={{
title: "Ogłoszenia",
tabBarLabel: "Ogłoszenia",
tabBarIcon: ({color, size}) => (
<Ionicons name="list-outline" size={size} color={color}/>
),
}}
/>
<Tabs.Screen
name="notice/create"
options={{
title: "Dodaj",
tabBarLabel: "Dodaj",
tabBarIcon: ({color, size}) => (
<Ionicons name="add-circle-outline" size={size} color={color}/>
),
}}
/>
<Tabs.Screen
name="wishlist"
options={{
title: "Ulubione",
tabBarLabel: "Ulubione",
tabBarIcon: ({color, size}) => (
<Ionicons name="heart-outline" size={size} color={color}/>
),
}}
/>
<Tabs.Screen
name="login"
options={{
headerShown: false, // Ukryj nagłówek dla Drawer
title: "Authentication",
tabBarLabel: "Authentication",
tabBarIcon: ({color, size}) => (
<Ionicons name="key" size={size} color={color}/>
),
}}
/>
<Tabs.Screen
name="dashboard"
options={{
headerShown: false, // Ukryj nagłówek dla Drawer
title: "Konto",
tabBarLabel: "Konto",
tabBarIcon: ({color, size}) => (
<Ionicons name="person-outline" size={size} color={color}/>
),
}}
/>
</Tabs>
);
}

View File

@@ -0,0 +1,24 @@
import { Drawer } from "expo-router/drawer";
export default function AccountDrawerLayout() {
return (
<Drawer
screenOptions={{
drawerActiveTintColor: "#1c1c1e",
drawerInactiveTintColor: "#8e8e8f",
drawerActiveBackgroundColor: "#f0f0f0",
drawerItemStyle: {
borderRadius: 8,
// backgroundColor: "transparent",
},
headerTintColor: "#1c1c1e",
}}
>
<Drawer.Screen name="account" options={{ title: "Konto" }} />
<Drawer.Screen
name="userNotices"
options={{ title: "Moje ogłoszenia" }}
/>
</Drawer>
);
}

View File

@@ -0,0 +1,4 @@
import { Text } from "@/components/ui/text";
export default function UserNotices() {
return <Text>Użytkownik</Text>;
}

View File

@@ -0,0 +1,180 @@
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';
import {Box} from "@/components/ui/box"
import {Button, ButtonText, ButtonIcon} from "@/components/ui/button"
import {Center} from "@/components/ui/center"
import {Heading} from "@/components/ui/heading"
import {Input, InputField} from "@/components/ui/input"
import {Text} from "@/components/ui/text"
import {VStack} from "@/components/ui/vstack"
import {HStack} from "@/components/ui/hstack"
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, 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.');
return;
}
try {
await signIn(email, password);
alert(`Zalogowano jako ${email}`);
router.replace('/');
} catch (e) {
alert("Błąd logowania: " + (e.response?.data?.message || e.message));
}
}
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 (
<View style={styles.container}>
<ActivityIndicator size="large"/>
</View>
);
}
return (
<SafeAreaView style={styles.container}>
<Center>
<Box className="p-5 max-w-96 border border-background-300 rounded-lg">
<VStack className="pb-4" space="xs">
<Heading className="leading-[30px]">Logowanie</Heading>
<Box className="flex flex-row">
<Link href="/registration" asChild>
<Button variant="link" size="sm" className="p-0">
<ButtonText style={styles.signupbutton}>Nie masz jeszcze konta? Załóz je
tutaj!</ButtonText>
<ButtonIcon className="mr-1" size="md" as={ArrowRightIcon}/>
</Button>
</Link>
</Box>
</VStack>
<VStack space="xl" className="py-2">
<Input>
<InputField className="py-2" placeholder="Login" onChangeText={setEmail}/>
</Input>
<Input>
<InputField type="password" className="py-2" placeholder="Hasło"
onChangeText={setPassword}/>
</Input>
</VStack>
<VStack space="lg" className="pt-4">
<Button size="sm" onPress={handleInternalLogin}>
<ButtonText>Zaloguj się</ButtonText>
</Button>
</VStack>
<HStack alignItems="center" space="sm" className="pt-6 pb-6">
<Divider flex={1}/>
<Text fontSize="$sm" className="text-gray-300">
lub
</Text>
<Divider flex={1}/>
</HStack>
<Button size="sm" onPress={() => promptAsync()}>
<Ionicons name="logo-google" color="#fff"/>
</Button>
</Box>
</Center>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 20,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 5,
marginBottom: 15,
padding: 10,
},
errorText: {
color: 'red',
marginBottom: 10,
},
signupbutton: {
fontWeight: '300',
},
});

View File

@@ -21,17 +21,20 @@ import {
} from "@/components/ui/select";
import {ChevronDownIcon} from "@/components/ui/icon";
import {useMutation} from "@tanstack/react-query";
import {createNotice} from "@/api/notices";
import {useNoticesStore} from "@/store/noticesStore";
import {listCategories} from "@/api/categories";
import {useRouter} from "expo-router";
export default function CreateNotice() {
const router = useRouter();
const {addNotice, fetchNotices} = useNoticesStore();
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [price, setPrice] = useState("");
const [category, setCategory] = useState("");
const [image, setImage] = useState([]);
const [selectItems, setSelectItems] = useState([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
let isMounted = true;
@@ -73,26 +76,7 @@ export default function CreateNotice() {
},
});
const noticeMutation = useMutation({
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 = () => {
const handleAddNotice = async () => {
setError({
title: !title,
description: !description,
@@ -104,13 +88,49 @@ export default function CreateNotice() {
console.log("Error in form");
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);
await fetchNotices();
clearForm();
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 () => {
// No permissions request is necessary for launching the image library
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
mediaTypes: 'images',
selectionLimit: 8,
allowsEditing: false,
allowsMultipleSelection: true,
@@ -119,11 +139,24 @@ export default function CreateNotice() {
});
if (!result.canceled) {
// await uploadImage(1, result.assets[0].uri);
setImage(result.assets.map(asset => asset.uri));
}
};
const clearForm = () => {
setTitle("");
setDescription("");
setPrice("");
setCategory("");
setImage([]);
setError({
title: false,
description: false,
price: false,
category: false,
})
}
return (
<ScrollView h="$80" w="$80">
<FormControl className="p-4 border rounded-lg border-outline-300">
@@ -135,10 +168,13 @@ export default function CreateNotice() {
Wybierz zdjęcia
</ButtonText>
</Button>
<Text size="sm"
bold="true"
>
Pierwsze zdjęcie będzie zdjęciem głównym</Text>
<Button onPress={takePicture}>
<ButtonText>Zrób zdjęcie</ButtonText>
</Button>
<Text size="sm"
bold="true"
>
Pierwsze zdjęcie będzie zdjęciem głównym</Text>
{image && image.length > 0 && (
<VStack space="xs" className="flex-row flex-wrap">
{image.map((img, index) => (
@@ -208,8 +244,8 @@ export default function CreateNotice() {
</VStack>
<Button
className="mt-5 w-full"
onPress={() => addNotice()}
disabled={noticeMutation.isLoading}
onPress={handleAddNotice}
disabled={isLoading}
>
<ButtonText className="text-typography-0">Dodaj</ButtonText>
</Button>
@@ -217,4 +253,4 @@ export default function CreateNotice() {
</FormControl>
</ScrollView>
);
}
}

View File

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

View File

@@ -1,4 +1,4 @@
import { Tabs, Stack } from "expo-router";
import { Stack } from "expo-router";
import "@/global.css";
import { GluestackUIProvider } from "@/components/ui/gluestack-ui-provider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

View File

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

View File

@@ -0,0 +1,96 @@
import React, {useState} from 'react';
import {StyleSheet, ActivityIndicator, SafeAreaView, View} from 'react-native';
import {useAuthStore} from '@/store/authStore';
import {useRouter} from 'expo-router';
import {Box} from "@/components/ui/box"
import {Button, ButtonText} from "@/components/ui/button"
import {Center} from "@/components/ui/center"
import {Heading} from "@/components/ui/heading"
import {Input, InputField} from "@/components/ui/input"
import {VStack} from "@/components/ui/vstack"
export default function Registration() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const {signUp, isLoading} = useAuthStore();
const router = useRouter();
const handleInternalRegistration = async () => {
if (!email || !password || !firstName || !lastName) {
alert('Proszę uzupełnić wszystkie pola');
return;
}
try {
await signUp({email, password, firstName, lastName});
alert(`Zalogowano jako ${email}`);
router.replace('/');
} catch (e) {
alert("Błąd logowania: " + (e.response?.data?.message || e.message));
}
}
if (isLoading) {
return (
<View style={styles.container}>
<ActivityIndicator size="large"/>
</View>
);
}
return (
<SafeAreaView style={styles.container}>
<Center>
<Box className="p-5 w-[80%] border border-background-300 rounded-lg">
<VStack className="pb-4" space="xs">
<Heading className="leading-[30px]">Rejestracja</Heading>
</VStack>
<VStack space="xl" className="py-2">
<Input>
<InputField type="email" className="py-2" placeholder="E-mail" onChangeText={setEmail}/>
</Input>
<Input>
<InputField className="py-2" placeholder="Imię" onChangeText={setFirstName}/>
</Input>
<Input>
<InputField className="py-2" placeholder="Nazwisko" onChangeText={setLastName}/>
</Input>
<Input>
<InputField type="password" className="py-2" placeholder="Hasło"
onChangeText={setPassword}/>
</Input>
</VStack>
<VStack space="lg" className="pt-4">
<Button size="sm" onPress={handleInternalRegistration}>
<ButtonText>Zarejestruj się</ButtonText>
</Button>
</VStack>
</Box>
</Center>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
formContainer: {
width: '80%',
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 5,
marginBottom: 15,
padding: 10,
},
errorText: {
color: 'red',
marginBottom: 10,
},
});

View File

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

21
ArtisanConnect/eas.json Normal file
View File

@@ -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": {}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@
"@gluestack-style/react": "^1.0.57",
"@gluestack-ui/actionsheet": "^0.2.53",
"@gluestack-ui/button": "^1.0.14",
"@gluestack-ui/divider": "^0.1.10",
"@gluestack-ui/form-control": "^0.1.19",
"@gluestack-ui/hstack": "^0.1.17",
"@gluestack-ui/icon": "^0.1.27",
@@ -26,29 +27,38 @@
"@gluestack-ui/themed": "^1.1.73",
"@gluestack-ui/toast": "^1.0.9",
"@legendapp/motion": "^2.4.0",
"@react-native-async-storage/async-storage": "2.1.2",
"@react-native-community/cli-link-assets": "^18.0.0",
"@react-navigation/drawer": "^7.3.11",
"@tanstack/react-query": "^5.74.4",
"axios": "^1.9.0",
"babel-plugin-module-resolver": "^5.0.2",
"expo": "^53.0.0",
"expo-constants": "~17.1.5",
"expo-auth-session": "~6.1.5",
"expo-camera": "~16.1.6",
"expo-constants": "~17.1.6",
"expo-image-picker": "~16.1.4",
"expo-linking": "~7.1.4",
"expo-router": "~5.0.5",
"expo-status-bar": "~2.2.3",
"expo-web-browser": "~14.1.6",
"form-data": "^4.0.2",
"fs": "^0.0.1-security",
"lucide-react-native": "^0.511.0",
"nativewind": "^4.1.23",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.2",
"react-native-css-interop": "^0.1.22",
"react-native-reanimated": "3.17.4",
"react-native-gesture-handler": "~2.24.0",
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.10.0",
"react-native-screens": "~4.11.1",
"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",

View File

@@ -0,0 +1,105 @@
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://hopp.zikor.pl/api/v1";
export const useAuthStore = create(
persist(
(set) => ({
user_id: null,
token: null,
isLoading: false,
error: null,
signIn: async (email, password) => {
set({isLoading: true, error: null});
try {
const response = await axios.post(`${API_URL}/auth/login`, {
email,
password
});
const user_id = response.data.user_id;
const token = response.data.token;
set({user_id: user_id, token: token, isLoading: false});
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
} catch (error) {
set({error: error.response?.data?.message || error.message, isLoading: false});
throw error;
}
},
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_id = response.data.user_id;
const token = response.data.token;
set({user_id: user_id, token: token, isLoading: false});
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`, {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});
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
} catch (error) {
set({error: error.response?.data?.message || error.message, isLoading: false});
throw error;
}
},
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});
}
},
checkAuth: async () => {
const {token} = useAuthStore.getState();
if (!token) return null;
set({isLoading: true});
try {
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
const response = await axios.get(`${API_URL}/auth/me`);
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),
}
)
);

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) =>
set((state) => ({
wishlistNotices: state.wishlistNotices.filter(
(item) => item.noticeId != noticeId
(item) => item.noticeId !== noticeId
),
})),
}));