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
|
||||
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
|
||||
- clone to your github
|
||||
- Read the documentation https://sequelize.org/docs/v6/getting-started/
|
||||
- Read the documentation https://sequelize.org/docs/v7/getting-started/
|
||||
- Setup the following Models in models folder. Make sure tables made by sequelize:
|
||||
|
||||
```
|
||||
|
||||
+24
-15
@@ -1,29 +1,38 @@
|
||||
var createError = require('http-errors');
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
var createError = require("http-errors");
|
||||
var express = require("express");
|
||||
var path = require("path");
|
||||
var cookieParser = require("cookie-parser");
|
||||
var logger = require("morgan");
|
||||
|
||||
var indexRouter = require('./routes/index');
|
||||
var usersRouter = require('./routes/users');
|
||||
var indexRouter = require("./routes/index");
|
||||
var usersRouter = require("./routes/users");
|
||||
|
||||
const db = require("./models");
|
||||
var cors = require("cors");
|
||||
|
||||
const shippingDockRouter = require("./routes/shippingDock");
|
||||
const orderRouter = require("./routes/order");
|
||||
const transactionRouter = require("./routes/transaction");
|
||||
|
||||
var app = express();
|
||||
app.set("db", db);
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'jade');
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
app.set("view engine", "jade");
|
||||
app.use(cors());
|
||||
app.use(logger('dev'));
|
||||
app.use(logger("dev"));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
app.use('/', indexRouter);
|
||||
app.use('/users', usersRouter);
|
||||
app.use("/", indexRouter);
|
||||
app.use("/users", usersRouter);
|
||||
|
||||
// ROUTES
|
||||
app.use("/api/v1/shipping_dock", shippingDockRouter);
|
||||
app.use("/api/v1/order", orderRouter);
|
||||
app.use("/api/v1/transaction", transactionRouter);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function (req, res, next) {
|
||||
@@ -34,11 +43,11 @@ app.use(function (req, res, next) {
|
||||
app.use(function (err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
res.locals.error = req.app.get("env") === "development" ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
res.render("error");
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
+16
-19
@@ -4,16 +4,16 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var app = require('../app');
|
||||
var debug = require('debug')('day-1:server');
|
||||
var http = require('http');
|
||||
var app = require("../app");
|
||||
var debug = require("debug")("day-1:server");
|
||||
var http = require("http");
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
var port = normalizePort(process.env.PORT || '3000');
|
||||
app.set('port', port);
|
||||
var port = normalizePort(process.env.PORT || "3000");
|
||||
app.set("port", port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
@@ -26,8 +26,8 @@ var server = http.createServer(app);
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
server.on("error", onError);
|
||||
server.on("listening", onListening);
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
@@ -54,22 +54,20 @@ function normalizePort(val) {
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
if (error.syscall !== "listen") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
var bind = typeof port === "string" ? "Pipe " + port : "Port " + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
case "EACCES":
|
||||
console.error(bind + " requires elevated privileges");
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
case "EADDRINUSE":
|
||||
console.error(bind + " is already in use");
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
@@ -83,8 +81,7 @@ function onError(error) {
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
debug('Listening on ' + bind);
|
||||
var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
|
||||
debug("Listening on " + bind);
|
||||
console.log("Server is listening on " + bind);
|
||||
}
|
||||
|
||||
+48
-34
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
"use strict";
|
||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||
/**
|
||||
* Sequelize File
|
||||
@@ -8,49 +8,63 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let Sequelize = require('sequelize');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
let Sequelize = require("sequelize");
|
||||
const basename = path.basename(__filename);
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { DataTypes } = require("sequelize");
|
||||
const config = {
|
||||
DB_DATABASE: 'mysql',
|
||||
DB_USERNAME: 'root',
|
||||
DB_PASSWORD: 'root',
|
||||
DB_ADAPTER: 'mysql',
|
||||
DB_NAME: 'day_1',
|
||||
DB_HOSTNAME: 'localhost',
|
||||
DB_DATABASE: "mysql",
|
||||
DB_USERNAME: "root",
|
||||
DB_PASSWORD: process.env.DB_PASSWORD,
|
||||
DB_ADAPTER: "mysql",
|
||||
DB_NAME: "day_1",
|
||||
DB_HOSTNAME: "localhost",
|
||||
DB_PORT: 3306,
|
||||
};
|
||||
|
||||
let db = {};
|
||||
|
||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: '-04:00',
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
define: {
|
||||
timestamps: false,
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
});
|
||||
let sequelize = new Sequelize(
|
||||
config.DB_NAME,
|
||||
config.DB_USERNAME,
|
||||
config.DB_PASSWORD,
|
||||
{
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: "-04:00",
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
define: {
|
||||
timestamps: false,
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// sequelize.sync({ force: true });
|
||||
sequelize
|
||||
.sync()
|
||||
.then(() => {
|
||||
console.log("All tables synced successfully.");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to sync tables:", err);
|
||||
});
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter((file) => {
|
||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
||||
return (
|
||||
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||
);
|
||||
})
|
||||
.forEach((file) => {
|
||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||
@@ -66,4 +80,4 @@ Object.keys(db).forEach((modelName) => {
|
||||
db.sequelize = sequelize;
|
||||
db.Sequelize = Sequelize;
|
||||
|
||||
module.exports = db;
|
||||
module.exports = db;
|
||||
|
||||
+4
-10
@@ -1,5 +1,5 @@
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
const location = sequelize.define(
|
||||
return sequelize.define(
|
||||
"location",
|
||||
{
|
||||
id: {
|
||||
@@ -8,19 +8,13 @@ module.exports = (sequelize, DataTypes) => {
|
||||
autoIncrement: true,
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
created_at: DataTypes.DATEONLY,
|
||||
updated_at: DataTypes.DATE,
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
freezeTableName: true,
|
||||
tableName: "location",
|
||||
},
|
||||
{
|
||||
underscoredAll: false,
|
||||
underscored: false,
|
||||
createdAt: "created_at",
|
||||
updatedAt: "updated_at",
|
||||
}
|
||||
);
|
||||
|
||||
return location;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
"start": "node ./bin/www",
|
||||
"dev": "node --watch --env-file=.env ./bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie-parser": "~1.4.4",
|
||||
|
||||
@@ -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 express = require('express');
|
||||
var path = require('path');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
var createError = require("http-errors");
|
||||
var express = require("express");
|
||||
var path = require("path");
|
||||
var cookieParser = require("cookie-parser");
|
||||
var logger = require("morgan");
|
||||
|
||||
var indexRouter = require('./routes/index');
|
||||
var usersRouter = require('./routes/users');
|
||||
var indexRouter = require("./routes/index");
|
||||
var usersRouter = require("./routes/users");
|
||||
const codeRouter = require("./routes/code");
|
||||
|
||||
const db = require("./models");
|
||||
var cors = require("cors");
|
||||
@@ -13,17 +14,18 @@ var cors = require("cors");
|
||||
var app = express();
|
||||
app.set("db", db);
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'jade');
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
app.set("view engine", "jade");
|
||||
app.use(cors());
|
||||
app.use(logger('dev'));
|
||||
app.use(logger("dev"));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
app.use('/', indexRouter);
|
||||
app.use('/users', usersRouter);
|
||||
app.use("/", indexRouter);
|
||||
app.use("/users", usersRouter);
|
||||
app.use("/api/v1/code", codeRouter);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function (req, res, next) {
|
||||
@@ -34,11 +36,11 @@ app.use(function (req, res, next) {
|
||||
app.use(function (err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
res.locals.error = req.app.get("env") === "development" ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
res.render("error");
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
+16
-19
@@ -4,16 +4,16 @@
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var app = require('../app');
|
||||
var debug = require('debug')('day-1:server');
|
||||
var http = require('http');
|
||||
var app = require("../app");
|
||||
var debug = require("debug")("day-1:server");
|
||||
var http = require("http");
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
var port = normalizePort(process.env.PORT || '3000');
|
||||
app.set('port', port);
|
||||
var port = normalizePort(process.env.PORT || "3000");
|
||||
app.set("port", port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
@@ -26,8 +26,8 @@ var server = http.createServer(app);
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
server.on("error", onError);
|
||||
server.on("listening", onListening);
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
@@ -54,22 +54,20 @@ function normalizePort(val) {
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
if (error.syscall !== "listen") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
var bind = typeof port === "string" ? "Pipe " + port : "Port " + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
case "EACCES":
|
||||
console.error(bind + " requires elevated privileges");
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
case "EADDRINUSE":
|
||||
console.error(bind + " is already in use");
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
@@ -83,8 +81,7 @@ function onError(error) {
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
debug('Listening on ' + bind);
|
||||
var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
|
||||
debug("Listening on " + bind);
|
||||
console.log("Server listening on:", bind);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
"start": "node ./bin/www",
|
||||
"dev": "node --watch --env-file=.env ./bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie-parser": "~1.4.4",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "~2.6.9",
|
||||
"express": "~4.16.1",
|
||||
"html-pdf-node": "^1.0.8",
|
||||
"http-errors": "~1.6.3",
|
||||
"jade": "~1.11.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"morgan": "~1.9.1",
|
||||
"mysql2": "^2.3.3",
|
||||
"node-input-validator": "^4.5.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"sequelize": "^6.15.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
const QRCode = require("qrcode");
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', function(req, res, next) {
|
||||
res.render('index', { title: 'Express' });
|
||||
router.get("/", function (req, res, next) {
|
||||
res.render("index", { title: "Express" });
|
||||
});
|
||||
|
||||
router.get("/code", async function (req, res, next) {
|
||||
const code = Math.random().toString(36).substring(2, 8); // random code
|
||||
const qrUrl = `/api/v1/code/${code}?amount=1&service=software%20service`;
|
||||
const qrData = await QRCode.toDataURL(
|
||||
`http://localhost:${process.env.PORT || 3000}${qrUrl}`
|
||||
);
|
||||
res.render("code", {
|
||||
qrData,
|
||||
qrUrl: `http://localhost:${process.env.PORT || 3000}${qrUrl}`,
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -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*/
|
||||
/**
|
||||
* App
|
||||
@@ -8,52 +8,52 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
require('dotenv').config()
|
||||
const express = require('express')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const logger = require('morgan')
|
||||
const helmet = require('helmet')
|
||||
const cookieParser = require('cookie-parser')
|
||||
const cors = require('cors')
|
||||
const { ApolloServer } = require('apollo-server-express')
|
||||
const { graphqlUploadExpress } = require('graphql-upload')
|
||||
const body_parser = require('body-parser')
|
||||
require("dotenv").config();
|
||||
const express = require("express");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const logger = require("morgan");
|
||||
const helmet = require("helmet");
|
||||
const cookieParser = require("cookie-parser");
|
||||
const cors = require("cors");
|
||||
const { ApolloServer } = require("apollo-server-express");
|
||||
const { graphqlUploadExpress } = require("graphql-upload");
|
||||
const body_parser = require("body-parser");
|
||||
|
||||
const db = require('./models')
|
||||
const db = require("./models");
|
||||
const typeDefs = fs.readFileSync(
|
||||
path.join(__dirname, '/types/schema.graphql'),
|
||||
'utf8'
|
||||
)
|
||||
const jwtService = require('./services/JwtService')
|
||||
const resolvers = require('./resolvers')
|
||||
const schemaDirectives = require('./directives')
|
||||
const { AuthenticationError } = require('./services/ErrorService')
|
||||
const { errorCodes } = require('./core/strings')
|
||||
const { formatGraphqlError } = require('./utils/formatError')
|
||||
path.join(__dirname, "/types/schema.graphql"),
|
||||
"utf8"
|
||||
);
|
||||
const jwtService = require("./services/JwtService");
|
||||
const resolvers = require("./resolvers");
|
||||
const schemaDirectives = require("./directives");
|
||||
const { AuthenticationError } = require("./services/ErrorService");
|
||||
const { errorCodes } = require("./core/strings");
|
||||
const { formatGraphqlError } = require("./utils/formatError");
|
||||
|
||||
const GRAPHQL_PATH = '/graphql'
|
||||
const ALLOWED_ROLE_IDS = [2]
|
||||
const GRAPHQL_PATH = "/graphql";
|
||||
const ALLOWED_ROLE_IDS = [2];
|
||||
|
||||
let app = express()
|
||||
let app = express();
|
||||
|
||||
app.use(logger('dev'))
|
||||
app.use(logger("dev"));
|
||||
|
||||
if (process.env.MODE === 'development') {
|
||||
logger.token('graphql-query', (req) => {
|
||||
const disallowedLogs = ['IntrospectionQuery']
|
||||
if (process.env.MODE === "development") {
|
||||
logger.token("graphql-query", (req) => {
|
||||
const disallowedLogs = ["IntrospectionQuery"];
|
||||
|
||||
if (req.method === 'POST' && req.originalUrl === GRAPHQL_PATH) {
|
||||
const { query, variables, operationName } = req.body
|
||||
if (req.method === "POST" && req.originalUrl === GRAPHQL_PATH) {
|
||||
const { query, variables, operationName } = req.body;
|
||||
return !disallowedLogs.includes(operationName)
|
||||
? `GRAPHQL: \nOperation Name: ${operationName} \nQuery: ${query} \nVariables: ${JSON.stringify(
|
||||
variables
|
||||
)}`
|
||||
: ''
|
||||
variables
|
||||
)}`
|
||||
: "";
|
||||
}
|
||||
return ''
|
||||
})
|
||||
app.use(logger(':graphql-query'))
|
||||
return "";
|
||||
});
|
||||
app.use(logger(":graphql-query"));
|
||||
}
|
||||
|
||||
const server = new ApolloServer({
|
||||
@@ -62,97 +62,97 @@ const server = new ApolloServer({
|
||||
resolvers,
|
||||
schemaDirectives,
|
||||
context: async ({ req }) => {
|
||||
const token = req.headers.authorization
|
||||
// const token = req.headers.authorization
|
||||
|
||||
if (!token) {
|
||||
throw new AuthenticationError(
|
||||
'Invalid token',
|
||||
errorCodes.token.INVALID_TOKEN
|
||||
)
|
||||
}
|
||||
const cleanToken = token.replace('Bearer ', '')
|
||||
const verify = jwtService.verifyAccessToken(cleanToken)
|
||||
// if (!token) {
|
||||
// throw new AuthenticationError(
|
||||
// 'Invalid token',
|
||||
// errorCodes.token.INVALID_TOKEN
|
||||
// )
|
||||
// }
|
||||
// const cleanToken = token.replace('Bearer ', '')
|
||||
// const verify = jwtService.verifyAccessToken(cleanToken)
|
||||
|
||||
const roleId = verify?.role_id
|
||||
const user = verify?.user
|
||||
const credentialId = verify?.credential_id
|
||||
// const roleId = verify?.role_id
|
||||
// const user = verify?.user
|
||||
// const credentialId = verify?.credential_id
|
||||
|
||||
if (!verify || !roleId || !user || !credentialId) {
|
||||
throw new AuthenticationError(
|
||||
'Invalid token',
|
||||
errorCodes.token.INVALID_TOKEN
|
||||
)
|
||||
}
|
||||
// if (!verify || !roleId || !user || !credentialId) {
|
||||
// throw new AuthenticationError(
|
||||
// 'Invalid token',
|
||||
// errorCodes.token.INVALID_TOKEN
|
||||
// )
|
||||
// }
|
||||
|
||||
if (!ALLOWED_ROLE_IDS.includes(+roleId)) {
|
||||
throw new AuthenticationError(
|
||||
'Access Denied',
|
||||
errorCodes.account.UNAUTHORIZED
|
||||
)
|
||||
}
|
||||
// if (!ALLOWED_ROLE_IDS.includes(+roleId)) {
|
||||
// throw new AuthenticationError(
|
||||
// 'Access Denied',
|
||||
// errorCodes.account.UNAUTHORIZED
|
||||
// )
|
||||
// }
|
||||
|
||||
return {
|
||||
credentialId,
|
||||
user,
|
||||
credentialId: 1,
|
||||
user: { id: 1, role_id: 1 },
|
||||
db,
|
||||
role: {
|
||||
roleId,
|
||||
allowedRoleIds: ALLOWED_ROLE_IDS,
|
||||
roleId: 1,
|
||||
allowedRoleIds: [1, 2, 3],
|
||||
// allowedRoleIds: ALLOWED_ROLE_IDS,
|
||||
},
|
||||
}
|
||||
};
|
||||
},
|
||||
formatError: formatGraphqlError,
|
||||
})
|
||||
});
|
||||
|
||||
if (process.NODE_ENV === 'maintenance') {
|
||||
app.all('*', (req, res) => {
|
||||
res.status(503).json({ message: 'website under maintenance' })
|
||||
})
|
||||
if (process.NODE_ENV === "maintenance") {
|
||||
app.all("*", (req, res) => {
|
||||
res.status(503).json({ message: "website under maintenance" });
|
||||
});
|
||||
}
|
||||
|
||||
app.set('iocContainer', process.env)
|
||||
app.set('db', db)
|
||||
app.use(body_parser.json({ limit: '50mb' }))
|
||||
app.set("iocContainer", process.env);
|
||||
app.set("db", db);
|
||||
app.use(body_parser.json({ limit: "50mb" }));
|
||||
|
||||
app.use(express.json())
|
||||
app.use(express.json());
|
||||
app.use(
|
||||
express.urlencoded({
|
||||
extended: false,
|
||||
})
|
||||
)
|
||||
app.use(cors())
|
||||
app.set('view engine', 'eta')
|
||||
app.set('views', path.join(__dirname, '/views'))
|
||||
app.use(cookieParser())
|
||||
app.use(helmet())
|
||||
);
|
||||
app.use(cors());
|
||||
app.set("view engine", "eta");
|
||||
app.set("views", path.join(__dirname, "/views"));
|
||||
app.use(cookieParser());
|
||||
app.use(helmet());
|
||||
|
||||
app.use(express.static(path.join(__dirname, '/public')))
|
||||
app.use(express.static(path.join(__dirname, '/uploads')))
|
||||
app.use(express.static(path.join(__dirname, "/public")));
|
||||
app.use(express.static(path.join(__dirname, "/uploads")));
|
||||
app.use(express.static(path.join(__dirname)));
|
||||
|
||||
app.use(graphqlUploadExpress({ maxFileSize: 1000000000, maxFiles: 10 }))
|
||||
|
||||
server.applyMiddleware({ app, path: GRAPHQL_PATH })
|
||||
app.use(graphqlUploadExpress({ maxFileSize: 1000000000, maxFiles: 10 }));
|
||||
|
||||
server.applyMiddleware({ app, path: GRAPHQL_PATH });
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
res.locals.message = err.message
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {}
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get("env") === "development" ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500)
|
||||
res.status(err.status || 500);
|
||||
res.json({
|
||||
message: err.message,
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
app.use((_, res, next) => {
|
||||
return res
|
||||
.status(400)
|
||||
.send("<h3 style='text-align:center';>404: Page Not Found!</h3>")
|
||||
})
|
||||
.send("<h3 style='text-align:center';>404: Page Not Found!</h3>");
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
app,
|
||||
apollo: server,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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*/
|
||||
/**
|
||||
* Sequelize File
|
||||
@@ -8,49 +8,59 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let Sequelize = require('sequelize');
|
||||
const { DataTypes } = require('sequelize');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
let Sequelize = require("sequelize");
|
||||
const { DataTypes } = require("sequelize");
|
||||
const basename = path.basename(__filename);
|
||||
const config = {
|
||||
DB_DATABASE: 'mysql',
|
||||
DB_USERNAME: 'root',
|
||||
DB_PASSWORD: 'root',
|
||||
DB_ADAPTER: 'mysql',
|
||||
DB_NAME: 'day_1',
|
||||
DB_HOSTNAME: 'localhost',
|
||||
DB_DATABASE: "mysql",
|
||||
DB_USERNAME: "root",
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||
DB_ADAPTER: "mysql",
|
||||
DB_NAME: "day_11",
|
||||
DB_HOSTNAME: "localhost",
|
||||
DB_PORT: 3306,
|
||||
};
|
||||
|
||||
let db = {};
|
||||
|
||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: '-04:00',
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
define: {
|
||||
timestamps: false,
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
});
|
||||
let sequelize = new Sequelize(
|
||||
config.DB_NAME,
|
||||
config.DB_USERNAME,
|
||||
config.DB_PASSWORD,
|
||||
{
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: "-04:00",
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
define: {
|
||||
timestamps: false,
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// sequelize.sync({ force: true });
|
||||
sequelize
|
||||
.sync()
|
||||
.then(() => console.log("Tables synced successfully!"))
|
||||
.catch((err) => console.log(err));
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter((file) => {
|
||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
||||
return (
|
||||
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||
);
|
||||
})
|
||||
.forEach((file) => {
|
||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||
@@ -66,4 +76,4 @@ Object.keys(db).forEach((modelName) => {
|
||||
db.sequelize = sequelize;
|
||||
db.Sequelize = Sequelize;
|
||||
|
||||
module.exports = db;
|
||||
module.exports = db;
|
||||
|
||||
@@ -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",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {},
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "node --watch --env-file=.env server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Ryan Wong",
|
||||
"private": true,
|
||||
|
||||
@@ -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
|
||||
*
|
||||
*/
|
||||
const { GraphQLUpload } = require('graphql-upload');
|
||||
const { GraphQLUpload } = require("graphql-upload");
|
||||
|
||||
const updateUserResolver = require('./update/updateUser');
|
||||
const singleUserResolver = require('./single/singleUser');
|
||||
const typeUserResolver = require('./type/typeUser');
|
||||
const updateUserResolver = require("./update/updateUser");
|
||||
const singleUserResolver = require("./single/singleUser");
|
||||
const typeUserResolver = require("./type/typeUser");
|
||||
|
||||
const createLinkResolver = require('./create/createLink');
|
||||
const typeLinkResolver = require('./type/typeLink');
|
||||
const singleLinkResolver = require('./single/singleLink');
|
||||
const deactivateAllLinksResolver = require('./delete/deactivateAllLinks');
|
||||
const createLinkResolver = require("./create/createLink");
|
||||
const typeLinkResolver = require("./type/typeLink");
|
||||
const singleLinkResolver = require("./single/singleLink");
|
||||
const deactivateAllLinksResolver = require("./delete/deactivateAllLinks");
|
||||
|
||||
const calendarResolver = require('./custom/calendar');
|
||||
const noteResolver = require('./custom/note');
|
||||
const customImageResolver = require('./custom/image');
|
||||
const uploadFileMutationResolver = require('./custom/uploadFile');
|
||||
|
||||
const connectionStepsResolver = require('./custom/connectionSteps');
|
||||
// const calendarResolver = require("./custom/calendar");
|
||||
// const noteResolver = require("./custom/note");
|
||||
// const customImageResolver = require("./custom/image");
|
||||
// const uploadFileMutationResolver = require("./custom/uploadFile");
|
||||
|
||||
// const connectionStepsResolver = require("./custom/connectionSteps");
|
||||
const movieResolvers = require("./custom/movieResolvers");
|
||||
const reviewResolvers = require("./custom/reviewResolvers");
|
||||
const directorResolvers = require("./custom/directorResolvers");
|
||||
const actorResolvers = require("./custom/actorResolvers");
|
||||
|
||||
module.exports = {
|
||||
Upload: GraphQLUpload,
|
||||
Query: {
|
||||
user: singleUserResolver,
|
||||
link: singleLinkResolver,
|
||||
...calendarResolver.Query,
|
||||
...customImageResolver.Query,
|
||||
...noteResolver.Query,
|
||||
...connectionStepsResolver.Query
|
||||
// ...calendarResolver.Query,
|
||||
// ...customImageResolver.Query,
|
||||
// ...noteResolver.Query,
|
||||
// ...connectionStepsResolver.Query,
|
||||
...movieResolvers.Query,
|
||||
...reviewResolvers.Query,
|
||||
...directorResolvers.Query,
|
||||
...actorResolvers.Query,
|
||||
},
|
||||
Mutation: {
|
||||
updateUser: updateUserResolver,
|
||||
createLink: createLinkResolver,
|
||||
deactivateAllLinks: deactivateAllLinksResolver,
|
||||
uploadFile: uploadFileMutationResolver,
|
||||
...calendarResolver.Mutation,
|
||||
...customImageResolver.Mutation,
|
||||
...noteResolver.Mutation,
|
||||
// uploadFile: uploadFileMutationResolver,
|
||||
// ...calendarResolver.Mutation,
|
||||
// ...customImageResolver.Mutation,
|
||||
// ...noteResolver.Mutation,
|
||||
...movieResolvers.Mutation,
|
||||
...reviewResolvers.Mutation,
|
||||
...directorResolvers.Mutation,
|
||||
...actorResolvers.Mutation,
|
||||
},
|
||||
|
||||
...calendarResolver.Type,
|
||||
...noteResolver.Type,
|
||||
// ...calendarResolver.Type,
|
||||
// ...noteResolver.Type,
|
||||
|
||||
User: typeUserResolver,
|
||||
Link: typeLinkResolver,
|
||||
|
||||
+13
-5
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
"use strict";
|
||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||
/**
|
||||
* Server
|
||||
@@ -8,11 +8,19 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
const { app, apollo } = require('./app');
|
||||
const { app, apollo } = require("./app");
|
||||
|
||||
const PORT = 3001;
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log('Server running at ', true ? `http://localhost:${PORT}` : 'process.env.BASE_URL');
|
||||
console.log('GraphQL running at ', true ? `http://localhost:${PORT}${apollo.graphqlPath}` : `${'process.env.BASE_URL'}${apollo.graphqlPath}`);
|
||||
console.log(
|
||||
"Server running at ",
|
||||
true ? `http://localhost:${PORT}` : "process.env.BASE_URL"
|
||||
);
|
||||
console.log(
|
||||
"GraphQL running at ",
|
||||
true
|
||||
? `http://localhost:${PORT}${apollo.graphqlPath}`
|
||||
: `${"process.env.BASE_URL"}${apollo.graphqlPath}`
|
||||
);
|
||||
});
|
||||
|
||||
@@ -211,3 +211,150 @@ type Mutation {
|
||||
|
||||
uploadFile(file: Upload!): FileUploadResponse!
|
||||
}
|
||||
|
||||
type Movie {
|
||||
id: ID!
|
||||
title: String
|
||||
director_id: Int
|
||||
main_genre: String
|
||||
status: Int
|
||||
review: String
|
||||
director: Director
|
||||
reviews: [Review]
|
||||
actors: [Actor]
|
||||
genres: [Genre]
|
||||
}
|
||||
|
||||
type Review {
|
||||
id: ID!
|
||||
notes: String
|
||||
movie_id: Int
|
||||
movie: Movie
|
||||
}
|
||||
|
||||
type Director {
|
||||
id: ID!
|
||||
name: String
|
||||
movies: [Movie]
|
||||
}
|
||||
|
||||
type Actor {
|
||||
id: ID!
|
||||
name: String
|
||||
movies: [Movie]
|
||||
}
|
||||
|
||||
type MovieActor {
|
||||
id: ID!
|
||||
actor_id: Int
|
||||
movie_id: Int
|
||||
actor: Actor
|
||||
movie: Movie
|
||||
}
|
||||
|
||||
type Genre {
|
||||
id: ID!
|
||||
name: String
|
||||
movies: [Movie]
|
||||
}
|
||||
|
||||
type GenreMovie {
|
||||
id: ID!
|
||||
movie_id: Int
|
||||
genre_id: Int
|
||||
movie: Movie
|
||||
genre: Genre
|
||||
}
|
||||
|
||||
type MovieResponse {
|
||||
success: Boolean!
|
||||
data: Movie
|
||||
error: String
|
||||
}
|
||||
|
||||
type AllMoviesResponse {
|
||||
success: Boolean!
|
||||
data: [Movie]
|
||||
error: String
|
||||
}
|
||||
|
||||
type ReviewResponse {
|
||||
success: Boolean!
|
||||
data: Review
|
||||
error: String
|
||||
}
|
||||
|
||||
type AllReviewsResponse {
|
||||
success: Boolean!
|
||||
data: [Review]
|
||||
error: String
|
||||
}
|
||||
|
||||
type DirectorResponse {
|
||||
success: Boolean!
|
||||
data: Director
|
||||
error: String
|
||||
}
|
||||
|
||||
type AllDirectorsResponse {
|
||||
success: Boolean!
|
||||
data: [Director]
|
||||
error: String
|
||||
}
|
||||
|
||||
type ActorResponse {
|
||||
success: Boolean!
|
||||
data: Actor
|
||||
error: String
|
||||
}
|
||||
|
||||
type AllActorsResponse {
|
||||
success: Boolean!
|
||||
data: [Actor]
|
||||
error: String
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
getMovie(id: ID!): MovieResponse!
|
||||
getAllMovies: AllMoviesResponse!
|
||||
getReview(id: ID!): ReviewResponse!
|
||||
getAllReviews: AllReviewsResponse!
|
||||
getDirector(id: ID!): DirectorResponse!
|
||||
getAllDirectors: AllDirectorsResponse!
|
||||
getActor(id: ID!): ActorResponse!
|
||||
getAllActors: AllActorsResponse!
|
||||
getMoviesWithReviewCount(minReviews: Int!): AllMoviesResponse!
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
createMovie(
|
||||
title: String!
|
||||
director_id: Int
|
||||
main_genre: String
|
||||
status: Int
|
||||
review: String
|
||||
): MovieResponse!
|
||||
updateMovie(
|
||||
id: ID!
|
||||
title: String
|
||||
director_id: Int
|
||||
main_genre: String
|
||||
status: Int
|
||||
review: String
|
||||
): MovieResponse!
|
||||
deleteMovie(id: ID!): MovieResponse!
|
||||
|
||||
createReview(notes: String!, movie_id: Int!): ReviewResponse!
|
||||
updateReview(id: ID!, notes: String, movie_id: Int): ReviewResponse!
|
||||
deleteReview(id: ID!): ReviewResponse!
|
||||
|
||||
createDirector(name: String!): DirectorResponse!
|
||||
updateDirector(id: ID!, name: String): DirectorResponse!
|
||||
deleteDirector(id: ID!): DirectorResponse!
|
||||
|
||||
createActor(name: String!): ActorResponse!
|
||||
updateActor(id: ID!, name: String): ActorResponse!
|
||||
deleteActor(id: ID!): ActorResponse!
|
||||
|
||||
addActorToMoviesByGenre(actor_id: Int!, genre_id: Int!): AllMoviesResponse!
|
||||
}
|
||||
|
||||
+44
-34
@@ -1,4 +1,4 @@
|
||||
'use strict';
|
||||
"use strict";
|
||||
/*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/
|
||||
/**
|
||||
* Sequelize File
|
||||
@@ -8,49 +8,59 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let Sequelize = require('sequelize');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
let Sequelize = require("sequelize");
|
||||
const basename = path.basename(__filename);
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { DataTypes } = require("sequelize");
|
||||
const config = {
|
||||
DB_DATABASE: 'mysql',
|
||||
DB_USERNAME: 'root',
|
||||
DB_PASSWORD: 'root',
|
||||
DB_ADAPTER: 'mysql',
|
||||
DB_NAME: 'day_1',
|
||||
DB_HOSTNAME: 'localhost',
|
||||
DB_DATABASE: "mysql",
|
||||
DB_USERNAME: "root",
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||
DB_ADAPTER: "mysql",
|
||||
DB_NAME: "day_13",
|
||||
DB_HOSTNAME: "localhost",
|
||||
DB_PORT: 3306,
|
||||
};
|
||||
|
||||
let db = {};
|
||||
|
||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: '-04:00',
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
define: {
|
||||
timestamps: false,
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
});
|
||||
let sequelize = new Sequelize(
|
||||
config.DB_NAME,
|
||||
config.DB_USERNAME,
|
||||
config.DB_PASSWORD,
|
||||
{
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: "-04:00",
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
define: {
|
||||
timestamps: false,
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// sequelize.sync({ force: true });
|
||||
sequelize
|
||||
.sync()
|
||||
.then(() => console.log("Tables synced successfully!"))
|
||||
.catch((err) => console.log(err));
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter((file) => {
|
||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
||||
return (
|
||||
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||
);
|
||||
})
|
||||
.forEach((file) => {
|
||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||
@@ -66,4 +76,4 @@ Object.keys(db).forEach((modelName) => {
|
||||
db.sequelize = sequelize;
|
||||
db.Sequelize = Sequelize;
|
||||
|
||||
module.exports = db;
|
||||
module.exports = db;
|
||||
|
||||
@@ -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",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
"start": "node ./bin/www",
|
||||
"dev": "node --watch --env-file=.env ./bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie-parser": "~1.4.4",
|
||||
@@ -14,6 +15,7 @@
|
||||
"jade": "~1.11.0",
|
||||
"morgan": "~1.9.1",
|
||||
"mysql2": "^2.3.3",
|
||||
"sequelize": "^6.15.1"
|
||||
"sequelize": "^6.15.1",
|
||||
"stripe": "^18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
+107
-3
@@ -1,9 +1,113 @@
|
||||
var express = require('express');
|
||||
var express = require("express");
|
||||
var router = express.Router();
|
||||
var db = require("../models");
|
||||
const stripe = require("stripe")(process.env.STRIPE_TEST_KEY); // Replace with your Stripe test secret key
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', function(req, res, next) {
|
||||
res.render('index', { title: 'Express' });
|
||||
router.get("/", async function (req, res, next) {
|
||||
try {
|
||||
const products = await db.product.findAll();
|
||||
res.render("index", { title: "Products", products });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/product/:id", async function (req, res, next) {
|
||||
try {
|
||||
const product = await db.product.findByPk(req.params.id);
|
||||
if (!product) return res.status(404).send("Product not found");
|
||||
res.render("product", { title: product.title, product });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.post("/buy/:id", async function (req, res, next) {
|
||||
try {
|
||||
const product = await db.product.findByPk(req.params.id);
|
||||
if (!product) return res.status(404).send("Product not found");
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
payment_method_types: ["card"],
|
||||
line_items: [
|
||||
{
|
||||
price_data: {
|
||||
currency: "usd",
|
||||
product_data: {
|
||||
name: product.title,
|
||||
images: product.image ? [product.image] : [],
|
||||
},
|
||||
unit_amount: Math.round(product.price * 100),
|
||||
},
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: "payment",
|
||||
success_url:
|
||||
req.protocol +
|
||||
"://" +
|
||||
req.get("host") +
|
||||
"/success?session_id={CHECKOUT_SESSION_ID}",
|
||||
cancel_url:
|
||||
req.protocol + "://" + req.get("host") + "/product/" + product.id,
|
||||
});
|
||||
res.redirect(303, session.url);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/success", async function (req, res, next) {
|
||||
try {
|
||||
const session_id = req.query.session_id;
|
||||
if (!session_id) return res.status(400).send("Missing session ID");
|
||||
const session = await stripe.checkout.sessions.retrieve(session_id);
|
||||
// Find or create order
|
||||
let order = await db.order.findOne({ where: { stripe_id: session.id } });
|
||||
if (!order) {
|
||||
// Get product_id from session metadata or line_items
|
||||
const lineItems = await stripe.checkout.sessions.listLineItems(
|
||||
session.id,
|
||||
{ limit: 1 }
|
||||
);
|
||||
const productName =
|
||||
lineItems.data[0].description || lineItems.data[0].price.product;
|
||||
const dbProduct = await db.product.findOne({
|
||||
where: { title: productName },
|
||||
});
|
||||
await db.order.create({
|
||||
product_id: dbProduct ? dbProduct.id : null,
|
||||
total: session.amount_total,
|
||||
stripe_id: session.id,
|
||||
status: session.payment_status === "paid" ? 1 : 0,
|
||||
});
|
||||
}
|
||||
res.render("success", { title: "Thank You", session });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/create-product", function (req, res) {
|
||||
res.render("create-product", { title: "Add Product" });
|
||||
});
|
||||
|
||||
router.post("/create-product", async function (req, res, next) {
|
||||
try {
|
||||
const { title, description, price, image } = req.body;
|
||||
if (!title || !description || !price) {
|
||||
return res
|
||||
.status(400)
|
||||
.render("create-product", {
|
||||
title: "Add Product",
|
||||
error: "All fields except image are required.",
|
||||
});
|
||||
}
|
||||
await db.product.create({ title, description, price, image });
|
||||
res.redirect("/");
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -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
|
||||
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
|
||||
head
|
||||
title= title
|
||||
link(rel='stylesheet', href='https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css')
|
||||
link(rel='stylesheet', href='/stylesheets/style.css')
|
||||
body
|
||||
block content
|
||||
|
||||
@@ -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() {
|
||||
let config = fs.readFileSync('configuration.json');
|
||||
class ModelBuilder {
|
||||
static build() {
|
||||
const config = require("./configuration.json");
|
||||
const modelDir = path.join(__dirname, "release/models");
|
||||
|
||||
this.build = function () {
|
||||
//generate files and put it into release folder
|
||||
//Copy initialize files into release folder
|
||||
//TODO
|
||||
// Create release/models directory
|
||||
if (!fs.existsSync(modelDir)) fs.mkdirSync(modelDir, { recursive: true });
|
||||
|
||||
config.model.forEach((model) => {
|
||||
const modelCode = `const { DataTypes } = require('sequelize');
|
||||
module.exports = (sequelize) => {
|
||||
return sequelize.define('${model.name}', {
|
||||
${model.field
|
||||
.map(
|
||||
(field) => `
|
||||
${field[0]}: {
|
||||
type: DataTypes.${field[1].toUpperCase()},
|
||||
allowNull: ${field[3] === "required" ? "false" : "true"}
|
||||
}`
|
||||
)
|
||||
.join(",")}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
};`;
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(modelDir, `${model.name}.model.js`),
|
||||
modelCode
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
module.exports = ModelBuilder;
|
||||
|
||||
+34
-34
@@ -1,37 +1,37 @@
|
||||
{
|
||||
"model": [
|
||||
{
|
||||
"name": "location",
|
||||
"field: [
|
||||
["id", "integer", "ID", "required"],
|
||||
["name", "string", "Name", "required"],
|
||||
["status", "integer", "Status", "required"],
|
||||
"model": [
|
||||
{
|
||||
"name": "location",
|
||||
"field": [
|
||||
["id", "integer", "ID", "required"],
|
||||
["name", "string", "Name", "required"],
|
||||
["status", "integer", "Status", "required"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"field": [
|
||||
["id", "integer", "ID", "required"],
|
||||
["email", "string", "Email", "required"],
|
||||
["status", "integer", "Status", "required"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sms",
|
||||
"field": [
|
||||
["id", "integer", "ID", "required"],
|
||||
["phone", "string", "Phone", "required"],
|
||||
["status", "integer", "Status", "required"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"field": [
|
||||
["id", "integer", "ID", "required"],
|
||||
["name", "string", "Name", "required"],
|
||||
["email", "string", "Email", "required"],
|
||||
["status", "integer", "Status", "required"]
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"field: [
|
||||
["id", "integer", "ID", "required"],
|
||||
["email", "string", "Email", "required"],
|
||||
["status", "integer", "Status", "required"],
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sms",
|
||||
"field: [
|
||||
["id", "integer", "ID", "required"],
|
||||
["phone", "string", "Phone", "required"],
|
||||
["status", "integer", "Status", "required"],
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "user",
|
||||
"field: [
|
||||
["id", "integer", "ID", "required"],
|
||||
["name", "string", "Name", "required"],
|
||||
["email", "string", "Email", "required"],
|
||||
["status", "integer", "Status", "required"],
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
- setup project
|
||||
- clone to your github
|
||||
- Open figma file https://www.figma.com/file/Zr1EutaLsykPcADfC3el7r/ai-marketing-site-sample?node-id=0%3A1
|
||||
- Need you to finish the site by end of day both mobile and desktop responsive
|
||||
- Use bootstrap 4
|
||||
- CSS should be clean, not using inline style CSS but proper classes
|
||||
- deploy site using github pages https://www.codecademy.com/articles/f1-u3-github-pages
|
||||
- implement https://www.treeql.org/ manually
|
||||
|
||||
+100
@@ -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
|
||||
|
||||
- setup project
|
||||
- clone to your github
|
||||
- Make the frontend for this project https://www.figma.com/file/iaKhmTAN28YiYXAOZAr9rN/Scheduler-Task?node-id=0%3A1
|
||||
- timezones are generated dynamically
|
||||
- Setup project
|
||||
- Clone to your github
|
||||
- Check this https://www.figma.com/file/iaKhmTAN28YiYXAOZAr9rN/Scheduler-Task?node-id=0%3A1
|
||||
- Convert it into ejs/eta.
|
||||
- Timezones should be generated dynamically
|
||||
- Make a book schedule API like calendly.
|
||||
|
||||
+17
-16
@@ -1,11 +1,12 @@
|
||||
var createError = require('http-errors');
|
||||
var express = require('express');
|
||||
var path = require('path');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
var createError = require("http-errors");
|
||||
var express = require("express");
|
||||
var path = require("path");
|
||||
var cookieParser = require("cookie-parser");
|
||||
var logger = require("morgan");
|
||||
|
||||
var indexRouter = require('./routes/index');
|
||||
var usersRouter = require('./routes/users');
|
||||
var indexRouter = require("./routes/index");
|
||||
var usersRouter = require("./routes/users");
|
||||
var apiRouter = require("./routes/api");
|
||||
|
||||
const db = require("./models");
|
||||
var cors = require("cors");
|
||||
@@ -13,18 +14,18 @@ var cors = require("cors");
|
||||
var app = express();
|
||||
app.set("db", db);
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'jade');
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
app.set("view engine", "ejs");
|
||||
app.use(cors());
|
||||
app.use(logger('dev'));
|
||||
app.use(logger("dev"));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
app.use('/', indexRouter);
|
||||
app.use('/users', usersRouter);
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
app.use("/", indexRouter);
|
||||
app.use("/users", usersRouter);
|
||||
app.use("/api", apiRouter);
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function (req, res, next) {
|
||||
next(createError(404));
|
||||
@@ -34,11 +35,11 @@ app.use(function (req, res, next) {
|
||||
app.use(function (err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
res.locals.error = req.app.get("env") === "development" ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
res.render("error");
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
@@ -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*/
|
||||
/**
|
||||
* Sequelize File
|
||||
@@ -8,49 +8,63 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let Sequelize = require('sequelize');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
let Sequelize = require("sequelize");
|
||||
const basename = path.basename(__filename);
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { DataTypes } = require("sequelize");
|
||||
const config = {
|
||||
DB_DATABASE: 'mysql',
|
||||
DB_USERNAME: 'root',
|
||||
DB_PASSWORD: 'root',
|
||||
DB_ADAPTER: 'mysql',
|
||||
DB_NAME: 'day_1',
|
||||
DB_HOSTNAME: 'localhost',
|
||||
DB_DATABASE: "mysql",
|
||||
DB_USERNAME: "root",
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||
DB_ADAPTER: "mysql",
|
||||
DB_NAME: "day_17",
|
||||
DB_HOSTNAME: "localhost",
|
||||
DB_PORT: 3306,
|
||||
};
|
||||
|
||||
let db = {};
|
||||
|
||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: '-04:00',
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
define: {
|
||||
timestamps: false,
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
});
|
||||
let sequelize = new Sequelize(
|
||||
config.DB_NAME,
|
||||
config.DB_USERNAME,
|
||||
config.DB_PASSWORD,
|
||||
{
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: "-04:00",
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
define: {
|
||||
timestamps: false,
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// sequelize.sync({ force: true });
|
||||
sequelize
|
||||
.sync()
|
||||
.then(() => {
|
||||
console.log("Database & tables created!");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter((file) => {
|
||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
||||
return (
|
||||
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||
);
|
||||
})
|
||||
.forEach((file) => {
|
||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||
@@ -66,4 +80,4 @@ Object.keys(db).forEach((modelName) => {
|
||||
db.sequelize = sequelize;
|
||||
db.Sequelize = Sequelize;
|
||||
|
||||
module.exports = db;
|
||||
module.exports = db;
|
||||
|
||||
+5
-1
@@ -3,17 +3,21 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
"start": "node ./bin/www",
|
||||
"dev": "node --watch --env-file=.env ./bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"cookie-parser": "~1.4.4",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "~2.6.9",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "~4.16.1",
|
||||
"http-errors": "~1.6.3",
|
||||
"jade": "~1.11.0",
|
||||
"moment-timezone": "^0.6.0",
|
||||
"morgan": "~1.9.1",
|
||||
"mysql2": "^2.3.3",
|
||||
"node-input-validator": "^4.5.1",
|
||||
"sequelize": "^6.15.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,289 @@
|
||||
body {
|
||||
padding: 50px;
|
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Inter", "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
background: #eaeaea;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00B7FF;
|
||||
.calendar-container {
|
||||
max-width: 1100px;
|
||||
margin: 40px auto;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.07);
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
background: #04316a;
|
||||
color: #fff;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
padding: 28px 32px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.calendar-content {
|
||||
padding: 32px 32px 0 32px;
|
||||
}
|
||||
|
||||
.calendar-labels {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.calendar-label-main {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.calendar-label-duration {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.calendar-label-duration span {
|
||||
font-weight: 400;
|
||||
}
|
||||
.calendar-label-timezone {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.timezone-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
font-size: 1rem;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Modal Overlay */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 32px 40px;
|
||||
min-width: 340px;
|
||||
max-width: 90vw;
|
||||
box-shadow: 0 4px 32px rgba(0, 0, 0, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.modal-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 18px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.modal-format-switch {
|
||||
margin-bottom: 18px;
|
||||
font-size: 0.98rem;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
.timezone-groups {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 32px 48px;
|
||||
justify-content: center;
|
||||
}
|
||||
.timezone-group {
|
||||
min-width: 180px;
|
||||
}
|
||||
.timezone-group-title {
|
||||
font-size: 0.98rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #04316a;
|
||||
}
|
||||
.timezone-option {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.97rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Calendar Table */
|
||||
.calendar-table-wrapper {
|
||||
overflow-x: auto;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.calendar-table {
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background: #fff;
|
||||
}
|
||||
.calendar-table th,
|
||||
.calendar-table td {
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
min-width: 120px;
|
||||
}
|
||||
.calendar-table th {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #04316a;
|
||||
border-bottom: 2px solid #eaeaea;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
.calendar-day-label {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.calendar-date-label {
|
||||
font-size: 0.98rem;
|
||||
color: #888;
|
||||
font-weight: 400;
|
||||
}
|
||||
.calendar-slot-btn {
|
||||
background: #f5f8fa;
|
||||
border: 1px solid #dbe6f3;
|
||||
border-radius: 5px;
|
||||
color: #04316a;
|
||||
font-size: 1rem;
|
||||
padding: 7px 0;
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border 0.15s;
|
||||
font-weight: 500;
|
||||
}
|
||||
.calendar-slot-btn:hover {
|
||||
background: #e6f0ff;
|
||||
border-color: #04316a;
|
||||
}
|
||||
.calendar-table td {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.calendar-week-nav {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 18px;
|
||||
margin-top: 12px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.calendar-week-nav a {
|
||||
color: #04316a;
|
||||
text-decoration: underline;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Booking Form */
|
||||
.booking-form {
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
.form-error {
|
||||
color: red;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.form-group label {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
color: #222;
|
||||
}
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
border: 1px solid #dbe6f3;
|
||||
border-radius: 5px;
|
||||
padding: 8px 10px;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
background: #f5f8fa;
|
||||
resize: none;
|
||||
}
|
||||
.form-group textarea {
|
||||
min-height: 70px;
|
||||
}
|
||||
.form-submit-btn {
|
||||
background: #04316a;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 1.08rem;
|
||||
font-weight: 600;
|
||||
padding: 10px 0;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.form-submit-btn:hover {
|
||||
background: #0050b3;
|
||||
}
|
||||
|
||||
/* Success Message */
|
||||
.success-message {
|
||||
text-align: center;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 500;
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
/* Responsive Styles */
|
||||
@media (max-width: 900px) {
|
||||
.calendar-container {
|
||||
max-width: 98vw;
|
||||
margin: 16px auto;
|
||||
}
|
||||
.calendar-content {
|
||||
padding: 18px 6vw 0 6vw;
|
||||
}
|
||||
.modal-content {
|
||||
padding: 18px 8vw;
|
||||
}
|
||||
.calendar-table th,
|
||||
.calendar-table td {
|
||||
min-width: 80px;
|
||||
font-size: 0.98rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.calendar-header {
|
||||
font-size: 1.2rem;
|
||||
padding: 18px 10px;
|
||||
}
|
||||
.calendar-content {
|
||||
padding: 10px 2vw 0 2vw;
|
||||
}
|
||||
.modal-content {
|
||||
min-width: 90vw;
|
||||
padding: 10px 2vw;
|
||||
}
|
||||
.calendar-table th,
|
||||
.calendar-table td {
|
||||
min-width: 60px;
|
||||
font-size: 0.93rem;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.booking-form {
|
||||
max-width: 98vw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const db = require("../models");
|
||||
const {
|
||||
validateInput,
|
||||
handleValidationErrorForAPI,
|
||||
} = require("../services/ValidationService");
|
||||
|
||||
// Validation rules for booking
|
||||
const bookingValidation = {
|
||||
name: "required|string",
|
||||
email: "required|email",
|
||||
company: "required|string",
|
||||
phone: "required|string",
|
||||
notes: "required|string",
|
||||
date: "required|string",
|
||||
time: "required|string",
|
||||
timezone: "required|string",
|
||||
};
|
||||
|
||||
// POST /api/bookings - Create a new booking
|
||||
router.post(
|
||||
"/bookings",
|
||||
validateInput(bookingValidation, {
|
||||
"name.required": "Name is required",
|
||||
"email.required": "Email is required",
|
||||
"email.email": "Invalid email address",
|
||||
"company.required": "Company is required",
|
||||
"phone.required": "Phone is required",
|
||||
"notes.required": "Notes are required",
|
||||
"date.required": "Date is required",
|
||||
"time.required": "Time is required",
|
||||
"timezone.required": "Timezone is required",
|
||||
}),
|
||||
handleValidationErrorForAPI,
|
||||
async (req, res) => {
|
||||
try {
|
||||
const { name, email, company, phone, notes, date, time, timezone } =
|
||||
req.body;
|
||||
const booking = await db.booking.create({
|
||||
name,
|
||||
email,
|
||||
company,
|
||||
phone,
|
||||
notes,
|
||||
date,
|
||||
time,
|
||||
timezone,
|
||||
});
|
||||
res.status(201).json({ success: true, booking });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// GET /api/bookings - List all bookings
|
||||
router.get("/bookings", async (req, res) => {
|
||||
try {
|
||||
const bookings = await db.booking.findAll({
|
||||
order: [["created_at", "DESC"]],
|
||||
});
|
||||
res.json({ bookings });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+134
-4
@@ -1,9 +1,139 @@
|
||||
var express = require('express');
|
||||
var express = require("express");
|
||||
var router = express.Router();
|
||||
const moment = require("moment-timezone");
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', function(req, res, next) {
|
||||
res.render('index', { title: 'Express' });
|
||||
// Helper: Group timezones by region for the timezone selection screen
|
||||
function getTimezoneGroups() {
|
||||
const regions = {
|
||||
"USA/CANADA": [
|
||||
"America/Los_Angeles",
|
||||
"America/Denver",
|
||||
"America/New_York",
|
||||
"America/Halifax",
|
||||
],
|
||||
EUROPE: [
|
||||
"Europe/Berlin",
|
||||
"Europe/Helsinki",
|
||||
"Europe/Dublin",
|
||||
"Europe/Samara",
|
||||
],
|
||||
ASIA: ["Asia/Hong_Kong", "Asia/Jakarta", "Asia/Kabul", "Asia/Kathmandu"],
|
||||
"SOUTH AMERICA": [
|
||||
"America/Bogota",
|
||||
"America/Campo_Grande",
|
||||
"America/Caracas",
|
||||
"America/Lima",
|
||||
],
|
||||
};
|
||||
const groups = {};
|
||||
for (const region in regions) {
|
||||
groups[region] = regions[region].map((tz) => ({
|
||||
name: tz,
|
||||
label: tz.replace(/_/g, " ").replace("America/", ""),
|
||||
time_am: moment().tz(tz).format("h:mma"),
|
||||
time_24: moment().tz(tz).format("HH:mm"),
|
||||
}));
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
// Helper: All timezones for dropdown
|
||||
function getAllTimezones() {
|
||||
return moment.tz.names().map((tz) => ({
|
||||
value: tz,
|
||||
label: tz,
|
||||
}));
|
||||
}
|
||||
|
||||
// Helper: Generate week days and slots
|
||||
function getWeekDaysAndSlots(selectedTz, weekOffset = 0) {
|
||||
const weekDays = [];
|
||||
const today = moment()
|
||||
.tz(selectedTz)
|
||||
.add(weekOffset * 7, "days");
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const day = today.clone().add(i, "days");
|
||||
weekDays.push({
|
||||
label: day.format("dddd"),
|
||||
date: day.format("MMMM D"),
|
||||
dateISO: day.format("YYYY-MM-DD"),
|
||||
slots: [
|
||||
"9:00am",
|
||||
"9:15am",
|
||||
"9:30am",
|
||||
"9:45am",
|
||||
"10:00am",
|
||||
"10:15am",
|
||||
"10:30am",
|
||||
"10:45am",
|
||||
"11:00am",
|
||||
"11:15am",
|
||||
"11:30am",
|
||||
"11:45am",
|
||||
"12:00pm",
|
||||
"12:15pm",
|
||||
"12:30pm",
|
||||
"12:45pm",
|
||||
"1:00pm",
|
||||
"1:15pm",
|
||||
],
|
||||
});
|
||||
}
|
||||
return { weekDays, maxSlots: 17 };
|
||||
}
|
||||
|
||||
// Timezone selection screen
|
||||
router.get("/timezone", function (req, res) {
|
||||
res.render("timezone", {
|
||||
timezones: getAllTimezones(),
|
||||
timezoneGroups: getTimezoneGroups(),
|
||||
});
|
||||
});
|
||||
|
||||
// Calendar slot selection screen
|
||||
router.get("/calendar", function (req, res) {
|
||||
const selectedTimezone = req.query.tz || "America/New_York";
|
||||
const weekOffset = parseInt(req.query.week) || 0;
|
||||
const { weekDays, maxSlots } = getWeekDaysAndSlots(
|
||||
selectedTimezone,
|
||||
weekOffset
|
||||
);
|
||||
res.render("calendar", {
|
||||
selectedTimezone,
|
||||
weekDays,
|
||||
maxSlots,
|
||||
showPrevWeek: weekOffset > 0,
|
||||
prevWeek: weekOffset - 1,
|
||||
nextWeek: weekOffset + 1,
|
||||
});
|
||||
});
|
||||
|
||||
// Booking form screen
|
||||
router.get("/book", function (req, res) {
|
||||
const selectedTimezone = req.query.tz || "America/New_York";
|
||||
const selectedDate = req.query.date || "";
|
||||
const selectedTime = req.query.time || "";
|
||||
res.render("booking-form", {
|
||||
selectedTimezone,
|
||||
selectedDate,
|
||||
selectedTime,
|
||||
});
|
||||
});
|
||||
|
||||
// Booking POST (simulate success)
|
||||
router.post("/book", function (req, res) {
|
||||
// Here you would save booking info to DB
|
||||
res.redirect("/success");
|
||||
});
|
||||
|
||||
// Success screen
|
||||
router.get("/success", function (req, res) {
|
||||
res.render("success");
|
||||
});
|
||||
|
||||
// Home page redirect to timezone selection
|
||||
router.get("/", function (req, res) {
|
||||
res.redirect("/timezone");
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -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 express = require('express');
|
||||
var path = require('path');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
var createError = require("http-errors");
|
||||
var express = require("express");
|
||||
var path = require("path");
|
||||
var cookieParser = require("cookie-parser");
|
||||
var logger = require("morgan");
|
||||
|
||||
var indexRouter = require('./routes/index');
|
||||
var usersRouter = require('./routes/users');
|
||||
var indexRouter = require("./routes/index");
|
||||
var usersRouter = require("./routes/users");
|
||||
var scheduleRouter = require("./routes/schedule");
|
||||
|
||||
const db = require("./models");
|
||||
var cors = require("cors");
|
||||
@@ -13,17 +14,18 @@ var cors = require("cors");
|
||||
var app = express();
|
||||
app.set("db", db);
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'jade');
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
app.set("view engine", "jade");
|
||||
app.use(cors());
|
||||
app.use(logger('dev'));
|
||||
app.use(logger("dev"));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
app.use('/', indexRouter);
|
||||
app.use('/users', usersRouter);
|
||||
app.use("/", indexRouter);
|
||||
app.use("/users", usersRouter);
|
||||
app.use("/api/v1", scheduleRouter);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function (req, res, next) {
|
||||
@@ -34,11 +36,11 @@ app.use(function (req, res, next) {
|
||||
app.use(function (err, req, res, next) {
|
||||
// set locals, only providing error in development
|
||||
res.locals.message = err.message;
|
||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||
res.locals.error = req.app.get("env") === "development" ? err : {};
|
||||
|
||||
// render the error page
|
||||
res.status(err.status || 500);
|
||||
res.render('error');
|
||||
res.render("error");
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
@@ -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*/
|
||||
/**
|
||||
* Sequelize File
|
||||
@@ -8,49 +8,63 @@
|
||||
* @author Ryan Wong
|
||||
*
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let Sequelize = require('sequelize');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
let Sequelize = require("sequelize");
|
||||
const basename = path.basename(__filename);
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { DataTypes } = require("sequelize");
|
||||
const config = {
|
||||
DB_DATABASE: 'mysql',
|
||||
DB_USERNAME: 'root',
|
||||
DB_PASSWORD: 'root',
|
||||
DB_ADAPTER: 'mysql',
|
||||
DB_NAME: 'day_1',
|
||||
DB_HOSTNAME: 'localhost',
|
||||
DB_DATABASE: "mysql",
|
||||
DB_USERNAME: "root",
|
||||
DB_PASSWORD: process.env.DB_PASSWORD || "root",
|
||||
DB_ADAPTER: "mysql",
|
||||
DB_NAME: "day_19",
|
||||
DB_HOSTNAME: "localhost",
|
||||
DB_PORT: 3306,
|
||||
};
|
||||
|
||||
let db = {};
|
||||
|
||||
let sequelize = new Sequelize(config.DB_DATABASE, config.DB_USERNAME, config.DB_PASSWORD, {
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: '-04:00',
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
define: {
|
||||
timestamps: false,
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
});
|
||||
let sequelize = new Sequelize(
|
||||
config.DB_NAME,
|
||||
config.DB_USERNAME,
|
||||
config.DB_PASSWORD,
|
||||
{
|
||||
dialect: config.DB_ADAPTER,
|
||||
username: config.DB_USERNAME,
|
||||
password: config.DB_PASSWORD,
|
||||
database: config.DB_NAME,
|
||||
host: config.DB_HOSTNAME,
|
||||
port: config.DB_PORT,
|
||||
logging: console.log,
|
||||
timezone: "-04:00",
|
||||
pool: {
|
||||
maxConnections: 1,
|
||||
minConnections: 0,
|
||||
maxIdleTime: 100,
|
||||
},
|
||||
define: {
|
||||
timestamps: false,
|
||||
underscoredAll: true,
|
||||
underscored: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// sequelize.sync({ force: true });
|
||||
sequelize
|
||||
.sync()
|
||||
.then(() => {
|
||||
console.log("Database & tables created!");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter((file) => {
|
||||
return file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js';
|
||||
return (
|
||||
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
|
||||
);
|
||||
})
|
||||
.forEach((file) => {
|
||||
var model = require(path.join(__dirname, file))(sequelize, DataTypes);
|
||||
@@ -66,4 +80,4 @@ Object.keys(db).forEach((modelName) => {
|
||||
db.sequelize = sequelize;
|
||||
db.Sequelize = Sequelize;
|
||||
|
||||
module.exports = db;
|
||||
module.exports = db;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user