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:
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",
},
});