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",