diff --git a/ArtisanConnect/api/auth.jsx b/ArtisanConnect/api/auth.jsx new file mode 100644 index 0000000..e69de29 diff --git a/ArtisanConnect/api/notices.jsx b/ArtisanConnect/api/notices.jsx index 3bb9875..55cb0d8 100644 --- a/ArtisanConnect/api/notices.jsx +++ b/ArtisanConnect/api/notices.jsx @@ -1,12 +1,21 @@ 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 = "http://10.0.2.2:8080/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}` } : {}; + + 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()); @@ -17,11 +26,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) { diff --git a/ArtisanConnect/app.json b/ArtisanConnect/app.json index 3f7b0a0..71e8089 100644 --- a/ArtisanConnect/app.json +++ b/ArtisanConnect/app.json @@ -38,7 +38,8 @@ { "cameraPermission": "Please allow $(PRODUCT_NAME) to access your camera" } - ] + ], + "expo-web-browser" ] } -} \ No newline at end of file +} diff --git a/ArtisanConnect/app/(tabs)/_layout.jsx b/ArtisanConnect/app/(tabs)/_layout.jsx index 1dc6711..3ff6e26 100644 --- a/ArtisanConnect/app/(tabs)/_layout.jsx +++ b/ArtisanConnect/app/(tabs)/_layout.jsx @@ -1,64 +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 ( - - ( - - ), - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - ( - - ), - }} - /> - - ); + return ( + + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + + ); } diff --git a/ArtisanConnect/app/(tabs)/login.jsx b/ArtisanConnect/app/(tabs)/login.jsx new file mode 100644 index 0000000..0fbbf82 --- /dev/null +++ b/ArtisanConnect/app/(tabs)/login.jsx @@ -0,0 +1,114 @@ +import React, {useState} from 'react'; +import {StyleSheet, ActivityIndicator, SafeAreaView, View} 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"; + +export default function Login() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const {signIn, isLoading} = useAuthStore(); + const router = useRouter(); + + 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)); + } + } + + if (isLoading) { + return ( + + + + ); + } + + return ( + +
+ + + Logowanie + + + + + + + + + + + + + + + + + + + + + + lub + + + + + +
+
+ ); +} + +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', + }, +}); \ No newline at end of file diff --git a/ArtisanConnect/app/registration.jsx b/ArtisanConnect/app/registration.jsx new file mode 100644 index 0000000..555750c --- /dev/null +++ b/ArtisanConnect/app/registration.jsx @@ -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 ( + + + + ); + } + + return ( + +
+ + + Rejestracja + + + + + + + + + + + + + + + + + + + +
+
+ ); +} + +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, + }, +}); \ No newline at end of file diff --git a/ArtisanConnect/package.json b/ArtisanConnect/package.json index 4ff2af0..60be00d 100644 --- a/ArtisanConnect/package.json +++ b/ArtisanConnect/package.json @@ -15,6 +15,7 @@ "@gluestack-ui/actionsheet": "^0.2.53", "@gluestack-ui/avatar": "^0.1.18", "@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", @@ -29,19 +30,24 @@ "@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-auth-session": "~6.1.5", "expo-camera": "~16.1.6", "expo-constants": "~17.1.5", "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", diff --git a/ArtisanConnect/store/authStore.jsx b/ArtisanConnect/store/authStore.jsx new file mode 100644 index 0000000..f205790 --- /dev/null +++ b/ArtisanConnect/store/authStore.jsx @@ -0,0 +1,108 @@ +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 = "http://10.0.2.2:8080/api/v1"; + +export const useAuthStore = create( + persist( + (set) => ({ + user: 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 = 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; + } + }, + + signUp: async (userData) => { + set({isLoading: true, error: null}); + try { + console.log(userData); + + const response = await axios.post(`${API_URL}/auth/register`, userData, { + headers: {'Content-Type': 'application/json'} + }); + + console.log(response.data); + + const user = response.data.user; + const token = response.data.token; + set({user, token, isLoading: false}); + + return user; + } 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}); + + const {user, token} = response.data; + set({user, token, isLoading: false}); + + axios.defaults.headers.common["Authorization"] = `Bearer ${token}`; + + return user; + } 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: 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: 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