-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 0eaaee7
Showing
58 changed files
with
3,984 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
.env | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
|
||
# local env files | ||
.env*.local | ||
|
||
# vercel | ||
.vercel | ||
|
||
# typescript | ||
*.tsbuildinfo | ||
next-env.d.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
<div align="center"> | ||
<h1>EverPriced-Price Tracker</h1> | ||
</div> | ||
|
||
## 📋 <a name="table">Table of Contents</a> | ||
|
||
1. 🤖 [Introduction](#introduction) | ||
2. ⚙️ [Tech Stack](#tech-stack) | ||
3. 🔋 [Features](#features) | ||
4. 🤸 [Quick Start](#quick-start) | ||
|
||
## <a name="introduction">🤖 Introduction</a> | ||
|
||
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. | ||
|
||
|
||
|
||
## <a name="tech-stack">⚙️ Tech Stack</a> | ||
|
||
- Next.js | ||
- Bright Data | ||
- Cheerio | ||
- Nodemailer | ||
- MongoDB | ||
- Headless UI | ||
- Tailwind CSS | ||
|
||
## <a name="features">🔋 Features</a> | ||
|
||
👉 **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 | ||
|
||
## <a name="quick-start">🤸 Quick Start</a> | ||
|
||
|
||
**Cloning the Repository** | ||
1. Fork this repo. | ||
|
||
```bash | ||
git clone https://github.com/<your_username>/everpriced.git | ||
cd everpriced | ||
``` | ||
|
||
**Installation** | ||
|
||
Install the project dependencies using npm: | ||
|
||
```bash | ||
npm install | ||
``` | ||
|
||
**Set Up Environment Variables** | ||
|
||
Create a new file named `.env` in the root of your project and add the following content: | ||
|
||
```env | ||
BRIGHT_DATA_USERNAME= | ||
BRIGHT_DATA_PASSWORD= | ||
MONGODB_URI= | ||
SMTP_HOST=smtp.gmail.com | ||
SMTP_PORT= 465 | ||
SMTP_SERVICE=gmail | ||
SMTP_MAIL= | ||
EMAIL_PASSWORD= | ||
``` | ||
|
||
Replace the placeholder values with your actual credentials. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
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}`); | ||
} | ||
} |
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
@tailwind base; | ||
@tailwind components; | ||
@tailwind utilities; | ||
|
||
* { | ||
margin: 0; | ||
padding: 0; | ||
box-sizing: border-box; | ||
scroll-behavior: smooth; | ||
} | ||
body { | ||
background-color: #ffffff; | ||
opacity: 1; | ||
background-image: radial-gradient(#929292 0.75px, #ffffff 0.75px); | ||
background-size: 15px 15px; | ||
} | ||
|
||
@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 border-gray-300 rounded-[27px] shadow-xs; | ||
} | ||
|
||
.dialog-btn { | ||
@apply px-5 py-3 text-white text-base font-semibold border border-secondary bg-secondary rounded-lg mt-8; | ||
} | ||
|
||
/* NAVBAR */ | ||
.nav { | ||
@apply flex justify-between items-center px-6 md:px-20 py-4; | ||
} | ||
|
||
.nav-logo { | ||
@apply font-spaceGrotesk text-[21px] text-secondary font-bold; | ||
} | ||
|
||
/* PRICE INFO */ | ||
.price-info_card { | ||
@apply flex-1 min-w-[200px] flex flex-col gap-2 border-l-[3px] rounded-10 bg-white-100 px-5 py-4; | ||
} | ||
|
||
/* PRODUCT CARD */ | ||
.product-card { | ||
@apply sm:w-[292px] sm:max-w-[292px] w-full flex-1 flex flex-col gap-4 rounded-md bg-white border border-gray-400 p-1; | ||
} | ||
|
||
.product-card_img-container { | ||
@apply flex-1 relative flex flex-col gap-5 p-4 rounded-md; | ||
} | ||
|
||
.product-card_img { | ||
@apply max-h-[250px] object-contain w-full h-full bg-transparent; | ||
} | ||
|
||
.product-title { | ||
@apply text-secondary text-xl leading-6 font-semibold truncate; | ||
} | ||
|
||
/* SEARCHBAR INPUT */ | ||
.searchbar-input { | ||
@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; | ||
} | ||
|
||
.searchbar-btn { | ||
@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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import Navbar from '@/components/Navbar' | ||
import './globals.css' | ||
import type { Metadata } from 'next' | ||
import { Inter, Space_Grotesk } from 'next/font/google' | ||
|
||
const inter = Inter({ subsets: ['latin'] }) | ||
const spaceGrotesk = Space_Grotesk({ | ||
subsets: ['latin'], | ||
weight: ['300', '400', '500', '600', '700'] | ||
}) | ||
|
||
export const metadata: Metadata = { | ||
title: 'everpriced', | ||
description: 'Track product prices effortlessly and save money on your online shopping.', | ||
} | ||
|
||
export default function RootLayout({ | ||
children, | ||
}: { | ||
children: React.ReactNode | ||
}) { | ||
return ( | ||
<html lang="en"> | ||
<body className={inter.className}> | ||
<main className="max-w-10xl mx-auto"> | ||
<Navbar /> | ||
{children} | ||
</main> | ||
</body> | ||
</html> | ||
) | ||
} |
Oops, something went wrong.