minecraft-tools/app/saved-coordinates.tsx
SindreKjelsrud 2764f555de
feat: Add Saved Coordinates
Signed-off-by: SindreKjelsrud <sindre@kjelsrud.dev>
2025-07-05 17:55:50 +02:00

278 lines
No EOL
10 KiB
TypeScript

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>
);
}