DavitDvalashvili /
My-tech-blog
Explore my tech blog repository, featuring a wide range of articles on modern web development. Dive into tutorials, tips, and best practices to enhance your coding skills and knowledge.
31/100 healthLoading repository data…
emredkyc / repository
Dive into web scraping and build a Next.js 14 eCommerce price tracker within one project that teaches you data scraping, cron jobs, sending emails, deployment, and more.
A transparent discovery signal based on current public GitHub metadata.
This score does not audit code, security, maintainers, documentation quality, or suitability. Verify the repository and its current documentation before adoption.
This repository contains the code that corresponds to building an app from scratch. .
If you prefer to learn from the doc, this is the perfect resource for you. Follow along to learn how to create projects like these step by step in a beginner-friendly way!
Developed 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.
If you are just starting out and need help, or if you encounter any bugs, you can ask. This is a place where people help each other.
👉 Header with Carousel: Visually appealing header with a carousel showcasing key features and benefits
👉 Product Scraping: A search bar allowing users to input Amazon product links for scraping.
👉 Scraped Projects: Displays the details of products scraped so far, offering insights into tracked items.
👉 Scraped Product Details: Showcase the product image, title, pricing, details, and other relevant information scraped from the original website
👉 Track Option: Modal for users to provide email addresses and opt-in for tracking.
👉 Email Notifications: Send emails product alert emails for various scenarios, e.g., back in stock alerts or lowest price notifications.
👉 Automated Cron Jobs: Utilize cron jobs to automate periodic scraping, ensuring data is up-to-date.
and many more, including code architecture and reusability
Follow these steps to set up the project locally on your machine.
Selected from shared topics, language and repository description—not editorial ratings.
DavitDvalashvili /
Explore my tech blog repository, featuring a wide range of articles on modern web development. Dive into tutorials, tips, and best practices to enhance your coding skills and knowledge.
31/100 healthAndyAND-Y /
Explore my Airbnb Clone Project a Next.js learning journey. I built this project to delve into modern web development using React, Node.js, and MongoDB. Dive into the code to see how I crafted a responsive web app and experimented with the latest tech.
34/100 healthPrerequisites
Make sure you have the following installed on your machine:
Cloning the Repository
git clone https://github.com/emredkyc/price_tracker.git
cd price_tracker
Installation
Install the project dependencies using npm:
npm install
Set Up Environment Variables
Create a new file named .env in the root of your project and add the following content:
#SCRAPER
BRIGHT_DATA_USERNAME=
BRIGHT_DATA_PASSWORD=
#DB
MONGODB_URI=
#OUTLOOK
EMAIL_USER=
EMAIL_PASS=
Replace the placeholder values with your actual credentials. You can obtain these credentials by signing up on these specific websites from BrightData, MongoDB, and Node Mailer
Running the Project
npm run dev
Open http://localhost:3000 in your browser to view the project.
import { NextResponse } from "next/server";
import { getLowestPrice, getHighestPrice, getAveragePrice, getEmailNotifType } from "@/lib/utils";
import { connectToDB } from "@/lib/mongoose";
import Product from "@/lib/models/product.model";
import { scrapeAmazonProduct } from "@/lib/scraper";
import { generateEmailBody, sendEmail } from "@/lib/nodemailer";
export const maxDuration = 300; // This function can run for a maximum of 300 seconds
export const dynamic = "force-dynamic";
export const revalidate = 0;
export async function GET(request: Request) {
try {
connectToDB();
const products = await Product.find({});
if (!products) throw new Error("No product fetched");
// ======================== 1 SCRAPE LATEST PRODUCT DETAILS & UPDATE DB
const updatedProducts = await Promise.all(
products.map(async (currentProduct) => {
// Scrape product
const scrapedProduct = await scrapeAmazonProduct(currentProduct.url);
if (!scrapedProduct) return;
const updatedPriceHistory = [
...currentProduct.priceHistory,
{
price: scrapedProduct.currentPrice,
},
];
const product = {
...scrapedProduct,
priceHistory: updatedPriceHistory,
lowestPrice: getLowestPrice(updatedPriceHistory),
highestPrice: getHighestPrice(updatedPriceHistory),
averagePrice: getAveragePrice(updatedPriceHistory),
};
// Update Products in DB
const updatedProduct = await Product.findOneAndUpdate(
{
url: product.url,
},
product
);
// ======================== 2 CHECK EACH PRODUCT'S STATUS & SEND EMAIL ACCORDINGLY
const emailNotifType = getEmailNotifType(
scrapedProduct,
currentProduct
);
if (emailNotifType && updatedProduct.users.length > 0) {
const productInfo = {
title: updatedProduct.title,
url: updatedProduct.url,
};
// Construct emailContent
const emailContent = await generateEmailBody(productInfo, emailNotifType);
// Get array of user emails
const userEmails = updatedProduct.users.map((user: any) => user.email);
// Send email notification
await sendEmail(emailContent, userEmails);
}
return updatedProduct;
})
);
return NextResponse.json({
message: "Ok",
data: updatedProducts,
});
} catch (error: any) {
throw new Error(`Failed to get all products: ${error.message}`);
}
}
export async function generateEmailBody(
product: EmailProductInfo,
type: NotificationType
) {
const THRESHOLD_PERCENTAGE = 40;
// Shorten the product title
const shortenedTitle =
product.title.length > 20
? `${product.title.substring(0, 20)}...`
: product.title;
let subject = "";
let body = "";
switch (type) {
case Notification.WELCOME:
subject = `Welcome to Price Tracking for ${shortenedTitle}`;
body = `
<div>
<h2>Welcome to PriceTracker 🚀</h2>
<p>You are now tracking ${product.title}.</p>
<p>Here's an example of how you'll receive updates:</p>
<div style="border: 1px solid #ccc; padding: 10px; background-color: #f8f8f8;">
<h3>${product.title} is back in stock!</h3>
<p>We're excited to let you know that ${product.title} is now back in stock.</p>
<p>Don't miss out - <a href="${product.url}" target="_blank" rel="noopener noreferrer">buy it now</a>!</p>
<img src="https://i.ibb.co/pwFBRMC/Screenshot-2023-09-26-at-1-47-50-AM.png" alt="Product Image" style="max-width: 100%;" />
</div>
<p>Stay tuned for more updates on ${product.title} and other products you're tracking.</p>
</div>
`;
break;
case Notification.CHANGE_OF_STOCK:
subject = `${shortenedTitle} is now back in stock!`;
body = `
<div>
<h4>Hey, ${product.title} is now restocked! Grab yours before they run out again!</h4>
<p>See the product <a href="${product.url}" target="_blank" rel="noopener noreferrer">here</a>.</p>
</div>
`;
break;
case Notification.LOWEST_PRICE:
subject = `Lowest Price Alert for ${shortenedTitle}`;
body = `
<div>
<h4>Hey, ${product.title} has reached its lowest price ever!!</h4>
<p>Grab the product <a href="${product.url}" target="_blank" rel="noopener noreferrer">here</a> now.</p>
</div>
`;
break;
case Notification.THRESHOLD_MET:
subject = `Discount Alert for ${shortenedTitle}`;
body = `
<div>
<h4>Hey, ${product.title} is now available at a discount more than ${THRESHOLD_PERCENTAGE}%!</h4>
<p>Grab it right away from <a href="${product.url}" target="_blank" rel="noopener noreferrer">here</a>.</p>
</div>
`;
break;
default:
throw new Error("Invalid notification type.");
}
return { subject, body };
}
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
scroll-behavior: smooth;
}
@layer base {
body {
@apply font-inter;
}
}
@layer utilities {
.btn {
@apply py-4 px-4 bg-secondary hover:bg-opacity-70 rounded-[30px] text-white text-lg font-semibold;
}
.head-text {
@apply mt-4 text-6xl leading-[72px] font-bold tracking-[-1.2px] text-gray-900;
}
.section-text {
@apply text-secondary text-[32px] font-semibold;
}
.small-text {
@apply flex gap-2 text-sm font-medium text-primary;
}
.paragraph-text {
@apply text-xl leading-[30px] text-gray-600;
}
.hero-carousel {
@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;
}
.carousel {
@apply flex flex-col-reverse h-[700px];
}
.carousel .control-dots {
@apply static !important;
}
.carousel .control-dots .dot {
@apply w-[10px] h-[10px] bg-[#D9D9D9] rounded-full bottom-0 !important;
}
.carousel .control-dots .dot.selected {
@apply bg-[#475467] !important;
}
.trending-section {
@apply flex flex-col gap-10 px-6 md:px-20 py-24;
}
/* PRODUCT DETAILS PAGE STYLES */
.product-container {
@apply flex flex-col gap-16 flex-wrap px-6 md:px-20 py-24;
}
.product-image {
@apply flex-grow xl:max-w-[50%] max-w-full py-16 border border-[#CDDBFF] rounded-[17px];
}
.product-info {
@apply flex items-center flex-wrap gap-10 py-6 border-y border-y-[#E4E4E4];
}
.product-hearts {
@apply flex items-center gap-2 px-3 py-2 bg-[#FFF0F0] rounded-10;
}
.product-stars {
@apply flex items-center gap-2 px-3 py-2 bg-[#FBF3EA] rounded-[27px];
}
.product-reviews {
@apply flex items-center gap-2 px-3 py-2 bg-white-200 rounded-[27px];
}
/* MODAL */
.dialog-container {
@apply fixed inset-0 z-10 overflow-y-auto bg-black bg-opacity-60;
}
.dialog-content {
@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;
}
.dialog-head_text {
@apply text-secondary text-lg leading-[24px] font-semibold mt-4;
}
.dialog-input_container {
@apply px-5 py-3 mt-3 flex items-center gap-2 border border-gray-300 rounded-[27px];
}
.dialog-input {
@apply flex-1 pl-1 border-none text-gray-500 text-base focus:outline-none border borde