feat: complete day 17
This commit is contained in:
+2
-1
@@ -6,6 +6,7 @@ var logger = require("morgan");
|
|||||||
|
|
||||||
var indexRouter = require("./routes/index");
|
var indexRouter = require("./routes/index");
|
||||||
var usersRouter = require("./routes/users");
|
var usersRouter = require("./routes/users");
|
||||||
|
var apiRouter = require("./routes/api");
|
||||||
|
|
||||||
const db = require("./models");
|
const db = require("./models");
|
||||||
var cors = require("cors");
|
var cors = require("cors");
|
||||||
@@ -24,7 +25,7 @@ app.use(express.static(path.join(__dirname, "public")));
|
|||||||
|
|
||||||
app.use("/", indexRouter);
|
app.use("/", indexRouter);
|
||||||
app.use("/users", usersRouter);
|
app.use("/users", usersRouter);
|
||||||
|
app.use("/api", apiRouter);
|
||||||
// catch 404 and forward to error handler
|
// catch 404 and forward to error handler
|
||||||
app.use(function (req, res, next) {
|
app.use(function (req, res, next) {
|
||||||
next(createError(404));
|
next(createError(404));
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
module.exports = (sequelize, DataTypes) => {
|
||||||
|
const booking = sequelize.define(
|
||||||
|
"booking",
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
email: DataTypes.STRING,
|
||||||
|
company: DataTypes.STRING,
|
||||||
|
phone: DataTypes.STRING,
|
||||||
|
notes: DataTypes.TEXT,
|
||||||
|
date: DataTypes.STRING,
|
||||||
|
time: DataTypes.STRING,
|
||||||
|
timezone: DataTypes.STRING,
|
||||||
|
created_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
freezeTableName: true,
|
||||||
|
tableName: "booking",
|
||||||
|
createdAt: "created_at",
|
||||||
|
updatedAt: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return booking;
|
||||||
|
};
|
||||||
+48
-34
@@ -1,4 +1,4 @@
|
|||||||
'use strict';
|
"use strict";
|
||||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||||
/**
|
/**
|
||||||
* Sequelize File
|
* Sequelize File
|
||||||
@@ -8,49 +8,63 @@
|
|||||||
* @author Ryan Wong
|
* @author Ryan Wong
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
let Sequelize = require('sequelize');
|
let Sequelize = require("sequelize");
|
||||||
const basename = path.basename(__filename);
|
const basename = path.basename(__filename);
|
||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require("sequelize");
|
||||||
const config = {
|
const config = {
|
||||||
DB_DATABASE: 'mysql',
|
DB_DATABASE: "mysql",
|
||||||
DB_USERNAME: 'root',
|
DB_USERNAME: "root",
|
||||||
DB_PASSWORD: 'root',
|
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||||
DB_ADAPTER: 'mysql',
|
DB_ADAPTER: "mysql",
|
||||||
DB_NAME: 'day_1',
|
DB_NAME: "day_17",
|
||||||
DB_HOSTNAME: 'localhost',
|
DB_HOSTNAME: "localhost",
|
||||||
DB_PORT: 3306,
|
DB_PORT: 3306,
|
||||||
};
|
};
|
||||||
|
|
||||||
let db = {};
|
let db = {};
|
||||||
|
|
||||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
let sequelize = new Sequelize(
|
||||||
dialect: config.DB_ADAPTER,
|
config.DB_NAME,
|
||||||
username: config.DB_USERNAME,
|
config.DB_USERNAME,
|
||||||
password: config.DB_PASSWORD,
|
config.DB_PASSWORD,
|
||||||
database: config.DB_NAME,
|
{
|
||||||
host: config.DB_HOSTNAME,
|
dialect: config.DB_ADAPTER,
|
||||||
port: config.DB_PORT,
|
username: config.DB_USERNAME,
|
||||||
logging: console.log,
|
password: config.DB_PASSWORD,
|
||||||
timezone: '-04:00',
|
database: config.DB_NAME,
|
||||||
pool: {
|
host: config.DB_HOSTNAME,
|
||||||
maxConnections: 1,
|
port: config.DB_PORT,
|
||||||
minConnections: 0,
|
logging: console.log,
|
||||||
maxIdleTime: 100,
|
timezone: "-04:00",
|
||||||
},
|
pool: {
|
||||||
define: {
|
maxConnections: 1,
|
||||||
timestamps: false,
|
minConnections: 0,
|
||||||
underscoredAll: true,
|
maxIdleTime: 100,
|
||||||
underscored: true,
|
},
|
||||||
},
|
define: {
|
||||||
});
|
timestamps: false,
|
||||||
|
underscoredAll: true,
|
||||||
|
underscored: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// sequelize.sync({ force: true });
|
sequelize
|
||||||
|
.sync()
|
||||||
|
.then(() => {
|
||||||
|
console.log("Database & tables created!");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
|
||||||
fs.readdirSync(__dirname)
|
fs.readdirSync(__dirname)
|
||||||
.filter((file) => {
|
.filter((file) => {
|
||||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
return (
|
||||||
|
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.forEach((file) => {
|
.forEach((file) => {
|
||||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||||
@@ -66,4 +80,4 @@ Object.keys(db).forEach((modelName) => {
|
|||||||
db.sequelize = sequelize;
|
db.sequelize = sequelize;
|
||||||
db.Sequelize = Sequelize;
|
db.Sequelize = Sequelize;
|
||||||
|
|
||||||
module.exports = db;
|
module.exports = db;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"moment-timezone": "^0.6.0",
|
"moment-timezone": "^0.6.0",
|
||||||
"morgan": "~1.9.1",
|
"morgan": "~1.9.1",
|
||||||
"mysql2": "^2.3.3",
|
"mysql2": "^2.3.3",
|
||||||
|
"node-input-validator": "^4.5.1",
|
||||||
"sequelize": "^6.15.1"
|
"sequelize": "^6.15.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,289 @@
|
|||||||
body {
|
body {
|
||||||
padding: 50px;
|
margin: 0;
|
||||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
padding: 0;
|
||||||
|
font-family: "Inter", "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||||
|
background: #eaeaea;
|
||||||
|
color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
.calendar-container {
|
||||||
color: #00B7FF;
|
max-width: 1100px;
|
||||||
|
margin: 40px auto;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.07);
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header {
|
||||||
|
background: #04316a;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 28px 32px;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-content {
|
||||||
|
padding: 32px 32px 0 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-labels {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.calendar-label-main {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.calendar-label-duration {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.calendar-label-duration span {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.calendar-label-timezone {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.timezone-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #888;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Overlay */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 32px 40px;
|
||||||
|
min-width: 340px;
|
||||||
|
max-width: 90vw;
|
||||||
|
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.15);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.modal-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.modal-format-switch {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
font-size: 0.98rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.timezone-groups {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 32px 48px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.timezone-group {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
.timezone-group-title {
|
||||||
|
font-size: 0.98rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #04316a;
|
||||||
|
}
|
||||||
|
.timezone-option {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 0.97rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Table */
|
||||||
|
.calendar-table-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.calendar-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.calendar-table th,
|
||||||
|
.calendar-table td {
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
.calendar-table th {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #04316a;
|
||||||
|
border-bottom: 2px solid #eaeaea;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
.calendar-day-label {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.calendar-date-label {
|
||||||
|
font-size: 0.98rem;
|
||||||
|
color: #888;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.calendar-slot-btn {
|
||||||
|
background: #f5f8fa;
|
||||||
|
border: 1px solid #dbe6f3;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #04316a;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 7px 0;
|
||||||
|
width: 90%;
|
||||||
|
margin: 0 auto;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border 0.15s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.calendar-slot-btn:hover {
|
||||||
|
background: #e6f0ff;
|
||||||
|
border-color: #04316a;
|
||||||
|
}
|
||||||
|
.calendar-table td {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-week-nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 18px;
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.calendar-week-nav a {
|
||||||
|
color: #04316a;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Booking Form */
|
||||||
|
.booking-form {
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
.form-error {
|
||||||
|
color: red;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
.form-group input,
|
||||||
|
.form-group textarea {
|
||||||
|
border: 1px solid #dbe6f3;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
background: #f5f8fa;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
.form-group textarea {
|
||||||
|
min-height: 70px;
|
||||||
|
}
|
||||||
|
.form-submit-btn {
|
||||||
|
background: #04316a;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 1.08rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin-top: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.form-submit-btn:hover {
|
||||||
|
background: #0050b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success Message */
|
||||||
|
.success-message {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Styles */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.calendar-container {
|
||||||
|
max-width: 98vw;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
.calendar-content {
|
||||||
|
padding: 18px 6vw 0 6vw;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
padding: 18px 8vw;
|
||||||
|
}
|
||||||
|
.calendar-table th,
|
||||||
|
.calendar-table td {
|
||||||
|
min-width: 80px;
|
||||||
|
font-size: 0.98rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.calendar-header {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding: 18px 10px;
|
||||||
|
}
|
||||||
|
.calendar-content {
|
||||||
|
padding: 10px 2vw 0 2vw;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
min-width: 90vw;
|
||||||
|
padding: 10px 2vw;
|
||||||
|
}
|
||||||
|
.calendar-table th,
|
||||||
|
.calendar-table td {
|
||||||
|
min-width: 60px;
|
||||||
|
font-size: 0.93rem;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.booking-form {
|
||||||
|
max-width: 98vw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const db = require("../models");
|
||||||
|
const {
|
||||||
|
validateInput,
|
||||||
|
handleValidationErrorForAPI,
|
||||||
|
} = require("../services/ValidationService");
|
||||||
|
|
||||||
|
// Validation rules for booking
|
||||||
|
const bookingValidation = {
|
||||||
|
name: "required|string",
|
||||||
|
email: "required|email",
|
||||||
|
company: "required|string",
|
||||||
|
phone: "required|string",
|
||||||
|
notes: "required|string",
|
||||||
|
date: "required|string",
|
||||||
|
time: "required|string",
|
||||||
|
timezone: "required|string",
|
||||||
|
};
|
||||||
|
|
||||||
|
// POST /api/bookings - Create a new booking
|
||||||
|
router.post(
|
||||||
|
"/bookings",
|
||||||
|
validateInput(bookingValidation, {
|
||||||
|
"name.required": "Name is required",
|
||||||
|
"email.required": "Email is required",
|
||||||
|
"email.email": "Invalid email address",
|
||||||
|
"company.required": "Company is required",
|
||||||
|
"phone.required": "Phone is required",
|
||||||
|
"notes.required": "Notes are required",
|
||||||
|
"date.required": "Date is required",
|
||||||
|
"time.required": "Time is required",
|
||||||
|
"timezone.required": "Timezone is required",
|
||||||
|
}),
|
||||||
|
handleValidationErrorForAPI,
|
||||||
|
async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name, email, company, phone, notes, date, time, timezone } =
|
||||||
|
req.body;
|
||||||
|
const booking = await db.booking.create({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
company,
|
||||||
|
phone,
|
||||||
|
notes,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
timezone,
|
||||||
|
});
|
||||||
|
res.status(201).json({ success: true, booking });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /api/bookings - List all bookings
|
||||||
|
router.get("/bookings", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const bookings = await db.booking.findAll({
|
||||||
|
order: [["created_at", "DESC"]],
|
||||||
|
});
|
||||||
|
res.json({ bookings });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
+20
-6
@@ -28,8 +28,10 @@ function getTimezoneGroups() {
|
|||||||
const groups = {};
|
const groups = {};
|
||||||
for (const region in regions) {
|
for (const region in regions) {
|
||||||
groups[region] = regions[region].map((tz) => ({
|
groups[region] = regions[region].map((tz) => ({
|
||||||
value: tz,
|
name: tz,
|
||||||
label: moment().tz(tz).zoneAbbr() + " " + moment().tz(tz).format("h:mma"),
|
label: tz.replace(/_/g, " ").replace("America/", ""),
|
||||||
|
time_am: moment().tz(tz).format("h:mma"),
|
||||||
|
time_24: moment().tz(tz).format("HH:mm"),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return groups;
|
return groups;
|
||||||
@@ -44,9 +46,11 @@ function getAllTimezones() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Generate week days and slots
|
// Helper: Generate week days and slots
|
||||||
function getWeekDaysAndSlots(selectedTz) {
|
function getWeekDaysAndSlots(selectedTz, weekOffset = 0) {
|
||||||
const weekDays = [];
|
const weekDays = [];
|
||||||
const today = moment().tz(selectedTz);
|
const today = moment()
|
||||||
|
.tz(selectedTz)
|
||||||
|
.add(weekOffset * 7, "days");
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
const day = today.clone().add(i, "days");
|
const day = today.clone().add(i, "days");
|
||||||
weekDays.push({
|
weekDays.push({
|
||||||
@@ -89,20 +93,30 @@ router.get("/timezone", function (req, res) {
|
|||||||
// Calendar slot selection screen
|
// Calendar slot selection screen
|
||||||
router.get("/calendar", function (req, res) {
|
router.get("/calendar", function (req, res) {
|
||||||
const selectedTimezone = req.query.tz || "America/New_York";
|
const selectedTimezone = req.query.tz || "America/New_York";
|
||||||
const { weekDays, maxSlots } = getWeekDaysAndSlots(selectedTimezone);
|
const weekOffset = parseInt(req.query.week) || 0;
|
||||||
|
const { weekDays, maxSlots } = getWeekDaysAndSlots(
|
||||||
|
selectedTimezone,
|
||||||
|
weekOffset
|
||||||
|
);
|
||||||
res.render("calendar", {
|
res.render("calendar", {
|
||||||
selectedTimezone,
|
selectedTimezone,
|
||||||
weekDays,
|
weekDays,
|
||||||
maxSlots,
|
maxSlots,
|
||||||
showPrevWeek: false, // For demo, only next week link
|
showPrevWeek: weekOffset > 0,
|
||||||
|
prevWeek: weekOffset - 1,
|
||||||
|
nextWeek: weekOffset + 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Booking form screen
|
// Booking form screen
|
||||||
router.get("/book", function (req, res) {
|
router.get("/book", function (req, res) {
|
||||||
const selectedTimezone = req.query.tz || "America/New_York";
|
const selectedTimezone = req.query.tz || "America/New_York";
|
||||||
|
const selectedDate = req.query.date || "";
|
||||||
|
const selectedTime = req.query.time || "";
|
||||||
res.render("booking-form", {
|
res.render("booking-form", {
|
||||||
selectedTimezone,
|
selectedTimezone,
|
||||||
|
selectedDate,
|
||||||
|
selectedTime,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +1,97 @@
|
|||||||
<%- include('partials/header') %>
|
<%- include('partials/header') %>
|
||||||
<div style="padding: 32px; max-width: 700px; margin: 0 auto">
|
<div class="calendar-container">
|
||||||
<h3>Pick a date and time</h3>
|
<div class="calendar-header">Calendar</div>
|
||||||
<p><b>Duration:</b> 1 hour</p>
|
<div class="calendar-content">
|
||||||
<p>Your timezone: <%= selectedTimezone %></p>
|
<div class="calendar-labels">
|
||||||
<form action="/book" method="POST" style="margin-top: 32px">
|
<div class="calendar-label-main">Pick a date and time</div>
|
||||||
<h4>Additional Information</h4>
|
<div class="calendar-label-duration">Duration: <span>1 hour</span></div>
|
||||||
<label>*Full Name</label>
|
<div class="calendar-label-timezone">
|
||||||
<input
|
Your timezone: <%= selectedTimezone %>
|
||||||
type="text"
|
</div>
|
||||||
name="fullName"
|
<% if (selectedDate && selectedTime) { %>
|
||||||
required
|
<div class="calendar-label-selected">
|
||||||
style="width: 100%; margin-bottom: 12px"
|
<strong>Selected:</strong> <%= selectedDate %> at <%= selectedTime %>
|
||||||
/>
|
</div>
|
||||||
<label>*Email</label>
|
<% } %>
|
||||||
<input
|
</div>
|
||||||
type="email"
|
<form class="booking-form" id="bookingForm">
|
||||||
name="email"
|
<input type="hidden" name="date" value="<%= selectedDate %>" />
|
||||||
required
|
<input type="hidden" name="time" value="<%= selectedTime %>" />
|
||||||
style="width: 100%; margin-bottom: 12px"
|
<input type="hidden" name="tz" value="<%= selectedTimezone %>" />
|
||||||
/>
|
<div class="form-group">
|
||||||
<label>*Company</label>
|
<label for="fullName">Full Name</label>
|
||||||
<input
|
<input type="text" id="fullName" name="name" required />
|
||||||
type="text"
|
<div class="form-error" id="error-name"></div>
|
||||||
name="company"
|
</div>
|
||||||
required
|
<div class="form-group">
|
||||||
style="width: 100%; margin-bottom: 12px"
|
<label for="email">Email</label>
|
||||||
/>
|
<input type="email" id="email" name="email" required />
|
||||||
<label>*Phone</label>
|
<div class="form-error" id="error-email"></div>
|
||||||
<input
|
</div>
|
||||||
type="tel"
|
<div class="form-group">
|
||||||
name="phone"
|
<label for="company">Company</label>
|
||||||
required
|
<input type="text" id="company" name="company" required />
|
||||||
style="width: 100%; margin-bottom: 12px"
|
<div class="form-error" id="error-company"></div>
|
||||||
/>
|
</div>
|
||||||
<label>*Your Notes</label>
|
<div class="form-group">
|
||||||
<textarea
|
<label for="phone">Phone</label>
|
||||||
name="notes"
|
<input type="tel" id="phone" name="phone" required />
|
||||||
required
|
<div class="form-error" id="error-phone"></div>
|
||||||
style="width: 100%; margin-bottom: 16px"
|
</div>
|
||||||
></textarea>
|
<div class="form-group">
|
||||||
<button
|
<label for="notes">Your Notes</label>
|
||||||
type="submit"
|
<textarea id="notes" name="notes" required></textarea>
|
||||||
style="
|
<div class="form-error" id="error-notes"></div>
|
||||||
background: #063970;
|
</div>
|
||||||
color: #fff;
|
<button type="submit" class="form-submit-btn">Done</button>
|
||||||
padding: 8px 24px;
|
<div class="form-error" id="error-date"></div>
|
||||||
border: none;
|
<div class="form-error" id="error-time"></div>
|
||||||
border-radius: 4px;
|
<div class="form-error" id="error-timezone"></div>
|
||||||
"
|
<div class="form-error" id="error-general"></div>
|
||||||
>
|
</form>
|
||||||
Done
|
</div>
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
<%- include('partials/footer') %>
|
<%- include('partials/footer') %>
|
||||||
|
<script>
|
||||||
|
document.getElementById("bookingForm").onsubmit = async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
// Clear previous errors
|
||||||
|
document
|
||||||
|
.querySelectorAll(".form-error")
|
||||||
|
.forEach((el) => (el.textContent = ""));
|
||||||
|
const form = e.target;
|
||||||
|
const data = {
|
||||||
|
name: form.fullName.value,
|
||||||
|
email: form.email.value,
|
||||||
|
company: form.company.value,
|
||||||
|
phone: form.phone.value,
|
||||||
|
notes: form.notes.value,
|
||||||
|
date: form.date.value,
|
||||||
|
time: form.time.value,
|
||||||
|
timezone: form.tz.value,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/bookings", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
if (result.success) {
|
||||||
|
window.location.href = "/success";
|
||||||
|
} else if (result.error) {
|
||||||
|
if (typeof result.error === "object") {
|
||||||
|
for (const key in result.error) {
|
||||||
|
const el = document.getElementById("error-" + key);
|
||||||
|
if (el) el.textContent = result.error[key];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.getElementById("error-general").textContent = result.error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById("error-general").textContent =
|
||||||
|
"An error occurred. Please try again.";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|||||||
+56
-54
@@ -1,58 +1,60 @@
|
|||||||
<%- include('partials/header') %>
|
<%- include('partials/header') %>
|
||||||
<div style="padding: 32px; max-width: 1100px; margin: 0 auto">
|
<div class="calendar-container">
|
||||||
<h3>Pick a date and time</h3>
|
<div class="calendar-header">Calendar</div>
|
||||||
<p><b>Duration:</b> 1 hour</p>
|
<div class="calendar-content">
|
||||||
<p>Your timezone: <%= selectedTimezone %> <a href="/timezone">(Change)</a></p>
|
<div class="calendar-labels">
|
||||||
<div style="overflow-x: auto; margin-top: 32px">
|
<div class="calendar-label-main">Pick a date and time</div>
|
||||||
<table style="width: 100%; border-collapse: collapse">
|
<div class="calendar-label-duration">Duration: <span>1 hour</span></div>
|
||||||
<thead>
|
<div class="calendar-label-timezone">
|
||||||
<tr>
|
Your timezone: <%= selectedTimezone %> <a href="/timezone">(Change)</a>
|
||||||
<% weekDays.forEach(function(day) { %>
|
</div>
|
||||||
<th style="padding: 8px 12px; border-bottom: 2px solid #eee">
|
</div>
|
||||||
<div style="font-weight: bold; font-size: 1.1em">
|
<div class="calendar-table-wrapper">
|
||||||
<%= day.label %>
|
<table class="calendar-table">
|
||||||
</div>
|
<thead>
|
||||||
<div style="font-size: 0.95em; color: #888"><%= day.date %></div>
|
<tr>
|
||||||
</th>
|
<% weekDays.forEach(function(day) { %>
|
||||||
<% }) %>
|
<th>
|
||||||
</tr>
|
<div class="calendar-day-label"><%= day.label %></div>
|
||||||
</thead>
|
<div class="calendar-date-label"><%= day.date %></div>
|
||||||
<tbody>
|
</th>
|
||||||
<% for (let i = 0; i < maxSlots; i++) { %>
|
<% }) %>
|
||||||
<tr>
|
</tr>
|
||||||
<% weekDays.forEach(function(day) { %>
|
</thead>
|
||||||
<td style="padding: 8px 12px; text-align: center">
|
<tbody>
|
||||||
<% if (day.slots[i]) { %>
|
<% for (let i = 0; i < maxSlots; i++) { %>
|
||||||
<form action="/book" method="GET" style="display: inline">
|
<tr>
|
||||||
<input type="hidden" name="date" value="<%= day.dateISO %>" />
|
<% weekDays.forEach(function(day) { %>
|
||||||
<input type="hidden" name="time" value="<%= day.slots[i] %>" />
|
<td>
|
||||||
<button
|
<% if (day.slots[i]) { %>
|
||||||
type="submit"
|
<form action="/book" method="get">
|
||||||
style="
|
<input type="hidden" name="date" value="<%= day.dateISO %>" />
|
||||||
background: #fff;
|
<input type="hidden" name="time" value="<%= day.slots[i] %>" />
|
||||||
border: 1px solid #063970;
|
<input
|
||||||
color: #063970;
|
type="hidden"
|
||||||
border-radius: 4px;
|
name="tz"
|
||||||
padding: 4px 12px;
|
value="<%= selectedTimezone %>"
|
||||||
cursor: pointer;
|
/>
|
||||||
"
|
<button class="calendar-slot-btn"><%= day.slots[i] %></button>
|
||||||
>
|
</form>
|
||||||
<%= day.slots[i] %>
|
<% } %>
|
||||||
</button>
|
</td>
|
||||||
</form>
|
<% }) %>
|
||||||
<% } %>
|
</tr>
|
||||||
</td>
|
<% } %>
|
||||||
<% }) %>
|
</tbody>
|
||||||
</tr>
|
</table>
|
||||||
<% } %>
|
</div>
|
||||||
</tbody>
|
<div class="calendar-week-nav">
|
||||||
</table>
|
<% if (showPrevWeek) { %>
|
||||||
</div>
|
<a href="/calendar?tz=<%= selectedTimezone %>&week=<%= prevWeek %>"
|
||||||
<div style="margin-top: 16px">
|
>Previous Week</a
|
||||||
<% if (showPrevWeek) { %>
|
>
|
||||||
<a href="/calendar?week=prev">Previous Week</a>
|
<% } %>
|
||||||
<% } %>
|
<a href="/calendar?tz=<%= selectedTimezone %>&week=<%= nextWeek %>"
|
||||||
<a href="/calendar?week=next" style="margin-left: 24px">Next Week</a>
|
>Next Week</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('partials/footer') %>
|
<%- include('partials/footer') %>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<%- include('partials/header') %>
|
||||||
|
<div class="calendar-container">
|
||||||
|
<div class="calendar-header">Error</div>
|
||||||
|
<div class="calendar-content">
|
||||||
|
<h1><%= message %></h1>
|
||||||
|
<h2><%= error.status %></h2>
|
||||||
|
<pre><%= error.stack %></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%- include('partials/footer') %>
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
extends layout
|
|
||||||
|
|
||||||
block content
|
|
||||||
h1= message
|
|
||||||
h2= error.status
|
|
||||||
pre #{error.stack}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<%- include('partials/header') %>
|
||||||
|
<div class="calendar-container">
|
||||||
|
<div class="calendar-header"><%= title %></div>
|
||||||
|
<div class="calendar-content">
|
||||||
|
<p>Welcome to <%= title %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%- include('partials/footer') %>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
extends layout
|
|
||||||
|
|
||||||
block content
|
|
||||||
h1= title
|
|
||||||
p Welcome to #{title}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><%= title %></title>
|
||||||
|
<link rel="stylesheet" href="/stylesheets/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%- body %>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
doctype html
|
|
||||||
html
|
|
||||||
head
|
|
||||||
title= title
|
|
||||||
link(rel='stylesheet', href='/stylesheets/style.css')
|
|
||||||
body
|
|
||||||
block content
|
|
||||||
@@ -1 +1,2 @@
|
|||||||
<div style="height: 40px"></div>
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
<div
|
<!DOCTYPE html>
|
||||||
style="
|
<html>
|
||||||
background: #063970;
|
<head>
|
||||||
color: white;
|
<title>Calendar</title>
|
||||||
padding: 24px 24px 12px 24px;
|
<link rel="stylesheet" href="/stylesheets/style.css" />
|
||||||
font-size: 2rem;
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
font-weight: bold;
|
</head>
|
||||||
"
|
<body></body>
|
||||||
>
|
</html>
|
||||||
Calendar
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<%- include('partials/header') %>
|
<%- include('partials/header') %>
|
||||||
<div style="padding: 32px; max-width: 700px; margin: 0 auto">
|
<div class="calendar-container">
|
||||||
<p style="margin-top: 32px; font-size: 1.2em">
|
<div class="calendar-header">Calendar</div>
|
||||||
|
<div class="calendar-content success-message">
|
||||||
Thanks for filling in the form. You will be emailed next steps.
|
Thanks for filling in the form. You will be emailed next steps.
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('partials/footer') %>
|
<%- include('partials/footer') %>
|
||||||
|
|||||||
+71
-41
@@ -1,48 +1,78 @@
|
|||||||
<%- include('partials/header') %>
|
<%- include('partials/header') %>
|
||||||
<div style="padding: 32px; max-width: 900px; margin: 0 auto">
|
<div class="calendar-container">
|
||||||
<h3>Pick a date and time</h3>
|
<div class="calendar-header">Calendar</div>
|
||||||
<p><b>Duration:</b> 1 hour</p>
|
<div class="calendar-content">
|
||||||
<label>Your timezone:</label>
|
<div class="calendar-labels">
|
||||||
<select name="timezone" id="timezone-select">
|
<div class="calendar-label-main">Pick a date and time</div>
|
||||||
<% timezones.forEach(function(tz) { %>
|
<div class="calendar-label-duration">Duration: <span>1 hour</span></div>
|
||||||
<option value="<%= tz.value %>"><%= tz.label %></option>
|
<div class="calendar-label-timezone">
|
||||||
<% }) %>
|
Your timezone:
|
||||||
</select>
|
<button id="select-timezone-btn" class="timezone-btn">
|
||||||
<hr />
|
Please Select
|
||||||
<div style="display: flex; justify-content: center; margin-top: 40px">
|
</button>
|
||||||
<div
|
|
||||||
style="
|
|
||||||
background: #fff;
|
|
||||||
padding: 32px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 8px #0001;
|
|
||||||
min-width: 400px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<h4>TIME ZONE</h4>
|
|
||||||
<div style="display: flex; gap: 32px">
|
|
||||||
<% Object.keys(timezoneGroups).forEach(function(region) { %>
|
|
||||||
<div>
|
|
||||||
<b><%= region %></b>
|
|
||||||
<% timezoneGroups[region].forEach(function(tz) { %>
|
|
||||||
<div>
|
|
||||||
<input type="radio" name="timezoneRadio" value="<%= tz.value %>" />
|
|
||||||
<%= tz.label %>
|
|
||||||
</div>
|
|
||||||
<% }) %>
|
|
||||||
</div>
|
|
||||||
<% }) %>
|
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 16px">
|
</div>
|
||||||
<label
|
<!-- Modal Overlay -->
|
||||||
><input type="radio" name="format" value="ampm" checked />
|
<div id="timezone-modal" class="modal-overlay" style="display: none">
|
||||||
am/pm</label
|
<div class="modal-content">
|
||||||
>
|
<div class="modal-title">TIME ZONE</div>
|
||||||
<label style="margin-left: 16px"
|
<div class="modal-format-switch">
|
||||||
><input type="radio" name="format" value="24hr" /> 24hr</label
|
<label
|
||||||
>
|
><input type="radio" name="format" value="ampm" checked />
|
||||||
|
am/pm</label
|
||||||
|
>
|
||||||
|
<label><input type="radio" name="format" value="24hr" /> 24hr</label>
|
||||||
|
</div>
|
||||||
|
<div class="timezone-groups">
|
||||||
|
<% for (const group in timezoneGroups) { %>
|
||||||
|
<div class="timezone-group">
|
||||||
|
<div class="timezone-group-title"><%= group %></div>
|
||||||
|
<% timezoneGroups[group].forEach(function(tz) { %>
|
||||||
|
<label class="timezone-option">
|
||||||
|
<input type="radio" name="timezone" value="<%= tz.name %>" />
|
||||||
|
<span
|
||||||
|
class="tz-time"
|
||||||
|
data-am="<%= tz.time_am %>"
|
||||||
|
data-24="<%= tz.time_24 %>"
|
||||||
|
>
|
||||||
|
<%= tz.label %>
|
||||||
|
<span class="tz-time-value"><%= tz.time_am %></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<% }) %>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%- include('partials/footer') %>
|
<%- include('partials/footer') %>
|
||||||
|
<script>
|
||||||
|
// Modal logic
|
||||||
|
const btn = document.getElementById("select-timezone-btn");
|
||||||
|
const modal = document.getElementById("timezone-modal");
|
||||||
|
btn.onclick = () => {
|
||||||
|
modal.style.display = "flex";
|
||||||
|
};
|
||||||
|
modal.onclick = (e) => {
|
||||||
|
if (e.target === modal) modal.style.display = "none";
|
||||||
|
};
|
||||||
|
document.querySelectorAll('input[name="timezone"]').forEach((el) => {
|
||||||
|
el.onclick = () => {
|
||||||
|
window.location.href = "/calendar?tz=" + encodeURIComponent(el.value);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Time format toggle logic
|
||||||
|
document.querySelectorAll('input[name="format"]').forEach((el) => {
|
||||||
|
el.onchange = function () {
|
||||||
|
const is24 = this.value === "24hr";
|
||||||
|
document.querySelectorAll(".tz-time").forEach((span) => {
|
||||||
|
const am = span.getAttribute("data-am");
|
||||||
|
const t24 = span.getAttribute("data-24");
|
||||||
|
span.querySelector(".tz-time-value").textContent = is24 ? t24 : am;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user