Commit
·
8b105ad
1
Parent(s):
b4e7e2b
initial push of the update
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitignore +28 -0
- Dockerfile +40 -0
- README.md +3 -4
- backend/config.js +7 -0
- backend/config/googleOAuth.js +12 -0
- backend/controller/applicantController.js +117 -0
- backend/controller/authController.js +51 -0
- backend/controller/contactController.js +132 -0
- backend/controller/dateAvailability.js +53 -0
- backend/controller/demoRequestController.js +210 -0
- backend/controller/holidays.js +97 -0
- backend/controller/slots.js +88 -0
- backend/controller/subscriptionController.js +92 -0
- backend/controller/utils.js +57 -0
- backend/database/initdb +1 -0
- backend/get-oauth.js +94 -0
- backend/index.js +72 -0
- backend/package-lock.json +0 -0
- backend/package.json +49 -0
- backend/routes/applicantRoutes.js +13 -0
- backend/routes/authRoutes.js +10 -0
- backend/routes/availabilityRoutes.js +62 -0
- backend/routes/contactRoutes.js +9 -0
- backend/routes/demoRequestRoutes.js +10 -0
- backend/routes/subscriptionRoutes.js +10 -0
- backend/utils/addSubscriber.js +25 -0
- backend/utils/contactRequestDB.js +16 -0
- backend/utils/createEvent.js +112 -0
- backend/utils/demoRequestDB.js +22 -0
- backend/utils/jobRequestDB.js +17 -0
- backend/utils/queries.js +50 -0
- backend/utils/refreshToken.js +41 -0
- backend/utils/sendEmail.js +62 -0
- backend/utils/setupDB.js +87 -0
- frontend/.gitignore +23 -0
- frontend/README.md +70 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +81 -0
- frontend/public/index.html +43 -0
- frontend/public/logo.png +0 -0
- frontend/public/manifest.json +10 -0
- frontend/public/robots.txt +3 -0
- frontend/src/App.css +0 -0
- frontend/src/App.js +54 -0
- frontend/src/assets/images/blog-image-1.jpg +0 -0
- frontend/src/assets/images/blog-image-2.jpg +0 -0
- frontend/src/assets/images/blog-image-3.jpg +0 -0
- frontend/src/assets/images/dr_giridharan.jpeg +0 -0
- frontend/src/assets/images/dr_s_rajasekaran.jpeg +0 -0
- frontend/src/assets/images/dr_thangaraj.jpeg +0 -0
.gitignore
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Ignore test-related folders
|
2 |
+
allure_report/
|
3 |
+
allure-results/
|
4 |
+
__test__/
|
5 |
+
__mocks__/
|
6 |
+
coverage/
|
7 |
+
test_report/
|
8 |
+
|
9 |
+
# Ignore test configuration and setup files
|
10 |
+
jest.config.js
|
11 |
+
jest.setup.js
|
12 |
+
jesthtmlreporter.config.json
|
13 |
+
babel.config.js
|
14 |
+
postTestSetup.js
|
15 |
+
preTestSetup.js
|
16 |
+
runE2ETests.js
|
17 |
+
|
18 |
+
# Ignore environment files
|
19 |
+
.env
|
20 |
+
|
21 |
+
# Ignore database files
|
22 |
+
data.db
|
23 |
+
|
24 |
+
# Ignore video files
|
25 |
+
*.mp4
|
26 |
+
|
27 |
+
# Ignore node_modules
|
28 |
+
node_modules
|
Dockerfile
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Stage 1: Build the React frontend
|
2 |
+
FROM node:16 AS build
|
3 |
+
|
4 |
+
# Set the working directory
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Copy the React frontend code
|
8 |
+
COPY frontend/package.json frontend/package-lock.json ./frontend/
|
9 |
+
RUN cd frontend && npm install
|
10 |
+
|
11 |
+
# Build the React app (inside the frontend folder)
|
12 |
+
COPY frontend ./frontend
|
13 |
+
RUN cd frontend && npm run build
|
14 |
+
|
15 |
+
# Stage 2: Set up the Node.js backend
|
16 |
+
FROM node:16
|
17 |
+
|
18 |
+
# Set the working directory
|
19 |
+
WORKDIR /app
|
20 |
+
|
21 |
+
# Copy backend code
|
22 |
+
COPY backend/package.json backend/package-lock.json ./backend/
|
23 |
+
RUN cd backend && npm install
|
24 |
+
|
25 |
+
# Copy the React build files from Stage 1 to the backend/build folder
|
26 |
+
COPY --from=build /app/frontend/build ./backend/build
|
27 |
+
|
28 |
+
# Copy the backend source code
|
29 |
+
COPY backend ./backend
|
30 |
+
|
31 |
+
RUN mkdir -p /app/backend/database && chmod -R 777 /app/backend/database
|
32 |
+
|
33 |
+
|
34 |
+
# Expose the backend's port
|
35 |
+
EXPOSE 7860
|
36 |
+
|
37 |
+
WORKDIR /app/backend
|
38 |
+
|
39 |
+
# Start the backend server
|
40 |
+
CMD [ "node", "./index.js"]
|
README.md
CHANGED
@@ -1,11 +1,10 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
emoji: 🏃
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
-
short_description: Testing
|
9 |
---
|
10 |
|
11 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
---
|
2 |
+
title: Geno React Modularised
|
3 |
emoji: 🏃
|
4 |
+
colorFrom: pink
|
5 |
+
colorTo: red
|
6 |
sdk: docker
|
7 |
pinned: false
|
|
|
8 |
---
|
9 |
|
10 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
backend/config.js
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
//Flag for using vapi
|
2 |
+
const useVapi = false;
|
3 |
+
|
4 |
+
//Routes
|
5 |
+
const useApiPrefix = true; // Set to true to use /api prefix, false for no prefix
|
6 |
+
|
7 |
+
module.exports = {useVapi, useApiPrefix};
|
backend/config/googleOAuth.js
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { google } = require('googleapis');
|
2 |
+
require('dotenv').config();
|
3 |
+
|
4 |
+
// OAuth2 setup with your credentials from .env file
|
5 |
+
const CLIENT_ID = process.env.CLIENT_ID;
|
6 |
+
const CLIENT_SECRET = process.env.CLIENT_SECRET;
|
7 |
+
const REDIRECT_URI = process.env.REDIRECT_URI;
|
8 |
+
|
9 |
+
// Google OAuth2 client
|
10 |
+
const oAuth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI);
|
11 |
+
|
12 |
+
module.exports = { oAuth2Client };
|
backend/controller/applicantController.js
ADDED
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { checkAndRefreshAccessToken } = require('../utils/refreshToken');
|
2 |
+
const { checkExistingJobApplication, insertJobApplication } = require('../utils/jobRequestDB');
|
3 |
+
const { sendEmail } = require('../utils/sendEmail');
|
4 |
+
|
5 |
+
const submitJobApplication = async (req, res) => {
|
6 |
+
const { name, email, phone, experience, role, linkedin } = req.body;
|
7 |
+
const resume = req.file;
|
8 |
+
|
9 |
+
console.log('Received job application:', req.body);
|
10 |
+
console.log('Received job CV:', req.file);
|
11 |
+
|
12 |
+
// Basic validation
|
13 |
+
if (!name || !email || !phone || !experience || !role || !linkedin || !resume) {
|
14 |
+
console.log("Missing required fields");
|
15 |
+
return res.status(400).json({ error: 'Please fill in all required fields.' });
|
16 |
+
}
|
17 |
+
|
18 |
+
// Validate email format
|
19 |
+
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
20 |
+
if (!emailRegex.test(email)) {
|
21 |
+
console.log("Invalid email format");
|
22 |
+
return res.status(400).json({ error: 'Please enter a valid email address.' });
|
23 |
+
}
|
24 |
+
|
25 |
+
// Refresh access token if needed
|
26 |
+
await checkAndRefreshAccessToken();
|
27 |
+
|
28 |
+
// Check if job request already exists
|
29 |
+
const existingRequest = await checkExistingJobApplication(name, email, role);
|
30 |
+
if (existingRequest) {
|
31 |
+
console.log(`Your Job request for the role ${role} is already in queue`);
|
32 |
+
return res.status(400).json({ error: `Your Job request for the role ${role} is already in queue` });
|
33 |
+
}
|
34 |
+
|
35 |
+
const filename = resume.originalname;
|
36 |
+
|
37 |
+
// Insert application into the database
|
38 |
+
await insertJobApplication(name, email, phone, experience, role, linkedin, resume.buffer, filename);
|
39 |
+
console.log('Job Application added to DB successfully');
|
40 |
+
|
41 |
+
try{
|
42 |
+
// =============================
|
43 |
+
// Email to Applicant
|
44 |
+
// =============================
|
45 |
+
const applicantSubject = "Job Application Received";
|
46 |
+
const applicantHtmlMessage = `
|
47 |
+
<div style="font-family: Arial, sans-serif; color: #333;">
|
48 |
+
<center><img src="https://drive.google.com/thumbnail?id=17oMmzl_mTNvohvLhSWHbb_XNPfCC8KaO" alt="Genomatics Logo" height="150px"></center>
|
49 |
+
<h2 style="color: #0056b3;">Hello ${name},</h2>
|
50 |
+
<p>Thank you for submitting your application for the ${role} role at Genomatics. We’ve received your details and will review your qualifications shortly.</p>
|
51 |
+
<p><strong>Submitted Details:</strong></p>
|
52 |
+
<ul>
|
53 |
+
<li><strong>Name:</strong> ${name}</li>
|
54 |
+
<li><strong>Email:</strong> ${email}</li>
|
55 |
+
<li><strong>Phone:</strong> ${phone}</li>
|
56 |
+
<li><strong>Experience:</strong> ${experience} years</li>
|
57 |
+
<li><strong>Role:</strong> ${role}</li>
|
58 |
+
<li><strong>LinkedIn:</strong> <a href="${linkedin}" target="_blank">${linkedin}</a></li>
|
59 |
+
</ul>
|
60 |
+
<p>We appreciate your interest and will reach out if there’s a match for the position.</p>
|
61 |
+
<p style="margin-top: 20px;">Best regards,</p>
|
62 |
+
<p><strong>The Genomatics Hiring Team</strong></p>
|
63 |
+
|
64 |
+
<hr style="border: 0; height: 1px; background: #ddd; margin: 20px 0;">
|
65 |
+
<p style="font-size: 12px; color: #666;">
|
66 |
+
This email was sent in response to your job application form submission on the Genomatics platform.
|
67 |
+
</p>
|
68 |
+
|
69 |
+
<!-- Social Media Links Section -->
|
70 |
+
<div style="margin-top: 30px; text-align: center; display: flex; justify-content: center; align-items: center;">
|
71 |
+
<p>Follow us on social media:</p>
|
72 |
+
<a href="https://x.com/" target="_blank" style="margin: 0 10px;">
|
73 |
+
<img src="https://uxwing.com/wp-content/themes/uxwing/download/brands-and-social-media/x-social-media-white-icon.png" alt="Twitter" width="20" height="20" style="border-radius: 50%; background-color: #3A61B9; padding: 5px;">
|
74 |
+
</a>
|
75 |
+
<a href="https://www.linkedin.com/company/genomatics" target="_blank" style="margin: 0 10px;">
|
76 |
+
<img src="https://img.icons8.com/?size=100&id=102748&format=png&color=FFFFFF" alt="LinkedIn" width="20" height="20" style="border-radius: 50%; background-color: #3A61B9; padding: 5px;">
|
77 |
+
</a>
|
78 |
+
</div>
|
79 |
+
</div>`;
|
80 |
+
|
81 |
+
await sendEmail(email, applicantSubject, applicantHtmlMessage);
|
82 |
+
|
83 |
+
// =============================
|
84 |
+
// 🔹 Email to Authorities (HR Team)
|
85 |
+
// =============================
|
86 |
+
const authorityEmail = process.env.TEAM_MAIL_IDS;
|
87 |
+
const authoritySubject = `New Job Application for ${role}`;
|
88 |
+
const authorityHtmlMessage = `
|
89 |
+
<div style="font-family: Arial, sans-serif; color: #333; line-height: 1.6;">
|
90 |
+
<center><img src="https://drive.google.com/thumbnail?id=17oMmzl_mTNvohvLhSWHbb_XNPfCC8KaO" alt="Genomatics Logo" height="150px"></center>
|
91 |
+
<h2 style="color: #0056b3;">📢 New Job Application Received</h2>
|
92 |
+
<p>Dear Hiring Team,</p>
|
93 |
+
<p>We have received a new job application. Below are the applicant's details:</p>
|
94 |
+
<ul>
|
95 |
+
<li><strong>Name:</strong> ${name}</li>
|
96 |
+
<li><strong>Email:</strong> ${email}</li>
|
97 |
+
<li><strong>Phone:</strong> ${phone}</li>
|
98 |
+
<li><strong>Experience:</strong> ${experience} years</li>
|
99 |
+
<li><strong>Role Applied:</strong> ${role}</li>
|
100 |
+
<li><strong>LinkedIn:</strong> <a href="${linkedin}" target="_blank">${linkedin}</a></li>
|
101 |
+
</ul>
|
102 |
+
<p>📎 The applicant has attached their resume for your review.</p>
|
103 |
+
<p>Kindly review the application and take the necessary steps.</p>
|
104 |
+
<p style="margin-top: 20px;">Best regards,</p>
|
105 |
+
<p><strong>Genomatics Hiring System</strong></p>
|
106 |
+
</div>`;
|
107 |
+
|
108 |
+
await sendEmail(authorityEmail, authoritySubject, authorityHtmlMessage, resume, filename);
|
109 |
+
|
110 |
+
res.status(200).json({ message: 'Your Job Application has been submitted successfully!' });
|
111 |
+
}catch (error) {
|
112 |
+
console.error('Error sending emails:', error);
|
113 |
+
res.status(400).json({ error: 'Unable to send email for the confirmation of submission, but the application was received suvvessfully!' });
|
114 |
+
}
|
115 |
+
};
|
116 |
+
|
117 |
+
module.exports = { submitJobApplication };
|
backend/controller/authController.js
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const fs = require('fs');
|
2 |
+
const { google } = require('googleapis');
|
3 |
+
require('dotenv').config();
|
4 |
+
|
5 |
+
// import the OAuth2 client
|
6 |
+
const { oAuth2Client } = require('../config/googleOAuth');
|
7 |
+
|
8 |
+
// Controller function for /auth route
|
9 |
+
const redirectToGoogleOAuth = (req, res) => {
|
10 |
+
const authUrl = oAuth2Client.generateAuthUrl({
|
11 |
+
access_type: 'offline', // Request offline access for refresh token
|
12 |
+
scope: ['https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/calendar'], // Scopes
|
13 |
+
});
|
14 |
+
res.redirect(authUrl); // Redirect to Google's OAuth consent page
|
15 |
+
console.log('Redirecting for OAuth consent...');
|
16 |
+
};
|
17 |
+
|
18 |
+
// Controller function to handle OAuth callback
|
19 |
+
const handleOAuthCallback = async (req, res) => {
|
20 |
+
const code = req.query.code;
|
21 |
+
|
22 |
+
try {
|
23 |
+
const { tokens } = await oAuth2Client.getToken(code); // Exchange code for tokens
|
24 |
+
console.log('Received tokens:', tokens); // Log tokens for debugging
|
25 |
+
|
26 |
+
// Set tokens to the OAuth2 client
|
27 |
+
oAuth2Client.setCredentials(tokens);
|
28 |
+
|
29 |
+
// Save tokens to the .env file
|
30 |
+
saveTokensToEnv(tokens);
|
31 |
+
|
32 |
+
res.send('Authorization successful! You can now send emails.');
|
33 |
+
} catch (error) {
|
34 |
+
console.error('Error during OAuth callback:', error);
|
35 |
+
res.status(500).send('Failed to authenticate with Google');
|
36 |
+
}
|
37 |
+
};
|
38 |
+
|
39 |
+
// Helper function to save tokens to .env file
|
40 |
+
const saveTokensToEnv = (tokens) => {
|
41 |
+
const envVariables = `
|
42 |
+
ACCESS_TOKEN=${tokens.access_token}
|
43 |
+
REFRESH_TOKEN=${tokens.refresh_token || 'No refresh token available'}
|
44 |
+
`;
|
45 |
+
|
46 |
+
// Append tokens to the .env file
|
47 |
+
fs.appendFileSync('.env', envVariables, 'utf8');
|
48 |
+
console.log('Tokens saved to .env file.');
|
49 |
+
};
|
50 |
+
|
51 |
+
module.exports = { redirectToGoogleOAuth, handleOAuthCallback };
|
backend/controller/contactController.js
ADDED
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { checkAndRefreshAccessToken } = require('../utils/refreshToken');
|
2 |
+
const { checkExistingContactRequest, insertContactRequest } = require('../utils/contactRequestDB');
|
3 |
+
const fetch = require('node-fetch');
|
4 |
+
const { sendEmail } = require('../utils/sendEmail');
|
5 |
+
|
6 |
+
//Getting Vapi Usage Flag (Whether to use or not)
|
7 |
+
const { useVapi} = require("../config");
|
8 |
+
|
9 |
+
const assistant_url = 'https://api.vapi.ai/call';
|
10 |
+
|
11 |
+
const submitContactForm = async (req, res) => {
|
12 |
+
const { name, email, phone, subject, message } = req.body;
|
13 |
+
console.log('Received contact form submission:', req.body);
|
14 |
+
|
15 |
+
if (!name || !email || !phone || !subject || !message) {
|
16 |
+
return res.status(400).send({ error: 'All fields are required.' });
|
17 |
+
}
|
18 |
+
|
19 |
+
await checkAndRefreshAccessToken();
|
20 |
+
|
21 |
+
const existingRequest = await checkExistingContactRequest(name, email, subject);
|
22 |
+
if (existingRequest) {
|
23 |
+
console.log("Your contact request with the same subject is in queue");
|
24 |
+
return res.status(400).send({ error: 'Your contact request with the same subject is in queue' });
|
25 |
+
}
|
26 |
+
|
27 |
+
await insertContactRequest(name, email, phone, subject, message);
|
28 |
+
console.log('Contact request added successfully');
|
29 |
+
|
30 |
+
try{
|
31 |
+
// 🔹 Email to User
|
32 |
+
const userEmailSubject = `Contact Form Submission: ${subject}`;
|
33 |
+
const userEmailContent = `
|
34 |
+
<div style="font-family: Arial, sans-serif; color: #333;">
|
35 |
+
<center><img src="https://drive.google.com/thumbnail?id=17oMmzl_mTNvohvLhSWHbb_XNPfCC8KaO" alt="Genomatics Logo" height="150px"></center>
|
36 |
+
<h2 style="color: #0056b3;">Hello ${name},</h2>
|
37 |
+
<p>Thank you for reaching out to us! We have received your message and will get back to you soon.</p>
|
38 |
+
|
39 |
+
<p><strong>Your Submitted Information:</strong></p>
|
40 |
+
<p><strong>Name:</strong> ${name}</p>
|
41 |
+
<p><strong>Email:</strong> ${email}</p>
|
42 |
+
<p><strong>Subject:</strong> ${subject}</p>
|
43 |
+
<p><strong>Message:</strong></p>
|
44 |
+
<p>${message}</p>
|
45 |
+
|
46 |
+
<p>We appreciate your interest and are excited to help you. If you have any immediate questions, please feel free to reply to this email directly.</p>
|
47 |
+
|
48 |
+
<p style="margin-top: 20px;">Best regards,</p>
|
49 |
+
<p><strong>The Genomatics Team</strong></p>
|
50 |
+
|
51 |
+
<hr style="border: 0; height: 1px; background: #ddd; margin: 20px 0;">
|
52 |
+
<p style="font-size: 12px; color: #666;">
|
53 |
+
This email was sent in response to your contact form submission on the Genomatics platform.
|
54 |
+
</p>
|
55 |
+
|
56 |
+
<!-- Social Media Links Section -->
|
57 |
+
<div style="margin-top: 30px; text-align: center; display: flex; justify-content: center; align-items: center;">
|
58 |
+
<p>Follow us on social media:</p>
|
59 |
+
<a href="https://x.com/" target="_blank" style="margin: 0 10px;">
|
60 |
+
<img src="https://uxwing.com/wp-content/themes/uxwing/download/brands-and-social-media/x-social-media-white-icon.png" alt="Twitter" width="20" height="20" style="border-radius: 50%; background-color: #3A61B9; padding: 5px;">
|
61 |
+
</a>
|
62 |
+
<a href="https://www.linkedin.com/company/genomatics" target="_blank" style="margin: 0 10px;">
|
63 |
+
<img src="https://img.icons8.com/?size=100&id=102748&format=png&color=FFFFFF" alt="LinkedIn" width="20" height="20" style="border-radius: 50%; background-color: #3A61B9; padding: 5px;">
|
64 |
+
</a>
|
65 |
+
</div>
|
66 |
+
</div>`;
|
67 |
+
|
68 |
+
await sendEmail(email, userEmailSubject, userEmailContent);
|
69 |
+
console.log("Confirmation email sent to user");
|
70 |
+
|
71 |
+
// 🔹 Email to Team
|
72 |
+
const teamEmail = process.env.TEAM_MAIL_IDS;
|
73 |
+
const teamEmailSubject = `New Contact Form Submission: ${subject}`;
|
74 |
+
const teamEmailContent = `
|
75 |
+
<div style="font-family: Arial, sans-serif; color: #333; line-height: 1.6;">
|
76 |
+
<center><img src="https://drive.google.com/thumbnail?id=17oMmzl_mTNvohvLhSWHbb_XNPfCC8KaO" alt="Genomatics Logo" height="150px"></center>
|
77 |
+
<h2 style="color: #0056b3;">📩 New Contact Request Received</h2>
|
78 |
+
<p>Dear Team,</p>
|
79 |
+
<p>A new contact form with some general enquiry/request has been submitted. Details are as follows:</p>
|
80 |
+
<table style="width: 100%; border-collapse: collapse;">
|
81 |
+
<tr><td style="padding: 8px; font-weight: bold;">Name:</td><td style="padding: 8px;">${name}</td></tr>
|
82 |
+
<tr><td style="padding: 8px; font-weight: bold;">Email:</td><td style="padding: 8px;">${email}</td></tr>
|
83 |
+
<tr><td style="padding: 8px; font-weight: bold;">Phone:</td><td style="padding: 8px;">${phone}</td></tr>
|
84 |
+
<tr><td style="padding: 8px; font-weight: bold;">Subject:</td><td style="padding: 8px;">${subject}</td></tr>
|
85 |
+
<tr><td style="padding: 8px; font-weight: bold;">Message:</td><td style="padding: 8px;">${message}</td></tr>
|
86 |
+
</table>
|
87 |
+
<p>Please follow up with the requester accordingly.</p>
|
88 |
+
<p style="margin-top: 20px;">Best regards,</p>
|
89 |
+
<p><strong>Genomatics System</strong></p>
|
90 |
+
</div>`;
|
91 |
+
|
92 |
+
await sendEmail(teamEmail, teamEmailSubject, teamEmailContent);
|
93 |
+
console.log("Notification email sent to team");
|
94 |
+
|
95 |
+
res.status(200).send({ message: 'Contact message sent successfully!' });
|
96 |
+
} catch (error) {
|
97 |
+
console.error('Unable to send Contact request confirmation email, however request registered successfully!', error);
|
98 |
+
res.status(200).send({ error: 'Unable to send Contact request confirmation email, however request registered successfully!' });
|
99 |
+
}
|
100 |
+
|
101 |
+
// 🔹 Vapi AI Call
|
102 |
+
const curr_time = new Date().toLocaleDateString('en-GB').replace(/\//g, '-');
|
103 |
+
const postData = {
|
104 |
+
"name": `${phone}_${curr_time}_CRC`,
|
105 |
+
"assistantId": process.env.CONTACT_ASSISTANT_ID,
|
106 |
+
"assistantOverrides": {
|
107 |
+
"variableValues": {
|
108 |
+
"name": name,
|
109 |
+
"subject": subject,
|
110 |
+
"comments": message
|
111 |
+
}
|
112 |
+
},
|
113 |
+
"customer": { "number": phone },
|
114 |
+
"phoneNumberId": process.env.PHONE_NUMBER_ID
|
115 |
+
};
|
116 |
+
|
117 |
+
if(useVapi){
|
118 |
+
fetch(assistant_url, {
|
119 |
+
method: 'POST',
|
120 |
+
headers: {
|
121 |
+
'Content-Type': 'application/json',
|
122 |
+
'Authorization': `Bearer ${process.env.VAPI_KEY}`
|
123 |
+
},
|
124 |
+
body: JSON.stringify(postData)
|
125 |
+
})
|
126 |
+
.then(response => response.json())
|
127 |
+
.then(data => console.log('Vapi AI Call Success:', data))
|
128 |
+
.catch(error => console.error('Vapi AI Call Error:', error));
|
129 |
+
}
|
130 |
+
};
|
131 |
+
|
132 |
+
module.exports = { submitContactForm };
|
backend/controller/dateAvailability.js
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { runSelectQuery } = require('../utils/queries'); // Assuming db.js handles DB queries
|
2 |
+
const {isHoliday} = require("../controller/holidays")
|
3 |
+
const { DateTime } = require('luxon');
|
4 |
+
|
5 |
+
async function checkDateAvailability(date) {
|
6 |
+
const checkQuery = `SELECT COUNT(*) AS count FROM demo_requests WHERE demo_date = ?;`;
|
7 |
+
const results = await runSelectQuery(checkQuery, [date]);
|
8 |
+
|
9 |
+
return results.length === 0 || results[0].count < 11;
|
10 |
+
}
|
11 |
+
|
12 |
+
async function getAvailableDates(currentDate) {
|
13 |
+
const availableDates = [];
|
14 |
+
// Loop until we collect 14 available dates
|
15 |
+
while (availableDates.length < 14) {
|
16 |
+
// Check if the current date is a holiday in India
|
17 |
+
const isHolidayResult = await isHoliday(currentDate.toFormat('yyyy-MM-dd')); // Pass as JS Date
|
18 |
+
const isSundayResult = currentDate.weekday === 7; // Sunday in Luxon is 7
|
19 |
+
const isSaturdayResult = currentDate.weekday === 6; // Saturday in Luxon is 6
|
20 |
+
|
21 |
+
// Count which Saturday it is in the month
|
22 |
+
let saturdayCount = 0;
|
23 |
+
let tempDate = DateTime.local(currentDate.year, currentDate.month, 1);
|
24 |
+
|
25 |
+
while (tempDate < currentDate) {
|
26 |
+
if (tempDate.weekday === 6) { // If it's a Saturday
|
27 |
+
saturdayCount++;
|
28 |
+
}
|
29 |
+
tempDate = tempDate.plus({ days: 1 });
|
30 |
+
}
|
31 |
+
|
32 |
+
const isFirstThirdOrFifthSaturday = isSaturdayResult && [0, 2, 4].includes(saturdayCount);
|
33 |
+
|
34 |
+
// Check if the date is available
|
35 |
+
const isAvailable = await checkDateAvailability(currentDate.toJSDate());
|
36 |
+
|
37 |
+
// If it's not a holiday, not a Sunday, not a Saturday (if 1st, 3rd, or 5th Saturday), and it's available, add to available dates
|
38 |
+
if (!isHolidayResult && !isSundayResult && !isFirstThirdOrFifthSaturday && isAvailable) {
|
39 |
+
// Add the date in 'yyyy-MM-dd' format
|
40 |
+
availableDates.push(currentDate.toFormat('yyyy-MM-dd'));
|
41 |
+
console.log('Added date:', currentDate);
|
42 |
+
} else {
|
43 |
+
console.log('Date skipped:', currentDate.toISODate());
|
44 |
+
}
|
45 |
+
|
46 |
+
// Move to the next day (still in Asia/Kolkata timezone)
|
47 |
+
currentDate = currentDate.plus({ days: 1 });
|
48 |
+
}
|
49 |
+
|
50 |
+
return availableDates;
|
51 |
+
}
|
52 |
+
|
53 |
+
module.exports = { checkDateAvailability, getAvailableDates };
|
backend/controller/demoRequestController.js
ADDED
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { checkAndRefreshAccessToken } = require('../utils/refreshToken');
|
2 |
+
const { insertDemoRequest, checkExistingDemoRequest } = require('../utils/demoRequestDB');
|
3 |
+
const { sendEmail } = require('../utils/sendEmail');
|
4 |
+
const { DateTime } = require('luxon');
|
5 |
+
const fetch = require('node-fetch');
|
6 |
+
const {createGoogleCalendarEvent} = require('../utils/createEvent');
|
7 |
+
|
8 |
+
//Getting Vapi Usage Flag (Whether to use or not)
|
9 |
+
const { useVapi} = require("../config");
|
10 |
+
|
11 |
+
// The assistant URL for Vapi & other related config
|
12 |
+
const assistant_url = 'https://api.vapi.ai/call';
|
13 |
+
|
14 |
+
const demoRequest = async (req, res) => {
|
15 |
+
const { name, email, company, product, demoDate, selectedSlot, phone, additionalComments, timezone } = req.body;
|
16 |
+
console.log('Received demo request:', req.body);
|
17 |
+
|
18 |
+
// Basic validation on the server side
|
19 |
+
if (!name || !email || !company || !demoDate || !phone || !product || !selectedSlot) {
|
20 |
+
console.log("Missing required fields");
|
21 |
+
return res.status(400).send({ error: 'Please fill in all required fields.' });
|
22 |
+
}
|
23 |
+
|
24 |
+
const convertedDemoDate = DateTime.fromISO(demoDate, { zone: 'utc' }) // Parse demoDate as UTC
|
25 |
+
.setZone('Asia/Kolkata') // Convert it to Asia/Kolkata time zone
|
26 |
+
.toFormat('yyyy-MM-dd'); // Format it to 'yyyy-MM-dd'
|
27 |
+
|
28 |
+
console.log('Converted demoDate to Asia/Kolkata timezone:', convertedDemoDate);
|
29 |
+
|
30 |
+
// Step 2: Convert the Asia/Kolkata date to the user's time zone (const, as we're not reassigning it)
|
31 |
+
const userDemoDate = DateTime.fromISO(convertedDemoDate, { zone: 'Asia/Kolkata' }) // Parse demoDate as Asia/Kolkata
|
32 |
+
.setZone(timezone) // Convert it to user's time zone (e.g., 'America/New_York')
|
33 |
+
.toFormat('yyyy-MM-dd'); // Format it to 'yyyy-MM-dd'
|
34 |
+
|
35 |
+
console.log('Converted demoDate to user\'s timezone:', userDemoDate);
|
36 |
+
|
37 |
+
// 1. Check if a demo request already exists within the last 15 days
|
38 |
+
const existingRequest = await checkExistingDemoRequest(name, email, product, convertedDemoDate, phone);
|
39 |
+
|
40 |
+
if (existingRequest) {
|
41 |
+
console.log("Demo request already made within 15 days");
|
42 |
+
return res.status(400).send({
|
43 |
+
error: 'You have already requested a demo for this product within 15 days. Our team will get back to you shortly.'
|
44 |
+
});
|
45 |
+
}
|
46 |
+
|
47 |
+
const [startTime, endTime] = selectedSlot.split(' (')[0].split(' - ');
|
48 |
+
console.log('Selected time slot:', startTime, endTime);
|
49 |
+
console.log('User\'s timezone:', timezone);
|
50 |
+
// Convert start time and end time to the user's time zone first
|
51 |
+
const startUserTime = DateTime.fromFormat(startTime, 'HH:mm', { zone: timezone });
|
52 |
+
const endUserTime = DateTime.fromFormat(endTime, 'HH:mm', { zone: timezone });
|
53 |
+
|
54 |
+
// Ensure that the conversion to Asia/Kolkata time zone is successful
|
55 |
+
const startAsiaKolkata = startUserTime.setZone('Asia/Kolkata').toFormat('HH:mm');
|
56 |
+
const endAsiaKolkata = endUserTime.setZone('Asia/Kolkata').toFormat('HH:mm');
|
57 |
+
|
58 |
+
// Create the formatted time range string
|
59 |
+
const formattedRange = `${startAsiaKolkata} - ${endAsiaKolkata}`;
|
60 |
+
|
61 |
+
console.log('Converted time range to Asia/Kolkata:', formattedRange);
|
62 |
+
|
63 |
+
// 2. Insert the new demo request into the database
|
64 |
+
await insertDemoRequest(name, email, company, product, convertedDemoDate, formattedRange, phone, additionalComments);
|
65 |
+
console.log('Demo request added successfully');
|
66 |
+
|
67 |
+
const [ eventData, icsContent ] = await createGoogleCalendarEvent(name, email, demoDate, selectedSlot, product, timezone);
|
68 |
+
|
69 |
+
const attachment = {
|
70 |
+
buffer: Buffer.from(icsContent, 'utf-8'),
|
71 |
+
mimetype: 'text/calendar',
|
72 |
+
filename: 'event.ics',
|
73 |
+
contentType: 'text/calendar'
|
74 |
+
};
|
75 |
+
|
76 |
+
// // Update the email template with the calendar buttons
|
77 |
+
// const calendarButtonsHtml = `
|
78 |
+
// <div style="margin: 20px 0; text-align: center;">
|
79 |
+
// <div style="background-color:rgb(40, 131, 223); padding: 15px; border-radius: 8px; display: inline-block;">
|
80 |
+
// <p style="margin: 0 0 10px 0; font-weight: bold;">Add to Calendar:</p>
|
81 |
+
// <table style="border-collapse: separate; border-spacing: 8px; margin: 0 auto;">
|
82 |
+
// <tr>
|
83 |
+
// <td>
|
84 |
+
// <a href="${calendarLinks.google}" target="_blank" style="display: inline-block; padding: 8px 12px; background-color: #4285f4; color: white; text-decoration: none; border-radius: 4px; font-family: Arial, sans-serif; font-size: 14px;">
|
85 |
+
// Google Calendar
|
86 |
+
// </a>
|
87 |
+
// </td>
|
88 |
+
// <td>
|
89 |
+
// <a href="${calendarLinks.outlook}" target="_blank" style="display: inline-block; padding: 8px 12px; background-color: #0078d4; color: white; text-decoration: none; border-radius: 4px; font-family: Arial, sans-serif; font-size: 14px;">
|
90 |
+
// Outlook Calendar
|
91 |
+
// </a>
|
92 |
+
// </td>
|
93 |
+
// <td>
|
94 |
+
// <a href="${calendarLinks.yahoo}" target="_blank" style="display: inline-block; padding: 8px 12px; background-color: #720e9e; color: white; text-decoration: none; border-radius: 4px; font-family: Arial, sans-serif; font-size: 14px;">
|
95 |
+
// Yahoo Calendar
|
96 |
+
// </a>
|
97 |
+
// </td>
|
98 |
+
// </tr>
|
99 |
+
// </table>
|
100 |
+
// </div>
|
101 |
+
// </div>`;
|
102 |
+
|
103 |
+
// Refresh access token if needed
|
104 |
+
await checkAndRefreshAccessToken();
|
105 |
+
|
106 |
+
try {
|
107 |
+
// 🔹 Email to User
|
108 |
+
const userEmailSubject = "Demo Request Confirmation";
|
109 |
+
const userEmailContent = `
|
110 |
+
<div style="font-family: Arial, sans-serif; color: #333;">
|
111 |
+
<center><img src="https://drive.google.com/thumbnail?id=17oMmzl_mTNvohvLhSWHbb_XNPfCC8KaO" alt="Genomatics Logo" height="150px"></center>
|
112 |
+
<h2 style="color: #0056b3;">Hello ${name},</h2>
|
113 |
+
<p>Thank you for requesting a demo of our <strong>Genomatics</strong> product: <strong>${product}</strong>. We're excited to showcase how our solutions can benefit your team at <strong>${company}</strong>.</p>
|
114 |
+
<p><strong>Requested demo date:</strong> ${userDemoDate} - <strong>Time zone:</strong> ${timezone}</p>
|
115 |
+
<p><strong>Preferred time slot:</strong> ${selectedSlot}</p>
|
116 |
+
<p>We'll be in touch soon to confirm the details. In the meantime, please feel free to reach out with any specific questions or topics you’d like us to cover during the demo.</p>
|
117 |
+
<h3 style="color: #0056b3;">Additional Message from You:</h3>
|
118 |
+
<p>${additionalComments || "<em>No additional message provided.</em>"}</p>
|
119 |
+
<p style="margin-top: 20px;">Best regards,</p>
|
120 |
+
<p><strong>The Genomatics Team</strong></p>
|
121 |
+
|
122 |
+
<hr style="border: 0; height: 1px; background: #ddd; margin: 20px 0;">
|
123 |
+
<p style="font-size: 12px; color: #666;">
|
124 |
+
This email was sent in response to your contact form submission on the Genomatics platform.
|
125 |
+
</p>
|
126 |
+
|
127 |
+
<!-- Social Media Links Section -->
|
128 |
+
<div style="margin-top: 30px; text-align: center; display: flex; justify-content: center; align-items: center;">
|
129 |
+
<p>Follow us on social media:</p>
|
130 |
+
<a href="https://x.com/" target="_blank" style="margin: 0 10px;">
|
131 |
+
<img src="https://uxwing.com/wp-content/themes/uxwing/download/brands-and-social-media/x-social-media-white-icon.png" alt="Twitter" width="20" height="20" style="border-radius: 50%; background-color: #3A61B9; padding: 5px;">
|
132 |
+
</a>
|
133 |
+
<a href="https://www.linkedin.com/company/genomatics" target="_blank" style="margin: 0 10px;">
|
134 |
+
<img src="https://img.icons8.com/?size=100&id=102748&format=png&color=FFFFFF" alt="LinkedIn" width="20" height="20" style="border-radius: 50%; background-color: #3A61B9; padding: 5px;">
|
135 |
+
</a>
|
136 |
+
</div>
|
137 |
+
</div>`;
|
138 |
+
|
139 |
+
// Send the confirmation email to the user
|
140 |
+
await sendEmail(email, userEmailSubject, userEmailContent, attachment, 'event.ics');
|
141 |
+
console.log("Confirmation email sent to user");
|
142 |
+
|
143 |
+
// 🔹 Email to Team
|
144 |
+
const teamEmail = process.env.TEAM_MAIL_IDS;
|
145 |
+
const teamEmailSubject = `New Demo Request: ${product}`;
|
146 |
+
const teamEmailContent = `
|
147 |
+
<div style="font-family: Arial, sans-serif; color: #333; line-height: 1.6;">
|
148 |
+
<center><img src="https://drive.google.com/thumbnail?id=17oMmzl_mTNvohvLhSWHbb_XNPfCC8KaO" alt="Genomatics Logo" height="150px"></center>
|
149 |
+
<h2 style="color: #0056b3;">📩 New Demo Request Received</h2>
|
150 |
+
<p>Dear Team,</p>
|
151 |
+
<p>A new demo request has been submitted. Details are as follows:</p>
|
152 |
+
<table style="width: 100%; border-collapse: collapse;">
|
153 |
+
<tr><td style="padding: 8px; font-weight: bold;">Name:</td><td style="padding: 8px;">${name}</td></tr>
|
154 |
+
<tr><td style="padding: 8px; font-weight: bold;">Email:</td><td style="padding: 8px;">${email}</td></tr>
|
155 |
+
<tr><td style="padding: 8px; font-weight: bold;">Company:</td><td style="padding: 8px;">${company}</td></tr>
|
156 |
+
<tr><td style="padding: 8px; font-weight: bold;">Product:</td><td style="padding: 8px;">${product}</td></tr>
|
157 |
+
<tr><td style="padding: 8px; font-weight: bold;">Phone:</td><td style="padding: 8px;">${phone}</td></tr>
|
158 |
+
<tr><td style="padding: 8px; font-weight: bold;">Demo Date:</td><td style="padding: 8px;">${convertedDemoDate}</td></tr>
|
159 |
+
<tr><td style="padding: 8px; font-weight: bold;">Preferred Slot:</td><td style="padding: 8px;">${formattedRange}</td></tr>
|
160 |
+
<tr><td style="padding: 8px; font-weight: bold;">Additional Comments:</td><td style="padding: 8px;">${additionalComments || "<em>No additional comments provided.</em>"}</td></tr>
|
161 |
+
</table>
|
162 |
+
<p>Please follow up with the requester accordingly.</p>
|
163 |
+
<p style="margin-top: 20px;">Best regards,</p>
|
164 |
+
<p><strong>Genomatics System</strong></p>
|
165 |
+
</div>`;
|
166 |
+
|
167 |
+
// Send the notification email to the team
|
168 |
+
await sendEmail(teamEmail, teamEmailSubject, teamEmailContent, attachment, 'event.ics');
|
169 |
+
console.log("Notification email sent to team");
|
170 |
+
|
171 |
+
res.status(200).send({ message: 'Demo request submitted successfully! Our team will get in touch with you soon.' });
|
172 |
+
} catch (error) {
|
173 |
+
console.error('Unable to send demo request confirmation email, however request registered successfully!', error);
|
174 |
+
res.status(200).send({ error: 'Unable to send demo request confirmation email, however the demo request has been registered successfully!' });
|
175 |
+
}
|
176 |
+
|
177 |
+
// 🔹 Vapi AI Call
|
178 |
+
const curr_time = DateTime.now().toFormat('yyyy-MM-dd');
|
179 |
+
const postData = {
|
180 |
+
"name": `${phone}_${curr_time}_DRC`,
|
181 |
+
"assistantId": process.env.DEMO_ASSISTANT_ID,
|
182 |
+
"assistantOverrides": {
|
183 |
+
"variableValues": {
|
184 |
+
"name": name,
|
185 |
+
"product_name": product,
|
186 |
+
"demo_date": demoDate,
|
187 |
+
"slot": selectedSlot,
|
188 |
+
"comments": additionalComments
|
189 |
+
}
|
190 |
+
},
|
191 |
+
"customer": { "number": phone },
|
192 |
+
"phoneNumberId": process.env.PHONE_NUMBER_ID
|
193 |
+
};
|
194 |
+
|
195 |
+
if(useVapi){
|
196 |
+
fetch(assistant_url, {
|
197 |
+
method: 'POST',
|
198 |
+
headers: {
|
199 |
+
'Content-Type': 'application/json',
|
200 |
+
'Authorization': `Bearer ${process.env.VAPI_KEY}`
|
201 |
+
},
|
202 |
+
body: JSON.stringify(postData)
|
203 |
+
})
|
204 |
+
.then(response => response.json())
|
205 |
+
.then(data => console.log('Vapi AI Call Success:', data))
|
206 |
+
.catch(error => console.error('Vapi AI Call Error:', error));
|
207 |
+
}
|
208 |
+
};
|
209 |
+
|
210 |
+
module.exports = { demoRequest };
|
backend/controller/holidays.js
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { google } = require('googleapis');
|
2 |
+
const { oAuth2Client } = require('../config/googleOAuth');
|
3 |
+
const { checkAndRefreshAccessToken } = require('../utils/refreshToken');
|
4 |
+
|
5 |
+
let holidays = [];
|
6 |
+
let allHolidayDates = [];
|
7 |
+
|
8 |
+
async function getIndiaTamilHolidays() {
|
9 |
+
try {
|
10 |
+
const userTokens = await checkAndRefreshAccessToken();
|
11 |
+
|
12 |
+
// Check if OAuth tokens are available
|
13 |
+
if (!userTokens || !userTokens.access_token) {
|
14 |
+
console.log("OAuth credentials missing. Please authorize the app first.");
|
15 |
+
return { error: 'OAuth credentials missing. Please authorize the app first.' };
|
16 |
+
}
|
17 |
+
|
18 |
+
// Set OAuth2 credentials
|
19 |
+
oAuth2Client.setCredentials(userTokens);
|
20 |
+
|
21 |
+
// Initialize Google Calendar API
|
22 |
+
const calendar = google.calendar({ version: 'v3', auth: oAuth2Client });
|
23 |
+
|
24 |
+
// Define the Indian Holidays calendar ID and time range
|
25 |
+
// const calendarId = 'en.indian#[email protected]';
|
26 |
+
const calendarId = '[email protected]'; //Tamil Nadu holidays
|
27 |
+
const currentYear = new Date().getFullYear(); //works in Indian server only mostly I guess
|
28 |
+
const timeMin = `${currentYear}-01-01T00:00:00Z`;
|
29 |
+
const timeMax = `${currentYear + 50}-12-31T23:59:59Z`;
|
30 |
+
|
31 |
+
// Fetch holiday events
|
32 |
+
const response = await calendar.events.list({
|
33 |
+
calendarId,
|
34 |
+
timeMin,
|
35 |
+
timeMax,
|
36 |
+
singleEvents: true,
|
37 |
+
orderBy: 'startTime',
|
38 |
+
});
|
39 |
+
|
40 |
+
// Extract and format holiday details
|
41 |
+
const holidays = [];
|
42 |
+
response.data.items.forEach(event => {
|
43 |
+
const summary = event.summary;
|
44 |
+
const startDate = event.start.date;
|
45 |
+
const endDate = event.end?.date || event.start.date;
|
46 |
+
|
47 |
+
// Handle single-day or multi-day holidays
|
48 |
+
const start = new Date(startDate);
|
49 |
+
const end = new Date(endDate);
|
50 |
+
|
51 |
+
// Iterate through all dates for multi-day holidays
|
52 |
+
for (let d = new Date(start); d < end; d.setDate(d.getDate() + 1)) {
|
53 |
+
console.log('Holiday:', summary, d);
|
54 |
+
holidays.push({
|
55 |
+
festival: summary,
|
56 |
+
date: d.toISOString().split('T')[0], // Format date as YYYY-MM-DD
|
57 |
+
});
|
58 |
+
}
|
59 |
+
});
|
60 |
+
|
61 |
+
return holidays;
|
62 |
+
} catch (error) {
|
63 |
+
console.error('Error fetching India holidays:', error.message);
|
64 |
+
throw error;
|
65 |
+
}
|
66 |
+
}
|
67 |
+
|
68 |
+
// Call the function to get tamil holidays
|
69 |
+
async function refreshHolidays() {
|
70 |
+
try {
|
71 |
+
holidays = await getIndiaTamilHolidays(); // Fetch the holidays
|
72 |
+
console.log('India Holidays:', holidays);
|
73 |
+
console.log('Total holidays:', holidays.length);
|
74 |
+
console.log('Last holiday:', holidays[holidays.length - 1]);
|
75 |
+
|
76 |
+
allHolidayDates = holidays.reduce((accumulator, holiday) => {
|
77 |
+
accumulator.push(holiday.date); // Collect all dates in an array
|
78 |
+
return accumulator;
|
79 |
+
}, []);
|
80 |
+
|
81 |
+
console.log(allHolidayDates);
|
82 |
+
|
83 |
+
// After the function finishes, set the next interval
|
84 |
+
setTimeout(refreshHolidays, 14 * 24 * 60 * 60 * 1000); // 14 days in ms
|
85 |
+
} catch (error) {
|
86 |
+
console.error('Failed to fetch holidays:', error);
|
87 |
+
|
88 |
+
// If there's an error, retry after a short delay
|
89 |
+
setTimeout(refreshHolidays, 10000); // Retry in 10 seconds
|
90 |
+
}
|
91 |
+
}
|
92 |
+
|
93 |
+
async function isHoliday(date) {
|
94 |
+
return allHolidayDates.includes(date);
|
95 |
+
}
|
96 |
+
|
97 |
+
module.exports = { getIndiaTamilHolidays, refreshHolidays, isHoliday };
|
backend/controller/slots.js
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { runSelectQuery } = require('../utils/queries');
|
2 |
+
|
3 |
+
const generalSlots = [
|
4 |
+
`10:00 - 10:30`, `10:45 - 11:15`, `11:30 - 12:00`, `12:15 - 12:45`, `13:00 - 13:30`,
|
5 |
+
`13:45 - 14:15`, `14:30 - 15:00`, `15:15 - 15:45`, `16:00 - 16:30`, `16:45 - 17:15`, `17:30 - 18:00`
|
6 |
+
];
|
7 |
+
|
8 |
+
//async function to get list of booked slots for a given date
|
9 |
+
async function getBookedSlots(date) {
|
10 |
+
if (!date) {
|
11 |
+
throw new Error('Date is required');
|
12 |
+
}
|
13 |
+
|
14 |
+
// Ensure the date is in the correct format (YYYY-MM-DD)
|
15 |
+
const isValidDate = /^\d{4}-\d{2}-\d{2}$/.test(date);
|
16 |
+
if (!isValidDate) {
|
17 |
+
throw new Error('Invalid date format. Expected YYYY-MM-DD');
|
18 |
+
}
|
19 |
+
|
20 |
+
try {
|
21 |
+
const query = `
|
22 |
+
SELECT slot FROM demo_requests WHERE demo_date = ?;
|
23 |
+
`;
|
24 |
+
const results = await runSelectQuery(query, date);
|
25 |
+
|
26 |
+
console.log(`Fetched ${results.length} slots for date: ${date}`);
|
27 |
+
|
28 |
+
// Check if results are empty
|
29 |
+
if (!results || results.length === 0) {
|
30 |
+
console.log(`No slots found for date: ${date}`);
|
31 |
+
return []; // Return an empty array if no slots are found
|
32 |
+
}
|
33 |
+
|
34 |
+
// Extract slots, with checks to ensure each result contains a 'slot' property
|
35 |
+
const slots = results.map(result => {
|
36 |
+
if (!result.slot) {
|
37 |
+
console.warn(`No slot found for record: ${JSON.stringify(result)}`);
|
38 |
+
return null; // Handle cases where slot is undefined or null
|
39 |
+
}
|
40 |
+
return result.slot;
|
41 |
+
}).filter(slot => slot !== null); // Remove any null values (in case of missing slots)
|
42 |
+
|
43 |
+
return slots;
|
44 |
+
} catch (error) {
|
45 |
+
console.error('Error fetching booked slots:', error.message);
|
46 |
+
throw error; // Re-throw the error after logging it
|
47 |
+
}
|
48 |
+
}
|
49 |
+
|
50 |
+
//async function to get available slots for a given date
|
51 |
+
async function getAvailableSlots(date) {
|
52 |
+
if (!date) {
|
53 |
+
throw new Error('Date is required');
|
54 |
+
}
|
55 |
+
|
56 |
+
// Ensure the date is in the correct format (YYYY-MM-DD)
|
57 |
+
const isValidDate = /^\d{4}-\d{2}-\d{2}$/.test(date);
|
58 |
+
if (!isValidDate) {
|
59 |
+
throw new Error('Invalid date format. Expected YYYY-MM-DD');
|
60 |
+
}
|
61 |
+
|
62 |
+
try {
|
63 |
+
// Fetch the booked slots for the given date
|
64 |
+
const bookedSlots = await getBookedSlots(date);
|
65 |
+
console.log('Booked slots:', bookedSlots);
|
66 |
+
|
67 |
+
// If no booked slots, all general slots are available
|
68 |
+
if (bookedSlots.length === 0) {
|
69 |
+
return generalSlots;
|
70 |
+
}
|
71 |
+
|
72 |
+
// Check if generalSlots exists and is an array
|
73 |
+
if (!Array.isArray(generalSlots) || generalSlots.length === 0) {
|
74 |
+
throw new Error('General slots are not properly defined');
|
75 |
+
}
|
76 |
+
|
77 |
+
// Filter the general slots to remove any booked slots
|
78 |
+
const availableSlots = generalSlots.filter(slot => !bookedSlots.includes(slot));
|
79 |
+
|
80 |
+
// Return the available slots
|
81 |
+
return availableSlots;
|
82 |
+
} catch (error) {
|
83 |
+
console.error('Error fetching available slots:', error.message);
|
84 |
+
throw error; // Re-throw the error after logging it
|
85 |
+
}
|
86 |
+
}
|
87 |
+
|
88 |
+
module.exports = { getAvailableSlots, getBookedSlots};
|
backend/controller/subscriptionController.js
ADDED
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// controllers/subscriptionController.js
|
2 |
+
const {oAuth2Client} = require('../config/googleOAuth'); // Import the OAuth2 client
|
3 |
+
const { addSubscriber } = require('../utils/addSubscriber'); // Assuming this exists to add the email to the database
|
4 |
+
const { checkAndRefreshAccessToken } = require('../utils/refreshToken'); // Import the helper function
|
5 |
+
const {google} = require('googleapis');
|
6 |
+
|
7 |
+
const handleSubscriptionEmail = async (req, res) => {
|
8 |
+
const email = req.body.email;
|
9 |
+
console.log('Received request to send email to:', email);
|
10 |
+
|
11 |
+
if (!email) {
|
12 |
+
console.log("Email is required");
|
13 |
+
return res.status(400).send({ error: 'Email is required' });
|
14 |
+
}
|
15 |
+
|
16 |
+
const result = await addSubscriber(email);
|
17 |
+
|
18 |
+
if (result.error) {
|
19 |
+
// Return error if email already exists or any database error occurs
|
20 |
+
return res.status(result.status).send({ error: result.error });
|
21 |
+
}
|
22 |
+
|
23 |
+
await checkAndRefreshAccessToken(); // Refresh the token if expired
|
24 |
+
|
25 |
+
// Set OAuth2 credentials
|
26 |
+
// oAuth2Client.setCredentials(userTokens);
|
27 |
+
|
28 |
+
// Check if OAuth2 credentials are missing
|
29 |
+
if (!oAuth2Client.credentials || !oAuth2Client.credentials.access_token) {
|
30 |
+
return res.status(500).send({
|
31 |
+
error: 'OAuth credentials missing. Please authorize the app first.'
|
32 |
+
});
|
33 |
+
}
|
34 |
+
|
35 |
+
// Prepare the email content
|
36 |
+
const subject = "Subscription Confirmation";
|
37 |
+
const message = `<div style="font-family: Arial, sans-serif; color: #333;">
|
38 |
+
<img src="https://www.genomatics.in/wp-content/uploads/2020/01/cropped-LOGO-1-1.png" alt="Genomatics Logo">
|
39 |
+
<h2 style="color: #0056b3;">Hello,</h2>
|
40 |
+
<p>Thank you for subscribing to <strong>Genomatics</strong>! We’re excited to have you join our community of genomics enthusiasts, researchers, and professionals. As a valued subscriber, you’ll be among the first to receive updates on breakthrough discoveries, exclusive insights, and expert tips on leveraging genomics for impactful decision-making.</p>
|
41 |
+
|
42 |
+
<h3 style="color: #0056b3;">What You’ll Get:</h3>
|
43 |
+
<ul>
|
44 |
+
<li><strong>Exclusive Insights:</strong> Stay ahead with our latest findings on how genomics is shaping the future of medicine, biotechnology, and more.</li>
|
45 |
+
<li><strong>Updates on Our Innovations:</strong> Be the first to know about new products and tools designed to empower you in your genomics journey.</li>
|
46 |
+
<li><strong>Invitations to Webinars and Events:</strong> Gain access to events and discussions led by leaders in the field to deepen your understanding and application of genomics.</li>
|
47 |
+
</ul>
|
48 |
+
|
49 |
+
<p>If you have any questions or ideas you’d like us to cover, feel free to reach out. We’re here to support you every step of the way.</p>
|
50 |
+
|
51 |
+
<p>Once again, welcome aboard! We look forward to sharing this journey of discovery with you.</p>
|
52 |
+
|
53 |
+
<p style="margin-top: 20px;">Warm regards,</p>
|
54 |
+
<p><strong>The Genomatics Team</strong></p>
|
55 |
+
<hr style="border: 0; height: 1px; background: #ddd; margin: 20px 0;">
|
56 |
+
<p style="font-size: 12px; color: #666;">
|
57 |
+
This email was sent in response to your subscription to the Genomatics platform.
|
58 |
+
</p>
|
59 |
+
</div>`;
|
60 |
+
|
61 |
+
const emailContent = [
|
62 |
+
`To: ${email}`,
|
63 |
+
'Content-Type: text/html; charset="UTF-8"',
|
64 |
+
'MIME-Version: 1.0',
|
65 |
+
`Subject: ${subject}`,
|
66 |
+
'',
|
67 |
+
message,
|
68 |
+
].join('\n');
|
69 |
+
|
70 |
+
// Encode the email content
|
71 |
+
const encodedEmail = Buffer.from(emailContent)
|
72 |
+
.toString('base64')
|
73 |
+
.replace(/\+/g, '-')
|
74 |
+
.replace(/\//g, '_');
|
75 |
+
|
76 |
+
// Send email via Gmail API
|
77 |
+
try {
|
78 |
+
const gmail = google.gmail({ version: 'v1', auth: oAuth2Client });
|
79 |
+
const response = await gmail.users.messages.send({
|
80 |
+
userId: 'me',
|
81 |
+
requestBody: {
|
82 |
+
raw: encodedEmail,
|
83 |
+
},
|
84 |
+
});
|
85 |
+
res.status(200).send({ message: 'Subscription email sent!', response });
|
86 |
+
} catch (error) {
|
87 |
+
console.log('Error sending email:', error);
|
88 |
+
res.status(500).send({ error: 'Failed to send subscription confirmation email, however you are subscribed and will receive newsletters' });
|
89 |
+
}
|
90 |
+
};
|
91 |
+
|
92 |
+
module.exports = { handleSubscriptionEmail };
|
backend/controller/utils.js
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { DateTime } = require('luxon');
|
2 |
+
|
3 |
+
const createDateTimeSlotMapping = (data) => {
|
4 |
+
const dateTimeSlots = {};
|
5 |
+
data.forEach(dateObj => {
|
6 |
+
for (const [date, slots] of Object.entries(dateObj)) {
|
7 |
+
slots.forEach(slot => {
|
8 |
+
const [startTime, endTime] = slot.split(' - ');
|
9 |
+
const dateTime = `${date}T${startTime}`;
|
10 |
+
|
11 |
+
// Validate the dateTime before adding to the mapping
|
12 |
+
const dt = DateTime.fromISO(dateTime);
|
13 |
+
if (dt.isValid) {
|
14 |
+
dateTimeSlots[dateTime] = `${startTime} - ${endTime}`;
|
15 |
+
}
|
16 |
+
});
|
17 |
+
}
|
18 |
+
});
|
19 |
+
return dateTimeSlots;
|
20 |
+
};
|
21 |
+
|
22 |
+
|
23 |
+
async function slotsInUserZone(inter, timezone) {
|
24 |
+
const convertedData = {};
|
25 |
+
for (const [key, value] of Object.entries(inter)) {
|
26 |
+
const startTimeKolkata = DateTime.fromISO(key, { zone: "Asia/Kolkata" });
|
27 |
+
const startTimeUser = startTimeKolkata.setZone(timezone).toISO();
|
28 |
+
|
29 |
+
const [start, end] = value.split(" - ");
|
30 |
+
const startTime = DateTime.fromISO(key, { zone: "Asia/Kolkata" });
|
31 |
+
const localStartTime = startTime.setZone(timezone).toFormat("HH:mm");
|
32 |
+
|
33 |
+
const endTime = startTime.plus({ minutes: 30 });
|
34 |
+
const localEndTime = endTime.setZone(timezone).toFormat("HH:mm");
|
35 |
+
|
36 |
+
convertedData[startTimeUser] = `${localStartTime} - ${localEndTime} (${timezone})`;
|
37 |
+
}
|
38 |
+
return convertedData;
|
39 |
+
}
|
40 |
+
|
41 |
+
async function getDatesSlots(input) {
|
42 |
+
return Object.entries(input).reduce((acc, [timestamp, slot]) => {
|
43 |
+
// Validate timestamp
|
44 |
+
const dt = DateTime.fromISO(timestamp);
|
45 |
+
if (!dt.isValid) {
|
46 |
+
return acc; // Skip invalid timestamps
|
47 |
+
}
|
48 |
+
const date = timestamp.split('T')[0];
|
49 |
+
if (!acc[date]) {
|
50 |
+
acc[date] = [];
|
51 |
+
}
|
52 |
+
acc[date].push(slot);
|
53 |
+
return acc;
|
54 |
+
}, {});
|
55 |
+
}
|
56 |
+
|
57 |
+
module.exports = { createDateTimeSlotMapping, slotsInUserZone, getDatesSlots };
|
backend/database/initdb
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
//TEST
|
backend/get-oauth.js
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// index.js
|
2 |
+
const express = require('express');
|
3 |
+
const { google } = require('googleapis');
|
4 |
+
const fs = require('fs');
|
5 |
+
require('dotenv').config(); // For loading environment variables
|
6 |
+
|
7 |
+
const app = express();
|
8 |
+
const port = 7860;
|
9 |
+
|
10 |
+
// Set up OAuth2 client
|
11 |
+
const oAuth2Client = new google.auth.OAuth2(
|
12 |
+
process.env.CLIENT_ID,
|
13 |
+
process.env.CLIENT_SECRET,
|
14 |
+
process.env.REDIRECT_URI
|
15 |
+
);
|
16 |
+
|
17 |
+
let userTokens = null; // Store user tokens temporarily in memory
|
18 |
+
|
19 |
+
// Step 1: Generate the OAuth URL and redirect
|
20 |
+
app.get('/auth', (req, res) => {
|
21 |
+
const authUrl = oAuth2Client.generateAuthUrl({
|
22 |
+
access_type: 'offline', // Request offline access for refresh token
|
23 |
+
scope: ['https://www.googleapis.com/auth/gmail.send', 'https://www.googleapis.com/auth/calendar'], // Gmail send scope
|
24 |
+
});
|
25 |
+
res.redirect(authUrl); // Redirect to Google's OAuth consent page
|
26 |
+
console.log("Redirecting for OAuth consent...");
|
27 |
+
});
|
28 |
+
|
29 |
+
// Step 2: Handle OAuth callback, exchange code for tokens
|
30 |
+
app.get('/oauth2callback', async (req, res) => {
|
31 |
+
const code = req.query.code;
|
32 |
+
|
33 |
+
try {
|
34 |
+
const { tokens } = await oAuth2Client.getToken(code);
|
35 |
+
// console.log('Received tokens:', tokens); // Log tokens for debugging
|
36 |
+
|
37 |
+
// Store tokens securely (in memory, DB, or .env file)
|
38 |
+
oAuth2Client.setCredentials(tokens);
|
39 |
+
userTokens = tokens; // Store tokens in memory
|
40 |
+
|
41 |
+
// Save the tokens to .env file (for future use)
|
42 |
+
saveTokensToEnv(tokens);
|
43 |
+
|
44 |
+
res.send('Authorization successful! You can now send emails.');
|
45 |
+
} catch (error) {
|
46 |
+
console.error('Error during OAuth callback:', error);
|
47 |
+
res.status(500).send('Failed to authenticate with Google');
|
48 |
+
}
|
49 |
+
});
|
50 |
+
|
51 |
+
// Helper function to save tokens to .env file
|
52 |
+
function saveTokensToEnv(tokens) {
|
53 |
+
if (tokens.refresh_token){
|
54 |
+
const envVariables = `
|
55 |
+
ACCESS_TOKEN=${tokens.access_token}
|
56 |
+
REFRESH_TOKEN=${tokens.refresh_token || 'No refresh token available'}
|
57 |
+
`;
|
58 |
+
|
59 |
+
// Save tokens in the .env file
|
60 |
+
fs.appendFileSync('.env', envVariables, 'utf8');
|
61 |
+
console.log('Tokens saved to .env file.');
|
62 |
+
}else{
|
63 |
+
const envVariables = `
|
64 |
+
ACCESS_TOKEN=${tokens.access_token}
|
65 |
+
`;
|
66 |
+
|
67 |
+
// Save tokens in the .env file
|
68 |
+
fs.appendFileSync('.env', envVariables, 'utf8');
|
69 |
+
console.log('Tokens saved to .env file.');
|
70 |
+
}
|
71 |
+
|
72 |
+
}
|
73 |
+
|
74 |
+
// Method to check if the access token is expired
|
75 |
+
async function checkAndRefreshAccessToken() {
|
76 |
+
if (oAuth2Client.isTokenExpiring()) {
|
77 |
+
try {
|
78 |
+
// If the access token is expired or about to expire, refresh it
|
79 |
+
const response = await oAuth2Client.refreshAccessToken();
|
80 |
+
const newTokens = response.credentials;
|
81 |
+
oAuth2Client.setCredentials(newTokens); // Set the new credentials
|
82 |
+
userTokens = newTokens; // Update the tokens
|
83 |
+
|
84 |
+
console.log('Access token refreshed.');
|
85 |
+
} catch (error) {
|
86 |
+
console.error('Error refreshing access token:', error);
|
87 |
+
}
|
88 |
+
}
|
89 |
+
}
|
90 |
+
|
91 |
+
// Start the Express server
|
92 |
+
app.listen(port, () => {
|
93 |
+
console.log(`Server running at http://localhost:${port}`);
|
94 |
+
});
|
backend/index.js
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const express = require('express');
|
2 |
+
const bodyParser = require('body-parser');
|
3 |
+
const cors = require('cors');
|
4 |
+
//General
|
5 |
+
const path = require('path');
|
6 |
+
|
7 |
+
//Initialize Database
|
8 |
+
const {initializeDatabase} = require("./utils/setupDB");
|
9 |
+
initializeDatabase();
|
10 |
+
|
11 |
+
//Prefix Flag (Use or not)
|
12 |
+
const {useApiPrefix} = require('./config')
|
13 |
+
|
14 |
+
const fs = require('fs');
|
15 |
+
|
16 |
+
const path1 = '/app/backend/database';
|
17 |
+
|
18 |
+
if (fs.existsSync(path1)) {
|
19 |
+
console.log('The path exists');
|
20 |
+
} else {
|
21 |
+
console.log('The path does not exist');
|
22 |
+
}
|
23 |
+
|
24 |
+
//Refresh the holidays
|
25 |
+
const {refreshHolidays} = require("./controller/holidays");
|
26 |
+
refreshHolidays();
|
27 |
+
|
28 |
+
//Route Imports
|
29 |
+
const authRoutes = require('./routes/authRoutes'); // Import the auth routes
|
30 |
+
const availabilityRoutes = require('./routes/availabilityRoutes'); // Import availability routes
|
31 |
+
const subscriptionRoutes = require('./routes/subscriptionRoutes'); // Import subscription routes
|
32 |
+
const demoRequestRoutes = require('./routes/demoRequestRoutes'); // Import demo request routes
|
33 |
+
const contactRoutes = require('./routes/contactRoutes'); // Import contact routes
|
34 |
+
const applicantRoutes = require('./routes/applicantRoutes'); // Import applicant routes
|
35 |
+
|
36 |
+
const app = express();
|
37 |
+
|
38 |
+
app.use(express.json()); // To handle JSON request body
|
39 |
+
|
40 |
+
// Serve static files from the React build folder
|
41 |
+
app.use(express.static(path.join(__dirname, 'build')));
|
42 |
+
|
43 |
+
// Catch-all route to serve React's index.html for unknown routes
|
44 |
+
app.get('*', (req, res) => {
|
45 |
+
res.sendFile(path.join(__dirname, 'build', 'index.html'));
|
46 |
+
});
|
47 |
+
|
48 |
+
// Middleware to parse JSON body
|
49 |
+
app.use(bodyParser.json());
|
50 |
+
|
51 |
+
//Use CORS
|
52 |
+
app.use(cors());
|
53 |
+
|
54 |
+
// Routes - Dynamically add /api prefix if the flag is true
|
55 |
+
if (useApiPrefix) {
|
56 |
+
app.use('/api', authRoutes); // /api/auth
|
57 |
+
app.use('/api', subscriptionRoutes); // /api/subscribe
|
58 |
+
app.use('/api', availabilityRoutes); // /api/availability
|
59 |
+
app.use('/api', demoRequestRoutes); // /api/demo-request
|
60 |
+
app.use('/api', contactRoutes); // /api/contact
|
61 |
+
app.use('/api', applicantRoutes); // /api/applicant
|
62 |
+
} else {
|
63 |
+
app.use(authRoutes); // No prefix
|
64 |
+
app.use(subscriptionRoutes); // No prefix
|
65 |
+
app.use(availabilityRoutes); // No prefix
|
66 |
+
app.use(demoRequestRoutes); // No prefix
|
67 |
+
app.use(contactRoutes); // No prefix
|
68 |
+
app.use(applicantRoutes); // No prefix
|
69 |
+
}
|
70 |
+
|
71 |
+
const PORT = process.env.PORT || 7860;
|
72 |
+
app.listen(PORT, '0.0.0.0', () => console.log(`Server running on port: ${PORT}`));
|
backend/package-lock.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
backend/package.json
ADDED
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "email-service",
|
3 |
+
"version": "1.0.0",
|
4 |
+
"description": "",
|
5 |
+
"main": "index.js",
|
6 |
+
"scripts": {
|
7 |
+
"pretest": "node preTestSetup.js",
|
8 |
+
"test": "jest || :",
|
9 |
+
"posttest": "node postTestSetup.js"
|
10 |
+
},
|
11 |
+
"keywords": [],
|
12 |
+
"author": "",
|
13 |
+
"license": "ISC",
|
14 |
+
"dependencies": {
|
15 |
+
"@babel/plugin-transform-runtime": "^7.25.9",
|
16 |
+
"@babel/preset-env": "^7.26.7",
|
17 |
+
"@babel/preset-react": "^7.26.3",
|
18 |
+
"body-parser": "^1.20.3",
|
19 |
+
"cors": "^2.8.5",
|
20 |
+
"dotenv": "^16.4.5",
|
21 |
+
"express": "^4.21.1",
|
22 |
+
"googleapis": "^144.0.0",
|
23 |
+
"ics": "^3.8.1",
|
24 |
+
"luxon": "^3.5.0",
|
25 |
+
"multer": "^1.4.5-lts.1",
|
26 |
+
"mysql2": "^3.11.4",
|
27 |
+
"node-fetch": "^2.7.0",
|
28 |
+
"sqlite3": "^5.1.7",
|
29 |
+
"uuid": "^11.0.5"
|
30 |
+
},
|
31 |
+
"devDependencies": {
|
32 |
+
"allure-commandline": "^2.32.0",
|
33 |
+
"allure-jest": "^3.0.9",
|
34 |
+
"axios-mock-adapter": "^2.1.0",
|
35 |
+
"babel-jest": "^29.7.0",
|
36 |
+
"identity-obj-proxy": "^3.0.0",
|
37 |
+
"jest": "^29.7.0",
|
38 |
+
"jest-allure": "^0.1.3",
|
39 |
+
"jest-environment-jsdom": "^29.7.0",
|
40 |
+
"jest-html-reporter": "^3.10.2",
|
41 |
+
"supertest": "^7.0.0"
|
42 |
+
},
|
43 |
+
"jest-html-reporter": {
|
44 |
+
"pageTitle": "Backend Unit testing",
|
45 |
+
"outputPath": "test-report.html",
|
46 |
+
"includeFailureMsg": true,
|
47 |
+
"executionMode": "reporter"
|
48 |
+
}
|
49 |
+
}
|
backend/routes/applicantRoutes.js
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const express = require('express');
|
2 |
+
const router = express.Router();
|
3 |
+
|
4 |
+
// Multer setup to handle file uploads
|
5 |
+
const multer = require('multer');
|
6 |
+
const storage = multer.memoryStorage();
|
7 |
+
const upload = multer({ storage: storage });
|
8 |
+
|
9 |
+
const {submitJobApplication} = require('../controller/applicantController');
|
10 |
+
|
11 |
+
router.post('/submit-job-application', upload.single('resume'), submitJobApplication);
|
12 |
+
|
13 |
+
module.exports = router;
|
backend/routes/authRoutes.js
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const express = require('express');
|
2 |
+
const { redirectToGoogleOAuth, handleOAuthCallback } = require('../controller/authController');
|
3 |
+
|
4 |
+
const router = express.Router();
|
5 |
+
|
6 |
+
// Define the /auth routes
|
7 |
+
router.get('/auth', redirectToGoogleOAuth);
|
8 |
+
router.get('/oauth2callback', handleOAuthCallback);
|
9 |
+
|
10 |
+
module.exports = router;
|
backend/routes/availabilityRoutes.js
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const express = require('express');
|
2 |
+
const {DateTime} = require('luxon');
|
3 |
+
const { getAvailableDates } = require('../controller/dateAvailability');
|
4 |
+
const { getAvailableSlots } = require('../controller/slots');
|
5 |
+
const { createDateTimeSlotMapping, slotsInUserZone, getDatesSlots } = require('../controller/utils');
|
6 |
+
const { isHoliday } = require('../controller/holidays');
|
7 |
+
|
8 |
+
// This is the part where the route is defined.
|
9 |
+
const router = express.Router();
|
10 |
+
|
11 |
+
router.post('/get-available-dates-and-slots', async (req, res) => {
|
12 |
+
const { requiredMinDate, timezone } = req.body;
|
13 |
+
console.log('Received request to get available dates:', req.body);
|
14 |
+
|
15 |
+
if (!requiredMinDate) {
|
16 |
+
console.log("Minimum date is required");
|
17 |
+
return res.status(400).send({ error: 'Minimum date is required' });
|
18 |
+
}
|
19 |
+
|
20 |
+
try {
|
21 |
+
// Convert requiredMinDate to Asia/Kolkata timezone (starting of the day in Asia/Kolkata)
|
22 |
+
const currentDate = DateTime.fromISO(requiredMinDate).setZone('Asia/Kolkata').startOf('day');
|
23 |
+
console.log('Converted start date in Asia/Kolkata:', currentDate);
|
24 |
+
|
25 |
+
// Initialize the availableDates array
|
26 |
+
const availableDates = await getAvailableDates(currentDate);
|
27 |
+
|
28 |
+
console.log('Available dates:', availableDates);
|
29 |
+
if (availableDates){
|
30 |
+
//create an arrow function that creates an array of objects, where each object key is the availableDate and the value is the one returned by getAvailableSlots(availableDate)
|
31 |
+
const datesAndSlots = await Promise.all(availableDates.map(async availableDate => {
|
32 |
+
const slots = await getAvailableSlots(availableDate);
|
33 |
+
return { [availableDate]: slots };
|
34 |
+
}));
|
35 |
+
|
36 |
+
const inter = createDateTimeSlotMapping(datesAndSlots);
|
37 |
+
console.log(inter);
|
38 |
+
|
39 |
+
const userSlots = await slotsInUserZone(inter, timezone);
|
40 |
+
console.log(userSlots);
|
41 |
+
|
42 |
+
const finalDateSlots = await getDatesSlots(userSlots);
|
43 |
+
console.log(finalDateSlots);
|
44 |
+
|
45 |
+
const finalDates = Object.keys(finalDateSlots);
|
46 |
+
console.log(finalDates);
|
47 |
+
|
48 |
+
const response = {
|
49 |
+
finalDateSlots: finalDateSlots,
|
50 |
+
finalDates: finalDates
|
51 |
+
};
|
52 |
+
res.json(response);
|
53 |
+
}
|
54 |
+
|
55 |
+
} catch (error) {
|
56 |
+
console.error('Error fetching available dates:', error.message);
|
57 |
+
res.status(500).send({ error: 'Failed to fetch available dates' });
|
58 |
+
}
|
59 |
+
});
|
60 |
+
|
61 |
+
// Export the router so it can be used in the server.js file.
|
62 |
+
module.exports = router;
|
backend/routes/contactRoutes.js
ADDED
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const express = require('express');
|
2 |
+
const {bodyParser} = require('body-parser');
|
3 |
+
|
4 |
+
const router = express.Router();
|
5 |
+
const {submitContactForm} = require('../controller/contactController');
|
6 |
+
|
7 |
+
router.post('/submit-contact-form', submitContactForm);
|
8 |
+
|
9 |
+
module.exports = router;
|
backend/routes/demoRequestRoutes.js
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// routes/subscriptionRoutes.js
|
2 |
+
const express = require('express');
|
3 |
+
const { demoRequest } = require('../controller/demoRequestController'); // Import controller
|
4 |
+
|
5 |
+
const router = express.Router();
|
6 |
+
|
7 |
+
// POST route for subscription email
|
8 |
+
router.post('/demo-request', demoRequest);
|
9 |
+
|
10 |
+
module.exports = router;
|
backend/routes/subscriptionRoutes.js
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// routes/subscriptionRoutes.js
|
2 |
+
const express = require('express');
|
3 |
+
const { handleSubscriptionEmail } = require('../controller/subscriptionController'); // Import controller
|
4 |
+
|
5 |
+
const router = express.Router();
|
6 |
+
|
7 |
+
// POST route for subscription email
|
8 |
+
router.post('/subscription-email', handleSubscriptionEmail);
|
9 |
+
|
10 |
+
module.exports = router;
|
backend/utils/addSubscriber.js
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { runQuery, runSelectQuery } = require('./queries');
|
2 |
+
|
3 |
+
async function addSubscriber(email) {
|
4 |
+
try {
|
5 |
+
// Check if email exists
|
6 |
+
const results = await runSelectQuery('SELECT * FROM subscribers WHERE email = ?', [email]);
|
7 |
+
if (results.length > 0) {
|
8 |
+
console.log("Email already exists in subscribers");
|
9 |
+
return { status: 400, error: 'You have already subscribed!!' };
|
10 |
+
}
|
11 |
+
|
12 |
+
// Insert new subscriber
|
13 |
+
await runQuery(
|
14 |
+
'INSERT INTO subscribers (date, time, email) VALUES (DATE("now"), TIME("now"), ?)',
|
15 |
+
[email]
|
16 |
+
);
|
17 |
+
console.log('Subscriber added successfully');
|
18 |
+
return { status: 200, message: 'Subscriber added' };
|
19 |
+
} catch (error) {
|
20 |
+
console.error('Database error:', error);
|
21 |
+
return { status: 500, error: 'Failed to add subscriber' };
|
22 |
+
}
|
23 |
+
}
|
24 |
+
|
25 |
+
module.exports = { addSubscriber };
|
backend/utils/contactRequestDB.js
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const {runQuery, runSelectQuery} = require('./queries');
|
2 |
+
|
3 |
+
// Check existing contact request
|
4 |
+
async function checkExistingContactRequest(name, email, subject) {
|
5 |
+
const checkQuery = `SELECT * FROM contact_requests WHERE name = ? AND email = ? AND subject = ?;`;
|
6 |
+
const results = await runSelectQuery(checkQuery, [name, email, subject]);
|
7 |
+
return results.length > 0;
|
8 |
+
}
|
9 |
+
|
10 |
+
// Insert contact request
|
11 |
+
async function insertContactRequest(name, email, phone, subject, message) {
|
12 |
+
const insertQuery = `INSERT INTO contact_requests (date, time, name, email, phone, subject, message) VALUES (DATE("now"), TIME("now"), ?, ?, ?, ?, ?);`;
|
13 |
+
await runQuery(insertQuery, [name, email, phone, subject, message]);
|
14 |
+
}
|
15 |
+
|
16 |
+
module.exports = { checkExistingContactRequest, insertContactRequest };
|
backend/utils/createEvent.js
ADDED
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { checkAndRefreshAccessToken } = require('./refreshToken');
|
2 |
+
const { google } = require('googleapis');
|
3 |
+
const { oAuth2Client } = require('../config/googleOAuth');
|
4 |
+
const { DateTime } = require('luxon');
|
5 |
+
const ics = require('ics');
|
6 |
+
const { v4: uuid } = require('uuid');
|
7 |
+
const fs = require('fs');
|
8 |
+
const path = require('path');
|
9 |
+
|
10 |
+
async function convertToUTC(date, time, timezone) {
|
11 |
+
// Combine date and time into one string (e.g., '2025-01-31T14:30')
|
12 |
+
const dateTimeString = `${DateTime.fromISO(date).toFormat('yyyy-MM-dd')}T${time}`;
|
13 |
+
console.log(dateTimeString);
|
14 |
+
|
15 |
+
// Parse the combined date-time string in the given timezone
|
16 |
+
const dateTimeInTimezone = DateTime.fromISO(dateTimeString, { zone: timezone });
|
17 |
+
console.log(dateTimeInTimezone);
|
18 |
+
|
19 |
+
// Convert it to UTC
|
20 |
+
const dateTimeInUTC = dateTimeInTimezone.toUTC();
|
21 |
+
console.log(dateTimeInUTC);
|
22 |
+
|
23 |
+
// Return the UTC time as a string (ISO format)
|
24 |
+
return dateTimeInUTC.toISO(); // Returns the date-time in ISO 8601 format (e.g., '2025-01-31T08:00:00.000Z')
|
25 |
+
}
|
26 |
+
|
27 |
+
async function isoToArray(isoString) {
|
28 |
+
const dt = DateTime.fromISO(isoString, { zone: 'utc' }); // Ensure it's parsed as UTC
|
29 |
+
return [dt.year, dt.month, dt.day, dt.hour, dt.minute];
|
30 |
+
}
|
31 |
+
|
32 |
+
|
33 |
+
// Create Google Calendar Event
|
34 |
+
async function createGoogleCalendarEvent(name, email, demoDate, selectedSlot, product, timezone) {
|
35 |
+
|
36 |
+
const [startTime, endTime] = selectedSlot.split(' (')[0].split(' - ');
|
37 |
+
|
38 |
+
const startDatetime = await convertToUTC(demoDate, startTime, timezone);
|
39 |
+
const endDatetime = await convertToUTC(demoDate, endTime, timezone);
|
40 |
+
|
41 |
+
console.log(`Times: ${startDatetime}, ${endDatetime}`);
|
42 |
+
|
43 |
+
// Refresh access token if needed
|
44 |
+
await checkAndRefreshAccessToken();
|
45 |
+
|
46 |
+
const calendar = google.calendar({ version: 'v3', auth: oAuth2Client });
|
47 |
+
|
48 |
+
const event = {
|
49 |
+
summary: `Demo for ${product}`,
|
50 |
+
description: `Demo for ${product}`,
|
51 |
+
start: {
|
52 |
+
dateTime: startDatetime, // Use the demo date here
|
53 |
+
timeZone: 'UTC',
|
54 |
+
},
|
55 |
+
end: {
|
56 |
+
dateTime: endDatetime, // Assuming the demo lasts 30 minutes
|
57 |
+
timeZone: 'UTC',
|
58 |
+
},
|
59 |
+
colorId: 1,
|
60 |
+
conferenceData: {
|
61 |
+
createRequest: {
|
62 |
+
requestId: uuid(),
|
63 |
+
}
|
64 |
+
},
|
65 |
+
attendees: [
|
66 |
+
{email: email},
|
67 |
+
]
|
68 |
+
};
|
69 |
+
|
70 |
+
try {
|
71 |
+
const response = await calendar.events.insert({
|
72 |
+
calendarId: 'primary',
|
73 |
+
conferenceDataVersion: 1,
|
74 |
+
resource: event,
|
75 |
+
sendUpdates: 'all'
|
76 |
+
});
|
77 |
+
console.log('Google Calendar Event Created:', response.data);
|
78 |
+
|
79 |
+
const dateArray = await isoToArray(startDatetime);
|
80 |
+
const icsevent = {
|
81 |
+
start: dateArray,
|
82 |
+
startInputType: 'utc',
|
83 |
+
duration: { minutes: 30 },
|
84 |
+
title: `Demo for ${product}`,
|
85 |
+
description: `Product demonstration for ${product}`,
|
86 |
+
url: response.data.hangoutLink,
|
87 |
+
categories: ['Product Demo'],
|
88 |
+
status: 'CONFIRMED',
|
89 |
+
busyStatus: 'BUSY',
|
90 |
+
attendees: [
|
91 |
+
{ name: name, email: email, rsvp: true, partstat: 'ACCEPTED', role: 'REQ-PARTICIPANT' },
|
92 |
+
]
|
93 |
+
}
|
94 |
+
|
95 |
+
const icsFilePath = path.join(__dirname, '../demo-event.ics');
|
96 |
+
const icsContent = await new Promise((resolve, reject) => {
|
97 |
+
ics.createEvent(icsevent, (error, value) => {
|
98 |
+
if (error) {
|
99 |
+
reject(error);
|
100 |
+
} else {
|
101 |
+
resolve(value); // Return .ics content as string
|
102 |
+
}
|
103 |
+
});
|
104 |
+
});
|
105 |
+
return [response.data, icsContent];
|
106 |
+
} catch (error) {
|
107 |
+
console.error('Error creating Google Calendar event:', error);
|
108 |
+
throw error;
|
109 |
+
}
|
110 |
+
}
|
111 |
+
|
112 |
+
module.exports = {createGoogleCalendarEvent}
|
backend/utils/demoRequestDB.js
ADDED
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const {runSelectQuery, runQuery} = require('./queries');
|
2 |
+
|
3 |
+
async function checkExistingDemoRequest(name, email, product, demoDate, phone) {
|
4 |
+
const checkQuery = `
|
5 |
+
SELECT * FROM demo_requests
|
6 |
+
WHERE name = ? AND email = ? AND product = ?
|
7 |
+
AND ABS(julianday(demo_date) - julianday(?)) <= 15 AND phone = ?;
|
8 |
+
`;
|
9 |
+
const results = await runSelectQuery(checkQuery, [name, email, product, demoDate, phone]);
|
10 |
+
return results.length > 0;
|
11 |
+
}
|
12 |
+
|
13 |
+
// Insert demo request
|
14 |
+
async function insertDemoRequest(name, email, company, product, demoDate, slot, phone, message) {
|
15 |
+
const insertQuery = `
|
16 |
+
INSERT INTO demo_requests (date, time, name, email, company, product, demo_date, slot, phone, comments)
|
17 |
+
VALUES (DATE("now"), TIME("now"), ?, ?, ?, ?, ?, ?, ?, ?);
|
18 |
+
`;
|
19 |
+
await runQuery(insertQuery, [name, email, company, product, demoDate, slot, phone, message]);
|
20 |
+
}
|
21 |
+
|
22 |
+
module.exports = { checkExistingDemoRequest, insertDemoRequest };
|
backend/utils/jobRequestDB.js
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const {runQuery, runSelectQuery} = require('./queries');
|
2 |
+
|
3 |
+
// Check existing job application
|
4 |
+
async function checkExistingJobApplication(name, email, role) {
|
5 |
+
const checkQuery = `SELECT * FROM applicants WHERE name = ? AND email = ? AND role = ?;`;
|
6 |
+
const results = await runSelectQuery(checkQuery, [name, email, role]);
|
7 |
+
console.log(results);
|
8 |
+
return results.length > 0;
|
9 |
+
}
|
10 |
+
|
11 |
+
// Insert job application
|
12 |
+
async function insertJobApplication(name, email, phone, experience, role, linkedin, resume, filename) {
|
13 |
+
const insertQuery = `INSERT INTO applicants (date, time, name, email, phone, experience, role, linkedin, resume, filename) VALUES (DATE("now"), TIME("now"), ?, ?, ?, ?, ?, ?, ?, ?);`;
|
14 |
+
await runQuery(insertQuery, [name, email, phone, experience, role, linkedin, resume, filename]);
|
15 |
+
}
|
16 |
+
|
17 |
+
module.exports = { checkExistingJobApplication, insertJobApplication };
|
backend/utils/queries.js
ADDED
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
//General
|
2 |
+
const path = require('path');
|
3 |
+
|
4 |
+
//SQLite
|
5 |
+
const sqlite3 = require('sqlite3').verbose();
|
6 |
+
|
7 |
+
const fs = require('fs');
|
8 |
+
|
9 |
+
// Database connection
|
10 |
+
console.log(__dirname);
|
11 |
+
|
12 |
+
const dbFilePath = path.resolve(__dirname,'../database/data.db');
|
13 |
+
console.log('The dbpath is', dbFilePath);
|
14 |
+
|
15 |
+
const db = new sqlite3.Database(dbFilePath, (err) => {
|
16 |
+
if (err) {
|
17 |
+
console.error('Error connecting to SQLite database:', err.message);
|
18 |
+
console.log("Current working directory:", process.cwd());
|
19 |
+
} else {
|
20 |
+
console.log('Connected to SQLite database');
|
21 |
+
}
|
22 |
+
});
|
23 |
+
|
24 |
+
// Helper function to promisify db.run for INSERT, UPDATE, CREATE, etc.
|
25 |
+
function runQuery(query, params = []) {
|
26 |
+
return new Promise((resolve, reject) => {
|
27 |
+
db.run(query, params, function (err) {
|
28 |
+
if (err) {
|
29 |
+
reject(err);
|
30 |
+
} else {
|
31 |
+
resolve(this); // `this` contains metadata like `lastID` or `changes`
|
32 |
+
}
|
33 |
+
});
|
34 |
+
});
|
35 |
+
}
|
36 |
+
|
37 |
+
// Helper function to promisify db.all for SELECT queries
|
38 |
+
function runSelectQuery(query, params = []) {
|
39 |
+
return new Promise((resolve, reject) => {
|
40 |
+
db.all(query, params, (err, rows) => {
|
41 |
+
if (err) {
|
42 |
+
reject(err);
|
43 |
+
} else {
|
44 |
+
resolve(rows);
|
45 |
+
}
|
46 |
+
});
|
47 |
+
});
|
48 |
+
}
|
49 |
+
|
50 |
+
module.exports = {runQuery, runSelectQuery}
|
backend/utils/refreshToken.js
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// utils/refreshToken.js
|
2 |
+
const { oAuth2Client } = require('../config/googleOAuth'); // Import the OAuth client
|
3 |
+
|
4 |
+
// Store the user's tokens
|
5 |
+
let userTokens = {};
|
6 |
+
|
7 |
+
// Method to check if the access token is expired and refresh it
|
8 |
+
async function checkAndRefreshAccessToken() {
|
9 |
+
userTokens = {}; // Reset tokens at start
|
10 |
+
if (process.env.ACCESS_TOKEN && process.env.REFRESH_TOKEN) {
|
11 |
+
userTokens = {
|
12 |
+
access_token: process.env.ACCESS_TOKEN,
|
13 |
+
refresh_token: process.env.REFRESH_TOKEN,
|
14 |
+
};
|
15 |
+
oAuth2Client.setCredentials(userTokens);
|
16 |
+
console.log('Using tokens from .env file');
|
17 |
+
} else {
|
18 |
+
console.log('No tokens found in .env file. Please authenticate.');
|
19 |
+
}
|
20 |
+
|
21 |
+
// console.log(oAuth2Client)
|
22 |
+
if (oAuth2Client.isTokenExpiring()) {
|
23 |
+
try {
|
24 |
+
// Refresh the token if it's expired or about to expire
|
25 |
+
const response = await oAuth2Client.refreshAccessToken();
|
26 |
+
const newTokens = response.credentials;
|
27 |
+
oAuth2Client.setCredentials(newTokens); // Set new credentials
|
28 |
+
userTokens = newTokens; // Update tokens
|
29 |
+
|
30 |
+
process.env.ACCESS_TOKEN = newTokens.access_token
|
31 |
+
process.env.REFRESH_TOKEN = newTokens.refresh_token
|
32 |
+
|
33 |
+
console.log('Access token refreshed.');
|
34 |
+
} catch (error) {
|
35 |
+
console.error('Error refreshing access token:', error);
|
36 |
+
}
|
37 |
+
}
|
38 |
+
return userTokens
|
39 |
+
}
|
40 |
+
|
41 |
+
module.exports = { checkAndRefreshAccessToken };
|
backend/utils/sendEmail.js
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const { google } = require('googleapis');
|
2 |
+
const { oAuth2Client } = require('../config/googleOAuth');
|
3 |
+
|
4 |
+
const sendEmail = async (recipient, subject, htmlContent, attachment = null, attachmentName = '') => {
|
5 |
+
try {
|
6 |
+
const gmail = google.gmail({ version: 'v1', auth: oAuth2Client });
|
7 |
+
|
8 |
+
// Unique boundary string
|
9 |
+
const boundary = "----=_NextPart_001";
|
10 |
+
let rawEmail = [
|
11 |
+
`To: ${recipient}`,
|
12 |
+
`Subject: ${subject}`,
|
13 |
+
`MIME-Version: 1.0`,
|
14 |
+
];
|
15 |
+
|
16 |
+
if (attachment && attachment.buffer) {
|
17 |
+
const encodedAttachment = Buffer.isBuffer(attachment.buffer)
|
18 |
+
? attachment.buffer.toString('base64')
|
19 |
+
: Buffer.from(attachment.buffer).toString('base64');
|
20 |
+
|
21 |
+
const formattedAttachment = encodedAttachment.match(/.{1,76}/g).join("\n");
|
22 |
+
|
23 |
+
rawEmail.push(
|
24 |
+
`Content-Type: multipart/mixed; boundary="${boundary}"`,
|
25 |
+
"",
|
26 |
+
`--${boundary}`,
|
27 |
+
"Content-Type: text/html; charset=UTF-8",
|
28 |
+
"",
|
29 |
+
htmlContent,
|
30 |
+
"",
|
31 |
+
`--${boundary}`,
|
32 |
+
`Content-Type: ${attachment.mimetype}; name="${attachmentName}"`,
|
33 |
+
`Content-Disposition: attachment; filename="${attachmentName}"`,
|
34 |
+
"Content-Transfer-Encoding: base64",
|
35 |
+
"",
|
36 |
+
formattedAttachment,
|
37 |
+
"",
|
38 |
+
`--${boundary}--`
|
39 |
+
);
|
40 |
+
} else {
|
41 |
+
rawEmail.push("Content-Type: text/html; charset=UTF-8", "", htmlContent);
|
42 |
+
}
|
43 |
+
|
44 |
+
const encodedEmail = Buffer.from(rawEmail.join("\r\n"))
|
45 |
+
.toString("base64")
|
46 |
+
.replace(/\+/g, "-")
|
47 |
+
.replace(/\//g, "_")
|
48 |
+
.replace(/=+$/, ""); // Remove padding `=` characters
|
49 |
+
|
50 |
+
await gmail.users.messages.send({
|
51 |
+
userId: "me",
|
52 |
+
requestBody: { raw: encodedEmail },
|
53 |
+
});
|
54 |
+
|
55 |
+
console.log(`✅ Email sent successfully to ${recipient}`);
|
56 |
+
} catch (error) {
|
57 |
+
console.log(`❌ Failed to send email to ${recipient}:`, error);
|
58 |
+
throw error;
|
59 |
+
}
|
60 |
+
};
|
61 |
+
|
62 |
+
module.exports = { sendEmail };
|
backend/utils/setupDB.js
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
//SQLite
|
2 |
+
const sqlite3 = require('sqlite3').verbose();
|
3 |
+
const {runQuery, runSelectQuery} = require("./queries");
|
4 |
+
|
5 |
+
// Initialize the database by creating tables if they don't exist
|
6 |
+
async function initializeDatabase() {
|
7 |
+
try {
|
8 |
+
// Define the queries for table creation
|
9 |
+
const tables = {
|
10 |
+
subscribers: `CREATE TABLE IF NOT EXISTS subscribers (
|
11 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
12 |
+
date TEXT,
|
13 |
+
time TEXT,
|
14 |
+
email TEXT NOT NULL UNIQUE
|
15 |
+
)`,
|
16 |
+
applicants: `CREATE TABLE IF NOT EXISTS applicants (
|
17 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
18 |
+
date TEXT,
|
19 |
+
time TEXT,
|
20 |
+
name TEXT NOT NULL,
|
21 |
+
email TEXT NOT NULL,
|
22 |
+
phone TEXT,
|
23 |
+
role TEXT,
|
24 |
+
experience TEXT,
|
25 |
+
linkedin TEXT,
|
26 |
+
resume BLOB,
|
27 |
+
filename TEXT NOT NULL
|
28 |
+
)`,
|
29 |
+
demo_requests: `CREATE TABLE IF NOT EXISTS demo_requests (
|
30 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
31 |
+
date TEXT,
|
32 |
+
time TEXT,
|
33 |
+
name TEXT NOT NULL,
|
34 |
+
email TEXT NOT NULL,
|
35 |
+
company TEXT,
|
36 |
+
product TEXT,
|
37 |
+
demo_date TEXT,
|
38 |
+
slot TEXT,
|
39 |
+
phone TEXT,
|
40 |
+
comments TEXT
|
41 |
+
)`,
|
42 |
+
contact_requests: `CREATE TABLE IF NOT EXISTS contact_requests (
|
43 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
44 |
+
date TEXT,
|
45 |
+
time TEXT,
|
46 |
+
name TEXT NOT NULL,
|
47 |
+
email TEXT NOT NULL,
|
48 |
+
phone TEXT,
|
49 |
+
subject TEXT,
|
50 |
+
message TEXT
|
51 |
+
)`,
|
52 |
+
purchases: `CREATE TABLE IF NOT EXISTS purchases (
|
53 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
54 |
+
purchase_date TEXT,
|
55 |
+
time TEXT,
|
56 |
+
name TEXT NOT NULL,
|
57 |
+
email TEXT NOT NULL,
|
58 |
+
phone TEXT,
|
59 |
+
product TEXT,
|
60 |
+
subscription_plan TEXT,
|
61 |
+
subscription_type TEXT
|
62 |
+
)`,
|
63 |
+
jobs: `CREATE TABLE IF NOT EXISTS jobs (
|
64 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
65 |
+
title TEXT NOT NULL,
|
66 |
+
description TEXT NOT NULL,
|
67 |
+
status TEXT CHECK(status IN ('Open', 'Closed')) NOT NULL DEFAULT 'Open',
|
68 |
+
vacancies INTEGER NOT NULL DEFAULT 0,
|
69 |
+
job_type TEXT CHECK(job_type IN ('Full-Time', 'Part-Time', 'Contract')) NOT NULL,
|
70 |
+
location TEXT CHECK(location IN ('On-Site', 'Remote', 'Hybrid')) NOT NULL,
|
71 |
+
date_posted TEXT NOT NULL,
|
72 |
+
deadline TEXT,
|
73 |
+
image_url TEXT
|
74 |
+
)`
|
75 |
+
};
|
76 |
+
|
77 |
+
// Iterate through each table and create it if necessary
|
78 |
+
for (const [table, query] of Object.entries(tables)) {
|
79 |
+
await runQuery(query);
|
80 |
+
console.log(`Table '${table}' is checked/created successfully.`);
|
81 |
+
}
|
82 |
+
} catch (err) {
|
83 |
+
console.error('Database connection error:', err);
|
84 |
+
}
|
85 |
+
}
|
86 |
+
|
87 |
+
module.exports = {initializeDatabase}
|
frontend/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
|
8 |
+
# testing
|
9 |
+
/coverage
|
10 |
+
|
11 |
+
# production
|
12 |
+
/build
|
13 |
+
|
14 |
+
# misc
|
15 |
+
.DS_Store
|
16 |
+
.env.local
|
17 |
+
.env.development.local
|
18 |
+
.env.test.local
|
19 |
+
.env.production.local
|
20 |
+
|
21 |
+
npm-debug.log*
|
22 |
+
yarn-debug.log*
|
23 |
+
yarn-error.log*
|
frontend/README.md
ADDED
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Getting Started with Create React App
|
2 |
+
|
3 |
+
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
4 |
+
|
5 |
+
## Available Scripts
|
6 |
+
|
7 |
+
In the project directory, you can run:
|
8 |
+
|
9 |
+
### `npm start`
|
10 |
+
|
11 |
+
Runs the app in the development mode.\
|
12 |
+
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
13 |
+
|
14 |
+
The page will reload when you make changes.\
|
15 |
+
You may also see any lint errors in the console.
|
16 |
+
|
17 |
+
### `npm test`
|
18 |
+
|
19 |
+
Launches the test runner in the interactive watch mode.\
|
20 |
+
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
21 |
+
|
22 |
+
### `npm run build`
|
23 |
+
|
24 |
+
Builds the app for production to the `build` folder.\
|
25 |
+
It correctly bundles React in production mode and optimizes the build for the best performance.
|
26 |
+
|
27 |
+
The build is minified and the filenames include the hashes.\
|
28 |
+
Your app is ready to be deployed!
|
29 |
+
|
30 |
+
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
31 |
+
|
32 |
+
### `npm run eject`
|
33 |
+
|
34 |
+
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
|
35 |
+
|
36 |
+
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
37 |
+
|
38 |
+
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
|
39 |
+
|
40 |
+
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
|
41 |
+
|
42 |
+
## Learn More
|
43 |
+
|
44 |
+
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
45 |
+
|
46 |
+
To learn React, check out the [React documentation](https://reactjs.org/).
|
47 |
+
|
48 |
+
### Code Splitting
|
49 |
+
|
50 |
+
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
|
51 |
+
|
52 |
+
### Analyzing the Bundle Size
|
53 |
+
|
54 |
+
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
|
55 |
+
|
56 |
+
### Making a Progressive Web App
|
57 |
+
|
58 |
+
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
|
59 |
+
|
60 |
+
### Advanced Configuration
|
61 |
+
|
62 |
+
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
|
63 |
+
|
64 |
+
### Deployment
|
65 |
+
|
66 |
+
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
|
67 |
+
|
68 |
+
### `npm run build` fails to minify
|
69 |
+
|
70 |
+
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
|
frontend/package-lock.json
ADDED
The diff for this file is too large to render.
See raw diff
|
|
frontend/package.json
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "frontend",
|
3 |
+
"version": "0.1.0",
|
4 |
+
"private": true,
|
5 |
+
"dependencies": {
|
6 |
+
"@gsap/react": "^2.1.1",
|
7 |
+
"@react-icons/all-files": "^4.1.0",
|
8 |
+
"@testing-library/jest-dom": "^6.6.3",
|
9 |
+
"@testing-library/react": "^16.2.0",
|
10 |
+
"@testing-library/user-event": "^14.6.0",
|
11 |
+
"axios": "^1.7.9",
|
12 |
+
"cra-template": "1.2.0",
|
13 |
+
"eslint-config-react-app": "^7.0.1",
|
14 |
+
"gsap": "^3.12.5",
|
15 |
+
"intl-tel-input": "^25.2.0",
|
16 |
+
"luxon": "^3.5.0",
|
17 |
+
"react": "^18.0.0",
|
18 |
+
"react-datepicker": "^7.6.0",
|
19 |
+
"react-day-picker": "^9.5.0",
|
20 |
+
"react-dom": "^18.3.1",
|
21 |
+
"react-icons": "^5.4.0",
|
22 |
+
"react-router-dom": "^6.28.2",
|
23 |
+
"react-scripts": "5.0.1",
|
24 |
+
"react-typed": "^2.0.12",
|
25 |
+
"styled-components": "^6.1.13",
|
26 |
+
"web-vitals": "^4.2.4"
|
27 |
+
},
|
28 |
+
"scripts": {
|
29 |
+
"start": "react-scripts start",
|
30 |
+
"build": "react-scripts build",
|
31 |
+
"test": "jest",
|
32 |
+
"eject": "react-scripts eject",
|
33 |
+
"test:e2e": "node runE2ETests.js"
|
34 |
+
},
|
35 |
+
"eslintConfig": {
|
36 |
+
"extends": [
|
37 |
+
"react-app",
|
38 |
+
"react-app/jest"
|
39 |
+
]
|
40 |
+
},
|
41 |
+
"browserslist": {
|
42 |
+
"production": [
|
43 |
+
">0.2%",
|
44 |
+
"not dead",
|
45 |
+
"not op_mini all"
|
46 |
+
],
|
47 |
+
"development": [
|
48 |
+
"last 1 chrome version",
|
49 |
+
"last 1 firefox version",
|
50 |
+
"last 1 safari version"
|
51 |
+
]
|
52 |
+
},
|
53 |
+
"devDependencies": {
|
54 |
+
"@babel/core": "^7.26.0",
|
55 |
+
"@babel/preset-env": "^7.26.0",
|
56 |
+
"@babel/preset-react": "^7.26.3",
|
57 |
+
"allure-commandline": "^2.32.0",
|
58 |
+
"allure-jest": "^3.0.9",
|
59 |
+
"autoprefixer": "^10.4.20",
|
60 |
+
"axios-mock-adapter": "^2.1.0",
|
61 |
+
"babel-jest": "^29.7.0",
|
62 |
+
"chromedriver": "^132.0.2",
|
63 |
+
"identity-obj-proxy": "^3.0.0",
|
64 |
+
"jest": "^29.7.0",
|
65 |
+
"jest-allure": "^0.1.3",
|
66 |
+
"jest-environment-jsdom": "^29.7.0",
|
67 |
+
"jest-html-reporter": "^3.10.2",
|
68 |
+
"jest-stare": "^2.5.2",
|
69 |
+
"jest-styled-components": "^7.2.0",
|
70 |
+
"postcss": "^8.4.49",
|
71 |
+
"selenium-webdriver": "^4.28.1",
|
72 |
+
"tailwindcss": "^3.4.17"
|
73 |
+
},
|
74 |
+
"proxy": "http://localhost:7860",
|
75 |
+
"jest-html-reporter": {
|
76 |
+
"pageTitle": "Your test suite",
|
77 |
+
"outputPath": "test-report.html",
|
78 |
+
"includeFailureMsg": true,
|
79 |
+
"executionMode": "reporter"
|
80 |
+
}
|
81 |
+
}
|
frontend/public/index.html
ADDED
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="utf-8" />
|
5 |
+
<link rel="icon" href="%PUBLIC_URL%/logo.png" />
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
7 |
+
<meta name="theme-color" content="#000000" />
|
8 |
+
<meta
|
9 |
+
name="description"
|
10 |
+
content="Web site created using create-react-app"
|
11 |
+
/>
|
12 |
+
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.png" />
|
13 |
+
<!--
|
14 |
+
manifest.json provides metadata used when your web app is installed on a
|
15 |
+
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
16 |
+
-->
|
17 |
+
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
18 |
+
<!--
|
19 |
+
Notice the use of %PUBLIC_URL% in the tags above.
|
20 |
+
It will be replaced with the URL of the `public` folder during the build.
|
21 |
+
Only files inside the `public` folder can be referenced from the HTML.
|
22 |
+
|
23 |
+
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
24 |
+
work correctly both with client-side routing and a non-root public URL.
|
25 |
+
Learn how to configure a non-root public URL by running `npm run build`.
|
26 |
+
-->
|
27 |
+
<title>Genomatics | Variants Deciphered</title>
|
28 |
+
</head>
|
29 |
+
<body>
|
30 |
+
<noscript>You need to enable JavaScript to run this app.</noscript>
|
31 |
+
<div id="root"></div>
|
32 |
+
<!--
|
33 |
+
This HTML file is a template.
|
34 |
+
If you open it directly in the browser, you will see an empty page.
|
35 |
+
|
36 |
+
You can add webfonts, meta tags, or analytics to this file.
|
37 |
+
The build step will place the bundled scripts into the <body> tag.
|
38 |
+
|
39 |
+
To begin the development, run `npm start` or `yarn start`.
|
40 |
+
To create a production bundle, use `npm run build` or `yarn build`.
|
41 |
+
-->
|
42 |
+
</body>
|
43 |
+
</html>
|
frontend/public/logo.png
ADDED
![]() |
frontend/public/manifest.json
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"short_name": "React App",
|
3 |
+
"name": "Create React App Sample",
|
4 |
+
"icons": [
|
5 |
+
],
|
6 |
+
"start_url": ".",
|
7 |
+
"display": "standalone",
|
8 |
+
"theme_color": "#000000",
|
9 |
+
"background_color": "#ffffff"
|
10 |
+
}
|
frontend/public/robots.txt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
# https://www.robotstxt.org/robotstxt.html
|
2 |
+
User-agent: *
|
3 |
+
Disallow:
|
frontend/src/App.css
ADDED
File without changes
|
frontend/src/App.js
ADDED
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from "react";
|
2 |
+
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
3 |
+
import Header from "./sections/Header";
|
4 |
+
import Footer from "./sections/Footer";
|
5 |
+
import Home from "./pages/Home";
|
6 |
+
import About from "./pages/About";
|
7 |
+
import Blogs from "./pages/Blogs";
|
8 |
+
import BlogPage from "./pages/Blogpage"; // Import the BlogPage component
|
9 |
+
import Jobs from "./pages/Jobs";
|
10 |
+
import JobPage from "./pages/Jobpage";
|
11 |
+
import VarDiG from "./pages/VarDiG";
|
12 |
+
import Contact from "./pages/Contact";
|
13 |
+
import PrivacyPolicy from "./pages/Privacy";
|
14 |
+
|
15 |
+
import ScrollTop from "./components/ScrollTop";
|
16 |
+
import TransitionWrapper from "./components/Transition";
|
17 |
+
|
18 |
+
|
19 |
+
function App() {
|
20 |
+
return (
|
21 |
+
<Router>
|
22 |
+
<ScrollTop />
|
23 |
+
<TransitionWrapper>
|
24 |
+
<div className="flex flex-col min-h-screen">
|
25 |
+
<Header />
|
26 |
+
|
27 |
+
{/* Main content area */}
|
28 |
+
<main className="flex-grow">
|
29 |
+
<Routes>
|
30 |
+
<Route path="/" element={<Home />} />
|
31 |
+
<Route path="/vardig" element={<VarDiG />} />
|
32 |
+
<Route path="/about" element={<About />} />
|
33 |
+
<Route path="/blogs" element={<Blogs />} />
|
34 |
+
|
35 |
+
{/* Add Dynamic Route for Individual Blog Pages */}
|
36 |
+
<Route path="/blog/:slug" element={<BlogPage/>} />
|
37 |
+
|
38 |
+
<Route path="/jobs" element={<Jobs />} />
|
39 |
+
<Route path="/job/:slug" element={<JobPage/>} />
|
40 |
+
|
41 |
+
<Route path="/contact" element={<Contact />} />
|
42 |
+
<Route path="/privacy-policy" element={<PrivacyPolicy />} />
|
43 |
+
</Routes>
|
44 |
+
</main>
|
45 |
+
|
46 |
+
{/* Footer will stay at the bottom */}
|
47 |
+
<Footer />
|
48 |
+
</div>
|
49 |
+
</TransitionWrapper>
|
50 |
+
</Router>
|
51 |
+
);
|
52 |
+
}
|
53 |
+
|
54 |
+
export default App;
|
frontend/src/assets/images/blog-image-1.jpg
ADDED
![]() |
frontend/src/assets/images/blog-image-2.jpg
ADDED
![]() |
frontend/src/assets/images/blog-image-3.jpg
ADDED
![]() |
frontend/src/assets/images/dr_giridharan.jpeg
ADDED
![]() |
frontend/src/assets/images/dr_s_rajasekaran.jpeg
ADDED
![]() |
frontend/src/assets/images/dr_thangaraj.jpeg
ADDED
![]() |