Compare commits
2 Commits
4df6629bad
...
03ea99d24d
Author | SHA1 | Date | |
---|---|---|---|
03ea99d24d | |||
aed32d8de2 |
1
z2/shop/.gitignore
vendored
Normal file
1
z2/shop/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
13
z2/shop/Dockerfile
Normal file
13
z2/shop/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# install dependencies
|
||||
COPY package*.json ./
|
||||
RUN npm install --production
|
||||
|
||||
# copy app sources
|
||||
COPY . .
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "run", "start-server"]
|
45
z2/shop/app.js
Normal file
45
z2/shop/app.js
Normal file
@ -0,0 +1,45 @@
|
||||
const path = require("path");
|
||||
|
||||
const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
const mongoose = require("mongoose");
|
||||
|
||||
const errorController = require("./controllers/error");
|
||||
const User = require("./models/user");
|
||||
|
||||
const app = express();
|
||||
|
||||
app.set("view engine", "ejs");
|
||||
app.set("views", "views");
|
||||
|
||||
const adminRoutes = require("./routes/admin");
|
||||
const shopRoutes = require("./routes/shop");
|
||||
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
mongoose
|
||||
.connect(process.env.MONGO_URL || "mongodb://mongo:27017/shop")
|
||||
.then(async () => {
|
||||
let user = await User.findOne();
|
||||
if (!user) {
|
||||
user = new User({
|
||||
name: "Jawad",
|
||||
email: "jawad@test.com",
|
||||
cart: { items: [] },
|
||||
});
|
||||
await user.save();
|
||||
}
|
||||
|
||||
app.use((req, res, next) => {
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
|
||||
app.use("/admin", adminRoutes);
|
||||
app.use(shopRoutes);
|
||||
app.use(errorController.get404);
|
||||
|
||||
app.listen(3000, "0.0.0.0");
|
||||
})
|
||||
.catch((err) => console.log(err));
|
93
z2/shop/controllers/admin.js
Normal file
93
z2/shop/controllers/admin.js
Normal file
@ -0,0 +1,93 @@
|
||||
const Product = require('../models/product');
|
||||
|
||||
exports.getAddProduct = (req, res, next) => {
|
||||
res.render('admin/edit-product', {
|
||||
pageTitle: 'Add Product',
|
||||
path: '/admin/add-product',
|
||||
editing: false
|
||||
});
|
||||
};
|
||||
|
||||
exports.postAddProduct = async (req, res, next) => {
|
||||
try {
|
||||
console.log('§ postAddProduct ➡️', req.body);
|
||||
const { title, imageUrl, price, description } = req.body;
|
||||
const product = new Product({ title, price, description, imageUrl });
|
||||
await product.save();
|
||||
console.log('✓ Product saved:', product._id);
|
||||
return res.redirect('/admin/products');
|
||||
} catch (err) {
|
||||
console.error('✗ Error in postAddProduct:', err);
|
||||
next(err); // this will trigger your error‐handler (or crash if none)
|
||||
}
|
||||
};
|
||||
|
||||
exports.getEditProduct = (req, res, next) => {
|
||||
const editMode = req.query.edit;
|
||||
if (!editMode) {
|
||||
return res.redirect('/');
|
||||
}
|
||||
const prodId = req.params.productId;
|
||||
Product.findById(prodId)
|
||||
.then(product => {
|
||||
if (!product) {
|
||||
return res.redirect('/');
|
||||
}
|
||||
res.render('admin/edit-product', {
|
||||
pageTitle: 'Edit Product',
|
||||
path: '/admin/edit-product',
|
||||
editing: editMode,
|
||||
product: product
|
||||
});
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
exports.postEditProduct = async (req, res, next) => {
|
||||
try {
|
||||
console.log('§ postEditProduct ➡️', req.body);
|
||||
const { productId, title, imageUrl, price, description } = req.body;
|
||||
const product = await Product.findById(productId);
|
||||
if (!product) {
|
||||
console.warn('!! Tried to edit non‑existent product:', productId);
|
||||
return res.redirect('/admin/products');
|
||||
}
|
||||
product.title = title;
|
||||
product.price = price;
|
||||
product.description = description;
|
||||
product.imageUrl = imageUrl;
|
||||
await product.save();
|
||||
console.log('✓ Product updated:', product._id);
|
||||
return res.redirect('/admin/products');
|
||||
} catch (err) {
|
||||
console.error('✗ Error in postEditProduct:', err);
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getProducts = (req, res, next) => {
|
||||
Product.find()
|
||||
// .select('title price -_id')
|
||||
// .populate('userId', 'name')
|
||||
.then(products => {
|
||||
console.log(products);
|
||||
res.render('admin/products', {
|
||||
prods: products,
|
||||
pageTitle: 'Admin Products',
|
||||
path: '/admin/products'
|
||||
});
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
exports.postDeleteProduct = async (req, res, next) => {
|
||||
try {
|
||||
const prodId = req.body.productId;
|
||||
// Mongoose 6+: use findByIdAndDelete
|
||||
await Product.findByIdAndDelete(prodId);
|
||||
return res.redirect('/admin/products');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
next(err);
|
||||
}
|
||||
};
|
3
z2/shop/controllers/error.js
Normal file
3
z2/shop/controllers/error.js
Normal file
@ -0,0 +1,3 @@
|
||||
exports.get404 = (req, res, next) => {
|
||||
res.status(404).render('404', { pageTitle: 'Page Not Found', path: '/404' });
|
||||
};
|
127
z2/shop/controllers/shop.js
Normal file
127
z2/shop/controllers/shop.js
Normal file
@ -0,0 +1,127 @@
|
||||
const Product = require('../models/product');
|
||||
const Order = require('../models/order');
|
||||
|
||||
exports.getProducts = (req, res, next) => {
|
||||
Product.find()
|
||||
.then(products => {
|
||||
console.log(products);
|
||||
res.render('shop/product-list', {
|
||||
prods: products,
|
||||
pageTitle: 'All Products',
|
||||
path: '/products'
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
exports.getProduct = (req, res, next) => {
|
||||
const prodId = req.params.productId;
|
||||
Product.findById(prodId)
|
||||
.then(product => {
|
||||
res.render('shop/product-detail', {
|
||||
product: product,
|
||||
pageTitle: product.title,
|
||||
path: '/products'
|
||||
});
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
exports.getIndex = (req, res, next) => {
|
||||
Product.find()
|
||||
.then(products => {
|
||||
res.render('shop/index', {
|
||||
prods: products,
|
||||
pageTitle: 'Shop',
|
||||
path: '/'
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
exports.getCart = async (req, res, next) => {
|
||||
try {
|
||||
// populate() now returns a Promise in Mongoose 6+
|
||||
const userPop = await req.user.populate('cart.items.productId');
|
||||
// drop any cart‐items whose product was deleted
|
||||
const cartItems = userPop.cart.items.filter(item => item.productId);
|
||||
const products = cartItems.map(item => ({
|
||||
productId: item.productId._id,
|
||||
title: item.productId.title,
|
||||
price: item.productId.price,
|
||||
imageUrl: item.productId.imageUrl,
|
||||
quantity: item.quantity
|
||||
}));
|
||||
res.render('shop/cart', {
|
||||
pageTitle: 'Your Cart',
|
||||
path: '/cart',
|
||||
products
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
exports.postCart = (req, res, next) => {
|
||||
const prodId = req.body.productId;
|
||||
Product.findById(prodId)
|
||||
.then(product => {
|
||||
return req.user.addToCart(product);
|
||||
})
|
||||
.then(result => {
|
||||
console.log(result);
|
||||
res.redirect('/cart');
|
||||
});
|
||||
};
|
||||
|
||||
exports.postCartDeleteProduct = (req, res, next) => {
|
||||
const prodId = req.body.productId;
|
||||
req.user
|
||||
.removeFromCart(prodId)
|
||||
.then(result => {
|
||||
res.redirect('/cart');
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
exports.postOrder = (req, res, next) => {
|
||||
req.user
|
||||
.populate('cart.items.productId')
|
||||
.execPopulate()
|
||||
.then(user => {
|
||||
const products = user.cart.items.map(i => {
|
||||
return { quantity: i.quantity, product: { ...i.productId._doc } };
|
||||
});
|
||||
const order = new Order({
|
||||
user: {
|
||||
name: req.user.name,
|
||||
userId: req.user
|
||||
},
|
||||
products: products
|
||||
});
|
||||
return order.save();
|
||||
})
|
||||
.then(result => {
|
||||
return req.user.clearCart();
|
||||
})
|
||||
.then(() => {
|
||||
res.redirect('/orders');
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
||||
|
||||
exports.getOrders = (req, res, next) => {
|
||||
Order.find({ 'user.userId': req.user._id })
|
||||
.then(orders => {
|
||||
res.render('shop/orders', {
|
||||
path: '/orders',
|
||||
pageTitle: 'Your Orders',
|
||||
orders: orders
|
||||
});
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
};
|
1
z2/shop/data/cart.json
Normal file
1
z2/shop/data/cart.json
Normal file
@ -0,0 +1 @@
|
||||
{"products":[{"id":"0.41607315815753076","qty":1}],"totalPrice":12}
|
1
z2/shop/data/products.json
Normal file
1
z2/shop/data/products.json
Normal file
@ -0,0 +1 @@
|
||||
[{"id":"123245","title":"A Book","imageUrl":"https://www.publicdomainpictures.net/pictures/10000/velka/1-1210009435EGmE.jpg","description":"This is an awesome book!","price":"19"},{"id":"0.41607315815753076","title":"fasfd","imageUrl":"fdasfs","description":"fadsfads","price":"12"}]
|
25
z2/shop/kubernetes/deployment.yaml
Executable file
25
z2/shop/kubernetes/deployment.yaml
Executable file
@ -0,0 +1,25 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: shop-deployment
|
||||
namespace: shop-ns
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: shop-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: shop-app
|
||||
spec:
|
||||
containers:
|
||||
- name: shop-app
|
||||
image: shop-app:latest
|
||||
imagePullPolicy: IfNotPresent # ← add this
|
||||
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: MONGO_URL
|
||||
value: "mongodb://mongo:27017/shop"
|
4
z2/shop/kubernetes/namespace.yaml
Executable file
4
z2/shop/kubernetes/namespace.yaml
Executable file
@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: shop-ns
|
9
z2/shop/kubernetes/prepare-app.sh
Executable file
9
z2/shop/kubernetes/prepare-app.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
eval $(minikube docker-env)
|
||||
# build your Node image in Minikube’s Docker daemon
|
||||
cd ..
|
||||
docker build -t shop-app:latest .
|
||||
cd kubernetes
|
||||
# create namespace & Mongo PV/PVC/StatefulSet
|
||||
kubectl apply -f namespace.yaml
|
||||
kubectl apply -n shop-ns -f statefulset.yaml
|
13
z2/shop/kubernetes/service.yaml
Executable file
13
z2/shop/kubernetes/service.yaml
Executable file
@ -0,0 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: shop-service
|
||||
namespace: shop-ns
|
||||
spec:
|
||||
type: NodePort
|
||||
selector:
|
||||
app: shop-app
|
||||
ports:
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
nodePort: 30000
|
3
z2/shop/kubernetes/start-app.sh
Executable file
3
z2/shop/kubernetes/start-app.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env bash
|
||||
kubectl apply -n shop-ns -f deployment.yaml
|
||||
kubectl apply -n shop-ns -f service.yaml
|
67
z2/shop/kubernetes/statefulset.yaml
Executable file
67
z2/shop/kubernetes/statefulset.yaml
Executable file
@ -0,0 +1,67 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: mongo-pv
|
||||
namespace: shop-ns
|
||||
spec:
|
||||
capacity:
|
||||
storage: 1Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
hostPath:
|
||||
path: /data/db
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: mongo-pvc
|
||||
namespace: shop-ns
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mongo
|
||||
namespace: shop-ns
|
||||
spec:
|
||||
clusterIP: None
|
||||
selector:
|
||||
app: mongo
|
||||
ports:
|
||||
- port: 27017
|
||||
name: mongo
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: mongo
|
||||
namespace: shop-ns
|
||||
spec:
|
||||
serviceName: "mongo"
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mongo
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mongo
|
||||
spec:
|
||||
containers:
|
||||
- name: mongo
|
||||
image: mongo:5.0
|
||||
ports:
|
||||
- containerPort: 27017
|
||||
volumeMounts:
|
||||
- name: mongo-pvc
|
||||
mountPath: /data/db
|
||||
volumes:
|
||||
- name: mongo-pvc
|
||||
persistentVolumeClaim:
|
||||
claimName: mongo-pvc
|
4
z2/shop/kubernetes/stop-app.sh
Executable file
4
z2/shop/kubernetes/stop-app.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
kubectl delete -n shop-ns -f service.yaml
|
||||
kubectl delete -n shop-ns -f deployment.yaml
|
||||
kubectl delete namespace shop-ns
|
25
z2/shop/models/order.js
Normal file
25
z2/shop/models/order.js
Normal file
@ -0,0 +1,25 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const orderSchema = new Schema({
|
||||
products: [
|
||||
{
|
||||
product: { type: Object, required: true },
|
||||
quantity: { type: Number, required: true }
|
||||
}
|
||||
],
|
||||
user: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
required: true,
|
||||
ref: 'User'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Order', orderSchema);
|
90
z2/shop/models/product.js
Normal file
90
z2/shop/models/product.js
Normal file
@ -0,0 +1,90 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const productSchema = new Schema({
|
||||
title: { type: String, required: true },
|
||||
price: { type: Number, required: true },
|
||||
description:{ type: String, required: true },
|
||||
imageUrl: { type: String, required: true }
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Product', productSchema);
|
||||
|
||||
// const mongodb = require('mongodb');
|
||||
// const getDb = require('../util/database').getDb;
|
||||
|
||||
// class Product {
|
||||
// constructor(title, price, description, imageUrl, id, userId) {
|
||||
// this.title = title;
|
||||
// this.price = price;
|
||||
// this.description = description;
|
||||
// this.imageUrl = imageUrl;
|
||||
// this._id = id ? new mongodb.ObjectId(id) : null;
|
||||
// this.userId = userId;
|
||||
// }
|
||||
|
||||
// save() {
|
||||
// const db = getDb();
|
||||
// let dbOp;
|
||||
// if (this._id) {
|
||||
// // Update the product
|
||||
// dbOp = db
|
||||
// .collection('products')
|
||||
// .updateOne({ _id: this._id }, { $set: this });
|
||||
// } else {
|
||||
// dbOp = db.collection('products').insertOne(this);
|
||||
// }
|
||||
// return dbOp
|
||||
// .then(result => {
|
||||
// console.log(result);
|
||||
// })
|
||||
// .catch(err => {
|
||||
// console.log(err);
|
||||
// });
|
||||
// }
|
||||
|
||||
// static fetchAll() {
|
||||
// const db = getDb();
|
||||
// return db
|
||||
// .collection('products')
|
||||
// .find()
|
||||
// .toArray()
|
||||
// .then(products => {
|
||||
// console.log(products);
|
||||
// return products;
|
||||
// })
|
||||
// .catch(err => {
|
||||
// console.log(err);
|
||||
// });
|
||||
// }
|
||||
|
||||
// static findById(prodId) {
|
||||
// const db = getDb();
|
||||
// return db
|
||||
// .collection('products')
|
||||
// .find({ _id: new mongodb.ObjectId(prodId) })
|
||||
// .next()
|
||||
// .then(product => {
|
||||
// console.log(product);
|
||||
// return product;
|
||||
// })
|
||||
// .catch(err => {
|
||||
// console.log(err);
|
||||
// });
|
||||
// }
|
||||
|
||||
// static deleteById(prodId) {
|
||||
// const db = getDb();
|
||||
// return db
|
||||
// .collection('products')
|
||||
// .deleteOne({ _id: new mongodb.ObjectId(prodId) })
|
||||
// .then(result => {
|
||||
// console.log('Deleted');
|
||||
// })
|
||||
// .catch(err => {
|
||||
// console.log(err);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// module.exports = Product;
|
193
z2/shop/models/user.js
Normal file
193
z2/shop/models/user.js
Normal file
@ -0,0 +1,193 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const userSchema = new Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
cart: {
|
||||
items: [
|
||||
{
|
||||
productId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Product',
|
||||
required: true
|
||||
},
|
||||
quantity: { type: Number, required: true }
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
userSchema.methods.addToCart = function(product) {
|
||||
const cartProductIndex = this.cart.items.findIndex(cp => {
|
||||
return cp.productId.toString() === product._id.toString();
|
||||
});
|
||||
let newQuantity = 1;
|
||||
const updatedCartItems = [...this.cart.items];
|
||||
|
||||
if (cartProductIndex >= 0) {
|
||||
newQuantity = this.cart.items[cartProductIndex].quantity + 1;
|
||||
updatedCartItems[cartProductIndex].quantity = newQuantity;
|
||||
} else {
|
||||
updatedCartItems.push({
|
||||
productId: product._id,
|
||||
quantity: newQuantity
|
||||
});
|
||||
}
|
||||
const updatedCart = {
|
||||
items: updatedCartItems
|
||||
};
|
||||
this.cart = updatedCart;
|
||||
return this.save();
|
||||
};
|
||||
|
||||
userSchema.methods.removeFromCart = function(productId) {
|
||||
const updatedCartItems = this.cart.items.filter(item => {
|
||||
return item.productId.toString() !== productId.toString();
|
||||
});
|
||||
this.cart.items = updatedCartItems;
|
||||
return this.save();
|
||||
};
|
||||
|
||||
userSchema.methods.clearCart = function() {
|
||||
this.cart = { items: [] };
|
||||
return this.save();
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('User', userSchema);
|
||||
|
||||
// const mongodb = require('mongodb');
|
||||
// const getDb = require('../util/database').getDb;
|
||||
|
||||
// const ObjectId = mongodb.ObjectId;
|
||||
|
||||
// class User {
|
||||
// constructor(username, email, cart, id) {
|
||||
// this.name = username;
|
||||
// this.email = email;
|
||||
// this.cart = cart; // {items: []}
|
||||
// this._id = id;
|
||||
// }
|
||||
|
||||
// save() {
|
||||
// const db = getDb();
|
||||
// return db.collection('users').insertOne(this);
|
||||
// }
|
||||
|
||||
// addToCart(product) {
|
||||
// const cartProductIndex = this.cart.items.findIndex(cp => {
|
||||
// return cp.productId.toString() === product._id.toString();
|
||||
// });
|
||||
// let newQuantity = 1;
|
||||
// const updatedCartItems = [...this.cart.items];
|
||||
|
||||
// if (cartProductIndex >= 0) {
|
||||
// newQuantity = this.cart.items[cartProductIndex].quantity + 1;
|
||||
// updatedCartItems[cartProductIndex].quantity = newQuantity;
|
||||
// } else {
|
||||
// updatedCartItems.push({
|
||||
// productId: new ObjectId(product._id),
|
||||
// quantity: newQuantity
|
||||
// });
|
||||
// }
|
||||
// const updatedCart = {
|
||||
// items: updatedCartItems
|
||||
// };
|
||||
// const db = getDb();
|
||||
// return db
|
||||
// .collection('users')
|
||||
// .updateOne(
|
||||
// { _id: new ObjectId(this._id) },
|
||||
// { $set: { cart: updatedCart } }
|
||||
// );
|
||||
// }
|
||||
|
||||
// getCart() {
|
||||
// const db = getDb();
|
||||
// const productIds = this.cart.items.map(i => {
|
||||
// return i.productId;
|
||||
// });
|
||||
// return db
|
||||
// .collection('products')
|
||||
// .find({ _id: { $in: productIds } })
|
||||
// .toArray()
|
||||
// .then(products => {
|
||||
// return products.map(p => {
|
||||
// return {
|
||||
// ...p,
|
||||
// quantity: this.cart.items.find(i => {
|
||||
// return i.productId.toString() === p._id.toString();
|
||||
// }).quantity
|
||||
// };
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// deleteItemFromCart(productId) {
|
||||
// const updatedCartItems = this.cart.items.filter(item => {
|
||||
// return item.productId.toString() !== productId.toString();
|
||||
// });
|
||||
// const db = getDb();
|
||||
// return db
|
||||
// .collection('users')
|
||||
// .updateOne(
|
||||
// { _id: new ObjectId(this._id) },
|
||||
// { $set: { cart: { items: updatedCartItems } } }
|
||||
// );
|
||||
// }
|
||||
|
||||
// addOrder() {
|
||||
// const db = getDb();
|
||||
// return this.getCart()
|
||||
// .then(products => {
|
||||
// const order = {
|
||||
// items: products,
|
||||
// user: {
|
||||
// _id: new ObjectId(this._id),
|
||||
// name: this.name
|
||||
// }
|
||||
// };
|
||||
// return db.collection('orders').insertOne(order);
|
||||
// })
|
||||
// .then(result => {
|
||||
// this.cart = { items: [] };
|
||||
// return db
|
||||
// .collection('users')
|
||||
// .updateOne(
|
||||
// { _id: new ObjectId(this._id) },
|
||||
// { $set: { cart: { items: [] } } }
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
// getOrders() {
|
||||
// const db = getDb();
|
||||
// return db
|
||||
// .collection('orders')
|
||||
// .find({ 'user._id': new ObjectId(this._id) })
|
||||
// .toArray();
|
||||
// }
|
||||
|
||||
// static findById(userId) {
|
||||
// const db = getDb();
|
||||
// return db
|
||||
// .collection('users')
|
||||
// .findOne({ _id: new ObjectId(userId) })
|
||||
// .then(user => {
|
||||
// console.log(user);
|
||||
// return user;
|
||||
// })
|
||||
// .catch(err => {
|
||||
// console.log(err);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// module.exports = User;
|
2994
z2/shop/package-lock.json
generated
Normal file
2994
z2/shop/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
z2/shop/package.json
Normal file
27
z2/shop/package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "nodejs-complete-guide",
|
||||
"version": "1.0.0",
|
||||
"description": "Complete Node.js Guide",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "nodemon app.js",
|
||||
"start-server": "node app.js"
|
||||
},
|
||||
"author": "Maximilian Schwarzmüller",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"body-parser": "^1.18.3",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.16.3",
|
||||
"express-handlebars": "^8.0.1",
|
||||
"mongodb": "^3.1.6",
|
||||
"mongoose": "^8.13.2",
|
||||
"mysql2": "^3.14.0",
|
||||
"pug": "^3.0.3",
|
||||
"sequelize": "^6.37.7"
|
||||
}
|
||||
}
|
25
z2/shop/public/css/cart.css
Normal file
25
z2/shop/public/css/cart.css
Normal file
@ -0,0 +1,25 @@
|
||||
.cart__item-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
width: 40rem;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.cart__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.cart__item h1,
|
||||
.cart__item h2 {
|
||||
margin-right: 1rem;
|
||||
font-size: 1.2rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
23
z2/shop/public/css/forms.css
Normal file
23
z2/shop/public/css/forms.css
Normal file
@ -0,0 +1,23 @@
|
||||
.form-control {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.form-control label,
|
||||
.form-control input,
|
||||
.form-control textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.form-control input,
|
||||
.form-control textarea {
|
||||
border: 1px solid #a1a1a1;
|
||||
font: inherit;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.form-control input:focus,
|
||||
.form-control textarea:focus {
|
||||
outline-color: #00695c;
|
||||
}
|
227
z2/shop/public/css/main.css
Normal file
227
z2/shop/public/css/main.css
Normal file
@ -0,0 +1,227 @@
|
||||
@import url('https://fonts.googleapis.com/css?family=Open+Sans:400,700');
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 1rem;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.centered {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image {
|
||||
height: 20rem;
|
||||
}
|
||||
|
||||
.image img {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
width: 100%;
|
||||
height: 3.5rem;
|
||||
background-color: #00695c;
|
||||
padding: 0 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main-header__nav {
|
||||
height: 100%;
|
||||
display: none;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.main-header__item-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.main-header__item {
|
||||
margin: 0 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-header__item a {
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.main-header__item a:hover,
|
||||
.main-header__item a:active,
|
||||
.main-header__item a.active {
|
||||
color: #ffeb3b;
|
||||
}
|
||||
|
||||
.mobile-nav {
|
||||
width: 30rem;
|
||||
height: 100vh;
|
||||
max-width: 90%;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: white;
|
||||
z-index: 10;
|
||||
padding: 2rem 1rem 1rem 2rem;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
|
||||
.mobile-nav.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.mobile-nav__item-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mobile-nav__item {
|
||||
margin: 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mobile-nav__item a {
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
font-size: 1.5rem;
|
||||
padding: 0.5rem 2rem;
|
||||
}
|
||||
|
||||
.mobile-nav__item a:active,
|
||||
.mobile-nav__item a:hover,
|
||||
.mobile-nav__item a.active {
|
||||
background: #00695c;
|
||||
color: white;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#side-menu-toggle {
|
||||
border: 1px solid white;
|
||||
font: inherit;
|
||||
padding: 0.5rem;
|
||||
display: block;
|
||||
background: transparent;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#side-menu-toggle:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#side-menu-toggle:active,
|
||||
#side-menu-toggle:hover {
|
||||
color: #ffeb3b;
|
||||
border-color: #ffeb3b;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 5;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
|
||||
}
|
||||
|
||||
.card__header,
|
||||
.card__content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.card__header h1,
|
||||
.card__content h1,
|
||||
.card__content h2,
|
||||
.card__content p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card__image {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card__image img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card__actions {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card__actions button,
|
||||
.card__actions a {
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 1rem;
|
||||
text-decoration: none;
|
||||
font: inherit;
|
||||
border: 1px solid #00695c;
|
||||
color: #00695c;
|
||||
background: white;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:hover,
|
||||
.btn:active {
|
||||
background-color: #00695c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn.danger {
|
||||
color: red;
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
.btn.danger:hover,
|
||||
.btn.danger:active {
|
||||
background: red;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.main-header__nav {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#side-menu-toggle {
|
||||
display: none;
|
||||
}
|
||||
}
|
29
z2/shop/public/css/orders.css
Normal file
29
z2/shop/public/css/orders.css
Normal file
@ -0,0 +1,29 @@
|
||||
.orders {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.orders__item h1 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.orders__item {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.orders__products {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.orders__products-item {
|
||||
margin: 0.5rem 0;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #00695c;
|
||||
color: #00695c;
|
||||
}
|
27
z2/shop/public/css/product.css
Normal file
27
z2/shop/public/css/product.css
Normal file
@ -0,0 +1,27 @@
|
||||
.product-form {
|
||||
width: 20rem;
|
||||
max-width: 90%;
|
||||
margin: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
width: 20rem;
|
||||
max-width: 95%;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.product__title {
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.product__price {
|
||||
text-align: center;
|
||||
color: #4d4d4d;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.product__description {
|
||||
text-align: center;
|
||||
}
|
16
z2/shop/public/js/main.js
Normal file
16
z2/shop/public/js/main.js
Normal file
@ -0,0 +1,16 @@
|
||||
const backdrop = document.querySelector('.backdrop');
|
||||
const sideDrawer = document.querySelector('.mobile-nav');
|
||||
const menuToggle = document.querySelector('#side-menu-toggle');
|
||||
|
||||
function backdropClickHandler() {
|
||||
backdrop.style.display = 'none';
|
||||
sideDrawer.classList.remove('open');
|
||||
}
|
||||
|
||||
function menuToggleClickHandler() {
|
||||
backdrop.style.display = 'block';
|
||||
sideDrawer.classList.add('open');
|
||||
}
|
||||
|
||||
backdrop.addEventListener('click', backdropClickHandler);
|
||||
menuToggle.addEventListener('click', menuToggleClickHandler);
|
24
z2/shop/routes/admin.js
Normal file
24
z2/shop/routes/admin.js
Normal file
@ -0,0 +1,24 @@
|
||||
const path = require('path');
|
||||
|
||||
const express = require('express');
|
||||
|
||||
const adminController = require('../controllers/admin');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// /admin/add-product => GET
|
||||
router.get('/add-product', adminController.getAddProduct);
|
||||
|
||||
// /admin/products => GET
|
||||
router.get('/products', adminController.getProducts);
|
||||
|
||||
// /admin/add-product => POST
|
||||
router.post('/add-product', adminController.postAddProduct);
|
||||
|
||||
router.get('/edit-product/:productId', adminController.getEditProduct);
|
||||
|
||||
router.post('/edit-product', adminController.postEditProduct);
|
||||
|
||||
router.post('/delete-product', adminController.postDeleteProduct);
|
||||
|
||||
module.exports = router;
|
25
z2/shop/routes/shop.js
Normal file
25
z2/shop/routes/shop.js
Normal file
@ -0,0 +1,25 @@
|
||||
const path = require('path');
|
||||
|
||||
const express = require('express');
|
||||
|
||||
const shopController = require('../controllers/shop');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', shopController.getIndex);
|
||||
|
||||
router.get('/products', shopController.getProducts);
|
||||
|
||||
router.get('/products/:productId', shopController.getProduct);
|
||||
|
||||
router.get('/cart', shopController.getCart);
|
||||
|
||||
router.post('/cart', shopController.postCart);
|
||||
|
||||
router.post('/cart-delete-item', shopController.postCartDeleteProduct);
|
||||
|
||||
router.post('/create-order', shopController.postOrder);
|
||||
|
||||
router.get('/orders', shopController.getOrders);
|
||||
|
||||
module.exports = router;
|
0
z2/shop/use
Normal file
0
z2/shop/use
Normal file
3
z2/shop/util/path.js
Normal file
3
z2/shop/util/path.js
Normal file
@ -0,0 +1,3 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = path.dirname(process.mainModule.filename);
|
8
z2/shop/views/404.ejs
Normal file
8
z2/shop/views/404.ejs
Normal file
@ -0,0 +1,8 @@
|
||||
<%- include('includes/head.ejs') %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%- include('includes/navigation.ejs') %>
|
||||
<h1>Page Not Found!</h1>
|
||||
|
||||
<%- include('includes/end.ejs') %>
|
34
z2/shop/views/admin/edit-product.ejs
Normal file
34
z2/shop/views/admin/edit-product.ejs
Normal file
@ -0,0 +1,34 @@
|
||||
<%- include('../includes/head.ejs') %>
|
||||
<link rel="stylesheet" href="/css/forms.css">
|
||||
<link rel="stylesheet" href="/css/product.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%- include('../includes/navigation.ejs') %>
|
||||
|
||||
<main>
|
||||
<form class="product-form" action="/admin/<% if (editing) { %>edit-product<% } else { %>add-product<% } %>" method="POST">
|
||||
<div class="form-control">
|
||||
<label for="title">Title</label>
|
||||
<input type="text" name="title" id="title" value="<% if (editing) { %><%= product.title %><% } %>">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="imageUrl">Image URL</label>
|
||||
<input type="text" name="imageUrl" id="imageUrl" value="<% if (editing) { %><%= product.imageUrl %><% } %>">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="price">Price</label>
|
||||
<input type="number" name="price" id="price" step="0.01" value="<% if (editing) { %><%= product.price %><% } %>">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label for="description">Description</label>
|
||||
<textarea name="description" id="description" rows="5"><% if (editing) { %><%= product.description %><% } %></textarea>
|
||||
</div>
|
||||
<% if (editing) { %>
|
||||
<input type="hidden" value="<%= product._id %>" name="productId">
|
||||
<% } %>
|
||||
|
||||
<button class="btn" type="submit"><% if (editing) { %>Update Product<% } else { %>Add Product<% } %></button>
|
||||
</form>
|
||||
</main>
|
||||
<%- include('../includes/end.ejs') %>
|
44
z2/shop/views/admin/products.ejs
Normal file
44
z2/shop/views/admin/products.ejs
Normal file
@ -0,0 +1,44 @@
|
||||
<%- include('../includes/head.ejs') %>
|
||||
<link rel="stylesheet" href="/css/product.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%- include('../includes/navigation.ejs') %>
|
||||
|
||||
<main>
|
||||
<% if (prods.length > 0) { %>
|
||||
<div class="grid">
|
||||
<% for (let product of prods) { %>
|
||||
<article class="card product-item">
|
||||
<header class="card__header">
|
||||
<h1 class="product__title">
|
||||
<%= product.title %>
|
||||
</h1>
|
||||
</header>
|
||||
<div class="card__image">
|
||||
<img src="<%= product.imageUrl %>" alt="<%= product.title %>">
|
||||
</div>
|
||||
<div class="card__content">
|
||||
<h2 class="product__price">$
|
||||
<%= product.price %>
|
||||
</h2>
|
||||
<p class="product__description">
|
||||
<%= product.description %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="card__actions">
|
||||
<a href="/admin/edit-product/<%= product._id %>?edit=true" class="btn">Edit</a>
|
||||
<form action="/admin/delete-product" method="POST">
|
||||
<input type="hidden" value="<%= product._id %>" name="productId">
|
||||
<button class="btn" type="submit">Delete</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<h1>No Products Found!</h1>
|
||||
<% } %>
|
||||
</main>
|
||||
<%- include('../includes/end.ejs') %>
|
4
z2/shop/views/includes/add-to-cart.ejs
Normal file
4
z2/shop/views/includes/add-to-cart.ejs
Normal file
@ -0,0 +1,4 @@
|
||||
<form action="/cart" method="post">
|
||||
<button class="btn" type="submit">Add to Cart</button>
|
||||
<input type="hidden" name="productId" value="<%= product._id %>">
|
||||
</form>
|
4
z2/shop/views/includes/end.ejs
Normal file
4
z2/shop/views/includes/end.ejs
Normal file
@ -0,0 +1,4 @@
|
||||
<script src="/js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
9
z2/shop/views/includes/head.ejs
Normal file
9
z2/shop/views/includes/head.ejs
Normal file
@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title><%= pageTitle %></title>
|
||||
<link rel="stylesheet" href="/css/main.css">
|
53
z2/shop/views/includes/navigation.ejs
Normal file
53
z2/shop/views/includes/navigation.ejs
Normal file
@ -0,0 +1,53 @@
|
||||
<div class="backdrop"></div>
|
||||
<header class="main-header">
|
||||
<button id="side-menu-toggle">Menu</button>
|
||||
<nav class="main-header__nav">
|
||||
<ul class="main-header__item-list">
|
||||
<li class="main-header__item">
|
||||
<a class="<%= path === '/' ? 'active' : '' %>" href="/">Shop</a>
|
||||
</li>
|
||||
<li class="main-header__item">
|
||||
<a class="<%= path === '/products' ? 'active' : '' %>" href="/products">Products</a>
|
||||
</li>
|
||||
<li class="main-header__item">
|
||||
<a class="<%= path === '/cart' ? 'active' : '' %>" href="/cart">Cart</a>
|
||||
</li>
|
||||
<li class="main-header__item">
|
||||
<a class="<%= path === '/orders' ? 'active' : '' %>" href="/orders">Orders</a>
|
||||
</li>
|
||||
<li class="main-header__item">
|
||||
<a class="<%= path === '/admin/add-product' ? 'active' : '' %>" href="/admin/add-product">Add Product
|
||||
</a>
|
||||
</li>
|
||||
<li class="main-header__item">
|
||||
<a class="<%= path === '/admin/products' ? 'active' : '' %>" href="/admin/products">Admin Products
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<nav class="mobile-nav">
|
||||
<ul class="mobile-nav__item-list">
|
||||
<li class="mobile-nav__item">
|
||||
<a class="<%= path === '/' ? 'active' : '' %>" href="/">Shop</a>
|
||||
</li>
|
||||
<li class="mobile-nav__item">
|
||||
<a class="<%= path === '/products' ? 'active' : '' %>" href="/products">Products</a>
|
||||
</li>
|
||||
<li class="mobile-nav__item">
|
||||
<a class="<%= path === '/cart' ? 'active' : '' %>" href="/cart">Cart</a>
|
||||
</li>
|
||||
<li class="mobile-nav__item">
|
||||
<a class="<%= path === '/orders' ? 'active' : '' %>" href="/orders">Orders</a>
|
||||
</li>
|
||||
<li class="mobile-nav__item">
|
||||
<a class="<%= path === '/admin/add-product' ? 'active' : '' %>" href="/admin/add-product">Add Product
|
||||
</a>
|
||||
</li>
|
||||
<li class="mobile-nav__item">
|
||||
<a class="<%= path === '/admin/products' ? 'active' : '' %>" href="/admin/products">Admin Products
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
32
z2/shop/views/shop/cart.ejs
Normal file
32
z2/shop/views/shop/cart.ejs
Normal file
@ -0,0 +1,32 @@
|
||||
<%- include('../includes/head.ejs') %>
|
||||
<link rel="stylesheet" href="/css/cart.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%- include('../includes/navigation.ejs') %>
|
||||
<main>
|
||||
<% if (products.length > 0) { %>
|
||||
<ul class="cart__list">
|
||||
<% products.forEach(p => { %>
|
||||
<li class="cart__item">
|
||||
<h1><%= p.title %></h1>
|
||||
<h2>Quantity: <%= p.quantity %></h2>
|
||||
<form action="/cart-delete-item" method="POST">
|
||||
<input type="hidden" name="productId" value="<%= p.productId %>">
|
||||
<button type="submit">Remove</button>
|
||||
</form>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
<hr>
|
||||
<div class="centered">
|
||||
<form action="/create-order" method="POST">
|
||||
<button type="submit" class="btn">Order Now!</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<% } else { %>
|
||||
<h1>No Products in Cart!</h1>
|
||||
<% } %>
|
||||
</main>
|
||||
<%- include('../includes/end.ejs') %>
|
0
z2/shop/views/shop/checkout.ejs
Normal file
0
z2/shop/views/shop/checkout.ejs
Normal file
35
z2/shop/views/shop/index.ejs
Normal file
35
z2/shop/views/shop/index.ejs
Normal file
@ -0,0 +1,35 @@
|
||||
<%- include('../includes/head.ejs') %>
|
||||
<link rel="stylesheet" href="/css/product.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%- include('../includes/navigation.ejs') %>
|
||||
|
||||
<main>
|
||||
<% if (prods.length > 0) { %>
|
||||
<div class="grid">
|
||||
<% for (let product of prods) { %>
|
||||
<article class="card product-item">
|
||||
<header class="card__header">
|
||||
<h1 class="product__title"><%= product.title %></h1>
|
||||
</header>
|
||||
<div class="card__image">
|
||||
<img src="<%= product.imageUrl %>"
|
||||
alt="<%= product.title %>">
|
||||
</div>
|
||||
<div class="card__content">
|
||||
<h2 class="product__price">$<%= product.price %></h2>
|
||||
<p class="product__description"><%= product.description %></p>
|
||||
</div>
|
||||
<div class="card__actions">
|
||||
<a href="/products/<%= product._id %>" class="btn">Details</a>
|
||||
<%- include('../includes/add-to-cart.ejs', {product: product}) %>
|
||||
</div>
|
||||
</article>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<h1>No Products Found!</h1>
|
||||
<% } %>
|
||||
</main>
|
||||
<%- include('../includes/end.ejs') %>
|
25
z2/shop/views/shop/orders.ejs
Normal file
25
z2/shop/views/shop/orders.ejs
Normal file
@ -0,0 +1,25 @@
|
||||
<%- include('../includes/head.ejs') %>
|
||||
<link rel="stylesheet" href="/css/orders.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%- include('../includes/navigation.ejs') %>
|
||||
<main>
|
||||
<% if (orders.length <= 0) { %>
|
||||
<h1>Nothing there!</h1>
|
||||
<% } else { %>
|
||||
<ul class="orders">
|
||||
<% orders.forEach(order => { %>
|
||||
<li class="orders__item">
|
||||
<h1>Order - # <%= order._id %></h1>
|
||||
<ul class="orders__products">
|
||||
<% order.products.forEach(p => { %>
|
||||
<li class="orders__products-item"><%= p.product.title %> (<%= p.quantity %>)</li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
</li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
<% } %>
|
||||
</main>
|
||||
<%- include('../includes/end.ejs') %>
|
16
z2/shop/views/shop/product-detail.ejs
Normal file
16
z2/shop/views/shop/product-detail.ejs
Normal file
@ -0,0 +1,16 @@
|
||||
<%- include('../includes/head.ejs') %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%- include('../includes/navigation.ejs') %>
|
||||
<main class="centered">
|
||||
<h1><%= product.title %></h1>
|
||||
<hr>
|
||||
<div class="image">
|
||||
<img src="<%= product.imageUrl %>" alt="<%= product.title %>">
|
||||
</div>
|
||||
<h2><%= product.price %></h2>
|
||||
<p><%= product.description %></p>
|
||||
<%- include('../includes/add-to-cart.ejs') %>
|
||||
</main>
|
||||
<%- include('../includes/end.ejs') %>
|
40
z2/shop/views/shop/product-list.ejs
Normal file
40
z2/shop/views/shop/product-list.ejs
Normal file
@ -0,0 +1,40 @@
|
||||
<%- include('../includes/head.ejs') %>
|
||||
<link rel="stylesheet" href="/css/product.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<%- include('../includes/navigation.ejs') %>
|
||||
|
||||
<main>
|
||||
<% if (prods.length > 0) { %>
|
||||
<div class="grid">
|
||||
<% for (let product of prods) { %>
|
||||
<article class="card product-item">
|
||||
<header class="card__header">
|
||||
<h1 class="product__title">
|
||||
<%= product.title %>
|
||||
</h1>
|
||||
</header>
|
||||
<div class="card__image">
|
||||
<img src="<%= product.imageUrl %>" alt="<%= product.title %>">
|
||||
</div>
|
||||
<div class="card__content">
|
||||
<h2 class="product__price">$
|
||||
<%= product.price %>
|
||||
</h2>
|
||||
<p class="product__description">
|
||||
<%= product.description %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="card__actions">
|
||||
<a href="/products/<%= product._id %>" class="btn">Details</a>
|
||||
<%- include('../includes/add-to-cart.ejs', {product: product}) %>
|
||||
</div>
|
||||
</article>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<h1>No Products Found!</h1>
|
||||
<% } %>
|
||||
</main>
|
||||
<%- include('../includes/end.ejs') %>
|
Loading…
Reference in New Issue
Block a user