Commit
·
62c3fe0
1
Parent(s):
f53201f
initial update of the vite site
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitignore +28 -0
- .vite/deps/_metadata.json +8 -0
- .vite/deps/package.json +3 -0
- Dockerfile +40 -0
- README.md +4 -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 +79 -0
- backend/package-lock.json +0 -0
- backend/package.json +50 -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/server.log +14 -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 +24 -0
- frontend/README.md +12 -0
- frontend/eslint.config.js +33 -0
- frontend/index.html +13 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +56 -0
- frontend/postcss.config.js +6 -0
- frontend/public/logo.png +0 -0
- frontend/src/App copy.css +0 -0
- frontend/src/App.css +42 -0
- frontend/src/App.jsx +54 -0
- frontend/src/assets/images/blog-image-1.jpg +0 -0
- frontend/src/assets/images/blog-image-2.jpg +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
|
.vite/deps/_metadata.json
ADDED
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"hash": "052dd0dd",
|
3 |
+
"configHash": "0fd7b3b7",
|
4 |
+
"lockfileHash": "e3b0c442",
|
5 |
+
"browserHash": "bb296c15",
|
6 |
+
"optimized": {},
|
7 |
+
"chunks": {}
|
8 |
+
}
|
.vite/deps/package.json
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"type": "module"
|
3 |
+
}
|
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,8 +1,8 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
---
|
|
|
1 |
---
|
2 |
+
title: Geno React Modularised
|
3 |
+
emoji: 🏃
|
4 |
+
colorFrom: pink
|
5 |
+
colorTo: red
|
6 |
sdk: docker
|
7 |
pinned: false
|
8 |
---
|
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,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
//logger
|
8 |
+
const morgan = require('morgan');
|
9 |
+
|
10 |
+
//Initialize Database
|
11 |
+
const {initializeDatabase} = require("./utils/setupDB");
|
12 |
+
initializeDatabase();
|
13 |
+
|
14 |
+
//Prefix Flag (Use or not)
|
15 |
+
const {useApiPrefix} = require('./config')
|
16 |
+
|
17 |
+
const fs = require('fs');
|
18 |
+
|
19 |
+
const path1 = '/app/backend/database';
|
20 |
+
|
21 |
+
if (fs.existsSync(path1)) {
|
22 |
+
console.log('The path exists');
|
23 |
+
} else {
|
24 |
+
console.log('The path does not exist');
|
25 |
+
}
|
26 |
+
|
27 |
+
//Refresh the holidays
|
28 |
+
const {refreshHolidays} = require("./controller/holidays");
|
29 |
+
refreshHolidays();
|
30 |
+
|
31 |
+
//Route Imports
|
32 |
+
const authRoutes = require('./routes/authRoutes'); // Import the auth routes
|
33 |
+
const availabilityRoutes = require('./routes/availabilityRoutes'); // Import availability routes
|
34 |
+
const subscriptionRoutes = require('./routes/subscriptionRoutes'); // Import subscription routes
|
35 |
+
const demoRequestRoutes = require('./routes/demoRequestRoutes'); // Import demo request routes
|
36 |
+
const contactRoutes = require('./routes/contactRoutes'); // Import contact routes
|
37 |
+
const applicantRoutes = require('./routes/applicantRoutes'); // Import applicant routes
|
38 |
+
|
39 |
+
const app = express();
|
40 |
+
|
41 |
+
app.use(express.json()); // To handle JSON request body
|
42 |
+
|
43 |
+
// Serve static files from the React build folder
|
44 |
+
app.use(express.static(path.join(__dirname, 'build')));
|
45 |
+
|
46 |
+
// Catch-all route to serve React's index.html for unknown routes
|
47 |
+
app.get('*', (req, res) => {
|
48 |
+
res.sendFile(path.join(__dirname, 'build', 'index.html'));
|
49 |
+
});
|
50 |
+
|
51 |
+
// Middleware to parse JSON body
|
52 |
+
app.use(bodyParser.json());
|
53 |
+
|
54 |
+
//Use CORS
|
55 |
+
app.use(cors());
|
56 |
+
|
57 |
+
//Log everything to server.log
|
58 |
+
const logStream = fs.createWriteStream(path.join(__dirname, 'server.log'), { flags: 'a' });
|
59 |
+
app.use(morgan('combined', { stream: logStream }));
|
60 |
+
|
61 |
+
// Routes - Dynamically add /api prefix if the flag is true
|
62 |
+
if (useApiPrefix) {
|
63 |
+
app.use('/api', authRoutes); // /api/auth
|
64 |
+
app.use('/api', subscriptionRoutes); // /api/subscribe
|
65 |
+
app.use('/api', availabilityRoutes); // /api/availability
|
66 |
+
app.use('/api', demoRequestRoutes); // /api/demo-request
|
67 |
+
app.use('/api', contactRoutes); // /api/contact
|
68 |
+
app.use('/api', applicantRoutes); // /api/applicant
|
69 |
+
} else {
|
70 |
+
app.use(authRoutes); // No prefix
|
71 |
+
app.use(subscriptionRoutes); // No prefix
|
72 |
+
app.use(availabilityRoutes); // No prefix
|
73 |
+
app.use(demoRequestRoutes); // No prefix
|
74 |
+
app.use(contactRoutes); // No prefix
|
75 |
+
app.use(applicantRoutes); // No prefix
|
76 |
+
}
|
77 |
+
|
78 |
+
const PORT = process.env.PORT || 7860;
|
79 |
+
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,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
"morgan": "^1.10.0",
|
26 |
+
"multer": "^1.4.5-lts.1",
|
27 |
+
"mysql2": "^3.11.4",
|
28 |
+
"node-fetch": "^2.7.0",
|
29 |
+
"sqlite3": "^5.1.7",
|
30 |
+
"uuid": "^11.0.5"
|
31 |
+
},
|
32 |
+
"devDependencies": {
|
33 |
+
"allure-commandline": "^2.32.0",
|
34 |
+
"allure-jest": "^3.0.9",
|
35 |
+
"axios-mock-adapter": "^2.1.0",
|
36 |
+
"babel-jest": "^29.7.0",
|
37 |
+
"identity-obj-proxy": "^3.0.0",
|
38 |
+
"jest": "^29.7.0",
|
39 |
+
"jest-allure": "^0.1.3",
|
40 |
+
"jest-environment-jsdom": "^29.7.0",
|
41 |
+
"jest-html-reporter": "^3.10.2",
|
42 |
+
"supertest": "^7.0.0"
|
43 |
+
},
|
44 |
+
"jest-html-reporter": {
|
45 |
+
"pageTitle": "Backend Unit testing",
|
46 |
+
"outputPath": "test-report.html",
|
47 |
+
"includeFailureMsg": true,
|
48 |
+
"executionMode": "reporter"
|
49 |
+
}
|
50 |
+
}
|
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/server.log
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
127.0.0.1 - - [01/Mar/2025:05:31:37 +0000] "POST /api/get-available-dates-and-slots HTTP/1.1" 200 5323 "http://localhost:3000/contact" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
2 |
+
127.0.0.1 - - [01/Mar/2025:05:33:47 +0000] "POST /api/get-available-dates-and-slots HTTP/1.1" 200 5291 "http://localhost:3000/contact" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
3 |
+
127.0.0.1 - - [01/Mar/2025:05:34:14 +0000] "POST /api/demo-request HTTP/1.1" 400 117 "http://localhost:3000/contact" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
4 |
+
127.0.0.1 - - [01/Mar/2025:05:38:18 +0000] "POST /api/demo-request HTTP/1.1" 200 92 "http://localhost:3000/contact" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
5 |
+
127.0.0.1 - - [01/Mar/2025:05:38:18 +0000] "POST /api/get-available-dates-and-slots HTTP/1.1" 200 5259 "http://localhost:3000/contact" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
6 |
+
127.0.0.1 - - [01/Mar/2025:08:04:11 +0000] "POST /api/get-available-dates-and-slots HTTP/1.1" 200 5259 "http://localhost:3000/contact" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
7 |
+
127.0.0.1 - - [01/Mar/2025:08:15:45 +0000] "POST /get-available-dates-and-slots HTTP/1.1" 404 169 "http://localhost:5173/contact" "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
|
8 |
+
127.0.0.1 - - [01/Mar/2025:08:15:50 +0000] "POST /get-available-dates-and-slots HTTP/1.1" 404 169 "http://localhost:5173/contact" "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
|
9 |
+
127.0.0.1 - - [01/Mar/2025:08:16:53 +0000] "POST /get-available-dates-and-slots HTTP/1.1" 404 169 "http://localhost:5173/contact" "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
|
10 |
+
127.0.0.1 - - [01/Mar/2025:08:18:33 +0000] "POST /api/get-available-dates-and-slots HTTP/1.1" 200 5259 "http://localhost:5173/contact" "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
|
11 |
+
127.0.0.1 - - [01/Mar/2025:08:51:23 +0000] "POST /api/demo-request HTTP/1.1" 200 118 "http://localhost:5173/contact" "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
|
12 |
+
127.0.0.1 - - [01/Mar/2025:08:51:23 +0000] "POST /api/get-available-dates-and-slots HTTP/1.1" 200 5227 "http://localhost:5173/contact" "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
|
13 |
+
127.0.0.1 - - [01/Mar/2025:09:05:33 +0000] "POST /api/demo-request HTTP/1.1" 200 92 "http://localhost:5173/contact" "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
|
14 |
+
127.0.0.1 - - [01/Mar/2025:09:05:33 +0000] "POST /api/get-available-dates-and-slots HTTP/1.1" 200 5195 "http://localhost:5173/contact" "Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1"
|
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,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Logs
|
2 |
+
logs
|
3 |
+
*.log
|
4 |
+
npm-debug.log*
|
5 |
+
yarn-debug.log*
|
6 |
+
yarn-error.log*
|
7 |
+
pnpm-debug.log*
|
8 |
+
lerna-debug.log*
|
9 |
+
|
10 |
+
node_modules
|
11 |
+
dist
|
12 |
+
dist-ssr
|
13 |
+
*.local
|
14 |
+
|
15 |
+
# Editor directories and files
|
16 |
+
.vscode/*
|
17 |
+
!.vscode/extensions.json
|
18 |
+
.idea
|
19 |
+
.DS_Store
|
20 |
+
*.suo
|
21 |
+
*.ntvs*
|
22 |
+
*.njsproj
|
23 |
+
*.sln
|
24 |
+
*.sw?
|
frontend/README.md
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# React + Vite
|
2 |
+
|
3 |
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
4 |
+
|
5 |
+
Currently, two official plugins are available:
|
6 |
+
|
7 |
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
8 |
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
9 |
+
|
10 |
+
## Expanding the ESLint configuration
|
11 |
+
|
12 |
+
If you are developing a production application, we recommend using TypeScript and enable type-aware lint rules. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
frontend/eslint.config.js
ADDED
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import js from '@eslint/js'
|
2 |
+
import globals from 'globals'
|
3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
5 |
+
|
6 |
+
export default [
|
7 |
+
{ ignores: ['dist'] },
|
8 |
+
{
|
9 |
+
files: ['**/*.{js,jsx}'],
|
10 |
+
languageOptions: {
|
11 |
+
ecmaVersion: 2020,
|
12 |
+
globals: globals.browser,
|
13 |
+
parserOptions: {
|
14 |
+
ecmaVersion: 'latest',
|
15 |
+
ecmaFeatures: { jsx: true },
|
16 |
+
sourceType: 'module',
|
17 |
+
},
|
18 |
+
},
|
19 |
+
plugins: {
|
20 |
+
'react-hooks': reactHooks,
|
21 |
+
'react-refresh': reactRefresh,
|
22 |
+
},
|
23 |
+
rules: {
|
24 |
+
...js.configs.recommended.rules,
|
25 |
+
...reactHooks.configs.recommended.rules,
|
26 |
+
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
27 |
+
'react-refresh/only-export-components': [
|
28 |
+
'warn',
|
29 |
+
{ allowConstantExport: true },
|
30 |
+
],
|
31 |
+
},
|
32 |
+
},
|
33 |
+
]
|
frontend/index.html
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!doctype html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
7 |
+
<title>Genomatics | Variants Deciphered</title>
|
8 |
+
</head>
|
9 |
+
<body>
|
10 |
+
<div id="root"></div>
|
11 |
+
<script type="module" src="/src/main.jsx"></script>
|
12 |
+
</body>
|
13 |
+
</html>
|
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,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "geno-vite",
|
3 |
+
"private": true,
|
4 |
+
"version": "0.0.0",
|
5 |
+
"type": "module",
|
6 |
+
"scripts": {
|
7 |
+
"dev": "vite",
|
8 |
+
"build": "vite build",
|
9 |
+
"lint": "eslint .",
|
10 |
+
"preview": "vite preview"
|
11 |
+
},
|
12 |
+
"dependencies": {
|
13 |
+
"@gsap/react": "^2.1.2",
|
14 |
+
"@react-icons/all-files": "^4.1.0",
|
15 |
+
"axios": "^1.8.1",
|
16 |
+
"gsap": "^3.12.7",
|
17 |
+
"intl-tel-input": "^25.3.0",
|
18 |
+
"luxon": "^3.5.0",
|
19 |
+
"react": "^19.0.0",
|
20 |
+
"react-datepicker": "^8.1.0",
|
21 |
+
"react-day-picker": "^9.5.1",
|
22 |
+
"react-dom": "^19.0.0",
|
23 |
+
"react-icons": "^5.5.0",
|
24 |
+
"react-router-dom": "^6.30.0",
|
25 |
+
"react-typed": "^2.0.12",
|
26 |
+
"styled-components": "^6.1.15"
|
27 |
+
},
|
28 |
+
"devDependencies": {
|
29 |
+
"@babel/core": "^7.26.9",
|
30 |
+
"@babel/preset-env": "^7.26.9",
|
31 |
+
"@babel/preset-react": "^7.26.3",
|
32 |
+
"@eslint/js": "^9.21.0",
|
33 |
+
"@types/react": "^19.0.10",
|
34 |
+
"@types/react-dom": "^19.0.4",
|
35 |
+
"@vitejs/plugin-react": "^4.3.4",
|
36 |
+
"autoprefixer": "^10.4.20",
|
37 |
+
"babel-jest": "^29.7.0",
|
38 |
+
"eslint": "^9.21.0",
|
39 |
+
"eslint-plugin-react-hooks": "^5.1.0",
|
40 |
+
"eslint-plugin-react-refresh": "^0.4.19",
|
41 |
+
"globals": "^15.15.0",
|
42 |
+
"jest": "^29.7.0",
|
43 |
+
"jest-environment-jsdom": "^29.7.0",
|
44 |
+
"postcss": "^8.5.3",
|
45 |
+
"selenium-webdriver": "^4.29.0",
|
46 |
+
"tailwindcss": "^3.4.17",
|
47 |
+
"vite": "^6.2.0"
|
48 |
+
},
|
49 |
+
"proxy": "http://localhost:7860",
|
50 |
+
"jest-html-reporter": {
|
51 |
+
"pageTitle": "Your test suite",
|
52 |
+
"outputPath": "test-report.html",
|
53 |
+
"includeFailureMsg": true,
|
54 |
+
"executionMode": "reporter"
|
55 |
+
}
|
56 |
+
}
|
frontend/postcss.config.js
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export default {
|
2 |
+
plugins: {
|
3 |
+
tailwindcss: {},
|
4 |
+
autoprefixer: {},
|
5 |
+
},
|
6 |
+
}
|
frontend/public/logo.png
ADDED
![]() |
frontend/src/App copy.css
ADDED
File without changes
|
frontend/src/App.css
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#root {
|
2 |
+
max-width: 1280px;
|
3 |
+
margin: 0 auto;
|
4 |
+
padding: 2rem;
|
5 |
+
text-align: center;
|
6 |
+
}
|
7 |
+
|
8 |
+
.logo {
|
9 |
+
height: 6em;
|
10 |
+
padding: 1.5em;
|
11 |
+
will-change: filter;
|
12 |
+
transition: filter 300ms;
|
13 |
+
}
|
14 |
+
.logo:hover {
|
15 |
+
filter: drop-shadow(0 0 2em #646cffaa);
|
16 |
+
}
|
17 |
+
.logo.react:hover {
|
18 |
+
filter: drop-shadow(0 0 2em #61dafbaa);
|
19 |
+
}
|
20 |
+
|
21 |
+
@keyframes logo-spin {
|
22 |
+
from {
|
23 |
+
transform: rotate(0deg);
|
24 |
+
}
|
25 |
+
to {
|
26 |
+
transform: rotate(360deg);
|
27 |
+
}
|
28 |
+
}
|
29 |
+
|
30 |
+
@media (prefers-reduced-motion: no-preference) {
|
31 |
+
a:nth-of-type(2) .logo {
|
32 |
+
animation: logo-spin infinite 20s linear;
|
33 |
+
}
|
34 |
+
}
|
35 |
+
|
36 |
+
.card {
|
37 |
+
padding: 2em;
|
38 |
+
}
|
39 |
+
|
40 |
+
.read-the-docs {
|
41 |
+
color: #888;
|
42 |
+
}
|
frontend/src/App.jsx
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
![]() |