diff --git a/backend/app.py b/backend/app.py
index 287c29e5..2c1062a4 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -256,47 +256,52 @@ def get_recent_activity():
{"$unwind": {"path": "$log_info", "preserveNullAndEmptyArrays": True}},
{"$sort": {"timestamp": pymongo.DESCENDING}}
]
-
+
recent_activity = list(detection.aggregate(pipeline))
formatted_activity = []
for activity in recent_activity:
# Get log info if it exists
log_info = activity.get("log_info", {})
-
+
# Determine if it's phishing based on either detection details or log verdict
is_phishing = activity.get("details") == "Phishing" or log_info.get("verdict") == "Phishing"
-
+
# Format date for display
timestamp = activity.get("timestamp")
formatted_time = time_ago(timestamp) if timestamp else "Unknown"
-
+
# Get probability and ensure it's a valid float
probability = 0.0
if log_info.get("probability") is not None:
probability = float(log_info.get("probability"))
elif activity.get("ensemble_score") is not None:
probability = float(activity.get("ensemble_score"))
-
+
# Print debug info
- print(f"Debug - ID: {activity.get('_id')}, Probability: {probability}, Type: {type(probability)}")
-
+ # print(f"Debug - ID: {activity.get('_id')}, Probability: {probability}, Type: {type(probability)}") # Keep if needed
+
formatted_activity.append({
- "id": str(activity["_id"]),
+ # "id": str(activity["_id"]), # This is Detection _id, maybe rename or keep separate?
"detect_id": activity.get("detect_id", "N/A"),
+ "log_id": str(log_info.get("_id")) if log_info else None, # ✅ Add the Log's _id here
"title": "Phishing Detected" if is_phishing else "Safe Link Verified",
"link": f"{activity.get('url', 'Unknown URL')} - {activity.get('metadata', {}).get('source', 'Scan')}",
"time": formatted_time,
"icon": "suspicious-icon" if is_phishing else "safe-icon",
- "severity": activity.get("severity", "Medium"),
- "probability": probability, # Now guaranteed to be a float
+ "severity": activity.get("severity", "Medium"), # Use severity from Detection if available
+ "probability": probability,
"platform": log_info.get("platform", "Web"),
- "recommended_action": "Block URL" if is_phishing else "Allow URL",
+ "recommended_action": log_info.get("recommended_action", "N/A") if log_info else ("Block URL" if is_phishing else "Allow URL"), # Use log action if available
# Additional fields needed for modal
"url": activity.get("url", "Unknown URL"),
"date_scanned": timestamp
})
+ # Filter out entries where log_id is None if you only want entries that have a corresponding log
+ formatted_activity = [item for item in formatted_activity if item["log_id"] is not None]
+
+
return jsonify({"recent_activity": formatted_activity})
except Exception as e:
@@ -306,7 +311,6 @@ def get_recent_activity():
-
# ✅ **GET - Fetch Severity Counts**
@app.route("/severity-counts", methods=["GET"])
def get_severity_counts():
diff --git a/frontend/app/Navigation.js b/frontend/app/Navigation.js
index af0fb89d..1f4a2a01 100644
--- a/frontend/app/Navigation.js
+++ b/frontend/app/Navigation.js
@@ -2,22 +2,21 @@
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import {
- View,
- Text,
- TouchableOpacity,
- Switch,
- StyleSheet,
- Image,
- AppState
+View,
+Text,
+TouchableOpacity,
+Switch,
+StyleSheet,
+Image,
+AppState
} from "react-native";
// **** Import useRef ****
-import React, { useState, useEffect, useRef } from "react";
+import React, { useState, useEffect, useRef } from "react";
import GradientScreen from "./screens/components/GradientScreen";
import TopBar from "./screens/components/TopBar";
import NotificationToast from "./screens/components/NotificationToast";
import { requestNotificationPermissions, setupNotificationListeners, scheduleNotification } from "./services/NotificationService";
import config from "./config";
-
//Screens
import Home from "./screens/tabScreens/Home";
import Analytics from "./screens/tabScreens/Analytics";
@@ -30,393 +29,375 @@ import ForgotPassword from "./screens/ForgotPassword";
import Reports from "./screens/reportsPage/Reports";
import CreateReport from "./screens/reportsPage/CreateReport";
import EditReport from "./screens/reportsPage/EditReport";
-
-
//Icons
import { Entypo } from "@expo/vector-icons";
import Octicons from "@expo/vector-icons/Octicons";
import FontAwesome6 from "@expo/vector-icons/FontAwesome6";
import Ionicons from "@expo/vector-icons/Ionicons";
-
-
const Tab = createBottomTabNavigator();
const Stack = createNativeStackNavigator();
-
// Create a global navigation variable to store the navigation prop
let globalNavigation = null;
-
//Bottom Tab Bar Component
// TabGroup component
function TabGroup({ navigation, hasUnreadNotifications, onNotificationRead }) {
- // Store the navigation prop globally when this component mounts
- useEffect(() => {
- globalNavigation = navigation;
- }, [navigation]);
-
- const [isDarkMode, setDarkMode] = useState(false);
-
- const handleToggleDarkMode = () => {
- setDarkMode((prevMode) => !prevMode);
- };
-
- return (
-
- ({
- tabBarIcon: ({ color }) => {
- let iconName;
- let IconLibrary;
-
- if (route.name === "Home") {
- iconName = "home";
- IconLibrary = Entypo;
- } else if (route.name === "Analytics") {
- iconName = "graph";
- IconLibrary = Octicons;
- } else if (route.name === "Logs") {
- iconName = "list";
- IconLibrary = FontAwesome6;
- } else if (route.name === "Settings") {
- iconName = "settings-sharp";
- IconLibrary = Ionicons;
- }
-
- return (
-
-
-
- );
- },
- tabBarLabel: ({ focused }) =>
- focused ? (
-
- {route.name}
-
- ) : null,
-
- tabBarActiveTintColor: isDarkMode ? "#00A757" : "#3AED97",
- tabBarInactiveTintColor: isDarkMode ? "#AAAAAA" : "#218555",
- tabBarStyle: {
- backgroundColor: isDarkMode ? "#FFFFFF" : "#000000",
- height: 56,
- borderTopWidth: 0,
- },
- })}
- >
-
- {() => (
- {
+globalNavigation = navigation;
+}, [navigation]);
+const [isDarkMode, setDarkMode] = useState(false);
+const handleToggleDarkMode = () => {
+setDarkMode((prevMode) => !prevMode);
+};
+return (
+
+ ({
+tabBarIcon: ({ color }) => {
+let iconName;
+let IconLibrary;
+if (route.name === "Home") {
+ iconName = "home";
+ IconLibrary = Entypo;
+ } else if (route.name === "Analytics") {
+ iconName = "graph";
+ IconLibrary = Octicons;
+ } else if (route.name === "Logs") {
+ iconName = "list";
+ IconLibrary = FontAwesome6;
+ } else if (route.name === "Settings") {
+ iconName = "settings-sharp";
+ IconLibrary = Ionicons;
+ }
+
+ return (
+
+
+
+ );
+ },
+ tabBarLabel: ({ focused }) =>
+ focused ? (
+
+ {route.name}
+
+ ) : null,
+
+ tabBarActiveTintColor: isDarkMode ? "#00A757" : "#3AED97",
+ tabBarInactiveTintColor: isDarkMode ? "#AAAAAA" : "#218555",
+ tabBarStyle: {
+ backgroundColor: isDarkMode ? "#FFFFFF" : "#000000",
+ height: 56,
+ borderTopWidth: 0,
+ },
+ })}
+ >
+
+ {() => (
+
- }
- >
-
-
- )}
-
-
-
- {() => (
-
+ }
+ >
+
+
+ )}
+
+
+
+ {() => (
+
- }
- >
-
-
- )}
-
-
-
- {() => (
-
+ }
+ >
+
+
+ )}
+
+
+
+ {() => (
+
- }
- >
-
-
- )}
-
-
-
- {() => (
-
+ }
+ >
+
+
+ )}
+
+
+
+ {() => (
+
- }
- >
-
-
- )}
-
-
-
- );
-}
+ hasUnreadNotifications={hasUnreadNotifications}
+ onNotificationRead={onNotificationRead}
+ />
+ }
+ >
+
+
+ )}
+
+
+
+);
+}
// Main Stack Navigator
function MainStack({ hasUnreadNotifications, onNotificationRead }) {
- return (
-
-
-
-
-
- {props => (
-
- )}
-
-
-
-
-
-
- );
+return (
+
+
+
+
+
+{props => (
+
+)}
+
+
+
+
+
+
+);
}
-
export default function Navigation() {
- const [inAppNotification, setInAppNotification] = useState(null);
- const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false);
- const appState = useRef(AppState.currentState);
- // **** Add useRef to track the last notified ID ****
- const lastNotifiedIdRef = useRef(null);
-
- // Set up notifications on app start
- useEffect(() => {
- // Request permissions
- requestNotificationPermissions();
-
- // Set up listeners for system notifications
- const cleanupListeners = setupNotificationListeners(
- (notification) => {
- // Handle received notification
- console.log('Notification received', notification);
- },
- (response) => {
- // Handle notification response (user tap)
- const data = response.notification.request.content.data;
- navigateToNotificationsScreen(data);
- }
- );
-
- // Set up polling for new notifications
- const checkInterval = setInterval(checkForNewNotifications, 30000); // Every 30 seconds
- checkForNewNotifications(); // Check immediately on mount
-
- // Clean up on unmount
- return () => {
- cleanupListeners();
- clearInterval(checkInterval);
- };
- }, []);
+const [inAppNotification, setInAppNotification] = useState(null);
+const [hasUnreadNotifications, setHasUnreadNotifications] = useState(false);
+const appState = useRef(AppState.currentState);
+// **** Add useRef to track the last notified ID ****
+const lastNotifiedIdRef = useRef(null);
+// Set up notifications on app start
+useEffect(() => {
+// Request permissions
+requestNotificationPermissions();
+// Set up listeners for system notifications
+const cleanupListeners = setupNotificationListeners(
+ (notification) => {
+ // Handle received notification
+ console.log('Notification received', notification);
+ },
+ (response) => {
+ // Handle notification response (user tap)
+ const data = response.notification.request.content.data;
+ navigateToNotificationsScreen(data);
+ }
+);
+
+// Set up polling for new notifications
+const checkInterval = setInterval(checkForNewNotifications, 30000); // Every 30 seconds
+checkForNewNotifications(); // Check immediately on mount
+
+// Clean up on unmount
+return () => {
+ cleanupListeners();
+ clearInterval(checkInterval);
+};
+
+}, []);
+// Monitor app state to determine notification type
+useEffect(() => {
+const subscription = AppState.addEventListener('change', nextAppState => {
+appState.current = nextAppState;
+});
+return () => {
+ subscription.remove();
+};
+
+}, []);
+// Function to check for new notifications
+const checkForNewNotifications = async () => {
+try {
+// Check if config and BASE_URL exist
+if (!config || !config.BASE_URL) {
+console.error('BASE_URL is not defined in config');
+return;
+}
+const response = await fetch(`${config.BASE_URL}/logs`);
+ // **** Added error handling for non-ok responses ****
+ if (!response.ok) {
+ console.error(`Error fetching logs: ${response.status} ${response.statusText}`);
+ // Optionally handle specific statuses like 404, 500 etc.
+ return;
+ }
+ const data = await response.json();
+
+ // **** Changed check to ensure data exists and is an array ****
+ if (data && Array.isArray(data) && data.length > 0) {
+ // Get the latest notification (assuming API returns sorted newest first)
+ const latestNotification = data[0];
+ console.log(`[Check] Latest log ID from API: ${latestNotification.id}`); // Debug log
+ handleNewNotification(latestNotification);
+ } else {
+ // console.log("[Check] No logs found or invalid data format from API."); // Optional debug log
+ }
+} catch (error) {
+ console.error('Error during checkForNewNotifications fetch:', error);
+}
- // Monitor app state to determine notification type
- useEffect(() => {
- const subscription = AppState.addEventListener('change', nextAppState => {
- appState.current = nextAppState;
+};
+// Handle a new notification
+const handleNewNotification = (notification) => {
+// **** Ensure notification and its ID exist ****
+if (!notification || !notification.id) {
+console.warn("handleNewNotification received invalid notification data:", notification);
+return;
+}
+// Determine if link is malicious based on icon or other property
+const isMalicious = notification.icon === "suspicious-icon";
+const title = isMalicious ?
+ "Warning: Malicious Link Detected" :
+ "Safe Link Verified";
+const body = notification.link || ""; // Use link as body
+
+// Set unread notifications flag (might need refinement later if you track read status globally)
+setHasUnreadNotifications(true);
+
+// **** Core Logic: Check if the ID is new ****
+if (notification.id !== lastNotifiedIdRef.current) {
+ console.log(`[New Notification] ID ${notification.id} is different from last notified ID ${lastNotifiedIdRef.current}. Showing toast.`); // Debug log
+ if (appState.current === 'active') {
+ // App is in foreground, show in-app notification
+ setInAppNotification({
+ id: notification.id, // Use the actual ID from the log
+ link: notification.link, // Use link from log data
+ icon: notification.icon || (isMalicious ? "suspicious-icon" : "safe-icon"),
+ // Add any other properties your NotificationToast needs from the notification object
});
-
- return () => {
- subscription.remove();
- };
- }, []);
-
- // Function to check for new notifications
- const checkForNewNotifications = async () => {
- try {
- // Check if config and BASE_URL exist
- if (!config || !config.BASE_URL) {
- console.error('BASE_URL is not defined in config');
- return;
- }
-
- const response = await fetch(`${config.BASE_URL}/logs`);
- // **** Added error handling for non-ok responses ****
- if (!response.ok) {
- console.error(`Error fetching logs: ${response.status} ${response.statusText}`);
- // Optionally handle specific statuses like 404, 500 etc.
- return;
- }
- const data = await response.json();
-
- // **** Changed check to ensure data exists and is an array ****
- if (data && Array.isArray(data) && data.length > 0) {
- // Get the latest notification (assuming API returns sorted newest first)
- const latestNotification = data[0];
- console.log(`[Check] Latest log ID from API: ${latestNotification.id}`); // Debug log
- handleNewNotification(latestNotification);
- } else {
- // console.log("[Check] No logs found or invalid data format from API."); // Optional debug log
- }
- } catch (error) {
- console.error('Error during checkForNewNotifications fetch:', error);
- }
- };
-
- // Handle a new notification
- const handleNewNotification = (notification) => {
- // **** Ensure notification and its ID exist ****
- if (!notification || !notification.id) {
- console.warn("handleNewNotification received invalid notification data:", notification);
- return;
- }
-
- // Determine if link is malicious based on icon or other property
- const isMalicious = notification.icon === "suspicious-icon";
- const title = isMalicious ?
- "Warning: Malicious Link Detected" :
- "Safe Link Verified";
- const body = notification.link || ""; // Use link as body
-
- // Set unread notifications flag (might need refinement later if you track read status globally)
- setHasUnreadNotifications(true);
-
- // **** Core Logic: Check if the ID is new ****
- if (notification.id !== lastNotifiedIdRef.current) {
- console.log(`[New Notification] ID ${notification.id} is different from last notified ID ${lastNotifiedIdRef.current}. Showing toast.`); // Debug log
- if (appState.current === 'active') {
- // App is in foreground, show in-app notification
- setInAppNotification({
- id: notification.id, // Use the actual ID from the log
- link: notification.link, // Use link from log data
- icon: notification.icon || (isMalicious ? "suspicious-icon" : "safe-icon"),
- // Add any other properties your NotificationToast needs from the notification object
- });
- // **** Update the last notified ID ****
- lastNotifiedIdRef.current = notification.id;
- } else {
- // App is in background, show system notification
- console.log(`[New Notification] App not active. Scheduling system notification for ID ${notification.id}.`); // Debug log
- scheduleNotification(title, body, notification);
- // **** Update the last notified ID even for background notifications ****
- // This prevents showing an in-app toast for the same item when the app returns to foreground
- lastNotifiedIdRef.current = notification.id;
- }
- } else {
- console.log(`[New Notification] ID ${notification.id} is the SAME as last notified ID ${lastNotifiedIdRef.current}. Skipping toast.`); // Debug log
- }
- };
-
- // Function to navigate to notifications screen using the global navigation variable
- const navigateToNotificationsScreen = (data) => {
- if (globalNavigation) {
- globalNavigation.navigate('Notifications', {
- isDarkMode: false, // Pass relevant props if needed
- onToggleDarkMode: () => {},
- ...data // Pass any data needed by the Notifications screen
- });
- // Clear unread notifications when navigating to the Notifications screen
- setHasUnreadNotifications(false);
- } else {
- console.warn("Cannot navigate: globalNavigation is not set.");
- }
- };
-
- // Simply return the MainStack and NotificationToast
- return (
- <>
- setHasUnreadNotifications(false)}
- />
-
- {/* Notification Toast */}
- {inAppNotification && (
- { // Renamed param for clarity
- console.log("Toast pressed. Navigating with data:", notificationData); // Debug log
- if (globalNavigation) {
- // Pass the specific notification data needed by the screen
- globalNavigation.navigate('Notifications', {
- notificationIdToHighlight: notificationData.id, // Example: pass ID to highlight
- // Pass other relevant props if Notifications screen needs them
- });
- setInAppNotification(null); // Dismiss toast after navigation
- setHasUnreadNotifications(false); // Mark as read
- } else {
- console.warn("Cannot navigate from toast press: globalNavigation not set.");
- }
- }}
- onDismiss={() => {
- console.log("Toast dismissed (timeout or manual)."); // Debug log
- setInAppNotification(null);
- }}
- />
- )}
- >
- );
+ // **** Update the last notified ID ****
+ lastNotifiedIdRef.current = notification.id;
+ } else {
+ // App is in background, show system notification
+ console.log(`[New Notification] App not active. Scheduling system notification for ID ${notification.id}.`); // Debug log
+ scheduleNotification(title, body, notification);
+ // **** Update the last notified ID even for background notifications ****
+ // This prevents showing an in-app toast for the same item when the app returns to foreground
+ lastNotifiedIdRef.current = notification.id;
+ }
+} else {
+ console.log(`[New Notification] ID ${notification.id} is the SAME as last notified ID ${lastNotifiedIdRef.current}. Skipping toast.`); // Debug log
}
+};
+// Function to navigate to notifications screen using the global navigation variable
+const navigateToNotificationsScreen = (data) => {
+if (globalNavigation) {
+globalNavigation.navigate('Notifications', {
+isDarkMode: false, // Pass relevant props if needed
+onToggleDarkMode: () => {},
+...data // Pass any data needed by the Notifications screen
+});
+// Clear unread notifications when navigating to the Notifications screen
+setHasUnreadNotifications(false);
+} else {
+console.warn("Cannot navigate: globalNavigation is not set.");
+}
+};
+// Simply return the MainStack and NotificationToast
+return (
+<>
+ setHasUnreadNotifications(false)}
+/>
+{/* Notification Toast */}
+ {inAppNotification && (
+ { // Renamed param for clarity
+ console.log("Toast pressed. Navigating with data:", notificationData); // Debug log
+ if (globalNavigation) {
+ // Pass the specific notification data needed by the screen
+ globalNavigation.navigate('Notifications', {
+ notificationIdToHighlight: notificationData.id, // Example: pass ID to highlight
+ // Pass other relevant props if Notifications screen needs them
+ });
+ setInAppNotification(null); // Dismiss toast after navigation
+ setHasUnreadNotifications(false); // Mark as read
+ } else {
+ console.warn("Cannot navigate from toast press: globalNavigation not set.");
+ }
+ }}
+ onDismiss={() => {
+ console.log("Toast dismissed (timeout or manual)."); // Debug log
+ setInAppNotification(null);
+ }}
+ />
+ )}
+>
+
+);
+}
// Styles (Keep existing styles)
const styles = StyleSheet.create({
- topBar: {
- height: 60,
- backgroundColor: "transparent",
- flexDirection: "row",
- justifyContent: "space-between",
- alignItems: "center",
- paddingHorizontal: 16,
- },
- logo: {
- width: 120,
- height: 30,
- resizeMode: "contain",
- },
- topBarIcons: {
- flexDirection: "row",
- alignItems: "center",
- gap: 10,
- },
+topBar: {
+height: 60,
+backgroundColor: "transparent",
+flexDirection: "row",
+justifyContent: "space-between",
+alignItems: "center",
+paddingHorizontal: 16,
+},
+logo: {
+width: 120,
+height: 30,
+resizeMode: "contain",
+},
+topBarIcons: {
+flexDirection: "row",
+alignItems: "center",
+gap: 10,
+},
});
\ No newline at end of file
diff --git a/frontend/app/screens/components/DetailsModal.js b/frontend/app/screens/components/DetailsModal.js
index 602ca8aa..af4db9f1 100644
--- a/frontend/app/screens/components/DetailsModal.js
+++ b/frontend/app/screens/components/DetailsModal.js
@@ -1,4 +1,4 @@
-import React, { useEffect, useState, Linking } from 'react';
+import React, { useEffect, useState } from 'react';
import {
Modal,
View,
@@ -7,79 +7,35 @@ import {
StyleSheet,
Dimensions,
ActivityIndicator,
- Image,
- Alert
+ Alert,
+ Linking
} from 'react-native';
-import Icon from 'react-native-vector-icons/MaterialIcons'; // Ensure you have react-native-vector-icons installed
-const iconMap = {
- "suspicious": require("../../../assets/images/suspicious-icon.png"),
- "safe": require("../../../assets/images/safe-icon.png"),
-};
-
-const severityColors = {
- "low": "#31EE9A",
- "medium": "#FFC107",
- "high": "#FF8C00",
- "critical": "#FF0000"
-};
-
-
-const handleUpdate = async () => {
- if (!logDetails?.log_id) return;
-
- try {
- const response = await fetch(`${config.BASE_URL}/logs/${logDetails.log_id}`, {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({
- severity: "High",
- probability: 90,
- platform: "Web",
- recommended_action: "Review URL",
- }),
- });
-
- const data = await response.json();
- if (data.error) {
- console.error("Error updating log:", data.error);
- } else {
- console.log("Log updated successfully:", data);
- onClose();
- }
- } catch (error) {
- console.error("Error updating log:", error);
- }
-};
+// Removed the external handleDelete and handleUpdate as we'll integrate the logic
+// const handleUpdate = async (logDetails, onClose, config) => { ... };
+// const handleDelete = async (logDetails, onClose, config, onDelete) => { ... };
-const handleDelete = async () => {
- if (!logDetails?.log_id) return;
- try {
- const response = await fetch(`${config.BASE_URL}/logs/${logDetails.log_id}`, {
- method: "DELETE",
- });
-
- const data = await response.json();
- if (data.error) {
- console.error("Error deleting log:", data.error);
- } else {
- console.log("Log deleted successfully:", data);
- onClose();
- }
- } catch (error) {
- console.error("Error deleting log:", error);
- }
+const severityColors = {
+ "low": "#31EE9A",
+ "medium": "#FFC107",
+ "high": "#FF8C00",
+ "critical": "#FF0000"
};
-
-const DetailsModal = ({ visible, onClose, logDetails, loading, onUpdatePress, onDeletePress }) => {
+// Renamed onDelete prop to onLogDeleted for clarity
+const DetailsModal = ({ visible, onClose, logDetails, loading, config, onLogDeleted }) => {
const [screenDimensions, setScreenDimensions] = useState(Dimensions.get('window'));
const [editedLog, setEditedLog] = useState({});
+ const [isDeleting, setIsDeleting] = useState(false); // State to track deletion in progress
+ const [deleteError, setDeleteError] = useState(null); // State for deletion error messages
useEffect(() => {
if (logDetails) {
setEditedLog(logDetails);
+ // Reset delete states when new log details are loaded
+ setIsDeleting(false);
+ setDeleteError(null);
}
}, [logDetails]);
@@ -87,13 +43,11 @@ const DetailsModal = ({ visible, onClose, logDetails, loading, onUpdatePress, on
const updateDimensions = () => {
setScreenDimensions(Dimensions.get('window'));
};
-
- Dimensions.addEventListener('change', updateDimensions);
+ const dimensionChangeSubscription = Dimensions.addEventListener('change', updateDimensions);
return () => {
- if (Dimensions.removeEventListener) {
- Dimensions.removeEventListener('change', updateDimensions);
- }
+ // Clean up the event listener subscription
+ dimensionChangeSubscription.remove();
};
}, []);
@@ -109,6 +63,58 @@ const DetailsModal = ({ visible, onClose, logDetails, loading, onUpdatePress, on
}
};
+ // Function to execute the actual delete API call
+ const executeDelete = async () => {
+ if (!logDetails?.log_id) {
+ setDeleteError("Cannot delete: Log ID is missing.");
+ Alert.alert("Deletion Failed", "Log ID is missing."); // Also show alert for critical error
+ return;
+ }
+
+ setIsDeleting(true); // Start loading state
+ setDeleteError(null); // Clear any previous error
+
+ try {
+ // Your backend delete endpoint structure
+ const response = await fetch(`${config.BASE_URL}/logs/${logDetails.log_id}`, {
+ method: "DELETE",
+ // Add any necessary headers here (e.g., authorization)
+ });
+
+ if (!response.ok) {
+ // Attempt to read error message from the response body
+ let errorData = "Failed to delete log.";
+ try {
+ const jsonResponse = await response.json();
+ errorData = jsonResponse.error || jsonResponse.message || errorData;
+ } catch (e) {
+ // If parsing fails, use a generic message
+ console.warn("Could not parse error response body:", e);
+ }
+ throw new Error(errorData);
+ }
+
+ console.log("Log deleted successfully:", logDetails.log_id);
+
+ // Notify the parent component that this log was deleted
+ if (onLogDeleted) {
+ // Pass the ID of the log that was deleted back to the parent
+ onLogDeleted(logDetails.log_id);
+ }
+
+ // Close the modal ONLY after successful deletion
+ onClose();
+
+ } catch (error) {
+ console.error("Error deleting log:", error);
+ setDeleteError(error.message || "Failed to delete log."); // Set state for UI display
+ Alert.alert("Deletion Failed", error.message || "Failed to delete log."); // Also show an alert for immediate user feedback
+ } finally {
+ setIsDeleting(false); // End loading state
+ }
+ };
+
+
// Handle Delete Press with confirmation
const handleDeletePress = () => {
Alert.alert("Confirm Delete", "Are you sure you want to delete this log?", [
@@ -116,20 +122,11 @@ const DetailsModal = ({ visible, onClose, logDetails, loading, onUpdatePress, on
{
text: "Delete",
style: "destructive",
- onPress: () => {
- if (onDelete && editedLog?.id) {
- onDelete(editedLog.id); // Trigger the delete function
- handleClosePress(); // Optionally close the modal after deletion
- }
- },
+ onPress: executeDelete, // Call the function that performs the delete
},
]);
};
-
- const isSafe = editedLog?.recommended_action === "Allow URL";
- const iconSource = isSafe ? iconMap.safe : iconMap.suspicious;
-
const probability = editedLog?.probability ? Math.round(editedLog.probability) : "N/A";
const severity = editedLog?.severity ? editedLog.severity.toLowerCase() : "unknown";
const severityColor = severityColors[severity] || "#FFFFFF";
@@ -145,25 +142,41 @@ const DetailsModal = ({ visible, onClose, logDetails, loading, onUpdatePress, on
true}
+ onResponderRelease={() => {}}
>
-
- {loading ? (
-
- ) : logDetails ? (
- <>
-
- {/* Dynamic Icon */}
-
-
+ {/* Initial Loading state (e.g., fetching details) */}
+ {loading && (
+
+
+ Loading Details...
+ )}
+
+ {/* Deletion Loading state */}
+ {isDeleting && (
+
+
+ Deleting...
+
+ )}
+
+ {/* Error Display for initial load */}
+ {!loading && !logDetails && (
+ Error loading details.
+ )}
+ {/* Main Content (show if not loading and logDetails exist) */}
+ {!loading && logDetails && (
+ <>
{/* URL Display */}
{
@@ -177,6 +190,7 @@ const DetailsModal = ({ visible, onClose, logDetails, loading, onUpdatePress, on
});
}
}}
+ disabled={isDeleting} // Disable link while deleting
>
{logDetails.url || "Unknown URL"}
@@ -215,29 +229,35 @@ const DetailsModal = ({ visible, onClose, logDetails, loading, onUpdatePress, on
+ {/* Display Deletion Error Message */}
+ {deleteError && (
+ {deleteError}
+ )}
+
+
{/* Buttons Row (Delete before Close) */}
{/* Delete Button */}
-
- Delete
+ {isDeleting ? 'Deleting...' : 'Delete'}
{/* Close Button */}
-
Close
>
- ) : (
- Error loading details.
)}
@@ -259,32 +279,38 @@ const styles = StyleSheet.create({
padding: 20,
alignItems: 'center',
},
- headerIcons: {
- flexDirection: 'row',
- position: 'absolute',
- right: 15,
- top: 15,
- },
- iconButton: {
- marginLeft: 10,
- },
- iconContainer: {
- marginBottom: 15,
- alignItems: 'center',
- },
- icon: {
- width: 70,
- height: 70,
+ loadingContainer: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 20,
},
+ loadingText: {
+ marginTop: 10,
+ color: '#31EE9A',
+ fontSize: 16,
+ },
+ deletingContainer: {
+ position: 'absolute', // Position over content
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ backgroundColor: 'rgba(0,0,0,0.7)', // Semi-transparent overlay
+ borderRadius: 20,
+ justifyContent: 'center',
+ alignItems: 'center',
+ zIndex: 1, // Ensure it's on top
+ },
+ deletingText: {
+ marginTop: 10,
+ color: '#FFFFFF',
+ fontSize: 16,
+ },
urlText: {
fontSize: 18,
fontWeight: '600',
color: '#FFFFFF',
textAlign: 'center',
- },
- urlLabel: {
- fontSize: 14,
- color: '#AAAAAA',
marginBottom: 20,
},
analysisContainer: {
@@ -311,33 +337,30 @@ const styles = StyleSheet.create({
fontWeight: '500',
textAlign: 'right',
},
- actionButton: {
- backgroundColor: '#31EE9A',
- paddingVertical: 12,
- paddingHorizontal: 30,
- borderRadius: 15,
- marginTop: 10,
- width: '100%',
- alignItems: 'center',
- },
actionButtonText: {
color: '#000000',
fontSize: 16,
fontWeight: '600',
},
- errorText: {
+ errorText: { // Style for initial loading error
color: 'red',
fontSize: 16,
fontWeight: 'bold',
textAlign: 'center',
},
+ deleteErrorText: { // Style specifically for deletion error
+ color: '#FF4C4C', // Use a red color
+ fontSize: 14,
+ marginTop: 10,
+ textAlign: 'center',
+ fontWeight: 'bold',
+ },
buttonRow: {
flexDirection: 'row',
justifyContent: 'space-between',
width: '100%',
marginTop: 20,
},
-
sideButton: {
flex: 1,
backgroundColor: '#31EE9A',
@@ -345,8 +368,7 @@ const styles = StyleSheet.create({
marginHorizontal: 5,
borderRadius: 15,
alignItems: 'center',
- },
+ },
});
-export default DetailsModal;
-
\ No newline at end of file
+export default DetailsModal;
\ No newline at end of file
diff --git a/frontend/app/screens/tabScreens/Logs.js b/frontend/app/screens/tabScreens/Logs.js
index e434601d..c8eee226 100644
--- a/frontend/app/screens/tabScreens/Logs.js
+++ b/frontend/app/screens/tabScreens/Logs.js
@@ -1,14 +1,14 @@
import React, { useState, useCallback } from "react";
import {
- StyleSheet,
- SafeAreaView,
- Text,
- View,
- TextInput,
- TouchableOpacity,
- FlatList,
- ActivityIndicator,
- Alert,
+StyleSheet,
+SafeAreaView,
+Text,
+View,
+TextInput,
+TouchableOpacity,
+FlatList,
+ActivityIndicator,
+Alert,
} from "react-native";
import { useSharedValue } from "react-native-reanimated";
import Icon from "react-native-vector-icons/MaterialIcons";
@@ -20,373 +20,345 @@ import config from "../../config";
import DetailsModal from '../components/DetailsModal';
const iconMap = {
- "suspicious-icon": require("../../../assets/images/suspicious-icon.png"),
- "safe-icon": require("../../../assets/images/safe-icon.png"),
+"suspicious-icon": require("../../../assets/images/suspicious-icon.png"),
+"safe-icon": require("../../../assets/images/safe-icon.png"),
};
export default function Logs({ route }) {
- const [url, setUrl] = useState("");
- const [logs, setLogs] = useState([]);
- const [loading, setLoading] = useState(true); // For initial/filter fetch
- const [searching, setSearching] = useState(false); // For search input delay feedback
- const viewableItems = useSharedValue([]);
- const [activeFilter, setActiveFilter] = useState("recent");
- const [modalVisible, setModalVisible] = useState(false);
- const [selectedLog, setSelectedLog] = useState(null);
- const [logLoading, setLogLoading] = useState(false); // For modal details fetch
+const [url, setUrl] = useState("");
+const [logs, setLogs] = useState([]);
+const [loading, setLoading] = useState(true); // For initial/filter fetch
+const [searching, setSearching] = useState(false); // For search input delay feedback
+const viewableItems = useSharedValue([]);
+const [activeFilter, setActiveFilter] = useState("recent");
+const [modalVisible, setModalVisible] = useState(false);
+const [selectedLog, setSelectedLog] = useState(null);
+const [logLoading, setLogLoading] = useState(false); // For modal details fetch
- const filterOptions = [
- { id: 'recent', label: 'Recent' },
- { id: 'safe', label: 'Safe' },
- { id: 'phishing', label: 'Phishing' },
- { id: 'low', label: 'Low' },
- { id: 'medium', label: 'Medium' },
- { id: 'high', label: 'High' },
- { id: 'critical', label: 'Critical' }
- ];
+const filterOptions = [
+{ id: 'recent', label: 'Recent' },
+{ id: 'safe', label: 'Safe' },
+{ id: 'phishing', label: 'Phishing' },
+{ id: 'low', label: 'Low' },
+{ id: 'medium', label: 'Medium' },
+{ id: 'high', label: 'High' },
+{ id: 'critical', label: 'Critical' }
+];
- // --- Modal Logic ---
- const showModal = async (logId) => {
- console.log(`[Logs.js] showModal function started for ID: ${logId}`);
- if (!logId) {
- console.error("[Logs.js] showModal called with invalid logId:", logId);
- return;
- }
- setLogLoading(true);
- // Set modal visible slightly earlier maybe? So container appears while loading
- // setModalVisible(true); // Option: Make visible before fetch starts
- try {
- const response = await fetch(`${config.BASE_URL}/logs/${logId}`);
- console.log('[Logs.js] Fetch response status:', response.status);
- const data = await response.json();
- console.log('[Logs.js] Fetched data:', JSON.stringify(data, null, 2));
-
- if (data.error || !response.ok) {
- console.error("[Logs.js] Error fetching log details from API:", data.error || `Status ${response.status}`);
- Alert.alert("Error", "Could not load log details."); // Show error to user
- setModalVisible(false); // Hide modal on error
- setSelectedLog(null);
- } else {
- console.log("[Logs.js] Fetch successful. Setting selected log and making modal visible.");
- setSelectedLog(data);
- console.log("[Logs.js] Calling setModalVisible(true)");
- setModalVisible(true); // Make visible *after* data is ready
- }
- } catch (error) {
- console.error("[Logs.js] Error DURING fetch operation:", error);
- Alert.alert("Error", "An error occurred while fetching details."); // Show error to user
- setModalVisible(false); // Hide modal on error
- setSelectedLog(null);
- } finally {
- console.log("[Logs.js] Setting log loading to false.");
- setLogLoading(false);
- }
- };
+// --- Modal Logic ---
+const showModal = async (logId) => {
+console.log(`[Logs.js] showModal function started for ID: ${logId}`);
+if (!logId) {
+ console.error("[Logs.js] showModal called with invalid logId:", logId);
+ return;
+}
+setLogLoading(true);
- const closeModal = () => {
- // Modify the existing log to show current state BEFORE changing it
- console.log(`[Logs.js] closeModal function called. Current state before closing - modalVisible: ${modalVisible}, logLoading: ${logLoading}`); // <-- MODIFY THIS LOG
+try {
+ const response = await fetch(`${config.BASE_URL}/logs/${logId}`);
+ console.log('[Logs.js] Fetch response status:', response.status);
+ const data = await response.json();
+ console.log('[Logs.js] Fetched data:', JSON.stringify(data, null, 2));
- setModalVisible(false);
- setSelectedLog(null); // Set selected log to null too
-
- // Add a log IMMEDIATELY after setting state to confirm the call was made
- // Note: The state might not update *immediately* in the console here due to batching,
- // but seeing this log confirms setModalVisible(false) was executed.
- console.log(`[Logs.js] setModalVisible(false) EXECUTED.`); // <-- ADD THIS LOG
- };
-
- // --- Fetching and Filtering Logic --- (Keep as is)
- const handleFilterChange = (filter) => {
- setActiveFilter(filter);
- console.log("Active Filter:", filter);
- fetchLogs(filter);
- };
-
- const fetchLogs = async (filterType = activeFilter) => {
- if (!searching) {
- setLoading(true);
- }
- try {
- const response = await fetch(`${config.BASE_URL}/logs?filter=${filterType}`);
- const data = await response.json();
- setLogs(data);
- } catch (error) {
- console.error("Error fetching logs:", error);
- Alert.alert("Error", "Could not fetch logs.");
- } finally {
- setLoading(false);
- // setSearching(false); // Optional reset
- }
- };
-
- useFocusEffect(
- useCallback(() => {
- fetchLogs(activeFilter);
- }, [activeFilter])
- );
+ if (data.error || !response.ok) {
+ console.error("[Logs.js] Error fetching log details from API:", data.error || `Status ${response.status}`);
+ Alert.alert("Error", "Could not load log details."); // Show error to user
+ setModalVisible(false); // Hide modal on error
+ setSelectedLog(null);
+ } else {
+ console.log("[Logs.js] Fetch successful. Setting selected log and making modal visible.");
+ setSelectedLog(data);
+ console.log("[Logs.js] Calling setModalVisible(true)");
+ setModalVisible(true); // Make visible *after* data is ready
+ }
+} catch (error) {
+ console.error("[Logs.js] Error DURING fetch operation:", error);
+ Alert.alert("Error", "An error occurred while fetching details."); // Show error to user
+ setModalVisible(false); // Hide modal on error
+ setSelectedLog(null);
+} finally {
+ console.log("[Logs.js] Setting log loading to false.");
+ setLogLoading(false);
+}
+};
- useFocusEffect(
- useCallback(() => {
- if (route?.params?.logId) {
- showModal(route.params.logId);
- // Optional: Clear the param after showing the modal to prevent re-showing on focus gain
- // navigation.setParams({ logId: undefined }); // Requires navigation prop
- }
- }, [route?.params?.logId])
- );
+const closeModal = () => {
+// Modify the existing log to show current state BEFORE changing it
+console.log(`[Logs.js] closeModal function called. Current state before closing - modalVisible: ${modalVisible}, logLoading: ${logLoading}`);
+setModalVisible(false);
+setSelectedLog(null); // Set selected log to null too
- const viewabilityConfig = {
- itemVisiblePercentThreshold: 100,
- };
+// Add a log IMMEDIATELY after setting state to confirm the call was made
+console.log(`[Logs.js] setModalVisible(false) EXECUTED.`);
+};
- // --- Client-side filtering --- (Keep as is)
- const getFilteredLogs = () => {
- const categoryFilteredLogs = logs.filter(log => {
- switch (activeFilter) {
- case "recent": return true;
- case "safe": return log.status?.toLowerCase().includes("safe") || log.status?.toLowerCase().includes("safe");
- case "phishing": return log.status?.toLowerCase().includes("phishing") || log.status?.toLowerCase().includes("phishing");
- case "low": return log.severity?.toLowerCase() === "low";
- case "medium": return log.severity?.toLowerCase() === "medium";
- case "high": return log.severity?.toLowerCase() === "high";
- case "critical": return log.severity?.toLowerCase() === "critical";
- default: return true;
- }
- });
+// --- Updated Fetching and Filtering Logic ---
+const handleFilterChange = (filter) => {
+ if (activeFilter !== filter) {
+ console.log("Active Filter changing to:", filter);
+ setActiveFilter(filter); // Update active filter
+ fetchLogs(filter, url); // Fetch logs immediately with the new filter
+ }
+};
- if (!url || url.trim() === "") {
- return categoryFilteredLogs;
+const fetchLogs = async (filterType = activeFilter, searchQuery = "") => {
+ console.log(`[Logs.js] Fetching logs with filter: ${filterType}, search: "${searchQuery}"`);
+
+ if (!searching) {
+ setLoading(true);
+ }
+
+ try {
+ // Construct query parameters
+ let queryParams = `filter=${encodeURIComponent(filterType)}`;
+ if (searchQuery && searchQuery.trim() !== "") {
+ queryParams += `&search=${encodeURIComponent(searchQuery)}`;
}
- const searchTerm = url.toLowerCase().trim();
- return categoryFilteredLogs.filter(log =>
- Object.values(log).some(value =>
- typeof value === "string" && value.toLowerCase().includes(searchTerm)
- )
- );
- };
-
- // --- Handle Search Input --- (Keep as is)
- const handleInputChange = (text) => {
- setUrl(text);
- if (text.trim() !== "") {
- setSearching(true);
- // Debounce might be better here, but basic timeout for example:
- const timer = setTimeout(() => {
- setSearching(false);
- }, 500); // Shorter delay for search feedback
- // Need useRef to clear timer properly if implementing debounce
- } else {
- setSearching(false);
- }
- };
+
+ const response = await fetch(`${config.BASE_URL}/logs?${queryParams}`);
+
+ if (!response.ok) {
+ throw new Error(`HTTP status ${response.status}`);
+ }
+
+ const data = await response.json();
+ console.log(`[Logs.js] Fetched ${data.length} logs for filter "${filterType}"`);
+ setLogs(data);
+ } catch (error) {
+ console.error("[Logs.js] Error fetching logs:", error);
+ Alert.alert("Error", `Could not fetch logs: ${error.message}`);
+ } finally {
+ setLoading(false);
+ }
+};
- const displayedLogs = getFilteredLogs();
+useFocusEffect(
+ useCallback(() => {
+ fetchLogs(activeFilter, url);
+ }, [activeFilter])
+);
- // --- Delete Logic ---
- const handleDeleteLog = async (logId) => { // Make the function async
- if (!logId) {
- console.error("[Logs.js] handleDeleteLog called with invalid ID:", logId);
- Alert.alert("Error", "Cannot delete log: Invalid ID.");
- return;
+useFocusEffect(
+ useCallback(() => {
+ if (route?.params?.logId) {
+ showModal(route.params.logId);
}
+ }, [route?.params?.logId])
+);
- console.log(`[Logs.js] Attempting to delete log with ID: ${logId}`);
+const viewabilityConfig = {
+ itemVisiblePercentThreshold: 100,
+};
- // Optional: Add a loading indicator state specifically for deletion if needed
- // setDeleting(true);
+// --- Handle Search Input ---
+const handleInputChange = (text) => {
+ setUrl(text);
+ if (text.trim() !== "") {
+ setSearching(true);
+ // Debounce for search feedback
+ const timer = setTimeout(() => {
+ fetchLogs(activeFilter, text); // Search using API with current filter and search text
+ setSearching(false);
+ }, 500);
+
+ // Need useRef to clear timer properly in a complete implementation
+ return () => clearTimeout(timer);
+ } else {
+ setSearching(false);
+ fetchLogs(activeFilter, ""); // Clear search, fetch with current filter only
+ }
+};
- try {
- const response = await fetch(`${config.BASE_URL}/logs/${logId}`, {
- method: 'DELETE',
- headers: {
- // Add any necessary headers like Authorization if your API requires them
- 'Content-Type': 'application/json',
- },
- });
+// Handle search button press
+const handleSearch = () => {
+ fetchLogs(activeFilter, url);
+};
- console.log(`[Logs.js] DELETE response status for ID ${logId}:`, response.status);
+// --- Delete Logic ---
+const handleDeleteLog = async (logId) => {
+if (!logId) {
+ console.error("[Logs.js] handleDeleteLog called with invalid ID:", logId);
+ Alert.alert("Error", "Cannot delete log: Invalid ID.");
+ return;
+}
+console.log(`[Logs.js] Attempting to delete log with ID: ${logId}`);
- // Check if the deletion was successful (status code 200 OK)
- if (response.ok) {
- // If successful, THEN remove the log from the local state
- setLogs((prevLogs) => prevLogs.filter(log => log.id !== logId));
- console.log(`[Logs.js] Log ${logId} successfully deleted from state.`);
- Alert.alert("Success", "Log deleted successfully."); // Optional success feedback
- // The modal closes itself via handleClosePress called in DetailsModal.js after onDelete
- } else {
- // Handle errors (e.g., log not found, server error)
- const errorData = await response.json().catch(() => ({})); // Try to parse JSON error, default to empty object
- console.error(`[Logs.js] Failed to delete log ${logId}. Status: ${response.status}`, errorData);
- Alert.alert(
- "Deletion Failed",
- errorData.error || `Could not delete log (Status: ${response.status}). Please try again.`
- );
- }
- } catch (error) {
- // Handle network errors or other unexpected issues
- console.error(`[Logs.js] Error during fetch DELETE operation for ID ${logId}:`, error);
- Alert.alert("Error", "An error occurred while trying to delete the log. Check your connection.");
- } finally {
- // Optional: Stop delete-specific loading indicator
- // setDeleting(false);
- }
- };
-
+try {
+ const response = await fetch(`${config.BASE_URL}/logs/${logId}`, {
+ method: 'DELETE',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ console.log(`[Logs.js] DELETE response status for ID ${logId}:`, response.status);
- // --- Render ---
- console.log(`[Logs.js] Rendering component. Current modalVisible state: ${modalVisible}, Current logLoading state: ${logLoading}`);
+ if (response.ok) {
+ setLogs((prevLogs) => prevLogs.filter(log => log.id !== logId));
+ console.log(`[Logs.js] Log ${logId} successfully deleted from state.`);
+ Alert.alert("Success", "Log deleted successfully.");
+ } else {
+ const errorData = await response.json().catch(() => ({}));
+ console.error(`[Logs.js] Failed to delete log ${logId}. Status: ${response.status}`, errorData);
+ Alert.alert(
+ "Deletion Failed",
+ errorData.error || `Could not delete log (Status: ${response.status}). Please try again.`
+ );
+ }
+} catch (error) {
+ console.error(`[Logs.js] Error during fetch DELETE operation for ID ${logId}:`, error);
+ Alert.alert("Error", "An error occurred while trying to delete the log. Check your connection.");
+}
+};
- return (
-
- {/* --- Search Section --- */}
-
- Search:
-
-
-
-
-
-
-
+// --- Render ---
+console.log(`[Logs.js] Rendering component. Current modalVisible state: ${modalVisible}, Current logLoading state: ${logLoading}`);
+return (
+
+{/* --- Search Section --- */}
+
+Search:
+
+
+
+
+
+
+
- {/* --- Filter Section --- */}
-
-
-
+{/* --- Filter Section --- */}
+
+
+
- {/* --- Content Area (Spinner or List) --- */}
-
- {loading ? (
-
-
- Loading logs...
-
- ) : searching ? (
-
-
- {/* Searching... */}
-
- ) : displayedLogs.length === 0 ? (
-
- No logs found for "{activeFilter}" filter{url ? ` matching "${url}"` : ""}.
-
- ) : (
- item.id.toString()}
- renderItem={({ item }) => (
- {
- console.log(`[Logs.js] TouchableOpacity onPress - Calling showModal for ID: ${item.id}`);
- showModal(item.id);
- }}
- >
-
-
- )}
- showsVerticalScrollIndicator={false}
- contentContainerStyle={styles.listContent}
- onViewableItemsChanged={({ viewableItems: vItems }) => {
- viewableItems.value = vItems.map((vItem) => vItem.item.id);
+{/* --- Content Area (Spinner or List) --- */}
+
+ {loading ? (
+
+
+ Loading logs...
+
+ ) : searching ? (
+
+
+
+ ) : logs.length === 0 ? (
+
+ No logs found for "{activeFilter}" filter{url ? ` matching "${url}"` : ""}.
+
+ ) : (
+ item.id.toString()}
+ renderItem={({ item }) => (
+ {
+ console.log(`[Logs.js] TouchableOpacity onPress - Calling showModal for ID: ${item.id}`);
+ showModal(item.id);
+ }}
+ >
+
- )}
-
+
+ )}
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={styles.listContent}
+ onViewableItemsChanged={({ viewableItems: vItems }) => {
+ viewableItems.value = vItems.map((vItem) => vItem.item.id);
+ }}
+ viewabilityConfig={viewabilityConfig}
+ />
+ )}
+
- {/* --- Modal --- */}
- {/* Add the debug log before rendering */}
- {console.log(`[Logs.js] Preparing to render DetailsModal. Props being passed - visible: ${modalVisible || logLoading}, logLoading: ${logLoading}`)}
+{/* --- Modal --- */}
+{console.log(`[Logs.js] Preparing to render DetailsModal. Props being passed - visible: ${modalVisible || logLoading}, logLoading: ${logLoading}`)}
-
-
- );
+
+
+);
}
// --- Styles --- (Keep original Logs.js styles)
const styles = StyleSheet.create({
- container: {
- flex: 1,
- paddingHorizontal: 20,
- paddingTop: 20,
- paddingBottom: 0,
- backgroundColor: '#121212', // Example background color
- },
- searchSection: {
- marginBottom: 15,
- },
- filterSection: {
- marginBottom: 15,
- },
- contentArea: {
- flex: 1,
- },
- text: {
- color: "#31EE9A",
- fontSize: 12,
- marginBottom: 5,
- },
- inputWrapper: {
- position: "relative",
- },
- textInput: {
- fontSize: 14,
- height: 45,
- borderRadius: 12,
- color: "#000000",
- backgroundColor: "#3AED97",
- paddingRight: 45,
- paddingLeft: 15,
- },
- iconWrapper: {
- position: "absolute",
- right: 15,
- top: "50%",
- transform: [{ translateY: -12 }],
- },
- centeredIndicator: {
- flex: 1,
- justifyContent: "center",
- alignItems: "center",
- paddingBottom: 50,
- },
- loadingText: {
- marginTop: 10,
- color: "#31EE9A",
- fontSize: 16,
- },
- noResultsText: {
- color: "#AAAAAA", // Lighter grey
- fontSize: 16,
- textAlign: 'center',
- paddingHorizontal: 20, // Add some padding if text is long
- },
- listContent: {
- paddingBottom: 20,
- },
+container: {
+flex: 1,
+paddingHorizontal: 20,
+paddingTop: 20,
+paddingBottom: 0,
+backgroundColor: '#121212', // Example background color
+},
+searchSection: {
+marginBottom: 15,
+},
+filterSection: {
+marginBottom: 15,
+},
+contentArea: {
+flex: 1,
+},
+text: {
+color: "#31EE9A",
+fontSize: 12,
+marginBottom: 5,
+},
+inputWrapper: {
+position: "relative",
+},
+textInput: {
+fontSize: 14,
+height: 45,
+borderRadius: 12,
+color: "#000000",
+backgroundColor: "#3AED97",
+paddingRight: 45,
+paddingLeft: 15,
+},
+iconWrapper: {
+position: "absolute",
+right: 15,
+top: "50%",
+transform: [{ translateY: -12 }],
+},
+centeredIndicator: {
+flex: 1,
+justifyContent: "center",
+alignItems: "center",
+paddingBottom: 50,
+},
+loadingText: {
+marginTop: 10,
+color: "#31EE9A",
+fontSize: 16,
+},
+noResultsText: {
+color: "#AAAAAA", // Lighter grey
+fontSize: 16,
+textAlign: 'center',
+paddingHorizontal: 20, // Add some padding if text is long
+},
+listContent: {
+paddingBottom: 20,
+},
});
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index e053d707..77f304ac 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -489,6 +489,7 @@
"react-native-helmet-async": "^2.0.4",
"react-native-is-edge-to-edge": "^1.1.6",
"react-native-linear-gradient": "^2.8.3",
+ "react-native-modal": "^14.0.0-rc.1",
"react-native-pager-view": "6.5.1",
"react-native-paper": "^5.13.1",
"react-native-reanimated": "~3.16.1",
@@ -12933,6 +12934,15 @@
}
}
},
+ "node_modules/react-native-animatable": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.4.0.tgz",
+ "integrity": "sha512-DZwaDVWm2NBvBxf7I0wXKXLKb/TxDnkV53sWhCvei1pRyTX3MVFpkvdYBknNBqPrxYuAIlPxEp7gJOidIauUkw==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.8.1"
+ }
+ },
"node_modules/react-native-background-gradient": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/react-native-background-gradient/-/react-native-background-gradient-0.0.2.tgz",
@@ -13017,6 +13027,19 @@
"react-native": "*"
}
},
+ "node_modules/react-native-modal": {
+ "version": "14.0.0-rc.1",
+ "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-14.0.0-rc.1.tgz",
+ "integrity": "sha512-v5pvGyx1FlmBzdHyPqBsYQyS2mIJhVmuXyNo5EarIzxicKhuoul6XasXMviGcXboEUT0dTYWs88/VendojPiVw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-native-animatable": "1.4.0"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": ">=0.70.0"
+ }
+ },
"node_modules/react-native-pager-view": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.5.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index eaa3085e..613f1153 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -483,6 +483,7 @@
"react-native-helmet-async": "^2.0.4",
"react-native-is-edge-to-edge": "^1.1.6",
"react-native-linear-gradient": "^2.8.3",
+ "react-native-modal": "^14.0.0-rc.1",
"react-native-pager-view": "6.5.1",
"react-native-paper": "^5.13.1",
"react-native-reanimated": "~3.16.1",
diff --git a/package-lock.json b/package-lock.json
index 7074d9c0..f5ac707e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"react-native": "0.76.3",
"react-native-gesture-handler": "^2.24.0",
"react-native-linear-gradient": "^2.8.3",
+ "react-native-modal": "^14.0.0-rc.1",
"react-native-svg": "^15.10.0",
"tailwindcss": "^3.4.16"
},
@@ -5297,6 +5298,21 @@
"node": ">= 4.0.0"
}
},
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/axios": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
@@ -10571,6 +10587,18 @@
"node": ">= 6"
}
},
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/object-is": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
@@ -10616,18 +10644,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/object-inspect": {
- "version": "1.13.4",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
- "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@@ -11610,6 +11626,15 @@
}
}
},
+ "node_modules/react-native-animatable": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.4.0.tgz",
+ "integrity": "sha512-DZwaDVWm2NBvBxf7I0wXKXLKb/TxDnkV53sWhCvei1pRyTX3MVFpkvdYBknNBqPrxYuAIlPxEp7gJOidIauUkw==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.8.1"
+ }
+ },
"node_modules/react-native-css-interop": {
"version": "0.1.22",
"resolved": "https://registry.npmjs.org/react-native-css-interop/-/react-native-css-interop-0.1.22.tgz",
@@ -11678,6 +11703,19 @@
"react-native": "*"
}
},
+ "node_modules/react-native-modal": {
+ "version": "14.0.0-rc.1",
+ "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-14.0.0-rc.1.tgz",
+ "integrity": "sha512-v5pvGyx1FlmBzdHyPqBsYQyS2mIJhVmuXyNo5EarIzxicKhuoul6XasXMviGcXboEUT0dTYWs88/VendojPiVw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-native-animatable": "1.4.0"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": ">=0.70.0"
+ }
+ },
"node_modules/react-native-reanimated": {
"version": "3.16.3",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.16.3.tgz",
@@ -12147,6 +12185,23 @@
}
]
},
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
diff --git a/package.json b/package.json
index 880d751a..075c7ebb 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"@react-native-async-storage/async-storage": "1.23.1",
"axios": "^1.7.9",
"expo": "~52.0.11",
+ "expo-notifications": "~0.29.13",
"expo-status-bar": "~2.0.0",
"express": "^4.21.2",
"nativewind": "^4.1.23",
@@ -20,9 +21,9 @@
"react-native": "0.76.3",
"react-native-gesture-handler": "^2.24.0",
"react-native-linear-gradient": "^2.8.3",
+ "react-native-modal": "^14.0.0-rc.1",
"react-native-svg": "^15.10.0",
- "tailwindcss": "^3.4.16",
- "expo-notifications": "~0.29.13"
+ "tailwindcss": "^3.4.16"
},
"devDependencies": {
"@babel/core": "^7.20.0",