Skip to content

insights page #228

Open
saksh2110 wants to merge 7 commits intoDataBytes-Organisation:mainfrom
saksh2110:main
Open

insights page #228
saksh2110 wants to merge 7 commits intoDataBytes-Organisation:mainfrom
saksh2110:main

Conversation

@saksh2110
Copy link

This pull request adds fully implemented Insights functionality to DiscountMate, replacing placeholder content with working pages and reusable logic.
Key updates include:

Implemented complete Insights pages for:
Savings Overview
Category Trends
Retailer Performance
Added shared insights data utilities for:
Price parsing and savings calculations
Retailer comparison and cheapest-price normalization
Integrated insights navigation with homepage cards and routing
Ensured pages render with mock data fallback while backend integration is finalised
Resolved merge conflicts and synced fork with upstream main
These changes improve analytical depth, support user decision-making, and align the Insights section with the overall DiscountMate product vision.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds Insights functionality to DiscountMate, providing analytical views for savings, category trends, and retailer performance. However, it also includes an unrelated profile component and contains several critical issues that need to be addressed.

Changes:

  • Implemented three Insights pages (Savings Overview, Category Trends, Retailer Performance) with supporting utilities
  • Added reusable UI components for displaying insights statistics and bar charts
  • Integrated insights navigation through homepage cards with category routing
  • Added profile management screen with image upload functionality (unrelated to Insights)
  • Updated package dependencies (automatic lockfile updates)

Reviewed changes

Copilot reviewed 14 out of 17 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
Frontend/src/screens/profiles/profile.tsx New profile screen with image upload (unrelated to PR scope, contains critical import path errors)
Frontend/lib/insightsdata.ts Duplicate insights data utilities (should be removed)
Frontend/lib/insightsData.ts Core insights data fetching and calculation utilities with mock fallback
Frontend/components/insights/InsightStatCard.tsx Reusable stat card component for displaying key metrics
Frontend/components/insights/InsightBarRow.tsx Reusable bar chart row component for comparative visualizations
Frontend/components/home/TrendingInsightsSection.tsx Updated homepage section with category navigation
Frontend/app/insights/index.tsx Insights landing page with navigation cards
Frontend/app/insights/savings.tsx Savings overview analysis page
Frontend/app/insights/categories.tsx Category trends analysis page
Frontend/app/insights/retailers.tsx Retailer performance comparison page
Frontend/package-lock.json Automatic dependency resolution updates
Backend/yarn.lock Minor dependency addition (fsevents)
Backend/package-lock.json Dependency peer marking updates
Files not reviewed (2)
  • Backend/package-lock.json: Language not supported
  • Frontend/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +37 to +38
// IMPORTANT: Replace with your machine IP
const API_URL = "http://172.16.11.120:3000";
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded IP address should not be committed to the repository. This IP address (172.16.11.120:3000) appears to be a local development machine address. Use an environment variable instead (e.g., process.env.EXPO_PUBLIC_API_BASE_URL or similar) to allow configuration across different environments without code changes.

Suggested change
// IMPORTANT: Replace with your machine IP
const API_URL = "http://172.16.11.120:3000";
// API base URL is configurable via environment variable
const API_URL = process.env.EXPO_PUBLIC_API_BASE_URL || "http://localhost:3000";

Copilot uses AI. Check for mistakes.
import { useAuth } from "../context/AuthContext";
import Entypo from "react-native-vector-icons/Entypo";

const defaultImage = require("../assets/images/defaultprofileimage.png");
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import path "../assets/images/defaultprofileimage.png" appears incorrect. Based on the file location being Frontend/src/screens/profiles/profile.tsx, the path should likely be "../../assets/images/defaultprofileimage.png" (two levels up) or use an absolute import path. This will cause a runtime error when the image cannot be found.

Suggested change
const defaultImage = require("../assets/images/defaultprofileimage.png");
const defaultImage = require("../../assets/images/defaultprofileimage.png");

Copilot uses AI. Check for mistakes.
import axios from "axios";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as ImagePicker from "expo-image-picker";
import { useAuth } from "../context/AuthContext";
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import path "../context/AuthContext" appears incorrect. Since this file is at Frontend/src/screens/profiles/profile.tsx, the correct path should be "../../context/AuthContext" (two levels up to reach src/) or use an absolute import. This will cause a module resolution error.

Suggested change
import { useAuth } from "../context/AuthContext";
import { useAuth } from "../../context/AuthContext";

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +105
console.log("Profile fetch error:", err);
}
};

useEffect(() => {
fetchProfile();
}, []);

const pickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
base64: true,
quality: 1,
});

if (!result.canceled) {
const file = result.assets[0];
setImage(file.uri);
await uploadImage(file);
setModalVisible(true);
}
};

const uploadImage = async (file: any) => {
const token = await AsyncStorage.getItem("authToken");
if (!token) return;

const formData = new FormData();
formData.append("image", {
uri: file.uri,
type: file.mimeType || "image/jpeg",
name: file.fileName || "profile.jpg",
});

try {
await axios.post(`${API_URL}/api/users/upload-profile-image`, formData, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data",
},
});

fetchProfile(); // refresh UI
} catch (err) {
console.log("Upload error:", err);
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling using console.log is inconsistent with the empty catch blocks elsewhere. Consider using a consistent error handling pattern across the file, such as a dedicated error logging utility or at least console.error for error messages.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +126
export type Retailer = {
name: string;
price: string;
originalPrice?: string;
isCheapest?: boolean;
};

export type InsightsProduct = {
id: string;
name: string;
subtitle?: string;
category?: string;
retailers: Retailer[];
};

export function parseMoney(input?: string): number | null {
if (!input) return null;
const numeric = parseFloat(String(input).replace(/[^0-9.]/g, ""));
return Number.isFinite(numeric) ? numeric : null;
}

export function computeRetailerRow(ret: Retailer) {
const current = parseMoney(ret.price);
const original = parseMoney(ret.originalPrice);
const savings =
current != null && original != null && original > current ? original - current : 0;

const discountPct =
current != null && original != null && original > 0 && original > current
? (savings / original) * 100
: 0;

return { current, original, savings, discountPct };
}

export function normalizeCheapest(retailers: Retailer[]): Retailer[] {
if (!retailers || retailers.length === 0) return [];
const hasExplicit = retailers.some((r) => r.isCheapest);
if (hasExplicit) return retailers;

const scored = retailers.map((r) => {
const current = parseMoney(r.price);
return { r, current: current ?? Number.POSITIVE_INFINITY };
});

const min = scored.reduce((a, b) => (b.current < a.current ? b : a));
return retailers.map((r) => (r === min.r ? { ...r, isCheapest: true } : r));
}

export async function fetchProductsForInsights(): Promise<InsightsProduct[]> {
const base = process.env.EXPO_PUBLIC_API_BASE_URL;

if (!base) return mockProducts();

const candidates = [
`${base}/products`,
`${base}/api/products`,
`${base}/specials`,
`${base}/api/specials`,
];

for (const url of candidates) {
try {
const res = await fetch(url);
if (!res.ok) continue;
const json = await res.json();

const arr: any[] =
Array.isArray(json) ? json : Array.isArray(json?.products) ? json.products : [];

const cleaned: InsightsProduct[] = arr
.filter((p) => p && p.id && p.name && Array.isArray(p.retailers))
.map((p) => ({
id: String(p.id),
name: String(p.name),
subtitle: p.subtitle ? String(p.subtitle) : "",
category: p.category ? String(p.category) : p.subtitle ? String(p.subtitle) : "Other",
retailers: (p.retailers || []).map((r: any) => ({
name: String(r.name ?? r.retailer ?? "Unknown"),
price: String(r.price ?? ""),
originalPrice: r.originalPrice ? String(r.originalPrice) : undefined,
isCheapest: Boolean(r.isCheapest),
})),
}));

if (cleaned.length) return cleaned;
} catch {}
}

return mockProducts();
}

function mockProducts(): InsightsProduct[] {
return [
{
id: "p1",
name: "Greek Yogurt 1kg",
category: "Dairy, Eggs & Fridge",
retailers: normalizeCheapest([
{ name: "Coles", price: "$6.00", originalPrice: "$7.50" },
{ name: "Woolworths", price: "$6.50", originalPrice: "$7.50" },
{ name: "Aldi", price: "$5.80", originalPrice: "$7.00" },
]),
},
{
id: "p2",
name: "Dishwashing Liquid 750ml",
category: "Household",
retailers: normalizeCheapest([
{ name: "Coles", price: "$4.50", originalPrice: "$6.00" },
{ name: "Woolworths", price: "$4.20", originalPrice: "$6.00" },
{ name: "Aldi", price: "$4.80", originalPrice: "$5.50" },
]),
},
{
id: "p3",
name: "Bananas 1kg",
category: "Fruit & Vegetables",
retailers: normalizeCheapest([
{ name: "Coles", price: "$3.40", originalPrice: "$4.20" },
{ name: "Woolworths", price: "$3.60", originalPrice: "$4.20" },
{ name: "Aldi", price: "$3.20", originalPrice: "$4.00" },
]),
},
];
}
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two nearly identical files exist with different casing: insightsdata.ts and insightsData.ts. This creates confusion and potential issues on case-sensitive filesystems (Linux/Unix). Only one version should exist. Based on JavaScript/TypeScript conventions, insightsData.ts (with capital D) is the preferred naming. The insightsdata.ts file should be removed.

Copilot uses AI. Check for mistakes.
}));

if (cleaned.length) return cleaned;
} catch {}
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty catch block silently swallows errors, making debugging difficult. At minimum, log the error to help diagnose API connection issues during development and production troubleshooting.

Suggested change
} catch {}
} catch (err) {
console.error("Failed to fetch insights products from", url, err);
}

Copilot uses AI. Check for mistakes.
}));

if (cleaned.length) return cleaned;
} catch {
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty catch block silently swallows errors, making debugging difficult. At minimum, log the error to help diagnose API connection issues during development and production troubleshooting.

Suggested change
} catch {
} catch (error) {
console.error("Failed to fetch insights products from URL:", url, error);

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +233
import React, { useEffect, useState } from "react";
import {
View,
Text,
StyleSheet,
Image,
TouchableOpacity,
ScrollView,
Modal,
} from "react-native";
import axios from "axios";
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as ImagePicker from "expo-image-picker";
import { useAuth } from "../context/AuthContext";
import Entypo from "react-native-vector-icons/Entypo";

const defaultImage = require("../assets/images/defaultprofileimage.png");

interface Profile {
user_fname: string;
user_lname: string;
email: string;
phone_number: string;
address: string;
profile_image?: {
mime: string;
content: string;
} | null;
}

export default function Profile() {
const { logout } = useAuth();
const [profile, setProfile] = useState<Profile | null>(null);
const [image, setImage] = useState<string | null>(null);
const [modalVisible, setModalVisible] = useState(false);

// IMPORTANT: Replace with your machine IP
const API_URL = "http://172.16.11.120:3000";

const formatImage = (img: any) => {
if (!img) return null;
return `data:${img.mime};base64,${img.content}`;
};

const fetchProfile = async () => {
try {
const token = await AsyncStorage.getItem("authToken");
if (!token) return;

const response = await axios.get(`${API_URL}/api/users/profile`, {
headers: { Authorization: `Bearer ${token}` },
});

setProfile(response.data);

if (response.data.profile_image) {
setImage(formatImage(response.data.profile_image));
}
} catch (err) {
console.log("Profile fetch error:", err);
}
};

useEffect(() => {
fetchProfile();
}, []);

const pickImage = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
base64: true,
quality: 1,
});

if (!result.canceled) {
const file = result.assets[0];
setImage(file.uri);
await uploadImage(file);
setModalVisible(true);
}
};

const uploadImage = async (file: any) => {
const token = await AsyncStorage.getItem("authToken");
if (!token) return;

const formData = new FormData();
formData.append("image", {
uri: file.uri,
type: file.mimeType || "image/jpeg",
name: file.fileName || "profile.jpg",
});

try {
await axios.post(`${API_URL}/api/users/upload-profile-image`, formData, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data",
},
});

fetchProfile(); // refresh UI
} catch (err) {
console.log("Upload error:", err);
}
};

const signOut = () => {
logout();
setProfile(null);
};

return (
<ScrollView style={styles.container}>
<View style={styles.card}>
<Image
source={image ? { uri: image } : defaultImage}
style={styles.profileImage}
/>

<Text style={styles.name}>
{profile?.user_fname} {profile?.user_lname}
</Text>

<Text style={styles.text}>{profile?.email}</Text>
<Text style={styles.text}>{profile?.phone_number}</Text>
<Text style={styles.text}>{profile?.address}</Text>

<TouchableOpacity style={styles.button} onPress={pickImage}>
<Entypo name="camera" size={20} color="#fff" />
<Text style={styles.buttonText}>Change Profile Picture</Text>
</TouchableOpacity>

<TouchableOpacity style={styles.logoutButton} onPress={signOut}>
<Text style={styles.logoutButtonText}>Logout</Text>
</TouchableOpacity>
</View>

{/* Success Modal */}
<Modal transparent visible={modalVisible}>
<View style={styles.modalOverlay}>
<View style={styles.modal}>
<Text style={styles.modalText}>Profile Updated!</Text>
<TouchableOpacity
style={styles.closeBtn}
onPress={() => setModalVisible(false)}
>
<Text style={styles.closeTxt}>OK</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
</ScrollView>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: "#f5f5f5",
flex: 1,
padding: 20,
},
card: {
backgroundColor: "#fff",
padding: 25,
borderRadius: 15,
alignItems: "center",
elevation: 5,
},
profileImage: {
width: 140,
height: 140,
borderRadius: 80,
marginBottom: 20,
},
name: {
fontSize: 22,
fontWeight: "600",
marginBottom: 10,
},
text: {
fontSize: 16,
marginBottom: 4,
color: "#444",
},
button: {
flexDirection: "row",
backgroundColor: "#0c8",
padding: 10,
borderRadius: 8,
marginVertical: 20,
},
buttonText: {
color: "#fff",
marginLeft: 8,
fontSize: 16,
},
logoutButton: {
backgroundColor: "#d9534f",
padding: 12,
borderRadius: 8,
},
logoutButtonText: {
color: "#fff",
fontSize: 16,
},
modalOverlay: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(0,0,0,0.4)",
},
modal: {
backgroundColor: "#fff",
padding: 25,
borderRadius: 10,
},
modalText: {
fontSize: 18,
marginBottom: 20,
textAlign: "center",
},
closeBtn: {
backgroundColor: "#0c8",
borderRadius: 6,
padding: 10,
},
closeTxt: {
color: "#fff",
textAlign: "center",
},
});
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The profile screen is added to Frontend/src/screens/profiles/profile.tsx but the PR description focuses on Insights functionality. This profile component appears unrelated to the stated purpose of implementing Insights pages. Consider moving profile-related changes to a separate PR to maintain focused, single-purpose pull requests.

Copilot uses AI. Check for mistakes.
<Text className="text-sm text-primary_green font-semibold">
View all dairy deals
</Text>
<FontAwesome6 name="arrow-right" size={14} color="#16A34A" />
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded color value "#16A34A" should use the theme color variable "text-primary_green" className or a defined color constant to maintain consistency with the design system. The same applies to "#F97316" and "#2563EB" on lines 92 and 129.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant