{"id":21635286,"url":"https://github.com/sarthak-0-sach/amazon_webscraper_application","last_synced_at":"2026-02-08T13:35:41.197Z","repository":{"id":242215625,"uuid":"808886343","full_name":"SartHak-0-Sach/Amazon_WebScraper_application","owner":"SartHak-0-Sach","description":"A Next.js and Bright Data-powered e-commerce product scraping site. Get notified on price drops and stock status. Automate with cron jobs.","archived":false,"fork":false,"pushed_at":"2024-10-22T05:50:30.000Z","size":2976,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-11T15:48:29.750Z","etag":null,"topics":["bright-data","cheerio","headless-ui","mongodb","nextjs","nodemailer","responsive","tailwind-css","web-scraper"],"latest_commit_sha":null,"homepage":"https://pricewise-jsm.vercel.app/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SartHak-0-Sach.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-06-01T04:26:58.000Z","updated_at":"2024-10-22T05:50:34.000Z","dependencies_parsed_at":"2024-06-08T08:28:59.371Z","dependency_job_id":"c6bc08f6-5357-4b74-9cc6-0c38b8ec578e","html_url":"https://github.com/SartHak-0-Sach/Amazon_WebScraper_application","commit_stats":null,"previous_names":["sarthak-0-sach/amazon_webscraper_application"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/SartHak-0-Sach/Amazon_WebScraper_application","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SartHak-0-Sach%2FAmazon_WebScraper_application","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SartHak-0-Sach%2FAmazon_WebScraper_application/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SartHak-0-Sach%2FAmazon_WebScraper_application/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SartHak-0-Sach%2FAmazon_WebScraper_application/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SartHak-0-Sach","download_url":"https://codeload.github.com/SartHak-0-Sach/Amazon_WebScraper_application/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SartHak-0-Sach%2FAmazon_WebScraper_application/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29231509,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-08T13:10:22.947Z","status":"ssl_error","status_checked_at":"2026-02-08T13:08:18.779Z","response_time":57,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bright-data","cheerio","headless-ui","mongodb","nextjs","nodemailer","responsive","tailwind-css","web-scraper"],"created_at":"2024-11-25T03:20:36.683Z","updated_at":"2026-02-08T13:35:41.177Z","avatar_url":"https://github.com/SartHak-0-Sach.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv\u003e\n  \u003cimg src=\"https://img.shields.io/badge/-Web_Scraping-black?style=for-the-badge\u0026logoColor=white\u0026color=FF0000\" alt=\"webscraping\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/-Next_JS-black?style=for-the-badge\u0026logoColor=white\u0026logo=nextdotjs\u0026color=000000\" alt=\"nextjs\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/-Tailwind_CSS-black?style=for-the-badge\u0026logoColor=white\u0026logo=tailwindcss\u0026color=06B6D4\" alt=\"tailwindcss\" /\u003e\n  \u003cimg src=\"https://img.shields.io/badge/-MongoDB-black?style=for-the-badge\u0026logoColor=white\u0026logo=mongodb\u0026color=47A248\" alt=\"mongodb\" /\u003e\n\u003c/div\u003e\n\n## 📋 \u003ca name=\"table\"\u003eTable of Contents\u003c/a\u003e\n\n1. 🤖 [Introduction](#introduction)\n2. ⚙️ [Tech Stack](#tech-stack)\n3. 🔋 [Features](#features)\n4. 🤸 [Quick Start](#quick-start)\n5. 🕸️ [Snippets](#snippets)\n6. 🔗 [Links](#links)\n\n## \u003ca name=\"introduction\"\u003e🤖 Introduction\u003c/a\u003e\n\nDeveloped using Next.js and Bright Data's webunlocker, this e-commerce product scraping site is designed to assist users in making informed decisions. It notifies users when a product drops in price and helps competitors by alerting them when the product is out of stock, all managed through cron jobs.\n\n## \u003ca name=\"tech-stack\"\u003e⚙️ Tech Stack\u003c/a\u003e\n\n- Next.js\n- Bright Data\n- Cheerio\n- Nodemailer\n- MongoDB\n- Headless UI\n- Tailwind CSS\n\n## \u003ca name=\"features\"\u003e🔋 Features\u003c/a\u003e\n\n👉 **Header with Carousel**: Visually appealing header with a carousel showcasing key features and benefits\n\n👉 **Product Scraping**: A search bar allowing users to input Amazon product links for scraping.\n\n👉 **Scraped Projects**: Displays the details of products scraped so far, offering insights into tracked items.\n\n👉 **Scraped Product Details**: Showcase the product image, title, pricing, details, and other relevant information scraped from the original website\n\n👉 **Track Option**: Modal for users to provide email addresses and opt-in for tracking.\n\n👉 **Email Notifications**: Send emails product alert emails for various scenarios, e.g., back in stock alerts or lowest price notifications.\n\n👉 **Automated Cron Jobs**: Utilize cron jobs to automate periodic scraping, ensuring data is up-to-date.\n\nand many more, including code architecture and reusability \n\n## \u003ca name=\"quick-start\"\u003e🤸 Quick Start\u003c/a\u003e\n\nFollow these steps to set up the project locally on your machine.\n\n**Prerequisites**\n\nMake sure you have the following installed on your machine:\n\n- [Git](https://git-scm.com/)\n- [Node.js](https://nodejs.org/en)\n- [npm](https://www.npmjs.com/) (Node Package Manager)\n\n**Cloning the Repository**\n\n```bash\ngit clone https://github.com/SartHak-0-Sach/Amazon_WebScraper_application\ncd Amazon_WebScraper_application\n```\n\n**Installation**\n\nInstall the project dependencies using npm:\n\n```bash\nnpm install\n```\n\n**Set Up Environment Variables**\n\nCreate a new file named `.env` in the root of your project and add the following content:\n\n```env\n#SCRAPER\nBRIGHT_DATA_USERNAME=\nBRIGHT_DATA_PASSWORD=\n\n#DB\nMONGODB_URI=\n\n#OUTLOOK\nEMAIL_USER=\nEMAIL_PASS=\n```\n\nReplace the placeholder values with your actual credentials. You can obtain these credentials by signing up on these specific websites from [BrightData](https://brightdata.com/), [MongoDB](https://www.mongodb.com/), and [Node Mailer](https://nodemailer.com/)\n\n**Running the Project**\n\n```bash\nnpm run dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) in your browser to view the project.\n\n## \u003ca name=\"snippets\"\u003e🕸️ Snippets\u003c/a\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ecron.route.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { NextResponse } from \"next/server\";\n\nimport { getLowestPrice, getHighestPrice, getAveragePrice, getEmailNotifType } from \"@/lib/utils\";\nimport { connectToDB } from \"@/lib/mongoose\";\nimport Product from \"@/lib/models/product.model\";\nimport { scrapeAmazonProduct } from \"@/lib/scraper\";\nimport { generateEmailBody, sendEmail } from \"@/lib/nodemailer\";\n\nexport const maxDuration = 300; // This function can run for a maximum of 300 seconds\nexport const dynamic = \"force-dynamic\";\nexport const revalidate = 0;\n\nexport async function GET(request: Request) {\n  try {\n    connectToDB();\n\n    const products = await Product.find({});\n\n    if (!products) throw new Error(\"No product fetched\");\n\n    // ======================== 1 SCRAPE LATEST PRODUCT DETAILS \u0026 UPDATE DB\n    const updatedProducts = await Promise.all(\n      products.map(async (currentProduct) =\u003e {\n        // Scrape product\n        const scrapedProduct = await scrapeAmazonProduct(currentProduct.url);\n\n        if (!scrapedProduct) return;\n\n        const updatedPriceHistory = [\n          ...currentProduct.priceHistory,\n          {\n            price: scrapedProduct.currentPrice,\n          },\n        ];\n\n        const product = {\n          ...scrapedProduct,\n          priceHistory: updatedPriceHistory,\n          lowestPrice: getLowestPrice(updatedPriceHistory),\n          highestPrice: getHighestPrice(updatedPriceHistory),\n          averagePrice: getAveragePrice(updatedPriceHistory),\n        };\n\n        // Update Products in DB\n        const updatedProduct = await Product.findOneAndUpdate(\n          {\n            url: product.url,\n          },\n          product\n        );\n\n        // ======================== 2 CHECK EACH PRODUCT'S STATUS \u0026 SEND EMAIL ACCORDINGLY\n        const emailNotifType = getEmailNotifType(\n          scrapedProduct,\n          currentProduct\n        );\n\n        if (emailNotifType \u0026\u0026 updatedProduct.users.length \u003e 0) {\n          const productInfo = {\n            title: updatedProduct.title,\n            url: updatedProduct.url,\n          };\n          // Construct emailContent\n          const emailContent = await generateEmailBody(productInfo, emailNotifType);\n          // Get array of user emails\n          const userEmails = updatedProduct.users.map((user: any) =\u003e user.email);\n          // Send email notification\n          await sendEmail(emailContent, userEmails);\n        }\n\n        return updatedProduct;\n      })\n    );\n\n    return NextResponse.json({\n      message: \"Ok\",\n      data: updatedProducts,\n    });\n  } catch (error: any) {\n    throw new Error(`Failed to get all products: ${error.message}`);\n  }\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003egenerateEmailBody.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nexport async function generateEmailBody(\n  product: EmailProductInfo,\n  type: NotificationType\n  ) {\n  const THRESHOLD_PERCENTAGE = 40;\n  // Shorten the product title\n  const shortenedTitle =\n    product.title.length \u003e 20\n      ? `${product.title.substring(0, 20)}...`\n      : product.title;\n\n  let subject = \"\";\n  let body = \"\";\n\n  switch (type) {\n    case Notification.WELCOME:\n      subject = `Welcome to Price Tracking for ${shortenedTitle}`;\n      body = `\n        \u003cdiv\u003e\n          \u003ch2\u003eWelcome to PriceWise 🚀\u003c/h2\u003e\n          \u003cp\u003eYou are now tracking ${product.title}.\u003c/p\u003e\n          \u003cp\u003eHere's an example of how you'll receive updates:\u003c/p\u003e\n          \u003cdiv style=\"border: 1px solid #ccc; padding: 10px; background-color: #f8f8f8;\"\u003e\n            \u003ch3\u003e${product.title} is back in stock!\u003c/h3\u003e\n            \u003cp\u003eWe're excited to let you know that ${product.title} is now back in stock.\u003c/p\u003e\n            \u003cp\u003eDon't miss out - \u003ca href=\"${product.url}\" target=\"_blank\" rel=\"noopener noreferrer\"\u003ebuy it now\u003c/a\u003e!\u003c/p\u003e\n            \u003cimg src=\"https://i.ibb.co/pwFBRMC/Screenshot-2023-09-26-at-1-47-50-AM.png\" alt=\"Product Image\" style=\"max-width: 100%;\" /\u003e\n          \u003c/div\u003e\n          \u003cp\u003eStay tuned for more updates on ${product.title} and other products you're tracking.\u003c/p\u003e\n        \u003c/div\u003e\n      `;\n      break;\n\n    case Notification.CHANGE_OF_STOCK:\n      subject = `${shortenedTitle} is now back in stock!`;\n      body = `\n        \u003cdiv\u003e\n          \u003ch4\u003eHey, ${product.title} is now restocked! Grab yours before they run out again!\u003c/h4\u003e\n          \u003cp\u003eSee the product \u003ca href=\"${product.url}\" target=\"_blank\" rel=\"noopener noreferrer\"\u003ehere\u003c/a\u003e.\u003c/p\u003e\n        \u003c/div\u003e\n      `;\n      break;\n\n    case Notification.LOWEST_PRICE:\n      subject = `Lowest Price Alert for ${shortenedTitle}`;\n      body = `\n        \u003cdiv\u003e\n          \u003ch4\u003eHey, ${product.title} has reached its lowest price ever!!\u003c/h4\u003e\n          \u003cp\u003eGrab the product \u003ca href=\"${product.url}\" target=\"_blank\" rel=\"noopener noreferrer\"\u003ehere\u003c/a\u003e now.\u003c/p\u003e\n        \u003c/div\u003e\n      `;\n      break;\n\n    case Notification.THRESHOLD_MET:\n      subject = `Discount Alert for ${shortenedTitle}`;\n      body = `\n        \u003cdiv\u003e\n          \u003ch4\u003eHey, ${product.title} is now available at a discount more than ${THRESHOLD_PERCENTAGE}%!\u003c/h4\u003e\n          \u003cp\u003eGrab it right away from \u003ca href=\"${product.url}\" target=\"_blank\" rel=\"noopener noreferrer\"\u003ehere\u003c/a\u003e.\u003c/p\u003e\n        \u003c/div\u003e\n      `;\n      break;\n\n    default:\n      throw new Error(\"Invalid notification type.\");\n  }\n\n  return { subject, body };\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eglobals.css\u003c/code\u003e\u003c/summary\u003e\n\n```css\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n* {\n  margin: 0;\n  padding: 0;\n  box-sizing: border-box;\n  scroll-behavior: smooth;\n}\n\n@layer base {\n  body {\n    @apply font-inter;\n  }\n}\n\n@layer utilities {\n  .btn {\n    @apply py-4 px-4 bg-secondary hover:bg-opacity-70 rounded-[30px] text-white text-lg font-semibold;\n  }\n\n  .head-text {\n    @apply mt-4 text-6xl leading-[72px] font-bold tracking-[-1.2px] text-gray-900;\n  }\n\n  .section-text {\n    @apply text-secondary text-[32px] font-semibold;\n  }\n\n  .small-text {\n    @apply flex gap-2 text-sm font-medium text-primary;\n  }\n\n  .paragraph-text {\n    @apply text-xl leading-[30px] text-gray-600;\n  }\n\n  .hero-carousel {\n    @apply relative sm:px-10 py-5 sm:pt-20 pb-5 max-w-[560px] h-[700px] w-full bg-[#F2F4F7] rounded-[30px] sm:mx-auto;\n  }\n\n  .carousel {\n    @apply flex flex-col-reverse h-[700px];\n  }\n\n  .carousel .control-dots {\n    @apply static !important;\n  }\n\n  .carousel .control-dots .dot {\n    @apply w-[10px] h-[10px] bg-[#D9D9D9] rounded-full bottom-0 !important;\n  }\n\n  .carousel .control-dots .dot.selected {\n    @apply bg-[#475467] !important;\n  }\n\n  .trending-section {\n    @apply flex flex-col gap-10 px-6 md:px-20 py-24;\n  }\n\n  /* PRODUCT DETAILS PAGE STYLES */\n  .product-container {\n    @apply flex flex-col gap-16 flex-wrap px-6 md:px-20 py-24;\n  }\n\n  .product-image {\n    @apply flex-grow xl:max-w-[50%] max-w-full py-16 border border-[#CDDBFF] rounded-[17px];\n  }\n\n  .product-info {\n    @apply flex items-center flex-wrap gap-10 py-6 border-y border-y-[#E4E4E4];\n  }\n\n  .product-hearts {\n    @apply flex items-center gap-2 px-3 py-2 bg-[#FFF0F0] rounded-10;\n  }\n\n  .product-stars {\n    @apply flex items-center gap-2 px-3 py-2 bg-[#FBF3EA] rounded-[27px];\n  }\n\n  .product-reviews {\n    @apply flex items-center gap-2 px-3 py-2 bg-white-200 rounded-[27px];\n  }\n\n  /* MODAL */\n  .dialog-container {\n    @apply fixed inset-0 z-10 overflow-y-auto bg-black bg-opacity-60;\n  }\n\n  .dialog-content {\n    @apply p-6  bg-white inline-block w-full max-w-md my-8 overflow-hidden text-left align-middle transition-all transform  shadow-xl rounded-2xl;\n  }\n\n  .dialog-head_text {\n    @apply text-secondary text-lg leading-[24px] font-semibold mt-4;\n  }\n\n  .dialog-input_container {\n    @apply px-5 py-3 mt-3 flex items-center gap-2 border border-gray-300 rounded-[27px];\n  }\n\n  .dialog-input {\n    @apply flex-1 pl-1 border-none text-gray-500 text-base focus:outline-none border border-gray-300 rounded-[27px] shadow-xs;\n  }\n\n  .dialog-btn {\n    @apply px-5 py-3 text-white text-base font-semibold border border-secondary bg-secondary rounded-lg mt-8;\n  }\n\n  /* NAVBAR */\n  .nav {\n    @apply flex justify-between items-center px-6 md:px-20 py-4;\n  }\n\n  .nav-logo {\n    @apply font-spaceGrotesk text-[21px] text-secondary font-bold;\n  }\n\n  /* PRICE INFO */\n  .price-info_card {\n    @apply flex-1 min-w-[200px] flex flex-col gap-2 border-l-[3px] rounded-10 bg-white-100 px-5 py-4;\n  }\n\n  /* PRODUCT CARD */\n  .product-card {\n    @apply sm:w-[292px] sm:max-w-[292px] w-full flex-1 flex flex-col gap-4 rounded-md;\n  }\n\n  .product-card_img-container {\n    @apply flex-1 relative flex flex-col gap-5 p-4 rounded-md;\n  }\n\n  .product-card_img {\n    @apply max-h-[250px] object-contain w-full h-full bg-transparent;\n  }\n\n  .product-title {\n    @apply text-secondary text-xl leading-6 font-semibold truncate;\n  }\n\n  /* SEARCHBAR INPUT */\n  .searchbar-input {\n    @apply flex-1 min-w-[200px] w-full p-3 border border-gray-300 rounded-lg shadow-xs text-base text-gray-500 focus:outline-none;\n  }\n\n  .searchbar-btn {\n    @apply bg-gray-900 border border-gray-900 rounded-lg shadow-xs px-5 py-3 text-white text-base font-semibold hover:opacity-90 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-40;\n  }\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eindex.scraper.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n\"use server\"\n\nimport axios from 'axios';\nimport * as cheerio from 'cheerio';\nimport { extractCurrency, extractDescription, extractPrice } from '../utils';\n\nexport async function scrapeAmazonProduct(url: string) {\n  if(!url) return;\n\n  // BrightData proxy configuration\n  const username = String(process.env.BRIGHT_DATA_USERNAME);\n  const password = String(process.env.BRIGHT_DATA_PASSWORD);\n  const port = 22225;\n  const session_id = (1000000 * Math.random()) | 0;\n\n  const options = {\n    auth: {\n      username: `${username}-session-${session_id}`,\n      password,\n    },\n    host: 'brd.superproxy.io',\n    port,\n    rejectUnauthorized: false,\n  }\n\n  try {\n    // Fetch the product page\n    const response = await axios.get(url, options);\n    const $ = cheerio.load(response.data);\n\n    // Extract the product title\n    const title = $('#productTitle').text().trim();\n    const currentPrice = extractPrice(\n      $('.priceToPay span.a-price-whole'),\n      $('.a.size.base.a-color-price'),\n      $('.a-button-selected .a-color-base'),\n    );\n\n    const originalPrice = extractPrice(\n      $('#priceblock_ourprice'),\n      $('.a-price.a-text-price span.a-offscreen'),\n      $('#listPrice'),\n      $('#priceblock_dealprice'),\n      $('.a-size-base.a-color-price')\n    );\n\n    const outOfStock = $('#availability span').text().trim().toLowerCase() === 'currently unavailable';\n\n    const images = \n      $('#imgBlkFront').attr('data-a-dynamic-image') || \n      $('#landingImage').attr('data-a-dynamic-image') ||\n      '{}'\n\n    const imageUrls = Object.keys(JSON.parse(images));\n\n    const currency = extractCurrency($('.a-price-symbol'))\n    const discountRate = $('.savingsPercentage').text().replace(/[-%]/g, \"\");\n\n    const description = extractDescription($)\n\n    // Construct data object with scraped information\n    const data = {\n      url,\n      currency: currency || '$',\n      image: imageUrls[0],\n      title,\n      currentPrice: Number(currentPrice) || Number(originalPrice),\n      originalPrice: Number(originalPrice) || Number(currentPrice),\n      priceHistory: [],\n      discountRate: Number(discountRate),\n      category: 'category',\n      reviewsCount:100,\n      stars: 4.5,\n      isOutOfStock: outOfStock,\n      description,\n      lowestPrice: Number(currentPrice) || Number(originalPrice),\n      highestPrice: Number(originalPrice) || Number(currentPrice),\n      averagePrice: Number(currentPrice) || Number(originalPrice),\n    }\n\n    return data;\n  } catch (error: any) {\n    console.log(error);\n  }\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003enext.config.js\u003c/code\u003e\u003c/summary\u003e\n\n```javascript\n/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  experimental: {\n    serverActions: true,\n    serverComponentsExternalPackages: ['mongoose']\n  },\n  images: {\n    domains: ['m.media-amazon.com']\n  }\n}\n\nmodule.exports = nextConfig\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003etailwind.config.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\n    \"./pages/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./components/**/*.{js,ts,jsx,tsx,mdx}\",\n    \"./app/**/*.{js,ts,jsx,tsx,mdx}\",\n  ],\n  theme: {\n    extend: {\n      colors: {\n        primary: {\n          DEFAULT: \"#E43030\",\n          \"orange\": \"#D48D3B\",\n          \"green\": \"#3E9242\"\n        },\n        secondary: \"#282828\",\n        \"gray-200\": \"#EAECF0\",\n        \"gray-300\": \"D0D5DD\",\n        \"gray-500\": \"#667085\",\n        \"gray-600\": \"#475467\",\n        \"gray-700\": \"#344054\",\n        \"gray-900\": \"#101828\",\n        \"white-100\": \"#F4F4F4\",\n        \"white-200\": \"#EDF0F8\",\n        \"black-100\": \"#3D4258\",\n        \"neutral-black\": \"#23263B\",\n      },\n      boxShadow: {\n        xs: \"0px 1px 2px 0px rgba(16, 24, 40, 0.05)\",\n      },\n      maxWidth: {\n        \"10xl\": '1440px'\n      },\n      fontFamily: {\n        inter: ['Inter', 'sans-serif'],\n        spaceGrotesk: ['Space Grotesk', 'sans-serif'],\n      },\n      borderRadius: {\n        10: \"10px\"\n      }\n    },\n  },\n  plugins: [],\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003etypes.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nexport type PriceHistoryItem = {\n  price: number;\n};\n\nexport type User = {\n  email: string;\n};\n\nexport type Product = {\n  _id?: string;\n  url: string;\n  currency: string;\n  image: string;\n  title: string;\n  currentPrice: number;\n  originalPrice: number;\n  priceHistory: PriceHistoryItem[] | [];\n  highestPrice: number;\n  lowestPrice: number;\n  averagePrice: number;\n  discountRate: number;\n  description: string;\n  category: string;\n  reviewsCount: number;\n  stars: number;\n  isOutOfStock: Boolean;\n  users?: User[];\n};\n\nexport type NotificationType =\n  | \"WELCOME\"\n  | \"CHANGE_OF_STOCK\"\n  | \"LOWEST_PRICE\"\n  | \"THRESHOLD_MET\";\n\nexport type EmailContent = {\n  subject: string;\n  body: string;\n};\n\nexport type EmailProductInfo = {\n  title: string;\n  url: string;\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eutils.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { PriceHistoryItem, Product } from \"@/types\";\n\nconst Notification = {\n  WELCOME: 'WELCOME',\n  CHANGE_OF_STOCK: 'CHANGE_OF_STOCK',\n  LOWEST_PRICE: 'LOWEST_PRICE',\n  THRESHOLD_MET: 'THRESHOLD_MET',\n}\n\nconst THRESHOLD_PERCENTAGE = 40;\n\n// Extracts and returns the price from a list of possible elements.\nexport function extractPrice(...elements: any) {\n  for (const element of elements) {\n    const priceText = element.text().trim();\n\n    if(priceText) {\n      const cleanPrice = priceText.replace(/[^\\d.]/g, '');\n\n      let firstPrice; \n\n      if (cleanPrice) {\n        firstPrice = cleanPrice.match(/\\d+\\.\\d{2}/)?.[0];\n      } \n\n      return firstPrice || cleanPrice;\n    }\n  }\n\n  return '';\n}\n\n// Extracts and returns the currency symbol from an element.\nexport function extractCurrency(element: any) {\n  const currencyText = element.text().trim().slice(0, 1);\n  return currencyText ? currencyText : \"\";\n}\n\n// Extracts description from two possible elements from amazon\nexport function extractDescription($: any) {\n  // these are possible elements holding description of the product\n  const selectors = [\n    \".a-unordered-list .a-list-item\",\n    \".a-expander-content p\",\n    // Add more selectors here if needed\n  ];\n\n  for (const selector of selectors) {\n    const elements = $(selector);\n    if (elements.length \u003e 0) {\n      const textContent = elements\n        .map((_: any, element: any) =\u003e $(element).text().trim())\n        .get()\n        .join(\"\\n\");\n      return textContent;\n    }\n  }\n\n  // If no matching elements were found, return an empty string\n  return \"\";\n}\n\nexport function getHighestPrice(priceList: PriceHistoryItem[]) {\n  let highestPrice = priceList[0];\n\n  for (let i = 0; i \u003c priceList.length; i++) {\n    if (priceList[i].price \u003e highestPrice.price) {\n      highestPrice = priceList[i];\n    }\n  }\n\n  return highestPrice.price;\n}\n\nexport function getLowestPrice(priceList: PriceHistoryItem[]) {\n  let lowestPrice = priceList[0];\n\n  for (let i = 0; i \u003c priceList.length; i++) {\n    if (priceList[i].price \u003c lowestPrice.price) {\n      lowestPrice = priceList[i];\n    }\n  }\n\n  return lowestPrice.price;\n}\n\nexport function getAveragePrice(priceList: PriceHistoryItem[]) {\n  const sumOfPrices = priceList.reduce((acc, curr) =\u003e acc + curr.price, 0);\n  const averagePrice = sumOfPrices / priceList.length || 0;\n\n  return averagePrice;\n}\n\nexport const getEmailNotifType = (\n  scrapedProduct: Product,\n  currentProduct: Product\n) =\u003e {\n  const lowestPrice = getLowestPrice(currentProduct.priceHistory);\n\n  if (scrapedProduct.currentPrice \u003c lowestPrice) {\n    return Notification.LOWEST_PRICE as keyof typeof Notification;\n  }\n  if (!scrapedProduct.isOutOfStock \u0026\u0026 currentProduct.isOutOfStock) {\n    return Notification.CHANGE_OF_STOCK as keyof typeof Notification;\n  }\n  if (scrapedProduct.discountRate \u003e= THRESHOLD_PERCENTAGE) {\n    return Notification.THRESHOLD_MET as keyof typeof Notification;\n  }\n\n  return null;\n};\n\nexport const formatNumber = (num: number = 0) =\u003e {\n  return num.toLocaleString(undefined, {\n    minimumFractionDigits: 0,\n    maximumFractionDigits: 0,\n  });\n};\n```\n\n\u003c/details\u003e\n\n## \u003ca name=\"links\"\u003e🔗 Links\u003c/a\u003e\n\nAssets used in the project are [here](https://drive.google.com/file/d/1v6h993BgYX6axBoIXFbZ9HQAgqbR4PSH/view?usp=sharing)\n\n## Continued development\n\nI aim to enhance my skills in 3D graphics and animation, and further integrate advanced interactivity features in web projects to create engaging user experiences.\n\n## Author\n\n\u003cb\u003e\u003cstrong\u003eSarthak Sachdev\u003c/strong\u003e\u003c/b\u003e\n- Website - [Sarthak Sachdev](https://itsmesarthak.netlify.app/)\n- LeetCode - [@sarthak_sachdev](https://leetcode.com/u/sarthak_sachdev/)\n- Twitter - [@sarthak_sach69](https://www.twitter.com/sarthak_sach69)\n\n## Acknowledgments\n\nSpecial thanks to the vast knowledge base available on YouTube and Stack Overflow, where I learned many of the concepts integrated in this project and got my doubts cleared.\n\n## Got feedback for me?\n\nI love receiving feedback! I am always looking to improve my code and take up new innovative ideas to work upon. So if you have anything you'd like to mention, please email 'hi' at saarsaach30[at]gmail[dot]com.\n\nIf you liked this project, make sure to spread the word and share it with all your friends.\n\n**Happy Coding!** ✨🎖️\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsarthak-0-sach%2Famazon_webscraper_application","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsarthak-0-sach%2Famazon_webscraper_application","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsarthak-0-sach%2Famazon_webscraper_application/lists"}