//app.js
import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";
import HomeScreen from "./screens/HomeScreen";
import DetailScreen from "./screens/DetailScreen";
const Stack = createStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen
name="Details"
component={DetailScreen}
options={({ route }) => ({
title: route.params?.city
? `Weather • ${route.params.city}`
: "Details",
})}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
//DetailScreen.js
import React, { useState, useEffect } from "react";
import { View, Text, StyleSheet, ActivityIndicator, ImageBackground } from "react-native";
import Icon from "react-native-vector-icons/Feather";
const API_KEY = "fbd8a1595fc2352d6963a9a70e296358";
export default function DetailScreen({ route }) {
const { city } = route.params;
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [errorMsg, setErrorMsg] = useState("");
useEffect(() => {
setLoading(true);
setErrorMsg("");
fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(
city
)}&appid=${API_KEY}&units=metric`
)
.then((res) => res.json())
.then((json) => {
if (json.cod !== 200) {
setErrorMsg(json.message || "City not found");
setData(null);
} else {
setData(json);
}
})
.catch(() => setErrorMsg("Network error"))
.finally(() => setLoading(false));
}, []);
if (loading) return <ActivityIndicator style={{ flex: 1 }} size="large" />;
if (errorMsg || !data) {
return (
<ImageBackground source={{uri: "https://images.pexels.com/photos/209831/pexels-photo-209831.jpeg"}} style={styles.center}>
<View style={styles.errorCard}>
<Text style={styles.errorText}>Error: {errorMsg || "City not found"}</Text>
</View>
</ImageBackground>
);
}
const cond = (data.weather?.[0]?.main || "").toLowerCase();
const isDay = (data.weather?.[0]?.icon || "").includes("d");
const mainIconName =
cond.includes("clear")
? isDay ? "sun" : "moon"
: cond.includes("cloud")
? "cloud"
: cond.includes("rain")
? "cloud-rain"
: cond.includes("snow")
? "cloud-snow"
: cond.includes("drizzle")
? "cloud-drizzle"
: cond.includes("thunder")
? "cloud-lightning"
: "cloud";
const localTime = (() => {
const dt = data.dt || 0;
const tz = data.timezone || 0;
const d = new Date((dt + tz) * 1000);
const hh = String(d.getUTCHours()).padStart(2, "0");
const mm = String(d.getUTCMinutes()).padStart(2, "0");
return `${hh}:${mm}`;
})();
return (
<ImageBackground source={{uri: "https://images.pexels.com/photos/209831/pexels-photo-209831.jpeg"}} style={styles.container}>
<View style={styles.card}>
<Text style={styles.city}>{data.name}</Text>
<Text style={styles.subtle}>Local time: {localTime}</Text>
<View style={{ alignItems: "center", marginVertical: 10 }}>
<Icon name={mainIconName} size={56} color="dark gray" />
</View>
<Text style={styles.temp}>{Math.round(data.main.temp)}°C</Text>
<Text style={styles.cond}>
{data.weather[0].main} ({data.weather[0].description})
</Text>
<View style={styles.row}>
<View style={styles.infoBox}>
<Icon name="wind" size={18} />
<Text style={styles.infoText}>{data.wind.speed} m/s</Text>
</View>
<View style={styles.infoBox}>
<Icon name="cloud" size={18} />
<Text style={styles.infoText}>{data.clouds.all}%</Text>
</View>
</View>
</View>
</ImageBackground>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: "center", alignItems: "center" },
center: { flex: 1, justifyContent: "center", alignItems: "center" },
card: {
width: "85%",
padding: 20,
backgroundColor: "rgba(255,255,255,0.88)",
borderRadius: 14,
alignItems: "center",
},
city: { fontSize: 24, fontWeight: "800" },
subtle: { marginTop: 4, fontSize: 12, color: "gray" },
temp: { fontSize: 56, fontWeight: "800", marginVertical: 6 },
cond: { fontSize: 16, textTransform: "capitalize", marginBottom: 12 },
row: {
flexDirection: "row",
justifyContent: "space-between",
width: "70%",
marginTop: 6,
gap: 12,
},
infoBox: {
flex: 1,
flexDirection: "row",
alignItems: "center",
gap: 6,
paddingVertical: 10,
paddingHorizontal: 12,
backgroundColor: "light gray",
borderRadius: 10,
justifyContent: "center",
},
infoText: { fontSize: 14, fontWeight: "600" },
errorCard: {
padding: 16,
backgroundColor: "white",
borderRadius: 12,
marginHorizontal: 20,
},
errorText: { fontSize: 16, color: "red", fontWeight: "700" },
});
//HomeScreen.js
import React from "react";
import {
View,
Text,
TextInput,
StyleSheet,
ImageBackground,
TouchableOpacity,
TouchableWithoutFeedback,
Keyboard
} from "react-native";
import { Formik } from "formik";
import * as Yup from "yup";
const citySchema = Yup.object().shape({
city: Yup.string()
.required("Enter city name")
.test("no-spaces", "Enter valid city name", (value) => value && value.trim().length > 0),
});
export default function HomeScreen({ navigation }) {
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<ImageBackground
source={{
uri: "https://images.pexels.com/photos/209831/pexels-photo-209831.jpeg?auto=compress&cs=tinysrgb&w=1200",
}}
style={styles.container}
>
<View style={styles.card}>
<Text style={styles.title}>Search Weather</Text>
<Formik
initialValues={{ city: "" }}
validationSchema={citySchema}
onSubmit={(values) => {
const city = values.city.trim();
navigation.navigate("Details", { city });
}}
>
{({ handleChange, handleBlur, handleSubmit, values, errors, touched }) => {
const disabled = !values.city.trim();
return (
<>
<TextInput
style={[styles.input, touched.city && errors.city ? styles.inputError : null]}
placeholder="Enter city name"
value={values.city}
onChangeText={handleChange("city")}
onBlur={handleBlur("city")}
placeholderTextColor="gray"
returnKeyType="search"
onSubmitEditing={handleSubmit}
/>
{touched.city && errors.city ? (
<Text style={styles.errorText}>{errors.city}</Text>
) : null}
<TouchableOpacity
style={[styles.btn, disabled ? styles.btnDisabled : null]}
onPress={handleSubmit}
activeOpacity={0.7}
disabled={disabled}
>
<Text style={styles.btnText}>Search</Text>
</TouchableOpacity>
</>
);
}}
</Formik>
</View>
</ImageBackground>
</TouchableWithoutFeedback>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: "center" },
card: {
marginHorizontal: 20,
padding: 20,
backgroundColor: "rgba(255,255,255,0.88)",
borderRadius: 12,
},
title: { fontSize: 24, textAlign: "center", marginBottom: 16, fontWeight: "700", color: "black" },
input: {
borderWidth: 1,
borderColor: "white",
backgroundColor: "white",
padding: 12,
borderRadius: 8,
marginBottom: 8,
},
inputError: {
borderColor: "red",
},
errorText: {
color: "red",
marginBottom: 12,
},
btn: {
backgroundColor: "dodgerblue",
paddingVertical: 12,
borderRadius: 8,
alignItems: "center",
},
btnDisabled: {
opacity: 0.5,
},
btnText: {
color: "white",
fontWeight: "700",
fontSize: 16,
},
});
//package.json
{
"name": "weatherApp",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest"
},
"dependencies": {
"@react-native/new-app-screen": "0.82.0",
"@react-navigation/native": "^7.1.18",
"@react-navigation/stack": "^7.4.10",
"formik": "^2.4.6",
"react": "19.1.1",
"react-native": "0.82.0",
"react-native-gesture-handler": "^2.28.0",
"react-native-safe-area-context": "^5.6.1",
"react-native-screens": "^4.16.0",
"react-native-vector-icons": "^10.3.0",
"yup": "^1.7.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native-community/cli": "20.0.0",
"@react-native-community/cli-platform-android": "20.0.0",
"@react-native-community/cli-platform-ios": "20.0.0",
"@react-native/babel-preset": "0.82.0",
"@react-native/eslint-config": "0.82.0",
"@react-native/metro-config": "0.82.0",
"@react-native/typescript-config": "0.82.0",
"@types/jest": "^29.5.13",
"@types/react": "^19.1.1",
"@types/react-test-renderer": "^19.1.0",
"eslint": "^8.19.0",
"jest": "^29.6.3",
"prettier": "2.8.8",
"react-test-renderer": "19.1.1",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=20"
}
}
//app/build.gradle
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
//
import React, { useState, useEffect } from "react";
import { View, Text, StyleSheet, ActivityIndicator, ImageBackground } from "react-native";
import Icon from "react-native-vector-icons/Feather";
const API_KEY = "fbd8a1595fc2352d6963a9a70e296358";
export default function DetailScreen({ route }) {
const { city } = route.params;
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [errorMsg, setErrorMsg] = useState("");
useEffect(() => {
setLoading(true);
setErrorMsg("");
fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(
city
)}&appid=${API_KEY}&units=metric`
)
.then((res) => res.json())
.then((json) => {
if (json.cod !== 200) {
setErrorMsg(json.message || "City not found");
setData(null);
} else {
setData(json);
}
})
.catch(() => setErrorMsg("Network error"))
.finally(() => setLoading(false));
}, []);
if (loading) return <ActivityIndicator style={{ flex: 1 }} size="large" />;
if (errorMsg || !data) {
return (
<ImageBackground source={{uri: "https://images.pexels.com/photos/209831/pexels-photo-209831.jpeg"}} style={styles.center}>
<View style={styles.errorCard}>
<Text style={styles.errorText}>Error: {errorMsg || "City not found"}</Text>
</View>
</ImageBackground>
);
}
const cond = (data.weather?.[0]?.main || "").toLowerCase();
const isDay = (data.weather?.[0]?.icon || "").includes("d");
const mainIconName =
cond.includes("clear")
? isDay ? "sun" : "moon"
: cond.includes("cloud")
? "cloud"
: cond.includes("rain")
? "cloud-rain"
: cond.includes("snow")
? "cloud-snow"
: cond.includes("drizzle")
? "cloud-drizzle"
: cond.includes("thunder")
? "cloud-lightning"
: "cloud";
const localTime = (() => {
const dt = data.dt || 0;
const tz = data.timezone || 0;
const d = new Date((dt + tz) * 1000);
const hh = String(d.getUTCHours()).padStart(2, "0");
const mm = String(d.getUTCMinutes()).padStart(2, "0");
return `${hh}:${mm}`;
})();
return (
<ImageBackground source={{uri: "https://images.pexels.com/photos/209831/pexels-photo-209831.jpeg"}} style={styles.container}>
<View style={styles.card}>
<Text style={styles.city}>{data.name}</Text>
<Text style={styles.subtle}>Local time: {localTime}</Text>
<View style={{ alignItems: "center", marginVertical: 10 }}>
<Icon name={mainIconName} size={56} color="dark gray" />
</View>
<Text style={styles.temp}>{Math.round(data.main.temp)}°C</Text>
<Text style={styles.cond}>
{data.weather[0].main} ({data.weather[0].description})
</Text>
<View style={styles.row}>
<View style={styles.infoBox}>
<Icon name="wind" size={18} />
<Text style={styles.infoText}>{data.wind.speed} m/s</Text>
</View>
<View style={styles.infoBox}>
<Icon name="cloud" size={18} />
<Text style={styles.infoText}>{data.clouds.all}%</Text>
</View>
</View>
</View>
</ImageBackground>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: "center", alignItems: "center" },
center: { flex: 1, justifyContent: "center", alignItems: "center" },
card: {
width: "85%",
padding: 20,
backgroundColor: "rgba(255,255,255,0.88)",
borderRadius: 14,
alignItems: "center",
},
city: { fontSize: 24, fontWeight: "800" },
subtle: { marginTop: 4, fontSize: 12, color: "gray" },
temp: { fontSize: 56, fontWeight: "800", marginVertical: 6 },
cond: { fontSize: 16, textTransform: "capitalize", marginBottom: 12 },
row: {
flexDirection: "row",
justifyContent: "space-between",
width: "70%",
marginTop: 6,
gap: 12,
},
infoBox: {
flex: 1,
flexDirection: "row",
alignItems: "center",
gap: 6,
paddingVertical: 10,
paddingHorizontal: 12,
backgroundColor: "light gray",
borderRadius: 10,
justifyContent: "center",
},
infoText: { fontSize: 14, fontWeight: "600" },
errorCard: {
padding: 16,
backgroundColor: "white",
borderRadius: 12,
marginHorizontal: 20,
},
errorText: { fontSize: 16, color: "red", fontWeight: "700" },
});