first commit

This commit is contained in:
2024-05-16 16:05:41 +02:00
commit adf8283ae0
197 changed files with 26666 additions and 0 deletions
BIN
View File
Binary file not shown.
+50
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
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
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
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
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
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
View File
File diff suppressed because it is too large Load Diff
+57
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
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+75
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 { }
+7
View File
@@ -0,0 +1,7 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `<router-outlet></router-outlet>`
})
export class AppComponent {}
+32
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 { }
+49
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');
}
}
@@ -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(['/']);
});
});
+22
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;
}
}
}
@@ -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);
});
});
+32
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;
}
}
@@ -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.`);
}
}
@@ -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);
}
}
}
@@ -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();
})
);
}
}
@@ -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');
}
}
@@ -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));
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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;
};
}
@@ -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;
}
@@ -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;
}
@@ -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
});
}
}
@@ -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);
});
});
@@ -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);
}
}
@@ -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: [
]
};
}
}
@@ -0,0 +1,4 @@
app-select-check-all .mat-checkbox-layout,
app-select-check-all .mat-checkbox-label {
width:100% !important;
}
@@ -0,0 +1,4 @@
<mat-checkbox class="mat-option" [indeterminate]="isIndeterminate()" [checked]="isChecked()" (click)="$event.stopPropagation()"
(change)="toggleSelection($event)">
{{text}}
</mat-checkbox>
@@ -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([]);
}
}
}
@@ -0,0 +1,3 @@
mat-icon {
color: rgb(200, 0, 0);
}
@@ -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>
@@ -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();
});
});
@@ -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() { }
}
@@ -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 { }
@@ -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 { }
@@ -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>
@@ -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');
}
}
@@ -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 { }
@@ -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 { }
@@ -0,0 +1,6 @@
.password-rules .mat-divider {
position: unset !important;
}
.container{
padding-top: 20px;
}
@@ -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> -->
@@ -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);
}
);
}
}
@@ -0,0 +1,3 @@
.profile-card {
text-align: center;
}
@@ -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>
@@ -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;
}
}
@@ -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 { }
@@ -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 { }
@@ -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>
@@ -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']);
}
}
@@ -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>
@@ -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(['/']);
}
}
@@ -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>
@@ -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(['/']);
}
}
@@ -0,0 +1,7 @@
table {
width: 100%;
}
th.mat-sort-header-sorted {
color: black;
}
@@ -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>
@@ -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;
}
}
@@ -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 { }
@@ -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 { }
@@ -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;
}
@@ -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>
@@ -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!');
});
}
}
@@ -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 { }
@@ -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 { }
@@ -0,0 +1,3 @@
mat-icon {
color: rgb(200, 0, 0);
}
@@ -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>
@@ -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;
});
}
}
@@ -0,0 +1,4 @@
.example-card {
margin-bottom: 8px;
}
@@ -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>
@@ -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();
}
}
@@ -0,0 +1,3 @@
mat-icon {
color: rgb(200, 0, 0);
}
@@ -0,0 +1,121 @@
<mat-card class="dialog-card">
<mat-card-header>
<mat-card-title>Lot</mat-card-title>
<mat-card-subtitle>Lot information</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div fxLayout="row" fxLayoutGap="5px">
<div fxLayout="column" fxLayoutGap="10px" fxFlex>
<mat-form-field style="width: 100%">
<mat-label>Title</mat-label><input matInput maxlength="255" [(ngModel)]="lotInfo.title"/>
</mat-form-field>
<mat-form-field style="width: 40%">
<mat-label>Lot</mat-label><input matInput maxlength="30" [(ngModel)]="lotInfo.lotNumber"/>
</mat-form-field>
</div>
<div fxLayout="column" fxLayoutAlign="center center" fxFlex>
<div *ngIf="images.length == 0">
<p-skeleton width="150px" height="150px"></p-skeleton>
</div>
<div *ngIf="images.length > 0">
<img [src]="picture" style="width: 150px; height: 150px;" (click)="openChangeImage()"/>
</div>
</div>
</div>
<div fxLayout="row" fxLayoutGap="5px">
<mat-form-field style="width: 40%">
<mat-label>Fees</mat-label><input matInput maxlength="2" [(ngModel)]="lotInfo.fees"/>
<mat-icon matSuffix>percent</mat-icon>
</mat-form-field>
<p style="width: 100%">{{lotInfo.feesText}}</p>
</div>
<div fxLayout="row" fxLayoutGap="5px">
<mat-form-field style="width: 50%">
<mat-label>Estimate Low</mat-label><input matInput maxlength="30" [(ngModel)]="lotInfo.EstimateLow"/>
</mat-form-field>
<mat-form-field style="width: 50%">
<mat-label >Estimate High</mat-label><input matInput maxlength="30" [(ngModel)]="lotInfo.EstimateHigh"/>
</mat-form-field>
</div>
<div fxLayout="row">
<mat-form-field style="width: 100%">
<mat-label >Description</mat-label><textarea matInput style="height: 200px;" [(ngModel)]="lotInfo.Description"></textarea>
</mat-form-field>
</div>
</mat-card-content>
</mat-card>
<mat-card class="dialog-card">
<mat-card-header>
<mat-card-title>Sale</mat-card-title>
<mat-card-subtitle>Sale information</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div fxLayout="row" fxLayoutGap="5px">
<mat-form-field style="width: 100%">
<mat-label>Title</mat-label><input matInput maxlength="255" [(ngModel)]="SaleInfo.title"/>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="5px">
<div fxLayout="column" fxFlex="60">
<mat-form-field appearance="fill">
<mat-label>Choose a date</mat-label>
<input matInput [matDatepicker]="picker" [(ngModel)]="date">
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
</div>
<div fxLayout="column" fxFlex="40">
<mat-form-field >
<mat-label>Hour</mat-label><input matInput type="time" maxlength="5" [(ngModel)]="hour"/>
</mat-form-field>
</div>
</div>
<div fxLayout="row" fxLayoutGap="5px">
<mat-form-field style="width: 50%">
<mat-label>location</mat-label><input matInput maxlength="255" [(ngModel)]="SaleInfo.location"/>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="5px">
<mat-form-field style="width: 100%">
<mat-label>Sale House</mat-label><input matInput maxlength="255" [(ngModel)]="SaleInfo.saleHouseName"/>
</mat-form-field>
</div>
</mat-card-content>
</mat-card>
<mat-card class="dialog-card">
<mat-card-header>
<mat-card-title>Perso</mat-card-title>
</mat-card-header>
<mat-card-content>
<div fxLayout="row" fxLayoutGap="5px">
<mat-checkbox [(ngModel)]="buyProject">Buy Project</mat-checkbox>
</div>
<div fxLayout="row" fxLayoutGap="5px">
<mat-form-field style="width: 30%">
<mat-label>Max price</mat-label><input matInput maxlength="255" type="number" [(ngModel)]="maxPrice"/>
</mat-form-field>
</div>
<div fxLayout="row">
<mat-form-field style="width: 100%">
<mat-label >Note</mat-label><textarea matInput style="height: 100px;" [(ngModel)]="Note"></textarea>
</mat-form-field>
</div>
</mat-card-content>
</mat-card>
<mat-card>
<mat-card-content>
<div fxLayout="row" fxLayoutGap="5px">
<button mat-button color="warn" (click)="cancel()">Cancel</button>
<button mat-raised-button color="primary" (click)="save()">Save</button>
</div>
</mat-card-content>
</mat-card>
@@ -0,0 +1,138 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router, ActivatedRoute } from '@angular/router';
import {ChangeImageDialogComponent} from './change-image-dialog/change-image-dialog.component';
import * as moment from 'moment-timezone';
//Services
import { NotificationService } from 'src/app/core/services/notification.service';
import { apiService } from 'src/app/core/services/api.service';
// Models
import { LotInfo } from 'src/app/core/services/model/lotInfo.interface';
import { SaleInfo } from 'src/app/core/services/model/saleInfo.interface';
@Component({
selector: 'app-new-favorites-page',
templateUrl: './new-favorite-page.component.html',
styleUrls: ['./new-favorite-page.component.css']
})
export class NewFavoritesPageComponent implements OnInit {
url: string = '';
lotInfo: LotInfo;
SaleInfo: SaleInfo;
images: any[] = [];
picture: String = "";
date: Date;
hour: string = "";
buyProject: boolean = false;
maxPrice: number = 0;
Note: string = "";
constructor(
public dialog: MatDialog,
private ActivatedRoute: ActivatedRoute,
private router: Router,
private notificationService: NotificationService,
private apiService: apiService,) {
this.ActivatedRoute.params.subscribe(params => {
this.url = params['url'];
});
this.lotInfo = {
idLotInterencheres: '',
url: '',
title:'',
lotNumber: '',
EstimateLow: 0,
EstimateHigh: 0,
Description: "",
feesText: "",
fees: "",
saleInfo: {
idSaleInterencheres: "",
url: ""
}
};
this.SaleInfo = {
_id: "",
idPlatform: "",
platform: "",
url: "",
title: "",
date: "",
location: "",
saleHouseName: "",
status: ""
};
this.date = new Date();
}
ngOnInit(): void {
console.log("url: "+this.url);
// this.date = moment(this.SaleInfo.date).tz('Europe/Paris').toDate();
// this.hour = moment(this.SaleInfo.date).tz('Europe/Paris').format('HH:mm');
this.apiService.getLotInfo(this.url).subscribe( lotInfo => {
console.log(lotInfo);
this.lotInfo = lotInfo;
this.apiService.getSaleInfos(this.lotInfo.saleInfo.url).subscribe( SaleInfo => {
console.log(SaleInfo);
this.SaleInfo = SaleInfo;
//hour
// Europe/Paris is the timezone of the user
this.date = moment(this.SaleInfo.date).tz('Europe/Paris').toDate();
this.hour = moment(this.SaleInfo.date).tz('Europe/Paris').format('HH:mm');
this.notificationService.openSnackBar("Loaded: "+lotInfo.title);
}
);
}
);
this.apiService.getPictures(this.url).subscribe( pictures => {
this.images = pictures;
this.picture = pictures[0];
});
}
openChangeImage(): void {
const dialogRef = this.dialog.open(ChangeImageDialogComponent, {
width: '300px',
data: {images: this.images}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.picture = result;
}
});
}
cancel(): void {
this.router.navigate(['favorites']);
}
save(): void {
// Europe/Paris is the timezone of the user
let dateTime = moment.tz(`${this.date.toISOString().split('T')[0]}T${this.hour}`, 'Europe/Paris').format();
this.apiService.saveFavorite(this.lotInfo, this.SaleInfo, this.picture, dateTime, this.buyProject, this.maxPrice, this.Note).subscribe( res => {
this.notificationService.openSnackBar("Favorite saved");
this.router.navigate(['favorites']);
});
}
}
@@ -0,0 +1,23 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LayoutComponent } from '../../shared/layout/layout.component';
import { FavoritesPageComponent } from './favorites-page/favorites-page.component';
import { NewFavoritesPageComponent } from './favorites-page/new-favorite-page/new-favorite-page.component'
const routes: Routes = [
{
path: '',
component: LayoutComponent,
children: [
{ path: '', component: FavoritesPageComponent },
{ path: 'new/:url', component: NewFavoritesPageComponent}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class FavoritesRoutingModule { }
@@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FavoritesRoutingModule } from './favorites-routing.module';
import { FavoritesPageComponent } from './favorites-page/favorites-page.component';
import { NewFavoritesPageComponent } from './favorites-page/new-favorite-page/new-favorite-page.component';
import { ChangeImageDialogComponent } from './favorites-page/new-favorite-page/change-image-dialog/change-image-dialog.component';
import { SharedModule } from '../../shared/shared.module';
@NgModule({
declarations: [
FavoritesPageComponent,
NewFavoritesPageComponent,
ChangeImageDialogComponent
],
imports: [
CommonModule,
SharedModule,
FavoritesRoutingModule,
]
})
export class FavoritesModule { }
@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LayoutComponent } from 'src/app/shared/layout/layout.component';
import { IconsComponent } from './icons/icons.component';
const routes: Routes = [
{
path: '',
component: LayoutComponent,
children: [
{ path: '', component: IconsComponent },
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class IconsRoutingModule { }
@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IconsRoutingModule } from './icons-routing.module';
import { IconsComponent } from './icons/icons.component';
import { SharedModule } from 'src/app/shared/shared.module';
@NgModule({
declarations: [IconsComponent],
imports: [
CommonModule,
SharedModule,
IconsRoutingModule
]
})
export class IconsModule { }
@@ -0,0 +1,4 @@
iframe {
width: 100%;
height: 650px;
}
@@ -0,0 +1,16 @@
<div class="container" fxLayout="row" fxLayoutAlign="center none">
<div fxFlex="95%">
<mat-card>
<mat-card-content>
<h2>Icons</h2>
<iframe src="https://design.google.com/icons/">
<p>Your browser does not support iframes.</p>
</iframe>
</mat-card-content>
</mat-card>
</div>
</div>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { IconsComponent } from './icons.component';
describe('IconsComponent', () => {
let component: IconsComponent;
let fixture: ComponentFixture<IconsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ IconsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(IconsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-icons',
templateUrl: './icons.component.html',
styleUrls: ['./icons.component.css']
})
export class IconsComponent {
constructor() { }
}
@@ -0,0 +1,3 @@
mat-icon {
color: rgb(200, 0, 0);
}
@@ -0,0 +1,33 @@
<div class="container" fxLayout="row" fxLayoutAlign="center none">
<div fxFlex="95%">
<mat-card style="margin-bottom: 20px;">
<mat-card-content>
<h2>Pictures</h2>
<mat-form-field>
<mat-label>Url</mat-label>
<input matInput placeholder="Ex. https://drouot.com/..." maxlength="255" [(ngModel)]="url" >
</mat-form-field>
<button mat-raised-button color="primary" (click)="getPictures()">Go</button>
</mat-card-content>
</mat-card>
<mat-card *ngIf="images.length > 0">
<mat-card-content>
<div fxLayout="row" fxLayoutAlign="center none">
<div fxFlex="95%">
<p-galleria [value]="images" [responsiveOptions]="responsiveOptions" [containerStyle]="{ 'max-width': '640px' }" [numVisible]="5" [thumbnailsPosition]="position" >
<ng-template pTemplate="item" let-item>
<p-image [src]="item.itemImageSrc" width="600" [preview]="true"></p-image>
</ng-template>
<ng-template pTemplate="thumbnail" let-item>
<div class="grid grid-nogutter justify-content-center">
<img [src]="item.thumbnailImageSrc" style="height: 100px;" />
</div>
</ng-template>
</p-galleria>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FavoritesPageComponent } from './favorites-page.component';
describe('AboutHomeComponent', () => {
let component: FavoritesPageComponent;
let fixture: ComponentFixture<FavoritesPageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ FavoritesPageComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FavoritesPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,81 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { apiService } from 'src/app/core/services/api.service';
@Component({
selector: 'app-pictures-page',
templateUrl: './pictures-page.component.html',
styleUrls: ['./pictures-page.component.css']
})
export class PicturesPageComponent implements OnInit {
@Input() value: any;
@Output() valueChange = new EventEmitter<any>();
url: string = '';
images: any[] = [];
position: "bottom" | "top" | "left" | "right" | undefined = 'top';
responsiveOptions: any[] | undefined;
constructor(
private titleService: Title,
private apiService: apiService,
) {
}
ngOnInit() {
this.titleService.setTitle('Jucundus - Users');
this.responsiveOptions = [
{
breakpoint: '1024px',
numVisible: 5
},
{
breakpoint: '768px',
numVisible: 3
},
{
breakpoint: '560px',
numVisible: 1
}
];
}
getPictures(): void {
this.apiService.getPictures(this.url).subscribe( Pictures => {
this.images = [];
const newImages = [...this.images];
Pictures.forEach((picture: any) => {
newImages.push({
itemImageSrc: picture,
thumbnailImageSrc: picture,
alt: "img",
title: "img1"
});
})
this.images = newImages;
console.log(this.images);
this.updateValue(this.images);
})
}
updateValue(newValue: any) {
this.value = newValue;
this.valueChange.emit(this.value);
}
openFullscreen(event: any) {
const element = event.target;
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) { /* Firefox */
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) { /* Chrome, Safari and Opera */
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) { /* IE/Edge */
element.msRequestFullscreen();
}
}
}
@@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LayoutComponent } from '../../shared/layout/layout.component';
import { PicturesPageComponent } from './pictures-page/pictures-page.component';
const routes: Routes = [
{
path: '',
component: LayoutComponent,
children: [
{ path: '', component: PicturesPageComponent },
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class PicturesRoutingModule { }
@@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PicturesRoutingModule } from './pictures-routing.module';
import { PicturesPageComponent } from './pictures-page/pictures-page.component';
import { SharedModule } from '../../shared/shared.module';
@NgModule({
declarations: [PicturesPageComponent],
imports: [
CommonModule,
SharedModule,
PicturesRoutingModule,
]
})
export class PicturesModule { }
@@ -0,0 +1,4 @@
.example-card {
margin-bottom: 8px;
}
@@ -0,0 +1,31 @@
<mat-card>
<mat-card-header>
<mat-card-title>Sale</mat-card-title>
<mat-card-subtitle>Sale informations</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div *ngIf="SaleInfo.title == ''" fxLayout="column" fxLayoutGap="5px">
<p-skeleton width="250px" height="20px"></p-skeleton>
<p-skeleton width="150px" height="20px"></p-skeleton>
<p-skeleton width="200px" height="20px"></p-skeleton>
<p-skeleton width="150px" height="20px"></p-skeleton>
</div>
<div *ngIf="SaleInfo.title != ''" fxLayout="column" fxLayoutGap="5px">
<p><strong>{{SaleInfo.title}}</strong></p>
<p><mat-icon>event</mat-icon>{{date | date:'dd/MM/yyyy' }} - {{hour}}</p>
<p><mat-icon>account_balance</mat-icon>{{SaleInfo.saleHouseName}}</p>
<p><mat-icon>location_on</mat-icon>{{SaleInfo.location}}</p>
</div>
</mat-card-content>
</mat-card>
<mat-card>
<mat-card-content>
<div fxLayout="row" fxLayoutGap="5px">
<button mat-button color="warn" (click)="cancel()">Cancel</button>
<button mat-raised-button color="primary" (click)="save()">Save</button>
</div>
</mat-card-content>
</mat-card>
@@ -0,0 +1,68 @@
import { Component, OnInit, Inject } from '@angular/core';
import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
import * as moment from 'moment-timezone';
//Services
import { apiService } from 'src/app/core/services/api.service';
// Models
import { SaleInfo } from 'src/app/core/services/model/saleInfo.interface';
@Component({
selector: 'loading-sale-dialog-dialog',
templateUrl: './loading-sale-dialog.component.html',
styleUrls: ['./loading-sale-dialog.component.css']
})
export class LoadingSaleDialogComponent implements OnInit {
url: string = '';
SaleInfo: SaleInfo;
date: Date;
hour: string = "";
constructor(
private apiService: apiService,
public dialogRef: MatDialogRef<LoadingSaleDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.SaleInfo = {
_id: "",
idPlatform: "",
platform: "",
url: "",
title: "",
date: "",
location: "",
saleHouseName: "",
status: ""
};
this.date = new Date();
}
ngOnInit(): void {
this.url = this.data.url;
console.log(this.url);
this.apiService.getSaleInfos(this.url).subscribe( SaleInfo => {
console.log(SaleInfo);
this.SaleInfo = SaleInfo;
//hour
// Europe/Paris is the timezone of the user
this.date = moment(this.SaleInfo.date).tz('Europe/Paris').toDate();
this.hour = moment(this.SaleInfo.date).tz('Europe/Paris').format('HH:mm');
}
);
}
save(): void {
this.apiService.saveSale(this.SaleInfo).subscribe( SaleInfo => {
this.dialogRef.close(true);
});
}
cancel(): void {
this.dialogRef.close(false);
}
}

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