Skip to content
This repository has been archived by the owner on Dec 6, 2024. It is now read-only.

Display Product Details on Overview Screen #59

Merged
merged 5 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions backend/src/products/products.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
Post,
HttpException,
HttpStatus,
Get,
Param,
} from '@nestjs/common';
import { ProductDto } from './product.dto';
import { ProductsService } from './products.service';
Expand All @@ -28,4 +30,18 @@ export class ProductsController {
);
}
}

@Get(':productId')
async getProductDetails(@Param('productId') productId: string) {
try {
return this.productService.getProductDetails(productId);
} catch (error) {
console.error('Error:', error.message);

throw new HttpException(
{ error: error.message },
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
}
58 changes: 55 additions & 3 deletions backend/src/products/products.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Injectable } from '@nestjs/common';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ProductDto } from './product.dto';
import { Product } from 'src/interfaces/Product';
import { generateProductId } from 'src/common/utils/generateProductId';

@Injectable()
export class ProductsService {
private readonly PINATA_JWT = process.env.PINATA_JWT || '';
private readonly PINATA_GATEWAY =
process.env.PINATA_GATEWAY || 'https://gateway.pinata.cloud/ipfs/';

async pinToIPFS(product: Product) {
const url = 'https://api.pinata.cloud/pinning/pinFileToIPFS';
Expand All @@ -26,8 +28,6 @@ export class ProductsService {
body: data,
});

console.log('Response: ', await response);

if (!response.ok) {
const errorBody = await response.json();
console.error('Error Response:', errorBody);
Expand All @@ -51,4 +51,56 @@ export class ProductsService {
throw new Error('Error uploading to IPFS');
}
}

async getProductDetails(productId: string): Promise<Product> {
try {
// Step 1: Get all pins from Pinata to find our product
const searchUrl = 'https://api.pinata.cloud/data/pinList';
const searchResponse = await fetch(searchUrl, {
headers: {
Authorization: `Bearer ${this.PINATA_JWT}`,
},
});

if (!searchResponse.ok) {
throw new Error(`Failed to search pins: ${searchResponse.statusText}`);
}

const pinList = await searchResponse.json();

const targetPin = pinList.rows.find(
(pin: { metadata: { name: string } }) =>
pin.metadata?.name === `${productId}.txt`
);

if (!targetPin) {
throw new HttpException('Product not found', HttpStatus.NOT_FOUND);
}

// Step 3: Fetch the product data using the IPFS hash
const productUrl = `${this.PINATA_GATEWAY}${targetPin.ipfs_pin_hash}`;
const productResponse = await fetch(productUrl);

if (!productResponse.ok) {
throw new Error(
`Failed to fetch product: ${productResponse.statusText}`
);
}

const productData: Product = await productResponse.json();

// Step 4: Verify the product ID matches for security
if (productData.product_id !== productId) {
throw new HttpException('Product ID mismatch', HttpStatus.NOT_FOUND);
}

return productData;
} catch (error) {
console.error('Error fetching product:', error);
throw new HttpException(
'Failed to fetch product details',
HttpStatus.INTERNAL_SERVER_ERROR
);
}
}
}
70 changes: 70 additions & 0 deletions epress_backend/src/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ interface Product {
expiryDate: string;
}


const PINATA_GATEWAY = process.env.PINATA_GATEWAY || "https://gateway.pinata.cloud/ipfs/";

const PINATA_JWT: string = process.env.PINATA_JWT || "";

const pinToIPFS = async (product: Product) => {
Expand Down Expand Up @@ -63,3 +66,70 @@ export const submitProduct = async (req: Request, res: Response) => {
return res.status(500).json({ error: "Error uploading to IPFS" });
}
};


export const getProductDetails = async (req: Request, res: Response) => {
const { productId } = req.params;

if (!productId) {
return res.status(400).json({ error: "Product ID is required" });
}

try {
// Step 1: Get all pins from Pinata to find our product
const searchUrl = "https://api.pinata.cloud/data/pinList";
const searchResponse = await fetch(searchUrl, {
headers: {
Authorization: `Bearer ${PINATA_JWT}`
}
});

if (!searchResponse.ok) {
throw new Error(`Failed to search pins: ${searchResponse.status}`);
}

const pinList = await searchResponse.json();


const targetPin = pinList.rows.find((pin: any) =>
pin.metadata?.name === `${productId}.txt`
);

if (!targetPin) {
return res.status(404).json({
success: false,
error: "Product not found"
});
}

// Step 3: Fetch the product data using the IPFS hash
const productUrl = `${PINATA_GATEWAY}${targetPin.ipfs_pin_hash}`;
const productResponse = await fetch(productUrl);

if (!productResponse.ok) {
throw new Error(`Failed to fetch product: ${productResponse.status}`);
}

const productData: Product = await productResponse.json();

// Step 4: Verify the product ID matches for security
if (productData.product_id !== productId) {
return res.status(404).json({
success: false,
error: "Product ID mismatch"
});
}

return res.json({
success: true,
product: productData
});

} catch (error) {
console.error("Error fetching product:", error);
return res.status(500).json({
success: false,
error: "Failed to fetch product details"
});
}
};
4 changes: 3 additions & 1 deletion epress_backend/src/routes.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Router } from "express";
import { submitProduct } from "./product";
import { submitProduct, getProductDetails } from "./product";

const router = Router();

router.post("/submit", submitProduct);
router.get('/scan/:productId', getProductDetails);


export default router;
3 changes: 2 additions & 1 deletion frontend/src/app/scan/[product]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';


import Link from 'next/link';
import { useEffect, useMemo, useState } from 'react';
import { useAccount } from '@starknet-react/core';
Expand Down Expand Up @@ -85,7 +86,7 @@ export default function ScanPage() {
//TODO: center, add background, make it responsive and pixel perfect
//TODO: https://www.figma.com/design/dwXPww5jcUl55azC9EQ8H0/SCANGUARD?node-id=14-13&node-type=canvas&t=Q8gtO0EqfOBYEqke-0
*/}
<ProductPreview />
<ProductPreview productId='product' />
{open && <ScanProduct />}

<ConnectModal isOpen={openConnectedModal} onClose={toggleUserModal} />
Expand Down
100 changes: 70 additions & 30 deletions frontend/src/components/ProductPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,62 @@
'use client';
import { useState } from 'react';

import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import FlagProductModal from './FlagProductModal';
import { CheckmarkIcon, FlagIcon, NoticeIcon } from '@/assets/icons';

export default function ProductPreview() {
export default function ProductPreview({ productId }: { productId: string }) {
const [isFlagging, setIsFlagging] = useState(false);
const product = {
name: 'Jaicatace Juice',
authenticityStatus: 'Not Authentic',
manufacturer: 'Jaicatace Beverages Ltd.',
dateOfScan: 'October 17, 2024',
batchNumber: 'JT2024-BX-0923',
timesScanned: 153,
trustScore: 90,
reportedIssues: 2,
image: 'product-overview.png',
};

interface Product {
product_id: string;
name: string;
image: string;
manufacturer: string;
manufactureDate: string;
expiryDate: string;
trustScore:any;
authenticityStatus:string;
reportedIssues:string;
timesScanned:string;
batchNumber:number;
dateOfScan:string;
}

const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [product, setProduct] = useState<Product | null>(null);

useEffect(() => {
const fetchProduct = async () => {
if (!productId) return;

setLoading(true);
setError('');

try {
const response = await fetch(`/api/scan/${productId}`);
const data = await response.json();

if (!response.ok) {
throw new Error(data.error || 'Failed to fetch product details');
}
console.log(data)
if (data.success && data.product) {
setProduct(data.product);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch product details');
} finally {
setLoading(false);
}
};

fetchProduct();
}, [productId]);
console.log(product)
const formattedManufactureDate = 'lorem';// new Date(product.manufactureDate).toLocaleDateString();
const formattedExpiryDate = 'lorem'; //new Date(product.expiryDate).toLocaleDateString();

return (
<div className="pt-[64px]">
Expand All @@ -30,7 +70,7 @@
document.body
)}
<div className="w-full max-w-6xl relative lg:bg-[#1E1E1E] rounded-3xl p-6 lg:p-[88px] text-white mx-auto lg:border-[1px] border-[#303030] grid grid-cols-1 lg:grid-cols-2 gap-y-5 gap-x-[80px] items-stretch">
<img

Check warning on line 73 in frontend/src/components/ProductPreview.tsx

View workflow job for this annotation

GitHub Actions / lint-and-build-frontend

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src="/productEllipse.svg"
alt=""
className="absolute hidden lg:flex top-[-30px] left-[60px] z-[1]"
Expand All @@ -38,35 +78,35 @@
{/* Product Image Section */}
<div className="z-10">
<div className=" h-[402px] flex items-center justify-center">
<img

Check warning on line 81 in frontend/src/components/ProductPreview.tsx

View workflow job for this annotation

GitHub Actions / lint-and-build-frontend

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src="/product-overview.png"
alt="Jaicatace Juice"
src= {product?.image}
alt={product?.name ? product?.name:""}
className="h-full"
/>
</div>

{/* Metrics Section */}
<div className="grid grid-cols-3 gap-x-[9px] mt-12">
<div className="bg-[#232323] border-[1px] border-[#303030] rounded-2xl py-5 relative flex flex-col items-center">
{product.trustScore <= 30 && (
{product?.trustScore <= 30 && (
<NoticeIcon className="absolute top-[-12px] right-3" />
)}
{product.trustScore > 80 && (
{product?.trustScore > 80 && (
<CheckmarkIcon className="absolute top-[-12px] right-3" />
)}
<h3 className="text-sm leading-4 text-[#F9F9F9] mb-2">
Trust Score
</h3>
<div
className={`text-[24px] md:text-[32px] md:leading-[39px] font-semibold ${
product.trustScore <= 30 && 'text-[#FF2828]'
product?.trustScore <= 30 && 'text-[#FF2828]'
} ${
product.trustScore > 30 &&
product.trustScore <= 80 &&
product?.trustScore > 30 &&
product?.trustScore <= 80 &&
'text-[#F9F9F9]'
} ${product.trustScore > 80 && 'text-[#28FF37]'}`}
} ${product?.trustScore > 80 && 'text-[#28FF37]'}`}
>
{product.trustScore}%
{product?.trustScore}%
</div>
</div>

Expand Down Expand Up @@ -95,12 +135,12 @@
<div className="flex justify-between items-start mb-6">
<div>
<h2 className="text-xl leading-6 font-medium text-[#F9F9F9] mb-2">
{product.name}
{product?.name}
</h2>
<h5 className="text-base leading-6 text-[#ACACAC]">
Authenticity status:{' '}
<span className="text-[#F9F9F9]">
{product.authenticityStatus}
{product?.authenticityStatus}
</span>
</h5>
</div>
Expand All @@ -116,34 +156,34 @@
<div className="py-4 border-y-[1px] border-y-[#303030] flex flex-col gap-y-4 mb-4">
<h4 className="text-base leading-6 text-[#ACACAC]">
Manufacturer:
<span className="text-[#F9F9F9]"> {product.manufacturer}</span>
<span className="text-[#F9F9F9]"> {product?.manufacturer}</span>
</h4>
<h4 className="text-base leading-6 text-[#ACACAC]">
Date of Scan:
<span className="text-[#F9F9F9]"> {product.dateOfScan}</span>
<span className="text-[#F9F9F9]"> {product?.dateOfScan}</span>
</h4>
<h4 className="text-base leading-6 text-[#ACACAC]">
Batch Number:
<span className="text-[#F9F9F9]"> {product.batchNumber}</span>
<span className="text-[#F9F9F9]"> {product?.batchNumber}</span>
</h4>
</div>
<div className="py-4 flex flex-col gap-y-4 mb-[30px] border-b-[1px] border-b-[#303030]">
<h4 className="text-base leading-6 text-[#ACACAC]">
Times Scanned:
<span className="text-[#F9F9F9]">
{' '}
{product.timesScanned} times
{product?.timesScanned} times
</span>
</h4>
<h4 className="text-base leading-6 text-[#ACACAC]">
Trust Score:
<span className="text-[#F9F9F9]"> {product.trustScore}/100</span>
<span className="text-[#F9F9F9]"> {product?.trustScore}/100</span>
</h4>
<h4 className="text-base leading-6 text-[#ACACAC]">
Reported Issues:
<span className="text-[#F9F9F9]">
{' '}
{product.reportedIssues} flags
{product?.reportedIssues} flags
</span>
</h4>
</div>
Expand Down
Loading
Loading