Compare commits

...

12 Commits

15 changed files with 963 additions and 9383 deletions

20
App.js
View File

@@ -1,20 +0,0 @@
// import { StatusBar } from 'expo-status-bar';
// import { StyleSheet, Text, View } from 'react-native';
// export default function App() {
// return (
// <View style={styles.container}>
// <Text>Open up App.js to start working on your app!</Text>
// <StatusBar style="auto" />
// </View>
// );
// }
// const styles = StyleSheet.create({
// container: {
// flex: 1,
// backgroundColor: '#fff',
// alignItems: 'center',
// justifyContent: 'center',
// },
// });

View File

@@ -0,0 +1,175 @@
import axios from 'axios';
import {
listLocations,
getLocation,
addLocation,
updateLocation,
deleteLocation
} from '../api/locations';
jest.mock('axios');
describe('Location API Functions', () => {
afterEach(() => {
jest.clearAllMocks();
});
describe('listLocations', () => {
test('should fetch all locations successfully', async () => {
const mockLocations = [
{
"id": 1,
"name": "Warszawa",
"description": "Stolica Polski, położona w centralnej części kraju. Warszawa jest największym miastem w Polsce, znanym z bogatej historii, kultury i architektury. Warto zobaczyć Zamek Królewski, Stare Miasto oraz Muzeum Powstania Warszawskiego. Miasto oferuje wiele atrakcji turystycznych, w tym parki, muzea i teatry. Warszawa jest również ważnym ośrodkiem gospodarczym i kulturalnym.",
"image": "https://www.niesamowitapolska.eu/images/mazowieckie/warszawa/38607734_m.jpg",
"area": 517.21,
"population": 1790658
},
{
"id": 2,
"name": "Kielce",
"description": "Stolica województwa świętokrzyskiego, położona w centralnej Polsce. Kielce to miasto w centralnej Polsce, znane z pięknych krajobrazów i bogatej historii. Warto odwiedzić Kielecki Park Etnograficzny, Muzeum Zabawek oraz Katedrę Wniebowzięcia Najświętszej Maryi Panny. Kielce są również znane z licznych festiwali i wydarzeń kulturalnych. Miasto oferuje wiele atrakcji turystycznych, w tym parki, muzea i teatry.",
"image": "https://as2.ftcdn.net/jpg/05/42/90/67/1000_F_542906717_cf5i6HeCJsPluuH5tqq5MbsSdfpopmtT.webp",
"area": 109.2,
"population": 196000
}
];
axios.get.mockResolvedValueOnce({ data: mockLocations });
const result = await listLocations();
expect(axios.get).toHaveBeenCalledWith('https://hopp.zikor.pl/locations/all');
expect(result).toEqual(mockLocations);
});
test('should handle error when fetching locations fails', async () => {
axios.get.mockRejectedValueOnce(new Error('Network error'));
await expect(listLocations()).rejects.toThrow('Network error');
expect(axios.get).toHaveBeenCalledWith('https://hopp.zikor.pl/locations/all');
});
})
describe('getLocation', () => {
test('should fetch a location by id successfully', async () => {
const mockLocation = {
"id": 1,
"name": "Warszawa",
"description": "Stolica Polski, położona w centralnej części kraju.",
"image": "https://www.niesamowitapolska.eu/images/mazowieckie/warszawa/38607734_m.jpg",
"area": 517.21,
"population": 1790658
};
axios.get.mockResolvedValueOnce({ data: mockLocation });
const result = await getLocation(1);
expect(axios.get).toHaveBeenCalledWith('https://hopp.zikor.pl/locations/1');
expect(result).toEqual(mockLocation);
});
test('should handle error when fetching a location fails', async () => {
axios.get.mockRejectedValueOnce(new Error('Location not found'));
await expect(getLocation(999)).rejects.toThrow('Location not found');
expect(axios.get).toHaveBeenCalledWith('https://hopp.zikor.pl/locations/999');
});
});
describe('addLocation', () => {
test('should add a location successfully', async () => {
const newLocation = {
"name": "Gdańsk",
"description": "Miasto portowe na północy Polski.",
"image": "https://example.com/gdansk.jpg",
"area": 262.0,
"population": 470907
};
const responseLocation = {
"id": 3,
...newLocation
};
axios.post.mockResolvedValueOnce({ data: responseLocation });
const result = await addLocation(newLocation);
expect(axios.post).toHaveBeenCalledWith(
'https://hopp.zikor.pl/locations/add',
newLocation,
{ headers: { "Content-Type": "application/json" } }
);
expect(result).toEqual(responseLocation);
});
});
describe('updateLocation', () => {
test('should update a location successfully', async () => {
const locationId = 2;
const updatedDetails = {
"name": "Kielce",
"description": "Updated description for Kielce",
"population": 200000
};
const responseLocation = {
"id": locationId,
"name": "Kielce",
"description": "Updated description for Kielce",
"image": "https://as2.ftcdn.net/jpg/05/42/90/67/1000_F_542906717_cf5i6HeCJsPluuH5tqq5MbsSdfpopmtT.webp",
"area": 109.2,
"population": 200000
};
axios.put.mockResolvedValueOnce({ data: responseLocation });
const result = await updateLocation(locationId, updatedDetails);
expect(axios.put).toHaveBeenCalledWith(
`https://hopp.zikor.pl/locations/${locationId}`,
updatedDetails,
{ headers: { "Content-Type": "application/json" } }
);
expect(result).toEqual(responseLocation);
});
test('should handle error when updating a location fails', async () => {
const locationId = 999;
const updatedDetails = { population: 300000 };
axios.put.mockRejectedValueOnce(new Error('Failed to add location'));
await expect(updateLocation(locationId, updatedDetails)).rejects.toThrow('Failed to add location');
expect(axios.put).toHaveBeenCalledWith(
`https://hopp.zikor.pl/locations/${locationId}`,
updatedDetails,
{ headers: { "Content-Type": "application/json" } }
);
});
});
describe('deleteLocation', () => {
test('should delete a location successfully', async () => {
const locationId = 2;
const responseData = { success: true, message: "Location deleted successfully" };
axios.delete.mockResolvedValueOnce({ data: responseData });
const result = await deleteLocation(locationId);
expect(axios.delete).toHaveBeenCalledWith(`https://hopp.zikor.pl/locations/${locationId}`);
expect(result).toEqual(responseData);
});
test('should handle error when deleting a location fails', async () => {
const locationId = 999;
axios.delete.mockRejectedValueOnce(new Error('Failed to delete location'));
await expect(deleteLocation(locationId)).rejects.toThrow('Failed to delete location');
expect(axios.delete).toHaveBeenCalledWith(`https://hopp.zikor.pl/locations/${locationId}`);
});
});
})

101
api/locations.jsx Normal file
View File

@@ -0,0 +1,101 @@
import axios from "axios";
const API_URL = "http://192.168.0.118:9000";
export async function listLocations() {
try {
const response = await axios.get(`${API_URL}/locations/all`);
if (!response) {
return "No locations found";
}
return response.data.map(location => ({
...location,
imageSource: normalizeImageSource(location.image)
}));
} catch (error) {
console.error("Error fetching locations:", error);
throw error;
}
}
export const normalizeImageSource = (image) => {
if (!image) return null;
if (typeof image === 'string') {
if (image.startsWith('http://') || image.startsWith('https://')) {
return { uri: image };
}
if (image.startsWith('data:image')) {
return { uri: image };
} else if (image.length > 100) {
return { uri: `data:image/jpeg;base64,${image}` };
}
}
return null;
}
export async function getLocation(id) {
try {
const location = await axios.get(`${API_URL}/locations/${id}`);
if (!location) {
console.error("Could not find location");
return("Location not found");
}
return [...location.data, {image: normalizeImageSource(location.image)}];
} catch (error) {
console.error("Error fetching location:", error);
throw error;
}
}
export async function addLocation(location) {
try {
const response = await axios.post(`${API_URL}/locations/add`, location, {
headers: {
"Content-Type": "application/json",
},
});
if (!response) {
console.log("Error adding location");
return("Failed to add location");
}
return response.data;
} catch (error) {
console.error("Error adding location:", error);
throw error;
}
}
export async function updateLocation(id, location) {
try {
const response = await axios.put(`${API_URL}/locations/${id}`, location, {
headers: { "Content-Type": "application/json" },
});
if (!response) {
console.log("Failed to update location");
return("Failed to update location");
}
return response.data;
} catch (error) {
console.error("Error while updating location:", error);
throw error;
}
}
export async function deleteLocation(id) {
try {
const response = await axios.delete(`${API_URL}/locations/${id}`);
if (!response) {
console.log("Error deleting location");
return("Failed to delete location");
}
return response.data;
} catch (error) {
console.error("Error while deleting location:", error);
throw error;
}
}

View File

@@ -27,7 +27,19 @@
"bundler": "metro"
},
"plugins": [
"expo-router"
"expo-router",
[
"expo-image-picker",
{
"photosPermission": "The app accesses your photos to let you share them with your friends."
}
],
[
"expo-location",
{
"locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location."
}
]
]
}
}

View File

@@ -1,133 +1,283 @@
import { useState } from "react";
import {useState} from "react";
import {
StyleSheet,
Platform,
KeyboardAvoidingView,
ScrollView,
StyleSheet,
Platform,
KeyboardAvoidingView,
ScrollView,
Image,
View,
Alert,
} from "react-native";
import { TextInput, Button, Snackbar } from "react-native-paper";
import useLocationStore from "@/store";
import {TextInput, Button, Snackbar, Text, useTheme} from "react-native-paper";
import {requestCameraPermissionsAsync, launchCameraAsync} from 'expo-image-picker';
import * as Location from 'expo-location';
import useLocationStore from "@/locationStore";
export default function FormScreen() {
const addLocation = useLocationStore((state) => state.addLocation);
const [formData, setFormData] = useState({
name: "",
description: "",
image: "",
area: "",
population: "",
});
const [message, setMessage] = useState("");
const [visible, setVisible] = useState(false);
const handleAddLocation = () => {
if (
formData.name &&
formData.description &&
formData.image &&
formData.area &&
formData.population
) {
const newLocation = {
id: Date.now(),
name: formData.name,
description: formData.description,
image: formData.image,
area: formData.area,
population: formData.population,
};
addLocation(newLocation);
setFormData({
const [formData, setFormData] = useState({
name: "",
description: "",
image: "",
area: "",
population: "",
});
setMessage("Lokalizacja została dodana!");
setVisible(true);
} else {
setMessage("Wypełnij wszystkie pola!");
setVisible(true);
}
};
area: 0,
population: 0,
latitude: 0,
longitude: 0,
});
const [message, setMessage] = useState("");
const [visible, setVisible] = useState(false);
const [picture, setPicture] = useState(null);
const [imageMethod, setImageMethod] = useState(null);
const [location, setLocation] = useState < Location.LocationObject | null > (null);
const theme = useTheme();
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<ScrollView contentContainerStyle={styles.container}>
<Snackbar
visible={visible}
onDismiss={() => setVisible(false)}
duration={3000}
const addLocation = useLocationStore((state) => state.addLocation);
const handleAddLocation = async () => {
if (
formData.name &&
formData.description &&
formData.image &&
formData.area &&
formData.population &&
formData.latitude &&
formData.longitude
) {
const newLocation = {
id: Date.now(),
name: formData.name,
description: formData.description,
image: formData.image,
area: parseFloat(formData.area),
population: parseInt(formData.population),
latitude: parseFloat(formData.latitude),
longitude: parseFloat(formData.longitude),
};
const added = await addLocation(newLocation);
setFormData({
name: "",
description: "",
image: "",
area: 0,
population: 0,
latitude: 0,
longitude: 0
});
setPicture(null);
setImageMethod(null);
if (added != null) {
setMessage("Lokalizacja została dodana!");
setVisible(true);
} else {
setMessage("Wystąpił błąd podczas dodawania lokalizacji!");
setVisible(true);
}
} else {
setMessage("Wypełnij wszystkie pola!");
console.log(formData);
setVisible(true);
}
};
const takePicture = async () => {
const {status} = await requestCameraPermissionsAsync();
if (status !== 'granted') {
setMessage("Brak uprawnień do kamery!");
setVisible(true);
return;
}
const result = await launchCameraAsync({
allowsEditing: false,
base64: true,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
const image = result.assets[0];
setPicture(image.uri);
setFormData({...formData, image: image.base64});
}
}
async function getCurrentLocation() {
let {status} = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
console.log('Permission to access location was denied');
Alert.alert("Błąd", "Nie udało się uzyskać dostępu do pobierania lokalizacji");
return;
}
try {
let location = await Location.getCurrentPositionAsync({});
setLocation(location);
setFormData({
...formData,
latitude: location.coords.latitude,
longitude: location.coords.longitude
});
Alert.alert("Sukces!", "Pobrano lokalizację\n" +
"Szerokość geograficzna: " + location.coords.latitude + "\n" +
"Długość geograficzna: " + location.coords.longitude);
} catch (error) {
Alert.alert("Błąd", "Nie udało się pobrać lokalizacji");
console.error(error);
}
}
const selectImageMethod = (method) => {
setImageMethod(method);
if (method === 'camera') {
takePicture()
} else {
setFormData({...formData, image: ""});
setPicture(null);
}
}
return (
<KeyboardAvoidingView
style={{flex: 1}}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
{message}
</Snackbar>
<TextInput
mode="outlined"
label="Nazwa"
placeholder="Wpisz nazwę"
style={{ margin: 10, width: "100%" }}
value={formData.name}
onChangeText={(e) => setFormData({ ...formData, name: e })}
/>
<TextInput
mode="outlined"
label="Opis"
placeholder="Wpisz opis"
style={{ margin: 10, width: "100%" }}
multiline={true}
value={formData.description}
onChangeText={(e) => setFormData({ ...formData, description: e })}
/>
<TextInput
mode="outlined"
label="Link do zdjęcia"
placeholder="Wpisz link do zdjęcia"
multiline={true}
style={{ margin: 10, width: "100%" }}
value={formData.image}
onChangeText={(e) => setFormData({ ...formData, image: e })}
/>
<TextInput
mode="outlined"
label="Powierzchnia"
placeholder="Wpisz powierzchnię"
style={{ margin: 10, width: "100%" }}
value={formData.area}
onChangeText={(e) => setFormData({ ...formData, area: e })}
/>
<TextInput
mode="outlined"
label="Ludność"
placeholder="Wpisz liczbę ludności"
style={{ margin: 10, width: "100%" }}
value={formData.population}
keyboardType="numeric"
onChangeText={(e) => setFormData({ ...formData, population: e })}
/>
<Button
style={{ margin: 10, width: "100%" }}
icon="plus-circle-outline"
mode={"contained"}
onPress={handleAddLocation}
>
Dodaj
</Button>
</ScrollView>
</KeyboardAvoidingView>
);
<ScrollView contentContainerStyle={styles.container} contentInsetAdjustmentBehavior="automatic"
style={[{backgroundColor: theme.colors.background}]}
>
<Snackbar
visible={visible}
onDismiss={() => setVisible(false)}
duration={3000}
>
{message}
</Snackbar>
<Text style={styles.label} variant="labelLarge">Jak chcesz dodać zdjęcie?</Text>
<View style={styles.imageMethodButtons}>
<Button
mode={imageMethod === 'link' ? "contained" : "outlined"}
onPress={() => selectImageMethod('link')}
style={styles.methodButton}
>
Użyj linku
</Button>
<Button
mode={imageMethod === 'camera' ? "contained" : "outlined"}
onPress={() => selectImageMethod('camera')}
style={styles.methodButton}
>
Zrób zdjęcie
</Button>
</View>
{imageMethod === 'link' && (
<TextInput
mode="outlined"
label="Link do zdjęcia"
placeholder="Wpisz link do zdjęcia"
multiline={true}
style={{margin: 10, width: "100%"}}
value={formData.image}
onChangeText={(e) => setFormData({...formData, image: e})}
/>
)}
{picture && (
<View style={styles.imageContainer}>
<Image
source={{uri: picture}}
style={styles.image}
/>
</View>
)}
<TextInput
mode="outlined"
label="Nazwa"
placeholder="Wpisz nazwę"
style={{margin: 10, width: "100%"}}
value={formData.name}
onChangeText={(e) => setFormData({...formData, name: e})}
/>
<TextInput
mode="outlined"
label="Opis"
placeholder="Wpisz opis"
style={{margin: 10, width: "100%"}}
multiline={true}
value={formData.description}
onChangeText={(e) => setFormData({...formData, description: e})}
/>
<TextInput
mode="outlined"
label="Powierzchnia"
placeholder="Wpisz powierzchnię"
style={{margin: 10, width: "100%"}}
value={formData.area}
keyboardType="numbers-and-punctuation"
onChangeText={(e) => setFormData({...formData, area: e})}
/>
<TextInput
mode="outlined"
label="Ludność"
placeholder="Wpisz liczbę ludności"
style={{margin: 10, width: "100%"}}
value={formData.population}
keyboardType="numeric"
onChangeText={(e) => setFormData({...formData, population: e})}
/>
<Button
style={{margin: 10, width: "100%"}}
icon="plus-circle-outline"
mode={"contained"}
onPress={handleAddLocation}
>
Dodaj
</Button>
<Button
style={{margin: 10, width: "100%"}} mode={"contained"}
onPress={getCurrentLocation}
icon={location ? "check-circle-outline" : "map-marker-outline"}
>
Pobierz aktualną lokalizację
</Button>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#25292e",
justifyContent: "flex-start",
alignItems: "center",
},
});
container: {
// backgroundColor: "#25292e",
alignItems: "center",
padding: 10,
flexGrow: 1,
},
imageContainer: {
width: "100%",
alignItems: "flex-start",
marginVertical: 10,
},
image: {
width: 150,
height: 150,
borderStyle: "solid",
borderWidth: 1,
borderRadius: 5,
},
imageMethodButtons: {
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
marginVertical: 10,
},
methodButton: {
flex: 1,
marginHorizontal: 5,
},
label: {
marginTop: 10,
marginBottom: 10,
verticalAlign: "middle",
textAlign: "center",
}
});

View File

@@ -1,23 +1,34 @@
import { View, StyleSheet, FlatList } from 'react-native';
import { useTheme, Card, Text, Button } from 'react-native-paper';
import { Link } from 'expo-router';
import useLocationStore from '@/store';
import {
View,
StyleSheet,
FlatList,
} from "react-native";
import { useTheme, Card, Text, Button } from "react-native-paper";
import { Link } from "expo-router";
import useLocationStore from "@/locationStore";
export default function Index() {
const theme = useTheme();
const locations = useLocationStore((state) => state.locations);
const { locations } = useLocationStore();
return (
<View style={[styles.container, { backgroundColor: theme.colors.background }]}>
<View
style={[styles.container, { backgroundColor: theme.colors.background }]}
>
<FlatList
data={locations}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<Card style={{ margin: 10 }}>
<Card.Cover style={{ marginBottom: 10 }} source={{ uri: item.image }} />
<Card.Cover
style={{ marginBottom: 10 }}
source={item.imageSource}
/>
<Card.Content style={{ marginBottom: 10 }}>
<Text variant="titleLarge">{item.name}</Text>
<Text variant="bodyMedium">{item.description.split('.')[0]}...</Text>
<Text variant="bodyMedium">
{item.description && item.description.split(".")[0]}...
</Text>
</Card.Content>
<Card.Actions>
<Link href={`/location/${item.id}`} asChild>
@@ -34,8 +45,8 @@ export default function Index() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: "#25292e",
justifyContent: "center",
alignItems: "center",
},
});
});

View File

@@ -1,55 +1,78 @@
import { Link, Stack, useRouter } from 'expo-router';
import { useColorScheme} from 'react-native';
import { PaperProvider} from 'react-native-paper';
import { MD3LightTheme, MD3DarkTheme } from 'react-native-paper';
import Ionicons from '@expo/vector-icons/Ionicons';
import useLocationStore from '@/store';
import { Link, Stack, useRouter } from "expo-router";
import { useColorScheme } from "react-native";
import { PaperProvider } from "react-native-paper";
import { MD3LightTheme, MD3DarkTheme } from "react-native-paper";
import { useEffect } from "react";
import Ionicons from "@expo/vector-icons/Ionicons";
import useLocationStore from "@/locationStore";
export default function RootLayout() {
const colorScheme = useColorScheme();
const theme = colorScheme === 'dark' ? MD3DarkTheme :
MD3LightTheme;
const deleteLocation = useLocationStore((state) => state.deleteLocation);
const theme = colorScheme === "dark" ? MD3DarkTheme : MD3LightTheme;
const router = useRouter();
const handleDelete = (id) => {
deleteLocation(id);
router.replace('/');
const fetchLocations = useLocationStore((state) => state.fetchLocations);
useEffect(() => {
fetchLocations();
}, []);
console.log();
const deleteLocation = useLocationStore((state) => state.deleteLocation);
const handleDelete = async (id) => {
const isDeleted = await deleteLocation(id);
if (isDeleted) {
router.replace("/");
}
};
return (
<PaperProvider theme={theme} >
<Stack screenOptions={{
headerStyle: {
backgroundColor: theme.colors.primaryContainer,
borderBottomWidth:0
},
headerTintColor: theme.colors.primary,
}}>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="location/[id]"
options={({ route }) => ({
title: "Lokalizacja",
<PaperProvider theme={theme}>
<Stack
screenOptions={{
headerStyle: {
backgroundColor: theme.colors.primaryContainer,
borderBottomWidth: 0,
},
headerTintColor: theme.colors.primary,
}}
>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="location/[id]"
options={({ route }) => ({
title: "Lokalizacja",
headerBackTitle: "Powrót",
headerRight: () => (
<Link
href={`location/edit/${route.params.id}`}
asChild
style={{ marginRight: 11 }}
>
<Ionicons name="pencil" color={theme.colors.primary} size={24} />
</Link>
),
})}
<Link
href={`location/edit/${route.params.id}`}
asChild
style={{ marginRight: 11 }}
>
<Ionicons
name="pencil"
color={theme.colors.primary}
size={24}
/>
</Link>
),
})}
/>
<Stack.Screen name="location/edit/[id]" options={({ route }) =>({ title: "Edycja",
headerRight: () => (
<Ionicons name="trash-bin" color={theme.colors.primary} size={24} onPress={() => handleDelete(route.params.id)}/>
), })}
<Stack.Screen
name="location/edit/[id]"
options={({ route }) => ({
title: "Edycja",
headerRight: () => (
<Ionicons
name="trash-bin"
color={theme.colors.primary}
size={24}
onPress={() => handleDelete(route.params.id)}
/>
),
})}
/>
</Stack>
</Stack>
</PaperProvider>
);
}

View File

@@ -1,15 +1,25 @@
import { View, ScrollView, StyleSheet } from 'react-native';
import { Text, Card } from 'react-native-paper';
import { useLocalSearchParams } from 'expo-router';
import useLocationStore from '@/store';
import { View, ScrollView, StyleSheet } from "react-native";
import { useEffect } from "react";
import { useTheme, Text, Card } from "react-native-paper";
import { useLocalSearchParams } from "expo-router";
import useLocationStore from "@/locationStore";
export default function Location() {
const theme = useTheme();
const { id } = useLocalSearchParams();
const location = useLocationStore((state) =>
state.locations.find((loc) => loc.id == id)
);
const getLocation = useLocationStore((state) => state.getLocation);
const fetchLocations = useLocationStore((state) => state.fetchLocations);
const loading = useLocationStore((state) => state.loading);
if (!location) {
const location = getLocation(id);
useEffect(() => {
if (!location && !loading) {
fetchLocations();
}
}, [location, loading, fetchLocations]);
if (loading || !location) {
return (
<View style={styles.container}>
<Text style={styles.text}>Brak lokalizacji - {id}</Text>
@@ -18,10 +28,13 @@ export default function Location() {
}
return (
<View style={styles.container}>
<View style={[styles.container, {backgroundColor: theme.colors.background}]}>
<ScrollView>
<Card style={{ margin: 10 }}>
<Card.Cover style={{ marginBottom: 10 }} source={{ uri: location.image }} />
<Card.Cover
style={{ marginBottom: 10 }}
source={location.imageSource}
/>
<Card.Content style={{ marginBottom: 10 }}>
<Text variant="headlineLarge" style={{ marginBottom: 10 }}>
{location.name}
@@ -42,6 +55,9 @@ export default function Location() {
<Text variant="bodyMedium" style={{ marginBottom: 10 }}>
Ludność: {location.population} osób
</Text>
<Text variant="bodyMedium" style={{ marginBottom: 10 }}>
Współrzedne geograficzne: {location.latitude}, {location.longitude}
</Text>
</Card.Content>
</Card>
</ScrollView>
@@ -52,11 +68,10 @@ export default function Location() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: "#25292e",
justifyContent: "center",
},
text: {
color: '#fff',
color: "#fff",
},
});
});

View File

@@ -1,142 +1,197 @@
import { useState, useEffect } from 'react';
import { StyleSheet, Platform, ScrollView, KeyboardAvoidingView } from 'react-native';
import { TextInput, Button, Snackbar } from 'react-native-paper';
import { useLocalSearchParams, useRouter } from 'expo-router';
import useLocationStore from '@/store';
import {useState, useEffect} from "react";
import {
StyleSheet,
Platform,
ScrollView,
KeyboardAvoidingView,
View,
ActivityIndicator,
} from "react-native";
import {TextInput, Button, Snackbar, useTheme} from "react-native-paper";
import {useLocalSearchParams, useRouter} from "expo-router";
import useLocationStore from "@/locationStore";
export default function EditLocation() {
const { id } = useLocalSearchParams();
const router = useRouter();
const location = useLocationStore((state) =>
state.locations.find((loc) => loc.id == id)
);
const updateLocation = useLocationStore((state) => state.updateLocation);
const theme = useTheme();
const {id} = useLocalSearchParams();
const router = useRouter();
const [loading, setLoading] = useState(true);
const [formData, setFormData] = useState({
name: "",
description: "",
image: "",
area: "",
population: "",
longitude: "",
latitude: "",
});
const [formData, setFormData] = useState({
name: '',
description: '',
image: '',
area: '',
population: '',
});
const [message, setMessage] = useState("");
const [visible, setVisible] = useState(false);
const [message, setMessage] = useState('');
const [visible, setVisible] = useState(false);
const updateLocation = useLocationStore((state) => state.updateLocation);
const getLocation = useLocationStore((state) => state.getLocation);
useEffect(() => {
if (location) {
setFormData({
name: location.name,
description: location.description,
image: location.image,
area: location.area.toString(),
population: location.population.toString(),
});
useEffect(() => {
const data = getLocation(id);
if (data) {
setFormData({
name: data.name,
description: data.description,
image: data.image,
area: data.area.toString(),
population: data.population.toString(),
longitude: data.longitude.toString(),
latitude: data.latitude.toString(),
});
}
setLoading(false);
}, [id, getLocation]);
const handleEditLocation = async () => {
if (
formData.name &&
formData.description &&
formData.image &&
formData.area &&
formData.population &&
formData.longitude &&
formData.latitude
) {
// console.log("Form data:", formData);
const response = await updateLocation(id, {
name: formData.name,
description: formData.description,
image: formData.image,
area: parseFloat(formData.area),
population: parseInt(formData.population, 10),
longitude: parseFloat(formData.longitude),
latitude: parseFloat(formData.latitude),
});
if (response) {
setMessage("Lokalizacja została zaktualizowana!");
setVisible(true);
// while (router.canGoBack()) {
// Pop from stack until one element is left
// router.back();
// }
setTimeout(() => {
router.replace("/");
}, 300); // Replace the last remaining stack element
} else {
setMessage("Wystąpił błąd podczas aktualizacji lokalizacji!");
setVisible(true);
}
} else {
setMessage("Wypełnij wszystkie pola!");
setVisible(true);
}
};
if (loading) {
return (
<View
style={[styles.container, {backgroundColor: theme.colors.background}]}
>
<ActivityIndicator size="large" color={theme.colors.primary}/>
</View>
);
}
}, [location]);
const handleEditLocation = () => {
if (
formData.name &&
formData.description &&
formData.image &&
formData.area &&
formData.population
) {
updateLocation(id, {
name: formData.name,
description: formData.description,
image: formData.image,
area: parseFloat(formData.area),
population: parseInt(formData.population, 10),
});
setMessage('Lokalizacja została zaktualizowana!');
setVisible(true);
// setTimeout(() => {
// router.replace(`/location/${id}`);
// }, 2000);
} else {
setMessage('Wypełnij wszystkie pola!');
setVisible(true);
}
};
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView contentContainerStyle={styles.container}>
<Snackbar
visible={visible}
onDismiss={() => setVisible(false)}
duration={3000}
return (
<KeyboardAvoidingView
style={{flex: 1}}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
{message}
</Snackbar>
<TextInput
mode="outlined"
label="Nazwa"
placeholder="Wpisz nazwę"
style={{ margin: 10, width: '100%' }}
value={formData.name}
onChangeText={(e) => setFormData({ ...formData, name: e })}
/>
<TextInput
mode="outlined"
label="Opis"
placeholder="Wpisz opis"
style={{ margin: 10, width: '100%' }}
multiline={true}
value={formData.description}
onChangeText={(e) => setFormData({ ...formData, description: e })}
/>
<TextInput
mode="outlined"
label="Link do zdjęcia"
multiline={true}
placeholder="Wpisz link do zdjęcia"
style={{ margin: 10, width: '100%' }}
value={formData.image}
onChangeText={(e) => setFormData({ ...formData, image: e })}
/>
<TextInput
mode="outlined"
label="Powierzchnia"
placeholder="Wpisz powierzchnię"
style={{ margin: 10, width: '100%' }}
value={formData.area}
onChangeText={(e) => setFormData({ ...formData, area: e })}
/>
<TextInput
mode="outlined"
label="Ludność"
placeholder="Wpisz liczbę ludności"
style={{ margin: 10, width: '100%' }}
value={formData.population}
keyboardType="numeric"
onChangeText={(e) => setFormData({ ...formData, population: e })}
/>
<Button
style={{ margin: 10, width: '100%' }}
icon="plus-circle-outline"
mode={'contained'}
onPress={handleEditLocation}
>
Edytuj
</Button>
</ScrollView>
</KeyboardAvoidingView>
);
<ScrollView contentContainerStyle={styles.container}>
<Snackbar
visible={visible}
onDismiss={() => setVisible(false)}
duration={3000}
>
{message}
</Snackbar>
<TextInput
mode="outlined"
label="Nazwa"
placeholder="Wpisz nazwę"
style={{margin: 10, width: "100%"}}
value={formData.name}
onChangeText={(e) => setFormData({...formData, name: e})}
/>
<TextInput
mode="outlined"
label="Opis"
placeholder="Wpisz opis"
style={{margin: 10, width: "100%"}}
multiline={true}
value={formData.description}
onChangeText={(e) => setFormData({...formData, description: e})}
/>
<TextInput
mode="outlined"
label="Link do zdjęcia"
multiline={true}
placeholder="Wpisz link do zdjęcia"
style={{margin: 10, width: "100%"}}
value={formData.image}
onChangeText={(e) => setFormData({...formData, image: e})}
/>
<TextInput
mode="outlined"
label="Powierzchnia"
placeholder="Wpisz powierzchnię"
style={{margin: 10, width: "100%"}}
value={formData.area}
keyboardType="numbers-and-punctuation"
onChangeText={(e) => setFormData({...formData, area: e})}
/>
<TextInput
mode="outlined"
label="Ludność"
placeholder="Wpisz liczbę ludności"
style={{margin: 10, width: "100%"}}
value={formData.population}
keyboardType="numeric"
onChangeText={(e) => setFormData({...formData, population: e})}
/>
<TextInput
mode="outlined"
label="Długość geograficzna"
placeholder="Wpisz długość geograficzną"
style={{margin: 10, width: "100%"}}
value={formData.longitude}
keyboardType="numbers-and-punctuation"
onChangeText={(e) => setFormData({...formData, longitude: e})}
/>
<TextInput
mode="outlined"
label="Szerokość geograficzna"
placeholder="Wpisz szerokość geograficzną"
style={{margin: 10, width: "100%"}}
value={formData.latitude}
keyboardType="numbers-and-punctuation"
onChangeText={(e) => setFormData({...formData, latitude: e})}
/>
<Button
style={{margin: 10, width: "100%"}}
icon="plus-circle-outline"
mode={"contained"}
onPress={handleEditLocation}
>
Edytuj
</Button>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#25292e',
justifyContent: 'flex-start',
alignItems: 'center',
},
});
container: {
backgroundColor: "#25292e",
justifyContent: "flex-start",
alignItems: "center",
flexGrow: 1,
},
});

8
babel.config.js Normal file
View File

@@ -0,0 +1,8 @@
module.exports = {
presets: [
'@babel/preset-env',
'babel-preset-expo',
['@babel/preset-react', {runtime: 'automatic'}],
['@babel/preset-flow', {all: true}],
]
};

View File

@@ -1,44 +0,0 @@
export const locations = [
{
"id": 1,
"name": "Warszawa",
"description": "Stolica Polski, położona w centralnej części kraju. Warszawa jest największym miastem w Polsce, znanym z bogatej historii, kultury i architektury. Warto zobaczyć Zamek Królewski, Stare Miasto oraz Muzeum Powstania Warszawskiego. Miasto oferuje wiele atrakcji turystycznych, w tym parki, muzea i teatry. Warszawa jest również ważnym ośrodkiem gospodarczym i kulturalnym.",
"image": "https://www.niesamowitapolska.eu/images/mazowieckie/warszawa/38607734_m.jpg",
"area": 517.24,
"population": 1790658,
},
{
"id": 2,
"name": "Kielce",
"description": "Stolica województwa świętokrzyskiego, położona w centralnej Polsce. Kielce to miasto w centralnej Polsce, znane z pięknych krajobrazów i bogatej historii. Warto odwiedzić Kielecki Park Etnograficzny, Muzeum Zabawek oraz Katedrę Wniebowzięcia Najświętszej Maryi Panny. Kielce są również znane z licznych festiwali i wydarzeń kulturalnych. Miasto oferuje wiele atrakcji turystycznych, w tym parki, muzea i teatry.",
"image": "https://as2.ftcdn.net/jpg/05/42/90/67/1000_F_542906717_cf5i6HeCJsPluuH5tqq5MbsSdfpopmtT.webp",
"area": 109.4,
"population": 196000,
},
{
"id": 3,
"name": "Kraków",
"description": "Miasto położone w południowej Polsce, znane z bogatej historii i kultury. Kraków to jedno z najstarszych i najpiękniejszych miast w Polsce, znane z bogatej historii, kultury i architektury. Warto zobaczyć Wawel, Stare Miasto oraz Sukiennice. Kraków jest również znany z licznych festiwali, wydarzeń kulturalnych oraz tradycji kulinarnych. Miasto oferuje wiele atrakcji turystycznych, w tym parki, muzea i teatry.",
"image": "https://nawakacje.eu/wp-content/uploads/2020/12/krakow-atrakcje.jpg",
"area": 326.85,
"population": 779115,
},
{
"id": 4,
"name": "Wrocław",
"description": "Miasto w zachodniej Polsce, znane z pięknych mostów i architektury. Wrocław to miasto w zachodniej Polsce, znane z pięknych mostów, architektury i bogatej historii. Warto odwiedzić Ostrów Tumski, Rynek oraz Halę Stulecia. Wrocław jest również znany z licznych festiwali, wydarzeń kulturalnych oraz tradycji kulinarnych. Miasto oferuje wiele atrakcji turystycznych, w tym parki, muzea i teatry.",
"image": "https://backend.triverna.pl/blog/wp-content/uploads/2023/10/Wroclaw-z-lotu-ptaka-1.jpeg",
"area": 292.82,
"population": 640000,
},
{
"id": 5,
"name": "Poznań",
"description": "Miasto w zachodniej Polsce, znane z tradycji piwowarskich. Poznań to miasto w zachodniej Polsce, znane z tradycji piwowarskich, bogatej historii i kultury. Warto odwiedzić Stary Rynek, Katedrę Poznańską oraz Ostrów Tumski. Poznań jest również znany z licznych festiwali, wydarzeń kulturalnych oraz tradycji kulinarnych. Miasto oferuje wiele atrakcji turystycznych, w tym parki, muzea i teatry.",
"image":"https://wycieczkoteka.pl/images/2024/11/07/poznan-co-zwiedzic.jpg",
"area": 262.82,
"population": 535802,
}
]

79
locationStore.js Normal file
View File

@@ -0,0 +1,79 @@
import { create } from "zustand";
import * as api from "@/api/locations";
const useLocationStore = create((set, get) => ({
locations: [],
loading: false,
error: null,
fetchLocations: async () => {
set({ loading: true, error: null });
try {
const data = await api.listLocations();
set({ locations: data, loading: false });
} catch (error) {
set({ error, loading: false });
}
},
addLocation: async (location) => {
set({ loading: true, error: null });
try {
const newLoc = await api.addLocation(location);
const normalizedLoc = {
...newLoc,
imageSource: api.normalizeImageSource(newLoc.image)
};
set((state) => ({
locations: [...state.locations, normalizedLoc],
loading: false,
}));
return normalizedLoc;
} catch (error) {
set({ error, loading: false });
return null;
}
},
updateLocation: async (id, location) => {
set({ loading: true, error: null });
try {
const updated = await api.updateLocation(id, location);
set((state) => ({
locations: state.locations.map((loc) =>
loc.id === id ? updated : loc
),
loading: false,
}));
return updated;
} catch (error) {
set({ error, loading: false });
return null;
}
},
deleteLocation: async (id) => {
set({ loading: true, error: null });
try {
const deleted = await api.deleteLocation(id);
set((state) => ({
locations: state.locations.filter((loc) => loc.id !== id),
loading: false,
}));
if(deleted) {
return true;
}
} catch (error) {
set({ error, loading: false });
return false;
}
},
getLocation: (id) => {
return get().locations.find((loc) => String(loc.id) === String(id));
},
}));
export default useLocationStore;

8973
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,15 +6,19 @@
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
"web": "expo start --web",
"test": "jest"
},
"dependencies": {
"@expo/metro-runtime": "~5.0.4",
"axios": "^1.9.0",
"expo": "^53.0.0",
"expo-constants": "~17.1.6",
"expo-linking": "~7.1.4",
"expo-router": "~5.0.6",
"expo-router": "~5.0.7",
"expo-status-bar": "~2.2.3",
"expo-image-picker": "~16.1.4",
"expo-location": "~18.1.5",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.2",
@@ -25,8 +29,14 @@
"zustand": "^5.0.3"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/core": "^7.27.1",
"@babel/preset-env": "^7.27.2",
"@babel/preset-flow": "^7.27.1",
"@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
"@types/react": "~19.0.10",
"babel-jest": "^29.7.0",
"jest": "^29.7.0",
"typescript": "^5.3.3"
},
"private": true

View File

@@ -1,22 +0,0 @@
import { create } from 'zustand';
import { locations as initialLocations } from '@/data/locations';
const useLocationStore = create((set) => ({
locations: initialLocations,
addLocation: (newLocation) =>
set((state) => ({
locations: [newLocation, ...state.locations],
})),
updateLocation: (id, updatedData) =>
set((state) => ({
locations: state.locations.map((loc) =>
loc.id == id ? { ...loc, ...updatedData } : loc
),
})),
deleteLocation: (id) =>
set((state) => ({
locations: state.locations.filter((loc) => loc.id != id),
})),
}));
export default useLocationStore;