From 7d9350c3df951bde49c9680dbb86fdd4b0cbbff9 Mon Sep 17 00:00:00 2001 From: Ayobami Date: Thu, 17 Jul 2025 21:55:47 +0100 Subject: [PATCH] feat: complete day 13 --- day13/models/index.js | 78 ++++++++++++---------- day13/models/order.js | 24 +++++++ day13/models/product.js | 15 +++++ day13/package.json | 6 +- day13/routes/index.js | 110 +++++++++++++++++++++++++++++++- day13/views/create-product.jade | 22 +++++++ day13/views/index.jade | 17 ++++- day13/views/layout.jade | 1 + day13/views/product.jade | 15 +++++ day13/views/success.jade | 23 +++++++ 10 files changed, 271 insertions(+), 40 deletions(-) create mode 100644 day13/models/order.js create mode 100644 day13/models/product.js create mode 100644 day13/views/create-product.jade create mode 100644 day13/views/product.jade create mode 100644 day13/views/success.jade diff --git a/day13/models/index.js b/day13/models/index.js index 8c44682..1fe90a9 100644 --- a/day13/models/index.js +++ b/day13/models/index.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; /*Powered By: Manaknightdigital Inc. https://manaknightdigital.com/ Year: 2020*/ /** * Sequelize File @@ -8,49 +8,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; \ No newline at end of file +module.exports = db; diff --git a/day13/models/order.js b/day13/models/order.js new file mode 100644 index 0000000..1cc74bd --- /dev/null +++ b/day13/models/order.js @@ -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; +}; diff --git a/day13/models/product.js b/day13/models/product.js new file mode 100644 index 0000000..f1145a1 --- /dev/null +++ b/day13/models/product.js @@ -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; +}; diff --git a/day13/package.json b/day13/package.json index 362c6ed..feccdaf 100644 --- a/day13/package.json +++ b/day13/package.json @@ -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" } } diff --git a/day13/routes/index.js b/day13/routes/index.js index ecca96a..402dcf4 100644 --- a/day13/routes/index.js +++ b/day13/routes/index.js @@ -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; diff --git a/day13/views/create-product.jade b/day13/views/create-product.jade new file mode 100644 index 0000000..e806763 --- /dev/null +++ b/day13/views/create-product.jade @@ -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 \ No newline at end of file diff --git a/day13/views/index.jade b/day13/views/index.jade index 3d63b9a..e3c9c2d 100644 --- a/day13/views/index.jade +++ b/day13/views/index.jade @@ -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. diff --git a/day13/views/layout.jade b/day13/views/layout.jade index 15af079..e908552 100644 --- a/day13/views/layout.jade +++ b/day13/views/layout.jade @@ -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 diff --git a/day13/views/product.jade b/day13/views/product.jade new file mode 100644 index 0000000..0efce56 --- /dev/null +++ b/day13/views/product.jade @@ -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 \ No newline at end of file diff --git a/day13/views/success.jade b/day13/views/success.jade new file mode 100644 index 0000000..e347766 --- /dev/null +++ b/day13/views/success.jade @@ -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 \ No newline at end of file