Compare commits

...

27 Commits

Author SHA1 Message Date
Ayobami b0d86465f2 feat: complete day 20 react task 2025-07-23 23:25:19 +01:00
Ayobami 743187b216 feat: complete day 17 2025-07-22 19:21:32 +01:00
Ayobami 29e6eb82c7 feat: complete day 19 2025-07-22 17:49:50 +01:00
Ayobami dc35cfcb3f feat: complete day 16 2025-07-21 22:23:26 +01:00
Ayobami cbbb0ed4c4 feat: complete day 15 2025-07-18 21:34:29 +01:00
Ayobami 9113ba4c74 feat: complete day 14 2025-07-18 20:12:09 +01:00
Ayobami 7d9350c3df feat: complete day 13 2025-07-17 21:55:47 +01:00
Ayobami 9c84737fed feat: complete day 11 2025-07-17 16:55:13 +01:00
Ayobami 001e4b6d00 feat: complete day 10 2025-07-16 18:59:35 +01:00
Ayobami 825583e645 feat: complete day 9 2025-07-15 19:04:00 +01:00
Ayobami 7d8ed6d0ee feat: complete day 8 2025-07-15 17:41:52 +01:00
Ayobami 10447cd05e feat: complete day 7 2025-07-15 16:18:19 +01:00
Ayobami f87c93e85c feat: complete day 6 2025-07-14 21:52:29 +01:00
Ayobami 61d9872bab feat: complete day 6 2025-07-14 19:08:13 +01:00
Ayobami 95edc56088 feat: complete day 4 2025-07-11 20:19:55 +01:00
Ayobami 2b58c1d8b0 feat: complete day 3 2025-07-11 19:40:34 +01:00
Ayobami 325788cb39 feat: complete day 2 2025-07-11 18:05:50 +01:00
Ayobami b277c8182d feat: complete day 1 2025-07-10 21:08:42 +01:00
manaknightdigital 1703819bda Merge pull request #2 from emmymayo/master
baas task
2023-12-08 02:06:12 -05:00
Mayowa Emmanuel 7bbafc90a8 baas task 2023-12-07 21:44:21 +00:00
manaknightdigital b640c08c09 Update README.md 2023-12-04 21:28:57 -05:00
manaknightdigital 04a5ae01cf Update README.md 2023-11-21 13:26:44 -05:00
Possible 5485f1af1d Update 2023-11-16 18:50:16 +01:00
ryanwong 7a6da5203e add treeql 2023-05-09 09:33:31 -04:00
manaknightdigital a4177125f8 Update README.md 2022-10-21 12:32:23 -04:00
manaknightdigital 595876509a Merge pull request #1 from To-heeb/master
update of the sequelize link in README of day1 activity to a active doc
2022-10-17 09:28:06 -04:00
Toheeb Oyekola 059da72768 update of the sequelize link in README of day1 activity to a the active doc 2022-10-12 03:05:51 +01:00
424 changed files with 49127 additions and 1143 deletions
+12 -1
View File
@@ -1,2 +1,13 @@
node_modules
package-lock.json
package-lock.json
.env
# Environment variables in day projects
day*/.env
day*/.env.*
day*/**/.env
# Node modules in day projects
day*/node_modules/
day*/package-lock.json
+1
View File
@@ -0,0 +1 @@
# This project is a toy project for training and quality assurance purposes
+1 -1
View File
@@ -4,7 +4,7 @@
- setup project
- clone to your github
- Read the documentation https://sequelize.org/docs/v6/getting-started/
- Read the documentation https://sequelize.org/docs/v7/getting-started/
- Setup the following Models in models folder. Make sure tables made by sequelize:
```
+24 -15
View File
@@ -1,29 +1,38 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");
const db = require("./models");
var cors = require("cors");
const shippingDockRouter = require("./routes/shippingDock");
const orderRouter = require("./routes/order");
const transactionRouter = require("./routes/transaction");
var app = express();
app.set("db", db);
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");
app.use(cors());
app.use(logger('dev'));
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, "public")));
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use("/", indexRouter);
app.use("/users", usersRouter);
// ROUTES
app.use("/api/v1/shipping_dock", shippingDockRouter);
app.use("/api/v1/order", orderRouter);
app.use("/api/v1/transaction", transactionRouter);
// catch 404 and forward to error handler
app.use(function (req, res, next) {
@@ -34,11 +43,11 @@ app.use(function (req, res, next) {
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
res.render("error");
});
module.exports = app;
+16 -19
View File
@@ -4,16 +4,16 @@
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('day-1:server');
var http = require('http');
var app = require("../app");
var debug = require("debug")("day-1:server");
var http = require("http");
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
var port = normalizePort(process.env.PORT || "3000");
app.set("port", port);
/**
* Create HTTP server.
@@ -26,8 +26,8 @@ var server = http.createServer(app);
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
server.on("error", onError);
server.on("listening", onListening);
/**
* Normalize a port into a number, string, or false.
@@ -54,22 +54,20 @@ function normalizePort(val) {
*/
function onError(error) {
if (error.syscall !== 'listen') {
if (error.syscall !== "listen") {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
var bind = typeof port === "string" ? "Pipe " + port : "Port " + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
case "EACCES":
console.error(bind + " requires elevated privileges");
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
case "EADDRINUSE":
console.error(bind + " is already in use");
process.exit(1);
break;
default:
@@ -83,8 +81,7 @@ function onError(error) {
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
debug("Listening on " + bind);
console.log("Server is listening on " + bind);
}
+48 -34
View File
@@ -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,
DB_ADAPTER: "mysql",
DB_NAME: "day_1",
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("All tables synced successfully.");
})
.catch((err) => {
console.error("Failed to sync tables:", 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;
module.exports = db;
+4 -10
View File
@@ -1,5 +1,5 @@
module.exports = (sequelize, DataTypes) => {
const location = sequelize.define(
return sequelize.define(
"location",
{
id: {
@@ -8,19 +8,13 @@ module.exports = (sequelize, DataTypes) => {
autoIncrement: true,
},
name: DataTypes.STRING,
created_at: DataTypes.DATEONLY,
updated_at: DataTypes.DATE,
},
{
timestamps: true,
freezeTableName: true,
tableName: "location",
},
{
underscoredAll: false,
underscored: false,
createdAt: "created_at",
updatedAt: "updated_at",
}
);
return location;
};
};
+37
View File
@@ -0,0 +1,37 @@
module.exports = (sequelize, DataTypes) => {
return sequelize.define(
"Order",
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
order_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
user_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
shipping_dock_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
amount: {
type: DataTypes.INTEGER,
allowNull: false,
},
notes: DataTypes.STRING,
status: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
},
{
tableName: "orders",
}
);
};
+24
View File
@@ -0,0 +1,24 @@
module.exports = (sequelize, DataTypes) => {
return sequelize.define(
"ShippingDock",
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
status: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
},
{
tableName: "shipping_dock",
}
);
};
+32
View File
@@ -0,0 +1,32 @@
module.exports = (sequelize, DataTypes) => {
return sequelize.define(
"Transaction",
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
order_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
user_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
shipping_dock_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
amount: {
type: DataTypes.INTEGER,
allowNull: false,
},
notes: DataTypes.STRING,
},
{
tableName: "transactions",
}
);
};
+2 -1
View File
@@ -3,7 +3,8 @@
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
"start": "node ./bin/www",
"dev": "node --watch --env-file=.env ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
+104
View File
@@ -0,0 +1,104 @@
const express = require("express");
const router = express.Router();
const { Order } = require("../models");
const {
handleError,
handleSuccess,
handleSequelizeError,
deepEqual,
} = require("../utils");
// GET all orders
router.get("/", async (_, res) => {
try {
const orders = await Order.findAll();
handleSuccess(res, orders);
} catch (err) {
handleSequelizeError(err, res);
}
});
// GET one order by id
router.get("/:id", async (req, res) => {
try {
const order = await Order.findByPk(req.params.id);
if (!order) return handleError("Order not found", 404, res);
handleSuccess(res, order);
} catch (err) {
handleSequelizeError(err, res);
}
});
// POST create an order
router.post("/", async (req, res) => {
try {
const { order_id, user_id, shipping_dock_id, amount, notes } = req.body;
if (
order_id === undefined ||
user_id === undefined ||
shipping_dock_id === undefined ||
amount === undefined ||
notes === undefined
) {
return handleError("All fields are required", 400, res);
}
const order = await Order.create(req.body);
handleSuccess(res, order, 201);
} catch (err) {
handleError(err.message, 500, res);
}
});
// PUT update an order
router.put("/:id", async (req, res) => {
try {
const { id } = req.params;
const order = await Order.findByPk(id);
if (!order) return handleError("Order not found", 404, res);
// Only allow updating amount, notes, and status
const allowedFields = ["amount", "notes", "status"];
const changes = {};
for (const key of allowedFields) {
if (
req.body[key] !== undefined &&
!deepEqual(req.body[key], order[key])
) {
changes[key] = req.body[key];
}
}
// Validate status if present
if (
changes.status !== undefined &&
![0, 1].includes(Number(changes.status))
) {
return handleError("Status must be 0 or 1", 400, res);
}
if (Object.keys(changes).length === 0) {
return handleError("No updated fields", 400, res); // No changes
}
await order.update(changes);
handleSuccess(res, await Order.findByPk(id));
} catch (err) {
handleSequelizeError(err, res);
}
});
// DELETE an order
router.delete("/:id", async (req, res) => {
try {
const deleted = await Order.destroy({
where: { id: req.params.id },
});
if (deleted === 0) return handleError("Order not found", 404, res);
handleSuccess(res, null, 204); // 204 No Content
} catch (err) {
handleSequelizeError(err, res);
}
});
module.exports = router;
+94
View File
@@ -0,0 +1,94 @@
const express = require("express");
const router = express.Router();
const { ShippingDock } = require("../models");
const {
handleError,
handleSuccess,
handleSequelizeError,
deepEqual,
} = require("../utils");
// GET all shipping docks
router.get("/", async (_, res) => {
try {
const docks = await ShippingDock.findAll();
handleSuccess(res, docks);
} catch (err) {
handleSequelizeError(err, res);
}
});
// GET one shipping dock by id
router.get("/:id", async (req, res) => {
try {
const dock = await ShippingDock.findByPk(req.params.id);
if (!dock) return handleError("Dock not found", 404, res);
handleSuccess(res, dock);
} catch (err) {
handleSequelizeError(err, res);
}
});
// POST create a shipping dock
router.post("/", async (req, res) => {
try {
const { name } = req.body;
if (!name || !name.trim()) {
return handleError("Name is required and cannot be empty", 400, res);
}
await ShippingDock.create(req.body);
handleSuccess(res, null, 201);
} catch (err) {
handleError(err.message, 500, res);
}
});
// PUT update a shipping dock
router.put("/:id", async (req, res) => {
try {
const { id } = req.params;
const dock = await ShippingDock.findByPk(id);
if (!dock) return handleError("Dock not found", 404, res);
// Validate status separately
if (
req.body.status !== undefined &&
![0, 1].includes(Number(req.body.status))
) {
return handleError("Status must be 0 or 1", 400, res);
}
// Build changes object dynamically
const changes = {};
for (const key in req.body) {
if (req.body[key] !== undefined && !deepEqual(req.body[key], dock[key])) {
changes[key] = req.body[key];
}
}
if (Object.keys(changes).length === 0) {
return handleError("No updated fields", 400, res); // No changes
}
await dock.update(changes);
handleSuccess(res, await ShippingDock.findByPk(id));
} catch (err) {
handleSequelizeError(err, res);
}
});
// DELETE a shipping dock
router.delete("/:id", async (req, res) => {
try {
const deleted = await ShippingDock.destroy({
where: { id: req.params.id },
});
if (deleted === 0) return handleError("Dock not found", 404, res);
handleSuccess(res, null);
} catch (err) {
handleSequelizeError(err, res);
}
});
module.exports = router;
+96
View File
@@ -0,0 +1,96 @@
const express = require("express");
const router = express.Router();
const { Transaction } = require("../models");
const {
handleError,
handleSuccess,
handleSequelizeError,
deepEqual,
} = require("../utils");
// GET all transactions
router.get("/", async (_, res) => {
try {
const transactions = await Transaction.findAll();
handleSuccess(res, transactions);
} catch (err) {
handleSequelizeError(err, res);
}
});
// GET one transaction by id
router.get("/:id", async (req, res) => {
try {
const transaction = await Transaction.findByPk(req.params.id);
if (!transaction) return handleError("Transaction not found", 404, res);
handleSuccess(res, transaction);
} catch (err) {
handleSequelizeError(err, res);
}
});
// POST create a transaction
router.post("/", async (req, res) => {
try {
const { order_id, user_id, shipping_dock_id, amount, notes } = req.body;
if (
order_id === undefined ||
user_id === undefined ||
shipping_dock_id === undefined ||
amount === undefined ||
notes === undefined
) {
return handleError("All fields are required", 400, res);
}
const transaction = await Transaction.create(req.body);
handleSuccess(res, transaction, 201);
} catch (err) {
handleError(err.message, 500, res);
}
});
// PUT update a transaction
router.put("/:id", async (req, res) => {
try {
const { id } = req.params;
const transaction = await Transaction.findByPk(id);
if (!transaction) return handleError("Transaction not found", 404, res);
// Only allow updating amount and notes
const allowedFields = ["amount", "notes"];
const changes = {};
for (const key of allowedFields) {
if (
req.body[key] !== undefined &&
!deepEqual(req.body[key], transaction[key])
) {
changes[key] = req.body[key];
}
}
if (Object.keys(changes).length === 0) {
return handleError("No updated fields", 400, res); // No changes
}
await transaction.update(changes);
handleSuccess(res, await Transaction.findByPk(id));
} catch (err) {
handleSequelizeError(err, res);
}
});
// DELETE a transaction
router.delete("/:id", async (req, res) => {
try {
const deleted = await Transaction.destroy({
where: { id: req.params.id },
});
if (deleted === 0) return handleError("Transaction not found", 404, res);
handleSuccess(res, null, 204); // 204 No Content
} catch (err) {
handleSequelizeError(err, res);
}
});
module.exports = router;
+29
View File
@@ -0,0 +1,29 @@
const handleError = (message, status, res) => {
res.status(status).json({ success: false, message });
};
const handleSuccess = (res, data = null, status = 200) => {
res.status(status).json({
success: true,
...(data && { data }),
});
};
// Centralize Sequelize error handling
const handleSequelizeError = (err, res) => {
if (err.name === "SequelizeValidationError") {
return handleError(err.errors.map((e) => e.message).join(", "), 400, res);
}
handleError(err.message, 500, res);
};
function deepEqual(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
module.exports = {
handleError,
handleSuccess,
handleSequelizeError,
deepEqual,
};
+17 -15
View File
@@ -1,11 +1,12 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");
const codeRouter = require("./routes/code");
const db = require("./models");
var cors = require("cors");
@@ -13,17 +14,18 @@ var cors = require("cors");
var app = express();
app.set("db", db);
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");
app.use(cors());
app.use(logger('dev'));
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, "public")));
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use("/", indexRouter);
app.use("/users", usersRouter);
app.use("/api/v1/code", codeRouter);
// catch 404 and forward to error handler
app.use(function (req, res, next) {
@@ -34,11 +36,11 @@ app.use(function (req, res, next) {
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
res.render("error");
});
module.exports = app;
+16 -19
View File
@@ -4,16 +4,16 @@
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('day-1:server');
var http = require('http');
var app = require("../app");
var debug = require("debug")("day-1:server");
var http = require("http");
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
var port = normalizePort(process.env.PORT || "3000");
app.set("port", port);
/**
* Create HTTP server.
@@ -26,8 +26,8 @@ var server = http.createServer(app);
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
server.on("error", onError);
server.on("listening", onListening);
/**
* Normalize a port into a number, string, or false.
@@ -54,22 +54,20 @@ function normalizePort(val) {
*/
function onError(error) {
if (error.syscall !== 'listen') {
if (error.syscall !== "listen") {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
var bind = typeof port === "string" ? "Pipe " + port : "Port " + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
case "EACCES":
console.error(bind + " requires elevated privileges");
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
case "EADDRINUSE":
console.error(bind + " is already in use");
process.exit(1);
break;
default:
@@ -83,8 +81,7 @@ function onError(error) {
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
debug("Listening on " + bind);
console.log("Server listening on:", bind);
}
+191
View File
@@ -0,0 +1,191 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>A simple, clean, and responsive HTML invoice template</title>
<style>
.invoice-box {
max-width: 800px;
margin: auto;
padding: 30px;
border: 1px solid #eee;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
font-size: 16px;
line-height: 24px;
font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif;
color: #555;
}
.invoice-box table {
width: 100%;
line-height: inherit;
text-align: left;
}
.invoice-box table td {
padding: 5px;
vertical-align: top;
}
.invoice-box table tr td:nth-child(2) {
text-align: right;
}
.invoice-box table tr.top table td {
padding-bottom: 20px;
}
.invoice-box table tr.top table td.title {
font-size: 45px;
line-height: 45px;
color: #333;
}
.invoice-box table tr.information table td {
padding-bottom: 40px;
}
.invoice-box table tr.heading td {
background: #eee;
border-bottom: 1px solid #ddd;
font-weight: bold;
}
.invoice-box table tr.details td {
padding-bottom: 20px;
}
.invoice-box table tr.item td {
border-bottom: 1px solid #eee;
}
.invoice-box table tr.item.last td {
border-bottom: none;
}
.invoice-box table tr.total td:nth-child(2) {
border-top: 2px solid #eee;
font-weight: bold;
}
@media only screen and (max-width: 600px) {
.invoice-box table tr.top table td {
width: 100%;
display: block;
text-align: center;
}
.invoice-box table tr.information table td {
width: 100%;
display: block;
text-align: center;
}
}
/** RTL **/
.invoice-box.rtl {
direction: rtl;
font-family: Tahoma, "Helvetica Neue", "Helvetica", Helvetica, Arial,
sans-serif;
}
.invoice-box.rtl table {
text-align: right;
}
.invoice-box.rtl table tr td:nth-child(2) {
text-align: left;
}
</style>
</head>
<body>
<div class="invoice-box">
<table cellpadding="0" cellspacing="0">
<tr class="top">
<td colspan="2">
<table>
<tr>
<td class="title">
<img
src="https://sparksuite.github.io/simple-html-invoice-template/images/logo.png"
style="width: 100%; max-width: 300px"
/>
</td>
<td>
Invoice #: 123<br />
Created: January 1, 2023<br />
Due: February 1, 2023
</td>
</tr>
</table>
</td>
</tr>
<tr class="information">
<td colspan="2">
<table>
<tr>
<td>
Sparksuite, Inc.<br />
12345 Sunny Road<br />
Sunnyville, CA 12345
</td>
<td>
Acme Corp.<br />
John Doe<br />
john@example.com
</td>
</tr>
</table>
</td>
</tr>
<tr class="heading">
<td>Payment Method</td>
<td>Check #</td>
</tr>
<tr class="details">
<td>Check</td>
<td>1000</td>
</tr>
<tr class="heading">
<td>Item</td>
<td>Price</td>
</tr>
<tr class="item last">
<td>Website design</td>
<td>$300.00</td>
</tr>
<!-- <tr class="item">
<td>Hosting (3 months)</td>
<td>$75.00</td>
</tr>
<tr class="item last">
<td>Domain name (1 year)</td>
<td>$10.00</td>
</tr> -->
<tr class="total">
<td></td>
<td>Total: $385.00</td>
</tr>
</table>
</div>
</body>
</html>
+5 -1
View File
@@ -3,18 +3,22 @@
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
"start": "node ./bin/www",
"dev": "node --watch --env-file=.env ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"express": "~4.16.1",
"html-pdf-node": "^1.0.8",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
"jsonwebtoken": "^8.5.1",
"morgan": "~1.9.1",
"mysql2": "^2.3.3",
"node-input-validator": "^4.5.1",
"qrcode": "^1.5.4",
"sequelize": "^6.15.1"
}
}
+29
View File
@@ -0,0 +1,29 @@
const express = require("express");
const router = express.Router();
const fs = require("fs");
const path = require("path");
const pdf = require("html-pdf-node");
router.get("/:code", async (req, res) => {
const { amount = 1, service = "software service" } = req.query;
// Read the invoice template
const templatePath = path.join(__dirname, "../invoice.html");
let html = fs.readFileSync(templatePath, "utf8");
// Replace placeholders in the template
html = html
.replace("Website design", service)
.replace("$300.00", `$${amount}.00`)
.replace("Total: $385.00", `Total: $${amount}.00`);
// Generate PDF
let file = { content: html };
pdf.generatePdf(file, { format: "A4" }).then((pdfBuffer) => {
res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Disposition", "attachment; filename=invoice.pdf");
res.send(pdfBuffer);
});
});
module.exports = router;
+16 -3
View File
@@ -1,9 +1,22 @@
var express = require('express');
var express = require("express");
var router = express.Router();
const QRCode = require("qrcode");
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
router.get("/", function (req, res, next) {
res.render("index", { title: "Express" });
});
router.get("/code", async function (req, res, next) {
const code = Math.random().toString(36).substring(2, 8); // random code
const qrUrl = `/api/v1/code/${code}?amount=1&service=software%20service`;
const qrData = await QRCode.toDataURL(
`http://localhost:${process.env.PORT || 3000}${qrUrl}`
);
res.render("code", {
qrData,
qrUrl: `http://localhost:${process.env.PORT || 3000}${qrUrl}`,
});
});
module.exports = router;
+6
View File
@@ -0,0 +1,6 @@
extends layout
block content
h1 QR Code
img(src=qrData)
p Link: #{qrUrl}
+94 -94
View File
@@ -1,4 +1,4 @@
'use strict'
"use strict";
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
/**
* App
@@ -8,52 +8,52 @@
* @author Ryan Wong
*
*/
require('dotenv').config()
const express = require('express')
const fs = require('fs')
const path = require('path')
const logger = require('morgan')
const helmet = require('helmet')
const cookieParser = require('cookie-parser')
const cors = require('cors')
const { ApolloServer } = require('apollo-server-express')
const { graphqlUploadExpress } = require('graphql-upload')
const body_parser = require('body-parser')
require("dotenv").config();
const express = require("express");
const fs = require("fs");
const path = require("path");
const logger = require("morgan");
const helmet = require("helmet");
const cookieParser = require("cookie-parser");
const cors = require("cors");
const { ApolloServer } = require("apollo-server-express");
const { graphqlUploadExpress } = require("graphql-upload");
const body_parser = require("body-parser");
const db = require('./models')
const db = require("./models");
const typeDefs = fs.readFileSync(
path.join(__dirname, '/types/schema.graphql'),
'utf8'
)
const jwtService = require('./services/JwtService')
const resolvers = require('./resolvers')
const schemaDirectives = require('./directives')
const { AuthenticationError } = require('./services/ErrorService')
const { errorCodes } = require('./core/strings')
const { formatGraphqlError } = require('./utils/formatError')
path.join(__dirname, "/types/schema.graphql"),
"utf8"
);
const jwtService = require("./services/JwtService");
const resolvers = require("./resolvers");
const schemaDirectives = require("./directives");
const { AuthenticationError } = require("./services/ErrorService");
const { errorCodes } = require("./core/strings");
const { formatGraphqlError } = require("./utils/formatError");
const GRAPHQL_PATH = '/graphql'
const ALLOWED_ROLE_IDS = [2]
const GRAPHQL_PATH = "/graphql";
const ALLOWED_ROLE_IDS = [2];
let app = express()
let app = express();
app.use(logger('dev'))
app.use(logger("dev"));
if (process.env.MODE === 'development') {
logger.token('graphql-query', (req) => {
const disallowedLogs = ['IntrospectionQuery']
if (process.env.MODE === "development") {
logger.token("graphql-query", (req) => {
const disallowedLogs = ["IntrospectionQuery"];
if (req.method === 'POST' && req.originalUrl === GRAPHQL_PATH) {
const { query, variables, operationName } = req.body
if (req.method === "POST" && req.originalUrl === GRAPHQL_PATH) {
const { query, variables, operationName } = req.body;
return !disallowedLogs.includes(operationName)
? `GRAPHQL: \nOperation Name: ${operationName} \nQuery: ${query} \nVariables: ${JSON.stringify(
variables
)}`
: ''
variables
)}`
: "";
}
return ''
})
app.use(logger(':graphql-query'))
return "";
});
app.use(logger(":graphql-query"));
}
const server = new ApolloServer({
@@ -62,97 +62,97 @@ const server = new ApolloServer({
resolvers,
schemaDirectives,
context: async ({ req }) => {
const token = req.headers.authorization
// const token = req.headers.authorization
if (!token) {
throw new AuthenticationError(
'Invalid token',
errorCodes.token.INVALID_TOKEN
)
}
const cleanToken = token.replace('Bearer ', '')
const verify = jwtService.verifyAccessToken(cleanToken)
// if (!token) {
// throw new AuthenticationError(
// 'Invalid token',
// errorCodes.token.INVALID_TOKEN
// )
// }
// const cleanToken = token.replace('Bearer ', '')
// const verify = jwtService.verifyAccessToken(cleanToken)
const roleId = verify?.role_id
const user = verify?.user
const credentialId = verify?.credential_id
// const roleId = verify?.role_id
// const user = verify?.user
// const credentialId = verify?.credential_id
if (!verify || !roleId || !user || !credentialId) {
throw new AuthenticationError(
'Invalid token',
errorCodes.token.INVALID_TOKEN
)
}
// if (!verify || !roleId || !user || !credentialId) {
// throw new AuthenticationError(
// 'Invalid token',
// errorCodes.token.INVALID_TOKEN
// )
// }
if (!ALLOWED_ROLE_IDS.includes(+roleId)) {
throw new AuthenticationError(
'Access Denied',
errorCodes.account.UNAUTHORIZED
)
}
// if (!ALLOWED_ROLE_IDS.includes(+roleId)) {
// throw new AuthenticationError(
// 'Access Denied',
// errorCodes.account.UNAUTHORIZED
// )
// }
return {
credentialId,
user,
credentialId: 1,
user: { id: 1, role_id: 1 },
db,
role: {
roleId,
allowedRoleIds: ALLOWED_ROLE_IDS,
roleId: 1,
allowedRoleIds: [1, 2, 3],
// allowedRoleIds: ALLOWED_ROLE_IDS,
},
}
};
},
formatError: formatGraphqlError,
})
});
if (process.NODE_ENV === 'maintenance') {
app.all('*', (req, res) => {
res.status(503).json({ message: 'website under maintenance' })
})
if (process.NODE_ENV === "maintenance") {
app.all("*", (req, res) => {
res.status(503).json({ message: "website under maintenance" });
});
}
app.set('iocContainer', process.env)
app.set('db', db)
app.use(body_parser.json({ limit: '50mb' }))
app.set("iocContainer", process.env);
app.set("db", db);
app.use(body_parser.json({ limit: "50mb" }));
app.use(express.json())
app.use(express.json());
app.use(
express.urlencoded({
extended: false,
})
)
app.use(cors())
app.set('view engine', 'eta')
app.set('views', path.join(__dirname, '/views'))
app.use(cookieParser())
app.use(helmet())
);
app.use(cors());
app.set("view engine", "eta");
app.set("views", path.join(__dirname, "/views"));
app.use(cookieParser());
app.use(helmet());
app.use(express.static(path.join(__dirname, '/public')))
app.use(express.static(path.join(__dirname, '/uploads')))
app.use(express.static(path.join(__dirname, "/public")));
app.use(express.static(path.join(__dirname, "/uploads")));
app.use(express.static(path.join(__dirname)));
app.use(graphqlUploadExpress({ maxFileSize: 1000000000, maxFiles: 10 }))
server.applyMiddleware({ app, path: GRAPHQL_PATH })
app.use(graphqlUploadExpress({ maxFileSize: 1000000000, maxFiles: 10 }));
server.applyMiddleware({ app, path: GRAPHQL_PATH });
app.use((err, req, res, next) => {
res.locals.message = err.message
res.locals.error = req.app.get('env') === 'development' ? err : {}
res.locals.message = err.message;
res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
res.status(err.status || 500)
res.status(err.status || 500);
res.json({
message: err.message,
})
})
});
});
app.use((_, res, next) => {
return res
.status(400)
.send("<h3 style='text-align:center';>404: Page Not Found!</h3>")
})
.send("<h3 style='text-align:center';>404: Page Not Found!</h3>");
});
module.exports = {
app,
apollo: server,
}
};
+34
View File
@@ -0,0 +1,34 @@
const coreModel = require("./../core/models");
module.exports = (sequelize, DataTypes) => {
const Actor = sequelize.define(
"actor",
{
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
name: DataTypes.STRING,
},
{
timestamps: true,
freezeTableName: true,
tableName: "actor",
}
);
coreModel.call(this, Actor);
Actor.associate = function (models) {
Actor.belongsToMany(models.movie, {
through: models.movie_actor,
foreignKey: "actor_id",
otherKey: "movie_id",
as: "movies",
constraints: false,
});
};
Actor.allowFields = function () {
return ["id", "name"];
};
return Actor;
};
+32
View File
@@ -0,0 +1,32 @@
const coreModel = require("./../core/models");
module.exports = (sequelize, DataTypes) => {
const Director = sequelize.define(
"director",
{
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
name: DataTypes.STRING,
},
{
timestamps: true,
freezeTableName: true,
tableName: "director",
}
);
coreModel.call(this, Director);
Director.associate = function (models) {
Director.hasMany(models.movie, {
foreignKey: "director_id",
as: "movies",
constraints: false,
});
};
Director.allowFields = function () {
return ["id", "name"];
};
return Director;
};
+34
View File
@@ -0,0 +1,34 @@
const coreModel = require("./../core/models");
module.exports = (sequelize, DataTypes) => {
const Genre = sequelize.define(
"genre",
{
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
name: DataTypes.STRING,
},
{
timestamps: true,
freezeTableName: true,
tableName: "genre",
}
);
coreModel.call(this, Genre);
Genre.associate = function (models) {
Genre.belongsToMany(models.movie, {
through: models.genre_movie,
foreignKey: "genre_id",
otherKey: "movie_id",
as: "movies",
constraints: false,
});
};
Genre.allowFields = function () {
return ["id", "name"];
};
return Genre;
};
+38
View File
@@ -0,0 +1,38 @@
const coreModel = require("./../core/models");
module.exports = (sequelize, DataTypes) => {
const GenreMovie = sequelize.define(
"genre_movie",
{
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
movie_id: DataTypes.INTEGER,
genre_id: DataTypes.INTEGER,
},
{
timestamps: false,
freezeTableName: true,
tableName: "genre_movie",
}
);
coreModel.call(this, GenreMovie);
GenreMovie.associate = function (models) {
GenreMovie.belongsTo(models.movie, {
foreignKey: "movie_id",
as: "movie",
constraints: false,
});
GenreMovie.belongsTo(models.genre, {
foreignKey: "genre_id",
as: "genre",
constraints: false,
});
};
GenreMovie.allowFields = function () {
return ["id", "movie_id", "genre_id"];
};
return GenreMovie;
};
+44 -34
View File
@@ -1,4 +1,4 @@
'use strict';
"use strict";
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
/**
* Sequelize File
@@ -8,49 +8,59 @@
* @author Ryan Wong
*
*/
const fs = require('fs');
const path = require('path');
let Sequelize = require('sequelize');
const { DataTypes } = require('sequelize');
const fs = require("fs");
const path = require("path");
let Sequelize = require("sequelize");
const { DataTypes } = require("sequelize");
const basename = path.basename(__filename);
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_11",
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("Tables synced successfully!"))
.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 +76,4 @@ Object.keys(db).forEach((modelName) => {
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;
module.exports = db;
+55
View File
@@ -0,0 +1,55 @@
const coreModel = require("./../core/models");
module.exports = (sequelize, DataTypes) => {
const Movie = sequelize.define(
"movie",
{
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
title: DataTypes.STRING,
director_id: DataTypes.INTEGER,
main_genre: DataTypes.STRING,
status: DataTypes.INTEGER,
review: DataTypes.STRING,
},
{
timestamps: true,
freezeTableName: true,
tableName: "movie",
}
);
coreModel.call(this, Movie);
Movie.associate = function (models) {
Movie.belongsTo(models.director, {
foreignKey: "director_id",
as: "director",
constraints: false,
});
Movie.hasMany(models.review, {
foreignKey: "movie_id",
as: "reviews",
constraints: false,
});
Movie.belongsToMany(models.actor, {
through: models.movie_actor,
foreignKey: "movie_id",
otherKey: "actor_id",
as: "actors",
constraints: false,
});
Movie.belongsToMany(models.genre, {
through: models.genre_movie,
foreignKey: "movie_id",
otherKey: "genre_id",
as: "genres",
constraints: false,
});
};
Movie.allowFields = function () {
return ["id", "title", "director_id", "main_genre", "status", "review"];
};
return Movie;
};
+38
View File
@@ -0,0 +1,38 @@
const coreModel = require("./../core/models");
module.exports = (sequelize, DataTypes) => {
const MovieActor = sequelize.define(
"movie_actor",
{
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
actor_id: DataTypes.INTEGER,
movie_id: DataTypes.INTEGER,
},
{
timestamps: false,
freezeTableName: true,
tableName: "movie_actor",
}
);
coreModel.call(this, MovieActor);
MovieActor.associate = function (models) {
MovieActor.belongsTo(models.actor, {
foreignKey: "actor_id",
as: "actor",
constraints: false,
});
MovieActor.belongsTo(models.movie, {
foreignKey: "movie_id",
as: "movie",
constraints: false,
});
};
MovieActor.allowFields = function () {
return ["id", "actor_id", "movie_id"];
};
return MovieActor;
};
+33
View File
@@ -0,0 +1,33 @@
const coreModel = require("./../core/models");
module.exports = (sequelize, DataTypes) => {
const Review = sequelize.define(
"review",
{
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
notes: DataTypes.STRING,
movie_id: DataTypes.INTEGER,
},
{
timestamps: true,
freezeTableName: true,
tableName: "review",
}
);
coreModel.call(this, Review);
Review.associate = function (models) {
Review.belongsTo(models.movie, {
foreignKey: "movie_id",
as: "movie",
constraints: false,
});
};
Review.allowFields = function () {
return ["id", "notes", "movie_id"];
};
return Review;
};
+4 -1
View File
@@ -2,7 +2,10 @@
"name": "day11",
"version": "1.0.0",
"description": "",
"scripts": {},
"scripts": {
"start": "node server.js",
"dev": "node --watch --env-file=.env server.js"
},
"keywords": [],
"author": "Ryan Wong",
"private": true,
+63
View File
@@ -0,0 +1,63 @@
const db = require("../../models");
const ActorResolvers = {
Query: {
async getActor(_, { id }) {
try {
const actor = await db.actor.findByPk(id, {
include: [
{ model: db.movie, as: "movies", through: { attributes: [] } },
],
});
if (!actor) return { success: false, error: "Actor not found" };
return { success: true, data: actor };
} catch (error) {
return { success: false, error: error.message };
}
},
async getAllActors() {
try {
const actors = await db.actor.findAll({
include: [
{ model: db.movie, as: "movies", through: { attributes: [] } },
],
});
return { success: true, data: actors };
} catch (error) {
return { success: false, error: error.message };
}
},
},
Mutation: {
async createActor(_, args) {
try {
const actor = await db.actor.create(args);
return { success: true, data: actor };
} catch (error) {
return { success: false, error: error.message };
}
},
async updateActor(_, { id, ...args }) {
try {
const actor = await db.actor.findByPk(id);
if (!actor) return { success: false, error: "Actor not found" };
await actor.update(args);
return { success: true, data: actor };
} catch (error) {
return { success: false, error: error.message };
}
},
async deleteActor(_, { id }) {
try {
const actor = await db.actor.findByPk(id);
if (!actor) return { success: false, error: "Actor not found" };
await actor.destroy();
return { success: true, data: actor };
} catch (error) {
return { success: false, error: error.message };
}
},
},
};
module.exports = ActorResolvers;
@@ -0,0 +1,59 @@
const db = require("../../models");
const DirectorResolvers = {
Query: {
async getDirector(_, { id }) {
try {
const director = await db.director.findByPk(id, {
include: [{ model: db.movie, as: "movies" }],
});
if (!director) return { success: false, error: "Director not found" };
return { success: true, data: director };
} catch (error) {
return { success: false, error: error.message };
}
},
async getAllDirectors() {
try {
const directors = await db.director.findAll({
include: [{ model: db.movie, as: "movies" }],
});
return { success: true, data: directors };
} catch (error) {
return { success: false, error: error.message };
}
},
},
Mutation: {
async createDirector(_, args) {
try {
const director = await db.director.create(args);
return { success: true, data: director };
} catch (error) {
return { success: false, error: error.message };
}
},
async updateDirector(_, { id, ...args }) {
try {
const director = await db.director.findByPk(id);
if (!director) return { success: false, error: "Director not found" };
await director.update(args);
return { success: true, data: director };
} catch (error) {
return { success: false, error: error.message };
}
},
async deleteDirector(_, { id }) {
try {
const director = await db.director.findByPk(id);
if (!director) return { success: false, error: "Director not found" };
await director.destroy();
return { success: true, data: director };
} catch (error) {
return { success: false, error: error.message };
}
},
},
};
module.exports = DirectorResolvers;
+121
View File
@@ -0,0 +1,121 @@
const { fn, col } = require("sequelize");
const db = require("../../models");
const MovieResolvers = {
Query: {
async getMovie(_, { id }) {
try {
const movie = await db.movie.findByPk(id, {
include: [
{ model: db.director, as: "director" },
{ model: db.review, as: "reviews" },
{ model: db.actor, as: "actors", through: { attributes: [] } },
{ model: db.genre, as: "genres", through: { attributes: [] } },
],
});
if (!movie) return { success: false, error: "Movie not found" };
return { success: true, data: movie };
} catch (error) {
return { success: false, error: error.message };
}
},
async getAllMovies() {
try {
const movies = await db.movie.findAll({
include: [
{ model: db.director, as: "director" },
{ model: db.review, as: "reviews" },
{ model: db.actor, as: "actors", through: { attributes: [] } },
{ model: db.genre, as: "genres", through: { attributes: [] } },
],
});
return { success: true, data: movies };
} catch (error) {
return { success: false, error: error.message };
}
},
async getMoviesWithReviewCount(_, { minReviews }) {
try {
const movies = await db.movie.findAll({
// attributes: {
// include: [[fn("COUNT", col("reviews.id")), "reviewCount"]],
// },
include: [
{ model: db.review, as: "reviews" },
{ model: db.director, as: "director" },
{ model: db.actor, as: "actors", through: { attributes: [] } },
{ model: db.genre, as: "genres", through: { attributes: [] } },
],
// having: literal(`COUNT(reviews.id) > ${minReviews}`),
});
const filtered = movies.filter(
(m) => (m.reviews ? m.reviews.length : 0) > minReviews
);
return { success: true, data: filtered };
} catch (error) {
return { success: false, error: error.message };
}
},
},
Mutation: {
async createMovie(_, args) {
try {
const movie = await db.movie.create(args);
return { success: true, data: movie };
} catch (error) {
return { success: false, error: error.message };
}
},
async updateMovie(_, { id, ...args }) {
try {
const movie = await db.movie.findByPk(id);
if (!movie) return { success: false, error: "Movie not found" };
await movie.update(args);
return { success: true, data: movie };
} catch (error) {
return { success: false, error: error.message };
}
},
async deleteMovie(_, { id }) {
try {
const movie = await db.movie.findByPk(id);
if (!movie) return { success: false, error: "Movie not found" };
await movie.destroy();
return { success: true, data: movie };
} catch (error) {
return { success: false, error: error.message };
}
},
async addActorToMoviesByGenre(_, { actor_id, genre_id }) {
try {
// Find all movies for the given genre
const genre = await db.genre.findByPk(genre_id, {
include: [{ model: db.movie, as: "movies" }],
});
if (!genre) return { success: false, error: "Genre not found" };
const movies = genre.movies;
for (const movie of movies) {
await db.movie_actor.findOrCreate({
where: { movie_id: movie.id, actor_id },
defaults: { movie_id: movie.id, actor_id },
});
}
// Return updated movies
const updatedMovies = await db.movie.findAll({
where: { id: movies.map((m) => m.id) },
include: [
{ model: db.director, as: "director" },
{ model: db.review, as: "reviews" },
{ model: db.actor, as: "actors", through: { attributes: [] } },
{ model: db.genre, as: "genres", through: { attributes: [] } },
],
});
return { success: true, data: updatedMovies };
} catch (error) {
return { success: false, error: error.message };
}
},
},
};
module.exports = MovieResolvers;
+59
View File
@@ -0,0 +1,59 @@
const db = require("../../models");
const ReviewResolvers = {
Query: {
async getReview(_, { id }) {
try {
const review = await db.review.findByPk(id, {
include: [{ model: db.movie, as: "movie" }],
});
if (!review) return { success: false, error: "Review not found" };
return { success: true, data: review };
} catch (error) {
return { success: false, error: error.message };
}
},
async getAllReviews() {
try {
const reviews = await db.review.findAll({
include: [{ model: db.movie, as: "movie" }],
});
return { success: true, data: reviews };
} catch (error) {
return { success: false, error: error.message };
}
},
},
Mutation: {
async createReview(_, args) {
try {
const review = await db.review.create(args);
return { success: true, data: review };
} catch (error) {
return { success: false, error: error.message };
}
},
async updateReview(_, { id, ...args }) {
try {
const review = await db.review.findByPk(id);
if (!review) return { success: false, error: "Review not found" };
await review.update(args);
return { success: true, data: review };
} catch (error) {
return { success: false, error: error.message };
}
},
async deleteReview(_, { id }) {
try {
const review = await db.review.findByPk(id);
if (!review) return { success: false, error: "Review not found" };
await review.destroy();
return { success: true, data: review };
} catch (error) {
return { success: false, error: error.message };
}
},
},
};
module.exports = ReviewResolvers;
+35 -24
View File
@@ -7,47 +7,58 @@
* @author Ryan Wong
*
*/
const { GraphQLUpload } = require('graphql-upload');
const { GraphQLUpload } = require("graphql-upload");
const updateUserResolver = require('./update/updateUser');
const singleUserResolver = require('./single/singleUser');
const typeUserResolver = require('./type/typeUser');
const updateUserResolver = require("./update/updateUser");
const singleUserResolver = require("./single/singleUser");
const typeUserResolver = require("./type/typeUser");
const createLinkResolver = require('./create/createLink');
const typeLinkResolver = require('./type/typeLink');
const singleLinkResolver = require('./single/singleLink');
const deactivateAllLinksResolver = require('./delete/deactivateAllLinks');
const createLinkResolver = require("./create/createLink");
const typeLinkResolver = require("./type/typeLink");
const singleLinkResolver = require("./single/singleLink");
const deactivateAllLinksResolver = require("./delete/deactivateAllLinks");
const calendarResolver = require('./custom/calendar');
const noteResolver = require('./custom/note');
const customImageResolver = require('./custom/image');
const uploadFileMutationResolver = require('./custom/uploadFile');
const connectionStepsResolver = require('./custom/connectionSteps');
// const calendarResolver = require("./custom/calendar");
// const noteResolver = require("./custom/note");
// const customImageResolver = require("./custom/image");
// const uploadFileMutationResolver = require("./custom/uploadFile");
// const connectionStepsResolver = require("./custom/connectionSteps");
const movieResolvers = require("./custom/movieResolvers");
const reviewResolvers = require("./custom/reviewResolvers");
const directorResolvers = require("./custom/directorResolvers");
const actorResolvers = require("./custom/actorResolvers");
module.exports = {
Upload: GraphQLUpload,
Query: {
user: singleUserResolver,
link: singleLinkResolver,
...calendarResolver.Query,
...customImageResolver.Query,
...noteResolver.Query,
...connectionStepsResolver.Query
// ...calendarResolver.Query,
// ...customImageResolver.Query,
// ...noteResolver.Query,
// ...connectionStepsResolver.Query,
...movieResolvers.Query,
...reviewResolvers.Query,
...directorResolvers.Query,
...actorResolvers.Query,
},
Mutation: {
updateUser: updateUserResolver,
createLink: createLinkResolver,
deactivateAllLinks: deactivateAllLinksResolver,
uploadFile: uploadFileMutationResolver,
...calendarResolver.Mutation,
...customImageResolver.Mutation,
...noteResolver.Mutation,
// uploadFile: uploadFileMutationResolver,
// ...calendarResolver.Mutation,
// ...customImageResolver.Mutation,
// ...noteResolver.Mutation,
...movieResolvers.Mutation,
...reviewResolvers.Mutation,
...directorResolvers.Mutation,
...actorResolvers.Mutation,
},
...calendarResolver.Type,
...noteResolver.Type,
// ...calendarResolver.Type,
// ...noteResolver.Type,
User: typeUserResolver,
Link: typeLinkResolver,
+13 -5
View File
@@ -1,4 +1,4 @@
'use strict';
"use strict";
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
/**
* Server
@@ -8,11 +8,19 @@
* @author Ryan Wong
*
*/
const { app, apollo } = require('./app');
const { app, apollo } = require("./app");
const PORT = 3001;
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log('Server running at ', true ? `http://localhost:${PORT}` : 'process.env.BASE_URL');
console.log('GraphQL running at ', true ? `http://localhost:${PORT}${apollo.graphqlPath}` : `${'process.env.BASE_URL'}${apollo.graphqlPath}`);
console.log(
"Server running at ",
true ? `http://localhost:${PORT}` : "process.env.BASE_URL"
);
console.log(
"GraphQL running at ",
true
? `http://localhost:${PORT}${apollo.graphqlPath}`
: `${"process.env.BASE_URL"}${apollo.graphqlPath}`
);
});
+147
View File
@@ -211,3 +211,150 @@ type Mutation {
uploadFile(file: Upload!): FileUploadResponse!
}
type Movie {
id: ID!
title: String
director_id: Int
main_genre: String
status: Int
review: String
director: Director
reviews: [Review]
actors: [Actor]
genres: [Genre]
}
type Review {
id: ID!
notes: String
movie_id: Int
movie: Movie
}
type Director {
id: ID!
name: String
movies: [Movie]
}
type Actor {
id: ID!
name: String
movies: [Movie]
}
type MovieActor {
id: ID!
actor_id: Int
movie_id: Int
actor: Actor
movie: Movie
}
type Genre {
id: ID!
name: String
movies: [Movie]
}
type GenreMovie {
id: ID!
movie_id: Int
genre_id: Int
movie: Movie
genre: Genre
}
type MovieResponse {
success: Boolean!
data: Movie
error: String
}
type AllMoviesResponse {
success: Boolean!
data: [Movie]
error: String
}
type ReviewResponse {
success: Boolean!
data: Review
error: String
}
type AllReviewsResponse {
success: Boolean!
data: [Review]
error: String
}
type DirectorResponse {
success: Boolean!
data: Director
error: String
}
type AllDirectorsResponse {
success: Boolean!
data: [Director]
error: String
}
type ActorResponse {
success: Boolean!
data: Actor
error: String
}
type AllActorsResponse {
success: Boolean!
data: [Actor]
error: String
}
extend type Query {
getMovie(id: ID!): MovieResponse!
getAllMovies: AllMoviesResponse!
getReview(id: ID!): ReviewResponse!
getAllReviews: AllReviewsResponse!
getDirector(id: ID!): DirectorResponse!
getAllDirectors: AllDirectorsResponse!
getActor(id: ID!): ActorResponse!
getAllActors: AllActorsResponse!
getMoviesWithReviewCount(minReviews: Int!): AllMoviesResponse!
}
extend type Mutation {
createMovie(
title: String!
director_id: Int
main_genre: String
status: Int
review: String
): MovieResponse!
updateMovie(
id: ID!
title: String
director_id: Int
main_genre: String
status: Int
review: String
): MovieResponse!
deleteMovie(id: ID!): MovieResponse!
createReview(notes: String!, movie_id: Int!): ReviewResponse!
updateReview(id: ID!, notes: String, movie_id: Int): ReviewResponse!
deleteReview(id: ID!): ReviewResponse!
createDirector(name: String!): DirectorResponse!
updateDirector(id: ID!, name: String): DirectorResponse!
deleteDirector(id: ID!): DirectorResponse!
createActor(name: String!): ActorResponse!
updateActor(id: ID!, name: String): ActorResponse!
deleteActor(id: ID!): ActorResponse!
addActorToMoviesByGenre(actor_id: Int!, genre_id: Int!): AllMoviesResponse!
}
+44 -34
View File
@@ -1,4 +1,4 @@
'use strict';
"use strict";
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
/**
* Sequelize File
@@ -8,49 +8,59 @@
* @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_13",
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("Tables synced successfully!"))
.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 +76,4 @@ Object.keys(db).forEach((modelName) => {
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;
module.exports = db;
+24
View File
@@ -0,0 +1,24 @@
module.exports = (sequelize, DataTypes) => {
const order = sequelize.define("order", {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
total: { type: DataTypes.INTEGER, allowNull: false },
stripe_id: {
type: DataTypes.STRING,
allowNull: false,
},
product_id: {
type: DataTypes.INTEGER,
allowNull: false,
},
status: {
type: DataTypes.INTEGER,
allowNull: false,
},
});
return order;
};
+15
View File
@@ -0,0 +1,15 @@
module.exports = (sequelize, DataTypes) => {
const product = sequelize.define("product", {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
title: DataTypes.STRING,
description: DataTypes.STRING,
price: DataTypes.INTEGER,
image: DataTypes.STRING,
});
return product;
};
+4 -2
View File
@@ -3,7 +3,8 @@
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
"start": "node ./bin/www",
"dev": "node --watch --env-file=.env ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
@@ -14,6 +15,7 @@
"jade": "~1.11.0",
"morgan": "~1.9.1",
"mysql2": "^2.3.3",
"sequelize": "^6.15.1"
"sequelize": "^6.15.1",
"stripe": "^18.3.0"
}
}
+107 -3
View File
@@ -1,9 +1,113 @@
var express = require('express');
var express = require("express");
var router = express.Router();
var db = require("../models");
const stripe = require("stripe")(process.env.STRIPE_TEST_KEY); // Replace with your Stripe test secret key
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
router.get("/", async function (req, res, next) {
try {
const products = await db.product.findAll();
res.render("index", { title: "Products", products });
} catch (err) {
next(err);
}
});
router.get("/product/:id", async function (req, res, next) {
try {
const product = await db.product.findByPk(req.params.id);
if (!product) return res.status(404).send("Product not found");
res.render("product", { title: product.title, product });
} catch (err) {
next(err);
}
});
router.post("/buy/:id", async function (req, res, next) {
try {
const product = await db.product.findByPk(req.params.id);
if (!product) return res.status(404).send("Product not found");
const session = await stripe.checkout.sessions.create({
payment_method_types: ["card"],
line_items: [
{
price_data: {
currency: "usd",
product_data: {
name: product.title,
images: product.image ? [product.image] : [],
},
unit_amount: Math.round(product.price * 100),
},
quantity: 1,
},
],
mode: "payment",
success_url:
req.protocol +
"://" +
req.get("host") +
"/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url:
req.protocol + "://" + req.get("host") + "/product/" + product.id,
});
res.redirect(303, session.url);
} catch (err) {
next(err);
}
});
router.get("/success", async function (req, res, next) {
try {
const session_id = req.query.session_id;
if (!session_id) return res.status(400).send("Missing session ID");
const session = await stripe.checkout.sessions.retrieve(session_id);
// Find or create order
let order = await db.order.findOne({ where: { stripe_id: session.id } });
if (!order) {
// Get product_id from session metadata or line_items
const lineItems = await stripe.checkout.sessions.listLineItems(
session.id,
{ limit: 1 }
);
const productName =
lineItems.data[0].description || lineItems.data[0].price.product;
const dbProduct = await db.product.findOne({
where: { title: productName },
});
await db.order.create({
product_id: dbProduct ? dbProduct.id : null,
total: session.amount_total,
stripe_id: session.id,
status: session.payment_status === "paid" ? 1 : 0,
});
}
res.render("success", { title: "Thank You", session });
} catch (err) {
next(err);
}
});
router.get("/create-product", function (req, res) {
res.render("create-product", { title: "Add Product" });
});
router.post("/create-product", async function (req, res, next) {
try {
const { title, description, price, image } = req.body;
if (!title || !description || !price) {
return res
.status(400)
.render("create-product", {
title: "Add Product",
error: "All fields except image are required.",
});
}
await db.product.create({ title, description, price, image });
res.redirect("/");
} catch (err) {
next(err);
}
});
module.exports = router;
+22
View File
@@ -0,0 +1,22 @@
extends layout
block content
.container.mt-5
h2 Add Product
if error
.alert.alert-danger= error
form(method="POST" action="/create-product")
.form-group
label(for="title") Title
input.form-control(type="text" name="title" id="title" required)
.form-group
label(for="description") Description
textarea.form-control(name="description" id="description" required)
.form-group
label(for="price") Price (in dollars)
input.form-control(type="number" name="price" id="price" min="0" step="0.01" required)
.form-group
label(for="image") Image URL
input.form-control(type="text" name="image" id="image")
button.btn.btn-primary(type="submit") Add Product
a.btn.btn-secondary.ml-2(href="/") Cancel
+16 -1
View File
@@ -2,4 +2,19 @@ extends layout
block content
h1= title
p Welcome to #{title}
a.btn.btn-success.mb-4(href="/create-product") Create Product
if products && products.length
.row
each product in products
.col-md-4.mb-4
.card
if product.image
img.card-img-top(src=product.image, alt=product.title, style="width:100px;height:100px;object-fit:contain;")
.card-body
h5.card-title= product.title
p.card-text= product.description
p.card-text
strong $#{product.price}
a.btn.btn-primary(href=`/product/${product.id}`) View Details
else
p No products found.
+1
View File
@@ -2,6 +2,7 @@ doctype html
html
head
title= title
link(rel='stylesheet', href='https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css')
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content
+15
View File
@@ -0,0 +1,15 @@
extends layout
block content
.container.mt-5
.row
.col-md-6
if product.image
img.img-fluid(src=product.image, alt=product.title style="width:100px;height:100px;object-fit:contain;")
.col-md-6
h2= product.title
p= product.description
h4.text-success $#{product.price}
form(action=`/buy/${product.id}` method="POST")
button.btn.btn-success(type="submit") Buy Now
a.btn.btn-secondary.mt-3(href="/") Back to Products
+23
View File
@@ -0,0 +1,23 @@
extends layout
block content
.container.mt-5
.alert.alert-success
h2 Thank you for your purchase!
p Your payment was successful.
if session
h4 Payment Details
table.table
tr
th Session ID
td= session.id
tr
th Payment Status
td= session.payment_status
tr
th Amount Total
td $#{(session.amount_total / 100).toFixed(2)}
tr
th Payment Method
td= session.payment_method_types && session.payment_method_types[0]
a.btn.btn-primary.mt-4(href="/") Back to Products
+74
View File
@@ -0,0 +1,74 @@
const fs = require("fs");
const path = require("path");
class ControllerBuilder {
static build() {
const config = require("./configuration.json");
const controllerDir = path.join(__dirname, "release/controllers");
// Create release/controllers directory
if (!fs.existsSync(controllerDir))
fs.mkdirSync(controllerDir, { recursive: true });
config.model.forEach((model) => {
const controllerCode = `const express = require('express');
const router = express.Router();
const model = require('../models/${model.name}.model.js');
// CREATE
router.post('/', async (req, res) => {
try {
const data = await model.create(req.body);
res.status(201).json(data);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// READ ALL
router.get('/', async (req, res) => {
try {
const data = await model.findAll();
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// UPDATE
router.put('/:id', async (req, res) => {
try {
const updated = await model.update(req.body, {
where: { id: req.params.id }
});
if (updated[0] === 0) return res.status(404).json({ error: 'Not found' });
res.json(await model.findByPk(req.params.id));
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// DELETE
router.delete('/:id', async (req, res) => {
try {
const deleted = await model.destroy({
where: { id: req.params.id }
});
if (!deleted) return res.status(404).json({ error: 'Not found' });
res.status(204).send();
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;`;
fs.writeFileSync(
path.join(controllerDir, `${model.name}.controller.js`),
controllerCode
);
});
}
}
module.exports = ControllerBuilder;
+34 -9
View File
@@ -1,13 +1,38 @@
let fs = require('fs');
const fs = require("fs");
const path = require("path");
function Model_builder() {
let config = fs.readFileSync('configuration.json');
class ModelBuilder {
static build() {
const config = require("./configuration.json");
const modelDir = path.join(__dirname, "release/models");
this.build = function () {
//generate files and put it into release folder
//Copy initialize files into release folder
//TODO
// Create release/models directory
if (!fs.existsSync(modelDir)) fs.mkdirSync(modelDir, { recursive: true });
config.model.forEach((model) => {
const modelCode = `const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
return sequelize.define('${model.name}', {
${model.field
.map(
(field) => `
${field[0]}: {
type: DataTypes.${field[1].toUpperCase()},
allowNull: ${field[3] === "required" ? "false" : "true"}
}`
)
.join(",")}
}, {
timestamps: true
});
};`;
fs.writeFileSync(
path.join(modelDir, `${model.name}.model.js`),
modelCode
);
});
}
}
return this;
}
module.exports = ModelBuilder;
+34 -34
View File
@@ -1,37 +1,37 @@
{
"model": [
{
"name": "location",
"field: [
["id", "integer", "ID", "required"],
["name", "string", "Name", "required"],
["status", "integer", "Status", "required"],
"model": [
{
"name": "location",
"field": [
["id", "integer", "ID", "required"],
["name", "string", "Name", "required"],
["status", "integer", "Status", "required"]
]
},
{
"name": "email",
"field": [
["id", "integer", "ID", "required"],
["email", "string", "Email", "required"],
["status", "integer", "Status", "required"]
]
},
{
"name": "sms",
"field": [
["id", "integer", "ID", "required"],
["phone", "string", "Phone", "required"],
["status", "integer", "Status", "required"]
]
},
{
"name": "user",
"field": [
["id", "integer", "ID", "required"],
["name", "string", "Name", "required"],
["email", "string", "Email", "required"],
["status", "integer", "Status", "required"]
]
}
]
},
{
"name": "email",
"field: [
["id", "integer", "ID", "required"],
["email", "string", "Email", "required"],
["status", "integer", "Status", "required"],
]
},
{
"name": "sms",
"field: [
["id", "integer", "ID", "required"],
["phone", "string", "Phone", "required"],
["status", "integer", "Status", "required"],
]
},
{
"name": "user",
"field: [
["id", "integer", "ID", "required"],
["name", "string", "Name", "required"],
["email", "string", "Email", "required"],
["status", "integer", "Status", "required"],
]
}
]
}
+7
View File
@@ -0,0 +1,7 @@
const Model_builder = require("./Model_builder");
const Controller_builder = require("./Controller_builder");
Model_builder.build();
Controller_builder.build();
console.log("Model and controller files generated in /release");
@@ -0,0 +1,51 @@
const express = require('express');
const router = express.Router();
const model = require('../models/email.model.js');
// CREATE
router.post('/', async (req, res) => {
try {
const data = await model.create(req.body);
res.status(201).json(data);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// READ ALL
router.get('/', async (req, res) => {
try {
const data = await model.findAll();
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// UPDATE
router.put('/:id', async (req, res) => {
try {
const updated = await model.update(req.body, {
where: { id: req.params.id }
});
if (updated[0] === 0) return res.status(404).json({ error: 'Not found' });
res.json(await model.findByPk(req.params.id));
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// DELETE
router.delete('/:id', async (req, res) => {
try {
const deleted = await model.destroy({
where: { id: req.params.id }
});
if (!deleted) return res.status(404).json({ error: 'Not found' });
res.status(204).send();
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
@@ -0,0 +1,51 @@
const express = require('express');
const router = express.Router();
const model = require('../models/location.model.js');
// CREATE
router.post('/', async (req, res) => {
try {
const data = await model.create(req.body);
res.status(201).json(data);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// READ ALL
router.get('/', async (req, res) => {
try {
const data = await model.findAll();
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// UPDATE
router.put('/:id', async (req, res) => {
try {
const updated = await model.update(req.body, {
where: { id: req.params.id }
});
if (updated[0] === 0) return res.status(404).json({ error: 'Not found' });
res.json(await model.findByPk(req.params.id));
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// DELETE
router.delete('/:id', async (req, res) => {
try {
const deleted = await model.destroy({
where: { id: req.params.id }
});
if (!deleted) return res.status(404).json({ error: 'Not found' });
res.status(204).send();
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
@@ -0,0 +1,51 @@
const express = require('express');
const router = express.Router();
const model = require('../models/sms.model.js');
// CREATE
router.post('/', async (req, res) => {
try {
const data = await model.create(req.body);
res.status(201).json(data);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// READ ALL
router.get('/', async (req, res) => {
try {
const data = await model.findAll();
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// UPDATE
router.put('/:id', async (req, res) => {
try {
const updated = await model.update(req.body, {
where: { id: req.params.id }
});
if (updated[0] === 0) return res.status(404).json({ error: 'Not found' });
res.json(await model.findByPk(req.params.id));
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// DELETE
router.delete('/:id', async (req, res) => {
try {
const deleted = await model.destroy({
where: { id: req.params.id }
});
if (!deleted) return res.status(404).json({ error: 'Not found' });
res.status(204).send();
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
@@ -0,0 +1,51 @@
const express = require('express');
const router = express.Router();
const model = require('../models/user.model.js');
// CREATE
router.post('/', async (req, res) => {
try {
const data = await model.create(req.body);
res.status(201).json(data);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// READ ALL
router.get('/', async (req, res) => {
try {
const data = await model.findAll();
res.json(data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// UPDATE
router.put('/:id', async (req, res) => {
try {
const updated = await model.update(req.body, {
where: { id: req.params.id }
});
if (updated[0] === 0) return res.status(404).json({ error: 'Not found' });
res.json(await model.findByPk(req.params.id));
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// DELETE
router.delete('/:id', async (req, res) => {
try {
const deleted = await model.destroy({
where: { id: req.params.id }
});
if (!deleted) return res.status(404).json({ error: 'Not found' });
res.status(204).send();
} catch (error) {
res.status(500).json({ error: error.message });
}
});
module.exports = router;
+20
View File
@@ -0,0 +1,20 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
return sequelize.define('email', {
id: {
type: DataTypes.INTEGER,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false
},
status: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
timestamps: true
});
};
+20
View File
@@ -0,0 +1,20 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
return sequelize.define('location', {
id: {
type: DataTypes.INTEGER,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
status: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
timestamps: true
});
};
+20
View File
@@ -0,0 +1,20 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
return sequelize.define('sms', {
id: {
type: DataTypes.INTEGER,
allowNull: false
},
phone: {
type: DataTypes.STRING,
allowNull: false
},
status: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
timestamps: true
});
};
+24
View File
@@ -0,0 +1,24 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
return sequelize.define('user', {
id: {
type: DataTypes.INTEGER,
allowNull: false
},
name: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false
},
status: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
timestamps: true
});
};
+1 -6
View File
@@ -3,9 +3,4 @@
## Instructions
- setup project
- clone to your github
- Open figma file https://www.figma.com/file/Zr1EutaLsykPcADfC3el7r/ai-marketing-site-sample?node-id=0%3A1
- Need you to finish the site by end of day both mobile and desktop responsive
- Use bootstrap 4
- CSS should be clean, not using inline style CSS but proper classes
- deploy site using github pages https://www.codecademy.com/articles/f1-u3-github-pages
- implement https://www.treeql.org/ manually
+100
View File
@@ -0,0 +1,100 @@
require("dotenv").config();
const express = require("express");
const bodyParser = require("body-parser");
const { User, Post, sequelize } = require("./models");
const app = express();
app.use(bodyParser.json());
// Helper to get model by resource name
const models = { users: User, posts: Post };
// Sync DB
sequelize.sync();
// TreeQL-style dynamic GET
app.get("/:resource/:id?/:subresource?", async (req, res) => {
const { resource, id, subresource } = req.params;
const Model = models[resource];
if (!Model) return res.status(404).json({ error: "Resource not found" });
try {
if (id) {
const instance = await Model.findByPk(
id,
subresource ? { include: subresource } : {}
);
if (!instance) return res.status(404).json({ error: "Not found" });
if (subresource && instance[subresource]) {
return res.json(instance[subresource]);
}
return res.json(instance);
} else {
const all = await Model.findAll();
return res.json(all);
}
} catch (e) {
return res.status(500).json({ error: e.message });
}
});
// TreeQL-style POST (create resource or subresource)
app.post("/:resource/:id?/:subresource?", async (req, res) => {
const { resource, id, subresource } = req.params;
const Model = models[resource];
if (!Model) return res.status(404).json({ error: "Resource not found" });
try {
if (id && subresource) {
// e.g. POST /users/1/posts
const parent = await models[resource].findByPk(id);
if (!parent) return res.status(404).json({ error: "Parent not found" });
const childModel = models[subresource];
if (!childModel)
return res.status(404).json({ error: "Subresource not found" });
const child = await childModel.create({ ...req.body, UserId: id });
return res.status(201).json(child);
} else {
// e.g. POST /users
const instance = await Model.create(req.body);
return res.status(201).json(instance);
}
} catch (e) {
return res.status(500).json({ error: e.message });
}
});
// TreeQL-style PUT (update resource)
app.put("/:resource/:id", async (req, res) => {
const { resource, id } = req.params;
const Model = models[resource];
if (!Model) return res.status(404).json({ error: "Resource not found" });
try {
const instance = await Model.findByPk(id);
if (!instance) return res.status(404).json({ error: "Not found" });
await instance.update(req.body);
return res.json(instance);
} catch (e) {
return res.status(500).json({ error: e.message });
}
});
// TreeQL-style DELETE (delete resource)
app.delete("/:resource/:id", async (req, res) => {
const { resource, id } = req.params;
const Model = models[resource];
if (!Model) return res.status(404).json({ error: "Resource not found" });
try {
const instance = await Model.findByPk(id);
if (!instance) return res.status(404).json({ error: "Not found" });
await instance.destroy();
return res.json({ success: true });
} catch (e) {
return res.status(500).json({ error: e.message });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`TreeQL API running on port ${PORT}`));
+9
View File
@@ -0,0 +1,9 @@
require("dotenv").config();
const { Sequelize } = require("sequelize");
const sequelize = new Sequelize("day_15", "root", process.env.DB_PASSWORD, {
host: "localhost",
dialect: "mysql",
});
module.exports = sequelize;
+15
View File
@@ -0,0 +1,15 @@
const { DataTypes } = require("sequelize");
const sequelize = require("./db");
const User = sequelize.define("User", {
name: DataTypes.STRING,
});
const Post = sequelize.define("Post", {
title: DataTypes.STRING,
});
User.hasMany(Post, { as: "posts" });
Post.belongsTo(User);
module.exports = { User, Post, sequelize };
+19
View File
@@ -0,0 +1,19 @@
{
"name": "day15",
"version": "1.0.0",
"description": "- setup project\r - implement https://www.treeql.org/ manually",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^2.2.0",
"dotenv": "^17.2.0",
"express": "^5.1.0",
"mysql2": "^3.14.2",
"sequelize": "^6.37.7"
}
}
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+29
View File
@@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
{
"name": "day16",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
"fabric": "^6.7.0",
"fabricjs-react": "^1.2.2",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@vitejs/plugin-react": "^4.6.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.11",
"vite": "^7.0.4"
}
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -0,0 +1 @@
@import "tailwindcss";
+317
View File
@@ -0,0 +1,317 @@
import React, { useRef, useEffect, useState } from "react";
import { Canvas, Textbox, Image } from "fabric";
function App() {
const canvasRef = useRef(null);
const fabricRef = useRef(null);
const [activeMenu, setActiveMenu] = useState("Background");
const [selectedObject, setSelectedObject] = useState(null);
const [fontSize, setFontSize] = useState(60);
const [images, setImages] = useState([]);
const [isImagesLoading, setIsImagesLoading] = useState(false);
const colors = [
"#F4A261",
"#E76F51",
"#2A9D8F",
"#264653",
"#A8DADC",
"#457B9D",
"#E63946",
"#F7B801",
"#A259F7",
"#6D6875",
];
useEffect(() => {
const canvas = new Canvas(canvasRef.current, {
width: 900,
height: 400,
backgroundColor: "#F4A261",
});
fabricRef.current = canvas;
// Object selection logic
function updateSelectedObject() {
setSelectedObject(canvas.getActiveObject());
const obj = canvas.getActiveObject();
if (obj && obj.type === "text") {
setFontSize(obj.fontSize);
}
}
canvas.on("selection:created", updateSelectedObject);
canvas.on("selection:updated", updateSelectedObject);
canvas.on("selection:cleared", updateSelectedObject);
// Allow editing text on double click
canvas.on("mouse:dblclick", (e) => {
if (e.target && e.target.type === "text") {
e.target.enterEditing();
e.target.selectAll();
}
});
// Force initial render so canvas is visible
canvas.requestRenderAll();
return () => {
canvas.dispose();
};
}, []);
useEffect(() => {
if (images.length === 0) {
setIsImagesLoading(true);
fetch("https://picsum.photos/v2/list")
.then((res) => res.json())
.then((data) => setImages(data))
.catch((err) => console.error("Failed to fetch images", err))
.finally(() => setIsImagesLoading(false));
}
}, [images]);
// Add text to canvas
const addText = (type) => {
let text = "";
let size = 60;
if (type === "h1") {
text = "Add a heading";
size = 60;
} else if (type === "h6") {
text = "Add a subheading";
size = 32;
} else {
text = "Add a little bit of body text";
size = 20;
}
const canvas = fabricRef.current;
const textbox = new Textbox(text, {
left: canvas.width / 2,
top: canvas.height / 2,
fontSize: size,
fontWeight: type === "h1" ? "bold" : "normal",
editable: true,
originX: "center",
originY: "center",
fill: "#222",
});
canvas.add(textbox).setActiveObject(textbox);
canvas.renderAll();
};
// Add image to canvas
const addImageToCanvas = async (url) => {
const img = await Image.fromURL(url, { crossOrigin: "anonymous" });
const canvas = fabricRef.current;
img.scaleToWidth(200);
img.set({
left: canvas.width / 2,
top: canvas.height / 2,
originX: "center",
originY: "center",
});
canvas.add(img);
canvas.setActiveObject(img);
canvas.requestRenderAll();
};
// Set background color
const setBackgroundColor = (color) => {
if (fabricRef.current) {
fabricRef.current.backgroundColor = color;
fabricRef.current.requestRenderAll();
}
};
// Download canvas
const downloadCanvas = () => {
if (fabricRef.current) {
const dataURL = fabricRef.current.toDataURL({
format: "png",
quality: 1,
});
const link = document.createElement("a");
link.href = dataURL;
link.download = "canvas.png";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
// Change font size of selected text
const handleFontSizeChange = (e) => {
const size = parseInt(e.target.value, 10);
setFontSize(size);
if (selectedObject && selectedObject.type === "text") {
selectedObject.set({ fontSize: size });
fabricRef.current.renderAll();
}
};
// Delete selected object (text or image)
const deleteSelectedObject = () => {
if (selectedObject) {
fabricRef.current.remove(selectedObject);
setSelectedObject(null);
}
};
return (
<div className='h-screen w-screen flex flex-col bg-gray-100'>
{/* Navbar */}
<nav className='flex items-center justify-between h-12 bg-black px-4'>
<div className='text-white text-2xl font-bold flex items-center gap-2'>
<span role='img' aria-label='logo'>
🧩
</span>
</div>
<div className='flex items-center gap-4'>
{selectedObject && (
<>
{selectedObject.type === "text" && (
<select
className='border rounded px-2 py-1 text-sm'
value={fontSize}
onChange={handleFontSizeChange}
>
{[12, 16, 20, 24, 32, 40, 48, 60, 72, 96].map((size) => (
<option key={size} value={size}>
{size}
</option>
))}
</select>
)}
<button
className='ml-2 text-xl text-gray-700 hover:text-red-600'
onClick={deleteSelectedObject}
title='Delete selected object'
>
🗑
</button>
</>
)}
<button
className='bg-white text-black px-4 py-1 rounded shadow hover:bg-gray-200'
onClick={downloadCanvas}
>
Download
</button>
</div>
</nav>
<div className='flex flex-1 overflow-hidden'>
{/* Sidebar */}
<aside className='w-56 bg-white border-r flex flex-col p-4 gap-6'>
<div className='flex flex-col gap-4'>
<button
className={`text-left font-medium hover:text-blue-600 flex items-center gap-2 ${
activeMenu === "Elements" ? "text-blue-600" : ""
}`}
onClick={() => setActiveMenu("Elements")}
>
Elements
</button>
<button
className={`text-left font-medium hover:text-blue-600 flex items-center gap-2 ${
activeMenu === "Images" ? "text-blue-600" : ""
}`}
onClick={() => setActiveMenu("Images")}
>
Images
</button>
<button
className={`text-left font-medium hover:text-blue-600 flex items-center gap-2 ${
activeMenu === "Text" ? "text-blue-600" : ""
}`}
onClick={() => setActiveMenu("Text")}
>
Text
</button>
<button
className={`text-left font-medium hover:text-blue-600 flex items-center gap-2 ${
activeMenu === "Background" ? "text-blue-600" : ""
}`}
onClick={() => setActiveMenu("Background")}
>
Background
</button>
</div>
{/* Images menu */}
{activeMenu === "Images" && (
<>
{isImagesLoading ? (
<div className='w-full grid place-items-center'>
<div className='rounded-full border-2 border-black size-20 border-t-transparent animate-spin' />
</div>
) : (
<div className='flex flex-col gap-2 mt-6 overflow-y-auto'>
<div className='grid grid-cols-2 gap-2'>
{images.map((image) => (
<button
key={image.id}
onClick={() => addImageToCanvas(image.download_url)}
className='hover:opacity-80'
>
<img
src={image.download_url}
alt={image.author}
className='w-full h-auto rounded'
/>
</button>
))}
</div>
</div>
)}
</>
)}
{/* Text menu */}
{activeMenu === "Text" && (
<div className='flex flex-col gap-2 mt-6'>
<button
className='bg-gray-100 rounded px-3 py-2 text-left font-bold text-lg hover:bg-gray-200'
onClick={() => addText("h1")}
>
Add a heading
</button>
<button
className='bg-gray-100 rounded px-3 py-2 text-left font-semibold hover:bg-gray-200'
onClick={() => addText("h6")}
>
Add a subheading
</button>
<button
className='bg-gray-100 rounded px-3 py-2 text-left text-sm hover:bg-gray-200'
onClick={() => addText("p")}
>
Add a little bit of body text
</button>
</div>
)}
{/* Background color swatches */}
{activeMenu === "Background" && (
<div>
<div className='mb-2 text-sm font-semibold'>Default colors</div>
<div className='grid grid-cols-5 gap-2'>
{colors.map((color) => (
<button
key={color}
className='w-7 h-7 rounded shadow border-2 border-white hover:border-black focus:outline-none'
style={{ background: color }}
aria-label={color}
onClick={() => setBackgroundColor(color)}
/>
))}
</div>
</div>
)}
</aside>
{/* Canvas Area */}
<main className='flex-1 flex items-center justify-center bg-gray-50'>
<canvas ref={canvasRef} width={900} height={400} />
</main>
</div>
</div>
);
}
export default App;
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+7
View File
@@ -0,0 +1,7 @@
@import "tailwindcss";
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
+8
View File
@@ -0,0 +1,8 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
});
+6 -4
View File
@@ -2,7 +2,9 @@
## Instructions
- setup project
- clone to your github
- Make the frontend for this project https://www.figma.com/file/iaKhmTAN28YiYXAOZAr9rN/Scheduler-Task?node-id=0%3A1
- timezones are generated dynamically
- Setup project
- Clone to your github
- Check this https://www.figma.com/file/iaKhmTAN28YiYXAOZAr9rN/Scheduler-Task?node-id=0%3A1
- Convert it into ejs/eta.
- Timezones should be generated dynamically
- Make a book schedule API like calendly.
+17 -16
View File
@@ -1,11 +1,12 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");
var apiRouter = require("./routes/api");
const db = require("./models");
var cors = require("cors");
@@ -13,18 +14,18 @@ var cors = require("cors");
var app = express();
app.set("db", db);
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");
app.use(cors());
app.use(logger('dev'));
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/users', usersRouter);
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));
@@ -34,11 +35,11 @@ app.use(function (req, res, next) {
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
res.render("error");
});
module.exports = app;
+32
View File
@@ -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
View File
@@ -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;
module.exports = db;
+5 -1
View File
@@ -3,17 +3,21 @@
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
"start": "node ./bin/www",
"dev": "node --watch --env-file=.env ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"ejs": "^3.1.10",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
"moment-timezone": "^0.6.0",
"morgan": "~1.9.1",
"mysql2": "^2.3.3",
"node-input-validator": "^4.5.1",
"sequelize": "^6.15.1"
}
}
+285 -4
View File
@@ -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;
}
}
+69
View File
@@ -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;
+134 -4
View File
@@ -1,9 +1,139 @@
var express = require('express');
var express = require("express");
var router = express.Router();
const moment = require("moment-timezone");
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
// Helper: Group timezones by region for the timezone selection screen
function getTimezoneGroups() {
const regions = {
"USA/CANADA": [
"America/Los_Angeles",
"America/Denver",
"America/New_York",
"America/Halifax",
],
EUROPE: [
"Europe/Berlin",
"Europe/Helsinki",
"Europe/Dublin",
"Europe/Samara",
],
ASIA: ["Asia/Hong_Kong", "Asia/Jakarta", "Asia/Kabul", "Asia/Kathmandu"],
"SOUTH AMERICA": [
"America/Bogota",
"America/Campo_Grande",
"America/Caracas",
"America/Lima",
],
};
const groups = {};
for (const region in regions) {
groups[region] = regions[region].map((tz) => ({
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;
}
// Helper: All timezones for dropdown
function getAllTimezones() {
return moment.tz.names().map((tz) => ({
value: tz,
label: tz,
}));
}
// Helper: Generate week days and slots
function getWeekDaysAndSlots(selectedTz, weekOffset = 0) {
const weekDays = [];
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({
label: day.format("dddd"),
date: day.format("MMMM D"),
dateISO: day.format("YYYY-MM-DD"),
slots: [
"9:00am",
"9:15am",
"9:30am",
"9:45am",
"10:00am",
"10:15am",
"10:30am",
"10:45am",
"11:00am",
"11:15am",
"11:30am",
"11:45am",
"12:00pm",
"12:15pm",
"12:30pm",
"12:45pm",
"1:00pm",
"1:15pm",
],
});
}
return { weekDays, maxSlots: 17 };
}
// Timezone selection screen
router.get("/timezone", function (req, res) {
res.render("timezone", {
timezones: getAllTimezones(),
timezoneGroups: getTimezoneGroups(),
});
});
// Calendar slot selection screen
router.get("/calendar", function (req, res) {
const selectedTimezone = req.query.tz || "America/New_York";
const weekOffset = parseInt(req.query.week) || 0;
const { weekDays, maxSlots } = getWeekDaysAndSlots(
selectedTimezone,
weekOffset
);
res.render("calendar", {
selectedTimezone,
weekDays,
maxSlots,
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,
});
});
// Booking POST (simulate success)
router.post("/book", function (req, res) {
// Here you would save booking info to DB
res.redirect("/success");
});
// Success screen
router.get("/success", function (req, res) {
res.render("success");
});
// Home page redirect to timezone selection
router.get("/", function (req, res) {
res.redirect("/timezone");
});
module.exports = router;
+97
View File
@@ -0,0 +1,97 @@
<%- include('partials/header') %>
<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>
+60
View File
@@ -0,0 +1,60 @@
<%- include('partials/header') %>
<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') %>
+10
View File
@@ -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') %>
-6
View File
@@ -1,6 +0,0 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}
+8
View File
@@ -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') %>
-5
View File
@@ -1,5 +0,0 @@
extends layout
block content
h1= title
p Welcome to #{title}
+10
View File
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/stylesheets/style.css" />
</head>
<body>
<%- body %>
</body>
</html>
-7
View File
@@ -1,7 +0,0 @@
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content
+2
View File
@@ -0,0 +1,2 @@
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
<!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>
+8
View File
@@ -0,0 +1,8 @@
<%- include('partials/header') %>
<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.
</div>
</div>
<%- include('partials/footer') %>
+78
View File
@@ -0,0 +1,78 @@
<%- include('partials/header') %>
<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>
<!-- 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>
+17 -15
View File
@@ -1,11 +1,12 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var createError = require("http-errors");
var express = require("express");
var path = require("path");
var cookieParser = require("cookie-parser");
var logger = require("morgan");
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var indexRouter = require("./routes/index");
var usersRouter = require("./routes/users");
var scheduleRouter = require("./routes/schedule");
const db = require("./models");
var cors = require("cors");
@@ -13,17 +14,18 @@ var cors = require("cors");
var app = express();
app.set("db", db);
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");
app.use(cors());
app.use(logger('dev'));
app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, "public")));
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use("/", indexRouter);
app.use("/users", usersRouter);
app.use("/api/v1", scheduleRouter);
// catch 404 and forward to error handler
app.use(function (req, res, next) {
@@ -34,11 +36,11 @@ app.use(function (req, res, next) {
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
res.render("error");
});
module.exports = app;
+36
View File
@@ -0,0 +1,36 @@
module.exports = (sequelize, DataTypes) => {
const availability = sequelize.define(
"availability",
{
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
user_id: {
type: DataTypes.INTEGER,
allowNull: true,
},
day_of_week: {
type: DataTypes.INTEGER,
allowNull: false,
},
start_time: {
type: DataTypes.TIME,
allowNull: false,
},
end_time: {
type: DataTypes.TIME,
allowNull: false,
},
created_at: DataTypes.DATE,
updated_at: DataTypes.DATE,
},
{
timestamps: true,
freezeTableName: true,
tableName: "availability",
}
);
return availability;
};
+48 -34
View File
@@ -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_19",
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;
module.exports = db;

Some files were not shown because too many files have changed in this diff Show More