feat: Add coordinate-converter

Signed-off-by: SindreKjelsrud <sindre@kjelsrud.dev>
This commit is contained in:
Sid 2025-07-05 14:05:54 +02:00
parent 978e60c6d3
commit de79281681
Signed by: sidski
GPG key ID: D2BBDF3EDE6BA9A6
3 changed files with 312 additions and 2 deletions

View file

@ -4,6 +4,7 @@ export default function RootLayout() {
return ( return (
<Stack> <Stack>
<Stack.Screen name="index" options={{ headerShown: false }} /> <Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="coordinate-converter" options={{ headerShown: false }} />
</Stack> </Stack>
); );
} }

View file

@ -0,0 +1,193 @@
import { useRouter } from "expo-router";
import { useCallback, useState } from "react";
import {
ImageBackground,
KeyboardAvoidingView,
Platform,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { styles } from "./styles";
const router = useRouter();
// Define your background image URI
const converterBgImage = {
uri: "https://s1.qwant.com/thumbr/474x296/1/c/30947af0647acb7b3ef1eb2b5697b5dde49d5468269c918e41bb86286b007e/OIP.MpGUIDhV0CzgCK1mQYFbHQHaEo.jpg?u=https%3A%2F%2Ftse.mm.bing.net%2Fth%2Fid%2FOIP.MpGUIDhV0CzgCK1mQYFbHQHaEo%3Fr%3D0%26pid%3DApi&q=0&b=1&p=0&a=0",
};
export default function CoordinateConverter() {
const [overworldX, setOverworldX] = useState("");
const [overworldZ, setOverworldZ] = useState("");
const [netherX, setNetherX] = useState("");
const [netherZ, setNetherZ] = useState("");
const safeParseFloat = (value: string) => {
const num = parseFloat(value);
return isNaN(num) ? undefined : num;
};
const convertOWToNether = useCallback(
(xStr: string, zStr: string) => {
const x = safeParseFloat(xStr);
const z = safeParseFloat(zStr);
if (x !== undefined && z !== undefined) {
setNetherX(Math.floor(x / 8).toString());
setNetherZ(Math.floor(z / 8).toString());
} else {
setNetherX("");
setNetherZ("");
}
},
[]
);
const convertNetherToOW = useCallback(
(xStr: string, zStr: string) => {
const x = safeParseFloat(xStr);
const z = safeParseFloat(zStr);
if (x !== undefined && z !== undefined) {
setOverworldX(Math.floor(x * 8).toString());
setOverworldZ(Math.floor(z * 8).toString());
} else {
setOverworldX("");
setOverworldZ("");
}
},
[]
);
const handleOverworldXChange = useCallback(
(text: string) => {
setOverworldX(text);
convertOWToNether(text, overworldZ);
},
[overworldZ, convertOWToNether]
);
const handleOverworldZChange = useCallback(
(text: string) => {
setOverworldZ(text);
convertOWToNether(overworldX, text);
},
[overworldX, convertOWToNether]
);
const handleNetherXChange = useCallback(
(text: string) => {
setNetherX(text);
convertNetherToOW(text, netherZ);
},
[netherZ, convertNetherToOW]
);
const handleNetherZChange = useCallback(
(text: string) => {
setNetherZ(text);
convertNetherToOW(netherX, text);
},
[netherX, convertNetherToOW]
);
const handleClear = useCallback(() => {
setOverworldX("");
setOverworldZ("");
setNetherX("");
setNetherZ("");
}, []);
return (
<SafeAreaView style={styles.converterSafeArea}>
<ImageBackground
source={converterBgImage}
style={styles.converterBackgroundImage}
resizeMode="cover"
>
<View style={styles.converterBackgroundOverlay}>
<KeyboardAvoidingView
style={styles.converterContainer}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<TouchableOpacity
onPress={() => router.back()} // Correct!
style={styles.backButton}
>
<Text style={styles.backButtonText}> Go Back</Text>
</TouchableOpacity>
<ScrollView contentContainerStyle={styles.converterScrollContent}>
<Text style={styles.converterTitle}>Coordinate Converter</Text>
{/* Overworld Row */}
<View style={styles.dimensionContainer}>
<Text style={styles.dimensionTitle}>Overworld (X, Z)</Text>
<View style={styles.coordinatesRow}>
<View style={styles.coordInputGroup}>
<Text style={styles.coordInputLabel}>X:</Text>
<TextInput
style={styles.coordTextInput}
keyboardType="numeric"
placeholder="Enter X"
placeholderTextColor="#ccc"
value={overworldX}
onChangeText={handleOverworldXChange}
/>
</View>
<View style={styles.coordInputGroup}>
<Text style={styles.coordInputLabel}>Z:</Text>
<TextInput
style={styles.coordTextInput}
keyboardType="numeric"
placeholder="Enter Z"
placeholderTextColor="#ccc"
value={overworldZ}
onChangeText={handleOverworldZChange}
/>
</View>
</View>
</View>
{/* Nether Row */}
<View style={styles.dimensionContainer}>
<Text style={styles.dimensionTitle}>Nether (X, Z)</Text>
<View style={styles.coordinatesRow}>
<View style={styles.coordInputGroup}>
<Text style={styles.coordInputLabel}>X:</Text>
<TextInput
style={styles.coordTextInput}
keyboardType="numeric"
placeholder="Enter X"
placeholderTextColor="#ccc"
value={netherX}
onChangeText={handleNetherXChange}
/>
</View>
<View style={styles.coordInputGroup}>
<Text style={styles.coordInputLabel}>Z:</Text>
<TextInput
style={styles.coordTextInput}
keyboardType="numeric"
placeholder="Enter Z"
placeholderTextColor="#ccc"
value={netherZ}
onChangeText={handleNetherZChange}
/>
</View>
</View>
</View>
<TouchableOpacity
style={styles.clearButton}
onPress={handleClear}
>
<Text style={styles.clearButtonText}>Clear All</Text>
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
</View>
</ImageBackground>
</SafeAreaView>
);
}

View file

@ -2,11 +2,12 @@
import { StyleSheet } from "react-native"; import { StyleSheet } from "react-native";
export const styles = StyleSheet.create({ export const styles = StyleSheet.create({
container: { // --- Global / Reusable Styles (or main index page specific) ---
appContainer: {
flex: 1, flex: 1,
backgroundColor: "#2e2e2e", backgroundColor: "#2e2e2e",
}, },
title: { mainTitle: {
fontSize: 38, fontSize: 38,
fontWeight: "bold", fontWeight: "bold",
color: "#fff", color: "#fff",
@ -50,4 +51,119 @@ export const styles = StyleSheet.create({
textShadowOffset: { width: 1, height: 1 }, textShadowOffset: { width: 1, height: 1 },
textShadowRadius: 3, textShadowRadius: 3,
}, },
// --- Coordinate Converter Specific Styles (Adjusted and New) ---
converterSafeArea: {
flex: 1,
},
converterBackgroundImage: {
flex: 1,
width: "100%",
height: "100%",
justifyContent: "center",
alignItems: "center",
},
converterBackgroundOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: "rgba(0, 0, 0, 0.6)",
},
converterContainer: {
flex: 1,
width: "100%",
},
converterScrollContent: {
flexGrow: 1,
padding: 20,
alignItems: "center",
width: "100%",
},
converterTitle: {
fontSize: 28,
fontWeight: "bold",
color: "#fff",
marginBottom: 30,
marginTop: 20,
textShadowColor: "rgba(0, 0, 0, 0.7)",
textShadowOffset: { width: 1, height: 1 },
textShadowRadius: 3,
},
dimensionContainer: {
width: "90%",
backgroundColor: "rgba(58, 58, 58, 0.7)",
borderRadius: 10,
borderColor: "#6a5acd",
borderWidth: 2,
padding: 15,
marginBottom: 20,
alignItems: "center",
},
dimensionTitle: {
color: "#fff",
fontSize: 20,
fontWeight: "bold",
marginBottom: 15,
textShadowColor: "rgba(0, 0, 0, 0.5)",
textShadowOffset: { width: 1, height: 1 },
textShadowRadius: 2,
},
coordinatesRow: {
flexDirection: "row",
justifyContent: "space-between",
width: "100%",
},
coordInputGroup: {
flex: 1,
marginHorizontal: 5,
alignItems: "center",
},
coordInputLabel: {
color: "#e0e0e0",
fontSize: 16,
marginBottom: 5,
fontWeight: "500",
textShadowColor: "rgba(0, 0, 0, 0.5)",
textShadowOffset: { width: 1, height: 1 },
textShadowRadius: 2,
},
coordTextInput: {
backgroundColor: "rgba(58, 58, 58, 0.8)",
color: "#fff",
paddingVertical: 10,
paddingHorizontal: 10,
borderRadius: 8,
fontSize: 18,
borderWidth: 1,
borderColor: "#555",
width: "100%",
textAlign: "center",
},
clearButton: {
backgroundColor: "#d32f2f",
paddingVertical: 12,
paddingHorizontal: 25,
borderRadius: 8,
marginTop: 30,
},
clearButtonText: {
color: "#fff",
fontSize: 16,
fontWeight: "bold",
},
backButton: {
alignSelf: "flex-start",
marginTop: 10,
marginBottom: 20,
paddingVertical: 8,
paddingHorizontal: 15,
backgroundColor: "rgba(106, 90, 205, 0.7)",
borderRadius: 8,
marginLeft: 10,
},
backButtonText: {
color: "#fff",
fontSize: 16,
fontWeight: "bold",
},
}); });