feat: Add Saved Coordinates

Signed-off-by: SindreKjelsrud <sindre@kjelsrud.dev>
This commit is contained in:
Sid 2025-07-05 17:55:50 +02:00
parent 3cb81d2db2
commit 2764f555de
Signed by: sidski
GPG key ID: D2BBDF3EDE6BA9A6
7 changed files with 435 additions and 16 deletions

View file

@ -15,6 +15,7 @@ export default function RootLayout() {
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="nether-portal-calculator" options={{ headerShown: false }} />
<Stack.Screen name="todo" options={{ headerShown: false }} />
<Stack.Screen name="saved-coordinates" options={{ headerShown: false }} />
</Stack>
);
}

View file

@ -50,7 +50,7 @@ export default function Index() {
{/* Saved Coordinates */}
<View style={styles.gridItem}>
<Link href="/coordinates" asChild style={styles.touchableWrapper}>
<Link href="/saved-coordinates" asChild style={styles.touchableWrapper}>
<TouchableOpacity style={styles.touchableWrapper}>
<ImageBackground source={coordinatesImage} style={styles.imageBackground} resizeMode="cover">
<View style={styles.overlay}>

278
app/saved-coordinates.tsx Normal file
View file

@ -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<Coordinate[]>(
[]
);
// State for new coordinate input fields
const [name, setName] = useState<string>("");
const [xCoord, setXCoord] = useState<string>("");
const [yCoord, setYCoord] = useState<string>("");
const [zCoord, setZCoord] = useState<string>("");
// 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 (
<View style={styles.converterScreenRoot}>
<ImageBackground
source={converterBgImage}
style={styles.converterBackgroundImage}
resizeMode="cover"
>
<View style={styles.converterBackgroundOverlay} />
</ImageBackground>
<SafeAreaView style={styles.converterContentWrapper}>
<KeyboardAvoidingView
style={styles.converterContainer}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<ScrollView
contentContainerStyle={styles.converterScrollContent}
>
{/* Custom Back Button */}
<TouchableOpacity
onPress={() => router.back()}
style={styles.backButton}
>
<Text style={styles.backButtonText}>
Go Back
</Text>
</TouchableOpacity>
<Text style={styles.converterTitle}>
Saved Coordinates
</Text>
{/* Coordinate Input Section */}
<View style={styles.coordinateInputSection}>
<TextInput
style={styles.coordinateInputField}
placeholder="Location Name (Optional)"
placeholderTextColor="#999"
value={name}
onChangeText={setName}
returnKeyType="done"
/>
<View style={styles.coordinateValuesInputRow}>
<TextInput
style={styles.coordinateValueInput}
placeholder="X"
placeholderTextColor="#999"
keyboardType="default"
value={xCoord}
onChangeText={setXCoord}
returnKeyType="done"
/>
<TextInput
style={styles.coordinateValueInput}
placeholder="Y"
placeholderTextColor="#999"
keyboardType="default"
value={yCoord}
onChangeText={setYCoord}
returnKeyType="done"
/>
<TextInput
style={styles.coordinateValueInput}
placeholder="Z"
placeholderTextColor="#999"
keyboardType="default"
value={zCoord}
onChangeText={setZCoord}
returnKeyType="done"
/>
</View>
<TouchableOpacity
onPress={handleAddCoordinate}
style={styles.addCoordinateButton}
>
<Text
style={styles.addCoordinateButtonText}
>
Save Coordinate
</Text>
</TouchableOpacity>
</View>
{/* Coordinate List Display */}
<View style={styles.savedCoordinatesListContainer}>
{savedCoordinates.length === 0 ? (
<Text style={styles.placeholderText}>
No coordinates saved yet.
</Text>
) : (
savedCoordinates.map((coord) => (
<View
key={coord.id}
style={styles.coordinateListItem}
>
<View style={styles.coordinateTextInfo}>
<Text style={styles.coordinateListName}>
{coord.name}
</Text>
<Text style={styles.coordinateListValues}>
X: {coord.x}, Y: {coord.y}, Z:{" "}
{coord.z}
</Text>
</View>
<TouchableOpacity
onPress={() =>
handleDeleteCoordinate(coord.id)
}
style={styles.removeCoordinateButton}
>
<Text
style={styles.removeCoordinateButtonText}
>
X
</Text>
</TouchableOpacity>
</View>
))
)}
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
</View>
);
}

View file

@ -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",
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"
}
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

34
package-lock.json generated
View file

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

View file

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