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 usersRouter = require("./routes/users");
|
||||
var apiRouter = require("./routes/api");
|
||||
|
||||
const db = require("./models");
|
||||
var cors = require("cors");
|
||||
@@ -24,7 +25,7 @@ app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
app.use("/", indexRouter);
|
||||
app.use("/users", usersRouter);
|
||||
|
||||
app.use("/api", apiRouter);
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function (req, res, next) {
|
||||
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;
|
||||
};
|
||||
+47
-33
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
"use strict";
|
||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||
/**
|
||||
* Sequelize File
|
||||
@@ -8,49 +8,63 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let Sequelize = require('sequelize');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
let Sequelize = require("sequelize");
|
||||
const basename = path.basename(__filename);
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { DataTypes } = require("sequelize");
|
||||
const config = {
|
||||
DB_DATABASE: 'mysql',
|
||||
DB_USERNAME: 'root',
|
||||
DB_PASSWORD: 'root',
|
||||
DB_ADAPTER: 'mysql',
|
||||
DB_NAME: 'day_1',
|
||||
DB_HOSTNAME: 'localhost',
|
||||
DB_DATABASE: "mysql",
|
||||
DB_USERNAME: "root",
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||
DB_ADAPTER: "mysql",
|
||||
DB_NAME: "day_17",
|
||||
DB_HOSTNAME: "localhost",
|
||||
DB_PORT: 3306,
|
||||
};
|
||||
|
||||
let db = {};
|
||||
|
||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: '-04:00',
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
define: {
|
||||
timestamps: false,
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
});
|
||||
let sequelize = new Sequelize(
|
||||
config.DB_NAME,
|
||||
config.DB_USERNAME,
|
||||
config.DB_PASSWORD,
|
||||
{
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: "-04:00",
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
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)
|
||||
.filter((file) => {
|
||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
||||
return (
|
||||
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||
);
|
||||
})
|
||||
.forEach((file) => {
|
||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"moment-timezone": "^0.6.0",
|
||||
"morgan": "~1.9.1",
|
||||
"mysql2": "^2.3.3",
|
||||
"node-input-validator": "^4.5.1",
|
||||
"sequelize": "^6.15.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,289 @@
|
||||
body {
|
||||
padding: 50px;
|
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Inter", "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
background: #eaeaea;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00B7FF;
|
||||
.calendar-container {
|
||||
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 = {};
|
||||
for (const region in regions) {
|
||||
groups[region] = regions[region].map((tz) => ({
|
||||
value: tz,
|
||||
label: moment().tz(tz).zoneAbbr() + " " + moment().tz(tz).format("h:mma"),
|
||||
name: tz,
|
||||
label: tz.replace(/_/g, " ").replace("America/", ""),
|
||||
time_am: moment().tz(tz).format("h:mma"),
|
||||
time_24: moment().tz(tz).format("HH:mm"),
|
||||
}));
|
||||
}
|
||||
return groups;
|
||||
@@ -44,9 +46,11 @@ function getAllTimezones() {
|
||||
}
|
||||
|
||||
// Helper: Generate week days and slots
|
||||
function getWeekDaysAndSlots(selectedTz) {
|
||||
function getWeekDaysAndSlots(selectedTz, weekOffset = 0) {
|
||||
const weekDays = [];
|
||||
const today = moment().tz(selectedTz);
|
||||
const today = moment()
|
||||
.tz(selectedTz)
|
||||
.add(weekOffset * 7, "days");
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const day = today.clone().add(i, "days");
|
||||
weekDays.push({
|
||||
@@ -89,20 +93,30 @@ router.get("/timezone", function (req, res) {
|
||||
// Calendar slot selection screen
|
||||
router.get("/calendar", function (req, res) {
|
||||
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", {
|
||||
selectedTimezone,
|
||||
weekDays,
|
||||
maxSlots,
|
||||
showPrevWeek: false, // For demo, only next week link
|
||||
showPrevWeek: weekOffset > 0,
|
||||
prevWeek: weekOffset - 1,
|
||||
nextWeek: weekOffset + 1,
|
||||
});
|
||||
});
|
||||
|
||||
// Booking form screen
|
||||
router.get("/book", function (req, res) {
|
||||
const selectedTimezone = req.query.tz || "America/New_York";
|
||||
const selectedDate = req.query.date || "";
|
||||
const selectedTime = req.query.time || "";
|
||||
res.render("booking-form", {
|
||||
selectedTimezone,
|
||||
selectedDate,
|
||||
selectedTime,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,56 +1,97 @@
|
||||
<%- include('partials/header') %>
|
||||
<div style="padding: 32px; max-width: 700px; margin: 0 auto">
|
||||
<h3>Pick a date and time</h3>
|
||||
<p><b>Duration:</b> 1 hour</p>
|
||||
<p>Your timezone: <%= selectedTimezone %></p>
|
||||
<form action="/book" method="POST" style="margin-top: 32px">
|
||||
<h4>Additional Information</h4>
|
||||
<label>*Full Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="fullName"
|
||||
required
|
||||
style="width: 100%; margin-bottom: 12px"
|
||||
/>
|
||||
<label>*Email</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
style="width: 100%; margin-bottom: 12px"
|
||||
/>
|
||||
<label>*Company</label>
|
||||
<input
|
||||
type="text"
|
||||
name="company"
|
||||
required
|
||||
style="width: 100%; margin-bottom: 12px"
|
||||
/>
|
||||
<label>*Phone</label>
|
||||
<input
|
||||
type="tel"
|
||||
name="phone"
|
||||
required
|
||||
style="width: 100%; margin-bottom: 12px"
|
||||
/>
|
||||
<label>*Your Notes</label>
|
||||
<textarea
|
||||
name="notes"
|
||||
required
|
||||
style="width: 100%; margin-bottom: 16px"
|
||||
></textarea>
|
||||
<button
|
||||
type="submit"
|
||||
style="
|
||||
background: #063970;
|
||||
color: #fff;
|
||||
padding: 8px 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
"
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</form>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">Calendar</div>
|
||||
<div class="calendar-content">
|
||||
<div class="calendar-labels">
|
||||
<div class="calendar-label-main">Pick a date and time</div>
|
||||
<div class="calendar-label-duration">Duration: <span>1 hour</span></div>
|
||||
<div class="calendar-label-timezone">
|
||||
Your timezone: <%= selectedTimezone %>
|
||||
</div>
|
||||
<% if (selectedDate && selectedTime) { %>
|
||||
<div class="calendar-label-selected">
|
||||
<strong>Selected:</strong> <%= selectedDate %> at <%= selectedTime %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<form class="booking-form" id="bookingForm">
|
||||
<input type="hidden" name="date" value="<%= selectedDate %>" />
|
||||
<input type="hidden" name="time" value="<%= selectedTime %>" />
|
||||
<input type="hidden" name="tz" value="<%= selectedTimezone %>" />
|
||||
<div class="form-group">
|
||||
<label for="fullName">Full Name</label>
|
||||
<input type="text" id="fullName" name="name" required />
|
||||
<div class="form-error" id="error-name"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" required />
|
||||
<div class="form-error" id="error-email"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="company">Company</label>
|
||||
<input type="text" id="company" name="company" required />
|
||||
<div class="form-error" id="error-company"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="phone">Phone</label>
|
||||
<input type="tel" id="phone" name="phone" required />
|
||||
<div class="form-error" id="error-phone"></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="notes">Your Notes</label>
|
||||
<textarea id="notes" name="notes" required></textarea>
|
||||
<div class="form-error" id="error-notes"></div>
|
||||
</div>
|
||||
<button type="submit" class="form-submit-btn">Done</button>
|
||||
<div class="form-error" id="error-date"></div>
|
||||
<div class="form-error" id="error-time"></div>
|
||||
<div class="form-error" id="error-timezone"></div>
|
||||
<div class="form-error" id="error-general"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<%- 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') %>
|
||||
<div style="padding: 32px; max-width: 1100px; margin: 0 auto">
|
||||
<h3>Pick a date and time</h3>
|
||||
<p><b>Duration:</b> 1 hour</p>
|
||||
<p>Your timezone: <%= selectedTimezone %> <a href="/timezone">(Change)</a></p>
|
||||
<div style="overflow-x: auto; margin-top: 32px">
|
||||
<table style="width: 100%; border-collapse: collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<% weekDays.forEach(function(day) { %>
|
||||
<th style="padding: 8px 12px; border-bottom: 2px solid #eee">
|
||||
<div style="font-weight: bold; font-size: 1.1em">
|
||||
<%= day.label %>
|
||||
</div>
|
||||
<div style="font-size: 0.95em; color: #888"><%= day.date %></div>
|
||||
</th>
|
||||
<% }) %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for (let i = 0; i < maxSlots; i++) { %>
|
||||
<tr>
|
||||
<% weekDays.forEach(function(day) { %>
|
||||
<td style="padding: 8px 12px; text-align: center">
|
||||
<% if (day.slots[i]) { %>
|
||||
<form action="/book" method="GET" style="display: inline">
|
||||
<input type="hidden" name="date" value="<%= day.dateISO %>" />
|
||||
<input type="hidden" name="time" value="<%= day.slots[i] %>" />
|
||||
<button
|
||||
type="submit"
|
||||
style="
|
||||
background: #fff;
|
||||
border: 1px solid #063970;
|
||||
color: #063970;
|
||||
border-radius: 4px;
|
||||
padding: 4px 12px;
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
<%= day.slots[i] %>
|
||||
</button>
|
||||
</form>
|
||||
<% } %>
|
||||
</td>
|
||||
<% }) %>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="margin-top: 16px">
|
||||
<% if (showPrevWeek) { %>
|
||||
<a href="/calendar?week=prev">Previous Week</a>
|
||||
<% } %>
|
||||
<a href="/calendar?week=next" style="margin-left: 24px">Next Week</a>
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">Calendar</div>
|
||||
<div class="calendar-content">
|
||||
<div class="calendar-labels">
|
||||
<div class="calendar-label-main">Pick a date and time</div>
|
||||
<div class="calendar-label-duration">Duration: <span>1 hour</span></div>
|
||||
<div class="calendar-label-timezone">
|
||||
Your timezone: <%= selectedTimezone %> <a href="/timezone">(Change)</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-table-wrapper">
|
||||
<table class="calendar-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<% weekDays.forEach(function(day) { %>
|
||||
<th>
|
||||
<div class="calendar-day-label"><%= day.label %></div>
|
||||
<div class="calendar-date-label"><%= day.date %></div>
|
||||
</th>
|
||||
<% }) %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% for (let i = 0; i < maxSlots; i++) { %>
|
||||
<tr>
|
||||
<% weekDays.forEach(function(day) { %>
|
||||
<td>
|
||||
<% if (day.slots[i]) { %>
|
||||
<form action="/book" method="get">
|
||||
<input type="hidden" name="date" value="<%= day.dateISO %>" />
|
||||
<input type="hidden" name="time" value="<%= day.slots[i] %>" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="tz"
|
||||
value="<%= selectedTimezone %>"
|
||||
/>
|
||||
<button class="calendar-slot-btn"><%= day.slots[i] %></button>
|
||||
</form>
|
||||
<% } %>
|
||||
</td>
|
||||
<% }) %>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="calendar-week-nav">
|
||||
<% if (showPrevWeek) { %>
|
||||
<a href="/calendar?tz=<%= selectedTimezone %>&week=<%= prevWeek %>"
|
||||
>Previous Week</a
|
||||
>
|
||||
<% } %>
|
||||
<a href="/calendar?tz=<%= selectedTimezone %>&week=<%= nextWeek %>"
|
||||
>Next Week</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%- 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
|
||||
style="
|
||||
background: #063970;
|
||||
color: white;
|
||||
padding: 24px 24px 12px 24px;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
"
|
||||
>
|
||||
Calendar
|
||||
</div>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Calendar</title>
|
||||
<link rel="stylesheet" href="/stylesheets/style.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<%- include('partials/header') %>
|
||||
<div style="padding: 32px; max-width: 700px; margin: 0 auto">
|
||||
<p style="margin-top: 32px; font-size: 1.2em">
|
||||
<div class="calendar-container">
|
||||
<div class="calendar-header">Calendar</div>
|
||||
<div class="calendar-content success-message">
|
||||
Thanks for filling in the form. You will be emailed next steps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<%- include('partials/footer') %>
|
||||
|
||||
+71
-41
@@ -1,48 +1,78 @@
|
||||
<%- include('partials/header') %>
|
||||
<div style="padding: 32px; max-width: 900px; margin: 0 auto">
|
||||
<h3>Pick a date and time</h3>
|
||||
<p><b>Duration:</b> 1 hour</p>
|
||||
<label>Your timezone:</label>
|
||||
<select name="timezone" id="timezone-select">
|
||||
<% timezones.forEach(function(tz) { %>
|
||||
<option value="<%= tz.value %>"><%= tz.label %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
<hr />
|
||||
<div style="display: flex; justify-content: center; margin-top: 40px">
|
||||
<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 class="calendar-container">
|
||||
<div class="calendar-header">Calendar</div>
|
||||
<div class="calendar-content">
|
||||
<div class="calendar-labels">
|
||||
<div class="calendar-label-main">Pick a date and time</div>
|
||||
<div class="calendar-label-duration">Duration: <span>1 hour</span></div>
|
||||
<div class="calendar-label-timezone">
|
||||
Your timezone:
|
||||
<button id="select-timezone-btn" class="timezone-btn">
|
||||
Please Select
|
||||
</button>
|
||||
</div>
|
||||
<div style="margin-top: 16px">
|
||||
<label
|
||||
><input type="radio" name="format" value="ampm" checked />
|
||||
am/pm</label
|
||||
>
|
||||
<label style="margin-left: 16px"
|
||||
><input type="radio" name="format" value="24hr" /> 24hr</label
|
||||
>
|
||||
</div>
|
||||
<!-- Modal Overlay -->
|
||||
<div id="timezone-modal" class="modal-overlay" style="display: none">
|
||||
<div class="modal-content">
|
||||
<div class="modal-title">TIME ZONE</div>
|
||||
<div class="modal-format-switch">
|
||||
<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>
|
||||
<%- 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