matiru

Blockchain-Based Supply Chain Transparency for Agricultural Produce
commit 57e5e0226cec4442fe3987b398279e7bf7ecc8b1
parent 4c8d2143bfe158f79f1a2daedcb87790b35fa1c4
Author: maydayv7 <maydayv7@gmail.com>
Date:   Thu,  2 Oct 2025 18:15:00 +0530

feat(frontend): Add more functionality and style

Diffstat:
Mfrontend/App.js | 101++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Afrontend/assets/background.png | 0
Afrontend/components/LocationPicker.js | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afrontend/components/QRScanner.js | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afrontend/components/SearchNav.js | 45+++++++++++++++++++++++++++++++++++++++++++++
Mfrontend/package-lock.json | 211+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfrontend/package.json | 3+++
Dfrontend/screens/Consumer.js | 166-------------------------------------------------------------------------------
Mfrontend/screens/Distributor.js | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mfrontend/screens/Farmer.js | 264+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mfrontend/screens/Inspector.js | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mfrontend/screens/Login.js | 2+-
Mfrontend/screens/Retailer.js | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Afrontend/screens/Search.js | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfrontend/styles.js | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
15 files changed, 1303 insertions(+), 325 deletions(-)

diff --git a/frontend/App.js b/frontend/App.js @@ -1,26 +1,111 @@ -import React from "react"; +import { + ImageBackground, + ScrollView, + Text, + TouchableOpacity, + StatusBar, + View, +} from "react-native"; import { NavigationContainer } from "@react-navigation/native"; import { createStackNavigator } from "@react-navigation/stack"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { MaterialCommunityIcons } from "@expo/vector-icons"; +import styles, { colors } from "./styles"; -import LoginScreen from "./screens/Login"; -import ConsumerScreen from "./screens/Consumer"; -import DistributorScreen from "./screens/Distributor"; import FarmerScreen from "./screens/Farmer"; -import InspectorScreen from "./screens/Inspector"; +import DistributorScreen from "./screens/Distributor"; import RetailerScreen from "./screens/Retailer"; +import InspectorScreen from "./screens/Inspector"; +import SearchScreen from "./screens/Search"; const Stack = createStackNavigator(); +function HomeScreen({ navigation }) { + return ( + <ImageBackground + source={require("./assets/background.png")} + style={styles.bg} + > + <StatusBar + translucent={false} + backgroundColor={colors.darkGreen} + barStyle="light-content" + /> + + <SafeAreaView style={{ flex: 1 }}> + <ScrollView contentContainerStyle={styles.overlay}> + <View style={styles.topContent}> + <Text style={styles.welcome}>Welcome to Matiru!</Text> + + <TouchableOpacity + style={[ + styles.bigButton, + { backgroundColor: "#444", marginBottom: 30 }, + ]} + onPress={() => navigation.navigate("Search")} + > + <MaterialCommunityIcons name="magnify" size={24} color="white" /> + <Text style={styles.bigButtonText}>Global Search</Text> + </TouchableOpacity> + + <Text style={styles.subtitle}>Continue As</Text> + + <TouchableOpacity + style={styles.bigButton} + onPress={() => navigation.navigate("Farmer")} + > + <MaterialCommunityIcons name="tractor" size={24} color="white" /> + <Text style={styles.bigButtonText}>Farmer</Text> + </TouchableOpacity> + + <TouchableOpacity + style={styles.bigButton} + onPress={() => navigation.navigate("Distributor")} + > + <MaterialCommunityIcons name="truck" size={24} color="white" /> + <Text style={styles.bigButtonText}>Distributor</Text> + </TouchableOpacity> + + <TouchableOpacity + style={styles.bigButton} + onPress={() => navigation.navigate("Retailer")} + > + <MaterialCommunityIcons name="store" size={24} color="white" /> + <Text style={styles.bigButtonText}>Retailer</Text> + </TouchableOpacity> + + <TouchableOpacity + style={styles.bigButton} + onPress={() => navigation.navigate("Inspector")} + > + <MaterialCommunityIcons + name="shield-check" + size={24} + color="white" + /> + <Text style={styles.bigButtonText}>Inspector</Text> + </TouchableOpacity> + </View> + + <View style={styles.bottomContent}> + <Text style={styles.brand}>Matiru.</Text> + </View> + </ScrollView> + </SafeAreaView> + </ImageBackground> + ); +} + export default function App() { return ( <NavigationContainer> - <Stack.Navigator initialRouteName="Login"> - <Stack.Screen name="Login" component={LoginScreen} /> + <Stack.Navigator screenOptions={{ headerShown: false }}> + <Stack.Screen name="Home" component={HomeScreen} /> <Stack.Screen name="Farmer" component={FarmerScreen} /> <Stack.Screen name="Distributor" component={DistributorScreen} /> <Stack.Screen name="Retailer" component={RetailerScreen} /> <Stack.Screen name="Inspector" component={InspectorScreen} /> - <Stack.Screen name="Consumer" component={ConsumerScreen} /> + <Stack.Screen name="Search" component={SearchScreen} /> </Stack.Navigator> </NavigationContainer> ); diff --git a/frontend/assets/background.png b/frontend/assets/background.png Binary files differ. diff --git a/frontend/components/LocationPicker.js b/frontend/components/LocationPicker.js @@ -0,0 +1,54 @@ +import { useState } from "react"; +import { View, TextInput, Button, Text, Alert } from "react-native"; +import * as Location from "expo-location"; +import { colors } from "../styles"; + +export default function LocationPicker({ onPicked }) { + const [manual, setManual] = useState(""); + const [place, setPlace] = useState(""); + + const pickGPS = async () => { + try { + const { status } = await Location.requestForegroundPermissionsAsync(); + if (status !== "granted") { + return Alert.alert("Permission denied", "Enable location services."); + } + const pos = await Location.getCurrentPositionAsync({}); + const geo = await Location.reverseGeocodeAsync(pos.coords); + if (geo.length > 0) { + const name = `${geo[0].city || ""} ${geo[0].name || ""}`; + setPlace(name); + onPicked(name); + } + } catch (err) { + Alert.alert("Error", err.message); + } + }; + + return ( + <View style={{ marginTop: 10 }}> + <Button + title="Use Current Location" + color={colors.darkGreen} + onPress={pickGPS} + /> + <TextInput + style={{ + borderWidth: 1, + borderColor: colors.darkGreen, + borderRadius: 8, + padding: 10, + marginTop: 10, + backgroundColor: "white", + }} + placeholder="Or enter place manually" + value={manual} + onChangeText={(t) => { + setManual(t); + onPicked(t); + }} + /> + {place ? <Text style={{ marginTop: 6 }}>πŸ“ {place}</Text> : null} + </View> + ); +} diff --git a/frontend/components/QRScanner.js b/frontend/components/QRScanner.js @@ -0,0 +1,146 @@ +import { useState, useEffect } from "react"; +import { + View, + Text, + Button, + StyleSheet, + Dimensions, + TextInput, + TouchableOpacity, + Alert, +} from "react-native"; +import { Camera, CameraView } from "expo-camera"; +import { colors } from "../styles"; + +const { width } = Dimensions.get("window"); +const SCAN_AREA_SIZE = width * 0.72; + +export default function Scanner({ + onScanned, + placeholder = "Enter ID manually", +}) { + const [hasPermission, setHasPermission] = useState(null); + const [useCamera, setUseCamera] = useState(false); + const [scanned, setScanned] = useState(false); + const [manualId, setManualId] = useState(""); + + useEffect(() => { + (async () => { + const { status } = await Camera.requestCameraPermissionsAsync(); + setHasPermission(status === "granted"); + })(); + }, []); + + const handleBarCodeScanned = ({ data }) => { + setScanned(true); + onScanned(data); + }; + + return ( + <View style={{ marginVertical: 8 }}> + <Button + title={useCamera ? "Use Manual Entry" : "Scan QR Code"} + color={colors.darkGreen} + onPress={() => { + setUseCamera(!useCamera); + setScanned(false); + setManualId(""); + }} + /> + + {useCamera ? ( + <> + {hasPermission === null && ( + <Text>Requesting camera permission...</Text> + )} + {hasPermission === false && <Text>No access to camera</Text>} + {hasPermission && ( + <View style={{ height: 360, marginTop: 10 }}> + <CameraView + style={{ flex: 1 }} + onBarcodeScanned={scanned ? undefined : handleBarCodeScanned} + barcodeScannerSettings={{ barcodeTypes: ["qr"] }} + /> + + {/* Overlay */} + <View style={scannerStyles.overlay}> + <View style={scannerStyles.topBottomOverlay} /> + <View style={scannerStyles.middleRow}> + <View style={scannerStyles.sideOverlay} /> + <View style={scannerStyles.scanArea} /> + <View style={scannerStyles.sideOverlay} /> + </View> + <View style={scannerStyles.topBottomOverlay} /> + </View> + + {scanned && ( + <View style={{ marginTop: 8 }}> + <Button + title="Scan again" + color={colors.midGreen} + onPress={() => setScanned(false)} + /> + </View> + )} + </View> + )} + </> + ) : ( + <View style={{ marginTop: 8 }}> + <TextInput + placeholder={placeholder} + value={manualId} + onChangeText={setManualId} + style={{ + borderWidth: 1, + borderColor: colors.cream, + borderRadius: 10, + padding: 10, + backgroundColor: "#fff", + }} + /> + <TouchableOpacity + style={{ + backgroundColor: colors.midGreen, + padding: 12, + borderRadius: 10, + marginTop: 8, + alignItems: "center", + }} + onPress={() => { + if (!manualId) { + Alert.alert("Error", "ID cannot be empty"); + return; + } + onScanned(manualId); + }} + > + <Text style={{ color: "#fff", fontWeight: "700" }}>Submit</Text> + </TouchableOpacity> + </View> + )} + </View> + ); +} + +const scannerStyles = StyleSheet.create({ + overlay: { + ...StyleSheet.absoluteFillObject, + justifyContent: "center", + alignItems: "center", + }, + topBottomOverlay: { + flex: 1, + width: "100%", + backgroundColor: "rgba(0,0,0,0.5)", + }, + middleRow: { flexDirection: "row" }, + sideOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.5)" }, + scanArea: { + width: SCAN_AREA_SIZE, + height: SCAN_AREA_SIZE, + borderWidth: 2, + borderColor: colors.midGreen, + borderRadius: 12, + }, +}); diff --git a/frontend/components/SearchNav.js b/frontend/components/SearchNav.js @@ -0,0 +1,45 @@ +import { View, TouchableOpacity, Text } from "react-native"; +import { MaterialCommunityIcons } from "@expo/vector-icons"; +import { useNavigation } from "@react-navigation/native"; +import { colors } from "../styles"; + +export default function SearchNav() { + const navigation = useNavigation(); + return ( + <View + style={{ + flexDirection: "row", + justifyContent: "space-between", + marginVertical: 12, + }} + > + <TouchableOpacity + style={{ + flexDirection: "row", + alignItems: "center", + backgroundColor: colors.darkGreen, + padding: 12, + borderRadius: 8, + }} + onPress={() => navigation.navigate("Search")} + > + <MaterialCommunityIcons name="magnify" color="white" size={20} /> + <Text style={{ color: "white", marginLeft: 6 }}>Go to Search</Text> + </TouchableOpacity> + + <TouchableOpacity + style={{ + flexDirection: "row", + alignItems: "center", + backgroundColor: colors.midGreen, + padding: 12, + borderRadius: 8, + }} + onPress={() => navigation.goBack()} + > + <MaterialCommunityIcons name="arrow-left" color="white" size={20} /> + <Text style={{ color: "white", marginLeft: 6 }}>Back</Text> + </TouchableOpacity> + </View> + ); +} diff --git a/frontend/package-lock.json b/frontend/package-lock.json @@ -17,10 +17,12 @@ "expo": "~54.0.11", "expo-camera": "~17.0.8", "expo-constants": "~18.0.9", + "expo-dev-client": "^6.0.13", "expo-font": "~14.0.8", "expo-haptics": "~15.0.7", "expo-image": "~3.0.8", "expo-linking": "~8.0.8", + "expo-location": "~19.0.7", "expo-router": "~6.0.9", "expo-splash-screen": "~31.0.10", "expo-status-bar": "~3.0.8", @@ -34,6 +36,7 @@ "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", + "react-native-vector-icons": "^10.3.0", "react-native-web": "~0.21.0", "react-native-worklets": "0.5.1" } @@ -5081,6 +5084,56 @@ "react-native": "*" } }, + "node_modules/expo-dev-client": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.13.tgz", + "integrity": "sha512-zW3uLx4fBk5jhUafxJcrmbCbhcIMN6Vy7ebUTzLWkHuB0uEh2qwI2bJpeHgXCY+9OzA8HGjT8EUsA5sPKEATfA==", + "license": "MIT", + "dependencies": { + "expo-dev-launcher": "6.0.13", + "expo-dev-menu": "7.0.13", + "expo-dev-menu-interface": "2.0.0", + "expo-manifests": "~1.0.8", + "expo-updates-interface": "~2.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-launcher": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.13.tgz", + "integrity": "sha512-NmUOXKpSN0HaRneY4jeBgLpEYradw/uNHNGYVlE6bPTUXBw2P6cLChGqeclzq/Dj5eHoSCfSOgyFRfvfH1BcfQ==", + "license": "MIT", + "dependencies": { + "expo-dev-menu": "7.0.13", + "expo-manifests": "~1.0.8" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-menu": { + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.13.tgz", + "integrity": "sha512-jxT19gqgCCGhi8AhoVTULwEPZK1PaaevLnLRzCo/1fKVM4YaEV0RgJPPuSe4xVloUWYVkCmfn0t32IPBHp2SSA==", + "license": "MIT", + "dependencies": { + "expo-dev-menu-interface": "2.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-menu-interface": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz", + "integrity": "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-file-system": { "version": "19.0.16", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.16.tgz", @@ -5131,6 +5184,12 @@ } } }, + "node_modules/expo-json-utils": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", + "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==", + "license": "MIT" + }, "node_modules/expo-keep-awake": { "version": "15.0.7", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.7.tgz", @@ -5155,6 +5214,28 @@ "react-native": "*" } }, + "node_modules/expo-location": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/expo-location/-/expo-location-19.0.7.tgz", + "integrity": "sha512-YNkh4r9E6ECbPkBCAMG5A5yHDgS0pw+Rzyd0l2ZQlCtjkhlODB55nMCKr5CZnUI0mXTkaSm8CwfoCO8n2MpYfg==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-manifests": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-1.0.8.tgz", + "integrity": "sha512-nA5PwU2uiUd+2nkDWf9e71AuFAtbrb330g/ecvuu52bmaXtN8J8oiilc9BDvAX0gg2fbtOaZdEdjBYopt1jdlQ==", + "license": "MIT", + "dependencies": { + "@expo/config": "~12.0.8", + "expo-json-utils": "~0.15.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-modules-autolinking": { "version": "3.0.14", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.14.tgz", @@ -5326,6 +5407,15 @@ } } }, + "node_modules/expo-updates-interface": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz", + "integrity": "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-web-browser": { "version": "15.0.8", "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.8.tgz", @@ -7990,6 +8080,23 @@ "node": ">= 6" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -8270,6 +8377,110 @@ "react-native": "*" } }, + "node_modules/react-native-vector-icons": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz", + "integrity": "sha512-IFQ0RE57819hOUdFvgK4FowM5aMXg7C7XKsuGLevqXkkIJatc3QopN0wYrb2IrzUgmdpfP+QVIbI3S6h7M0btw==", + "deprecated": "react-native-vector-icons package has moved to a new model of per-icon-family packages. See the https://github.com/oblador/react-native-vector-icons/blob/master/MIGRATION.md on how to migrate", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2", + "yargs": "^16.1.1" + }, + "bin": { + "fa-upgrade.sh": "bin/fa-upgrade.sh", + "fa5-upgrade": "bin/fa5-upgrade.sh", + "fa6-upgrade": "bin/fa6-upgrade.sh", + "generate-icon": "bin/generate-icon.js" + } + }, + "node_modules/react-native-vector-icons/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/react-native-vector-icons/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/react-native-vector-icons/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-native-vector-icons/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react-native-vector-icons/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/react-native-vector-icons/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native-vector-icons/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/react-native-web": { "version": "0.21.1", "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.1.tgz", diff --git a/frontend/package.json b/frontend/package.json @@ -15,10 +15,12 @@ "expo": "~54.0.11", "expo-camera": "~17.0.8", "expo-constants": "~18.0.9", + "expo-dev-client": "^6.0.13", "expo-font": "~14.0.8", "expo-haptics": "~15.0.7", "expo-image": "~3.0.8", "expo-linking": "~8.0.8", + "expo-location": "~19.0.7", "expo-router": "~6.0.9", "expo-splash-screen": "~31.0.10", "expo-status-bar": "~3.0.8", @@ -32,6 +34,7 @@ "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", + "react-native-vector-icons": "^10.3.0", "react-native-web": "~0.21.0", "react-native-worklets": "0.5.1" } diff --git a/frontend/screens/Consumer.js b/frontend/screens/Consumer.js @@ -1,166 +0,0 @@ -import { useState, useEffect } from "react"; -import { - View, - Text, - Button, - StyleSheet, - Dimensions, - TextInput, - Alert, - TouchableOpacity, -} from "react-native"; -import { CameraView, Camera } from "expo-camera"; -import { API_BASE } from "../config"; -import styles, { colors } from "../styles"; - -const { width } = Dimensions.get("window"); -const SCAN_AREA_SIZE = width * 0.7; - -export default function ConsumerScreen() { - const [hasPermission, setHasPermission] = useState(null); - const [scanned, setScanned] = useState(false); - const [produce, setProduce] = useState(null); - const [manualId, setManualId] = useState(""); - const [useCamera, setUseCamera] = useState(false); - const [loading, setLoading] = useState(false); - - useEffect(() => { - (async () => { - const { status } = await Camera.requestCameraPermissionsAsync(); - setHasPermission(status === "granted"); - })(); - }, []); - - const fetchProduce = async (id) => { - if (!id) { - Alert.alert("Error", "Produce ID cannot be empty."); - return; - } - setLoading(true); - try { - const res = await fetch(`${API_BASE}/getProduce/${id}`); - if (!res.ok) throw new Error(`Server returned ${res.status}`); - const result = await res.json(); - if (!result.produce) throw new Error("Produce not found"); - setProduce(result.produce); - } catch (err) { - Alert.alert("Error fetching produce", err.message); - setProduce(null); - } finally { - setLoading(false); - } - }; - - const handleBarcodeScanned = async ({ data }) => { - setScanned(true); - await fetchProduce(data); - }; - - if (hasPermission === null && useCamera) - return <Text>Requesting camera permission...</Text>; - if (hasPermission === false && useCamera) - return <Text>No access to camera</Text>; - - return ( - <View style={styles.container}> - <Text style={styles.title}>Consumer Dashboard</Text> - - <View style={{ marginBottom: 12 }}> - <Button - title={useCamera ? "Use Manual Entry" : "Scan QR Code"} - color={colors.darkGreen} - onPress={() => { - setUseCamera(!useCamera); - setScanned(false); - setProduce(null); - }} - /> - </View> - - {useCamera ? ( - !scanned ? ( - <View style={{ flex: 1 }}> - <CameraView - style={{ flex: 1 }} - onBarcodeScanned={scanned ? undefined : handleBarcodeScanned} - barcodeScannerSettings={{ barcodeTypes: ["qr"] }} - /> - - {/* Overlay */} - <View style={cameraStyles.overlay}> - <View style={cameraStyles.topBottomOverlay} /> - <View style={cameraStyles.middleRow}> - <View style={cameraStyles.sideOverlay} /> - <View style={cameraStyles.scanArea} /> - <View style={cameraStyles.sideOverlay} /> - </View> - <View style={cameraStyles.topBottomOverlay} /> - </View> - </View> - ) : ( - <Button - title="Scan Again" - color={colors.darkGreen} - onPress={() => { - setScanned(false); - setProduce(null); - }} - /> - ) - ) : ( - <View style={{ alignItems: "center", marginTop: 20 }}> - <TextInput - style={styles.input} - placeholder="Enter Produce ID" - value={manualId} - onChangeText={setManualId} - /> - <TouchableOpacity - style={[styles.button, { backgroundColor: colors.midGreen }]} - onPress={() => fetchProduce(manualId)} - disabled={loading} - > - <Text style={styles.buttonText}> - {loading ? "Loading..." : "Submit"} - </Text> - </TouchableOpacity> - </View> - )} - - {produce && ( - <View style={{ marginTop: 20 }}> - <Text style={{ color: colors.darkGreen, fontSize: 16 }}> - {JSON.stringify(produce, null, 2)} - </Text> - </View> - )} - </View> - ); -} - -const cameraStyles = StyleSheet.create({ - overlay: { - ...StyleSheet.absoluteFillObject, - justifyContent: "center", - alignItems: "center", - }, - topBottomOverlay: { - flex: 1, - width: "100%", - backgroundColor: "rgba(0,0,0,0.5)", - }, - middleRow: { - flexDirection: "row", - }, - sideOverlay: { - flex: 1, - backgroundColor: "rgba(0,0,0,0.5)", - }, - scanArea: { - width: SCAN_AREA_SIZE, - height: SCAN_AREA_SIZE, - borderWidth: 2, - borderColor: colors.midGreen, - borderRadius: 12, - }, -}); diff --git a/frontend/screens/Distributor.js b/frontend/screens/Distributor.js @@ -1,11 +1,27 @@ -import { useState } from "react"; -import { View, Text, TextInput, Button } from "react-native"; +// screens/DistributorScreen.js +import React, { useState } from "react"; +import { + ScrollView, + View, + Text, + TextInput, + TouchableOpacity, + Alert, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { MaterialCommunityIcons } from "@expo/vector-icons"; import { API_BASE } from "../config"; -import styles from "../styles"; +import styles, { colors } from "../styles"; +import QRScanner from "../components/QRScanner"; +import LocationPicker from "../components/LocationPicker"; +import SearchNav from "../components/SearchNav"; export default function DistributorScreen() { + const [active, setActive] = useState(null); + const distributorId = "dist1"; const [produceId, setProduceId] = useState(""); - const [location, setLocation] = useState(""); + const [location, setLocation] = useState(null); + const [newOwner, setNewOwner] = useState(""); const updateLocation = async () => { try { @@ -14,34 +30,94 @@ export default function DistributorScreen() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ produceId, - actorId: "distributor1", - inTransit: false, + actorId: distributorId, newLocation: location, }), }); const data = await res.json(); - alert("Updated: " + JSON.stringify(data.produce)); - } catch (err) { - alert("Error: " + err.message); + Alert.alert("Updated", JSON.stringify(data)); + } catch (e) { + Alert.alert("Error", e.message); + } + }; + + const transferOwnership = async () => { + try { + const res = await fetch(`${API_BASE}/transferOwnership`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + produceId, + newOwnerId: newOwner, + qty: 1, + salePrice: 100, + }), + }); + const data = await res.json(); + Alert.alert("Transferred", JSON.stringify(data)); + } catch (e) { + Alert.alert("Error", e.message); } }; return ( - <View style={styles.container}> - <Text>Distributor Dashboard</Text> - <TextInput - placeholder="Produce ID" - value={produceId} - onChangeText={setProduceId} - style={styles.input} - /> - <TextInput - placeholder="New Location" - value={location} - onChangeText={setLocation} - style={styles.input} - /> - <Button title="Update Location" onPress={updateLocation} /> - </View> + <SafeAreaView style={styles.container}> + <ScrollView> + <Text style={styles.title}>🚚 Distributor Dashboard</Text> + <SearchNav /> + + <View style={styles.actionGrid}> + <TouchableOpacity + style={styles.actionButton} + onPress={() => setActive("location")} + > + <MaterialCommunityIcons name="map-marker" size={20} color="white" /> + <Text style={styles.actionText}>Update Location</Text> + </TouchableOpacity> + <TouchableOpacity + style={styles.actionButton} + onPress={() => setActive("transfer")} + > + <MaterialCommunityIcons + name="swap-horizontal" + size={20} + color="white" + /> + <Text style={styles.actionText}>Transfer</Text> + </TouchableOpacity> + </View> + + {active === "location" && ( + <View> + <QRScanner onScanned={setProduceId} placeholder="Produce ID" /> + <LocationPicker onPicked={setLocation} /> + <TouchableOpacity + style={styles.primaryButton} + onPress={updateLocation} + > + <Text style={styles.buttonText}>Update</Text> + </TouchableOpacity> + </View> + )} + + {active === "transfer" && ( + <View> + <QRScanner onScanned={setProduceId} placeholder="Produce ID" /> + <TextInput + style={styles.input} + placeholder="New Owner ID" + value={newOwner} + onChangeText={setNewOwner} + /> + <TouchableOpacity + style={styles.primaryButton} + onPress={transferOwnership} + > + <Text style={styles.buttonText}>Transfer</Text> + </TouchableOpacity> + </View> + )} + </ScrollView> + </SafeAreaView> ); } diff --git a/frontend/screens/Farmer.js b/frontend/screens/Farmer.js @@ -1,74 +1,234 @@ import { useState } from "react"; -import { Text, TextInput, Button, ScrollView } from "react-native"; +import { + ScrollView, + View, + Text, + TextInput, + TouchableOpacity, + Alert, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { MaterialCommunityIcons } from "@expo/vector-icons"; import { API_BASE } from "../config"; import styles, { colors } from "../styles"; +import QRScanner from "../components/QRScanner"; +import LocationPicker from "../components/LocationPicker"; +import SearchNav from "../components/SearchNav"; export default function FarmerScreen() { + const [active, setActive] = useState(null); + const farmerId = "farmer1"; + const [cropType, setCropType] = useState(""); const [qty, setQty] = useState(""); + const [qtyUnit, setQtyUnit] = useState("KG"); + const [harvestDate, setHarvestDate] = useState(""); + const [quality, setQuality] = useState(""); + const [expiryDate, setExpiryDate] = useState(""); + const [storageConditions, setStorageConditions] = useState(""); + const [location, setLocation] = useState(null); + const [produceId, setProduceId] = useState(""); + const [pricePerUnit, setPricePerUnit] = useState(""); + const [storageUpdate, setStorageUpdate] = useState(""); + const [splitId, setSplitId] = useState(""); + const [splitQty, setSplitQty] = useState(""); const registerProduce = async () => { - const res = await fetch(`${API_BASE}/registerProduce`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - farmerId: "farmer1", - details: { cropType, qty: parseInt(qty), pricePerUnit: 30 }, - }), - }); - const data = await res.json(); - alert("Registered: " + JSON.stringify(data.produce)); + try { + const res = await fetch(`${API_BASE}/registerProduce`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + farmerId, + details: { + cropType, + qty, + qtyUnit, + harvestDate, + quality, + expiryDate, + storageConditions, + currentLocation: location || "Unknown", + }, + }), + }); + const data = await res.json(); + Alert.alert("Success", JSON.stringify(data)); + } catch (e) { + Alert.alert("Error", e.message); + } }; const updateDetails = async () => { - const res = await fetch(`${API_BASE}/updateDetails`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - produceId, - actorId: "farmer1", - details: { storageConditions: "Cold", pricePerUnit: 35 }, - }), - }); - alert(await res.text()); + try { + const res = await fetch(`${API_BASE}/updateDetails`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + produceId, + actorId: farmerId, + details: { pricePerUnit, storageConditions: storageUpdate }, + }), + }); + const data = await res.json(); + Alert.alert("Updated", JSON.stringify(data)); + } catch (e) { + Alert.alert("Error", e.message); + } }; const splitProduce = async () => { - const res = await fetch(`${API_BASE}/splitProduce`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ produceId, qty: 50, OwnerId: "farmer1" }), - }); - alert(await res.text()); + try { + const res = await fetch(`${API_BASE}/splitProduce`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + produceId: splitId, + qty: splitQty, + OwnerId: farmerId, + }), + }); + const data = await res.json(); + Alert.alert("Split Done", JSON.stringify(data)); + } catch (e) { + Alert.alert("Error", e.message); + } }; return ( - <ScrollView style={styles.container}> - <Text style={styles.title}>Farmer Dashboard</Text> - <TextInput - style={styles.input} - placeholder="Crop Type" - value={cropType} - onChangeText={setCropType} - /> - <TextInput - style={styles.input} - placeholder="Quantity" - value={qty} - onChangeText={setQty} - keyboardType="numeric" - /> - <Button title="Register Produce" onPress={registerProduce} /> + <SafeAreaView style={styles.container}> + <ScrollView> + <Text style={styles.title}>πŸ‘¨β€πŸŒΎ Farmer Dashboard</Text> + <SearchNav /> + + <View style={styles.actionGrid}> + <TouchableOpacity + style={styles.actionButton} + onPress={() => setActive("register")} + > + <MaterialCommunityIcons name="plus-box" size={20} color="white" /> + <Text style={styles.actionText}>Register</Text> + </TouchableOpacity> + <TouchableOpacity + style={styles.actionButton} + onPress={() => setActive("update")} + > + <MaterialCommunityIcons name="pencil" size={20} color="white" /> + <Text style={styles.actionText}>Update</Text> + </TouchableOpacity> + <TouchableOpacity + style={styles.actionButton} + onPress={() => setActive("split")} + > + <MaterialCommunityIcons + name="content-cut" + size={20} + color="white" + /> + <Text style={styles.actionText}>Split</Text> + </TouchableOpacity> + </View> + + {active === "register" && ( + <View> + <TextInput + style={styles.input} + placeholder="Crop Type" + value={cropType} + onChangeText={setCropType} + /> + <TextInput + style={styles.input} + placeholder="Quantity" + keyboardType="numeric" + value={qty} + onChangeText={setQty} + /> + <TextInput + style={styles.input} + placeholder="Unit (KG/Number)" + value={qtyUnit} + onChangeText={setQtyUnit} + /> + <TextInput + style={styles.input} + placeholder="Harvest Date" + value={harvestDate} + onChangeText={setHarvestDate} + /> + <TextInput + style={styles.input} + placeholder="Quality" + value={quality} + onChangeText={setQuality} + /> + <TextInput + style={styles.input} + placeholder="Expiry Date" + value={expiryDate} + onChangeText={setExpiryDate} + /> + <TextInput + style={styles.input} + placeholder="Storage Conditions" + value={storageConditions} + onChangeText={setStorageConditions} + /> + <LocationPicker onPicked={setLocation} /> + <TouchableOpacity + style={styles.primaryButton} + onPress={registerProduce} + > + <Text style={styles.buttonText}>Submit</Text> + </TouchableOpacity> + </View> + )} + + {active === "update" && ( + <View> + <QRScanner onScanned={setProduceId} placeholder="Produce ID" /> + <TextInput + style={styles.input} + placeholder="Price/Unit" + keyboardType="numeric" + value={pricePerUnit} + onChangeText={setPricePerUnit} + /> + <TextInput + style={styles.input} + placeholder="Storage Conditions" + value={storageUpdate} + onChangeText={setStorageUpdate} + /> + <TouchableOpacity + style={styles.primaryButton} + onPress={updateDetails} + > + <Text style={styles.buttonText}>Update</Text> + </TouchableOpacity> + </View> + )} - <TextInput - style={styles.input} - placeholder="Produce ID" - value={produceId} - onChangeText={setProduceId} - /> - <Button title="Update Details" onPress={updateDetails} /> - <Button title="Split Produce" onPress={splitProduce} /> - </ScrollView> + {active === "split" && ( + <View> + <QRScanner onScanned={setSplitId} placeholder="Produce ID" /> + <TextInput + style={styles.input} + placeholder="Qty to Split" + keyboardType="numeric" + value={splitQty} + onChangeText={setSplitQty} + /> + <TouchableOpacity + style={styles.primaryButton} + onPress={splitProduce} + > + <Text style={styles.buttonText}>Split</Text> + </TouchableOpacity> + </View> + )} + </ScrollView> + </SafeAreaView> ); } diff --git a/frontend/screens/Inspector.js b/frontend/screens/Inspector.js @@ -1,46 +1,138 @@ -import { useState } from "react"; -import { View, Text, TextInput, Button, StyleSheet } from "react-native"; +// screens/InspectorScreen.js +import React, { useState } from "react"; +import { + ScrollView, + View, + Text, + TextInput, + TouchableOpacity, + Alert, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { MaterialCommunityIcons } from "@expo/vector-icons"; import { API_BASE } from "../config"; -import styles from "../styles"; +import styles, { colors } from "../styles"; +import QRScanner from "../components/QRScanner"; +import SearchNav from "../components/SearchNav"; export default function InspectorScreen() { + const [active, setActive] = useState(null); + const inspectorId = "insp1"; const [produceId, setProduceId] = useState(""); const [quality, setQuality] = useState(""); + const [expiryDate, setExpiryDate] = useState(""); + const [reason, setReason] = useState(""); - const inspect = async () => { + const inspectProduce = async () => { try { const res = await fetch(`${API_BASE}/inspectProduce`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ produceId, - inspectorId: "inspector1", - qualityUpdate: quality, + inspectorId, + qualityUpdate: { quality, expiryDate }, }), }); const data = await res.json(); - alert("Inspected: " + JSON.stringify(data.produce)); - } catch (err) { - alert("Error: " + err.message); + Alert.alert("Inspected", JSON.stringify(data)); + } catch (e) { + Alert.alert("Error", e.message); + } + }; + + const markUnavailable = async () => { + try { + const res = await fetch(`${API_BASE}/markAsUnavailable`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + produceId, + actorId: inspectorId, + reason, + newStatus: "Removed", + }), + }); + const data = await res.json(); + Alert.alert("Marked", JSON.stringify(data)); + } catch (e) { + Alert.alert("Error", e.message); } }; return ( - <View style={styles.container}> - <Text>Inspector Dashboard</Text> - <TextInput - placeholder="Produce ID" - value={produceId} - onChangeText={setProduceId} - style={styles.input} - /> - <TextInput - placeholder="Quality" - value={quality} - onChangeText={setQuality} - style={styles.input} - /> - <Button title="Inspect Produce" onPress={inspect} /> - </View> + <SafeAreaView style={styles.container}> + <ScrollView> + <Text style={styles.title}>πŸ•΅οΈ Inspector Dashboard</Text> + <SearchNav /> + + <View style={styles.actionGrid}> + <TouchableOpacity + style={styles.actionButton} + onPress={() => setActive("inspect")} + > + <MaterialCommunityIcons + name="check-decagram" + size={20} + color="white" + /> + <Text style={styles.actionText}>Inspect</Text> + </TouchableOpacity> + <TouchableOpacity + style={styles.actionButton} + onPress={() => setActive("remove")} + > + <MaterialCommunityIcons + name="close-octagon" + size={20} + color="white" + /> + <Text style={styles.actionText}>Mark Unavailable</Text> + </TouchableOpacity> + </View> + + {active === "inspect" && ( + <View> + <QRScanner onScanned={setProduceId} placeholder="Produce ID" /> + <TextInput + style={styles.input} + placeholder="Quality" + value={quality} + onChangeText={setQuality} + /> + <TextInput + style={styles.input} + placeholder="Expiry Date" + value={expiryDate} + onChangeText={setExpiryDate} + /> + <TouchableOpacity + style={styles.primaryButton} + onPress={inspectProduce} + > + <Text style={styles.buttonText}>Inspect</Text> + </TouchableOpacity> + </View> + )} + + {active === "remove" && ( + <View> + <QRScanner onScanned={setProduceId} placeholder="Produce ID" /> + <TextInput + style={styles.input} + placeholder="Reason" + value={reason} + onChangeText={setReason} + /> + <TouchableOpacity + style={styles.primaryButton} + onPress={markUnavailable} + > + <Text style={styles.buttonText}>Remove</Text> + </TouchableOpacity> + </View> + )} + </ScrollView> + </SafeAreaView> ); } diff --git a/frontend/screens/Login.js b/frontend/screens/Login.js @@ -2,7 +2,7 @@ import { View, Text, TouchableOpacity, Image } from "react-native"; import styles, { colors } from "../styles"; export default function LoginScreen({ navigation }) { - const roles = ["Farmer", "Distributor", "Retailer", "Inspector", "Consumer"]; + const roles = ["Farmer", "Distributor", "Retailer", "Inspector", "Search"]; return ( <View style={styles.container}> diff --git a/frontend/screens/Retailer.js b/frontend/screens/Retailer.js @@ -1,40 +1,109 @@ -import { useState } from "react"; -import { View, Text, TextInput, Button, StyleSheet } from "react-native"; +// screens/RetailerScreen.js +import React, { useState } from "react"; +import { + ScrollView, + View, + Text, + TextInput, + TouchableOpacity, + Alert, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { MaterialCommunityIcons } from "@expo/vector-icons"; import { API_BASE } from "../config"; -import styles from "../styles"; +import styles, { colors } from "../styles"; +import QRScanner from "../components/QRScanner"; +import LocationPicker from "../components/LocationPicker"; +import SearchNav from "../components/SearchNav"; export default function RetailerScreen() { + const [active, setActive] = useState(null); + const retailerId = "ret1"; const [produceId, setProduceId] = useState(""); + const [location, setLocation] = useState(null); - const markSold = async () => { + const updateLocation = async () => { + try { + const res = await fetch(`${API_BASE}/updateLocation`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + produceId, + actorId: retailerId, + newLocation: location, + }), + }); + const data = await res.json(); + Alert.alert("Updated", JSON.stringify(data)); + } catch (e) { + Alert.alert("Error", e.message); + } + }; + + const markSale = async () => { try { const res = await fetch(`${API_BASE}/transferOwnership`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ produceId, - newOwnerId: "consumer1", + newOwnerId: "consumer", qty: 1, - salePrice: 100, + salePrice: 200, }), }); const data = await res.json(); - alert("Sold: " + JSON.stringify(data.produce)); - } catch (err) { - alert("Error: " + err.message); + Alert.alert("Sold", JSON.stringify(data)); + } catch (e) { + Alert.alert("Error", e.message); } }; return ( - <View style={styles.container}> - <Text>Retailer Dashboard</Text> - <TextInput - placeholder="Produce ID" - value={produceId} - onChangeText={setProduceId} - style={styles.input} - /> - <Button title="Mark as Sold to Consumer" onPress={markSold} /> - </View> + <SafeAreaView style={styles.container}> + <ScrollView> + <Text style={styles.title}>🏬 Retailer Dashboard</Text> + <SearchNav /> + + <View style={styles.actionGrid}> + <TouchableOpacity + style={styles.actionButton} + onPress={() => setActive("location")} + > + <MaterialCommunityIcons name="map-marker" size={20} color="white" /> + <Text style={styles.actionText}>Update Location</Text> + </TouchableOpacity> + <TouchableOpacity + style={styles.actionButton} + onPress={() => setActive("sale")} + > + <MaterialCommunityIcons name="cart" size={20} color="white" /> + <Text style={styles.actionText}>Mark Sale</Text> + </TouchableOpacity> + </View> + + {active === "location" && ( + <View> + <QRScanner onScanned={setProduceId} placeholder="Produce ID" /> + <LocationPicker onPicked={setLocation} /> + <TouchableOpacity + style={styles.primaryButton} + onPress={updateLocation} + > + <Text style={styles.buttonText}>Update</Text> + </TouchableOpacity> + </View> + )} + + {active === "sale" && ( + <View> + <QRScanner onScanned={setProduceId} placeholder="Produce ID" /> + <TouchableOpacity style={styles.primaryButton} onPress={markSale}> + <Text style={styles.buttonText}>Sell</Text> + </TouchableOpacity> + </View> + )} + </ScrollView> + </SafeAreaView> ); } diff --git a/frontend/screens/Search.js b/frontend/screens/Search.js @@ -0,0 +1,110 @@ +/* + Produce β†’ search by produceId β†’ shows produce asset + Owner β†’ search by role id (e.g. "farmer1" or "retailer1") β†’ lists all assets owned by entity + User β†’ search by user key (e.g. "FARMER-farmer1") β†’ shows the user’s profile +*/ + +import { useState } from "react"; +import { View, Text, TouchableOpacity, Alert, ScrollView } from "react-native"; +import styles, { colors } from "../styles"; +import { API_BASE } from "../config"; +import Scanner from "../components/QRScanner"; + +export default function SearchScreen() { + const [mode, setMode] = useState("produce"); // produce | owner | user + const [queryId, setQueryId] = useState(""); + const [result, setResult] = useState(null); + + const searchProduce = async (id) => { + try { + const res = await fetch(`${API_BASE}/getProduce/${id}`); + if (!res.ok) throw new Error("Not found"); + const data = await res.json(); + setResult({ type: "produce", data: data.produce }); + } catch (err) { + Alert.alert("Error", err.message); + setResult(null); + } + }; + + const searchOwner = async (id) => { + try { + const res = await fetch(`${API_BASE}/getOwner/${id}`); + const data = await res.json(); + setResult({ type: "owner", data: data.produces }); + } catch (err) { + Alert.alert("Error", err.message); + setResult(null); + } + }; + + const getUser = async (id) => { + try { + const res = await fetch(`${API_BASE}/getUser/${id}`); + const data = await res.json(); + setResult({ type: "user", data: data.user }); + } catch (err) { + Alert.alert("Error", err.message); + setResult(null); + } + }; + + const handleSearch = (id) => { + if (!id) return Alert.alert("Error", "ID required"); + setQueryId(id); + if (mode === "produce") searchProduce(id); + if (mode === "owner") searchOwner(id); + if (mode === "user") getUser(id); + }; + + return ( + <ScrollView style={styles.container}> + <Text style={styles.title}>Global Search</Text> + + <View + style={{ + flexDirection: "row", + justifyContent: "space-around", + marginVertical: 10, + }} + > + {["produce", "owner", "user"].map((m) => ( + <TouchableOpacity + key={m} + style={[ + styles.actionButton, + { + backgroundColor: + mode === m ? colors.midGreen : colors.lightGreen, + width: "30%", + }, + ]} + onPress={() => { + setMode(m); + setResult(null); + }} + > + <Text style={styles.actionText}>{m.toUpperCase()}</Text> + </TouchableOpacity> + ))} + </View> + + <Scanner onScanned={handleSearch} placeholder={`Enter ${mode} ID`} /> + + <View style={{ marginTop: 20 }}> + <Text style={{ fontWeight: "700", color: colors.darkGreen }}> + Result + </Text> + {result ? ( + <View style={styles.card}> + <Text style={{ color: colors.darkGreen }}> + {JSON.stringify(result.data, null, 2)} + </Text> + </View> + ) : ( + <Text style={{ color: "#666", marginTop: 8 }}>No results</Text> + )} + </View> + </ScrollView> + ); +} diff --git a/frontend/styles.js b/frontend/styles.js @@ -1,4 +1,6 @@ -import { StyleSheet } from "react-native"; +import { StyleSheet, Dimensions, StatusBar } from "react-native"; + +const { width, height } = Dimensions.get("window"); export const colors = { darkGreen: "#184B2C", @@ -9,44 +11,135 @@ export const colors = { }; export default StyleSheet.create({ - container: { flex: 1, backgroundColor: colors.cream }, - headerBar: { - height: 80, - backgroundColor: colors.darkGreen, - paddingTop: 30, - paddingHorizontal: 16, - borderBottomLeftRadius: 16, - borderBottomRightRadius: 16, + container: { + flex: 1, + backgroundColor: colors.cream, + padding: 16, + paddingTop: StatusBar.currentHeight || 0, // safe top padding }, - headerTitle: { color: "#fff", fontSize: 20, fontWeight: "700" }, - centerContent: { flex: 1, justifyContent: "center", alignItems: "center" }, - button: { - padding: 12, - marginVertical: 8, - borderRadius: 10, - width: "80%", + bg: { + flex: 1, + width: width, + height: height, + resizeMode: "cover", + }, + overlay: { + flexGrow: 1, + padding: 20, + backgroundColor: "rgba(0,0,0,0.5)", + }, + topContent: { alignItems: "center", + flex: 1, + justifyContent: "center", }, - buttonText: { color: "#fff", fontWeight: "600" }, - boxCard: { - backgroundColor: "#fff", - padding: 12, - borderRadius: 12, - marginVertical: 8, + bottomContent: { + alignItems: "center", + marginBottom: 10, + }, + welcome: { + color: "white", + fontSize: 30, + fontWeight: "bold", + marginBottom: 10, + textAlign: "center", + }, + subtitle: { + color: colors.accent, + fontSize: 20, + marginBottom: 20, + textAlign: "center", + }, + bigButton: { + flexDirection: "row", + alignItems: "center", + backgroundColor: colors.darkGreen, + paddingVertical: 18, + paddingHorizontal: 24, + borderRadius: 16, + marginVertical: 10, + width: "85%", + justifyContent: "center", + shadowColor: "#000", + shadowOpacity: 0.2, + shadowOffset: { width: 0, height: 3 }, + shadowRadius: 5, + elevation: 4, // Android shadow + }, + bigButtonText: { + color: "white", + fontSize: 20, + fontWeight: "600", + marginLeft: 10, + }, + brand: { + color: colors.accent, + fontSize: 40, + fontWeight: "bold", + textAlign: "center", }, title: { - fontSize: 22, + fontSize: 24, fontWeight: "bold", - color: colors.midGreen, - marginBottom: 15, + color: colors.darkGreen, + marginBottom: 16, + textAlign: "center", + }, + actionGrid: { + flexDirection: "row", + flexWrap: "wrap", + justifyContent: "center", + marginTop: 10, + }, + actionButton: { + flexDirection: "row", + alignItems: "center", + backgroundColor: colors.midGreen, + paddingVertical: 14, + paddingHorizontal: 20, + margin: 8, + borderRadius: 12, + flexBasis: "40%", + justifyContent: "center", + shadowColor: "#000", + shadowOpacity: 0.15, + shadowOffset: { width: 0, height: 2 }, + shadowRadius: 3, + elevation: 2, + }, + actionText: { + color: "white", + fontSize: 16, + fontWeight: "500", + marginLeft: 6, textAlign: "center", }, input: { borderWidth: 1, - borderColor: colors.lightGreen, + borderColor: colors.darkGreen, borderRadius: 10, - padding: 10, - marginBottom: 10, - backgroundColor: "#fff", + padding: 12, + marginVertical: 6, + backgroundColor: "white", + fontSize: 16, + }, + button: { + padding: 16, + borderRadius: 12, + alignItems: "center", + marginTop: 12, + backgroundColor: colors.darkGreen, + }, + primaryButton: { + backgroundColor: colors.darkGreen, + paddingVertical: 16, + borderRadius: 14, + alignItems: "center", + marginVertical: 12, + }, + buttonText: { + color: "white", + fontSize: 18, + fontWeight: "600", }, });