first commit

This commit is contained in:
Cyril Rouillon 2024-05-16 16:05:41 +02:00
commit adf8283ae0
197 changed files with 26666 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
backend/node_modules
client/.angular
client/node_modules
client/.vscode
.vscode
data/
vendore/

42
backend/README.md Normal file
View File

@ -0,0 +1,42 @@
## Backend
# Dev
npm run dev
http://localhost:3000
## Docker Database Dev
```bash
docker-compose -f docker-compose-dev.yml build
docker-compose -f docker-compose-dev.yml up
```
## Agenda
http://localhost:3000/dash/
## API
# Lot
GET http://localhost:3000/api/lot/getInfos/https%3A%2F%2Fwww.interencheres.com%2Fvehicules%2Fvehicules-624955%2Flot-75622389.html
GET http://localhost:3000/api/lot/getPictures/https%3A%2F%2Fwww.interencheres.com%2Fvehicules%2Fvehicules-624955%2Flot-75622389.html
POST http://localhost:3000/api/lot/NextItem
POST http://localhost:3000/api/lot/AuctionedItem
POST http://localhost:3000/api/lot/Bid
# Sale
GET http://localhost:3000/api/sale/getSaleInfos/https%3A%2F%2Fwww.interencheres.com%2Fvehicules%2Fvehicules-624955
GET http://localhost:3000/api/sale/followSale/624955
GET http://localhost:3000/api/sale/sale/624955
POST http://localhost:3000/api/sale/sale
PUT http://localhost:3000/api/sale/sale/624955
DELETE http://localhost:3000/api/sale/sale/624955
GET http://localhost:3000/api/sale/getAll
GET http://localhost:3000/api/sale/getByUrl/https%3A%2F%2Fwww.interencheres.com%2Fvehicules%2Fvehicules-624955
# Favorite
POST http://localhost:3000/api/favorite/save
GET http://localhost:3000/api/favorite/getAll
# Prod
npm run start

View File

@ -0,0 +1,27 @@
const asyncHandler = require("express-async-handler");
const { save, getAll } = require("../services/favorites");
exports.save = asyncHandler(async (req, res, next) => {
try{
let result = await save(req.body);
console.log(result);
res.status(204).send();
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
exports.getAll = asyncHandler(async (req, res, next) => {
console.log("controller getAll");
try{
let result = await getAll();
console.log(result);
res.status(200).send(result);
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});

167
backend/controllers/lot.js Normal file
View File

@ -0,0 +1,167 @@
const asyncHandler = require("express-async-handler");
const fetch = require('node-fetch');
const { LotDb } = require("../services/lotDb");
const lotDb = new LotDb();
const { SaleDb } = require("../services/saleDb");
const saleDb = new SaleDb();
const ApiURL = "http://host.docker.internal:3020/api";
// scrapping
exports.getInfos = asyncHandler(async (req, res, next) => {
let url = req.params.url
url = encodeURIComponent(url);
fetch(ApiURL+'/lot/getInfos/'+url)
.then(response => response.json())
.then(data => {
res.json(data);
})
.catch(error => {
console.error(error);
});
});
exports.getPictures = asyncHandler(async (req, res, next) => {
let url = req.params.url
url = encodeURIComponent(url);
fetch(ApiURL+'/lot/getPictures/'+url)
.then(response => response.json())
.then(data => {
res.json(data);
})
.catch(error => {
console.error(error);
});
});
exports.getLotsBySale = asyncHandler(async (req, res, next) => {
let id = req.params.id
const Sale = await saleDb.get(id);
if(!Sale){
console.error("Sale not found");
return res.status(404).send("Sale not found");
}
Lots = await lotDb.getBySaleId(Sale._id.toString(),Sale.platform);
res.json(Lots);
});
// Follow Sale
exports.NextItem = asyncHandler(async (req, res, next) => {
try{
Sale = await saleDb.getByIDPlatform(req.body.idSalePlatform, req.body.platform);
if(!Sale){
console.error("Sale not found");
return res.status(404).send("Sale not found");
}
let Lot = await lotDb.getByIDPlatform(req.body.idPlatform, req.body.platform);
if(Lot == null){
console.log("Creating new Lot");
Lot = {
idPlatform: String(req.body.idPlatform),
platform: req.body.platform,
timestamp: req.body.timestamp,
lotNumber: String(req.body.lotNumber),
title: req.body.title,
description: req.body.description,
EstimateLow: req.body.EstimateLow,
EstimateHigh: req.body.EstimateHigh,
RawData: req.body.RawData,
sale_id: Sale._id,
}
await lotDb.post(Lot);
}else{
console.log("Updating Lot");
Lot.timestamp = req.body.timestamp;
Lot.lotNumber = String(req.body.lotNumber);
Lot.title = req.body.title;
Lot.description = req.body.description;
Lot.EstimateLow = req.body.EstimateLow;
Lot.EstimateHigh = req.body.EstimateHigh;
Lot.RawData = req.body.RawData;
await lotDb.put(Lot._id, Lot);
}
res.status(204).send();
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
exports.Bid = asyncHandler(async (req, res, next) => {
try{
let Lot = await lotDb.getByIDPlatform(req.body.idPlatform, req.body.platform);
if(Lot){
console.log("Update Lot Bid");
BidInfo = {
timestamp: req.body.timestamp,
amount: req.body.amount,
auctioned_type: req.body.auctioned_type,
}
// If Lot.BidInfo doesn't exist, initialize it as an empty array
if (!Lot.Bids) {
Lot.Bids = [];
}
// Add BidInfo to the array
Lot.Bids.push(BidInfo);
await lotDb.put(Lot._id, Lot);
}else{
console.error("Lot not found");
return res.status(404).send("Lot not found");
}
res.status(204).send();
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
exports.AuctionedItem = asyncHandler(async (req, res, next) => {
try{
let Lot = await lotDb.getByIDPlatform(req.body.idPlatform, req.body.platform);
if(Lot){
console.log("Update Lot AuctionedItem");
Lot.auctioned = {
timestamp: req.body.timestamp,
amount: req.body.amount,
auctioned_type: req.body.auctioned_type,
sold: req.body.sold,
}
await lotDb.put(Lot._id, Lot);
}else{
console.error("Lot not found");
return res.status(404).send("Lot not found");
}
res.status(204).send();
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});

282
backend/controllers/sale.js Normal file
View File

@ -0,0 +1,282 @@
const asyncHandler = require("express-async-handler");
const moment = require('moment-timezone');
const { ObjectId } = require('mongodb');
const { SaleDb } = require("../services/saleDb");
const saleDb = new SaleDb();
const { LotDb } = require("../services/lotDb");
const lotDb = new LotDb();
const agenda = require('../services/agenda');
const {Agent} = require('../services/agent');
const agent = new Agent();
exports.getSaleInfos = asyncHandler(async (req, res, next) => {
let url = req.params.url
agent.getSaleInfos(url)
.then(data => {
return res.status(200).json(data);
})
.catch(error => {
console.error(error);
return res.status(500).send(error);
});
// url = encodeURIComponent(url);
// fetch(ApiAgentURL+'/sale/getSaleInfos/'+url)
// .then(response => response.json())
// .then(data => {
// res.json(data);
// })
// .catch(error => {
// console.error(error);
// });
});
exports.prepareSale = asyncHandler(async (req, res, next) => {
try{
const id = req.params.id;
agent.prepareSale(id)
.then(data => {
return res.status(200).json({"message": "Lots created"});
})
.catch(error => {
console.error(error);
return res.status(500).send(error);
});
// url = encodeURIComponent(url);
// fetch(ApiAgentURL+'/sale/getLotList/'+url)
// .then(response => response.json())
// .then(async data => {
// console.log(data);
// for (let lot of data) {
// lot.sale_id = Sale._id
// await lotDb.post(lot);
// }
// res.status(200).send({"message": "Lots created"})
// })
// .catch(error => {
// console.error(error);
// return res.status(500).send(error);
// });
}catch(err){
console.error(err);
return res.status(500).send(err);
}
});
exports.followSale = asyncHandler(async (req, res, next) => {
try{
const id = req.params.id;
agent.followSale(id)
.then(data => {
res.status(200).send(data);
})
.catch(error => {
console.error(error);
return res.status(500).send(error);
});
}catch(err){
console.error(err);
return res.status(500).send(err);
}
});
// DB
exports.get = asyncHandler(async (req, res, next) => {
try{
const id = req.params.id;
let result = await saleDb.get(id);
res.status(200).send(result);
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
exports.post = asyncHandler(async (req, res, next) => {
try{
// check if double
let Sale = await saleDb.getByIDPlatform(req.body.idPlatform, req.body.platform);
if(Sale){
return res.status(500).send("Sale already exists");
}
let createData = await saleDb.post(req.body);
console.log(createData.insertedId);
const NowParis = moment.tz(new Date(),"Europe/Paris")
// Scheduling the Prepare job
const dateSaleMinus24Hours = moment.tz(req.body.date, "Europe/Paris").subtract(24, 'hours');
if(dateSaleMinus24Hours.isAfter(NowParis)){
const jobPrepare = agenda.create('prepareSale', { saleId: createData.insertedId });
jobPrepare.schedule(dateSaleMinus24Hours.toDate());
await jobPrepare.save();
}else{ console.log("Sale is less than 24 hours away, no Prepare Job");}
// Scheduling the Follow job
const dateSale = moment.tz(req.body.date, "Europe/Paris");
if(dateSale.isAfter(NowParis)){
const jobFollow = agenda.create('followSale', { saleId: createData.insertedId });
jobFollow.schedule(dateSale.toDate());
await jobFollow.save();
}else{ console.log("Sale is in the past, no Follow Job");}
res.status(204).send();
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
exports.put = asyncHandler(async (req, res, next) => {
try{
const id = req.params.id;
let updatedDocument = { ...req.body };
delete updatedDocument._id;
console.log(updatedDocument);
let result = await saleDb.put(id, updatedDocument);
console.log(result);
res.status(200).send(result);
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
exports.delete = asyncHandler(async (req, res, next) => {
try{
const id = req.params.id;
// Remove all lots linked to the sale
console.log("Deleting lots sale_id: "+id);
await lotDb.deleteAllLotBySaleId(id);
// Remove the sale
await saleDb.remove(id);
//remove the Jobs
const JobSale = await agenda.jobs({ 'data.saleId': new ObjectId(id) });
for (const job of JobSale) {
await job.remove();
}
res.status(200).send({"message": "Sale and Lots deleted"});
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
// Fucntions
exports.getAll = asyncHandler(async (req, res, next) => {
try{
let result = await saleDb.getAll();
res.status(200).send(result);
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
exports.getByUrl = asyncHandler(async (req, res, next) => {
try{
let url = req.params.url
url = decodeURIComponent(url);
let result = await saleDb.getByUrl(url);
//console.log(result);
res.status(200).send(result);
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
exports.postProcessing = asyncHandler(async (req, res, next) => {
try{
const id = req.params.id;
Sale = await saleDb.get(id);
if(!Sale){
console.error("Sale not found");
return res.status(404).send("Sale not found");
}
Lots = await lotDb.getBySaleId(Sale._id.toString(),Sale.platform);
let startTime = 0;
if (Array.isArray(Lots[0].Bids)) {
startTime = Lots[0].Bids[0].timestamp;
}else{
startTime = Lots[0].timestamp;
}
let LastBid = [...Lots].reverse().find(lot => lot.auctioned !== undefined);
let endTime = 0;
if (Array.isArray(LastBid.Bids)) {
endTime = LastBid.Bids[LastBid.Bids.length-1].timestamp;
}else{
endTime = LastBid.timestamp;
}
console.log("Start Time: "+startTime);
console.log("End Time: "+endTime);
let duration = endTime-startTime;
let totalAmount = 0;
for (let lot of Lots) {
if (lot.auctioned) {
totalAmount += lot.auctioned.amount;
}
}
function calculateMedian(array) {
array.sort((a, b) => a - b);
let middleIndex = Math.floor(array.length / 2);
if (array.length % 2 === 0) { // array has an even length
return (array[middleIndex - 1] + array[middleIndex]) / 2;
} else { // array has an odd length
return array[middleIndex];
}
}
const amounts = Lots.map(lot => lot.auctioned?.amount).filter(Boolean);
//console.error(Lots);
let postProcessing = {
nbrLots: Lots.length,
duration: duration,
durationPerLots: (duration/Lots.length).toFixed(0),
totalAmount: totalAmount,
averageAmount: (totalAmount/Lots.length).toFixed(2),
medianAmount: calculateMedian(amounts).toFixed(2),
}
console.log(postProcessing);
Sale.postProcessing = postProcessing;
await saleDb.put(Sale._id, Sale);
res.status(200).send({"message": "Post Processing done"});
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});

27
backend/index.js Normal file
View File

@ -0,0 +1,27 @@
const express = require('express')
const app = express()
var bodyParser = require('body-parser');
app.use(bodyParser.json())
const cors = require('cors');
app.use(cors());
// Enable preflight requests for all routes
app.options('*', cors());
// Agenda Scheduller
const agenda = require('./services/agenda');
(async function() {
await agenda.start();
})();
// Agenda UI
var Agendash = require("agendash");
app.use("/dash", Agendash(agenda));
// routes
app.use('/api/lot', require('./routes/lot'));
app.use('/api/sale', require('./routes/sale'));
app.use('/api/favorite', require('./routes/favorite'));
module.exports = app

10
backend/launch.json Normal file
View File

@ -0,0 +1,10 @@
{
"type": "node",
"request": "attach",
"name": "Attach to Process",
"restart": true,
"address": "127.0.0.1",
"port": 53481,
"localRoot": "${workspaceFolder}",
"remoteRoot": "${workspaceFolder}"
}

5416
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
backend/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "jucundus",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "nodemon --watch ./ server.js --ignore node_modules/"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@angular/cli": "^17.1.3",
"@hokify/agenda": "^6.3.0",
"agendash": "^4.0.0",
"cors": "^2.8.5",
"express": "^4.18.2",
"express-async-handler": "^1.2.0",
"mongodb": "^6.5.0",
"node-fetch": "^2.7.0"
},
"devDependencies": {
"nodemon": "^3.0.3"
}
}

View File

@ -0,0 +1,7 @@
const controllers = require('../controllers/favorite')
const router = require('express').Router()
router.post('/save/', controllers.save)
router.get('/getAll/', controllers.getAll)
module.exports = router

12
backend/routes/lot.js Normal file
View File

@ -0,0 +1,12 @@
const controllers = require('../controllers/lot')
const router = require('express').Router()
router.get('/getInfos/:url', controllers.getInfos)
router.get('/getPictures/:url', controllers.getPictures)
router.get('/getLotsBySale/:id', controllers.getLotsBySale)
router.post('/NextItem/', controllers.NextItem)
router.post('/AuctionedItem/', controllers.AuctionedItem)
router.post('/Bid/', controllers.Bid)
module.exports = router

21
backend/routes/sale.js Normal file
View File

@ -0,0 +1,21 @@
const controllers = require('../controllers/sale')
const router = require('express').Router()
// AuctionAgent
router.get('/getSaleInfos/:url', controllers.getSaleInfos)
router.get('/prepareSale/:id', controllers.prepareSale)
router.get('/followSale/:id', controllers.followSale)
// DB
router.get('/sale/:id', controllers.get)
router.post('/sale/', controllers.post)
router.put('/sale/:id', controllers.put)
router.delete('/sale/:id', controllers.delete)
router.get('/getAll/', controllers.getAll)
router.get('/getByUrl/:url', controllers.getByUrl)
router.get('/postProcessing/:id', controllers.postProcessing)
module.exports = router

5
backend/server.js Normal file
View File

@ -0,0 +1,5 @@
const app = require('./index.js')
const port = process.env.PORT || '3000'
app.listen(port, '0.0.0.0', () => {
console.log('Server listening on port '+port);
});

View File

@ -0,0 +1,23 @@
const Agenda = require("agenda");
const agenda = new Agenda({ db: { address: "mongodb://db:27017/agendaDb" } });
const {Agent} = require('./agent');
const agent = new Agent();
// Define a job
agenda.define('followSale', async (job, done) => {
const { saleId } = job.attrs.data;
agent.followSale(saleId)
.then(data => {;
done();
})
});
agenda.define('prepareSale', async (job, done) => {
const { saleId } = job.attrs.data;
agent.prepareSale(saleId)
.then(data => {;
done();
})
});
module.exports = agenda;

89
backend/services/agent.js Normal file
View File

@ -0,0 +1,89 @@
const fetch = require('node-fetch');
const { LotDb } = require("./lotDb");
const lotDb = new LotDb();
const { SaleDb } = require("./saleDb");
const saleDb = new SaleDb();
const moment = require('moment-timezone');
const Agent = class
{
constructor()
{
this.ApiAgentURL = "http://host.docker.internal:3020/api";
}
async getSaleInfos(url)
{
return new Promise((resolve, reject) => {
url = encodeURIComponent(url);
fetch(this.ApiAgentURL+'/sale/getSaleInfos/'+url)
.then(response => response.json())
.then(data => {
resolve(data);
})
.catch(error => {
reject(error);
});
});
}
async prepareSale(id)
{
return new Promise(async (resolve, reject) => {
let Sale = await saleDb.get(id);
const DateSale = moment.tz(Sale.date, "Europe/Paris");
const NowParis = moment.tz(new Date(),"Europe/Paris")
if (NowParis.isBefore(DateSale)){
let url = Sale.url
url = encodeURIComponent(url);
fetch(this.ApiAgentURL+'/sale/getLotList/'+url)
.then(response => response.json())
.then( async data => {
for (let lot of data) {
lot.sale_id = Sale._id
await lotDb.post(lot);
}
resolve(data);
})
.catch(error => {
reject(error);
});
}else{
console.log("Sale started or finished");
resolve([]);
}
});
}
async followSale(id)
{
return new Promise(async (resolve, reject) => {
let Sale = await saleDb.get(id);
let url = Sale.url
url = encodeURIComponent(url);
fetch(this.ApiAgentURL+'/sale/followSale/'+url)
.then(response => response.json())
.then(async data => {
// set the Sale status to following
Sale.status = "following";
Sale = await saleDb.put(id, Sale);
resolve(data);
})
.catch(error => {
reject(error);
});
});
}
}
module.exports = {Agent};

18
backend/services/db.js Normal file
View File

@ -0,0 +1,18 @@
const MongoClient = require("mongodb").MongoClient;
const connectionString = "mongodb://db:27017";
const client = new MongoClient(connectionString);
let db;
const connectDb = async () => {
if (db) return db;
try {
const conn = await client.connect();
db = conn.db("jucundus");
return db;
} catch(e) {
console.error(e);
}
};
module.exports = connectDb;

View File

@ -0,0 +1,27 @@
const connectDb = require("./db");
const save = async (newDocument) => {
const db = await connectDb();
if (!db) {
throw new Error('Database not connected');
}
const collection = db.collection("Favorites");
let result = await collection.insertOne(newDocument);
return result;
};
const getAll = async () => {
const db = await connectDb();
if (!db) {
throw new Error('Database not connected');
}
const collection = db.collection("Favorites");
let result = await collection.find({}).toArray();
return result;
};
module.exports = { save, getAll };

75
backend/services/lotDb.js Normal file
View File

@ -0,0 +1,75 @@
const { ObjectId } = require('mongodb');
const connectDb = require("./db");
const LotDb = class
{
constructor()
{
this.getCollection();
}
async getCollection()
{
const db = await connectDb();
if (!db) {
throw new Error('Database not connected');
}
this.collection = db.collection("Lots");
}
// CRUD
async get(id)
{
let result = await this.collection.findOne({_id: new ObjectId(id)});
return result;
}
async post(newDocument)
{
let result = await this.collection.insertOne(newDocument);
return result;
}
async put(id, data)
{
let result = await this.collection.updateOne({_id: new ObjectId(id)}, {$set: data});
return result;
}
async remove(id)
{
let result = await this.collection.deleteOne({_id: new ObjectId(id)});
return result;
}
// Fucntions
async getAll()
{
let result = await this.collection.find({}).toArray();
return result;
}
async getBySaleId(idSalePlatform, platformName)
{
console.log(platformName);
let result = await this.collection.find({sale_id: new ObjectId(idSalePlatform), platform: platformName});
return result.toArray();
}
async getByIDPlatform(idLotPlatform, platformName)
{
let result = await this.collection.findOne({idPlatform: String(idLotPlatform), platform: platformName});
return result;
}
async deleteAllLotBySaleId(sale_id){
let result = await this.collection.deleteMany({sale_id: new ObjectId(sale_id)});
return result;
}
}
module.exports = {LotDb};

View File

@ -0,0 +1,67 @@
const { ObjectId } = require('mongodb');
const connectDb = require("./db");
const SaleDb = class
{
constructor()
{
this.getCollection();
}
async getCollection()
{
const db = await connectDb();
if (!db) {
throw new Error('Database not connected');
}
this.collection = db.collection("Sales");
}
// CRUD
async get(id)
{
let result = await this.collection.findOne({_id: new ObjectId(id)});
return result;
}
async post(newDocument)
{
let result = await this.collection.insertOne(newDocument);
return result;
}
async put(id, data)
{
let result = await this.collection.updateOne({_id: new ObjectId(id)}, {$set: data});
return result;
}
async remove(id)
{
let result = await this.collection.deleteOne({_id: new ObjectId(id)});
return result;
}
// Fucntions
async getAll()
{
let result = await this.collection.find({}).toArray();
return result;
}
async getByUrl(url)
{
let result = await this.collection.findOne({url: url});
return result;
}
async getByIDPlatform(idSalePlatform, platformName)
{
let result = await this.collection.findOne({idPlatform: String(idSalePlatform), platform: String(platformName)});
return result;
}
}
module.exports = { SaleDb };

BIN
client/.DS_Store vendored Normal file

Binary file not shown.

50
client/.eslintrc.json Normal file
View File

@ -0,0 +1,50 @@
{
"root": true,
"ignorePatterns": [
"projects/**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"parserOptions": {
"project": [
"tsconfig.json"
],
"createDefaultProgram": true
},
"extends": [
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "app",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "app",
"style": "kebab-case"
}
]
}
},
{
"files": [
"*.html"
],
"extends": [
"plugin:@angular-eslint/template/recommended"
],
"rules": {}
}
]
}

21
client/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Umut Esen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

18
client/README copy.md Normal file
View File

@ -0,0 +1,18 @@
## Backend
# Dev
npm run dev
http://localhost:3000
# Prod
npm run start
## Frontend
# Dev
ng serve
http://localhost:4200
## Docker Database Dev
```bash
docker-compose -f docker-compose-dev.yml build
docker-compose -f docker-compose-dev.yml up
```

67
client/README.md Normal file
View File

@ -0,0 +1,67 @@
[![Build status](https://dev.azure.com/umutesen/onthecode/_apis/build/status/Material%20Template%20CI)](https://dev.azure.com/umutesen/onthecode/_build/latest?definitionId=11)
# Angular Material Starter Template
![Product Gif](https://github.com/umutesen/angular-material-template/blob/media/material-template-demo.gif)
Angular Material Starter Template is a free template built with Angular and Angular Material. You can use it out of the box without having to change any file paths. Everything you need to start development on an Angular project is here.
Angular Material starter template has been built with the official style guide in mind, which means it promotes a clean folder structure and separation of concerns. The material template is fully responsive and contains the fundamental building blocks of a scalable Angular application:
Authentication module with login, logout and password reset components
Responsive Admin dashboard with sidebar
Account area with change password component
All Angular Material components
In addition to Angular, other well-known open-source libraries such as rxjs, moment and ngx-logger are also included.
This application template came as a result of several applications that I have developed over the past few years.
Having mostly used Angular Material component, I wanted to create a starter template to save time for greenfield projects. I developed it based on user feedback and it is a powerful Angular admin dashboard, which allows you to build products like admin panels, content management systems (CMS) and customer relationship management (CRM) software.
## Starter Template Features
Clean folder structure
Core module
Shared module
Example feature modules
Lazy-loaded feature modules
Global error-handling
Error logging with ngx-logger (logging to browser & remote API)
HTTP Interceptors to inject JWT-tokens Authentication and role guards (for Role-based access)
Shows spinner for all HTTP requests
Angular flex layout
Browser Support
At present, the template aims to support the last two versions of the following browsers:
Chrome
Firefox
Microsoft Edge
Safari
Opera
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

122
client/angular.json Normal file
View File

@ -0,0 +1,122 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"angular-material-template": {
"projectType": "application",
"schematics": {
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/angular-material-template",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css",
"node_modules/primeng/resources/themes/lara-light-blue/theme.css",
"node_modules/primeng/resources/primeng.min.css"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "2mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true,
"allowedCommonJsDependencies": ["moment-timezone"]
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "angular-material-template:build:production"
},
"development": {
"buildTarget": "angular-material-template:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "angular-material-template:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"src/**/*.html"
]
}
}
}
}
},
"cli": {
"schematicCollections": [
"@angular-eslint/schematics"
]
}
}

44
client/karma.conf.js Normal file
View File

@ -0,0 +1,44 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/angular-material-template'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

15330
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

57
client/package.json Normal file
View File

@ -0,0 +1,57 @@
{
"name": "angular-material-template",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"lint": "ng lint",
"test": "ng test",
"build-production": "ng build --configuration=production --base-href=/angular-material-template/"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.2.2",
"@angular/cdk": "^16.2.14",
"@angular/common": "^17.2.2",
"@angular/compiler": "^17.2.2",
"@angular/core": "^17.2.2",
"@angular/flex-layout": "^15.0.0-beta.42",
"@angular/forms": "^17.2.2",
"@angular/material": "^16.2.14",
"@angular/material-moment-adapter": "^16.2.14",
"@angular/platform-browser": "^17.2.2",
"@angular/platform-browser-dynamic": "^17.2.2",
"@angular/router": "^17.2.2",
"jwt-decode": "^3.1.2",
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"ngx-logger": "^5.0.7",
"primeng": "^17.8.0",
"rxjs": "^7.8.1",
"tslib": "^2.3.0",
"zone.js": "~0.14.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.2.1",
"@angular-eslint/builder": "14.1.1",
"@angular-eslint/eslint-plugin": "14.1.1",
"@angular-eslint/eslint-plugin-template": "14.1.1",
"@angular-eslint/template-parser": "14.1.1",
"@angular/cli": "^17.2.1",
"@angular/compiler-cli": "^17.2.2",
"@types/jasmine": "~3.10.0",
"@types/node": "^12.11.1",
"@typescript-eslint/eslint-plugin": "^5.36.2",
"@typescript-eslint/parser": "^5.36.2",
"eslint": "^8.23.0",
"jasmine-core": "~4.0.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.1.0",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.7.0",
"typescript": "~5.3.3"
}
}

BIN
client/src/.DS_Store vendored Normal file

Binary file not shown.

BIN
client/src/app/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,75 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AuthGuard } from './core/guards/auth.guard';
const appRoutes: Routes = [
{
path: 'auth',
loadChildren: () => import('./features/auth/auth.module').then(m => m.AuthModule),
},
{
path: 'dashboard',
loadChildren: () => import('./features/dashboard/dashboard.module').then(m => m.DashboardModule),
canActivate: [AuthGuard]
},
{
path: 'sales',
loadChildren: () => import('./features/sales/sales.module').then(m => m.SalesModule),
canActivate: [AuthGuard]
},
{
path: 'favorites',
loadChildren: () => import('./features/favorites/favorites.module').then(m => m.FavoritesModule),
canActivate: [AuthGuard]
},
{
path: 'pictures',
loadChildren: () => import('./features/pictures/pictures.module').then(m => m.PicturesModule),
canActivate: [AuthGuard]
},
{
path: 'customers',
loadChildren: () => import('./features/customers/customers.module').then(m => m.CustomersModule),
canActivate: [AuthGuard]
},
{
path: 'users',
loadChildren: () => import('./features/users/users.module').then(m => m.UsersModule),
canActivate: [AuthGuard]
},
{
path: 'account',
loadChildren: () => import('./features/account/account.module').then(m => m.AccountModule),
canActivate: [AuthGuard]
},
{
path: 'icons',
loadChildren: () => import('./features/icons/icons.module').then(m => m.IconsModule),
canActivate: [AuthGuard]
},
{
path: 'typography',
loadChildren: () => import('./features/typography/typography.module').then(m => m.TypographyModule),
canActivate: [AuthGuard]
},
{
path: 'about',
loadChildren: () => import('./features/about/about.module').then(m => m.AboutModule),
canActivate: [AuthGuard]
},
{
path: '**',
redirectTo: 'dashboard',
pathMatch: 'full'
}
];
@NgModule({
imports: [
RouterModule.forRoot(appRoutes)
],
exports: [RouterModule],
providers: []
})
export class AppRoutingModule { }

View File

@ -0,0 +1,7 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `<router-outlet></router-outlet>`
})
export class AppComponent {}

View File

@ -0,0 +1,32 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { CoreModule } from './core/core.module';
import { SharedModule } from './shared/shared.module';
import { CustomMaterialModule } from './custom-material/custom-material.module';
import { AppRoutingModule } from './app-routing.module';
import { LoggerModule } from 'ngx-logger';
import { environment } from '../environments/environment';
@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
BrowserAnimationsModule,
CoreModule,
SharedModule,
CustomMaterialModule.forRoot(),
AppRoutingModule,
LoggerModule.forRoot({
serverLoggingUrl: `http://my-api/logs`,
level: environment.logLevel,
serverLogLevel: environment.serverLogLevel
})
],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@ -0,0 +1,49 @@
import { NgModule, Optional, SkipSelf, ErrorHandler } from '@angular/core';
import { CommonModule } from '@angular/common';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { MediaMatcher } from '@angular/cdk/layout';
import { NGXLogger } from 'ngx-logger';
import { AuthInterceptor } from './interceptors/auth.interceptor';
import { SpinnerInterceptor } from './interceptors/spinner.interceptor';
import { AuthGuard } from './guards/auth.guard';
import { throwIfAlreadyLoaded } from './guards/module-import.guard';
import { GlobalErrorHandler } from './services/globar-error.handler';
import { AdminGuard } from './guards/admin.guard';
@NgModule({
imports: [
CommonModule,
HttpClientModule
],
declarations: [
],
providers: [
AuthGuard,
AdminGuard,
MediaMatcher,
{
provide: HTTP_INTERCEPTORS,
useClass: SpinnerInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
},
{
provide: ErrorHandler,
useClass: GlobalErrorHandler
},
{ provide: NGXLogger, useClass: NGXLogger },
{ provide: 'LOCALSTORAGE', useValue: window.localStorage }
],
exports: [
]
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
throwIfAlreadyLoaded(parentModule, 'CoreModule');
}
}

View File

@ -0,0 +1,66 @@
import { AdminGuard } from './admin.guard';
describe('AdminGuard', () => {
let router;
let authService;
beforeEach(() => {
router = jasmine.createSpyObj(['navigate']);
authService = jasmine.createSpyObj(['getCurrentUser']);
});
it('create an instance', () => {
const guard = new AdminGuard(router, authService);
expect(guard).toBeTruthy();
});
it('returns true if user is admin', () => {
const user = { 'isAdmin': true };
authService.getCurrentUser.and.returnValue(user);
const guard = new AdminGuard(router, authService);
const result = guard.canActivate();
expect(result).toBe(true);
});
it('returns false if user does not exist', () => {
authService.getCurrentUser.and.returnValue(null);
const guard = new AdminGuard(router, authService);
const result = guard.canActivate();
expect(result).toBe(false);
});
it('returns false if user is not admin', () => {
const user = { 'isAdmin': false };
authService.getCurrentUser.and.returnValue(user);
const guard = new AdminGuard(router, authService);
const result = guard.canActivate();
expect(result).toBe(false);
});
it('redirects to root if user is not an admin', () => {
const user = { 'isAdmin': false };
authService.getCurrentUser.and.returnValue(user);
const guard = new AdminGuard(router, authService);
guard.canActivate();
expect(router.navigate).toHaveBeenCalledWith(['/']);
});
it('redirects to root if user does not exist', () => {
authService.getCurrentUser.and.returnValue(null);
const guard = new AdminGuard(router, authService);
guard.canActivate();
expect(router.navigate).toHaveBeenCalledWith(['/']);
});
});

View File

@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { AuthenticationService } from '../services/auth.service';
@Injectable()
export class AdminGuard {
constructor(private router: Router,
private authService: AuthenticationService) { }
canActivate() {
const user = this.authService.getCurrentUser();
if (user && user.isAdmin) {
return true;
} else {
this.router.navigate(['/']);
return false;
}
}
}

View File

@ -0,0 +1,79 @@
import { AuthGuard } from './auth.guard';
import * as moment from 'moment';
describe('AuthGuard', () => {
let router;
let authService;
let notificationService;
beforeEach(() => {
router = jasmine.createSpyObj(['navigate']);
authService = jasmine.createSpyObj(['getCurrentUser']);
notificationService = jasmine.createSpyObj(['openSnackBar']);
});
it('create an instance', () => {
const guard = new AuthGuard(router, notificationService, authService);
expect(guard).toBeTruthy();
});
it('returns false if user is null', () => {
authService.getCurrentUser.and.returnValue(null);
const guard = new AuthGuard(router, notificationService, authService);
const result = guard.canActivate();
expect(result).toBe(false);
});
it('redirects to login if user is null', () => {
authService.getCurrentUser.and.returnValue(null);
const guard = new AuthGuard(router, notificationService, authService);
guard.canActivate();
expect(router.navigate).toHaveBeenCalledWith(['auth/login']);
});
it('does not display expired notification if user is null', () => {
authService.getCurrentUser.and.returnValue(null);
const guard = new AuthGuard(router, notificationService, authService);
guard.canActivate();
expect(notificationService.openSnackBar).toHaveBeenCalledTimes(0);
});
it('redirects to login if user session has expired', () => {
const user = { expiration: moment().add(-1, 'minutes') };
authService.getCurrentUser.and.returnValue(user);
const guard = new AuthGuard(router, notificationService, authService);
guard.canActivate();
expect(router.navigate).toHaveBeenCalledWith(['auth/login']);
});
it('displays notification if user session has expired', () => {
const user = { expiration: moment().add(-1, 'seconds') };
authService.getCurrentUser.and.returnValue(user);
const guard = new AuthGuard(router, notificationService, authService);
guard.canActivate();
expect(notificationService.openSnackBar)
.toHaveBeenCalledWith('Your session has expired');
});
it('returns true if user session is valid', () => {
const user = { expiration: moment().add(1, 'minutes') };
authService.getCurrentUser.and.returnValue(user);
const guard = new AuthGuard(router, notificationService, authService);
const result = guard.canActivate();
expect(result).toBe(true);
});
});

View File

@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import * as moment from 'moment';
import { AuthenticationService } from '../services/auth.service';
import { NotificationService } from '../services/notification.service';
@Injectable()
export class AuthGuard {
constructor(private router: Router,
private notificationService: NotificationService,
private authService: AuthenticationService) { }
canActivate() {
const user = this.authService.getCurrentUser();
if (user && user.expiration) {
if (moment() < moment(user.expiration)) {
return true;
} else {
this.notificationService.openSnackBar('Your session has expired');
this.router.navigate(['auth/login']);
return false;
}
}
this.router.navigate(['auth/login']);
return false;
}
}

View File

@ -0,0 +1,5 @@
export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) {
if (parentModule) {
throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);
}
}

View File

@ -0,0 +1,44 @@
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpErrorResponse } from '@angular/common/http';
import { HttpRequest } from '@angular/common/http';
import { HttpHandler } from '@angular/common/http';
import { HttpEvent } from '@angular/common/http';
import { tap } from 'rxjs/operators';
import { AuthenticationService } from '../services/auth.service';
import { MatDialog } from '@angular/material/dialog';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthenticationService,
private router: Router,
private dialog: MatDialog) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const user = this.authService.getCurrentUser();
if (user && user.token) {
const cloned = req.clone({
headers: req.headers.set('Authorization',
'Bearer ' + user.token)
});
return next.handle(cloned).pipe(tap(() => { }, (err: any) => {
if (err instanceof HttpErrorResponse) {
if (err.status === 401) {
this.dialog.closeAll();
this.router.navigate(['/auth/login']);
}
}
}));
} else {
return next.handle(req);
}
}
}

View File

@ -0,0 +1,33 @@
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpResponse } from '@angular/common/http';
import { HttpRequest } from '@angular/common/http';
import { HttpHandler } from '@angular/common/http';
import { HttpEvent } from '@angular/common/http';
import { tap } from 'rxjs/operators';
import { SpinnerService } from './../services/spinner.service';
@Injectable()
export class SpinnerInterceptor implements HttpInterceptor {
constructor(private spinnerService: SpinnerService) { }
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
this.spinnerService.show();
return next
.handle(req)
.pipe(
tap((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
this.spinnerService.hide();
}
}, (error) => {
this.spinnerService.hide();
})
);
}
}

View File

@ -0,0 +1,88 @@
import { Injectable, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { LotInfo } from './model/lotInfo.interface';
import { SaleInfo } from './model/saleInfo.interface';
import { Sale } from './model/sale.interface';
import { Lot } from './model/lot.interface';
@Injectable({
providedIn: 'root'
})
export class apiService {
ApiURL = "http://localhost:3000/api";
constructor(private http: HttpClient){}
// Lot
getLotInfo(url: string): Observable<LotInfo> {
let encodeUrl = encodeURIComponent(url);
return this.http.get<LotInfo>(this.ApiURL+'/lot/getInfos/'+encodeUrl);
}
getPictures(url: string): Observable<String[]> {
let encodeUrl = encodeURIComponent(url);
return this.http.get<String[]>(this.ApiURL+'/lot/getPictures/'+encodeUrl);
}
getLotsBySale(_id: String): Observable<Lot[]> {
return this.http.get<Lot[]>(this.ApiURL+'/lot/getLotsBySale/'+_id);
}
// Sale
getSaleInfos(url: string): Observable<SaleInfo> {
let encodeUrl = encodeURIComponent(url);
return this.http.get<SaleInfo>(this.ApiURL+'/sale/getSaleInfos/'+encodeUrl);
}
prepareSale(saleInfo: SaleInfo): Observable<any> {
let follow = this.http.get<any>(this.ApiURL+'/sale/prepareSale/'+saleInfo._id);
return follow
}
followSale(saleInfo: SaleInfo): Observable<any> {
let follow = this.http.get<any>(this.ApiURL+'/sale/followSale/'+saleInfo._id);
return follow
}
// CRUD DB Sale
getSale(_id: String): Observable<Sale> {
return this.http.get<Sale>(this.ApiURL+'/sale/sale/'+_id);
}
saveSale(saleInfo: SaleInfo): Observable<SaleInfo> {
return this.http.post<SaleInfo>(this.ApiURL+'/sale/sale', saleInfo);
}
updateSale(saleInfo: SaleInfo): Observable<SaleInfo> {
return this.http.put<SaleInfo>(this.ApiURL+'/sale/sale/'+saleInfo._id, saleInfo);
}
deleteSale(_id: String): Observable<any> {
return this.http.delete<any>(this.ApiURL+'/sale/sale/'+_id);
}
// Function DB Sale
getAllSale(): Observable<SaleInfo[]> {
return this.http.get<SaleInfo[]>(this.ApiURL+'/sale/getAll');
}
postProcessing(_id: String): Observable<any> {
return this.http.get<any>(this.ApiURL+'/sale/postProcessing/'+_id);
}
//Favorites
saveFavorite(lotInfo: LotInfo, saleInfo: SaleInfo, picture: String, dateTime: string, buyProject: boolean, maxPrice: number, Note: string): Observable<any> {
return this.http.post(this.ApiURL+'/favorite/save', {lotInfo, saleInfo, picture, dateTime, buyProject, maxPrice, Note});
}
getAllFavorite(): Observable<any> {
return this.http.get(this.ApiURL+'/favorite/getAll');
}
}

View File

@ -0,0 +1,71 @@
import { Injectable, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { delay, map } from 'rxjs/operators';
import * as jwt_decode from 'jwt-decode';
import * as moment from 'moment';
import { environment } from '../../../environments/environment';
import { of, EMPTY } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class AuthenticationService {
constructor(private http: HttpClient,
@Inject('LOCALSTORAGE') private localStorage: Storage) {
}
login(email: string, password: string) {
return of(true)
.pipe(delay(1000),
map((/*response*/) => {
// set token property
// const decodedToken = jwt_decode(response['token']);
// store email and jwt token in local storage to keep user logged in between page refreshes
this.localStorage.setItem('currentUser', JSON.stringify({
token: 'aisdnaksjdn,axmnczm',
isAdmin: true,
email: 'john.doe@gmail.com',
id: '12312323232',
alias: 'john.doe@gmail.com'.split('@')[0],
expiration: moment().add(1, 'days').toDate(),
fullName: 'John Doe'
}));
return true;
}));
}
logout(): void {
// clear token remove user from local storage to log user out
this.localStorage.removeItem('currentUser');
}
getCurrentUser(): any {
// TODO: Enable after implementation
// return JSON.parse(this.localStorage.getItem('currentUser'));
return {
token: 'aisdnaksjdn,axmnczm',
isAdmin: true,
email: 'john.doe@gmail.com',
id: '12312323232',
alias: 'john.doe@gmail.com'.split('@')[0],
expiration: moment().add(1, 'days').toDate(),
fullName: 'John Doe'
};
}
passwordResetRequest(email: string) {
return of(true).pipe(delay(1000));
}
changePassword(email: string, currentPwd: string, newPwd: string) {
return of(true).pipe(delay(1000));
}
passwordReset(email: string, token: string, password: string, confirmPassword: string): any {
return of(true).pipe(delay(1000));
}
}

View File

@ -0,0 +1,27 @@
import { ErrorHandler, Injectable, Injector } from '@angular/core';
import { NGXLogger } from 'ngx-logger';
@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
constructor(private injector: Injector) { }
handleError(error: Error) {
// Obtain dependencies at the time of the error
// This is because the GlobalErrorHandler is registered first
// which prevents constructor dependency injection
const logger = this.injector.get(NGXLogger);
const err = {
message: error.message ? error.message : error.toString(),
stack: error.stack ? error.stack : ''
};
// Log the error
logger.error(err);
// Re-throw the error
throw error;
}
}

View File

@ -0,0 +1,29 @@
export interface Bid {
timestamp: string;
amount: number;
auctioned_type: string;
}
export interface Auctioned {
timestamp: string;
amount: number;
auctioned_type: string;
sold: boolean;
}
export interface Lot {
_id: {
$oid: string;
};
idPlatform: string;
platform: string;
timestamp: string;
lotNumber: string;
RawData?: Object;
sale_id: {
$oid: string;
};
Bids?: Bid[];
auctioned?: Auctioned;
}

View File

@ -0,0 +1,15 @@
export interface LotInfo {
idLotInterencheres: string;
url: string;
title: string;
lotNumber: string;
EstimateLow: number;
EstimateHigh: number;
Description: string;
feesText: string;
fees: string;
saleInfo: {
idSaleInterencheres: string;
url: string;
};
}

View File

@ -0,0 +1,24 @@
export interface PostProcessing {
nbrLots: number;
duration: number;
durationPerLots: string;
totalAmount: number;
averageAmount: string;
medianAmount: string;
}
export interface Sale {
_id: {
$oid: string;
};
idPlatform: string;
platform: string;
url: string;
title: string;
date: string;
location: string;
saleHouseName: string;
status: string;
postProcessing?: PostProcessing;
}

View File

@ -0,0 +1,11 @@
export interface SaleInfo {
_id: string;
idPlatform: string;
platform: string;
url: string;
title: string;
date: string;
location: string;
saleHouseName: string;
status: string;
}

View File

@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
@Injectable({
providedIn: 'root'
})
export class NotificationService {
constructor(private snackBar: MatSnackBar) { }
public openSnackBar(message: string) {
this.snackBar.open(message, '', {
duration: 5000
});
}
}

View File

@ -0,0 +1,36 @@
import { SpinnerConsumer } from '../../shared/mocks/spinner-consumer';
import { SpinnerService } from './spinner.service';
describe('BusyIndicatorService', () => {
let component: SpinnerService;
let consumer1: SpinnerConsumer;
let consumer2: SpinnerConsumer;
beforeEach(() => {
component = new SpinnerService();
consumer1 = new SpinnerConsumer(component);
consumer2 = new SpinnerConsumer(component);
});
it('should be created', () => {
expect(component).toBeTruthy();
});
it('should initialise visibility to false', () => {
component.visibility.subscribe((value: boolean) => {
expect(value).toBe(false);
});
});
it('should broadcast visibility to all consumers', () => {
expect(consumer1.isBusy).toBe(false);
expect(consumer2.isBusy).toBe(false);
});
it('should broadcast visibility to all consumers when the value changes', () => {
component.visibility.next(true);
expect(consumer1.isBusy).toBe(true);
expect(consumer2.isBusy).toBe(true);
});
});

View File

@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class SpinnerService {
visibility = new BehaviorSubject(false);
constructor() {
}
show() {
this.visibility.next(true);
}
hide() {
this.visibility.next(false);
}
}

View File

@ -0,0 +1,98 @@
import { NgModule, LOCALE_ID } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatMomentDateModule, MomentDateAdapter, MAT_MOMENT_DATE_FORMATS } from '@angular/material-moment-adapter';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule} from '@angular/material/form-field';
import {MatRadioModule} from '@angular/material/radio';
import { MatSelectModule } from '@angular/material/select';
import {MatSliderModule} from '@angular/material/slider';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatMenuModule } from '@angular/material/menu';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatBadgeModule } from '@angular/material/badge';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatListModule } from '@angular/material/list';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatCardModule } from '@angular/material/card';
import { MatStepperModule } from '@angular/material/stepper';
import {MatTabsModule} from '@angular/material/tabs';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import {MatProgressBarModule} from '@angular/material/progress-bar';
import { MatDialogModule } from '@angular/material/dialog';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort';
import { MatPaginatorModule } from '@angular/material/paginator';
import { SelectCheckAllComponent } from './select-check-all/select-check-all.component';
import { DragDropModule } from '@angular/cdk/drag-drop';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
// Prime NG
import { GalleriaModule } from 'primeng/galleria';
import { ImageModule } from 'primeng/image';
import { SkeletonModule } from 'primeng/skeleton';
export const MY_FORMATS = {
parse: {
dateInput: 'DD MMM YYYY',
},
display: {
dateInput: 'DD MMM YYYY',
monthYearLabel: 'MMM YYYY',
dateA11yLabel: 'LL',
monthYearA11yLabel: 'MMMM YYYY'
}
};
@NgModule({
imports: [
CommonModule,
MatMomentDateModule,
MatSidenavModule, MatIconModule, MatToolbarModule, MatButtonModule,
MatListModule, MatGridListModule, MatCardModule, MatProgressBarModule, MatInputModule,
MatSnackBarModule, MatProgressSpinnerModule, MatDatepickerModule,
MatAutocompleteModule, MatTableModule, MatDialogModule, MatTabsModule,
MatTooltipModule, MatSelectModule, MatPaginatorModule, MatChipsModule,
MatButtonToggleModule, MatSlideToggleModule, MatBadgeModule, MatCheckboxModule,
MatExpansionModule, DragDropModule, MatSortModule,
GalleriaModule,ImageModule,SkeletonModule
],
exports: [
CommonModule,
MatSidenavModule, MatIconModule, MatToolbarModule, MatButtonModule,
MatListModule, MatGridListModule, MatCardModule, MatProgressBarModule, MatInputModule,
MatSnackBarModule, MatMenuModule, MatProgressSpinnerModule, MatDatepickerModule,
MatAutocompleteModule, MatTableModule, MatDialogModule, MatTabsModule,
MatTooltipModule, MatSelectModule, MatPaginatorModule, MatChipsModule,
MatButtonToggleModule, MatSlideToggleModule, MatBadgeModule, MatCheckboxModule,
MatExpansionModule, SelectCheckAllComponent, DragDropModule, MatSortModule,
GalleriaModule,ImageModule,SkeletonModule
],
providers: [
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
{ provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS },
{ provide: LOCALE_ID, useValue: 'en-gb' }
],
declarations: [SelectCheckAllComponent]
})
export class CustomMaterialModule {
static forRoot() {
return {
ngModule: CustomMaterialModule,
providers: [
]
};
}
}

View File

@ -0,0 +1,4 @@
app-select-check-all .mat-checkbox-layout,
app-select-check-all .mat-checkbox-label {
width:100% !important;
}

View File

@ -0,0 +1,4 @@
<mat-checkbox class="mat-option" [indeterminate]="isIndeterminate()" [checked]="isChecked()" (click)="$event.stopPropagation()"
(change)="toggleSelection($event)">
{{text}}
</mat-checkbox>

View File

@ -0,0 +1,35 @@
import { Component, Input, ViewEncapsulation } from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
import { MatCheckboxChange } from '@angular/material/checkbox';
@Component({
selector: 'app-select-check-all',
templateUrl: './select-check-all.component.html',
styleUrls: ['./select-check-all.component.css'],
encapsulation: ViewEncapsulation.None
})
export class SelectCheckAllComponent {
@Input()
model: UntypedFormControl = new UntypedFormControl;
@Input() values = [];
@Input() text = 'Select All';
constructor() { }
isChecked(): boolean {
return this.model.value && this.values.length
&& this.model.value.length === this.values.length;
}
isIndeterminate(): boolean {
return this.model.value && this.values.length && this.model.value.length
&& this.model.value.length < this.values.length;
}
toggleSelection(change: MatCheckboxChange): void {
if (change.checked) {
this.model.setValue(this.values);
} else {
this.model.setValue([]);
}
}
}

View File

@ -0,0 +1,3 @@
mat-icon {
color: rgb(200, 0, 0);
}

View File

@ -0,0 +1,32 @@
<div class="container" fxLayout="row" fxLayoutAlign="center none">
<div fxFlex="95%">
<mat-card>
<mat-card-content>
<h2>About</h2>
<p>
Built on top of <a rel="noreferrer noopener" aria-label="Angular (opens in a new tab)"
href="http://angular.io" target="_blank">Angular</a> &amp; <a rel="noreferrer noopener"
aria-label="Angular Material (opens in a new tab)" href="http://material.angular.io"
target="_blank">Angular Material</a>, angular-material-template provides a simple template that you can use for your next project.
</p>
<p>
Support the project by starring it on <a href="https://github.com/umutesen/angular-material-template"
target="_blank">
GitHub
</a>.
</p>
<p>
Made with <mat-icon>favorite</mat-icon> by <a href="https://onthecode.co.uk" target="_blank"
aria-label="onthecode (opens in a new tab)">onthecode</a>.
</p>
</mat-card-content>
</mat-card>
</div>
</div>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AboutPageComponent } from './about-page.component';
describe('AboutHomeComponent', () => {
let component: AboutPageComponent;
let fixture: ComponentFixture<AboutPageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AboutPageComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AboutPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,12 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-about-page',
templateUrl: './about-page.component.html',
styleUrls: ['./about-page.component.css']
})
export class AboutPageComponent {
constructor() { }
}

View File

@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LayoutComponent } from '../../shared/layout/layout.component';
import { AboutPageComponent } from './about-page/about-page.component';
const routes: Routes = [
{
path: '',
component: LayoutComponent,
children: [
{ path: '', component: AboutPageComponent },
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AboutRoutingModule { }

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AboutRoutingModule } from './about-routing.module';
import { AboutPageComponent } from './about-page/about-page.component';
import { SharedModule } from '../../shared/shared.module';
@NgModule({
declarations: [AboutPageComponent],
imports: [
CommonModule,
SharedModule,
AboutRoutingModule
]
})
export class AboutModule { }

View File

@ -0,0 +1,35 @@
<div class="container" fxLayout="row" fxLayoutAlign="center none">
<div fxFlex="95%">
<mat-card>
<mat-card-content>
<h2>My Profile</h2>
<div fxLayout="row" fxLayout.sm="column" fxLayout.xs="column">
<div fxFlex="30%" fxFlex.sm="95%" fxFlex.xs="95%">
<app-profile-details></app-profile-details>
</div>
<div fxFlex></div>
<div fxFlex="65%" fxFlex.sm="95%" fxFlex.xs="950%">
<mat-tab-group>
<mat-tab label="Change Password">
<app-change-password></app-change-password>
</mat-tab>
</mat-tab-group>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

View File

@ -0,0 +1,17 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
@Component({
selector: 'app-account-page',
templateUrl: './account-page.component.html',
styleUrls: ['./account-page.component.css']
})
export class AccountPageComponent implements OnInit {
constructor(private titleService: Title) { }
ngOnInit() {
this.titleService.setTitle('Jucundus - Account');
}
}

View File

@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LayoutComponent } from 'src/app/shared/layout/layout.component';
import { AccountPageComponent } from './account-page/account-page.component';
const routes: Routes = [
{
path: '',
component: LayoutComponent,
children: [
{ path: 'profile', component: AccountPageComponent },
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AccountRoutingModule { }

View File

@ -0,0 +1,19 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AccountRoutingModule } from './account-routing.module';
import { AccountPageComponent } from './account-page/account-page.component';
import { ChangePasswordComponent } from './change-password/change-password.component';
import { ProfileDetailsComponent } from './profile-details/profile-details.component';
import { SharedModule } from 'src/app/shared/shared.module';
@NgModule({
imports: [
CommonModule,
SharedModule,
AccountRoutingModule
],
declarations: [AccountPageComponent, ChangePasswordComponent, ProfileDetailsComponent],
exports: [AccountPageComponent]
})
export class AccountModule { }

View File

@ -0,0 +1,6 @@
.password-rules .mat-divider {
position: unset !important;
}
.container{
padding-top: 20px;
}

View File

@ -0,0 +1,70 @@
<form [formGroup]="form">
<p>Use the form below to change your password.</p>
<div fxLayout="row">
<div fxFlex="40%" fxFlex.md="60%" fxFlex.sm="50%" fxFlex.xs="100%">
<mat-form-field class="full-width">
<input matInput placeholder="Current Password" formControlName="currentPassword" [type]="hideCurrentPassword ? 'password' : 'text'"
autocomplete="current-password">
<mat-icon matSuffix (click)="hideCurrentPassword = !hideCurrentPassword">
{{hideCurrentPassword ? 'visibility' : 'visibility_off'}}
</mat-icon>
<mat-error *ngIf="form.controls['currentPassword'].hasError('required')">
Please enter a your current password
</mat-error>
</mat-form-field>
<mat-form-field class="full-width">
<input matInput placeholder="New Password" formControlName="newPassword" [type]="hideNewPassword ? 'password' : 'text'" autocomplete="new-password">
<mat-icon matSuffix (click)="hideNewPassword = !hideNewPassword">
{{hideNewPassword ? 'visibility' : 'visibility_off'}}
</mat-icon>
<mat-error *ngIf="form.controls['newPassword'].hasError('required')">
Please enter a new password
</mat-error>
</mat-form-field>
<mat-form-field class="full-width">
<input matInput placeholder="Confirm New Password" formControlName="newPasswordConfirm" [type]="hideNewPassword ? 'password' : 'text'"
autocomplete="new-password">
<mat-icon matSuffix (click)="hideNewPassword = !hideNewPassword">
{{hideNewPassword ? 'visibility' : 'visibility_off'}}
</mat-icon>
<mat-error *ngIf="form.controls['newPasswordConfirm'].hasError('required')">
Please confirm your new password
</mat-error>
</mat-form-field>
<button mat-raised-button color="primary" [disabled]="form.invalid || disableSubmit" (click)="changePassword()">Save</button>
</div>
</div>
</form>
<!-- <div class="password-rules" fxFlex="65%" fxFlex.sm="90%" fxFlex.xs="95%">
Password rules:
<mat-list>
<mat-list-item>
Must be at least 6 characters
</mat-list-item>
<mat-list-item>
Must contain at least one non alphanumeric character
</mat-list-item>
<mat-list-item>
Must contain at least one lowercase ('a'-'z')
</mat-list-item>
<mat-list-item>
Must contain at least one uppercase ('A'-'Z')
</mat-list-item>
</mat-list>
</div> -->

View File

@ -0,0 +1,75 @@
import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms';
import { Component, OnInit } from '@angular/core';
import { NGXLogger } from 'ngx-logger';
import { AuthenticationService } from 'src/app/core/services/auth.service';
import { NotificationService } from 'src/app/core/services/notification.service';
import { SpinnerService } from 'src/app/core/services/spinner.service';
@Component({
selector: 'app-change-password',
templateUrl: './change-password.component.html',
styleUrls: ['./change-password.component.css']
})
export class ChangePasswordComponent implements OnInit {
form!: UntypedFormGroup;
hideCurrentPassword: boolean;
hideNewPassword: boolean;
currentPassword!: string;
newPassword!: string;
newPasswordConfirm!: string;
disableSubmit!: boolean;
constructor(private authService: AuthenticationService,
private logger: NGXLogger,
private spinnerService: SpinnerService,
private notificationService: NotificationService) {
this.hideCurrentPassword = true;
this.hideNewPassword = true;
}
ngOnInit() {
this.form = new UntypedFormGroup({
currentPassword: new UntypedFormControl('', Validators.required),
newPassword: new UntypedFormControl('', Validators.required),
newPasswordConfirm: new UntypedFormControl('', Validators.required),
});
this.form.get('currentPassword')?.valueChanges
.subscribe(val => { this.currentPassword = val; });
this.form.get('newPassword')?.valueChanges
.subscribe(val => { this.newPassword = val; });
this.form.get('newPasswordConfirm')?.valueChanges
.subscribe(val => { this.newPasswordConfirm = val; });
this.spinnerService.visibility.subscribe((value) => {
this.disableSubmit = value;
});
}
changePassword() {
if (this.newPassword !== this.newPasswordConfirm) {
this.notificationService.openSnackBar('New passwords do not match.');
return;
}
const email = this.authService.getCurrentUser().email;
this.authService.changePassword(email, this.currentPassword, this.newPassword)
.subscribe(
data => {
this.logger.info(`User ${email} changed password.`);
this.form.reset();
this.notificationService.openSnackBar('Your password has been changed.');
},
error => {
this.notificationService.openSnackBar(error.error);
}
);
}
}

View File

@ -0,0 +1,3 @@
.profile-card {
text-align: center;
}

View File

@ -0,0 +1,15 @@
<div class="profile-card">
<img src="assets/images/user.png" [alt]="fullName">
<h2 class="title">
{{fullName}}
</h2>
<label>
{{alias}}
</label>
<label>
{{email}}
</label>
</div>

View File

@ -0,0 +1,22 @@
import { Component, OnInit } from '@angular/core';
import { AuthenticationService } from 'src/app/core/services/auth.service';
@Component({
selector: 'app-profile-details',
templateUrl: './profile-details.component.html',
styleUrls: ['./profile-details.component.css']
})
export class ProfileDetailsComponent implements OnInit {
fullName: string = "";
email: string = "";
alias: string = "";
constructor(private authService: AuthenticationService) { }
ngOnInit() {
this.fullName = this.authService.getCurrentUser().fullName;
this.email = this.authService.getCurrentUser().email;
}
}

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { PasswordResetRequestComponent } from './password-reset-request/password-reset-request.component';
import { PasswordResetComponent } from './password-reset/password-reset.component';
const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'password-reset-request', component: PasswordResetRequestComponent },
{ path: 'password-reset', component: PasswordResetComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AuthRoutingModule { }

View File

@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AuthRoutingModule } from './auth-routing.module';
import { LoginComponent } from './login/login.component';
import { PasswordResetRequestComponent } from './password-reset-request/password-reset-request.component';
import { PasswordResetComponent } from './password-reset/password-reset.component';
import { SharedModule } from 'src/app/shared/shared.module';
@NgModule({
imports: [
CommonModule,
SharedModule,
AuthRoutingModule
],
declarations: [LoginComponent, PasswordResetRequestComponent, PasswordResetComponent]
})
export class AuthModule { }

View File

@ -0,0 +1,47 @@
<div class="container login-container" fxLayout="row" fxLayoutAlign="center center">
<form [formGroup]="loginForm" fxFlex="30%" fxFlex.sm="50%" fxFlex.xs="90%">
<mat-card>
<mat-card-title>Jucundus</mat-card-title>
<mat-card-subtitle>Log in to your account</mat-card-subtitle>
<mat-card-content>
<mat-form-field class="full-width">
<input id="emailInput" matInput placeholder="Email" formControlName="email" autocomplete="email"
type="email">
<mat-error id="invalidEmailError" *ngIf="loginForm.controls['email'].hasError('email')">
Please enter a valid email address
</mat-error>
<mat-error id="requiredEmailError" *ngIf="loginForm.controls['email'].hasError('required')">
Email is
<strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="full-width">
<input id="passwordInput" matInput placeholder="Password" formControlName="password" type="password"
autocomplete="current-password">
<mat-error id="requiredPasswordError" *ngIf="loginForm.controls['email'].hasError('required')">
Password is
<strong>required</strong>
</mat-error>
</mat-form-field>
<div class="full-width">
<mat-slide-toggle formControlName="rememberMe">Remember my email address</mat-slide-toggle>
</div>
</mat-card-content>
<mat-card-actions class="login-actions">
<button mat-raised-button id="login" color="primary" [disabled]="loginForm.invalid || loading"
(click)="login()">Login</button>
<button mat-button id="resetPassword" (click)="resetPassword()" type="button">Reset Password</button>
</mat-card-actions>
</mat-card>
<mat-progress-bar *ngIf="loading" mode="indeterminate"></mat-progress-bar>
</form>
</div>

View File

@ -0,0 +1,67 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { UntypedFormControl, Validators, UntypedFormGroup } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { AuthenticationService } from 'src/app/core/services/auth.service';
import { NotificationService } from 'src/app/core/services/notification.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
loginForm!: UntypedFormGroup;
loading!: boolean;
constructor(private router: Router,
private titleService: Title,
private notificationService: NotificationService,
private authenticationService: AuthenticationService) {
}
ngOnInit() {
this.titleService.setTitle('Jucundus - Login');
this.authenticationService.logout();
this.createForm();
}
private createForm() {
const savedUserEmail = localStorage.getItem('savedUserEmail');
this.loginForm = new UntypedFormGroup({
email: new UntypedFormControl(savedUserEmail, [Validators.required, Validators.email]),
password: new UntypedFormControl('', Validators.required),
rememberMe: new UntypedFormControl(savedUserEmail !== null)
});
}
login() {
const email = this.loginForm.get('email')?.value;
const password = this.loginForm.get('password')?.value;
const rememberMe = this.loginForm.get('rememberMe')?.value;
this.loading = true;
this.authenticationService
.login(email.toLowerCase(), password)
.subscribe(
data => {
if (rememberMe) {
localStorage.setItem('savedUserEmail', email);
} else {
localStorage.removeItem('savedUserEmail');
}
this.router.navigate(['/']);
},
error => {
this.notificationService.openSnackBar(error.error);
this.loading = false;
}
);
}
resetPassword() {
this.router.navigate(['/auth/password-reset-request']);
}
}

View File

@ -0,0 +1,32 @@
<div class="container login-container" fxLayout="row" fxLayoutAlign="center center">
<form [formGroup]="form" fxFlex="30%" fxFlex.sm="50%" fxFlex.xs="90%">
<mat-card>
<mat-card-title>Jucundus</mat-card-title>
<mat-card-subtitle>Reset your password</mat-card-subtitle>
<mat-card-content>
<mat-form-field class="full-width">
<input id="emailInput" matInput placeholder="Email" formControlName="email" autocomplete="email" type="email">
<mat-error id="invalidEmailError" *ngIf="form.controls['email'].hasError('email')">
Please enter a valid email address
</mat-error>
<mat-error id="requiredEmailError" *ngIf="form.controls['email'].hasError('required')">
Email is
<strong>required</strong>
</mat-error>
</mat-form-field>
</mat-card-content>
<mat-card-actions class="login-actions">
<button id="submit" mat-raised-button color="primary" [disabled]="form.invalid || loading"
(click)="resetPassword()">Reset Password</button>
<button id="cancel" mat-button (click)="cancel()">Cancel</button>
</mat-card-actions>
</mat-card>
<mat-progress-bar *ngIf="loading" mode="indeterminate"></mat-progress-bar>
</form>
</div>

View File

@ -0,0 +1,54 @@
import { Router } from '@angular/router';
import { Component, OnInit } from '@angular/core';
import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { NotificationService } from 'src/app/core/services/notification.service';
import { AuthenticationService } from 'src/app/core/services/auth.service';
@Component({
selector: 'app-password-reset-request',
templateUrl: './password-reset-request.component.html',
styleUrls: ['./password-reset-request.component.css']
})
export class PasswordResetRequestComponent implements OnInit {
private email!: string;
form!: UntypedFormGroup;
loading!: boolean;
constructor(private authService: AuthenticationService,
private notificationService: NotificationService,
private titleService: Title,
private router: Router) { }
ngOnInit() {
this.titleService.setTitle('Jucundus - Password Reset Request');
this.form = new UntypedFormGroup({
email: new UntypedFormControl('', [Validators.required, Validators.email])
});
this.form.get('email')?.valueChanges
.subscribe((val: string) => { this.email = val.toLowerCase(); });
}
resetPassword() {
this.loading = true;
this.authService.passwordResetRequest(this.email)
.subscribe(
results => {
this.router.navigate(['/auth/login']);
this.notificationService.openSnackBar('Password verification mail has been sent to your email address.');
},
error => {
this.loading = false;
this.notificationService.openSnackBar(error.error);
}
);
}
cancel() {
this.router.navigate(['/']);
}
}

View File

@ -0,0 +1,45 @@
<div class="container login-container" fxLayout="row" fxLayoutAlign="center center">
<form [formGroup]="form" fxFlex="30%" fxFlex.sm="50%" fxFlex.xs="90%">
<mat-card>
<mat-card-title>Jucundus</mat-card-title>
<mat-card-subtitle>Reset your password</mat-card-subtitle>
<mat-card-content>
<mat-form-field class="full-width">
<input id="emailInput" matInput readonly disabled [value]="email">
</mat-form-field>
<mat-form-field class="full-width">
<input id="passwordInput" matInput placeholder="New Password" formControlName="newPassword" [type]="hideNewPassword ? 'password' : 'text'" autocomplete="new-password">
<mat-icon id="togglePasswordVisibility" matSuffix (click)="hideNewPassword = !hideNewPassword">
{{hideNewPassword ? 'visibility' : 'visibility_off'}}
</mat-icon>
<mat-error *ngIf="form.controls['newPassword'].hasError('required')">
Please enter a new password
</mat-error>
</mat-form-field>
<mat-form-field class="full-width">
<input id="passwordConfirmInput" matInput placeholder="New Password Confirmation" formControlName="newPasswordConfirm" [type]="hideNewPasswordConfirm ? 'password' : 'text'" autocomplete="new-password">
<mat-icon id="togglePasswordConfirmVisibility" matSuffix (click)="hideNewPasswordConfirm = !hideNewPasswordConfirm">
{{hideNewPasswordConfirm ? 'visibility' : 'visibility_off'}}
</mat-icon>
<mat-error *ngIf="form.controls['newPasswordConfirm'].hasError('required')">
Please enter a your current password
</mat-error>
</mat-form-field>
</mat-card-content>
<mat-card-actions class="login-actions">
<button id="submit" mat-raised-button color="primary" [disabled]="form.invalid || loading" (click)="resetPassword()">OK</button>
<button id="cancel" mat-button (click)="cancel()">Back to Login</button>
</mat-card-actions>
</mat-card>
<mat-progress-bar *ngIf="loading" mode="indeterminate"></mat-progress-bar>
</form>
</div>

View File

@ -0,0 +1,77 @@
import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms';
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { AuthenticationService } from 'src/app/core/services/auth.service';
import { NotificationService } from 'src/app/core/services/notification.service';
@Component({
selector: 'app-password-reset',
templateUrl: './password-reset.component.html',
styleUrls: ['./password-reset.component.css']
})
export class PasswordResetComponent implements OnInit {
private token!: string;
email!: string;
form!: UntypedFormGroup;
loading!: boolean;
hideNewPassword: boolean;
hideNewPasswordConfirm: boolean;
constructor(private activeRoute: ActivatedRoute,
private router: Router,
private authService: AuthenticationService,
private notificationService: NotificationService,
private titleService: Title) {
this.titleService.setTitle('Jucundus - Password Reset');
this.hideNewPassword = true;
this.hideNewPasswordConfirm = true;
}
ngOnInit() {
this.activeRoute.queryParamMap.subscribe((params: ParamMap) => {
this.token = params.get('token') + '';
this.email = params.get('email') + '';
if (!this.token || !this.email) {
this.router.navigate(['/']);
}
});
this.form = new UntypedFormGroup({
newPassword: new UntypedFormControl('', Validators.required),
newPasswordConfirm: new UntypedFormControl('', Validators.required)
});
}
resetPassword() {
const password = this.form.get('newPassword')?.value;
const passwordConfirm = this.form.get('newPasswordConfirm')?.value;
if (password !== passwordConfirm) {
this.notificationService.openSnackBar('Passwords do not match');
return;
}
this.loading = true;
this.authService.passwordReset(this.email, this.token, password, passwordConfirm)
.subscribe(
() => {
this.notificationService.openSnackBar('Your password has been changed.');
this.router.navigate(['/auth/login']);
},
(error: any) => {
this.notificationService.openSnackBar(error.error);
this.loading = false;
}
);
}
cancel() {
this.router.navigate(['/']);
}
}

View File

@ -0,0 +1,7 @@
table {
width: 100%;
}
th.mat-sort-header-sorted {
color: black;
}

View File

@ -0,0 +1,42 @@
<div class="container" fxLayout="row" fxLayoutAlign="center none">
<div fxFlex="95%">
<mat-card>
<mat-card-content>
<h2>Customers</h2>
<table mat-table [dataSource]="dataSource" matSort>
<!-- Position Column -->
<ng-container matColumnDef="position">
<th mat-header-cell *matHeaderCellDef mat-sort-header> No. </th>
<td mat-cell *matCellDef="let element"> {{element.position}} </td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th>
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
</ng-container>
<!-- Weight Column -->
<ng-container matColumnDef="weight">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Weight </th>
<td mat-cell *matCellDef="let element"> {{element.weight}} </td>
</ng-container>
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Symbol </th>
<td mat-cell *matCellDef="let element"> {{element.symbol}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</mat-card-content>
</mat-card>
</div>
</div>

View File

@ -0,0 +1,53 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { NGXLogger } from 'ngx-logger';
import { Title } from '@angular/platform-browser';
import { NotificationService } from 'src/app/core/services/notification.service';
export interface PeriodicElement {
name: string;
position: number;
weight: number;
symbol: string;
}
const ELEMENT_DATA: PeriodicElement[] = [
{ position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H' },
{ position: 2, name: 'Helium', weight: 4.0026, symbol: 'He' },
{ position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li' },
{ position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be' },
{ position: 5, name: 'Boron', weight: 10.811, symbol: 'B' },
{ position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C' },
{ position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N' },
{ position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O' },
{ position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F' },
{ position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne' },
];
@Component({
selector: 'app-customer-list',
templateUrl: './customer-list.component.html',
styleUrls: ['./customer-list.component.css']
})
export class CustomerListComponent implements OnInit {
displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
dataSource = new MatTableDataSource(ELEMENT_DATA);
@ViewChild(MatSort, { static: true })
sort: MatSort = new MatSort;
constructor(
private logger: NGXLogger,
private notificationService: NotificationService,
private titleService: Title
) { }
ngOnInit() {
this.titleService.setTitle('Jucundus - Customers');
this.logger.log('Customers loaded');
this.notificationService.openSnackBar('Customers loaded');
this.dataSource.sort = this.sort;
}
}

View File

@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LayoutComponent } from 'src/app/shared/layout/layout.component';
import { CustomerListComponent } from './customer-list/customer-list.component';
const routes: Routes = [
{
path: '',
component: LayoutComponent,
children: [
{ path: '', component: CustomerListComponent },
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class CustomersRoutingModule { }

View File

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CustomersRoutingModule } from './customers-routing.module';
import { SharedModule } from 'src/app/shared/shared.module';
import { CustomerListComponent } from './customer-list/customer-list.component';
@NgModule({
imports: [
CommonModule,
CustomersRoutingModule,
SharedModule
],
declarations: [
CustomerListComponent
]
})
export class CustomersModule { }

View File

@ -0,0 +1,17 @@
.single-cards {
margin: 20px 0;
}
.single-card .mat-card-avatar {
width: 50px;
height: 50px;
}
.single-card .mat-icon {
font-size: 55px;
}
.projects-card>mat-card-content {
max-height: 400px;
overflow: auto;
}

View File

@ -0,0 +1,18 @@
<div class="container" fxLayout="row" fxLayoutAlign="center none">
<div fxFlex="95%">
<div class="container" fxLayout="row" fxLayoutAlign="center none">
<h2>Welcome back, {{currentUser.fullName}}!</h2>
</div>
<div class="container" fxLayout="row" fxLayoutAlign="center none">
<div fxFlex="50%" class="text-center no-records animate">
<mat-icon>dashboard</mat-icon>
<p>This is the dashboard.</p>
</div>
<mat-icon> </mat-icon>
</div>
</div>
</div>

View File

@ -0,0 +1,30 @@
import { Component, OnInit } from '@angular/core';
import { NotificationService } from 'src/app/core/services/notification.service';
import { Title } from '@angular/platform-browser';
import { NGXLogger } from 'ngx-logger';
import { AuthenticationService } from 'src/app/core/services/auth.service';
@Component({
selector: 'app-dashboard-home',
templateUrl: './dashboard-home.component.html',
styleUrls: ['./dashboard-home.component.css']
})
export class DashboardHomeComponent implements OnInit {
currentUser: any;
constructor(private notificationService: NotificationService,
private authService: AuthenticationService,
private titleService: Title,
private logger: NGXLogger) {
}
ngOnInit() {
this.currentUser = this.authService.getCurrentUser();
this.titleService.setTitle('Jucundus - Dashboard');
this.logger.log('Dashboard loaded');
setTimeout(() => {
this.notificationService.openSnackBar('Welcome!');
});
}
}

View File

@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LayoutComponent } from 'src/app/shared/layout/layout.component';
import { DashboardHomeComponent } from './dashboard-home/dashboard-home.component';
const routes: Routes = [
{
path: '',
component: LayoutComponent,
children: [
{ path: '', component: DashboardHomeComponent },
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DashboardRoutingModule { }

View File

@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DashboardRoutingModule } from './dashboard-routing.module';
import { DashboardHomeComponent } from './dashboard-home/dashboard-home.component';
import { SharedModule } from 'src/app/shared/shared.module';
@NgModule({
declarations: [DashboardHomeComponent],
imports: [
CommonModule,
DashboardRoutingModule,
SharedModule
]
})
export class DashboardModule { }

View File

@ -0,0 +1,3 @@
mat-icon {
color: rgb(200, 0, 0);
}

View File

@ -0,0 +1,64 @@
<div class="container" fxLayout="row" fxLayoutAlign="center none">
<div fxFlex="95%">
<mat-card>
<mat-card-header>
<mat-card-title>New Lot</mat-card-title>
</mat-card-header>
<mat-card-content>
<div fxLayout="row" fxLayoutGap="5px">
<mat-form-field>
<mat-label>Url</mat-label>
<input matInput placeholder="Ex. https://drouot.com/..." maxlength="255" st [(ngModel)]="url" >
</mat-form-field>
<button mat-raised-button color="primary" (click)="openDialog()">Add</button>
</div>
</mat-card-content>
</mat-card>
<mat-card style="margin-top: 10px;">
<mat-card-header>
<mat-card-title>Favorites</mat-card-title>
</mat-card-header>
<mat-card-content>
<div fxLayout="row" fxLayoutGap="5px">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<!--- Note that these columns can be defined in any order.
The actual rendered columns are set as a property on the row definition" -->
<!-- Position Column -->
<ng-container matColumnDef="picture">
<th mat-header-cell *matHeaderCellDef> Picture </th>
<td mat-cell *matCellDef="let element"><img [src]="element.picture" alt="Picture" style="width: 50px"></td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="lot">
<th mat-header-cell *matHeaderCellDef> Lot </th>
<td mat-cell *matCellDef="let element"> {{element.lotInfo.lotNumber}} </td>
</ng-container>
<!-- Weight Column -->
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef> Title </th>
<td mat-cell *matCellDef="let element"> {{element.lotInfo.title}} </td>
</ng-container>
<!-- Symbol Column -->
<ng-container matColumnDef="estimate">
<th mat-header-cell *matHeaderCellDef> Estimate </th>
<td mat-cell *matCellDef="let element"> {{element.lotInfo.EstimateLow}} - {{element.lotInfo.EstimateHigh}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</mat-card-content>
</mat-card>
</div>
</div>

View File

@ -0,0 +1,34 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { apiService } from 'src/app/core/services/api.service';
@Component({
selector: 'app-favorites-page',
templateUrl: './favorites-page.component.html',
styleUrls: ['./favorites-page.component.css']
})
export class FavoritesPageComponent implements OnInit {
url: string = '';
displayedColumns: string[] = ['picture', 'lot', 'title', 'estimate'];
dataSource = []
constructor(
private router: Router,
private apiService: apiService) {}
openDialog(): void {
this.router.navigate(['favorites/new', this.url]);
}
ngOnInit(): void {
this.apiService.getAllFavorite().subscribe((data: any) => {
this.dataSource = data;
});
}
}

View File

@ -0,0 +1,4 @@
.example-card {
margin-bottom: 8px;
}

View File

@ -0,0 +1,16 @@
<mat-card>
<mat-card-header>
<mat-card-title>Picture</mat-card-title>
<mat-card-subtitle>select the picture</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<mat-grid-list cols="2" >
<mat-grid-tile *ngFor="let picture of images">
<div style="width: 300px; height: 300px;">
<img src="{{picture}}" (click)="onSelectImage(picture)" style="width: 100%; height: 100%; object-fit: contain;">
</div>
</mat-grid-tile>
</mat-grid-list>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,33 @@
import { Component, OnInit, Inject } from '@angular/core';
import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
@Component({
selector: 'change-image-dialog-dialog',
templateUrl: './change-image-dialog.component.html',
styleUrls: ['./change-image-dialog.component.css']
})
export class ChangeImageDialogComponent implements OnInit {
images: any[] = [];
constructor(
public dialogRef: MatDialogRef<ChangeImageDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
}
ngOnInit(): void {
this.images = this.data.images;
console.log(this.images);
}
onSelectImage(picture: any): void {
this.dialogRef.close(picture);
}
onNoClick(): void {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,3 @@
mat-icon {
color: rgb(200, 0, 0);
}

Some files were not shown because too many files have changed in this diff Show More