├── .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 |
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 |
137 |
138 | );
139 | };
140 |
141 | export default BidForm;
--------------------------------------------------------------------------------