Compare commits

..

2 Commits

Author SHA1 Message Date
03ea99d24d Second Assignment 2025-04-23 06:14:47 +02:00
aed32d8de2 Second Assignment 2025-04-23 06:10:34 +02:00
43 changed files with 4441 additions and 0 deletions

1
z2/shop/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

13
z2/shop/Dockerfile Normal file
View 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
View 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));

View 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 errorhandler (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 nonexistent 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);
}
};

View 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
View 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 cartitems 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
View File

@ -0,0 +1 @@
{"products":[{"id":"0.41607315815753076","qty":1}],"totalPrice":12}

View 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"}]

View 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"

View File

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: shop-ns

View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
eval $(minikube docker-env)
# build your Node image in Minikubes 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
View 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

View 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

View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

27
z2/shop/package.json Normal file
View 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"
}
}

View 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;
}

View 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
View 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;
}
}

View 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;
}

View 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
View 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
View 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
View 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
View File

3
z2/shop/util/path.js Normal file
View File

@ -0,0 +1,3 @@
const path = require('path');
module.exports = path.dirname(process.mainModule.filename);

8
z2/shop/views/404.ejs Normal file
View File

@ -0,0 +1,8 @@
<%- include('includes/head.ejs') %>
</head>
<body>
<%- include('includes/navigation.ejs') %>
<h1>Page Not Found!</h1>
<%- include('includes/end.ejs') %>

View 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') %>

View 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') %>

View 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>

View File

@ -0,0 +1,4 @@
<script src="/js/main.js"></script>
</body>
</html>

View 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">

View 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>

View 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') %>

View File

View 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') %>

View 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') %>

View 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') %>

View 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') %>