├── .eslintrc.json ├── src ├── app │ ├── favicon.ico │ ├── page.js │ ├── models │ │ └── reminder.js │ ├── layout.js │ ├── components │ │ └── ui │ │ │ ├── TextInput.js │ │ │ ├── Form.js │ │ │ ├── Modal.js │ │ │ ├── MultiEmailSelector.js │ │ │ ├── Button.js │ │ │ └── SelectInput.js │ ├── globals.css │ ├── Screens │ │ ├── AddBidRemind.js │ │ └── BidFrom.js │ └── api │ │ └── email │ │ └── route.js └── lib │ ├── db.js │ └── scheduler.js ├── jsconfig.json ├── next.config.mjs ├── postcss.config.mjs ├── .gitignore ├── tailwind.config.js ├── public ├── vercel.svg └── next.svg ├── package.json └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codewithsadaf/bid-reminder/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/app/page.js: -------------------------------------------------------------------------------- 1 | import Button from "@/app/components/ui/Button"; 2 | import AddBidRemind from "./Screens/AddBidRemind"; 3 | 4 | export default function Home() { 5 | 6 | return ( 7 |
8 |

Bid Manager

9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/app/models/reminder.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const reminderSchema = new mongoose.Schema({ 4 | recipients: [String], 5 | candidateName: String, 6 | searchQuery: String, 7 | frequency: String, 8 | relevancyScore: Number, 9 | day: String, 10 | time: String, 11 | }, { 12 | timestamps: true, 13 | }); 14 | 15 | export const Reminder = mongoose.models.Reminder || mongoose.model('Reminder', reminderSchema); 16 | -------------------------------------------------------------------------------- /src/app/layout.js: -------------------------------------------------------------------------------- 1 | import { Inter } from "next/font/google"; 2 | import "./globals.css"; 3 | 4 | const inter = Inter({ subsets: ["latin"] }); 5 | 6 | export const metadata = { 7 | title: "Create Next App", 8 | description: "Generated by create next app", 9 | }; 10 | 11 | export default function RootLayout({ children }) { 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/db.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const MONGODB_URI = process.env.MONGODB_URI; 4 | 5 | const connectToDatabase = async () => { 6 | if (mongoose.connection.readyState >= 1) { 7 | return; 8 | } 9 | 10 | try { 11 | await mongoose.connect(MONGODB_URI); 12 | console.log('Connected to MongoDB'); 13 | } catch (error) { 14 | console.error('Failed to connect to MongoDB:', error); 15 | throw new Error('Failed to connect to MongoDB'); 16 | } 17 | }; 18 | 19 | export default connectToDatabase; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 12 | "gradient-conic": 13 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | }; 19 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/components/ui/TextInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const TextInput = ({ label, name, register, errors = {}, ...rest }) => ( 4 |
5 | 8 | 13 | {errors[name] && ( 14 |

{errors[name]?.message}

15 | )} 16 |
17 | ); 18 | 19 | export default TextInput; -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bid-manager", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@hookform/resolvers": "^3.9.0", 13 | "axios": "^1.7.4", 14 | "classnames": "^2.5.1", 15 | "mongoose": "^8.5.3", 16 | "next": "14.2.6", 17 | "node-cron": "^3.0.3", 18 | "nodemailer": "^6.9.14", 19 | "react": "^18", 20 | "react-dom": "^18", 21 | "react-hook-form": "^7.52.2", 22 | "react-multi-email": "^1.0.25", 23 | "yup": "^1.4.0" 24 | }, 25 | "devDependencies": { 26 | "eslint": "^8", 27 | "eslint-config-next": "14.2.6", 28 | "postcss": "^8", 29 | "tailwindcss": "^3.4.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app/Screens/AddBidRemind.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | import React, { useState } from 'react' 3 | import Button from '../components/ui/Button'; 4 | import Modal from '../components/ui/Modal'; 5 | import BidForm from './BidFrom'; 6 | 7 | const AddBidRemind = () => { 8 | const [isModalOpen, setIsModalOpen] = useState(false); 9 | const handleOpenModal = () => { 10 | setIsModalOpen(true); 11 | }; 12 | const handleCloseModal = () => { 13 | setIsModalOpen(false); 14 | }; 15 | return ( 16 |
17 | 20 | 21 | 22 | 23 |
24 | ) 25 | } 26 | 27 | export default AddBidRemind -------------------------------------------------------------------------------- /src/app/components/ui/Form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | 4 | const Form = ({ defaultValues = {}, onSubmit, children, validationSchema, mode = 'onSubmit' }) => { 5 | const { register, handleSubmit, control, formState: { errors } } = useForm({ 6 | defaultValues, 7 | resolver: validationSchema, 8 | mode, 9 | }); 10 | 11 | return ( 12 |
13 | {React.Children.map(children, (child) => { 14 | if (React.isValidElement(child) && child.props.name) { 15 | return React.cloneElement(child, { 16 | register, 17 | control, 18 | errors, 19 | }); 20 | } 21 | return child; 22 | })} 23 |
24 | ); 25 | }; 26 | 27 | export default Form; 28 | -------------------------------------------------------------------------------- /src/app/components/ui/Modal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Modal = ({ isOpen, onClose, children, title }) => { 4 | if (!isOpen) return null; 5 | 6 | return ( 7 |
8 |
9 |
10 |

{title}

11 | 18 |
19 |
20 | {children} 21 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default Modal; 28 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/components/ui/MultiEmailSelector.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ReactMultiEmail } from 'react-multi-email'; 3 | import "react-multi-email/dist/style.css"; 4 | 5 | const MultiEmailSelector = ({emailsListError, emailsList, label, setEmailsList }) => ( 6 | <> 7 | 10 |
11 | setEmailsList(_emails)} 15 | getLabel={(email, index, removeEmail) => { 16 | return ( 17 |
18 | {email} 19 | removeEmail(index)}> 20 | × 21 | 22 |
23 | ); 24 | }} 25 | /> 26 |
27 | {emailsListError &&

{emailsListError}

} 28 | 29 | ); 30 | export default MultiEmailSelector; -------------------------------------------------------------------------------- /src/app/components/ui/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | const Button = ({ 5 | children, 6 | onClick, 7 | type = 'button', 8 | variant = 'primary', 9 | size = 'md', 10 | disabled = false, 11 | className = '', 12 | }) => { 13 | const baseStyles = 'font-semibold rounded focus:outline-none'; 14 | 15 | const variants = { 16 | primary: 'bg-blue-500 text-white hover:bg-blue-600', 17 | secondary: 'bg-gray-500 text-white hover:bg-gray-600', 18 | danger: 'bg-red-500 text-white hover:bg-red-600', 19 | success: 'bg-green-500 text-white hover:bg-green-600', 20 | warning: 'bg-yellow-500 text-black hover:bg-yellow-600', 21 | }; 22 | 23 | const sizes = { 24 | sm: 'px-2 py-1 text-sm', 25 | md: 'px-4 py-2 text-md', 26 | lg: 'px-6 py-3 text-lg', 27 | }; 28 | 29 | const classes = classNames( 30 | baseStyles, 31 | variants[variant], 32 | sizes[size], 33 | { 34 | 'opacity-50 cursor-not-allowed': disabled, 35 | }, 36 | className 37 | ); 38 | 39 | return ( 40 | 48 | ); 49 | }; 50 | export default Button; -------------------------------------------------------------------------------- /src/app/components/ui/SelectInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useController } from 'react-hook-form'; 3 | 4 | const SelectInput = ({ label, name, options, defaultValue, control, error }) => { 5 | const { 6 | field: { onChange, onBlur, value, name: fieldName }, 7 | } = useController({ 8 | name, 9 | control, 10 | }); 11 | 12 | return ( 13 |
14 | 17 | 33 | {error &&

{error.message}

} 34 |
35 | ); 36 | }; 37 | 38 | export default SelectInput; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /src/lib/scheduler.js: -------------------------------------------------------------------------------- 1 | import cron from 'node-cron'; 2 | import nodemailer from 'nodemailer'; 3 | import connectToDatabase from './db'; 4 | import { Reminder } from '@/app/models/reminder'; 5 | 6 | const transporter = nodemailer.createTransport({ 7 | service: 'Gmail', 8 | auth: { 9 | user: process.env.EMAIL_USER, 10 | pass: process.env.EMAIL_APP_PASSWORD, 11 | }, 12 | }); 13 | 14 | const sendEmail = async (to, subject, candidateName, searchQuery) => { 15 | const emailContent = ` 16 | Hello! 17 | 18 | This is ${candidateName}. You’ve requested notifications about ${searchQuery}. 19 | 20 | Have a great day, 21 | 22 | ${candidateName} 23 | `; 24 | 25 | try { 26 | await transporter.sendMail({ 27 | from: process.env.EMAIL_USER, 28 | to, 29 | subject, 30 | text: emailContent, 31 | }); 32 | console.log(`Email sent to ${to}`); 33 | } catch (error) { 34 | console.error('Error sending email:', error); 35 | } 36 | }; 37 | 38 | const getCronExpression = (frequency, day, time) => { 39 | const [hour, minute] = time.split(':').map(Number); 40 | 41 | if (frequency === 'daily') { 42 | return `${minute} ${hour} * * *`; 43 | } 44 | 45 | if (frequency === 'weekly') { 46 | const daysOfWeek = { 47 | Sunday: 0, 48 | Monday: 1, 49 | Tuesday: 2, 50 | Wednesday: 3, 51 | Thursday: 4, 52 | Friday: 5, 53 | Saturday: 6, 54 | }; 55 | return `${minute} ${hour} * * ${daysOfWeek[day]}`; 56 | } 57 | 58 | return ''; 59 | }; 60 | 61 | export const scheduleEmails = async () => { 62 | try { 63 | await connectToDatabase(); 64 | const allReminders = await Reminder.find().exec(); 65 | console.log("allReminders >>>>>", allReminders); 66 | 67 | allReminders.forEach((notification) => { 68 | const { recipients, frequency, day, time, searchQuery, candidateName } = notification; 69 | const cronExpression = getCronExpression(frequency, day, time); 70 | 71 | if (cronExpression) { 72 | recipients.forEach((email) => { 73 | cron.schedule(cronExpression, async () => { 74 | await sendEmail(email, 'Notification Update', candidateName, searchQuery); 75 | }); 76 | 77 | console.log(`Scheduled email for ${email} with cron expression: ${cronExpression}`); 78 | }); 79 | } else { 80 | console.error(`Invalid cron expression for notification: ${notification}`); 81 | } 82 | }); 83 | } catch (error) { 84 | console.error('Error scheduling emails:', error); 85 | } 86 | }; 87 | 88 | -------------------------------------------------------------------------------- /src/app/api/email/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | import nodemailer from 'nodemailer'; 3 | import connectToDatabase from '@/lib/db'; 4 | import { Reminder } from '../../models/reminder'; 5 | import { scheduleEmails } from '@/lib/scheduler'; 6 | 7 | const transporter = nodemailer.createTransport({ 8 | service: 'gmail', 9 | host: "smtp.gmail.com", 10 | host: "smtp.ethereal.email", 11 | port: 587, 12 | secure: false, 13 | auth: { 14 | user: process.env.EMAIL_USER, 15 | pass: process.env.EMAIL_APP_PASSWORD, 16 | }, 17 | }); 18 | 19 | const sendNotificationEmail = async ({ recipient, candidateName, searchQuery }) => { 20 | const mailOptions = { 21 | from: process.env.EMAIL_USER, 22 | to: recipient, 23 | subject: 'Notification from Bid Reminder', 24 | text: `Hello!\n\nThis is ${candidateName}. You’ve requested notifications about ${searchQuery}.\n\nHave a great day,\n\n${candidateName}`, 25 | }; 26 | 27 | try { 28 | const sendMailResp = await transporter.sendMail(mailOptions); 29 | console.log(`Email sent to ${recipient}`); 30 | console.log(`sendMailResp >>>>>>>`, sendMailResp); 31 | } catch (error) { 32 | console.error(`Error sending email to ${recipient}:`, error); 33 | throw new Error(`Failed to send email to ${recipient}`); 34 | } 35 | }; 36 | 37 | export async function POST(req) { 38 | const headers = new Headers({ 39 | 'Access-Control-Allow-Origin': '*', 40 | 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', 41 | 'Access-Control-Allow-Headers': 'Content-Type', 42 | }); 43 | 44 | if (req.method === 'OPTIONS') { 45 | return new Response(null, { status: 204, headers }); 46 | } 47 | try { 48 | await connectToDatabase(); 49 | 50 | const { recipients, candidateName, searchQuery, frequency, day, time, relevancyScore } = await req.json(); 51 | 52 | if (!recipients || !candidateName || !searchQuery || !Array.isArray(recipients)) { 53 | return NextResponse.json({ error: 'Invalid input data' }, { status: 400 }); 54 | } 55 | if (typeof relevancyScore !== 'number' || relevancyScore < 0 || relevancyScore > 100) { 56 | return NextResponse.json({ error: 'Invalid relevancy score' }, { status: 400 }); 57 | } 58 | console.log("payload >>>>>>>>>", { 59 | recipients, 60 | candidateName, 61 | searchQuery, 62 | frequency, 63 | day, 64 | time, 65 | relevancyScore, 66 | }); 67 | await Reminder.create({ 68 | recipients, 69 | candidateName, 70 | searchQuery, 71 | frequency, 72 | relevancyScore, 73 | day, 74 | time, 75 | }); 76 | 77 | await scheduleEmails(); 78 | 79 | if (relevancyScore >= 100) { 80 | for (const recipient of recipients) { 81 | await sendNotificationEmail({ recipient, candidateName, searchQuery }); 82 | } 83 | } 84 | 85 | return NextResponse.json({ success: true, message: 'Notifications sent successfully' }, { status: 200 }); 86 | } catch (error) { 87 | console.error('Error handling POST request:', error.message); 88 | return NextResponse.json({ error: 'Failed to send notifications' }, { status: 500 }); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/app/Screens/BidFrom.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import * as yup from 'yup'; 3 | import { yupResolver } from '@hookform/resolvers/yup'; 4 | import { useForm } from 'react-hook-form'; 5 | import Form from '../components/ui/Form'; 6 | import TextInput from '../components/ui/TextInput'; 7 | import SelectInput from '../components/ui/SelectInput'; 8 | import Button from '../components/ui/Button'; 9 | import axios from 'axios'; 10 | import MultiEmailSelector from '../components/ui/MultiEmailSelector'; 11 | 12 | const schema = yup.object().shape({ 13 | notificationInterval: yup.string().required('Notification interval is required'), 14 | weeklyNotificationDay: yup.string() 15 | .oneOf(['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'], 'Invalid day') 16 | .required('Weekly notification day is required'), 17 | notificationTime: yup.string().required('Notification time is required'), 18 | searchQuery: yup.string().required('Search query is required'), 19 | relevancyScore: yup.number().min(0).max(100).required('Relevancy score is required'), 20 | }); 21 | 22 | const BidForm = ({ onClose }) => { 23 | const { register, control, handleSubmit, reset, formState: { errors } } = useForm({ 24 | defaultValues: { 25 | notificationInterval: '', 26 | weeklyNotificationDay: '', 27 | notificationTime: '', 28 | searchQuery: '', 29 | relevancyScore: '', 30 | }, 31 | resolver: yupResolver(schema), 32 | }); 33 | 34 | const [loading, setLoading] = useState(false); 35 | const [submitError, setSubmitError] = useState(null); 36 | const [emailsList, setEmailsList] = useState([]); 37 | const [emailsListError, setEmailsListError] = useState(""); 38 | 39 | const onSubmit = async (data) => { 40 | if (emailsList.length === 0) { 41 | setEmailsListError("*At least one email required*"); 42 | return; 43 | } 44 | setEmailsListError(""); 45 | 46 | setLoading(true); 47 | setSubmitError(null); 48 | try { 49 | const payLoad = { 50 | recipients: emailsList, 51 | frequency: data.notificationInterval, 52 | day: data.weeklyNotificationDay, 53 | time: data.notificationTime, 54 | searchQuery: data.searchQuery, 55 | candidateName: "John Doe", 56 | relevancyScore: data.relevancyScore, 57 | }; 58 | 59 | await axios.post('/api/email', payLoad, { headers: { 'Content-Type': 'application/json' } }); 60 | alert("Email Notification reminder set successfully!"); 61 | reset(); 62 | onClose(); 63 | } catch (error) { 64 | setSubmitError("An error occurred while submitting the form. Please try again."); 65 | } finally { 66 | setLoading(false); 67 | } 68 | }; 69 | 70 | const handleCancel = () => { 71 | reset(); 72 | onClose(); 73 | }; 74 | 75 | return ( 76 |
77 |

Manage Bid Notification

78 |
Configure your email notification settings for relevant business bids
79 | {submitError &&

{submitError}

} 80 |
81 |
82 | 89 | 97 | 105 | 112 | 120 |
121 | 127 | 128 |
129 | 132 | 135 |
136 | 137 |
138 | ); 139 | }; 140 | 141 | export default BidForm; --------------------------------------------------------------------------------