Compare commits
27 Commits
763aebf1d3
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b0d86465f2 | |||
| 743187b216 | |||
| 29e6eb82c7 | |||
| dc35cfcb3f | |||
| cbbb0ed4c4 | |||
| 9113ba4c74 | |||
| 7d9350c3df | |||
| 9c84737fed | |||
| 001e4b6d00 | |||
| 825583e645 | |||
| 7d8ed6d0ee | |||
| 10447cd05e | |||
| f87c93e85c | |||
| 61d9872bab | |||
| 95edc56088 | |||
| 2b58c1d8b0 | |||
| 325788cb39 | |||
| b277c8182d | |||
| 1703819bda | |||
| 7bbafc90a8 | |||
| b640c08c09 | |||
| 04a5ae01cf | |||
| 5485f1af1d | |||
| 7a6da5203e | |||
| a4177125f8 | |||
| 595876509a | |||
| 059da72768 |
+12
-1
@@ -1,2 +1,13 @@
|
|||||||
node_modules
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# This project is a toy project for training and quality assurance purposes
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
- setup project
|
- setup project
|
||||||
- clone to your github
|
- 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:
|
- Setup the following Models in models folder. Make sure tables made by sequelize:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+24
-15
@@ -1,29 +1,38 @@
|
|||||||
var createError = require('http-errors');
|
var createError = require("http-errors");
|
||||||
var express = require('express');
|
var express = require("express");
|
||||||
var path = require('path');
|
var path = require("path");
|
||||||
var cookieParser = require('cookie-parser');
|
var cookieParser = require("cookie-parser");
|
||||||
var logger = require('morgan');
|
var logger = require("morgan");
|
||||||
|
|
||||||
var indexRouter = require('./routes/index');
|
var indexRouter = require("./routes/index");
|
||||||
var usersRouter = require('./routes/users');
|
var usersRouter = require("./routes/users");
|
||||||
|
|
||||||
const db = require("./models");
|
const db = require("./models");
|
||||||
var cors = require("cors");
|
var cors = require("cors");
|
||||||
|
|
||||||
|
const shippingDockRouter = require("./routes/shippingDock");
|
||||||
|
const orderRouter = require("./routes/order");
|
||||||
|
const transactionRouter = require("./routes/transaction");
|
||||||
|
|
||||||
var app = express();
|
var app = express();
|
||||||
app.set("db", db);
|
app.set("db", db);
|
||||||
// view engine setup
|
// view engine setup
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
app.set("views", path.join(__dirname, "views"));
|
||||||
app.set('view engine', 'jade');
|
app.set("view engine", "jade");
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(logger('dev'));
|
app.use(logger("dev"));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: false }));
|
app.use(express.urlencoded({ extended: false }));
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, "public")));
|
||||||
|
|
||||||
app.use('/', indexRouter);
|
app.use("/", indexRouter);
|
||||||
app.use('/users', usersRouter);
|
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
|
// catch 404 and forward to error handler
|
||||||
app.use(function (req, res, next) {
|
app.use(function (req, res, next) {
|
||||||
@@ -34,11 +43,11 @@ app.use(function (req, res, next) {
|
|||||||
app.use(function (err, req, res, next) {
|
app.use(function (err, req, res, next) {
|
||||||
// set locals, only providing error in development
|
// set locals, only providing error in development
|
||||||
res.locals.message = err.message;
|
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
|
// render the error page
|
||||||
res.status(err.status || 500);
|
res.status(err.status || 500);
|
||||||
res.render('error');
|
res.render("error");
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|||||||
+16
-19
@@ -4,16 +4,16 @@
|
|||||||
* Module dependencies.
|
* Module dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var app = require('../app');
|
var app = require("../app");
|
||||||
var debug = require('debug')('day-1:server');
|
var debug = require("debug")("day-1:server");
|
||||||
var http = require('http');
|
var http = require("http");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get port from environment and store in Express.
|
* Get port from environment and store in Express.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var port = normalizePort(process.env.PORT || '3000');
|
var port = normalizePort(process.env.PORT || "3000");
|
||||||
app.set('port', port);
|
app.set("port", port);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create HTTP server.
|
* Create HTTP server.
|
||||||
@@ -26,8 +26,8 @@ var server = http.createServer(app);
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
server.listen(port);
|
server.listen(port);
|
||||||
server.on('error', onError);
|
server.on("error", onError);
|
||||||
server.on('listening', onListening);
|
server.on("listening", onListening);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize a port into a number, string, or false.
|
* Normalize a port into a number, string, or false.
|
||||||
@@ -54,22 +54,20 @@ function normalizePort(val) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function onError(error) {
|
function onError(error) {
|
||||||
if (error.syscall !== 'listen') {
|
if (error.syscall !== "listen") {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bind = typeof port === 'string'
|
var bind = typeof port === "string" ? "Pipe " + port : "Port " + port;
|
||||||
? 'Pipe ' + port
|
|
||||||
: 'Port ' + port;
|
|
||||||
|
|
||||||
// handle specific listen errors with friendly messages
|
// handle specific listen errors with friendly messages
|
||||||
switch (error.code) {
|
switch (error.code) {
|
||||||
case 'EACCES':
|
case "EACCES":
|
||||||
console.error(bind + ' requires elevated privileges');
|
console.error(bind + " requires elevated privileges");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
break;
|
break;
|
||||||
case 'EADDRINUSE':
|
case "EADDRINUSE":
|
||||||
console.error(bind + ' is already in use');
|
console.error(bind + " is already in use");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -83,8 +81,7 @@ function onError(error) {
|
|||||||
|
|
||||||
function onListening() {
|
function onListening() {
|
||||||
var addr = server.address();
|
var addr = server.address();
|
||||||
var bind = typeof addr === 'string'
|
var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
|
||||||
? 'pipe ' + addr
|
debug("Listening on " + bind);
|
||||||
: 'port ' + addr.port;
|
console.log("Server is listening on " + bind);
|
||||||
debug('Listening on ' + bind);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-34
@@ -1,4 +1,4 @@
|
|||||||
'use strict';
|
"use strict";
|
||||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||||
/**
|
/**
|
||||||
* Sequelize File
|
* Sequelize File
|
||||||
@@ -8,49 +8,63 @@
|
|||||||
* @author Ryan Wong
|
* @author Ryan Wong
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
let Sequelize = require('sequelize');
|
let Sequelize = require("sequelize");
|
||||||
const basename = path.basename(__filename);
|
const basename = path.basename(__filename);
|
||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require("sequelize");
|
||||||
const config = {
|
const config = {
|
||||||
DB_DATABASE: 'mysql',
|
DB_DATABASE: "mysql",
|
||||||
DB_USERNAME: 'root',
|
DB_USERNAME: "root",
|
||||||
DB_PASSWORD: 'root',
|
DB_PASSWORD: process.env.DB_PASSWORD,
|
||||||
DB_ADAPTER: 'mysql',
|
DB_ADAPTER: "mysql",
|
||||||
DB_NAME: 'day_1',
|
DB_NAME: "day_1",
|
||||||
DB_HOSTNAME: 'localhost',
|
DB_HOSTNAME: "localhost",
|
||||||
DB_PORT: 3306,
|
DB_PORT: 3306,
|
||||||
};
|
};
|
||||||
|
|
||||||
let db = {};
|
let db = {};
|
||||||
|
|
||||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
let sequelize = new Sequelize(
|
||||||
dialect: config.DB_ADAPTER,
|
config.DB_NAME,
|
||||||
username: config.DB_USERNAME,
|
config.DB_USERNAME,
|
||||||
password: config.DB_PASSWORD,
|
config.DB_PASSWORD,
|
||||||
database: config.DB_NAME,
|
{
|
||||||
host: config.DB_HOSTNAME,
|
dialect: config.DB_ADAPTER,
|
||||||
port: config.DB_PORT,
|
username: config.DB_USERNAME,
|
||||||
logging: console.log,
|
password: config.DB_PASSWORD,
|
||||||
timezone: '-04:00',
|
database: config.DB_NAME,
|
||||||
pool: {
|
host: config.DB_HOSTNAME,
|
||||||
maxConnections: 1,
|
port: config.DB_PORT,
|
||||||
minConnections: 0,
|
logging: console.log,
|
||||||
maxIdleTime: 100,
|
timezone: "-04:00",
|
||||||
},
|
pool: {
|
||||||
define: {
|
maxConnections: 1,
|
||||||
timestamps: false,
|
minConnections: 0,
|
||||||
underscoredAll: true,
|
maxIdleTime: 100,
|
||||||
underscored: true,
|
},
|
||||||
},
|
define: {
|
||||||
});
|
timestamps: false,
|
||||||
|
underscoredAll: true,
|
||||||
|
underscored: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// sequelize.sync({ force: true });
|
sequelize
|
||||||
|
.sync()
|
||||||
|
.then(() => {
|
||||||
|
console.log("All tables synced successfully.");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to sync tables:", err);
|
||||||
|
});
|
||||||
|
|
||||||
fs.readdirSync(__dirname)
|
fs.readdirSync(__dirname)
|
||||||
.filter((file) => {
|
.filter((file) => {
|
||||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
return (
|
||||||
|
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.forEach((file) => {
|
.forEach((file) => {
|
||||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||||
@@ -66,4 +80,4 @@ Object.keys(db).forEach((modelName) => {
|
|||||||
db.sequelize = sequelize;
|
db.sequelize = sequelize;
|
||||||
db.Sequelize = Sequelize;
|
db.Sequelize = Sequelize;
|
||||||
|
|
||||||
module.exports = db;
|
module.exports = db;
|
||||||
|
|||||||
+4
-10
@@ -1,5 +1,5 @@
|
|||||||
module.exports = (sequelize, DataTypes) => {
|
module.exports = (sequelize, DataTypes) => {
|
||||||
const location = sequelize.define(
|
return sequelize.define(
|
||||||
"location",
|
"location",
|
||||||
{
|
{
|
||||||
id: {
|
id: {
|
||||||
@@ -8,19 +8,13 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
autoIncrement: true,
|
autoIncrement: true,
|
||||||
},
|
},
|
||||||
name: DataTypes.STRING,
|
name: DataTypes.STRING,
|
||||||
created_at: DataTypes.DATEONLY,
|
|
||||||
updated_at: DataTypes.DATE,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
freezeTableName: true,
|
freezeTableName: true,
|
||||||
tableName: "location",
|
tableName: "location",
|
||||||
},
|
createdAt: "created_at",
|
||||||
{
|
updatedAt: "updated_at",
|
||||||
underscoredAll: false,
|
|
||||||
underscored: false,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
};
|
||||||
return location;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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
@@ -3,7 +3,8 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./bin/www"
|
"start": "node ./bin/www",
|
||||||
|
"dev": "node --watch --env-file=.env ./bin/www"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie-parser": "~1.4.4",
|
"cookie-parser": "~1.4.4",
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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
@@ -1,11 +1,12 @@
|
|||||||
var createError = require('http-errors');
|
var createError = require("http-errors");
|
||||||
var express = require('express');
|
var express = require("express");
|
||||||
var path = require('path');
|
var path = require("path");
|
||||||
var cookieParser = require('cookie-parser');
|
var cookieParser = require("cookie-parser");
|
||||||
var logger = require('morgan');
|
var logger = require("morgan");
|
||||||
|
|
||||||
var indexRouter = require('./routes/index');
|
var indexRouter = require("./routes/index");
|
||||||
var usersRouter = require('./routes/users');
|
var usersRouter = require("./routes/users");
|
||||||
|
const codeRouter = require("./routes/code");
|
||||||
|
|
||||||
const db = require("./models");
|
const db = require("./models");
|
||||||
var cors = require("cors");
|
var cors = require("cors");
|
||||||
@@ -13,17 +14,18 @@ var cors = require("cors");
|
|||||||
var app = express();
|
var app = express();
|
||||||
app.set("db", db);
|
app.set("db", db);
|
||||||
// view engine setup
|
// view engine setup
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
app.set("views", path.join(__dirname, "views"));
|
||||||
app.set('view engine', 'jade');
|
app.set("view engine", "jade");
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(logger('dev'));
|
app.use(logger("dev"));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: false }));
|
app.use(express.urlencoded({ extended: false }));
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, "public")));
|
||||||
|
|
||||||
app.use('/', indexRouter);
|
app.use("/", indexRouter);
|
||||||
app.use('/users', usersRouter);
|
app.use("/users", usersRouter);
|
||||||
|
app.use("/api/v1/code", codeRouter);
|
||||||
|
|
||||||
// catch 404 and forward to error handler
|
// catch 404 and forward to error handler
|
||||||
app.use(function (req, res, next) {
|
app.use(function (req, res, next) {
|
||||||
@@ -34,11 +36,11 @@ app.use(function (req, res, next) {
|
|||||||
app.use(function (err, req, res, next) {
|
app.use(function (err, req, res, next) {
|
||||||
// set locals, only providing error in development
|
// set locals, only providing error in development
|
||||||
res.locals.message = err.message;
|
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
|
// render the error page
|
||||||
res.status(err.status || 500);
|
res.status(err.status || 500);
|
||||||
res.render('error');
|
res.render("error");
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|||||||
+16
-19
@@ -4,16 +4,16 @@
|
|||||||
* Module dependencies.
|
* Module dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var app = require('../app');
|
var app = require("../app");
|
||||||
var debug = require('debug')('day-1:server');
|
var debug = require("debug")("day-1:server");
|
||||||
var http = require('http');
|
var http = require("http");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get port from environment and store in Express.
|
* Get port from environment and store in Express.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var port = normalizePort(process.env.PORT || '3000');
|
var port = normalizePort(process.env.PORT || "3000");
|
||||||
app.set('port', port);
|
app.set("port", port);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create HTTP server.
|
* Create HTTP server.
|
||||||
@@ -26,8 +26,8 @@ var server = http.createServer(app);
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
server.listen(port);
|
server.listen(port);
|
||||||
server.on('error', onError);
|
server.on("error", onError);
|
||||||
server.on('listening', onListening);
|
server.on("listening", onListening);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize a port into a number, string, or false.
|
* Normalize a port into a number, string, or false.
|
||||||
@@ -54,22 +54,20 @@ function normalizePort(val) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
function onError(error) {
|
function onError(error) {
|
||||||
if (error.syscall !== 'listen') {
|
if (error.syscall !== "listen") {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bind = typeof port === 'string'
|
var bind = typeof port === "string" ? "Pipe " + port : "Port " + port;
|
||||||
? 'Pipe ' + port
|
|
||||||
: 'Port ' + port;
|
|
||||||
|
|
||||||
// handle specific listen errors with friendly messages
|
// handle specific listen errors with friendly messages
|
||||||
switch (error.code) {
|
switch (error.code) {
|
||||||
case 'EACCES':
|
case "EACCES":
|
||||||
console.error(bind + ' requires elevated privileges');
|
console.error(bind + " requires elevated privileges");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
break;
|
break;
|
||||||
case 'EADDRINUSE':
|
case "EADDRINUSE":
|
||||||
console.error(bind + ' is already in use');
|
console.error(bind + " is already in use");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -83,8 +81,7 @@ function onError(error) {
|
|||||||
|
|
||||||
function onListening() {
|
function onListening() {
|
||||||
var addr = server.address();
|
var addr = server.address();
|
||||||
var bind = typeof addr === 'string'
|
var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
|
||||||
? 'pipe ' + addr
|
debug("Listening on " + bind);
|
||||||
: 'port ' + addr.port;
|
console.log("Server listening on:", bind);
|
||||||
debug('Listening on ' + bind);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -3,18 +3,22 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./bin/www"
|
"start": "node ./bin/www",
|
||||||
|
"dev": "node --watch --env-file=.env ./bin/www"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie-parser": "~1.4.4",
|
"cookie-parser": "~1.4.4",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"debug": "~2.6.9",
|
"debug": "~2.6.9",
|
||||||
"express": "~4.16.1",
|
"express": "~4.16.1",
|
||||||
|
"html-pdf-node": "^1.0.8",
|
||||||
"http-errors": "~1.6.3",
|
"http-errors": "~1.6.3",
|
||||||
"jade": "~1.11.0",
|
"jade": "~1.11.0",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"morgan": "~1.9.1",
|
"morgan": "~1.9.1",
|
||||||
"mysql2": "^2.3.3",
|
"mysql2": "^2.3.3",
|
||||||
|
"node-input-validator": "^4.5.1",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"sequelize": "^6.15.1"
|
"sequelize": "^6.15.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -1,9 +1,22 @@
|
|||||||
var express = require('express');
|
var express = require("express");
|
||||||
var router = express.Router();
|
var router = express.Router();
|
||||||
|
const QRCode = require("qrcode");
|
||||||
|
|
||||||
/* GET home page. */
|
/* GET home page. */
|
||||||
router.get('/', function(req, res, next) {
|
router.get("/", function (req, res, next) {
|
||||||
res.render('index', { title: 'Express' });
|
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;
|
module.exports = router;
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
extends layout
|
||||||
|
|
||||||
|
block content
|
||||||
|
h1 QR Code
|
||||||
|
img(src=qrData)
|
||||||
|
p Link: #{qrUrl}
|
||||||
+94
-94
@@ -1,4 +1,4 @@
|
|||||||
'use strict'
|
"use strict";
|
||||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||||
/**
|
/**
|
||||||
* App
|
* App
|
||||||
@@ -8,52 +8,52 @@
|
|||||||
* @author Ryan Wong
|
* @author Ryan Wong
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
require('dotenv').config()
|
require("dotenv").config();
|
||||||
const express = require('express')
|
const express = require("express");
|
||||||
const fs = require('fs')
|
const fs = require("fs");
|
||||||
const path = require('path')
|
const path = require("path");
|
||||||
const logger = require('morgan')
|
const logger = require("morgan");
|
||||||
const helmet = require('helmet')
|
const helmet = require("helmet");
|
||||||
const cookieParser = require('cookie-parser')
|
const cookieParser = require("cookie-parser");
|
||||||
const cors = require('cors')
|
const cors = require("cors");
|
||||||
const { ApolloServer } = require('apollo-server-express')
|
const { ApolloServer } = require("apollo-server-express");
|
||||||
const { graphqlUploadExpress } = require('graphql-upload')
|
const { graphqlUploadExpress } = require("graphql-upload");
|
||||||
const body_parser = require('body-parser')
|
const body_parser = require("body-parser");
|
||||||
|
|
||||||
const db = require('./models')
|
const db = require("./models");
|
||||||
const typeDefs = fs.readFileSync(
|
const typeDefs = fs.readFileSync(
|
||||||
path.join(__dirname, '/types/schema.graphql'),
|
path.join(__dirname, "/types/schema.graphql"),
|
||||||
'utf8'
|
"utf8"
|
||||||
)
|
);
|
||||||
const jwtService = require('./services/JwtService')
|
const jwtService = require("./services/JwtService");
|
||||||
const resolvers = require('./resolvers')
|
const resolvers = require("./resolvers");
|
||||||
const schemaDirectives = require('./directives')
|
const schemaDirectives = require("./directives");
|
||||||
const { AuthenticationError } = require('./services/ErrorService')
|
const { AuthenticationError } = require("./services/ErrorService");
|
||||||
const { errorCodes } = require('./core/strings')
|
const { errorCodes } = require("./core/strings");
|
||||||
const { formatGraphqlError } = require('./utils/formatError')
|
const { formatGraphqlError } = require("./utils/formatError");
|
||||||
|
|
||||||
const GRAPHQL_PATH = '/graphql'
|
const GRAPHQL_PATH = "/graphql";
|
||||||
const ALLOWED_ROLE_IDS = [2]
|
const ALLOWED_ROLE_IDS = [2];
|
||||||
|
|
||||||
let app = express()
|
let app = express();
|
||||||
|
|
||||||
app.use(logger('dev'))
|
app.use(logger("dev"));
|
||||||
|
|
||||||
if (process.env.MODE === 'development') {
|
if (process.env.MODE === "development") {
|
||||||
logger.token('graphql-query', (req) => {
|
logger.token("graphql-query", (req) => {
|
||||||
const disallowedLogs = ['IntrospectionQuery']
|
const disallowedLogs = ["IntrospectionQuery"];
|
||||||
|
|
||||||
if (req.method === 'POST' && req.originalUrl === GRAPHQL_PATH) {
|
if (req.method === "POST" && req.originalUrl === GRAPHQL_PATH) {
|
||||||
const { query, variables, operationName } = req.body
|
const { query, variables, operationName } = req.body;
|
||||||
return !disallowedLogs.includes(operationName)
|
return !disallowedLogs.includes(operationName)
|
||||||
? `GRAPHQL: \nOperation Name: ${operationName} \nQuery: ${query} \nVariables: ${JSON.stringify(
|
? `GRAPHQL: \nOperation Name: ${operationName} \nQuery: ${query} \nVariables: ${JSON.stringify(
|
||||||
variables
|
variables
|
||||||
)}`
|
)}`
|
||||||
: ''
|
: "";
|
||||||
}
|
}
|
||||||
return ''
|
return "";
|
||||||
})
|
});
|
||||||
app.use(logger(':graphql-query'))
|
app.use(logger(":graphql-query"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = new ApolloServer({
|
const server = new ApolloServer({
|
||||||
@@ -62,97 +62,97 @@ const server = new ApolloServer({
|
|||||||
resolvers,
|
resolvers,
|
||||||
schemaDirectives,
|
schemaDirectives,
|
||||||
context: async ({ req }) => {
|
context: async ({ req }) => {
|
||||||
const token = req.headers.authorization
|
// const token = req.headers.authorization
|
||||||
|
|
||||||
if (!token) {
|
// if (!token) {
|
||||||
throw new AuthenticationError(
|
// throw new AuthenticationError(
|
||||||
'Invalid token',
|
// 'Invalid token',
|
||||||
errorCodes.token.INVALID_TOKEN
|
// errorCodes.token.INVALID_TOKEN
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
const cleanToken = token.replace('Bearer ', '')
|
// const cleanToken = token.replace('Bearer ', '')
|
||||||
const verify = jwtService.verifyAccessToken(cleanToken)
|
// const verify = jwtService.verifyAccessToken(cleanToken)
|
||||||
|
|
||||||
const roleId = verify?.role_id
|
// const roleId = verify?.role_id
|
||||||
const user = verify?.user
|
// const user = verify?.user
|
||||||
const credentialId = verify?.credential_id
|
// const credentialId = verify?.credential_id
|
||||||
|
|
||||||
if (!verify || !roleId || !user || !credentialId) {
|
// if (!verify || !roleId || !user || !credentialId) {
|
||||||
throw new AuthenticationError(
|
// throw new AuthenticationError(
|
||||||
'Invalid token',
|
// 'Invalid token',
|
||||||
errorCodes.token.INVALID_TOKEN
|
// errorCodes.token.INVALID_TOKEN
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (!ALLOWED_ROLE_IDS.includes(+roleId)) {
|
// if (!ALLOWED_ROLE_IDS.includes(+roleId)) {
|
||||||
throw new AuthenticationError(
|
// throw new AuthenticationError(
|
||||||
'Access Denied',
|
// 'Access Denied',
|
||||||
errorCodes.account.UNAUTHORIZED
|
// errorCodes.account.UNAUTHORIZED
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
credentialId,
|
credentialId: 1,
|
||||||
user,
|
user: { id: 1, role_id: 1 },
|
||||||
db,
|
db,
|
||||||
role: {
|
role: {
|
||||||
roleId,
|
roleId: 1,
|
||||||
allowedRoleIds: ALLOWED_ROLE_IDS,
|
allowedRoleIds: [1, 2, 3],
|
||||||
|
// allowedRoleIds: ALLOWED_ROLE_IDS,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
formatError: formatGraphqlError,
|
formatError: formatGraphqlError,
|
||||||
})
|
});
|
||||||
|
|
||||||
if (process.NODE_ENV === 'maintenance') {
|
if (process.NODE_ENV === "maintenance") {
|
||||||
app.all('*', (req, res) => {
|
app.all("*", (req, res) => {
|
||||||
res.status(503).json({ message: 'website under maintenance' })
|
res.status(503).json({ message: "website under maintenance" });
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
app.set('iocContainer', process.env)
|
app.set("iocContainer", process.env);
|
||||||
app.set('db', db)
|
app.set("db", db);
|
||||||
app.use(body_parser.json({ limit: '50mb' }))
|
app.use(body_parser.json({ limit: "50mb" }));
|
||||||
|
|
||||||
app.use(express.json())
|
app.use(express.json());
|
||||||
app.use(
|
app.use(
|
||||||
express.urlencoded({
|
express.urlencoded({
|
||||||
extended: false,
|
extended: false,
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
app.use(cors())
|
app.use(cors());
|
||||||
app.set('view engine', 'eta')
|
app.set("view engine", "eta");
|
||||||
app.set('views', path.join(__dirname, '/views'))
|
app.set("views", path.join(__dirname, "/views"));
|
||||||
app.use(cookieParser())
|
app.use(cookieParser());
|
||||||
app.use(helmet())
|
app.use(helmet());
|
||||||
|
|
||||||
app.use(express.static(path.join(__dirname, '/public')))
|
app.use(express.static(path.join(__dirname, "/public")));
|
||||||
app.use(express.static(path.join(__dirname, '/uploads')))
|
app.use(express.static(path.join(__dirname, "/uploads")));
|
||||||
app.use(express.static(path.join(__dirname)));
|
app.use(express.static(path.join(__dirname)));
|
||||||
|
|
||||||
app.use(graphqlUploadExpress({ maxFileSize: 1000000000, maxFiles: 10 }))
|
app.use(graphqlUploadExpress({ maxFileSize: 1000000000, maxFiles: 10 }));
|
||||||
|
|
||||||
server.applyMiddleware({ app, path: GRAPHQL_PATH })
|
|
||||||
|
|
||||||
|
server.applyMiddleware({ app, path: GRAPHQL_PATH });
|
||||||
|
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
res.locals.message = err.message
|
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
|
// render the error page
|
||||||
res.status(err.status || 500)
|
res.status(err.status || 500);
|
||||||
res.json({
|
res.json({
|
||||||
message: err.message,
|
message: err.message,
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
app.use((_, res, next) => {
|
app.use((_, res, next) => {
|
||||||
return res
|
return res
|
||||||
.status(400)
|
.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 = {
|
module.exports = {
|
||||||
app,
|
app,
|
||||||
apollo: server,
|
apollo: server,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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
@@ -1,4 +1,4 @@
|
|||||||
'use strict';
|
"use strict";
|
||||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||||
/**
|
/**
|
||||||
* Sequelize File
|
* Sequelize File
|
||||||
@@ -8,49 +8,59 @@
|
|||||||
* @author Ryan Wong
|
* @author Ryan Wong
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
let Sequelize = require('sequelize');
|
let Sequelize = require("sequelize");
|
||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require("sequelize");
|
||||||
const basename = path.basename(__filename);
|
const basename = path.basename(__filename);
|
||||||
const config = {
|
const config = {
|
||||||
DB_DATABASE: 'mysql',
|
DB_DATABASE: "mysql",
|
||||||
DB_USERNAME: 'root',
|
DB_USERNAME: "root",
|
||||||
DB_PASSWORD: 'root',
|
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||||
DB_ADAPTER: 'mysql',
|
DB_ADAPTER: "mysql",
|
||||||
DB_NAME: 'day_1',
|
DB_NAME: "day_11",
|
||||||
DB_HOSTNAME: 'localhost',
|
DB_HOSTNAME: "localhost",
|
||||||
DB_PORT: 3306,
|
DB_PORT: 3306,
|
||||||
};
|
};
|
||||||
|
|
||||||
let db = {};
|
let db = {};
|
||||||
|
|
||||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
let sequelize = new Sequelize(
|
||||||
dialect: config.DB_ADAPTER,
|
config.DB_NAME,
|
||||||
username: config.DB_USERNAME,
|
config.DB_USERNAME,
|
||||||
password: config.DB_PASSWORD,
|
config.DB_PASSWORD,
|
||||||
database: config.DB_NAME,
|
{
|
||||||
host: config.DB_HOSTNAME,
|
dialect: config.DB_ADAPTER,
|
||||||
port: config.DB_PORT,
|
username: config.DB_USERNAME,
|
||||||
logging: console.log,
|
password: config.DB_PASSWORD,
|
||||||
timezone: '-04:00',
|
database: config.DB_NAME,
|
||||||
pool: {
|
host: config.DB_HOSTNAME,
|
||||||
maxConnections: 1,
|
port: config.DB_PORT,
|
||||||
minConnections: 0,
|
logging: console.log,
|
||||||
maxIdleTime: 100,
|
timezone: "-04:00",
|
||||||
},
|
pool: {
|
||||||
define: {
|
maxConnections: 1,
|
||||||
timestamps: false,
|
minConnections: 0,
|
||||||
underscoredAll: true,
|
maxIdleTime: 100,
|
||||||
underscored: true,
|
},
|
||||||
},
|
define: {
|
||||||
});
|
timestamps: false,
|
||||||
|
underscoredAll: true,
|
||||||
|
underscored: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// sequelize.sync({ force: true });
|
sequelize
|
||||||
|
.sync()
|
||||||
|
.then(() => console.log("Tables synced successfully!"))
|
||||||
|
.catch((err) => console.log(err));
|
||||||
|
|
||||||
fs.readdirSync(__dirname)
|
fs.readdirSync(__dirname)
|
||||||
.filter((file) => {
|
.filter((file) => {
|
||||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
return (
|
||||||
|
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.forEach((file) => {
|
.forEach((file) => {
|
||||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||||
@@ -66,4 +76,4 @@ Object.keys(db).forEach((modelName) => {
|
|||||||
db.sequelize = sequelize;
|
db.sequelize = sequelize;
|
||||||
db.Sequelize = Sequelize;
|
db.Sequelize = Sequelize;
|
||||||
|
|
||||||
module.exports = db;
|
module.exports = db;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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
@@ -2,7 +2,10 @@
|
|||||||
"name": "day11",
|
"name": "day11",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {},
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "node --watch --env-file=.env server.js"
|
||||||
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Ryan Wong",
|
"author": "Ryan Wong",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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
@@ -7,47 +7,58 @@
|
|||||||
* @author Ryan Wong
|
* @author Ryan Wong
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const { GraphQLUpload } = require('graphql-upload');
|
const { GraphQLUpload } = require("graphql-upload");
|
||||||
|
|
||||||
const updateUserResolver = require('./update/updateUser');
|
const updateUserResolver = require("./update/updateUser");
|
||||||
const singleUserResolver = require('./single/singleUser');
|
const singleUserResolver = require("./single/singleUser");
|
||||||
const typeUserResolver = require('./type/typeUser');
|
const typeUserResolver = require("./type/typeUser");
|
||||||
|
|
||||||
const createLinkResolver = require('./create/createLink');
|
const createLinkResolver = require("./create/createLink");
|
||||||
const typeLinkResolver = require('./type/typeLink');
|
const typeLinkResolver = require("./type/typeLink");
|
||||||
const singleLinkResolver = require('./single/singleLink');
|
const singleLinkResolver = require("./single/singleLink");
|
||||||
const deactivateAllLinksResolver = require('./delete/deactivateAllLinks');
|
const deactivateAllLinksResolver = require("./delete/deactivateAllLinks");
|
||||||
|
|
||||||
const calendarResolver = require('./custom/calendar');
|
// const calendarResolver = require("./custom/calendar");
|
||||||
const noteResolver = require('./custom/note');
|
// const noteResolver = require("./custom/note");
|
||||||
const customImageResolver = require('./custom/image');
|
// const customImageResolver = require("./custom/image");
|
||||||
const uploadFileMutationResolver = require('./custom/uploadFile');
|
// const uploadFileMutationResolver = require("./custom/uploadFile");
|
||||||
|
|
||||||
const connectionStepsResolver = require('./custom/connectionSteps');
|
|
||||||
|
|
||||||
|
// 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 = {
|
module.exports = {
|
||||||
Upload: GraphQLUpload,
|
Upload: GraphQLUpload,
|
||||||
Query: {
|
Query: {
|
||||||
user: singleUserResolver,
|
user: singleUserResolver,
|
||||||
link: singleLinkResolver,
|
link: singleLinkResolver,
|
||||||
...calendarResolver.Query,
|
// ...calendarResolver.Query,
|
||||||
...customImageResolver.Query,
|
// ...customImageResolver.Query,
|
||||||
...noteResolver.Query,
|
// ...noteResolver.Query,
|
||||||
...connectionStepsResolver.Query
|
// ...connectionStepsResolver.Query,
|
||||||
|
...movieResolvers.Query,
|
||||||
|
...reviewResolvers.Query,
|
||||||
|
...directorResolvers.Query,
|
||||||
|
...actorResolvers.Query,
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
updateUser: updateUserResolver,
|
updateUser: updateUserResolver,
|
||||||
createLink: createLinkResolver,
|
createLink: createLinkResolver,
|
||||||
deactivateAllLinks: deactivateAllLinksResolver,
|
deactivateAllLinks: deactivateAllLinksResolver,
|
||||||
uploadFile: uploadFileMutationResolver,
|
// uploadFile: uploadFileMutationResolver,
|
||||||
...calendarResolver.Mutation,
|
// ...calendarResolver.Mutation,
|
||||||
...customImageResolver.Mutation,
|
// ...customImageResolver.Mutation,
|
||||||
...noteResolver.Mutation,
|
// ...noteResolver.Mutation,
|
||||||
|
...movieResolvers.Mutation,
|
||||||
|
...reviewResolvers.Mutation,
|
||||||
|
...directorResolvers.Mutation,
|
||||||
|
...actorResolvers.Mutation,
|
||||||
},
|
},
|
||||||
|
|
||||||
...calendarResolver.Type,
|
// ...calendarResolver.Type,
|
||||||
...noteResolver.Type,
|
// ...noteResolver.Type,
|
||||||
|
|
||||||
User: typeUserResolver,
|
User: typeUserResolver,
|
||||||
Link: typeLinkResolver,
|
Link: typeLinkResolver,
|
||||||
|
|||||||
+13
-5
@@ -1,4 +1,4 @@
|
|||||||
'use strict';
|
"use strict";
|
||||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||||
/**
|
/**
|
||||||
* Server
|
* Server
|
||||||
@@ -8,11 +8,19 @@
|
|||||||
* @author Ryan Wong
|
* @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, () => {
|
app.listen(PORT, () => {
|
||||||
console.log('Server running at ', true ? `http://localhost:${PORT}` : 'process.env.BASE_URL');
|
console.log(
|
||||||
console.log('GraphQL running at ', true ? `http://localhost:${PORT}${apollo.graphqlPath}` : `${'process.env.BASE_URL'}${apollo.graphqlPath}`);
|
"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}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -211,3 +211,150 @@ type Mutation {
|
|||||||
|
|
||||||
uploadFile(file: Upload!): FileUploadResponse!
|
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
@@ -1,4 +1,4 @@
|
|||||||
'use strict';
|
"use strict";
|
||||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||||
/**
|
/**
|
||||||
* Sequelize File
|
* Sequelize File
|
||||||
@@ -8,49 +8,59 @@
|
|||||||
* @author Ryan Wong
|
* @author Ryan Wong
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
let Sequelize = require('sequelize');
|
let Sequelize = require("sequelize");
|
||||||
const basename = path.basename(__filename);
|
const basename = path.basename(__filename);
|
||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require("sequelize");
|
||||||
const config = {
|
const config = {
|
||||||
DB_DATABASE: 'mysql',
|
DB_DATABASE: "mysql",
|
||||||
DB_USERNAME: 'root',
|
DB_USERNAME: "root",
|
||||||
DB_PASSWORD: 'root',
|
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||||
DB_ADAPTER: 'mysql',
|
DB_ADAPTER: "mysql",
|
||||||
DB_NAME: 'day_1',
|
DB_NAME: "day_13",
|
||||||
DB_HOSTNAME: 'localhost',
|
DB_HOSTNAME: "localhost",
|
||||||
DB_PORT: 3306,
|
DB_PORT: 3306,
|
||||||
};
|
};
|
||||||
|
|
||||||
let db = {};
|
let db = {};
|
||||||
|
|
||||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
let sequelize = new Sequelize(
|
||||||
dialect: config.DB_ADAPTER,
|
config.DB_NAME,
|
||||||
username: config.DB_USERNAME,
|
config.DB_USERNAME,
|
||||||
password: config.DB_PASSWORD,
|
config.DB_PASSWORD,
|
||||||
database: config.DB_NAME,
|
{
|
||||||
host: config.DB_HOSTNAME,
|
dialect: config.DB_ADAPTER,
|
||||||
port: config.DB_PORT,
|
username: config.DB_USERNAME,
|
||||||
logging: console.log,
|
password: config.DB_PASSWORD,
|
||||||
timezone: '-04:00',
|
database: config.DB_NAME,
|
||||||
pool: {
|
host: config.DB_HOSTNAME,
|
||||||
maxConnections: 1,
|
port: config.DB_PORT,
|
||||||
minConnections: 0,
|
logging: console.log,
|
||||||
maxIdleTime: 100,
|
timezone: "-04:00",
|
||||||
},
|
pool: {
|
||||||
define: {
|
maxConnections: 1,
|
||||||
timestamps: false,
|
minConnections: 0,
|
||||||
underscoredAll: true,
|
maxIdleTime: 100,
|
||||||
underscored: true,
|
},
|
||||||
},
|
define: {
|
||||||
});
|
timestamps: false,
|
||||||
|
underscoredAll: true,
|
||||||
|
underscored: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// sequelize.sync({ force: true });
|
sequelize
|
||||||
|
.sync()
|
||||||
|
.then(() => console.log("Tables synced successfully!"))
|
||||||
|
.catch((err) => console.log(err));
|
||||||
|
|
||||||
fs.readdirSync(__dirname)
|
fs.readdirSync(__dirname)
|
||||||
.filter((file) => {
|
.filter((file) => {
|
||||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
return (
|
||||||
|
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.forEach((file) => {
|
.forEach((file) => {
|
||||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||||
@@ -66,4 +76,4 @@ Object.keys(db).forEach((modelName) => {
|
|||||||
db.sequelize = sequelize;
|
db.sequelize = sequelize;
|
||||||
db.Sequelize = Sequelize;
|
db.Sequelize = Sequelize;
|
||||||
|
|
||||||
module.exports = db;
|
module.exports = db;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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
@@ -3,7 +3,8 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./bin/www"
|
"start": "node ./bin/www",
|
||||||
|
"dev": "node --watch --env-file=.env ./bin/www"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie-parser": "~1.4.4",
|
"cookie-parser": "~1.4.4",
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
"jade": "~1.11.0",
|
"jade": "~1.11.0",
|
||||||
"morgan": "~1.9.1",
|
"morgan": "~1.9.1",
|
||||||
"mysql2": "^2.3.3",
|
"mysql2": "^2.3.3",
|
||||||
"sequelize": "^6.15.1"
|
"sequelize": "^6.15.1",
|
||||||
|
"stripe": "^18.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-3
@@ -1,9 +1,113 @@
|
|||||||
var express = require('express');
|
var express = require("express");
|
||||||
var router = express.Router();
|
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. */
|
/* GET home page. */
|
||||||
router.get('/', function(req, res, next) {
|
router.get("/", async function (req, res, next) {
|
||||||
res.render('index', { title: 'Express' });
|
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;
|
module.exports = router;
|
||||||
|
|||||||
@@ -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
@@ -2,4 +2,19 @@ extends layout
|
|||||||
|
|
||||||
block content
|
block content
|
||||||
h1= title
|
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.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ doctype html
|
|||||||
html
|
html
|
||||||
head
|
head
|
||||||
title= title
|
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')
|
link(rel='stylesheet', href='/stylesheets/style.css')
|
||||||
body
|
body
|
||||||
block content
|
block content
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -1,13 +1,38 @@
|
|||||||
let fs = require('fs');
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
function Model_builder() {
|
class ModelBuilder {
|
||||||
let config = fs.readFileSync('configuration.json');
|
static build() {
|
||||||
|
const config = require("./configuration.json");
|
||||||
|
const modelDir = path.join(__dirname, "release/models");
|
||||||
|
|
||||||
this.build = function () {
|
// Create release/models directory
|
||||||
//generate files and put it into release folder
|
if (!fs.existsSync(modelDir)) fs.mkdirSync(modelDir, { recursive: true });
|
||||||
//Copy initialize files into release folder
|
|
||||||
//TODO
|
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
@@ -1,37 +1,37 @@
|
|||||||
{
|
{
|
||||||
"model": [
|
"model": [
|
||||||
{
|
{
|
||||||
"name": "location",
|
"name": "location",
|
||||||
"field: [
|
"field": [
|
||||||
["id", "integer", "ID", "required"],
|
["id", "integer", "ID", "required"],
|
||||||
["name", "string", "Name", "required"],
|
["name", "string", "Name", "required"],
|
||||||
["status", "integer", "Status", "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"],
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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
@@ -3,9 +3,4 @@
|
|||||||
## Instructions
|
## Instructions
|
||||||
|
|
||||||
- setup project
|
- setup project
|
||||||
- clone to your github
|
- implement https://www.treeql.org/ manually
|
||||||
- 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
|
|
||||||
|
|||||||
+100
@@ -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}`));
|
||||||
@@ -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;
|
||||||
@@ -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 };
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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?
|
||||||
@@ -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_]' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 |
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
@@ -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;
|
||||||
@@ -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 |
@@ -0,0 +1,7 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
@@ -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>,
|
||||||
|
)
|
||||||
@@ -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
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
## Instructions
|
## Instructions
|
||||||
|
|
||||||
- setup project
|
- Setup project
|
||||||
- clone to your github
|
- Clone to your github
|
||||||
- Make the frontend for this project https://www.figma.com/file/iaKhmTAN28YiYXAOZAr9rN/Scheduler-Task?node-id=0%3A1
|
- Check this https://www.figma.com/file/iaKhmTAN28YiYXAOZAr9rN/Scheduler-Task?node-id=0%3A1
|
||||||
- timezones are generated dynamically
|
- Convert it into ejs/eta.
|
||||||
|
- Timezones should be generated dynamically
|
||||||
|
- Make a book schedule API like calendly.
|
||||||
|
|||||||
+17
-16
@@ -1,11 +1,12 @@
|
|||||||
var createError = require('http-errors');
|
var createError = require("http-errors");
|
||||||
var express = require('express');
|
var express = require("express");
|
||||||
var path = require('path');
|
var path = require("path");
|
||||||
var cookieParser = require('cookie-parser');
|
var cookieParser = require("cookie-parser");
|
||||||
var logger = require('morgan');
|
var logger = require("morgan");
|
||||||
|
|
||||||
var indexRouter = require('./routes/index');
|
var indexRouter = require("./routes/index");
|
||||||
var usersRouter = require('./routes/users');
|
var usersRouter = require("./routes/users");
|
||||||
|
var apiRouter = require("./routes/api");
|
||||||
|
|
||||||
const db = require("./models");
|
const db = require("./models");
|
||||||
var cors = require("cors");
|
var cors = require("cors");
|
||||||
@@ -13,18 +14,18 @@ var cors = require("cors");
|
|||||||
var app = express();
|
var app = express();
|
||||||
app.set("db", db);
|
app.set("db", db);
|
||||||
// view engine setup
|
// view engine setup
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
app.set("views", path.join(__dirname, "views"));
|
||||||
app.set('view engine', 'jade');
|
app.set("view engine", "ejs");
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(logger('dev'));
|
app.use(logger("dev"));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: false }));
|
app.use(express.urlencoded({ extended: false }));
|
||||||
app.use(cookieParser());
|
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", apiRouter);
|
||||||
// catch 404 and forward to error handler
|
// catch 404 and forward to error handler
|
||||||
app.use(function (req, res, next) {
|
app.use(function (req, res, next) {
|
||||||
next(createError(404));
|
next(createError(404));
|
||||||
@@ -34,11 +35,11 @@ app.use(function (req, res, next) {
|
|||||||
app.use(function (err, req, res, next) {
|
app.use(function (err, req, res, next) {
|
||||||
// set locals, only providing error in development
|
// set locals, only providing error in development
|
||||||
res.locals.message = err.message;
|
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
|
// render the error page
|
||||||
res.status(err.status || 500);
|
res.status(err.status || 500);
|
||||||
res.render('error');
|
res.render("error");
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
module.exports = (sequelize, DataTypes) => {
|
||||||
|
const booking = sequelize.define(
|
||||||
|
"booking",
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
name: DataTypes.STRING,
|
||||||
|
email: DataTypes.STRING,
|
||||||
|
company: DataTypes.STRING,
|
||||||
|
phone: DataTypes.STRING,
|
||||||
|
notes: DataTypes.TEXT,
|
||||||
|
date: DataTypes.STRING,
|
||||||
|
time: DataTypes.STRING,
|
||||||
|
timezone: DataTypes.STRING,
|
||||||
|
created_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
freezeTableName: true,
|
||||||
|
tableName: "booking",
|
||||||
|
createdAt: "created_at",
|
||||||
|
updatedAt: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return booking;
|
||||||
|
};
|
||||||
+48
-34
@@ -1,4 +1,4 @@
|
|||||||
'use strict';
|
"use strict";
|
||||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||||
/**
|
/**
|
||||||
* Sequelize File
|
* Sequelize File
|
||||||
@@ -8,49 +8,63 @@
|
|||||||
* @author Ryan Wong
|
* @author Ryan Wong
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
let Sequelize = require('sequelize');
|
let Sequelize = require("sequelize");
|
||||||
const basename = path.basename(__filename);
|
const basename = path.basename(__filename);
|
||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require("sequelize");
|
||||||
const config = {
|
const config = {
|
||||||
DB_DATABASE: 'mysql',
|
DB_DATABASE: "mysql",
|
||||||
DB_USERNAME: 'root',
|
DB_USERNAME: "root",
|
||||||
DB_PASSWORD: 'root',
|
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||||
DB_ADAPTER: 'mysql',
|
DB_ADAPTER: "mysql",
|
||||||
DB_NAME: 'day_1',
|
DB_NAME: "day_17",
|
||||||
DB_HOSTNAME: 'localhost',
|
DB_HOSTNAME: "localhost",
|
||||||
DB_PORT: 3306,
|
DB_PORT: 3306,
|
||||||
};
|
};
|
||||||
|
|
||||||
let db = {};
|
let db = {};
|
||||||
|
|
||||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
let sequelize = new Sequelize(
|
||||||
dialect: config.DB_ADAPTER,
|
config.DB_NAME,
|
||||||
username: config.DB_USERNAME,
|
config.DB_USERNAME,
|
||||||
password: config.DB_PASSWORD,
|
config.DB_PASSWORD,
|
||||||
database: config.DB_NAME,
|
{
|
||||||
host: config.DB_HOSTNAME,
|
dialect: config.DB_ADAPTER,
|
||||||
port: config.DB_PORT,
|
username: config.DB_USERNAME,
|
||||||
logging: console.log,
|
password: config.DB_PASSWORD,
|
||||||
timezone: '-04:00',
|
database: config.DB_NAME,
|
||||||
pool: {
|
host: config.DB_HOSTNAME,
|
||||||
maxConnections: 1,
|
port: config.DB_PORT,
|
||||||
minConnections: 0,
|
logging: console.log,
|
||||||
maxIdleTime: 100,
|
timezone: "-04:00",
|
||||||
},
|
pool: {
|
||||||
define: {
|
maxConnections: 1,
|
||||||
timestamps: false,
|
minConnections: 0,
|
||||||
underscoredAll: true,
|
maxIdleTime: 100,
|
||||||
underscored: true,
|
},
|
||||||
},
|
define: {
|
||||||
});
|
timestamps: false,
|
||||||
|
underscoredAll: true,
|
||||||
|
underscored: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// sequelize.sync({ force: true });
|
sequelize
|
||||||
|
.sync()
|
||||||
|
.then(() => {
|
||||||
|
console.log("Database & tables created!");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
|
||||||
fs.readdirSync(__dirname)
|
fs.readdirSync(__dirname)
|
||||||
.filter((file) => {
|
.filter((file) => {
|
||||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
return (
|
||||||
|
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.forEach((file) => {
|
.forEach((file) => {
|
||||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||||
@@ -66,4 +80,4 @@ Object.keys(db).forEach((modelName) => {
|
|||||||
db.sequelize = sequelize;
|
db.sequelize = sequelize;
|
||||||
db.Sequelize = Sequelize;
|
db.Sequelize = Sequelize;
|
||||||
|
|
||||||
module.exports = db;
|
module.exports = db;
|
||||||
|
|||||||
+5
-1
@@ -3,17 +3,21 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./bin/www"
|
"start": "node ./bin/www",
|
||||||
|
"dev": "node --watch --env-file=.env ./bin/www"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cookie-parser": "~1.4.4",
|
"cookie-parser": "~1.4.4",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"debug": "~2.6.9",
|
"debug": "~2.6.9",
|
||||||
|
"ejs": "^3.1.10",
|
||||||
"express": "~4.16.1",
|
"express": "~4.16.1",
|
||||||
"http-errors": "~1.6.3",
|
"http-errors": "~1.6.3",
|
||||||
"jade": "~1.11.0",
|
"jade": "~1.11.0",
|
||||||
|
"moment-timezone": "^0.6.0",
|
||||||
"morgan": "~1.9.1",
|
"morgan": "~1.9.1",
|
||||||
"mysql2": "^2.3.3",
|
"mysql2": "^2.3.3",
|
||||||
|
"node-input-validator": "^4.5.1",
|
||||||
"sequelize": "^6.15.1"
|
"sequelize": "^6.15.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,289 @@
|
|||||||
body {
|
body {
|
||||||
padding: 50px;
|
margin: 0;
|
||||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
padding: 0;
|
||||||
|
font-family: "Inter", "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||||
|
background: #eaeaea;
|
||||||
|
color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
.calendar-container {
|
||||||
color: #00B7FF;
|
max-width: 1100px;
|
||||||
|
margin: 40px auto;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.07);
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header {
|
||||||
|
background: #04316a;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 28px 32px;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-content {
|
||||||
|
padding: 32px 32px 0 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-labels {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.calendar-label-main {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.calendar-label-duration {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.calendar-label-duration span {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.calendar-label-timezone {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.timezone-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #888;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Overlay */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 32px 40px;
|
||||||
|
min-width: 340px;
|
||||||
|
max-width: 90vw;
|
||||||
|
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.15);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.modal-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.modal-format-switch {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
font-size: 0.98rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.timezone-groups {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 32px 48px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.timezone-group {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
.timezone-group-title {
|
||||||
|
font-size: 0.98rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #04316a;
|
||||||
|
}
|
||||||
|
.timezone-option {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 0.97rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Calendar Table */
|
||||||
|
.calendar-table-wrapper {
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.calendar-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.calendar-table th,
|
||||||
|
.calendar-table td {
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
.calendar-table th {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #04316a;
|
||||||
|
border-bottom: 2px solid #eaeaea;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
.calendar-day-label {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.calendar-date-label {
|
||||||
|
font-size: 0.98rem;
|
||||||
|
color: #888;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
.calendar-slot-btn {
|
||||||
|
background: #f5f8fa;
|
||||||
|
border: 1px solid #dbe6f3;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #04316a;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 7px 0;
|
||||||
|
width: 90%;
|
||||||
|
margin: 0 auto;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border 0.15s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.calendar-slot-btn:hover {
|
||||||
|
background: #e6f0ff;
|
||||||
|
border-color: #04316a;
|
||||||
|
}
|
||||||
|
.calendar-table td {
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-week-nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 18px;
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.calendar-week-nav a {
|
||||||
|
color: #04316a;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Booking Form */
|
||||||
|
.booking-form {
|
||||||
|
max-width: 420px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
.form-error {
|
||||||
|
color: red;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.form-group label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
.form-group input,
|
||||||
|
.form-group textarea {
|
||||||
|
border: 1px solid #dbe6f3;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: inherit;
|
||||||
|
background: #f5f8fa;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
.form-group textarea {
|
||||||
|
min-height: 70px;
|
||||||
|
}
|
||||||
|
.form-submit-btn {
|
||||||
|
background: #04316a;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 1.08rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin-top: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.form-submit-btn:hover {
|
||||||
|
background: #0050b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Success Message */
|
||||||
|
.success-message {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Styles */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.calendar-container {
|
||||||
|
max-width: 98vw;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
.calendar-content {
|
||||||
|
padding: 18px 6vw 0 6vw;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
padding: 18px 8vw;
|
||||||
|
}
|
||||||
|
.calendar-table th,
|
||||||
|
.calendar-table td {
|
||||||
|
min-width: 80px;
|
||||||
|
font-size: 0.98rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.calendar-header {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding: 18px 10px;
|
||||||
|
}
|
||||||
|
.calendar-content {
|
||||||
|
padding: 10px 2vw 0 2vw;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
min-width: 90vw;
|
||||||
|
padding: 10px 2vw;
|
||||||
|
}
|
||||||
|
.calendar-table th,
|
||||||
|
.calendar-table td {
|
||||||
|
min-width: 60px;
|
||||||
|
font-size: 0.93rem;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.booking-form {
|
||||||
|
max-width: 98vw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const db = require("../models");
|
||||||
|
const {
|
||||||
|
validateInput,
|
||||||
|
handleValidationErrorForAPI,
|
||||||
|
} = require("../services/ValidationService");
|
||||||
|
|
||||||
|
// Validation rules for booking
|
||||||
|
const bookingValidation = {
|
||||||
|
name: "required|string",
|
||||||
|
email: "required|email",
|
||||||
|
company: "required|string",
|
||||||
|
phone: "required|string",
|
||||||
|
notes: "required|string",
|
||||||
|
date: "required|string",
|
||||||
|
time: "required|string",
|
||||||
|
timezone: "required|string",
|
||||||
|
};
|
||||||
|
|
||||||
|
// POST /api/bookings - Create a new booking
|
||||||
|
router.post(
|
||||||
|
"/bookings",
|
||||||
|
validateInput(bookingValidation, {
|
||||||
|
"name.required": "Name is required",
|
||||||
|
"email.required": "Email is required",
|
||||||
|
"email.email": "Invalid email address",
|
||||||
|
"company.required": "Company is required",
|
||||||
|
"phone.required": "Phone is required",
|
||||||
|
"notes.required": "Notes are required",
|
||||||
|
"date.required": "Date is required",
|
||||||
|
"time.required": "Time is required",
|
||||||
|
"timezone.required": "Timezone is required",
|
||||||
|
}),
|
||||||
|
handleValidationErrorForAPI,
|
||||||
|
async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name, email, company, phone, notes, date, time, timezone } =
|
||||||
|
req.body;
|
||||||
|
const booking = await db.booking.create({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
company,
|
||||||
|
phone,
|
||||||
|
notes,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
timezone,
|
||||||
|
});
|
||||||
|
res.status(201).json({ success: true, booking });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /api/bookings - List all bookings
|
||||||
|
router.get("/bookings", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const bookings = await db.booking.findAll({
|
||||||
|
order: [["created_at", "DESC"]],
|
||||||
|
});
|
||||||
|
res.json({ bookings });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
+134
-4
@@ -1,9 +1,139 @@
|
|||||||
var express = require('express');
|
var express = require("express");
|
||||||
var router = express.Router();
|
var router = express.Router();
|
||||||
|
const moment = require("moment-timezone");
|
||||||
|
|
||||||
/* GET home page. */
|
// Helper: Group timezones by region for the timezone selection screen
|
||||||
router.get('/', function(req, res, next) {
|
function getTimezoneGroups() {
|
||||||
res.render('index', { title: 'Express' });
|
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;
|
module.exports = router;
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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') %>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<%- include('partials/header') %>
|
||||||
|
<div class="calendar-container">
|
||||||
|
<div class="calendar-header">Error</div>
|
||||||
|
<div class="calendar-content">
|
||||||
|
<h1><%= message %></h1>
|
||||||
|
<h2><%= error.status %></h2>
|
||||||
|
<pre><%= error.stack %></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%- include('partials/footer') %>
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
extends layout
|
|
||||||
|
|
||||||
block content
|
|
||||||
h1= message
|
|
||||||
h2= error.status
|
|
||||||
pre #{error.stack}
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<%- include('partials/header') %>
|
||||||
|
<div class="calendar-container">
|
||||||
|
<div class="calendar-header"><%= title %></div>
|
||||||
|
<div class="calendar-content">
|
||||||
|
<p>Welcome to <%= title %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<%- include('partials/footer') %>
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
extends layout
|
|
||||||
|
|
||||||
block content
|
|
||||||
h1= title
|
|
||||||
p Welcome to #{title}
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title><%= title %></title>
|
||||||
|
<link rel="stylesheet" href="/stylesheets/style.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%- body %>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
doctype html
|
|
||||||
html
|
|
||||||
head
|
|
||||||
title= title
|
|
||||||
link(rel='stylesheet', href='/stylesheets/style.css')
|
|
||||||
body
|
|
||||||
block content
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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>
|
||||||
@@ -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') %>
|
||||||
@@ -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
@@ -1,11 +1,12 @@
|
|||||||
var createError = require('http-errors');
|
var createError = require("http-errors");
|
||||||
var express = require('express');
|
var express = require("express");
|
||||||
var path = require('path');
|
var path = require("path");
|
||||||
var cookieParser = require('cookie-parser');
|
var cookieParser = require("cookie-parser");
|
||||||
var logger = require('morgan');
|
var logger = require("morgan");
|
||||||
|
|
||||||
var indexRouter = require('./routes/index');
|
var indexRouter = require("./routes/index");
|
||||||
var usersRouter = require('./routes/users');
|
var usersRouter = require("./routes/users");
|
||||||
|
var scheduleRouter = require("./routes/schedule");
|
||||||
|
|
||||||
const db = require("./models");
|
const db = require("./models");
|
||||||
var cors = require("cors");
|
var cors = require("cors");
|
||||||
@@ -13,17 +14,18 @@ var cors = require("cors");
|
|||||||
var app = express();
|
var app = express();
|
||||||
app.set("db", db);
|
app.set("db", db);
|
||||||
// view engine setup
|
// view engine setup
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
app.set("views", path.join(__dirname, "views"));
|
||||||
app.set('view engine', 'jade');
|
app.set("view engine", "jade");
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(logger('dev'));
|
app.use(logger("dev"));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: false }));
|
app.use(express.urlencoded({ extended: false }));
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, "public")));
|
||||||
|
|
||||||
app.use('/', indexRouter);
|
app.use("/", indexRouter);
|
||||||
app.use('/users', usersRouter);
|
app.use("/users", usersRouter);
|
||||||
|
app.use("/api/v1", scheduleRouter);
|
||||||
|
|
||||||
// catch 404 and forward to error handler
|
// catch 404 and forward to error handler
|
||||||
app.use(function (req, res, next) {
|
app.use(function (req, res, next) {
|
||||||
@@ -34,11 +36,11 @@ app.use(function (req, res, next) {
|
|||||||
app.use(function (err, req, res, next) {
|
app.use(function (err, req, res, next) {
|
||||||
// set locals, only providing error in development
|
// set locals, only providing error in development
|
||||||
res.locals.message = err.message;
|
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
|
// render the error page
|
||||||
res.status(err.status || 500);
|
res.status(err.status || 500);
|
||||||
res.render('error');
|
res.render("error");
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
|
|||||||
@@ -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
@@ -1,4 +1,4 @@
|
|||||||
'use strict';
|
"use strict";
|
||||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||||
/**
|
/**
|
||||||
* Sequelize File
|
* Sequelize File
|
||||||
@@ -8,49 +8,63 @@
|
|||||||
* @author Ryan Wong
|
* @author Ryan Wong
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
let Sequelize = require('sequelize');
|
let Sequelize = require("sequelize");
|
||||||
const basename = path.basename(__filename);
|
const basename = path.basename(__filename);
|
||||||
const { DataTypes } = require('sequelize');
|
const { DataTypes } = require("sequelize");
|
||||||
const config = {
|
const config = {
|
||||||
DB_DATABASE: 'mysql',
|
DB_DATABASE: "mysql",
|
||||||
DB_USERNAME: 'root',
|
DB_USERNAME: "root",
|
||||||
DB_PASSWORD: 'root',
|
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||||
DB_ADAPTER: 'mysql',
|
DB_ADAPTER: "mysql",
|
||||||
DB_NAME: 'day_1',
|
DB_NAME: "day_19",
|
||||||
DB_HOSTNAME: 'localhost',
|
DB_HOSTNAME: "localhost",
|
||||||
DB_PORT: 3306,
|
DB_PORT: 3306,
|
||||||
};
|
};
|
||||||
|
|
||||||
let db = {};
|
let db = {};
|
||||||
|
|
||||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
let sequelize = new Sequelize(
|
||||||
dialect: config.DB_ADAPTER,
|
config.DB_NAME,
|
||||||
username: config.DB_USERNAME,
|
config.DB_USERNAME,
|
||||||
password: config.DB_PASSWORD,
|
config.DB_PASSWORD,
|
||||||
database: config.DB_NAME,
|
{
|
||||||
host: config.DB_HOSTNAME,
|
dialect: config.DB_ADAPTER,
|
||||||
port: config.DB_PORT,
|
username: config.DB_USERNAME,
|
||||||
logging: console.log,
|
password: config.DB_PASSWORD,
|
||||||
timezone: '-04:00',
|
database: config.DB_NAME,
|
||||||
pool: {
|
host: config.DB_HOSTNAME,
|
||||||
maxConnections: 1,
|
port: config.DB_PORT,
|
||||||
minConnections: 0,
|
logging: console.log,
|
||||||
maxIdleTime: 100,
|
timezone: "-04:00",
|
||||||
},
|
pool: {
|
||||||
define: {
|
maxConnections: 1,
|
||||||
timestamps: false,
|
minConnections: 0,
|
||||||
underscoredAll: true,
|
maxIdleTime: 100,
|
||||||
underscored: true,
|
},
|
||||||
},
|
define: {
|
||||||
});
|
timestamps: false,
|
||||||
|
underscoredAll: true,
|
||||||
|
underscored: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// sequelize.sync({ force: true });
|
sequelize
|
||||||
|
.sync()
|
||||||
|
.then(() => {
|
||||||
|
console.log("Database & tables created!");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
|
||||||
fs.readdirSync(__dirname)
|
fs.readdirSync(__dirname)
|
||||||
.filter((file) => {
|
.filter((file) => {
|
||||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
return (
|
||||||
|
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.forEach((file) => {
|
.forEach((file) => {
|
||||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||||
@@ -66,4 +80,4 @@ Object.keys(db).forEach((modelName) => {
|
|||||||
db.sequelize = sequelize;
|
db.sequelize = sequelize;
|
||||||
db.Sequelize = Sequelize;
|
db.Sequelize = Sequelize;
|
||||||
|
|
||||||
module.exports = db;
|
module.exports = db;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user