diff --git a/app/_layout.tsx b/app/_layout.tsx index 9ac8117..f454520 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -15,6 +15,7 @@ export default function RootLayout() { + ); } \ No newline at end of file diff --git a/app/index.tsx b/app/index.tsx index 954f052..48c2a94 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -50,7 +50,7 @@ export default function Index() { {/* Saved Coordinates */} - + diff --git a/app/saved-coordinates.tsx b/app/saved-coordinates.tsx new file mode 100644 index 0000000..b0cb463 --- /dev/null +++ b/app/saved-coordinates.tsx @@ -0,0 +1,278 @@ +import AsyncStorage from "@react-native-async-storage/async-storage"; +import { useRouter } from "expo-router"; +import React, { useEffect, useState } from "react"; +import { + Alert, + ImageBackground, + KeyboardAvoidingView, + Platform, + ScrollView, + Text, + TextInput, + TouchableOpacity, + View +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { styles } from "./styles"; + +const converterBgImage = require("../assets/images/saved-coordinates.jpg"); + +// Define a type for a single coordinate entry +interface Coordinate { + id: string; + name: string; + x: string; + y: string; + z: string; +} + +export default function CoordinateConverter() { + const router = useRouter(); + + // State for the list of saved coordinates + const [savedCoordinates, setSavedCoordinates] = useState( + [] + ); + // State for new coordinate input fields + const [name, setName] = useState(""); + const [xCoord, setXCoord] = useState(""); + const [yCoord, setYCoord] = useState(""); + const [zCoord, setZCoord] = useState(""); + + // Key for AsyncStorage + const STORAGE_KEY = "@saved_minecraft_coordinates"; + + // Persistence Logic + // Load coordinates from AsyncStorage on component mount + useEffect(() => { + loadCoordinates(); + }, []); + + // Save coordinates to AsyncStorage + useEffect(() => { + saveCoordinates(); + }, [savedCoordinates]); + + const loadCoordinates = async () => { + try { + const jsonValue = await AsyncStorage.getItem(STORAGE_KEY); + if (jsonValue != null) { + const parsedValue = JSON.parse(jsonValue) as Coordinate[]; + if (Array.isArray(parsedValue)) { + setSavedCoordinates(parsedValue); + } else { + console.warn( + "Loaded data is not an array, initializing with empty array." + ); + setSavedCoordinates([]); + } + } + } catch (e) { + console.error("Failed to load coordinates:", e); + Alert.alert( + "Error", + "Failed to load saved coordinates. Please try again." + ); + } + }; + + const saveCoordinates = async () => { + try { + const jsonValue = JSON.stringify(savedCoordinates); + await AsyncStorage.setItem(STORAGE_KEY, jsonValue); + } catch (e) { + console.error("Failed to save coordinates:", e); + Alert.alert( + "Error", + "Failed to save coordinates. Please try again." + ); + } + }; + + // Coordinate Logic + const handleAddCoordinate = () => { + const trimmedName = name.trim(); + const trimmedX = xCoord.trim(); + const trimmedY = yCoord.trim(); + const trimmedZ = zCoord.trim(); + + const integerRegex = /^-?\d+$/; + + if ( + !integerRegex.test(trimmedX) || + !integerRegex.test(trimmedY) || + !integerRegex.test(trimmedZ) + ) { + Alert.alert( + "Invalid Input", + "Please enter valid integer numbers for X, Y, and Z (e.g., 100, -50). Decimals or non-numeric characters are not allowed." + ); + return; + } + + const newCoordinate: Coordinate = { + id: Date.now().toString(), + name: trimmedName || `Unnamed Location ${savedCoordinates.length + 1}`, + x: trimmedX, + y: trimmedY, + z: trimmedZ + }; + + setSavedCoordinates((prevCoords) => [...prevCoords, newCoordinate]); + + // Clear input fields + setName(""); + setXCoord(""); + setYCoord(""); + setZCoord(""); + }; + + const handleDeleteCoordinate = (id: string) => { + Alert.alert( + "Delete Coordinate", + "Are you sure you want to delete this coordinate?", + [ + { + text: "Cancel", + style: "cancel" + }, + { + text: "Delete", + onPress: () => { + setSavedCoordinates((prevCoords) => + prevCoords.filter((coord) => coord.id !== id) + ); + }, + style: "destructive" + } + ], + { cancelable: true } + ); + }; + + return ( + + + + + + + + + {/* Custom Back Button */} + router.back()} + style={styles.backButton} + > + + ← Go Back + + + + + Saved Coordinates + + + {/* Coordinate Input Section */} + + + + + + + + + + Save Coordinate + + + + + {/* Coordinate List Display */} + + {savedCoordinates.length === 0 ? ( + + No coordinates saved yet. + + ) : ( + savedCoordinates.map((coord) => ( + + + + {coord.name} + + + X: {coord.x}, Y: {coord.y}, Z:{" "} + {coord.z} + + + + handleDeleteCoordinate(coord.id) + } + style={styles.removeCoordinateButton} + > + + X + + + + )) + )} + + + + + + ); +} \ No newline at end of file diff --git a/app/styles.ts b/app/styles.ts index ba24882..721fdb1 100644 --- a/app/styles.ts +++ b/app/styles.ts @@ -1,4 +1,3 @@ -// styles.ts import { StyleSheet } from "react-native"; export const styles = StyleSheet.create({ @@ -54,20 +53,17 @@ export const styles = StyleSheet.create({ fontFamily: "Minecraft", }, backButton: { - alignSelf: "flex-start", - marginTop: 0, - marginBottom: 20, - paddingVertical: 8, - paddingHorizontal: 15, - backgroundColor: "rgba(106, 90, 205, 0.7)", + marginTop: 20, + padding: 10, + backgroundColor: "rgba(255,255,255,0.2)", borderRadius: 8, - marginLeft: 0, + alignSelf: "flex-start" }, backButtonText: { - color: "#fff", - fontSize: 16, - fontWeight: "bold", - fontFamily: "Minecraft", + color: "#fff", + fontSize: 16, + fontWeight: "bold", + fontFamily: "Minecraft" }, // --- Nether Portal Calculator --- @@ -89,12 +85,12 @@ export const styles = StyleSheet.create({ converterContainer: { flex: 1, width: "100%", + paddingHorizontal: 20 }, converterScrollContent: { flexGrow: 1, padding: 20, - alignItems: "center", - width: "100%", + paddingBottom: 20 }, converterTitle: { fontSize: 44, @@ -249,4 +245,113 @@ export const styles = StyleSheet.create({ textShadowRadius: 5, fontFamily: "Minecraft", }, + + // --- Saved Coordinates --- + coordinateInputSection: { + flexDirection: "column", + backgroundColor: "rgba(30,30,30,0.8)", + borderRadius: 10, + padding: 15, + marginBottom: 20, + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 5, + elevation: 8 + }, + coordinateInputField: { + flex: 1, + backgroundColor: "#444", + color: "#eee", + padding: 12, + borderRadius: 8, + fontSize: 16, + marginBottom: 10, + height: 50, + fontFamily: "Minecraft" + }, + coordinateValuesInputRow: { + flexDirection: "row", + justifyContent: "space-between", + marginBottom: 10 + }, + coordinateValueInput: { + flex: 1, + backgroundColor: "#444", + color: "#eee", + padding: 12, + borderRadius: 8, + fontSize: 16, + marginHorizontal: 5, + height: 50, + fontFamily: "Minecraft" + }, + addCoordinateButton: { + backgroundColor: "#2196F3", + padding: 15, + borderRadius: 8, + alignItems: "center", + justifyContent: "center" + }, + addCoordinateButtonText: { + color: "#fff", + fontSize: 18, + fontWeight: "bold", + fontFamily: "Minecraft" + }, + savedCoordinatesListContainer: { + backgroundColor: "rgba(30,30,30,0.8)", + borderRadius: 10, + padding: 15, + minHeight: 100, + shadowColor: "#000", + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 5, + elevation: 8 + }, + coordinateListItem: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + backgroundColor: "#555", + borderRadius: 8, + padding: 15, + marginBottom: 10, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.2, + shadowRadius: 3, + elevation: 3 + }, + coordinateTextInfo: { + flex: 1, + marginRight: 15, + fontFamily: "Minecraft" + }, + coordinateListName: { + color: "#FFD700", + fontSize: 18, + fontWeight: "bold", + marginBottom: 5, + fontFamily: "Minecraft" + }, + coordinateListValues: { + color: "#ADD8E6", + fontSize: 16 + }, + removeCoordinateButton: { + backgroundColor: "#FF5252", + width: 35, + height: 35, + borderRadius: 17.5, + justifyContent: "center", + alignItems: "center" + }, + removeCoordinateButtonText: { + color: "#fff", + fontSize: 18, + fontWeight: "bold", + fontFamily: "Minecraft" + } }); \ No newline at end of file diff --git a/assets/images/saved-coordinates.jpg b/assets/images/saved-coordinates.jpg new file mode 100644 index 0000000..4d95d5b Binary files /dev/null and b/assets/images/saved-coordinates.jpg differ diff --git a/package-lock.json b/package-lock.json index c3ff4de..b606855 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@expo/vector-icons": "^14.1.0", + "@react-native-async-storage/async-storage": "2.1.2", "@react-navigation/bottom-tabs": "^7.3.10", "@react-navigation/elements": "^2.3.8", "@react-navigation/native": "^7.1.6", @@ -2756,6 +2757,18 @@ } } }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.1.2.tgz", + "integrity": "sha512-dvlNq4AlGWC+ehtH12p65+17V0Dx7IecOWl6WanF2ja38O1Dcjjvn7jVzkUHJ5oWkQBlyASurTPlTHgKXyYiow==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.79.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.5.tgz", @@ -7665,6 +7678,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -8651,6 +8673,18 @@ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", diff --git a/package.json b/package.json index 87c352f..d1ec7ea 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "react-native-safe-area-context": "^5.5.1", "react-native-screens": "^4.11.1", "react-native-web": "~0.20.0", - "react-native-webview": "13.13.5" + "react-native-webview": "13.13.5", + "@react-native-async-storage/async-storage": "2.1.2" }, "devDependencies": { "@babel/core": "^7.25.2",