feat: Add Saved Coordinates
Signed-off-by: SindreKjelsrud <sindre@kjelsrud.dev>
This commit is contained in:
parent
3cb81d2db2
commit
2764f555de
7 changed files with 435 additions and 16 deletions
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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
278
app/saved-coordinates.tsx
Normal 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>
|
||||
);
|
||||
}
|
133
app/styles.ts
133
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"
|
||||
}
|
||||
});
|
BIN
assets/images/saved-coordinates.jpg
Normal file
BIN
assets/images/saved-coordinates.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 354 KiB |
34
package-lock.json
generated
34
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Reference in a new issue