diff --git a/day17/app.js b/day17/app.js index b87197d..bc584f2 100644 --- a/day17/app.js +++ b/day17/app.js @@ -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)); diff --git a/day17/models/booking.js b/day17/models/booking.js new file mode 100644 index 0000000..505996d --- /dev/null +++ b/day17/models/booking.js @@ -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; +}; diff --git a/day17/models/index.js b/day17/models/index.js index 8c44682..9e18134 100644 --- a/day17/models/index.js +++ b/day17/models/index.js @@ -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); @@ -66,4 +80,4 @@ Object.keys(db).forEach((modelName) => { db.sequelize = sequelize; db.Sequelize = Sequelize; -module.exports = db; \ No newline at end of file +module.exports = db; diff --git a/day17/package.json b/day17/package.json index af64762..897135f 100644 --- a/day17/package.json +++ b/day17/package.json @@ -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" } } diff --git a/day17/public/stylesheets/style.css b/day17/public/stylesheets/style.css index 9453385..bb4c1d3 100644 --- a/day17/public/stylesheets/style.css +++ b/day17/public/stylesheets/style.css @@ -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; + } } diff --git a/day17/routes/api.js b/day17/routes/api.js new file mode 100644 index 0000000..c273499 --- /dev/null +++ b/day17/routes/api.js @@ -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; diff --git a/day17/routes/index.js b/day17/routes/index.js index 228bf0b..d3b59f3 100644 --- a/day17/routes/index.js +++ b/day17/routes/index.js @@ -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, }); }); diff --git a/day17/views/booking-form.ejs b/day17/views/booking-form.ejs index 57087ef..007e758 100644 --- a/day17/views/booking-form.ejs +++ b/day17/views/booking-form.ejs @@ -1,56 +1,97 @@ <%- include('partials/header') %> -
-

Pick a date and time

-

Duration: 1 hour

-

Your timezone: <%= selectedTimezone %>

-
-

Additional Information

- - - - - - - - - - - -
+
+
Calendar
+
+
+
Pick a date and time
+
Duration: 1 hour
+
+ Your timezone: <%= selectedTimezone %> +
+ <% if (selectedDate && selectedTime) { %> +
+ Selected: <%= selectedDate %> at <%= selectedTime %> +
+ <% } %> +
+
+ + + +
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+
+
+
+
+
<%- include('partials/footer') %> + diff --git a/day17/views/calendar.ejs b/day17/views/calendar.ejs index 20621f9..b0a2d7e 100644 --- a/day17/views/calendar.ejs +++ b/day17/views/calendar.ejs @@ -1,58 +1,60 @@ <%- include('partials/header') %> -
-

Pick a date and time

-

Duration: 1 hour

-

Your timezone: <%= selectedTimezone %> (Change)

-
- - - - <% weekDays.forEach(function(day) { %> - - <% }) %> - - - - <% for (let i = 0; i < maxSlots; i++) { %> - - <% weekDays.forEach(function(day) { %> - - <% }) %> - - <% } %> - -
-
- <%= day.label %> -
-
<%= day.date %>
-
- <% if (day.slots[i]) { %> -
- - - -
- <% } %> -
-
-
- <% if (showPrevWeek) { %> - Previous Week - <% } %> - Next Week +
+
Calendar
+
+
+
Pick a date and time
+
Duration: 1 hour
+
+ Your timezone: <%= selectedTimezone %> (Change) +
+
+
+ + + + <% weekDays.forEach(function(day) { %> + + <% }) %> + + + + <% for (let i = 0; i < maxSlots; i++) { %> + + <% weekDays.forEach(function(day) { %> + + <% }) %> + + <% } %> + +
+
<%= day.label %>
+
<%= day.date %>
+
+ <% if (day.slots[i]) { %> +
+ + + + +
+ <% } %> +
+
+
+ <% if (showPrevWeek) { %> + Previous Week + <% } %> + Next Week +
<%- include('partials/footer') %> diff --git a/day17/views/error.ejs b/day17/views/error.ejs new file mode 100644 index 0000000..786f631 --- /dev/null +++ b/day17/views/error.ejs @@ -0,0 +1,10 @@ +<%- include('partials/header') %> +
+
Error
+
+

<%= message %>

+

<%= error.status %>

+
<%= error.stack %>
+
+
+<%- include('partials/footer') %> diff --git a/day17/views/error.jade b/day17/views/error.jade deleted file mode 100644 index 51ec12c..0000000 --- a/day17/views/error.jade +++ /dev/null @@ -1,6 +0,0 @@ -extends layout - -block content - h1= message - h2= error.status - pre #{error.stack} diff --git a/day17/views/index.ejs b/day17/views/index.ejs new file mode 100644 index 0000000..e0ded35 --- /dev/null +++ b/day17/views/index.ejs @@ -0,0 +1,8 @@ +<%- include('partials/header') %> +
+
<%= title %>
+
+

Welcome to <%= title %>

+
+
+<%- include('partials/footer') %> diff --git a/day17/views/index.jade b/day17/views/index.jade deleted file mode 100644 index 3d63b9a..0000000 --- a/day17/views/index.jade +++ /dev/null @@ -1,5 +0,0 @@ -extends layout - -block content - h1= title - p Welcome to #{title} diff --git a/day17/views/layout.ejs b/day17/views/layout.ejs new file mode 100644 index 0000000..d58b8be --- /dev/null +++ b/day17/views/layout.ejs @@ -0,0 +1,10 @@ + + + + <%= title %> + + + + <%- body %> + + diff --git a/day17/views/layout.jade b/day17/views/layout.jade deleted file mode 100644 index 15af079..0000000 --- a/day17/views/layout.jade +++ /dev/null @@ -1,7 +0,0 @@ -doctype html -html - head - title= title - link(rel='stylesheet', href='/stylesheets/style.css') - body - block content diff --git a/day17/views/partials/footer.ejs b/day17/views/partials/footer.ejs index 3ea674b..cad7968 100644 --- a/day17/views/partials/footer.ejs +++ b/day17/views/partials/footer.ejs @@ -1 +1,2 @@ -
+ + \ No newline at end of file diff --git a/day17/views/partials/header.ejs b/day17/views/partials/header.ejs index f396e52..1d69288 100644 --- a/day17/views/partials/header.ejs +++ b/day17/views/partials/header.ejs @@ -1,11 +1,9 @@ -
- Calendar -
+ + + + Calendar + + + + + diff --git a/day17/views/success.ejs b/day17/views/success.ejs index bf70b11..dae2aed 100644 --- a/day17/views/success.ejs +++ b/day17/views/success.ejs @@ -1,7 +1,8 @@ <%- include('partials/header') %> -
-

+

+
Calendar
+
Thanks for filling in the form. You will be emailed next steps. -

+
<%- include('partials/footer') %> diff --git a/day17/views/timezone.ejs b/day17/views/timezone.ejs index 4fe458c..d413820 100644 --- a/day17/views/timezone.ejs +++ b/day17/views/timezone.ejs @@ -1,48 +1,78 @@ <%- include('partials/header') %> -
-

Pick a date and time

-

Duration: 1 hour

- - -
-
-
-

TIME ZONE

-
- <% Object.keys(timezoneGroups).forEach(function(region) { %> -
- <%= region %> - <% timezoneGroups[region].forEach(function(tz) { %> -
- - <%= tz.label %> -
- <% }) %> -
- <% }) %> +
+
Calendar
+
+
+
Pick a date and time
+
Duration: 1 hour
+
+ Your timezone: +
-
- - +
+ +
<%- include('partials/footer') %> +