Add Angular6 version test (WIP).
13
src/main/ts2/.editorconfig
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
# Editor configuration, see http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
max_line_length = off
|
||||||
|
trim_trailing_whitespace = false
|
||||||
39
src/main/ts2/.gitignore
vendored
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# misc
|
||||||
|
/.sass-cache
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System Files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
153
src/main/ts2/angular.json
Executable file
@@ -0,0 +1,153 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||||
|
"version": 1,
|
||||||
|
"newProjectRoot": "projects",
|
||||||
|
"projects": {
|
||||||
|
"codiki": {
|
||||||
|
"root": "",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"projectType": "application",
|
||||||
|
"prefix": "app",
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"styleext": "scss"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"architect": {
|
||||||
|
"build": {
|
||||||
|
"builder": "@angular-devkit/build-angular:browser",
|
||||||
|
"options": {
|
||||||
|
"outputPath": "../resources/static/",
|
||||||
|
"index": "src/index.html",
|
||||||
|
"main": "src/main.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "src/tsconfig.app.json",
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"node_modules/font-awesome/scss/font-awesome.scss",
|
||||||
|
"node_modules/angular-bootstrap-md/scss/bootstrap/bootstrap.scss",
|
||||||
|
"node_modules/angular-bootstrap-md/scss/mdb-free.scss",
|
||||||
|
"src/styles.scss"
|
||||||
|
],
|
||||||
|
"scripts": [
|
||||||
|
"node_modules/hammerjs/hammer.min.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": false,
|
||||||
|
"extractCss": true,
|
||||||
|
"namedChunks": false,
|
||||||
|
"aot": true,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"vendorChunk": false,
|
||||||
|
"buildOptimizer": true
|
||||||
|
},
|
||||||
|
"integ": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.integ.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"optimization": true,
|
||||||
|
"outputHashing": "all",
|
||||||
|
"sourceMap": true,
|
||||||
|
"extractCss": true,
|
||||||
|
"namedChunks": true,
|
||||||
|
"aot": true,
|
||||||
|
"extractLicenses": true,
|
||||||
|
"vendorChunk": false,
|
||||||
|
"buildOptimizer": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "codiki:build"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"browserTarget": "codiki:build:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extract-i18n": {
|
||||||
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
|
"options": {
|
||||||
|
"browserTarget": "codiki:build"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"builder": "@angular-devkit/build-angular:karma",
|
||||||
|
"options": {
|
||||||
|
"main": "src/test.ts",
|
||||||
|
"polyfills": "src/polyfills.ts",
|
||||||
|
"tsConfig": "src/tsconfig.spec.json",
|
||||||
|
"karmaConfig": "src/karma.conf.js",
|
||||||
|
"styles": [
|
||||||
|
"src/styles.css"
|
||||||
|
],
|
||||||
|
"scripts": [],
|
||||||
|
"assets": [
|
||||||
|
"src/favicon.ico",
|
||||||
|
"src/assets"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@angular-devkit/build-angular:tslint",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": [
|
||||||
|
"src/tsconfig.app.json",
|
||||||
|
"src/tsconfig.spec.json"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/node_modules/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"codiki-e2e": {
|
||||||
|
"root": "e2e/",
|
||||||
|
"projectType": "application",
|
||||||
|
"architect": {
|
||||||
|
"e2e": {
|
||||||
|
"builder": "@angular-devkit/build-angular:protractor",
|
||||||
|
"options": {
|
||||||
|
"protractorConfig": "e2e/protractor.conf.js",
|
||||||
|
"devServerTarget": "codiki:serve"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"devServerTarget": "codiki:serve:production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@angular-devkit/build-angular:tslint",
|
||||||
|
"options": {
|
||||||
|
"tsConfig": "e2e/tsconfig.e2e.json",
|
||||||
|
"exclude": [
|
||||||
|
"**/node_modules/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"defaultProject": "codiki"
|
||||||
|
}
|
||||||
28
src/main/ts2/e2e/protractor.conf.js
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
// Protractor configuration file, see link for more information
|
||||||
|
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||||
|
|
||||||
|
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||||
|
|
||||||
|
exports.config = {
|
||||||
|
allScriptsTimeout: 11000,
|
||||||
|
specs: [
|
||||||
|
'./src/**/*.e2e-spec.ts'
|
||||||
|
],
|
||||||
|
capabilities: {
|
||||||
|
'browserName': 'chrome'
|
||||||
|
},
|
||||||
|
directConnect: true,
|
||||||
|
baseUrl: 'http://localhost:4200/',
|
||||||
|
framework: 'jasmine',
|
||||||
|
jasmineNodeOpts: {
|
||||||
|
showColors: true,
|
||||||
|
defaultTimeoutInterval: 30000,
|
||||||
|
print: function() {}
|
||||||
|
},
|
||||||
|
onPrepare() {
|
||||||
|
require('ts-node').register({
|
||||||
|
project: require('path').join(__dirname, './tsconfig.e2e.json')
|
||||||
|
});
|
||||||
|
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||||
|
}
|
||||||
|
};
|
||||||
14
src/main/ts2/e2e/src/app.e2e-spec.ts
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
import { AppPage } from './app.po';
|
||||||
|
|
||||||
|
describe('workspace-project App', () => {
|
||||||
|
let page: AppPage;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
page = new AppPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display welcome message', () => {
|
||||||
|
page.navigateTo();
|
||||||
|
expect(page.getParagraphText()).toEqual('Welcome to Codiki!');
|
||||||
|
});
|
||||||
|
});
|
||||||
11
src/main/ts2/e2e/src/app.po.ts
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
import { browser, by, element } from 'protractor';
|
||||||
|
|
||||||
|
export class AppPage {
|
||||||
|
navigateTo() {
|
||||||
|
return browser.get('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
getParagraphText() {
|
||||||
|
return element(by.css('app-root h1')).getText();
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/main/ts2/e2e/tsconfig.e2e.json
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../out-tsc/app",
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"types": [
|
||||||
|
"jasmine",
|
||||||
|
"jasminewd2",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/main/ts2/package.json
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "codiki",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"ng": "ng",
|
||||||
|
"start": "ng serve --proxy-config proxy.conf.json",
|
||||||
|
"build": "ng build",
|
||||||
|
"test": "ng test",
|
||||||
|
"lint": "ng lint",
|
||||||
|
"e2e": "ng e2e"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@angular/animations": "^7.2.1",
|
||||||
|
"@angular/common": "^7.2.1",
|
||||||
|
"@angular/compiler": "^7.2.1",
|
||||||
|
"@angular/core": "^7.2.1",
|
||||||
|
"@angular/forms": "^7.2.1",
|
||||||
|
"@angular/http": "^7.2.1",
|
||||||
|
"@angular/platform-browser": "^7.2.1",
|
||||||
|
"@angular/platform-browser-dynamic": "^7.2.1",
|
||||||
|
"@angular/router": "^7.2.1",
|
||||||
|
"angular-bootstrap-md": "^7.2.0",
|
||||||
|
"core-js": "^2.5.4",
|
||||||
|
"font-awesome": "^4.7.0",
|
||||||
|
"chart.js": "^2.5.0",
|
||||||
|
"hammerjs": "^2.0.8",
|
||||||
|
"rxjs": "~6.3.3",
|
||||||
|
"zone.js": "~0.8.28"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "~0.8.0",
|
||||||
|
"@angular/cli": "^6.2.9",
|
||||||
|
"@angular/compiler-cli": "^7.2.1",
|
||||||
|
"@angular/language-service": "^7.2.1",
|
||||||
|
"@types/jasmine": "~2.8.8",
|
||||||
|
"@types/jasminewd2": "~2.0.3",
|
||||||
|
"@types/node": "~8.9.4",
|
||||||
|
"codelyzer": "~4.3.0",
|
||||||
|
"jasmine-core": "~2.99.1",
|
||||||
|
"jasmine-spec-reporter": "~4.2.1",
|
||||||
|
"karma": "~3.0.0",
|
||||||
|
"karma-chrome-launcher": "~2.2.0",
|
||||||
|
"karma-coverage-istanbul-reporter": "~2.0.1",
|
||||||
|
"karma-jasmine": "~1.1.2",
|
||||||
|
"karma-jasmine-html-reporter": "^0.2.2",
|
||||||
|
"protractor": "~5.4.0",
|
||||||
|
"ts-node": "~7.0.0",
|
||||||
|
"tslint": "~5.11.0",
|
||||||
|
"typescript": "~3.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/main/ts2/src/app/account-settings/account-settings.component.html
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
<h1>Paramètres</h1>
|
||||||
|
<div class="align-top">
|
||||||
|
<div class="card hoverable">
|
||||||
|
<a routerLink="/changePassword">
|
||||||
|
<div class="card-body row">
|
||||||
|
<div class="col-10">
|
||||||
|
<h4>
|
||||||
|
<i class="fa fa-lock blue-text"></i>
|
||||||
|
Mot de passe
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="col-1 d-flex align-items-center text-right">
|
||||||
|
<i class="fa fa-chevron-right"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body grey lighten-4">
|
||||||
|
Changer de mot de passe
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card hoverable">
|
||||||
|
<a routerLink="/profilEdit">
|
||||||
|
<div class="card-body row">
|
||||||
|
<div class="col-10">
|
||||||
|
<h4>
|
||||||
|
<i class="fa fa-user-circle blue-text"></i>
|
||||||
|
Mes informations
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="col-1 d-flex align-items-center text-right">
|
||||||
|
<i class="fa fa-chevron-right"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body grey lighten-4">
|
||||||
|
Modifier les informations personnelles comme mon nom, mon adresse mail ou encore mon image de profil
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card grey-text">
|
||||||
|
<div class="card-body row">
|
||||||
|
<div class="col-10">
|
||||||
|
<h4>
|
||||||
|
<i class="fa fa-cog"></i>
|
||||||
|
Paramètres de compte
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="col-1 d-flex align-items-center text-right">
|
||||||
|
<i class="fa fa-chevron-right"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body grey lighten-4">
|
||||||
|
Paramètres divers
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
24
src/main/ts2/src/app/account-settings/account-settings.component.ts
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-account-settings',
|
||||||
|
templateUrl: './account-settings.component.html',
|
||||||
|
styles: [`
|
||||||
|
h4 i {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
a, a:visited, a:hover {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
margin-right: 20px;
|
||||||
|
width: 30%;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
`]
|
||||||
|
})
|
||||||
|
export class AccountSettingsComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<div class="card col-md-8 offset-md-2 col-lg-6 offset-lg-3">
|
||||||
|
<div id="form" class="card-body">
|
||||||
|
<h4 class="card-title">Mot de passe</h4>
|
||||||
|
<form (ngSubmit)="onSubmit()" #changePasswordForm="ngForm">
|
||||||
|
<div class="md-form">
|
||||||
|
<input mdbActive
|
||||||
|
id="oldPassword"
|
||||||
|
name="oldPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="model.oldPassword"
|
||||||
|
#oldPassword="ngModel"
|
||||||
|
data-error="Veuillez saisir votre mot de passe actuel"
|
||||||
|
data-sucess=""
|
||||||
|
required />
|
||||||
|
<label for="oldPassword">Mot de passe actuel</label>
|
||||||
|
</div>
|
||||||
|
<div class="md-form">
|
||||||
|
<input mdbActive
|
||||||
|
id="newPassword"
|
||||||
|
name="newPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="model.newPassword"
|
||||||
|
#newPassword="ngModel"
|
||||||
|
data-error="Veuillez saisir votre nouveau mot de passe"
|
||||||
|
data-sucess=""
|
||||||
|
required />
|
||||||
|
<label for="newPassword">Nouveau mot de passe</label>
|
||||||
|
</div>
|
||||||
|
<div class="md-form">
|
||||||
|
<input mdbActive
|
||||||
|
id="confirmPassword"
|
||||||
|
name="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="model.confirmPassword"
|
||||||
|
#confirmPassword="ngModel"
|
||||||
|
data-error="Veuillez confirmer votre nouveau mot de passe"
|
||||||
|
data-sucess=""
|
||||||
|
required />
|
||||||
|
<label for="confirmPassword">Confirmation de votre mot de passe</label>
|
||||||
|
</div>
|
||||||
|
<div class="card red lighten-2 text-center z-depth-2" *ngIf="error">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="white-text mb-0">{{error}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col submitFormArea">
|
||||||
|
<a routerLink="/accountSettings" class="indigo-text">
|
||||||
|
Annuler
|
||||||
|
</a>
|
||||||
|
<button class="float-right waves-effect waves-light indigo btn"
|
||||||
|
type="submit" [disabled]="!changePasswordForm.form.valid">
|
||||||
|
Suivant
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { PasswordWrapper } from '../../core/entities';
|
||||||
|
import { ChangePasswordService } from './change-password.service';
|
||||||
|
import { FormGroup, Validators, FormBuilder } from '@angular/forms';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-change-password',
|
||||||
|
templateUrl: './change-password.component.html',
|
||||||
|
styles: [`
|
||||||
|
#form.card-body {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitFormArea {
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
`]
|
||||||
|
})
|
||||||
|
export class ChangePasswordComponent {
|
||||||
|
model: PasswordWrapper = new PasswordWrapper('', '', '');
|
||||||
|
|
||||||
|
error: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private changePasswordService: ChangePasswordService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
if (this.model.newPassword !== this.model.confirmPassword) {
|
||||||
|
this.error = 'Les mots de passe saisis ne correspondent pas.';
|
||||||
|
} else {
|
||||||
|
this.changePasswordService.changePassword(this.model).subscribe(null, error => {
|
||||||
|
this.error = 'Le mot de passe saisi ne correspond pas au votre.';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { PasswordWrapper } from '../../core/entities';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
|
||||||
|
const ACCOUNT_URL = environment.apiUrl + '/api/account';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ChangePasswordService {
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
changePassword(passwordWrapper: PasswordWrapper): Observable<any> {
|
||||||
|
return this.http.put(ACCOUNT_URL + '/changePassword', passwordWrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<input id="profilImageInput" type="file" (change)="uploadImage($event)" [hidden]="true">
|
||||||
|
<div class="col-md-8 offset-md-2 col-lg-6 offset-lg-3">
|
||||||
|
<div class="card">
|
||||||
|
<img id="profil-image" class="card-img-top"
|
||||||
|
(click)="triggerProfilImageChange()"
|
||||||
|
[src]="getAvatarUrl()"
|
||||||
|
alt="Card image cap"
|
||||||
|
mdbTooltip="Modifier mon image de profil"
|
||||||
|
placement="bottom">
|
||||||
|
<div id="form" class="card-body">
|
||||||
|
<form (ngSubmit)="onSubmit()" #profilEditionForm="ngForm">
|
||||||
|
<div class="md-form">
|
||||||
|
<input mdbActive
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="model.name"
|
||||||
|
#name="ngModel"
|
||||||
|
data-error="Veuillez saisir votre nom d'utilisateur"
|
||||||
|
data-sucess=""
|
||||||
|
required />
|
||||||
|
<label for="name">Nom d'utilisateur</label>
|
||||||
|
</div>
|
||||||
|
<div class="md-form">
|
||||||
|
<input mdbActive
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="model.email"
|
||||||
|
#email="ngModel"
|
||||||
|
data-error="Veuillez saisir votre adresse email"
|
||||||
|
data-sucess=""
|
||||||
|
required />
|
||||||
|
<label for="email">Email</label>
|
||||||
|
</div>
|
||||||
|
<div id="errorMsg" class="card red lighten-2 text-center z-depth-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="white-text mb-0">{{modelError}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="resultMsg" class="card green lighten-2 text-center z-depth-2" >
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="white-text mb-0">{{result}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col submitFormArea">
|
||||||
|
<a routerLink="/accountSettings" class="indigo-text">
|
||||||
|
Annuler
|
||||||
|
</a>
|
||||||
|
<button class="float-right waves-effect waves-light indigo btn"
|
||||||
|
type="submit" [disabled]="!profilEditionForm.form.valid">
|
||||||
|
Suivant
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
101
src/main/ts2/src/app/account-settings/profil-edition/profil-edition.component.ts
Executable file
@@ -0,0 +1,101 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ProfilEditionService } from './profil-edition.service';
|
||||||
|
import { HttpEventType, HttpResponse } from '@angular/common/http';
|
||||||
|
import { User } from '../../core/entities';
|
||||||
|
import { AuthService } from '../../core/services/auth.service';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-profil-edition',
|
||||||
|
templateUrl: './profil-edition.component.html',
|
||||||
|
styles: [`
|
||||||
|
#form {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitFormArea {
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profil-image {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#resultMsg, #errorMsg {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.5s ease-out;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`]
|
||||||
|
})
|
||||||
|
export class ProfilEditionComponent implements OnInit {
|
||||||
|
model: User;
|
||||||
|
selectedFiles: FileList;
|
||||||
|
currentFileUpload: File;
|
||||||
|
progress: { percentage: number } = { percentage: 0 };
|
||||||
|
modelError: string;
|
||||||
|
result: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private profilEditionService: ProfilEditionService,
|
||||||
|
private authService: AuthService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.model = this.authService.getUser();
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvatarUrl(): string {
|
||||||
|
return this.model.image
|
||||||
|
? `${environment.apiUrl}/api/images/loadAvatar/${this.model.image}`
|
||||||
|
: './assets/images/default_user.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerProfilImageChange(): void {
|
||||||
|
document.getElementById('profilImageInput').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadImage(event) {
|
||||||
|
this.selectedFiles = event.target.files;
|
||||||
|
this.progress.percentage = 0;
|
||||||
|
|
||||||
|
this.currentFileUpload = this.selectedFiles.item(0);
|
||||||
|
// This prevents error 400 if user doesn't select any file to upload and close the input file.
|
||||||
|
if (this.currentFileUpload) {
|
||||||
|
this.profilEditionService.uploadAvatarPicture(this.currentFileUpload).subscribe(result => {
|
||||||
|
if (result.type === HttpEventType.UploadProgress) {
|
||||||
|
this.progress.percentage = Math.round(100 * result.loaded / result.total);
|
||||||
|
} else if (result instanceof HttpResponse) {
|
||||||
|
console.log('File ' + result.body + ' completely uploaded!');
|
||||||
|
this.model.image = result.body as string;
|
||||||
|
this.authService.setUser(this.model);
|
||||||
|
}
|
||||||
|
this.selectedFiles = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
this.profilEditionService.updateUser(this.model).subscribe(() => {
|
||||||
|
this.setMessage('Modification enregistrée.', false);
|
||||||
|
}, error => {
|
||||||
|
this.setMessage('L\'adresse mail saisie n\'est pas disponible.', true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessage(message: string, error: boolean): void {
|
||||||
|
this[error ? 'modelError' : 'result'] = message;
|
||||||
|
|
||||||
|
const resultMsgDiv = document.getElementById(error ? 'errorMsg' : 'resultMsg');
|
||||||
|
resultMsgDiv.style.maxHeight = '64px';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
resultMsgDiv.style.maxHeight = '0px';
|
||||||
|
setTimeout(() => {
|
||||||
|
this[error ? 'modelError' : 'result'] = undefined;
|
||||||
|
}, 550);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {HttpClient, HttpRequest, HttpEvent} from '@angular/common/http';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import { User } from '../../core/entities';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
|
||||||
|
const IMAGES_URL = environment.apiUrl + '/api/images';
|
||||||
|
const ACCOUNT_URL = environment.apiUrl + '/api/account';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProfilEditionService {
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
uploadAvatarPicture(file: File): Observable<HttpEvent<{}>> {
|
||||||
|
const formData: FormData = new FormData();
|
||||||
|
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
return this.http.request(new HttpRequest(
|
||||||
|
'POST', IMAGES_URL + '/uploadAvatar', formData, {
|
||||||
|
reportProgress: true,
|
||||||
|
responseType: 'text'
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUser(user: User): Observable<any> {
|
||||||
|
return this.http.put<any>(`${ACCOUNT_URL}/`, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/main/ts2/src/app/app.component.css
Executable file
5
src/main/ts2/src/app/app.component.html
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
<app-header></app-header>
|
||||||
|
<main class="container">
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</main>
|
||||||
|
<app-footer></app-footer>
|
||||||
27
src/main/ts2/src/app/app.component.spec.ts
Executable file
@@ -0,0 +1,27 @@
|
|||||||
|
import { TestBed, async } from '@angular/core/testing';
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
describe('AppComponent', () => {
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
it('should create the app', async(() => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.debugElement.componentInstance;
|
||||||
|
expect(app).toBeTruthy();
|
||||||
|
}));
|
||||||
|
it(`should have as title 'Codiki'`, async(() => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
const app = fixture.debugElement.componentInstance;
|
||||||
|
expect(app.title).toEqual('Codiki');
|
||||||
|
}));
|
||||||
|
it('should render title in a h1 tag', async(() => {
|
||||||
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
|
fixture.detectChanges();
|
||||||
|
const compiled = fixture.debugElement.nativeElement;
|
||||||
|
expect(compiled.querySelector('h1').textContent).toContain('Welcome to Codiki!');
|
||||||
|
}));
|
||||||
|
});
|
||||||
10
src/main/ts2/src/app/app.component.ts
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-root',
|
||||||
|
templateUrl: './app.component.html',
|
||||||
|
styleUrls: ['./app.component.css']
|
||||||
|
})
|
||||||
|
export class AppComponent {
|
||||||
|
title = 'codiki';
|
||||||
|
}
|
||||||
120
src/main/ts2/src/app/app.module.ts
Executable file
@@ -0,0 +1,120 @@
|
|||||||
|
// Angular core
|
||||||
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
|
import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
|
|
||||||
|
// Dependencies
|
||||||
|
import { MDBBootstrapModule } from 'angular-bootstrap-md';
|
||||||
|
|
||||||
|
// Router
|
||||||
|
import { appRoutes } from './app.routes';
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import { AppComponent } from './app.component';
|
||||||
|
import { HeaderComponent } from './header/header.component';
|
||||||
|
import { FooterComponent } from './footer/footer.component';
|
||||||
|
import { NotFoundComponent } from './not-found/not-found.component';
|
||||||
|
import { HomeComponent } from './home/home.component';
|
||||||
|
import { LoginComponent } from './login/login.component';
|
||||||
|
import { DisconnectionComponent } from './disconnection/disconnection.component';
|
||||||
|
import { PostCardComponent } from './core/post-card/post-card.component';
|
||||||
|
import { PostComponent } from './posts/post.component';
|
||||||
|
import { ByCategoryComponent } from './posts/byCategory/by-category.component';
|
||||||
|
import { MyPostsComponent } from './posts/myPosts/my-posts.component';
|
||||||
|
import { AccountSettingsComponent } from './account-settings/account-settings.component';
|
||||||
|
import { ChangePasswordComponent } from './account-settings/change-password/change-password.component';
|
||||||
|
import { ProfilEditionComponent } from './account-settings/profil-edition/profil-edition.component';
|
||||||
|
import { CreateUpdatePostComponent } from './posts/create-update/create-update-post.component';
|
||||||
|
import { ForbiddenComponent } from './forbidden/forbidden.component';
|
||||||
|
import { SearchComponent } from './search/search.component';
|
||||||
|
import { SigninComponent } from './signin/signin.component';
|
||||||
|
import { VersionRevisionComponent } from './version-revisions/version-revisions.component';
|
||||||
|
|
||||||
|
// html components
|
||||||
|
import { ProgressBarComponent } from './core/directives/progress-bar/progress-bar.component';
|
||||||
|
import { SpinnerComponent } from './core/directives/spinner/spinner.component';
|
||||||
|
import { SearchBarComponent } from './core/directives/search-bar/search-bar.component';
|
||||||
|
|
||||||
|
// Services
|
||||||
|
import { AuthService } from './core/services/auth.service';
|
||||||
|
import { LoginService } from './login/login.service';
|
||||||
|
import { HomeService } from './home/home.service';
|
||||||
|
import { PostService } from './posts/post.service';
|
||||||
|
import { ByCategoryService } from './posts/byCategory/by-category.service';
|
||||||
|
import { MyPostsService } from './posts/myPosts/my-posts.service';
|
||||||
|
import { ChangePasswordService } from './account-settings/change-password/change-password.service';
|
||||||
|
import { ProfilEditionService } from './account-settings/profil-edition/profil-edition.service';
|
||||||
|
import { HeaderService } from './header/header.service';
|
||||||
|
import { CreateUpdatePostService } from './posts/create-update/create-update-post.service';
|
||||||
|
import { SearchService } from './search/search.service';
|
||||||
|
import { VersionRevisionService } from './version-revisions/version-revisions.service';
|
||||||
|
|
||||||
|
// Guards
|
||||||
|
import { AuthGuard } from './core/guards/auth.guard';
|
||||||
|
|
||||||
|
// Interceptors
|
||||||
|
import { TokenInterceptor } from './core/interceptors/token-interceptor';
|
||||||
|
import { SigninService } from './signin/signin.service';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AppComponent,
|
||||||
|
HeaderComponent,
|
||||||
|
FooterComponent,
|
||||||
|
NotFoundComponent,
|
||||||
|
HomeComponent,
|
||||||
|
LoginComponent,
|
||||||
|
DisconnectionComponent,
|
||||||
|
PostCardComponent,
|
||||||
|
PostComponent,
|
||||||
|
ByCategoryComponent,
|
||||||
|
MyPostsComponent,
|
||||||
|
AccountSettingsComponent,
|
||||||
|
CreateUpdatePostComponent,
|
||||||
|
ChangePasswordComponent,
|
||||||
|
ProfilEditionComponent,
|
||||||
|
ForbiddenComponent,
|
||||||
|
SearchComponent,
|
||||||
|
SigninComponent,
|
||||||
|
ProgressBarComponent,
|
||||||
|
SpinnerComponent,
|
||||||
|
SearchBarComponent,
|
||||||
|
VersionRevisionComponent
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
BrowserModule,
|
||||||
|
HttpClientModule,
|
||||||
|
FormsModule,
|
||||||
|
MDBBootstrapModule.forRoot(),
|
||||||
|
RouterModule.forRoot(
|
||||||
|
appRoutes,
|
||||||
|
// { enableTracing: true } // Enabling tracing
|
||||||
|
{ onSameUrlNavigation: 'reload' }
|
||||||
|
)
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
LoginService,
|
||||||
|
SigninService,
|
||||||
|
HomeService,
|
||||||
|
PostService,
|
||||||
|
ByCategoryService,
|
||||||
|
MyPostsService,
|
||||||
|
ChangePasswordService,
|
||||||
|
ProfilEditionService,
|
||||||
|
HeaderService,
|
||||||
|
CreateUpdatePostService,
|
||||||
|
SearchService,
|
||||||
|
VersionRevisionService,
|
||||||
|
// AuthGuard,
|
||||||
|
// {
|
||||||
|
// provide: HTTP_INTERCEPTORS,
|
||||||
|
// useClass: TokenInterceptor,
|
||||||
|
// multi: true
|
||||||
|
// }
|
||||||
|
],
|
||||||
|
bootstrap: [AppComponent]
|
||||||
|
})
|
||||||
|
export class AppModule { }
|
||||||
59
src/main/ts2/src/app/app.routes.ts
Executable file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Routes } from '@angular/router';
|
||||||
|
|
||||||
|
import { HomeComponent } from './home/home.component';
|
||||||
|
import { NotFoundComponent } from './not-found/not-found.component';
|
||||||
|
import { LoginComponent } from './login/login.component';
|
||||||
|
import { SigninComponent } from './signin/signin.component';
|
||||||
|
import { DisconnectionComponent } from './disconnection/disconnection.component';
|
||||||
|
import { PostComponent } from './posts/post.component';
|
||||||
|
import { ByCategoryComponent } from './posts/byCategory/by-category.component';
|
||||||
|
import { MyPostsComponent } from './posts/myPosts/my-posts.component';
|
||||||
|
import { AccountSettingsComponent } from './account-settings/account-settings.component';
|
||||||
|
import { ChangePasswordComponent } from './account-settings/change-password/change-password.component';
|
||||||
|
import { CreateUpdatePostComponent } from './posts/create-update/create-update-post.component';
|
||||||
|
import { ForbiddenComponent } from './forbidden/forbidden.component';
|
||||||
|
import { ProfilEditionComponent } from './account-settings/profil-edition/profil-edition.component';
|
||||||
|
import { SearchComponent } from './search/search.component';
|
||||||
|
import { VersionRevisionComponent } from './version-revisions/version-revisions.component';
|
||||||
|
|
||||||
|
import { AuthGuard } from './core/guards/auth.guard';
|
||||||
|
|
||||||
|
// export const appRoutes: Routes = [
|
||||||
|
// { path: 'login', component: LoginComponent },
|
||||||
|
// { path: 'signin', component: SigninComponent },
|
||||||
|
// { path: 'home', component: HomeComponent },
|
||||||
|
// { path: 'disconnection', component: DisconnectionComponent },
|
||||||
|
// { path: 'posts/new', component: CreateUpdatePostComponent, canActivate: [AuthGuard] },
|
||||||
|
// { path: 'posts/update/:postKey', component: CreateUpdatePostComponent, canActivate: [AuthGuard] },
|
||||||
|
// { path: 'posts/:postKey', component: PostComponent },
|
||||||
|
// { path: 'posts/byCategory/:categoryId', component: ByCategoryComponent},
|
||||||
|
// { path: 'posts/search/:searchCriteria', component: SearchComponent},
|
||||||
|
// { path: 'myPosts', component: MyPostsComponent, canActivate: [AuthGuard]},
|
||||||
|
// { path: 'accountSettings', component: AccountSettingsComponent, canActivate: [AuthGuard] },
|
||||||
|
// { path: 'changePassword', component: ChangePasswordComponent, canActivate: [AuthGuard] },
|
||||||
|
// { path: 'profilEdit', component: ProfilEditionComponent, canActivate: [AuthGuard] },
|
||||||
|
// { path: 'versionrevisions', component: VersionRevisionComponent },
|
||||||
|
// { path: 'forbidden', component: ForbiddenComponent },
|
||||||
|
// { path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||||
|
// { path: '**', component: NotFoundComponent }
|
||||||
|
// ];
|
||||||
|
|
||||||
|
export const appRoutes: Routes = [
|
||||||
|
{ path: 'login', component: LoginComponent },
|
||||||
|
{ path: 'signin', component: SigninComponent },
|
||||||
|
{ path: 'home', component: HomeComponent },
|
||||||
|
{ path: 'disconnection', component: DisconnectionComponent },
|
||||||
|
{ path: 'posts/new', component: CreateUpdatePostComponent },
|
||||||
|
{ path: 'posts/update/:postKey', component: CreateUpdatePostComponent },
|
||||||
|
{ path: 'posts/:postKey', component: PostComponent },
|
||||||
|
{ path: 'posts/byCategory/:categoryId', component: ByCategoryComponent},
|
||||||
|
{ path: 'posts/search/:searchCriteria', component: SearchComponent},
|
||||||
|
{ path: 'myPosts', component: MyPostsComponent},
|
||||||
|
{ path: 'accountSettings', component: AccountSettingsComponent },
|
||||||
|
{ path: 'changePassword', component: ChangePasswordComponent },
|
||||||
|
{ path: 'profilEdit', component: ProfilEditionComponent },
|
||||||
|
{ path: 'versionrevisions', component: VersionRevisionComponent },
|
||||||
|
{ path: 'forbidden', component: ForbiddenComponent },
|
||||||
|
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||||
|
{ path: '**', component: NotFoundComponent }
|
||||||
|
];
|
||||||
112
src/main/ts2/src/app/core/directives/progress-bar/progress-bar.component.scss
Executable file
@@ -0,0 +1,112 @@
|
|||||||
|
.progress {
|
||||||
|
position: relative;
|
||||||
|
height: 4px;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #3F729B;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-clip: padding-box;
|
||||||
|
margin: 0.5rem 0 1rem 0;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-animation: random-background 5s infinite;
|
||||||
|
animation: random-background 5s infinite;transition: all 0.3s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes random-background {
|
||||||
|
15% { background-color: #ff444480; }
|
||||||
|
30% { background-color: #ffbb3381; }
|
||||||
|
45% { background-color: rgba(0, 200, 80, 0.575); }
|
||||||
|
60% { background-color: #33b6e58c; }
|
||||||
|
75% { background-color: #aa66cc7c; }
|
||||||
|
}
|
||||||
|
.progress .indeterminate {
|
||||||
|
background-color: #1C2331;
|
||||||
|
-webkit-animation: random-bar 5s infinite;
|
||||||
|
animation: random-bar 5s infinite;transition: all 0.3s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes random-bar {
|
||||||
|
15% { background-color: #ff4444; }
|
||||||
|
30% { background-color: #ffbb33; }
|
||||||
|
45% { background-color: #00C851; }
|
||||||
|
60% { background-color: #33b5e5; }
|
||||||
|
75% { background-color: #aa66cc; }
|
||||||
|
}
|
||||||
|
.progress .indeterminate:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background-color: inherit;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
will-change: left, right;
|
||||||
|
-webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
|
||||||
|
animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
|
||||||
|
}
|
||||||
|
.progress .indeterminate:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background-color: inherit;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
will-change: left, right;
|
||||||
|
-webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
|
||||||
|
animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
|
||||||
|
-webkit-animation-delay: 1.15s;
|
||||||
|
animation-delay: 1.15s;
|
||||||
|
}
|
||||||
|
@-webkit-keyframes indeterminate {
|
||||||
|
0% {
|
||||||
|
left: -35%;
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
left: 100%;
|
||||||
|
right: -90%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
right: -90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes indeterminate {
|
||||||
|
0% {
|
||||||
|
left: -35%;
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
left: 100%;
|
||||||
|
right: -90%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
right: -90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-webkit-keyframes indeterminate-short {
|
||||||
|
0% {
|
||||||
|
left: -200%;
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
left: 107%;
|
||||||
|
right: -8%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 107%;
|
||||||
|
right: -8%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes indeterminate-short {
|
||||||
|
0% {
|
||||||
|
left: -200%;
|
||||||
|
right: 100%;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
left: 107%;
|
||||||
|
right: -8%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 107%;
|
||||||
|
right: -8%;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/main/ts2/src/app/core/directives/progress-bar/progress-bar.component.ts
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-progress-bar',
|
||||||
|
template: `
|
||||||
|
<div class="progress">
|
||||||
|
<div class="indeterminate"></div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styleUrls: ['./progress-bar.component.scss']
|
||||||
|
})
|
||||||
|
export class ProgressBarComponent {}
|
||||||
66
src/main/ts2/src/app/core/directives/search-bar/search-bar.component.ts
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-search-bar',
|
||||||
|
template: `
|
||||||
|
<div id="search-bar">
|
||||||
|
<input id="search" name="search" type="text" [(ngModel)]="model" (keyup.enter)="search()" />
|
||||||
|
<label for="search" class="label-icon">
|
||||||
|
<i id="search-icon" class="fa fa-search" (click)="search()"></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: [`
|
||||||
|
div#search-bar {
|
||||||
|
position: relative;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input#search, input#search:focus {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input#search {
|
||||||
|
width: 400px;
|
||||||
|
height: 36px;
|
||||||
|
color: white;
|
||||||
|
background-color: #5c6bc0;
|
||||||
|
border-radius: 2px;
|
||||||
|
border-style: unset;
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input#search:focus {
|
||||||
|
background: white;
|
||||||
|
color: #3f51b5;
|
||||||
|
}
|
||||||
|
|
||||||
|
i#search-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
position: absolute;
|
||||||
|
right: 15px;
|
||||||
|
top: 8px;
|
||||||
|
color: #9e9e9e;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`]
|
||||||
|
})
|
||||||
|
export class SearchBarComponent {
|
||||||
|
model: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
search(): void {
|
||||||
|
if (this.model) {
|
||||||
|
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||||
|
this.router.navigateByUrl(`/posts/search/${this.model}`).then(() => {
|
||||||
|
this.router.navigated = false;
|
||||||
|
this.router.navigate([this.router.url]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/main/ts2/src/app/core/directives/spinner/spinner.component.scss
Executable file
@@ -0,0 +1,84 @@
|
|||||||
|
$green: #00C851;
|
||||||
|
$blue: #33b5e5;
|
||||||
|
$red: #ff4444;
|
||||||
|
$yellow: #ffbb33;
|
||||||
|
$white: #eee;
|
||||||
|
|
||||||
|
$width: 100px;
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showbox {
|
||||||
|
padding: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: $width;
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
padding-top: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.circular {
|
||||||
|
animation: rotate 2s linear infinite;
|
||||||
|
height: 100%;
|
||||||
|
transform-origin: center center;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
stroke-dasharray: 1, 200;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dash {
|
||||||
|
0% {
|
||||||
|
stroke-dasharray: 1, 200;
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
stroke-dasharray: 89, 200;
|
||||||
|
stroke-dashoffset: -35px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
stroke-dasharray: 89, 200;
|
||||||
|
stroke-dashoffset: -124px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes color {
|
||||||
|
100%,
|
||||||
|
0% {
|
||||||
|
stroke: $red;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
stroke: $blue;
|
||||||
|
}
|
||||||
|
66% {
|
||||||
|
stroke: $green;
|
||||||
|
}
|
||||||
|
80%,
|
||||||
|
90% {
|
||||||
|
stroke: $yellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/ts2/src/app/core/directives/spinner/spinner.component.ts
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-spinner',
|
||||||
|
template: `
|
||||||
|
<div class="showbox">
|
||||||
|
<div class="loader">
|
||||||
|
<svg class="circular" viewBox="25 25 50 50">
|
||||||
|
<circle class="path" cx="50" cy="50" r="20" fill="none" stroke-width="2" stroke-miterlimit="10"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styleUrls: ['./spinner.component.scss']
|
||||||
|
})
|
||||||
|
export class SpinnerComponent {
|
||||||
|
|
||||||
|
}
|
||||||
75
src/main/ts2/src/app/core/entities.ts
Executable file
@@ -0,0 +1,75 @@
|
|||||||
|
export class Role {
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public name: string
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class User {
|
||||||
|
constructor(
|
||||||
|
public key: string,
|
||||||
|
public name: string,
|
||||||
|
public email: string,
|
||||||
|
public password: string,
|
||||||
|
public image: string,
|
||||||
|
public inscriptionDate: Date,
|
||||||
|
public role: Role,
|
||||||
|
public token: string
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Post {
|
||||||
|
constructor(
|
||||||
|
public key: string,
|
||||||
|
public title: string,
|
||||||
|
public text: string,
|
||||||
|
public description: string,
|
||||||
|
public image: string,
|
||||||
|
public creationDate: Date,
|
||||||
|
public author: User,
|
||||||
|
public category: Category
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Category {
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public name: string,
|
||||||
|
public listSubCategories: Array<Category>
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Image {
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public link: string
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to send the new password to backoffice in order to change the user password.
|
||||||
|
*/
|
||||||
|
export class PasswordWrapper {
|
||||||
|
constructor(
|
||||||
|
public oldPassword: string,
|
||||||
|
public newPassword: string,
|
||||||
|
public confirmPassword: string
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Version {
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public number: string,
|
||||||
|
public active: boolean
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VersionRevision {
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public text: string,
|
||||||
|
public version: Version,
|
||||||
|
public bugfix: boolean
|
||||||
|
) { }
|
||||||
|
}
|
||||||
26
src/main/ts2/src/app/core/guards/auth.guard.ts
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||||
|
|
||||||
|
import { AuthService } from '../services/auth.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthGuard implements CanActivate {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private authService: AuthService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
|
||||||
|
let result: boolean;
|
||||||
|
|
||||||
|
if (this.authService.isAuthenticated()) {
|
||||||
|
result = true;
|
||||||
|
} else {
|
||||||
|
result = false;
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/main/ts2/src/app/core/interceptors/token-interceptor.ts
Executable file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { User } from '../entities';
|
||||||
|
import { AuthService } from '../services/auth.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TokenInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||||
|
const token = this.authService.getToken();
|
||||||
|
|
||||||
|
let request: HttpRequest<any> = req;
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
request = req.clone({
|
||||||
|
setHeaders: {
|
||||||
|
token: token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
// return next.handle(request).tap((event: HttpEvent<any>) => {
|
||||||
|
// // Do nothing for the interceptor
|
||||||
|
// }, (err: any) => {
|
||||||
|
// if (err instanceof HttpErrorResponse && err.status === 401) {
|
||||||
|
// this.authService.disconnect();
|
||||||
|
// this.router.navigate(['/login']);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
60
src/main/ts2/src/app/core/post-card/post-card.component.ts
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Post } from '../entities';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-post-card',
|
||||||
|
template: `
|
||||||
|
<div class="card hoverable">
|
||||||
|
<div class="view hm-white-slight waves-light" mdbRippleRadius>
|
||||||
|
<img id="post-image" class="img-fluid" [src]="post.image" alt="Article" />
|
||||||
|
<a routerLink="/posts/{{post.key}}">
|
||||||
|
<div class="mask"></div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">{{post.title}}</h4>
|
||||||
|
<p class="card-text">{{post.description}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-data">
|
||||||
|
<img [src]="getAvatarUrl()"
|
||||||
|
class="author-img"
|
||||||
|
[alt]="post.author.name"
|
||||||
|
[mdbTooltip]="post.author.name"
|
||||||
|
placement="bottom"/>
|
||||||
|
Article écrit par {{post.author.name}}
|
||||||
|
<span class="creation-date-area">({{post.creationDate | date:'yyyy-MM-dd HH:mm:ss'}})</span>
|
||||||
|
</div>
|
||||||
|
</div>`,
|
||||||
|
styles: [`
|
||||||
|
div.card {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
.card .card-data {
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.creation-date-area {
|
||||||
|
color: #bdbdbd;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.author-img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
#post-image {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`]
|
||||||
|
})
|
||||||
|
export class PostCardComponent {
|
||||||
|
@Input() post: Post;
|
||||||
|
|
||||||
|
getAvatarUrl(): string {
|
||||||
|
return this.post.author.image
|
||||||
|
? `${environment.apiUrl}/api/images/loadAvatar/${this.post.author.image}`
|
||||||
|
: './assets/images/default_user.png';
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/main/ts2/src/app/core/services/auth.service.ts
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { User } from '../entities';
|
||||||
|
|
||||||
|
const PARAM_TOKEN = 'token';
|
||||||
|
const PARAM_USER = 'user';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
authenticated: boolean = false;
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
public setAuthenticated(pAuthenticated: boolean): void {
|
||||||
|
this.authenticated = pAuthenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getToken(): string {
|
||||||
|
return localStorage.getItem(PARAM_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setToken(token: string): void {
|
||||||
|
localStorage.setItem(PARAM_TOKEN, token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isAuthenticated(): boolean {
|
||||||
|
return this.authenticated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnect(): void {
|
||||||
|
localStorage.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isAdmin(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setUser(user: User): void {
|
||||||
|
localStorage.setItem(PARAM_USER, JSON.stringify(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUser(): User {
|
||||||
|
return JSON.parse(localStorage.getItem(PARAM_USER));
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/main/ts2/src/app/disconnection/disconnection.component.ts
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { AuthService } from '../core/services/auth.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-disconnection',
|
||||||
|
template: 'Déconnexion...'
|
||||||
|
})
|
||||||
|
export class DisconnectionComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.authService.setAuthenticated(false);
|
||||||
|
this.router.navigate(['/home']);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/ts2/src/app/footer/footer.component.html
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
<footer class="page-footer indigo center-on-small-only">
|
||||||
|
<!-- fixed-bottom -->
|
||||||
|
<div class="footer-copyright">
|
||||||
|
<div class="container">
|
||||||
|
<span class="float-left">
|
||||||
|
<span class="anticopy">©</span> 2017 Tous droits réservés -
|
||||||
|
<i id="appVersion" routerLink="/versionrevisions" mdbTooltip="Notes de versions" placement="top" mdbRippleRadius>
|
||||||
|
{{appVersion}}
|
||||||
|
</i>
|
||||||
|
</span>
|
||||||
|
<span class="float-right">
|
||||||
|
<a target="_blank" href="./assets/doc/codiki_user_manual.pdf" mdbTooltip="Manuel d'utilisation" placement="top" mdbRippleRadius>
|
||||||
|
<i class="fa fa-book"></i>
|
||||||
|
</a> -
|
||||||
|
Développements réalisés par
|
||||||
|
<a href="mailto:florian.thierry72@gmail.com">Florian THIERRY</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
19
src/main/ts2/src/app/footer/footer.component.scss
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
footer {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-footer {
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.anticopy {
|
||||||
|
display: inline-block;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#appVersion {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
12
src/main/ts2/src/app/footer/footer.component.ts
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-footer',
|
||||||
|
templateUrl: './footer.component.html',
|
||||||
|
styleUrls: ['./footer.component.scss']
|
||||||
|
})
|
||||||
|
export class FooterComponent {
|
||||||
|
appVersion = environment.appVersion;
|
||||||
|
}
|
||||||
18
src/main/ts2/src/app/forbidden/forbidden.component.ts
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-forbidden',
|
||||||
|
template: `
|
||||||
|
<div class="text-center">
|
||||||
|
<img src="./assets/images/403.png" alt="403" />
|
||||||
|
<h3>Vous n'êtes pas autorisé à effectuer cette action...</h3>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styles: [`
|
||||||
|
img {
|
||||||
|
max-height: 500px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
`]
|
||||||
|
})
|
||||||
|
export class ForbiddenComponent {}
|
||||||
62
src/main/ts2/src/app/header/header.component.html
Executable file
@@ -0,0 +1,62 @@
|
|||||||
|
<mdb-navbar SideClass="navbar fixed-top navbar-expand-lg navbar-dark indigo scrolling-navbar ie-nav" [containerInside]="false">
|
||||||
|
<logo>
|
||||||
|
<a class="logo navbar-brand" routerLink="/">
|
||||||
|
<img id="logo" src="./assets/images/codiki.png" alt="logo" />
|
||||||
|
<span id="title" *ngIf="!title">Codiki</span>
|
||||||
|
<span id="title" *ngIf="title">Codiki - {{title}}</span>
|
||||||
|
</a>
|
||||||
|
</logo>
|
||||||
|
<links>
|
||||||
|
<div class="navbar-nav ml-auto nav-flex-icons">
|
||||||
|
<app-search-bar></app-search-bar>
|
||||||
|
</div>
|
||||||
|
<ul class="navbar-nav ml-auto nav-flex-icons" style="margin-left: 0 !important;">
|
||||||
|
<li class="nav-item" *ngIf="!isAuthenticated()">
|
||||||
|
<a routerLink="/login" class="nav-link waves-light" mdbRippleRadius>
|
||||||
|
<i class="fa fa-sign-in"></i> Connexion
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item dropdown" dropdown *ngIf="isAuthenticated()">
|
||||||
|
<a dropdownToggle mdbRippleRadius type="button" class="nav-link dropdown-toggle waves-light" mdbRippleRadius>
|
||||||
|
<i class="fa fa-user-circle"></i> Mon Compte
|
||||||
|
</a>
|
||||||
|
<div *dropdownMenu class="dropdown-menu dropdown-menu-right dropdown dropdown-primary" role="menu">
|
||||||
|
<a class="dropdown-item waves-light" mdbRippleRadius routerLink="/myPosts">
|
||||||
|
<i class="fa fa-list-alt"></i> Mes articles
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item waves-light" mdbRippleRadius routerLink="/accountSettings">
|
||||||
|
<i class="fa fa-gear"></i> Paramètres
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item waves-light danger-color-dark" mdbRippleRadius routerLink="/disconnection">
|
||||||
|
<span class="white-text">
|
||||||
|
<i class="fa fa-sign-out"></i> Déconnexion
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<a (click)="openSidebar()">
|
||||||
|
<i id="sidebarButton" class="fa fa-lg fa-bars white-text"></i>
|
||||||
|
</a>
|
||||||
|
</links>
|
||||||
|
</mdb-navbar>
|
||||||
|
|
||||||
|
<div id="sidenav" class="sidenav">
|
||||||
|
<h3>Catégories</h3>
|
||||||
|
<a class="closebtn" (click)="closeSidebar()">
|
||||||
|
<i class="fa fa-chevron-left"></i>
|
||||||
|
</a>
|
||||||
|
<div *ngFor="let category of listCategories">
|
||||||
|
<a [id]="'category-' + category.id" class="collapsible" *ngIf="category.listSubCategories.length" (click)="openCategoriesLinks(category)">
|
||||||
|
{{category.name}} <i class="fa fa-chevron-down pull-right"></i>
|
||||||
|
</a>
|
||||||
|
<div class="categoriesLinks" >
|
||||||
|
<a *ngFor="let subCategory of category.listSubCategories"
|
||||||
|
(click)="openPostsByCategory(subCategory.id)">
|
||||||
|
{{subCategory.name}}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="overlay" (click)="closeSidebar()"></div>
|
||||||
87
src/main/ts2/src/app/header/header.component.scss
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
#title {
|
||||||
|
color: white;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logo {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
margin-top: -7px;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebarButton {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The side navigation menu */
|
||||||
|
.sidenav {
|
||||||
|
height: 100%; /* 100% Full-height */
|
||||||
|
width: 0; /* 0 width - change this with JavaScript */
|
||||||
|
position: fixed; /* Stay in place */
|
||||||
|
z-index: 1000; /* Stay on top */
|
||||||
|
top: 0; /* Stay at the top */
|
||||||
|
left: 0;
|
||||||
|
background-color: #3f51b5;
|
||||||
|
overflow-x: hidden; /* Disable horizontal scroll */
|
||||||
|
padding-top: 20px; /* Place content 60px from the top */
|
||||||
|
transition: 0.3s; /* 0.5 second transition effect to slide in the sidenav */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The navigation menu links */
|
||||||
|
.sidenav a {
|
||||||
|
padding: 8px 32px 8px 32px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
display: block;
|
||||||
|
transition: 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When you mouse over the navigation links, change their color */
|
||||||
|
.sidenav a:hover {
|
||||||
|
color: #ccc;
|
||||||
|
background-color: #5c6bc0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidenav h3 {
|
||||||
|
padding: 8px 8px 8px 32px;
|
||||||
|
color: white;
|
||||||
|
padding-bottom: 25px;
|
||||||
|
border-bottom: 1px solid #5c6bc0;
|
||||||
|
}
|
||||||
|
/* Position and style the close button (top right corner) */
|
||||||
|
.sidenav .closebtn {
|
||||||
|
position: absolute;
|
||||||
|
top: 25px;
|
||||||
|
right: 25px;
|
||||||
|
margin-left: 50px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On smaller screens, where height is less than 450px, change the style of the sidenav (less padding and a smaller font size) */
|
||||||
|
@media screen and (max-height: 450px) {
|
||||||
|
.sidenav {padding-top: 15px;}
|
||||||
|
.sidenav a {font-size: 18px;}
|
||||||
|
}
|
||||||
|
|
||||||
|
#overlay {
|
||||||
|
position: fixed; /* Sit on top of the page content */
|
||||||
|
display: none; /* Hidden by default */
|
||||||
|
width: 100%; /* Full width (cover the whole page) */
|
||||||
|
height: 100%; /* Full height (cover the whole page) */
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0,0,0,0.5); /* Black background with opacity */
|
||||||
|
z-index: 999; /* Specify a stack order in case you're using a different order for other elements */
|
||||||
|
transition: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categoriesLinks {
|
||||||
|
background-color: #303f9f;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.2s ease-out;
|
||||||
|
}
|
||||||
80
src/main/ts2/src/app/header/header.component.ts
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { AuthService } from '../core/services/auth.service';
|
||||||
|
import { Category } from '../core/entities';
|
||||||
|
import { HeaderService } from './header.service';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
|
||||||
|
const SIDENAV_WIDTH = '300px';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-header',
|
||||||
|
templateUrl: './header.component.html',
|
||||||
|
styleUrls: ['./header.component.scss']
|
||||||
|
})
|
||||||
|
export class HeaderComponent implements OnInit {
|
||||||
|
isAdmin: boolean;
|
||||||
|
listCategories: Array<Category> = [];
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private headerService: HeaderService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.title = environment.title;
|
||||||
|
this.isAdmin = this.authService.isAdmin();
|
||||||
|
this.headerService.getAllCategories().subscribe(listCategories => {
|
||||||
|
this.listCategories = listCategories;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isAuthenticated(): boolean {
|
||||||
|
// console.log('Checking if user is connected... for header');
|
||||||
|
return this.authService.isAuthenticated();
|
||||||
|
}
|
||||||
|
|
||||||
|
openSidebar(): void {
|
||||||
|
document.getElementById('sidenav').style.width = SIDENAV_WIDTH;
|
||||||
|
document.getElementById('overlay').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
closeSidebar(): void {
|
||||||
|
document.getElementById('sidenav').style.width = '0';
|
||||||
|
document.getElementById('overlay').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect the user to the page to show the posts of the category which its id is in parameters.
|
||||||
|
*
|
||||||
|
* @param categoryId The id of the category which we need to show its posts after redirection.
|
||||||
|
*/
|
||||||
|
openPostsByCategory(categoryId: string): void {
|
||||||
|
this.closeSidebar();
|
||||||
|
|
||||||
|
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||||
|
this.router.navigateByUrl('/posts/byCategory/' + categoryId).then(() => {
|
||||||
|
this.router.navigated = false;
|
||||||
|
this.router.navigate([this.router.url]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the accordion which correspond to the category in parameters.
|
||||||
|
*
|
||||||
|
* @param category The category which its accodion needs to be open.
|
||||||
|
*/
|
||||||
|
openCategoriesLinks(category: Category): void {
|
||||||
|
const divCategoriesLinks = document.getElementById('category-' + category.id);
|
||||||
|
|
||||||
|
divCategoriesLinks.classList.toggle('active');
|
||||||
|
const divContent = divCategoriesLinks.nextElementSibling as HTMLElement;
|
||||||
|
if (divContent.style.maxHeight) {
|
||||||
|
divContent.style.maxHeight = null;
|
||||||
|
} else {
|
||||||
|
divContent.style.maxHeight = divContent.scrollHeight + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/main/ts2/src/app/header/header.service.ts
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { Category } from '../core/entities';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
|
||||||
|
const CATEGORY_URL = environment.apiUrl + '/api/categories/';
|
||||||
|
@Injectable()
|
||||||
|
export class HeaderService {
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
getAllCategories(): Observable<Array<Category>> {
|
||||||
|
return this.http.get<Array<Category>>(CATEGORY_URL);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/main/ts2/src/app/home/home.component.html
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
<div>
|
||||||
|
<h1>Derniers articles</h1>
|
||||||
|
<app-spinner *ngIf="!listArticle"></app-spinner>
|
||||||
|
<div *ngIf="listArticle" class="col-lg-8 offset-lg-2">
|
||||||
|
<app-post-card *ngFor="let post of listArticle" [post]="post"></app-post-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
3
src/main/ts2/src/app/home/home.component.scss
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
div.card {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
24
src/main/ts2/src/app/home/home.component.ts
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { HomeService } from './home.service';
|
||||||
|
import { Post } from '../core/entities';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-home',
|
||||||
|
templateUrl: './home.component.html',
|
||||||
|
styleUrls: ['./home.component.scss']
|
||||||
|
})
|
||||||
|
export class HomeComponent implements OnInit {
|
||||||
|
listArticle: Array<Post>;
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private homeService: HomeService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.homeService.getLastPosts().subscribe(lastPosts => {
|
||||||
|
this.listArticle = lastPosts;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/ts2/src/app/home/home.service.ts
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { Post } from '../core/entities';
|
||||||
|
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
|
||||||
|
const POSTS_URL = environment.apiUrl + '/api/posts';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HomeService {
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
getLastPosts(): Observable<Array<Post>> {
|
||||||
|
return this.http.get<Array<Post>>(POSTS_URL + '/last');
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/main/ts2/src/app/login/login.component.html
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
<div class="card col-md-8 offset-md-2 col-lg-6 offset-lg-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="card-title">Connexion</h4>
|
||||||
|
<form id="form" (ngSubmit)="onSubmit()" #loginForm="ngForm">
|
||||||
|
<div class="md-form">
|
||||||
|
<i class="fa fa-envelope prefix grey-text"></i>
|
||||||
|
<input mdbActive
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="model.email"
|
||||||
|
#email="ngModel"
|
||||||
|
data-error="Veuillez saisir une adresse email valide"
|
||||||
|
data-sucess=""
|
||||||
|
required />
|
||||||
|
<label for="email">Email</label>
|
||||||
|
</div>
|
||||||
|
<div class="md-form">
|
||||||
|
<i class="fa fa-lock prefix grey-text"></i>
|
||||||
|
<input mdbActive
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="model.password"
|
||||||
|
#password="ngModel"
|
||||||
|
data-error="Veuillez saisir votre mot de passe"
|
||||||
|
data-sucess=""
|
||||||
|
required />
|
||||||
|
<label for="password">Password</label>
|
||||||
|
</div>
|
||||||
|
<div id="errorMsg" class="card red lighten-2 text-center z-depth-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="white-text mb-0">{{loginError}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col submitFormArea">
|
||||||
|
<a routerLink="/signin" class="indigo-text">
|
||||||
|
Je n'ai pas de compte
|
||||||
|
</a>
|
||||||
|
<button class="float-right waves-effect waves-light indigo btn"
|
||||||
|
type="submit" [disabled]="!loginForm.form.valid">
|
||||||
|
Suivant
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
63
src/main/ts2/src/app/login/login.component.ts
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { User } from '../core/entities';
|
||||||
|
import { AuthService } from '../core/services/auth.service';
|
||||||
|
import { LoginService } from './login.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-login',
|
||||||
|
templateUrl: './login.component.html',
|
||||||
|
styles: [`
|
||||||
|
#form {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitFormArea {
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#errorMsg {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.5s ease-out;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`]
|
||||||
|
})
|
||||||
|
export class LoginComponent {
|
||||||
|
model: User = new User('', '', '', '', '', null, null, '');
|
||||||
|
loginError: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private loginService: LoginService,
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
this.loginError = undefined;
|
||||||
|
|
||||||
|
this.loginService.login(this.model).subscribe(user => {
|
||||||
|
// this.authService.setToken(user.token);
|
||||||
|
this.authService.setAuthenticated(true);
|
||||||
|
this.authService.setUser(user);
|
||||||
|
this.router.navigate(['/myPosts']);
|
||||||
|
}, error => {
|
||||||
|
this.setMessage('Email ou password incorrect.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessage(message: string): void {
|
||||||
|
this.loginError = message;
|
||||||
|
|
||||||
|
const resultMsgDiv = document.getElementById('errorMsg');
|
||||||
|
resultMsgDiv.style.maxHeight = '64px';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
resultMsgDiv.style.maxHeight = '0px';
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loginError = undefined;
|
||||||
|
}, 550);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/ts2/src/app/login/login.service.ts
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { RequestOptions } from '@angular/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { User } from '../core/entities';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
|
||||||
|
const LOGIN_URL = environment.apiUrl + '/api/account/login';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LoginService {
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
login(user: User): Observable<User> {
|
||||||
|
const options = { withCredentials: true };
|
||||||
|
return this.http.post<User>('/api/account/login', user, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/main/ts2/src/app/not-found/not-found.component.ts
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-not-found',
|
||||||
|
template: `
|
||||||
|
<div class="text-center">
|
||||||
|
<img src="./assets/images/404.png" alt="404" />
|
||||||
|
<h3>Page non trouvée...</h3>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class NotFoundComponent {}
|
||||||
7
src/main/ts2/src/app/posts/byCategory/by-category.component.html
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
<div>
|
||||||
|
<app-spinner *ngIf="!listPosts"></app-spinner>
|
||||||
|
<h1 *ngIf="listPosts">Catégorie {{category.name}}</h1>
|
||||||
|
<div *ngIf="listPosts" class="col-lg-8 offset-lg-2">
|
||||||
|
<app-post-card *ngFor="let post of listPosts" [post]="post"></app-post-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
31
src/main/ts2/src/app/posts/byCategory/by-category.component.ts
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
import { Post, Category } from '../../core/entities';
|
||||||
|
import { ByCategoryService } from './by-category.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-posts-by-category',
|
||||||
|
templateUrl: './by-category.component.html'
|
||||||
|
})
|
||||||
|
export class ByCategoryComponent implements OnInit {
|
||||||
|
category: Category;
|
||||||
|
listPosts: Array<Post>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private byCategoryService: ByCategoryService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
// Get the category
|
||||||
|
this.byCategoryService.getCategoryById(+this.route.snapshot.paramMap.get('categoryId')).subscribe(category => {
|
||||||
|
this.category = category;
|
||||||
|
|
||||||
|
// Get the posts by category
|
||||||
|
this.byCategoryService.getPostsByCategory(this.category).subscribe(listPosts => {
|
||||||
|
this.listPosts = listPosts;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/ts2/src/app/posts/byCategory/by-category.service.ts
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { Post, Category } from '../../core/entities';
|
||||||
|
|
||||||
|
const CATEGORIES_URL = environment.apiUrl + '/api/categories';
|
||||||
|
const POSTS_URL = environment.apiUrl + '/api/posts';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ByCategoryService {
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
getCategoryById(categoryId: number): Observable<Category> {
|
||||||
|
return this.http.get<Category>(`${CATEGORIES_URL}/${categoryId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPostsByCategory(category: Category): Observable<Array<Post>> {
|
||||||
|
return this.http.get<Array<Post>>(`${POSTS_URL}/byCategory/${category.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
240
src/main/ts2/src/app/posts/create-update/create-update-post.component.html
Executable file
@@ -0,0 +1,240 @@
|
|||||||
|
<div class="card hoverable">
|
||||||
|
<div class="card-header indigo white-text" mdbRippleRadius>
|
||||||
|
<h1 *ngIf="!model.key">Création d'un article</h1>
|
||||||
|
<h1 *ngIf="model.key">Modification de l'article {{model.key}}</h1>
|
||||||
|
<div class="row">
|
||||||
|
<a class="waves-light tabs"
|
||||||
|
[ngClass]="{'active': activatedTab === 'Édition'}"
|
||||||
|
(click)="activateEdition()">
|
||||||
|
Édition
|
||||||
|
</a>
|
||||||
|
<a class="waves-light tabs"
|
||||||
|
[ngClass]="{'active': activatedTab === 'Aperçu'}"
|
||||||
|
(click)="activatePreview()">
|
||||||
|
Aperçu
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div *ngIf="activatedTab === 'Édition'">
|
||||||
|
<div class="md-form">
|
||||||
|
<input mdbActive
|
||||||
|
id="title"
|
||||||
|
name="title"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="model.title"
|
||||||
|
#title="ngModel"
|
||||||
|
data-error="Veuillez saisir un titre d'article"
|
||||||
|
data-sucess=""
|
||||||
|
required />
|
||||||
|
<label for="title">Titre de l'article</label>
|
||||||
|
</div>
|
||||||
|
<div class="md-form">
|
||||||
|
<input mdbActive
|
||||||
|
id="image"
|
||||||
|
name="image"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="model.image"
|
||||||
|
#image="ngModel"
|
||||||
|
data-error="Veuillez saisir une adresse URL ou uploader une image"
|
||||||
|
data-success=""
|
||||||
|
required />
|
||||||
|
<label for="image">Image de l'article</label>
|
||||||
|
</div>
|
||||||
|
<div class="md-form">
|
||||||
|
<input mdbActive
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="model.description"
|
||||||
|
#description="ngModel"
|
||||||
|
data-error="Veuillez saisir la description de l'article"
|
||||||
|
data-success=""
|
||||||
|
required />
|
||||||
|
<label for="description">Description de l'article</label>
|
||||||
|
</div>
|
||||||
|
<div class="input-group mb-3 wrap">
|
||||||
|
<div class="select">
|
||||||
|
<select id="category" class="select-text" [(ngModel)]="model.category" required>
|
||||||
|
<option value="" disabled selected></option>
|
||||||
|
<option *ngFor="let category of listCategories" [ngValue]="category">{{category.name}}</option>
|
||||||
|
</select>
|
||||||
|
<span class="select-highlight"></span>
|
||||||
|
<span class="select-bar"></span>
|
||||||
|
<label class="select-label">Catégorie de l'article</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="toolbox" class="row">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-floating waves-light"
|
||||||
|
(click)="injectHeader('h1')"
|
||||||
|
mdbTooltip="Titre 1"
|
||||||
|
placement="bottom"
|
||||||
|
mdbRippleRadius>H1</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-floating waves-light"
|
||||||
|
(click)="injectHeader('h2')"
|
||||||
|
mdbTooltip="Titre 2"
|
||||||
|
placement="bottom"
|
||||||
|
mdbRippleRadius>H2</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-floating waves-light"
|
||||||
|
(click)="injectHeader('h3')"
|
||||||
|
mdbTooltip="Titre 3"
|
||||||
|
placement="bottom"
|
||||||
|
mdbRippleRadius>H3</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-floating waves-light"
|
||||||
|
(click)="openImagesModal()"
|
||||||
|
mdbTooltip="Image"
|
||||||
|
placement="bottom"
|
||||||
|
mdbRippleRadius>
|
||||||
|
<i class="fa fa-image"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-floating waves-light"
|
||||||
|
(click)="injectLink()"
|
||||||
|
mdbTooltip="Lien"
|
||||||
|
placement="bottom"
|
||||||
|
mdbRippleRadius>
|
||||||
|
<i class="fa fa-link"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-floating waves-light"
|
||||||
|
(click)="frameCode.show()"
|
||||||
|
mdbTooltip="Extrait de code"
|
||||||
|
placement="bottom"
|
||||||
|
mdbRippleRadius>
|
||||||
|
<i class="fa fa-code"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="md-form">
|
||||||
|
<textarea mdbActive
|
||||||
|
id="text"
|
||||||
|
name="text"
|
||||||
|
type="text"
|
||||||
|
class="md-textarea"
|
||||||
|
[(ngModel)]="model.text"
|
||||||
|
#text="ngModel"
|
||||||
|
data-error="Veuillez saisir le contenu de l'article"
|
||||||
|
data-sucess=""
|
||||||
|
required>
|
||||||
|
</textarea>
|
||||||
|
<label for="text">Contenu de l'article</label>
|
||||||
|
</div>
|
||||||
|
<div id="errorMsg" class="card red lighten-2 text-center z-depth-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="white-text mb-0">{{modelError}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="resultMsg" class="card green lighten-2 text-center z-depth-2" >
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="white-text mb-0">{{result}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="activatedTab === 'Aperçu'">
|
||||||
|
<app-spinner *ngIf="!parsedPost"></app-spinner>
|
||||||
|
<div class="card" *ngIf="parsedPost">
|
||||||
|
<img [src]="parsedPost.image" class="img-fluid" alt="Post image">
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="card-title">{{parsedPost.title}}</h1>
|
||||||
|
<h4>{{parsedPost.description}}</h4>
|
||||||
|
<hr/>
|
||||||
|
<div [innerHTML]="getContent()"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="footer">
|
||||||
|
<a routerLink="/myPosts">Annuler</a>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-primary waves-light float-right"
|
||||||
|
(click)="save()" mdbRippleRadius>Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div mdbModal #frameCode="mdb-modal" class="modal fade top"
|
||||||
|
tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-full-height modal-top" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title w-100" id="myModalLabel">Ajout de code</h4>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="frameCode.hide()">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="wrap">
|
||||||
|
<div class="select">
|
||||||
|
<select id="languageTmp" class="select-text" [(ngModel)]="languageTmp" required>
|
||||||
|
<option value="" disabled selected></option>
|
||||||
|
<option value="java">Java</option>
|
||||||
|
<option value="python">Python</option>
|
||||||
|
<option value="markup">html/xml</option>
|
||||||
|
<option value="sql">SQL</option>
|
||||||
|
<option value="bash">Bash</option>
|
||||||
|
</select>
|
||||||
|
<span class="select-highlight"></span>
|
||||||
|
<span class="select-bar"></span>
|
||||||
|
<label class="select-label">Langage de programmation</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="md-form" style="margin-top: 15px; margin-bottom: 0;">
|
||||||
|
<textarea mdbActive
|
||||||
|
id="codeTmp"
|
||||||
|
name="codeTmp"
|
||||||
|
type="text"
|
||||||
|
class="md-textarea"
|
||||||
|
[(ngModel)]="codeTmp"
|
||||||
|
required>
|
||||||
|
</textarea>
|
||||||
|
<label for="codeTmp">Extrait de code</label>
|
||||||
|
</div>
|
||||||
|
<div class="card red lighten-1 text-center z-depth-2" [hidden]="!codeError" style="margin-bottom: 0;">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="white-text mb-0">Le langage et l'extrait de code doivent être renseignés.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-primary waves-effect waves-light"
|
||||||
|
data-dismiss="modal" (click)="frameCode.hide()">Fermer</button>
|
||||||
|
<button type="button" class="btn btn-primary waves-effect waves-light"
|
||||||
|
(click)="injectCode()">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div mdbModal #frameImages="mdb-modal" class="modal fade top"
|
||||||
|
tabindex="-1" role="dialog" aria-labelledby="frameImagesLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-full-height modal-top" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title w-100" id="frameImagesLabel">Ajout d'image</h4>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="frameCode.hide()">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<app-spinner *ngIf="!imagesLoaded"></app-spinner>
|
||||||
|
<div id="image-div" *ngIf="imagesLoaded">
|
||||||
|
<img *ngFor="let img of listImages"
|
||||||
|
[src]="getLinkSrc(img.link)"
|
||||||
|
class="uploaded-image hoverable"
|
||||||
|
(click)="injectImage(img.link)" />
|
||||||
|
</div>
|
||||||
|
<a class="fixed-action-btn green white-text" *ngIf="imagesLoaded" (click)="openNewImageInput()">+</a>
|
||||||
|
<input id="newImageInput" type="file" (change)="uploadImage($event)" [hidden]="true">
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-primary waves-effect waves-light"
|
||||||
|
data-dismiss="modal" (click)="frameImages.hide()">Fermer</button>
|
||||||
|
<button type="button" class="btn btn-primary waves-effect waves-light"
|
||||||
|
(click)="injectCode()">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
237
src/main/ts2/src/app/posts/create-update/create-update-post.component.scss
Executable file
@@ -0,0 +1,237 @@
|
|||||||
|
.card {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
padding-top: 24px;
|
||||||
|
padding-left: 24px;
|
||||||
|
padding-right: 24px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
border-width: 0px;
|
||||||
|
}
|
||||||
|
.tabs {
|
||||||
|
width: 50%;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 62px;
|
||||||
|
}
|
||||||
|
.tabs.active {
|
||||||
|
border-bottom: 4px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
height: 250px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select {
|
||||||
|
border: 0px;
|
||||||
|
border-bottom: 1px #aaa solid;
|
||||||
|
border-radius: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select:focus {
|
||||||
|
-webkit-box-shadow: 0;
|
||||||
|
box-shadow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-form {
|
||||||
|
margin-bottom: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer {
|
||||||
|
line-height: 57px;
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toolbox {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-floating {
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 2px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background-color: #3f51b5;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 5px 11px 0 rgba(0,0,0,.18), 0 4px 15px 0 rgba(0,0,0,.15);
|
||||||
|
transition: box-shadow 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-floating:hover {
|
||||||
|
box-shadow: 0 8px 17px 0 rgba(0,0,0,.2), 0 6px 20px 0 rgba(0,0,0,.19);
|
||||||
|
}
|
||||||
|
|
||||||
|
#text {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#resultMsg, #errorMsg {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.5s ease-out;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
top: 40%;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* select starting stylings ------------------------------*/
|
||||||
|
.select {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-text {
|
||||||
|
position: relative;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: transparent;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 10px 10px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #bdbdbd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove focus */
|
||||||
|
.select-text:focus {
|
||||||
|
outline: none;
|
||||||
|
border-bottom: 1px solid rgba(0,0,0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Use custom arrow */
|
||||||
|
.select .select-text {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance:none
|
||||||
|
}
|
||||||
|
|
||||||
|
.select:after {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 10px;
|
||||||
|
/* Styling the down arrow */
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
padding: 0;
|
||||||
|
content: '';
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-top: 6px solid #bdbdbd;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* LABEL ======================================= */
|
||||||
|
.select-label {
|
||||||
|
color: #757575;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: normal;
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
left: 0;
|
||||||
|
top: -5px;
|
||||||
|
transition: 0.2s ease all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* active state */
|
||||||
|
.select-text:focus ~ .select-label {
|
||||||
|
color: #2F80ED;
|
||||||
|
}
|
||||||
|
.select-text:focus ~ .select-label, .select-text:valid ~ .select-label {
|
||||||
|
top: -20px;
|
||||||
|
transition: 0.2s ease all;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* BOTTOM BARS ================================= */
|
||||||
|
.select-bar {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-bar:before, .select-bar:after {
|
||||||
|
content: '';
|
||||||
|
height: 2px;
|
||||||
|
width: 0;
|
||||||
|
bottom: 1px;
|
||||||
|
position: absolute;
|
||||||
|
background: #2F80ED;
|
||||||
|
transition: 0.2s ease all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-bar:before {
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-bar:after {
|
||||||
|
right: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* active state */
|
||||||
|
.select-text:focus ~ .select-bar:before, .select-text:focus ~ .select-bar:after {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HIGHLIGHTER ================================== */
|
||||||
|
.select-highlight {
|
||||||
|
position: absolute;
|
||||||
|
height: 60%;
|
||||||
|
width: 40%;
|
||||||
|
top: 25%;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
$btnSize: 45px;
|
||||||
|
|
||||||
|
.fixed-action-btn {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 40px;
|
||||||
|
z-index: 997;
|
||||||
|
width: $btnSize;
|
||||||
|
height: $btnSize;
|
||||||
|
border-radius: 50%;
|
||||||
|
line-height: $btnSize;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 25px;
|
||||||
|
box-shadow: 0 5px 11px 0 rgba(0,0,0,.18), 0 4px 15px 0 rgba(0,0,0,.15);
|
||||||
|
transition: box-shadow 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.fixed-action-btn:hover {
|
||||||
|
box-shadow: 0 8px 17px 0 rgba(0,0,0,.2), 0 6px 20px 0 rgba(0,0,0,.19);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#image-div {
|
||||||
|
height: 60vh;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uploaded-image {
|
||||||
|
display: inline-flex;
|
||||||
|
position: relative;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2);
|
||||||
|
cursor:pointer;
|
||||||
|
margin-right: 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
232
src/main/ts2/src/app/posts/create-update/create-update-post.component.ts
Executable file
@@ -0,0 +1,232 @@
|
|||||||
|
import { Component, OnInit, SecurityContext, ViewChild } from '@angular/core';
|
||||||
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
|
import { Router, ActivatedRoute, RoutesRecognized } from '@angular/router';
|
||||||
|
|
||||||
|
import { Post, Category, Image } from '../../core/entities';
|
||||||
|
import { AuthService } from '../../core/services/auth.service';
|
||||||
|
import { CreateUpdatePostService } from './create-update-post.service';
|
||||||
|
|
||||||
|
import { filter } from 'rxjs/operators';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { HttpEventType, HttpResponse } from '@angular/common/http';
|
||||||
|
|
||||||
|
enum Tabs {
|
||||||
|
EDITION = 'Édition',
|
||||||
|
PREVIEW = 'Aperçu'
|
||||||
|
}
|
||||||
|
|
||||||
|
declare let Prism: any;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-create-update-post',
|
||||||
|
templateUrl: './create-update-post.component.html',
|
||||||
|
styleUrls: ['./create-update-post.component.scss']
|
||||||
|
})
|
||||||
|
export class CreateUpdatePostComponent implements OnInit {
|
||||||
|
static INPUT_POST_TEXT = 'text';
|
||||||
|
|
||||||
|
@ViewChild('frameCode') public contentModal;
|
||||||
|
@ViewChild('frameImages') public imagesModal;
|
||||||
|
|
||||||
|
model: Post = new Post('', '', '', '', '', null, null, null);
|
||||||
|
parsedPost: Post;
|
||||||
|
|
||||||
|
listCategories: Array<Category>;
|
||||||
|
|
||||||
|
activatedTab: string;
|
||||||
|
|
||||||
|
modelError: string;
|
||||||
|
result: string;
|
||||||
|
|
||||||
|
// Variables for the code popup
|
||||||
|
codeTmp: string;
|
||||||
|
languageTmp: string;
|
||||||
|
codeError: string;
|
||||||
|
|
||||||
|
// Variables for the images popup
|
||||||
|
imagesLoaded: boolean;
|
||||||
|
listImages: Array<Image>;
|
||||||
|
selectedFiles: FileList;
|
||||||
|
currentFileUpload: File;
|
||||||
|
progress: { percentage: number } = { percentage: 0 };
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private createUpdatePostService: CreateUpdatePostService,
|
||||||
|
private activatedRoute: ActivatedRoute,
|
||||||
|
private sanitizer: DomSanitizer,
|
||||||
|
private router: Router,
|
||||||
|
private authService: AuthService
|
||||||
|
) {
|
||||||
|
this.imagesLoaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.listCategories = [];
|
||||||
|
this.activatedTab = Tabs.EDITION;
|
||||||
|
this.createUpdatePostService.getCategories().subscribe(listCategories => {
|
||||||
|
this.listCategories = listCategories.filter(category => !category.listSubCategories.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
const postKey = this.activatedRoute.snapshot.paramMap.get('postKey');
|
||||||
|
if (postKey) {
|
||||||
|
this.createUpdatePostService.getPost(postKey).subscribe(post => {
|
||||||
|
if (post.author.key === this.authService.getUser().key) {
|
||||||
|
this.model = post;
|
||||||
|
} else {
|
||||||
|
this.router.navigate(['/forbidden']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// this.router.events.filter(e => e instanceof RoutesRecognized).pairwise().subscribe((event: any[]) => {
|
||||||
|
// if (event[0].urlAfterRedirects === '/posts/new') {
|
||||||
|
// this.result = 'Article créé.';
|
||||||
|
// setTimeout(() => {
|
||||||
|
// this.result = undefined;
|
||||||
|
// }, 3500);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activateEdition(): void {
|
||||||
|
this.activatedTab = Tabs.EDITION;
|
||||||
|
}
|
||||||
|
|
||||||
|
activatePreview(): void {
|
||||||
|
this.activatedTab = Tabs.PREVIEW;
|
||||||
|
this.parsedPost = undefined;
|
||||||
|
this.createUpdatePostService.processPreview(this.model).subscribe(parsedPost => {
|
||||||
|
this.parsedPost = parsedPost;
|
||||||
|
setTimeout(() => {
|
||||||
|
Prism.highlightAll();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getContent(): string {
|
||||||
|
return this.sanitizer.sanitize(SecurityContext.HTML, this.parsedPost.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
injectHeader(header: string): void {
|
||||||
|
this.injectElement('[' + header + '][/' + header + ']', 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
injectLink(): void {
|
||||||
|
this.injectElement('[link href="" txt="" /]', 11);
|
||||||
|
}
|
||||||
|
|
||||||
|
injectCode(): void {
|
||||||
|
if (this.languageTmp && this.codeTmp) {
|
||||||
|
const codeExtract = '\n[code lg="' + this.languageTmp + '"]\n'
|
||||||
|
+ this.codeTmp + '\n[/code]\n\n';
|
||||||
|
|
||||||
|
this.injectElement(codeExtract, codeExtract.length);
|
||||||
|
|
||||||
|
this.contentModal.hide();
|
||||||
|
|
||||||
|
this.codeTmp = undefined;
|
||||||
|
this.languageTmp = undefined;
|
||||||
|
this.resetSelect('languageTmp');
|
||||||
|
} else {
|
||||||
|
this.codeError = 'Le langage et l\'extrait de code doivent être renseignés.';
|
||||||
|
setTimeout(() => {
|
||||||
|
this.codeError = undefined;
|
||||||
|
}, 3500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private injectElement(elementToInject: string, lengthForCursor: number): void {
|
||||||
|
const input = <HTMLInputElement>document.getElementById(CreateUpdatePostComponent.INPUT_POST_TEXT);
|
||||||
|
const contentValue = input.value;
|
||||||
|
const cursorPosition = input.selectionStart;
|
||||||
|
|
||||||
|
this.model.text = contentValue.slice(0, cursorPosition) + elementToInject + contentValue.slice(cursorPosition);
|
||||||
|
input.focus();
|
||||||
|
const newCursor = cursorPosition + lengthForCursor;
|
||||||
|
input.selectionStart = newCursor;
|
||||||
|
input.selectionEnd = newCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resetSelect(selectId: string): void {
|
||||||
|
const select = <HTMLSelectElement>document.getElementById(selectId);
|
||||||
|
select.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
save(): void {
|
||||||
|
if (this.model.title && this.model.image && this.model.description && this.model.category && this.model.text) {
|
||||||
|
this.model.author = this.authService.getUser();
|
||||||
|
|
||||||
|
if (this.model.key) {
|
||||||
|
this.createUpdatePostService.updatePost(this.model).subscribe(post => {
|
||||||
|
this.setMessage('Modification enregistrée', false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.createUpdatePostService.addPost(this.model).subscribe(post => {
|
||||||
|
this.router.navigate([`/posts/update/${post.key}`]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.setMessage('Veuillez saisir les champs obligatoires.', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessage(message: string, error: boolean): void {
|
||||||
|
this[error ? 'modelError' : 'result'] = message;
|
||||||
|
|
||||||
|
const resultMsgDiv = document.getElementById(error ? 'errorMsg' : 'resultMsg');
|
||||||
|
resultMsgDiv.style.maxHeight = '64px';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
resultMsgDiv.style.maxHeight = '0px';
|
||||||
|
setTimeout(() => {
|
||||||
|
this[error ? 'modelError' : 'result'] = undefined;
|
||||||
|
}, 550);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
openImagesModal(): void {
|
||||||
|
this.imagesLoaded = false;
|
||||||
|
this.imagesModal.show();
|
||||||
|
|
||||||
|
this.createUpdatePostService.getImages().subscribe(listImages => {
|
||||||
|
this.listImages = listImages;
|
||||||
|
this.imagesLoaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getLinkSrc(pLink: string): string {
|
||||||
|
return `${environment.apiUrl}/api/images/${pLink}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
openNewImageInput(): void {
|
||||||
|
document.getElementById('newImageInput').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadImage(event): void {
|
||||||
|
this.selectedFiles = event.target.files;
|
||||||
|
this.progress.percentage = 0;
|
||||||
|
|
||||||
|
this.currentFileUpload = this.selectedFiles.item(0);
|
||||||
|
// This prevents error 400 if user doesn't select any file to upload and close the input file.
|
||||||
|
if (this.currentFileUpload) {
|
||||||
|
this.createUpdatePostService.uploadPicture(this.currentFileUpload).subscribe(result => {
|
||||||
|
if (result.type === HttpEventType.UploadProgress) {
|
||||||
|
this.progress.percentage = Math.round(100 * result.loaded / result.total);
|
||||||
|
} else if (result instanceof HttpResponse) {
|
||||||
|
console.log('File ' + result.body + ' completely uploaded!');
|
||||||
|
this.createUpdatePostService.getImageDetails(result.body as string).subscribe(image => {
|
||||||
|
this.listImages.push(image);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.selectedFiles = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
injectImage(pImageLink: string): void {
|
||||||
|
const imgTag = `[img src="${this.getLinkSrc(pImageLink)}" /]`;
|
||||||
|
this.injectElement(imgTag, imgTag.length);
|
||||||
|
this.imagesModal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/main/ts2/src/app/posts/create-update/create-update-post.service.ts
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { HttpClient, HttpEvent, HttpRequest } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { Post, Category, Image } from '../../core/entities';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
|
||||||
|
const IMAGES_URL = environment.apiUrl + '/api/images';
|
||||||
|
const POSTS_URL = environment.apiUrl + '/api/posts';
|
||||||
|
const CATEGORIES_URL = environment.apiUrl + '/api/categories';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CreateUpdatePostService {
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
processPreview(post: Post): Observable<Post> {
|
||||||
|
return this.http.post<Post>(`${POSTS_URL}/preview`, post);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCategories(): Observable<Array<Category>> {
|
||||||
|
return this.http.get<Array<Category>>(`${CATEGORIES_URL}/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
addPost(post: Post): Observable<Post> {
|
||||||
|
return this.http.post<Post>(`${POSTS_URL}/`, post);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePost(post: Post): Observable<Post> {
|
||||||
|
return this.http.put<Post>(`${POSTS_URL}/`, post);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPost(postKey: string): Observable<Post> {
|
||||||
|
return this.http.get<Post>(`${POSTS_URL}/${postKey}/source`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getImages(): Observable<Array<Image>> {
|
||||||
|
return this.http.get<Array<Image>>(`${IMAGES_URL}/myImages`);
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadPicture(file: File): Observable<HttpEvent<{}>> {
|
||||||
|
const formData: FormData = new FormData();
|
||||||
|
|
||||||
|
formData.append('file', file);
|
||||||
|
|
||||||
|
return this.http.request(new HttpRequest(
|
||||||
|
'POST', IMAGES_URL, formData, {
|
||||||
|
reportProgress: true,
|
||||||
|
responseType: 'text'
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
getImageDetails(imageLink: string): Observable<Image> {
|
||||||
|
return this.http.get<Image>(`${IMAGES_URL}/${imageLink}/details`);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/main/ts2/src/app/posts/myPosts/my-posts.component.html
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
<div>
|
||||||
|
<h1>Mes articles</h1>
|
||||||
|
<app-spinner *ngIf="!listPosts"></app-spinner>
|
||||||
|
<div *ngIf="listPosts" class="col-lg-8 offset-lg-2">
|
||||||
|
<app-post-card *ngFor="let post of listPosts" [post]="post"></app-post-card>
|
||||||
|
</div>
|
||||||
|
<a routerLink="/posts/new" class="fixed-action-btn green white-text">+</a>
|
||||||
|
</div>
|
||||||
20
src/main/ts2/src/app/posts/myPosts/my-posts.component.scss
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
$btnSize: 55px;
|
||||||
|
|
||||||
|
.fixed-action-btn {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 23px;
|
||||||
|
right: 23px;
|
||||||
|
z-index: 997;
|
||||||
|
width: $btnSize;
|
||||||
|
height: $btnSize;
|
||||||
|
border-radius: 50%;
|
||||||
|
line-height: $btnSize;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 30px;
|
||||||
|
box-shadow: 0 5px 11px 0 rgba(0,0,0,.18), 0 4px 15px 0 rgba(0,0,0,.15);
|
||||||
|
transition: box-shadow 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.fixed-action-btn:hover {
|
||||||
|
box-shadow: 0 8px 17px 0 rgba(0,0,0,.2), 0 6px 20px 0 rgba(0,0,0,.19);
|
||||||
|
}
|
||||||
23
src/main/ts2/src/app/posts/myPosts/my-posts.component.ts
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
|
||||||
|
import { Post } from '../../core/entities';
|
||||||
|
import { MyPostsService } from './my-posts.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-my-posts',
|
||||||
|
templateUrl: './my-posts.component.html',
|
||||||
|
styleUrls: ['./my-posts.component.scss']
|
||||||
|
})
|
||||||
|
export class MyPostsComponent implements OnInit {
|
||||||
|
listPosts: Array<Post>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private myPostsService: MyPostsService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.myPostsService.getMyPosts().subscribe(listPosts => {
|
||||||
|
this.listPosts = listPosts;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/ts2/src/app/posts/myPosts/my-posts.service.ts
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { Post } from '../../core/entities';
|
||||||
|
|
||||||
|
const POSTS_URL = environment.apiUrl + '/api/posts';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MyPostsService {
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
getMyPosts(): Observable<Array<Post>> {
|
||||||
|
return this.http.get<Array<Post>>(`${POSTS_URL}/myPosts`);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/main/ts2/src/app/posts/post.component.html
Executable file
@@ -0,0 +1,67 @@
|
|||||||
|
<div id="content" *ngIf="!notFound">
|
||||||
|
<app-spinner *ngIf="!loaded"></app-spinner>
|
||||||
|
<div class="card" *ngIf="loaded">
|
||||||
|
<img [src]="post?.image" class="img-fluid" alt="Post image">
|
||||||
|
|
||||||
|
<a *ngIf="owned" class="btn-card-floating waves-light white-text"
|
||||||
|
routerLink="/posts/update/{{post.key}}">
|
||||||
|
<i class="fa fa-pencil"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<h1 class="card-title">{{post.title}}</h1>
|
||||||
|
<h4>{{post.description}}</h4>
|
||||||
|
<hr/>
|
||||||
|
<div [innerHTML]="getContent()"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-data">
|
||||||
|
<img [src]="getAvatarUrl()"
|
||||||
|
class="author-img"
|
||||||
|
[alt]="post.author.name"
|
||||||
|
[mdbTooltip]="post.author.name"
|
||||||
|
placement="bottom"/>
|
||||||
|
Article écrit par {{post.author.name}}
|
||||||
|
<span class="creation-date-area">({{post.creationDate | date:'yyyy-MM-dd HH:mm:ss'}})</span>
|
||||||
|
<button *ngIf="owned" type="button" class="btn btn-danger waves-light float-right" (click)="alertDelete.show()" mdbRippleRadius>
|
||||||
|
<i class="fa fa-trash mr-1"></i> Supprimer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div mdbModal #alertDelete="mdb-modal" class="modal fade" role="dialog" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-notify modal-danger" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<p class="heading lead">Suppression de l'article</p>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close" (click)="alertDelete.hide()">
|
||||||
|
<span aria-hidden="true" class="white-text">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="text-center">
|
||||||
|
<p>Êtes vous sûr de vouloir supprimer cet article ?</p>
|
||||||
|
<p *ngIf="postDeletionFailed" class="red-text">
|
||||||
|
Une erreur est survenue lors de la suppression de l'article.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<a type="button"
|
||||||
|
class="btn btn-outline-secondary-modal"
|
||||||
|
data-dismiss="modal"
|
||||||
|
(click)="alertDelete.hide()"
|
||||||
|
mdbRippleRadius>
|
||||||
|
Annuler
|
||||||
|
</a>
|
||||||
|
<a type="button" mdbRippleRadius
|
||||||
|
class="btn btn-primary-modal waves-light"
|
||||||
|
(click)="deletePost()">
|
||||||
|
<i class="fa fa-trash"></i> Supprimer
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<app-not-found *ngIf="notFound"></app-not-found>
|
||||||
107
src/main/ts2/src/app/posts/post.component.ts
Executable file
@@ -0,0 +1,107 @@
|
|||||||
|
import { Component, OnInit, SecurityContext, ViewChild } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { Post, User } from '../core/entities';
|
||||||
|
import { PostService } from './post.service';
|
||||||
|
import { AuthService } from '../core/services/auth.service';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
|
||||||
|
declare let Prism: any;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-post',
|
||||||
|
templateUrl: './post.component.html',
|
||||||
|
styles: [`
|
||||||
|
.author-img {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
.card .card-data {
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
#content {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
.creation-date-area {
|
||||||
|
color: #bdbdbd;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
`]
|
||||||
|
})
|
||||||
|
export class PostComponent implements OnInit {
|
||||||
|
post: Post = new Post('', '', '', '', '', null, new User('', '', '', '', '', null, null, ''), null);
|
||||||
|
loaded: boolean;
|
||||||
|
notFound: boolean;
|
||||||
|
owned: boolean;
|
||||||
|
|
||||||
|
@ViewChild('alertDelete') alertDelete;
|
||||||
|
|
||||||
|
postDeletionFailed: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private activatedRoute: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private postService: PostService,
|
||||||
|
private sanitizer: DomSanitizer,
|
||||||
|
private authService: AuthService
|
||||||
|
) {
|
||||||
|
this.loaded = false;
|
||||||
|
this.owned = false;
|
||||||
|
this.postDeletionFailed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.postService.getPost(this.activatedRoute.snapshot.paramMap.get('postKey')).subscribe(post => {
|
||||||
|
this.post = post;
|
||||||
|
this.loaded = true;
|
||||||
|
this.owned = this.isOwned();
|
||||||
|
setTimeout(() => {
|
||||||
|
Prism.highlightAll();
|
||||||
|
}, 100);
|
||||||
|
}, error => {
|
||||||
|
if (error instanceof HttpErrorResponse && error.status === 404) {
|
||||||
|
this.notFound = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private isOwned(): boolean {
|
||||||
|
let result = false;
|
||||||
|
|
||||||
|
const connectedUser = this.authService.getUser();
|
||||||
|
if (connectedUser) {
|
||||||
|
result = this.post.author.key === connectedUser.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
getContent(): SafeHtml {
|
||||||
|
return this.sanitizer.sanitize(SecurityContext.HTML, this.post.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAvatarUrl(): string {
|
||||||
|
return this.post.author.image
|
||||||
|
? `${environment.apiUrl}/api/images/loadAvatar/${this.post.author.image}`
|
||||||
|
: './assets/images/default_user.png';
|
||||||
|
}
|
||||||
|
|
||||||
|
deletePost(): void {
|
||||||
|
this.postDeletionFailed = false;
|
||||||
|
|
||||||
|
this.postService.deletePost(this.post).subscribe(() => {
|
||||||
|
this.alertDelete.hide();
|
||||||
|
this.router.navigate(['/myPosts']);
|
||||||
|
}, error => {
|
||||||
|
this.postDeletionFailed = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.postDeletionFailed = false;
|
||||||
|
}, 3500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/main/ts2/src/app/posts/post.service.ts
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { Post } from '../core/entities';
|
||||||
|
|
||||||
|
const POSTS_URL = environment.apiUrl + '/api/posts';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PostService {
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
getPost(postKey: string): Observable<Post> {
|
||||||
|
return this.http.get<Post>(`${POSTS_URL}/${postKey}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
deletePost(postToDelete: Post): Observable<any> {
|
||||||
|
return this.http.delete(`${POSTS_URL}/${postToDelete.key}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
src/main/ts2/src/app/search/search.component.ts
Executable file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Post } from '../core/entities';
|
||||||
|
import { SearchService } from './search.service';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-search',
|
||||||
|
template: `
|
||||||
|
<div>
|
||||||
|
<h1>Résultats de la recherche</h1>
|
||||||
|
<app-spinner *ngIf="!listArticle"></app-spinner>
|
||||||
|
<div *ngIf="listArticle" class="col-lg-8 offset-lg-2">
|
||||||
|
<app-post-card *ngFor="let post of listArticle" [post]="post"></app-post-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class SearchComponent implements OnInit {
|
||||||
|
searchLoading: boolean;
|
||||||
|
listArticle: Array<Post>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private activatedRoute: ActivatedRoute,
|
||||||
|
private searchService: SearchService
|
||||||
|
) {
|
||||||
|
this.searchLoading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.searchService.search(this.activatedRoute.snapshot.paramMap.get('searchCriteria'))
|
||||||
|
.subscribe(listArticle => {
|
||||||
|
this.listArticle = listArticle;
|
||||||
|
this.searchLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/ts2/src/app/search/search.service.ts
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { Post } from '../core/entities';
|
||||||
|
|
||||||
|
const POSTS_URL = environment.apiUrl + '/api/posts';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SearchService {
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
search(searchCriteria: string): Observable<Array<Post>> {
|
||||||
|
return this.http.get<Array<Post>>(`${POSTS_URL}/search/${searchCriteria}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/main/ts2/src/app/signin/signin.component.html
Executable file
@@ -0,0 +1,76 @@
|
|||||||
|
<div class="card col-md-8 offset-md-2 col-lg-6 offset-lg-3">
|
||||||
|
<div id="form" class="card-body">
|
||||||
|
<h4 class="card-title">Inscrition</h4>
|
||||||
|
<form (ngSubmit)="onSubmit()" #signinForm="ngForm">
|
||||||
|
<div class="md-form">
|
||||||
|
<i class="fa fa-id-badge prefix grey-text"></i>
|
||||||
|
<input mdbActive
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="model.name"
|
||||||
|
#name="ngModel"
|
||||||
|
data-error="Veuillez saisir un nom d'utilisateur"
|
||||||
|
data-sucess=""
|
||||||
|
required />
|
||||||
|
<label for="name">Nom d'utilisateur</label>
|
||||||
|
</div>
|
||||||
|
<div class="md-form">
|
||||||
|
<i class="fa fa-envelope prefix grey-text"></i>
|
||||||
|
<input mdbActive
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="model.email"
|
||||||
|
#email="ngModel"
|
||||||
|
data-error="Veuillez saisir une adresse email valide"
|
||||||
|
data-sucess=""
|
||||||
|
required />
|
||||||
|
<label for="email">Email</label>
|
||||||
|
</div>
|
||||||
|
<div class="md-form">
|
||||||
|
<i class="fa fa-lock prefix grey-text"></i>
|
||||||
|
<input mdbActive
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="model.password"
|
||||||
|
#password="ngModel"
|
||||||
|
data-error="Veuillez saisir votre mot de passe"
|
||||||
|
data-sucess=""
|
||||||
|
required />
|
||||||
|
<label for="password">Password</label>
|
||||||
|
</div>
|
||||||
|
<div class="md-form">
|
||||||
|
<i class="fa fa-lock prefix grey-text"></i>
|
||||||
|
<input mdbActive
|
||||||
|
id="confirmPassword"
|
||||||
|
name="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
[(ngModel)]="confirmPassword"
|
||||||
|
data-error="Veuillez confirmer votre mot de passe"
|
||||||
|
data-sucess=""
|
||||||
|
required />
|
||||||
|
<label for="confirmPassword">Confirmez votre mot de passe</label>
|
||||||
|
</div>
|
||||||
|
<div id="errorMsg" class="card red lighten-2 text-center z-depth-2">
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="white-text mb-0">{{errorMsg}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col submitFormArea">
|
||||||
|
<a routerLink="/login" class="indigo-text">
|
||||||
|
Connexion
|
||||||
|
</a>
|
||||||
|
<button class="float-right waves-effect waves-light indigo btn"
|
||||||
|
type="submit" [disabled]="!signinForm.form.valid">
|
||||||
|
Suivant
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
68
src/main/ts2/src/app/signin/signin.component.ts
Executable file
@@ -0,0 +1,68 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { User } from '../core/entities';
|
||||||
|
import { SigninService } from './signin.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-signin',
|
||||||
|
templateUrl: './signin.component.html',
|
||||||
|
styles: [`
|
||||||
|
#form {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitFormArea {
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#errorMsg {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.5s ease-out;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
`]
|
||||||
|
})
|
||||||
|
export class SigninComponent {
|
||||||
|
model: User = new User('', '', '', '', '', null, null, '');
|
||||||
|
confirmPassword: string;
|
||||||
|
errorMsg: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private signinService: SigninService,
|
||||||
|
private router: Router
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
if (this.confirmPassword && this.confirmPassword === this.model.password) {
|
||||||
|
this.signinService.signin(this.model).subscribe(user => {
|
||||||
|
this.router.navigate(['/login']);
|
||||||
|
}, error => {
|
||||||
|
switch (error.status) {
|
||||||
|
case 409:
|
||||||
|
this.setMessage('L\'adresse mail saisie n\'est pas disponible');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.setMessage('Une erreur est survenue lors de l\'inscription, veuillez réessayer plus tard');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setMessage('Les mots de passe saisis ne correspondent pas');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessage(message: string): void {
|
||||||
|
this.errorMsg = message;
|
||||||
|
|
||||||
|
const resultMsgDiv = document.getElementById('errorMsg');
|
||||||
|
resultMsgDiv.style.maxHeight = '64px';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
resultMsgDiv.style.maxHeight = '0px';
|
||||||
|
setTimeout(() => {
|
||||||
|
this.errorMsg = undefined;
|
||||||
|
}, 550);
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/ts2/src/app/signin/signin.service.ts
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { User } from '../core/entities';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
|
||||||
|
const SIGNIN_URL = environment.apiUrl + '/api/account/signin';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SigninService {
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
signin(user: User): Observable<User> {
|
||||||
|
return this.http.post<User>(SIGNIN_URL, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/main/ts2/src/app/version-revisions/version-revisions.component.html
Executable file
@@ -0,0 +1,34 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 col-lg-2">
|
||||||
|
<h1>Versions</h1>
|
||||||
|
<ul class="list-group">
|
||||||
|
<a *ngFor="let version of versionsList">
|
||||||
|
<li [className]="version.active ? 'list-group-item active' : 'list-group-item'"
|
||||||
|
(click)="showVersionRevision(version)">
|
||||||
|
{{version.number}}
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div id="versionRevisionsArea" class="col-md-9 col-lg-10">
|
||||||
|
<div *ngIf="versionRevisionsList.length">
|
||||||
|
<h3>Ajouts de fonctionnalités</h3>
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let versionRevision of versionRevisionsList">
|
||||||
|
{{versionRevision.text}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<h4 *ngIf="!versionRevisionsList.length">Aucune nouvelle fonctionnalité pour cette version.</h4>
|
||||||
|
|
||||||
|
<div *ngIf="versionRevisionsBugfixList.length">
|
||||||
|
<h3>Correction d'anomalies</h3>
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let versionRevision of versionRevisionsBugfixList">
|
||||||
|
{{versionRevision.text}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<h4 *ngIf="!versionRevisionsBugfixList.length">Aucune correction d'anomalie pour cette version.</h4>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
51
src/main/ts2/src/app/version-revisions/version-revisions.component.ts
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { VersionRevisionService } from './version-revisions.service';
|
||||||
|
import { VersionRevision, Version } from '../core/entities';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-version-revisions',
|
||||||
|
templateUrl: 'version-revisions.component.html',
|
||||||
|
styles: [`
|
||||||
|
#versionRevisionsArea {
|
||||||
|
padding-top: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 767px) {
|
||||||
|
#versionRevisionsArea {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`]
|
||||||
|
})
|
||||||
|
export class VersionRevisionComponent implements OnInit {
|
||||||
|
versionsList: Array<Version>;
|
||||||
|
versionRevisionsList: Array<VersionRevision>;
|
||||||
|
versionRevisionsBugfixList: Array<VersionRevision>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private versionRevisionService: VersionRevisionService
|
||||||
|
) {
|
||||||
|
this.versionsList = [];
|
||||||
|
this.versionRevisionsList = [];
|
||||||
|
this.versionRevisionsBugfixList = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.versionRevisionService.getVersions().subscribe(versionsList => {
|
||||||
|
this.versionsList = versionsList;
|
||||||
|
this.showVersionRevision(this.versionsList[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showVersionRevision(version: Version): void {
|
||||||
|
this.versionsList.forEach(versionTmp => versionTmp.active = false);
|
||||||
|
version.active = true;
|
||||||
|
|
||||||
|
this.versionRevisionService.findByVersionNumber(version.number).subscribe(versionRevisionsList => {
|
||||||
|
this.versionRevisionsList = versionRevisionsList.filter(vr => !vr.bugfix);
|
||||||
|
this.versionRevisionsBugfixList = versionRevisionsList.filter(vr => vr.bugfix);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/ts2/src/app/version-revisions/version-revisions.service.ts
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Version, VersionRevision } from '../core/entities';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
|
||||||
|
const VERSION_REVISIONS_URL = environment.apiUrl + '/api/versionrevisions';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class VersionRevisionService {
|
||||||
|
constructor(
|
||||||
|
private http: HttpClient
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getVersions(): Observable<Array<Version>> {
|
||||||
|
return this.http.get<Array<Version>>(`${VERSION_REVISIONS_URL}/versions`);
|
||||||
|
}
|
||||||
|
|
||||||
|
findByVersionNumber(versionNumber: string): Observable<Array<VersionRevision>> {
|
||||||
|
return this.http.get<Array<VersionRevision>>(`${VERSION_REVISIONS_URL}/${versionNumber}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/main/ts2/src/assets/.gitkeep
Executable file
271
src/main/ts2/src/assets/css/prism.css
Executable file
@@ -0,0 +1,271 @@
|
|||||||
|
/* http://prismjs.com/download.html?themes=prism-okaidia&languages=markup+css+clike+javascript+apacheconf+c+bash+batch+cpp+csharp+ruby+css-extras+django+docker+git+http+ini+java+json+latex+less+lua+makefile+markdown+nginx+php+php-extras+powershell+properties+python+jsx+rest+rust+sass+scss+scala+sql+typescript+vim+wiki+yaml&plugins=line-numbers+autolinker+file-highlight+toolbar+unescaped-markup+command-line+show-language+copy-to-clipboard */
|
||||||
|
/**
|
||||||
|
* okaidia theme for JavaScript, CSS and HTML
|
||||||
|
* Loosely based on Monokai textmate theme by http://www.monokai.nl/
|
||||||
|
* @author ocodia
|
||||||
|
*/
|
||||||
|
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
color: #f8f8f2;
|
||||||
|
background: none;
|
||||||
|
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
|
||||||
|
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks */
|
||||||
|
pre[class*="language-"] {
|
||||||
|
padding: 1em;
|
||||||
|
margin: .5em 0;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(pre) > code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
background: #272822;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
padding: .1em;
|
||||||
|
border-radius: .3em;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: slategray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.namespace {
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol,
|
||||||
|
.token.deleted {
|
||||||
|
color: #f92672;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.boolean,
|
||||||
|
.token.number {
|
||||||
|
color: #ae81ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.builtin,
|
||||||
|
.token.inserted {
|
||||||
|
color: #a6e22e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string,
|
||||||
|
.token.variable {
|
||||||
|
color: #f8f8f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.function {
|
||||||
|
color: #e6db74;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.keyword {
|
||||||
|
color: #66d9ef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.regex,
|
||||||
|
.token.important {
|
||||||
|
color: #fd971f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important,
|
||||||
|
.token.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.line-numbers {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 3.8em;
|
||||||
|
counter-reset: linenumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.line-numbers > code {
|
||||||
|
position: relative;
|
||||||
|
white-space: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers .line-numbers-rows {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
top: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
left: -3.8em;
|
||||||
|
width: 3em; /* works for line-numbers below 1000 lines */
|
||||||
|
letter-spacing: -1px;
|
||||||
|
border-right: 1px solid #999;
|
||||||
|
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers-rows > span {
|
||||||
|
pointer-events: none;
|
||||||
|
display: block;
|
||||||
|
counter-increment: linenumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers-rows > span:before {
|
||||||
|
content: counter(linenumber);
|
||||||
|
color: #999;
|
||||||
|
display: block;
|
||||||
|
padding-right: 0.8em;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.token a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
pre.code-toolbar {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.code-toolbar > .toolbar {
|
||||||
|
position: absolute;
|
||||||
|
top: .3em;
|
||||||
|
right: .2em;
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.code-toolbar:hover > .toolbar {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.code-toolbar > .toolbar .toolbar-item {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.code-toolbar > .toolbar a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.code-toolbar > .toolbar button {
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
line-height: normal;
|
||||||
|
overflow: visible;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-user-select: none; /* for button */
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.code-toolbar > .toolbar a,
|
||||||
|
pre.code-toolbar > .toolbar button,
|
||||||
|
pre.code-toolbar > .toolbar span {
|
||||||
|
color: #bbb;
|
||||||
|
font-size: .8em;
|
||||||
|
padding: 0 .5em;
|
||||||
|
background: #f5f2f0;
|
||||||
|
background: rgba(224, 224, 224, 0.2);
|
||||||
|
box-shadow: 0 2px 0 0 rgba(0,0,0,0.2);
|
||||||
|
border-radius: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.code-toolbar > .toolbar a:hover,
|
||||||
|
pre.code-toolbar > .toolbar a:focus,
|
||||||
|
pre.code-toolbar > .toolbar button:hover,
|
||||||
|
pre.code-toolbar > .toolbar button:focus,
|
||||||
|
pre.code-toolbar > .toolbar span:hover,
|
||||||
|
pre.code-toolbar > .toolbar span:focus {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback, in case JS does not run, to ensure the code is at least visible */
|
||||||
|
.lang-markup script[type='text/plain'],
|
||||||
|
.language-markup script[type='text/plain'],
|
||||||
|
script[type='text/plain'].lang-markup,
|
||||||
|
script[type='text/plain'].language-markup {
|
||||||
|
display: block;
|
||||||
|
font: 100% Consolas, Monaco, monospace;
|
||||||
|
white-space: pre;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-line-prompt {
|
||||||
|
border-right: 1px solid #999;
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
font-size: 100%;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
margin-right: 1em;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-line-prompt > span:before {
|
||||||
|
color: #999;
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
padding-right: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-line-prompt > span[data-user]:before {
|
||||||
|
content: "[" attr(data-user) "@" attr(data-host) "] $";
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-line-prompt > span[data-user="root"]:before {
|
||||||
|
content: "[" attr(data-user) "@" attr(data-host) "] #";
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-line-prompt > span[data-prompt]:before {
|
||||||
|
content: attr(data-prompt);
|
||||||
|
}
|
||||||
|
|
||||||
BIN
src/main/ts2/src/assets/images/403.png
Executable file
|
After Width: | Height: | Size: 207 KiB |
BIN
src/main/ts2/src/assets/images/404.png
Executable file
|
After Width: | Height: | Size: 188 KiB |
BIN
src/main/ts2/src/assets/images/500.png
Executable file
|
After Width: | Height: | Size: 822 KiB |
BIN
src/main/ts2/src/assets/images/background.png
Executable file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
src/main/ts2/src/assets/images/code.jpg
Executable file
|
After Width: | Height: | Size: 154 KiB |
BIN
src/main/ts2/src/assets/images/codiki.png
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/main/ts2/src/assets/images/default_image.png
Executable file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
src/main/ts2/src/assets/images/default_user.png
Executable file
|
After Width: | Height: | Size: 36 KiB |
51
src/main/ts2/src/assets/js/prism.js
Executable file
11
src/main/ts2/src/browserslist
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
|
||||||
|
# For additional information regarding the format and rule options, please see:
|
||||||
|
# https://github.com/browserslist/browserslist#queries
|
||||||
|
#
|
||||||
|
# For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed
|
||||||
|
|
||||||
|
> 0.5%
|
||||||
|
last 2 versions
|
||||||
|
Firefox ESR
|
||||||
|
not dead
|
||||||
|
not IE 9-11
|
||||||
11
src/main/ts2/src/environments/environment.integ.ts
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
// The file contents for the current environment will overwrite these during build.
|
||||||
|
// The build system defaults to the dev environment which uses `environment.ts`, but if you do
|
||||||
|
// `ng build --env=prod` then `environment.prod.ts` will be used instead.
|
||||||
|
// The list of which env maps to which file can be found in `.angular-cli.json`.
|
||||||
|
|
||||||
|
export const environment = {
|
||||||
|
production: false,
|
||||||
|
apiUrl: 'http://192.168.0.153:8445',
|
||||||
|
appVersion: '1.0.1',
|
||||||
|
title: 'Intégration'
|
||||||
|
};
|
||||||
6
src/main/ts2/src/environments/environment.prod.ts
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
export const environment = {
|
||||||
|
production: true,
|
||||||
|
apiUrl: 'https://176.188.217.1:54444',
|
||||||
|
appVersion: '1.0.1',
|
||||||
|
title: ''
|
||||||
|
};
|
||||||
12
src/main/ts2/src/environments/environment.ts
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
// The file contents for the current environment will overwrite these during build.
|
||||||
|
// The build system defaults to the dev environment which uses `environment.ts`, but if you do
|
||||||
|
// `ng build --env=prod` then `environment.prod.ts` will be used instead.
|
||||||
|
// The list of which env maps to which file can be found in `.angular-cli.json`.
|
||||||
|
|
||||||
|
export const environment = {
|
||||||
|
production: false,
|
||||||
|
apiUrl: 'http://localhost:8080',
|
||||||
|
// apiUrl: 'http://192.168.0.153:8445',
|
||||||
|
appVersion: '1.0.1',
|
||||||
|
title: 'Développement'
|
||||||
|
};
|
||||||
BIN
src/main/ts2/src/favicon.png
Executable file
|
After Width: | Height: | Size: 1.5 KiB |
17
src/main/ts2/src/index.html
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Codiki</title>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<base href="/" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="favicon.png" />
|
||||||
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
|
||||||
|
<link href="./assets/css/prism.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<app-root></app-root>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/1.5.8/clipboard.min.js"></script>
|
||||||
|
<script type="text/javascript" src="./assets/js/prism.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
31
src/main/ts2/src/karma.conf.js
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
// 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-istanbul-reporter'),
|
||||||
|
require('@angular-devkit/build-angular/plugins/karma')
|
||||||
|
],
|
||||||
|
client: {
|
||||||
|
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||||
|
},
|
||||||
|
coverageIstanbulReporter: {
|
||||||
|
dir: require('path').join(__dirname, '../coverage'),
|
||||||
|
reports: ['html', 'lcovonly'],
|
||||||
|
fixWebpackSourcePaths: true
|
||||||
|
},
|
||||||
|
reporters: ['progress', 'kjhtml'],
|
||||||
|
port: 9876,
|
||||||
|
colors: true,
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
autoWatch: true,
|
||||||
|
browsers: ['Chrome'],
|
||||||
|
singleRun: false
|
||||||
|
});
|
||||||
|
};
|
||||||
13
src/main/ts2/src/main.ts
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
import { enableProdMode } from '@angular/core';
|
||||||
|
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
|
||||||
|
|
||||||
|
import { AppModule } from './app/app.module';
|
||||||
|
import { environment } from './environments/environment';
|
||||||
|
|
||||||
|
if (environment.production) {
|
||||||
|
enableProdMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
platformBrowserDynamic().bootstrapModule(AppModule)
|
||||||
|
.catch(err => console.error(err));
|
||||||
|
|
||||||
80
src/main/ts2/src/polyfills.ts
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* This file includes polyfills needed by Angular and is loaded before the app.
|
||||||
|
* You can add your own extra polyfills to this file.
|
||||||
|
*
|
||||||
|
* This file is divided into 2 sections:
|
||||||
|
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
|
||||||
|
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
|
||||||
|
* file.
|
||||||
|
*
|
||||||
|
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
|
||||||
|
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
|
||||||
|
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
|
||||||
|
*
|
||||||
|
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
/***************************************************************************************************
|
||||||
|
* BROWSER POLYFILLS
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
|
||||||
|
// import 'core-js/es6/symbol';
|
||||||
|
// import 'core-js/es6/object';
|
||||||
|
// import 'core-js/es6/function';
|
||||||
|
// import 'core-js/es6/parse-int';
|
||||||
|
// import 'core-js/es6/parse-float';
|
||||||
|
// import 'core-js/es6/number';
|
||||||
|
// import 'core-js/es6/math';
|
||||||
|
// import 'core-js/es6/string';
|
||||||
|
// import 'core-js/es6/date';
|
||||||
|
// import 'core-js/es6/array';
|
||||||
|
// import 'core-js/es6/regexp';
|
||||||
|
// import 'core-js/es6/map';
|
||||||
|
// import 'core-js/es6/weak-map';
|
||||||
|
// import 'core-js/es6/set';
|
||||||
|
|
||||||
|
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
|
||||||
|
// import 'classlist.js'; // Run `npm install --save classlist.js`.
|
||||||
|
|
||||||
|
/** IE10 and IE11 requires the following for the Reflect API. */
|
||||||
|
// import 'core-js/es6/reflect';
|
||||||
|
|
||||||
|
|
||||||
|
/** Evergreen browsers require these. **/
|
||||||
|
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
|
||||||
|
import 'core-js/es7/reflect';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Web Animations `@angular/platform-browser/animations`
|
||||||
|
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
|
||||||
|
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
|
||||||
|
**/
|
||||||
|
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, zone.js will patch all possible macroTask and DomEvents
|
||||||
|
* user can disable parts of macroTask/DomEvents patch by setting following flags
|
||||||
|
*/
|
||||||
|
|
||||||
|
// (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
|
||||||
|
// (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
|
||||||
|
// (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
|
||||||
|
|
||||||
|
/*
|
||||||
|
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
|
||||||
|
* with the following flag, it will bypass `zone.js` patch for IE/Edge
|
||||||
|
*/
|
||||||
|
// (window as any).__Zone_enable_cross_context_check = true;
|
||||||
|
|
||||||
|
/***************************************************************************************************
|
||||||
|
* Zone JS is required by default for Angular itself.
|
||||||
|
*/
|
||||||
|
import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/***************************************************************************************************
|
||||||
|
* APPLICATION IMPORTS
|
||||||
|
*/
|
||||||
38
src/main/ts2/src/styles.scss
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
/* You can add global styles to this file, and also import other style files */
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
// 64px for the header and 15px for a litle margin.
|
||||||
|
padding-top: 90px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content:space-between;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ***** Floating card button ***** */
|
||||||
|
.btn-card-floating {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 47px;
|
||||||
|
height: 47px;
|
||||||
|
margin: -23px 20px;
|
||||||
|
margin-left: auto;
|
||||||
|
line-height: 0;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 47px;
|
||||||
|
padding-left: 2px;
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: 0 5px 11px 0 rgba(0,0,0,.18), 0 4px 15px 0 rgba(0,0,0,.15);
|
||||||
|
background-color: #3f51b5;
|
||||||
|
transition: box-shadow 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-card-floating:hover {
|
||||||
|
box-shadow: 0 8px 17px 0 rgba(0,0,0,.2), 0 6px 20px 0 rgba(0,0,0,.19);
|
||||||
|
}
|
||||||
|
/* ***** End of floating card button ***** */
|
||||||
20
src/main/ts2/src/test.ts
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
|
||||||
|
|
||||||
|
import 'zone.js/dist/zone-testing';
|
||||||
|
import { getTestBed } from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
BrowserDynamicTestingModule,
|
||||||
|
platformBrowserDynamicTesting
|
||||||
|
} from '@angular/platform-browser-dynamic/testing';
|
||||||
|
|
||||||
|
declare const require: any;
|
||||||
|
|
||||||
|
// First, initialize the Angular testing environment.
|
||||||
|
getTestBed().initTestEnvironment(
|
||||||
|
BrowserDynamicTestingModule,
|
||||||
|
platformBrowserDynamicTesting()
|
||||||
|
);
|
||||||
|
// Then we find all the tests.
|
||||||
|
const context = require.context('./', true, /\.spec\.ts$/);
|
||||||
|
// And load the modules.
|
||||||
|
context.keys().map(context);
|
||||||
11
src/main/ts2/src/tsconfig.app.json
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../out-tsc/app",
|
||||||
|
"types": []
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"test.ts",
|
||||||
|
"**/*.spec.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
18
src/main/ts2/src/tsconfig.spec.json
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../out-tsc/spec",
|
||||||
|
"types": [
|
||||||
|
"jasmine",
|
||||||
|
"node"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"test.ts",
|
||||||
|
"polyfills.ts"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"**/*.spec.ts",
|
||||||
|
"**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
21
src/main/ts2/src/tslint.json
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"extends": "../tslint.json",
|
||||||
|
"rules": {
|
||||||
|
"directive-selector": [
|
||||||
|
true,
|
||||||
|
"attribute",
|
||||||
|
"app",
|
||||||
|
"camelCase"
|
||||||
|
],
|
||||||
|
"component-selector": [
|
||||||
|
true,
|
||||||
|
"element",
|
||||||
|
"app",
|
||||||
|
"kebab-case"
|
||||||
|
],
|
||||||
|
"indent": [
|
||||||
|
true,
|
||||||
|
"tabs"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/main/ts2/tsconfig.json
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compileOnSave": false,
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"outDir": "./dist/out-tsc",
|
||||||
|
"sourceMap": true,
|
||||||
|
"declaration": false,
|
||||||
|
"module": "es2015",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"target": "es5",
|
||||||
|
"typeRoots": [
|
||||||
|
"node_modules/@types"
|
||||||
|
],
|
||||||
|
"lib": [
|
||||||
|
"es2017",
|
||||||
|
"dom"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
131
src/main/ts2/tslint.json
Executable file
@@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"rulesDirectory": [
|
||||||
|
"node_modules/codelyzer"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"arrow-return-shorthand": true,
|
||||||
|
"callable-types": true,
|
||||||
|
"class-name": true,
|
||||||
|
"comment-format": [
|
||||||
|
true,
|
||||||
|
"check-space"
|
||||||
|
],
|
||||||
|
"curly": true,
|
||||||
|
"deprecation": {
|
||||||
|
"severity": "warn"
|
||||||
|
},
|
||||||
|
"eofline": true,
|
||||||
|
"forin": true,
|
||||||
|
"import-blacklist": [
|
||||||
|
true,
|
||||||
|
"rxjs/Rx"
|
||||||
|
],
|
||||||
|
"import-spacing": true,
|
||||||
|
"indent": [
|
||||||
|
true,
|
||||||
|
"spaces"
|
||||||
|
],
|
||||||
|
"interface-over-type-literal": true,
|
||||||
|
"label-position": true,
|
||||||
|
"max-line-length": [
|
||||||
|
true,
|
||||||
|
140
|
||||||
|
],
|
||||||
|
"member-access": false,
|
||||||
|
"member-ordering": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"order": [
|
||||||
|
"static-field",
|
||||||
|
"instance-field",
|
||||||
|
"static-method",
|
||||||
|
"instance-method"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-arg": true,
|
||||||
|
"no-bitwise": true,
|
||||||
|
"no-console": [
|
||||||
|
true,
|
||||||
|
"debug",
|
||||||
|
"info",
|
||||||
|
"time",
|
||||||
|
"timeEnd",
|
||||||
|
"trace"
|
||||||
|
],
|
||||||
|
"no-construct": true,
|
||||||
|
"no-debugger": true,
|
||||||
|
"no-duplicate-super": true,
|
||||||
|
"no-empty": false,
|
||||||
|
"no-empty-interface": true,
|
||||||
|
"no-eval": true,
|
||||||
|
"no-inferrable-types": [
|
||||||
|
true,
|
||||||
|
"ignore-params"
|
||||||
|
],
|
||||||
|
"no-misused-new": true,
|
||||||
|
"no-non-null-assertion": true,
|
||||||
|
"no-redundant-jsdoc": true,
|
||||||
|
"no-shadowed-variable": true,
|
||||||
|
"no-string-literal": false,
|
||||||
|
"no-string-throw": true,
|
||||||
|
"no-switch-case-fall-through": true,
|
||||||
|
"no-trailing-whitespace": true,
|
||||||
|
"no-unnecessary-initializer": true,
|
||||||
|
"no-unused-expression": true,
|
||||||
|
"no-use-before-declare": true,
|
||||||
|
"no-var-keyword": true,
|
||||||
|
"object-literal-sort-keys": false,
|
||||||
|
"one-line": [
|
||||||
|
true,
|
||||||
|
"check-open-brace",
|
||||||
|
"check-catch",
|
||||||
|
"check-else",
|
||||||
|
"check-whitespace"
|
||||||
|
],
|
||||||
|
"prefer-const": true,
|
||||||
|
"quotemark": [
|
||||||
|
true,
|
||||||
|
"single"
|
||||||
|
],
|
||||||
|
"radix": true,
|
||||||
|
"semicolon": [
|
||||||
|
true,
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"triple-equals": [
|
||||||
|
true,
|
||||||
|
"allow-null-check"
|
||||||
|
],
|
||||||
|
"typedef-whitespace": [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
"call-signature": "nospace",
|
||||||
|
"index-signature": "nospace",
|
||||||
|
"parameter": "nospace",
|
||||||
|
"property-declaration": "nospace",
|
||||||
|
"variable-declaration": "nospace"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"unified-signatures": true,
|
||||||
|
"variable-name": false,
|
||||||
|
"whitespace": [
|
||||||
|
true,
|
||||||
|
"check-branch",
|
||||||
|
"check-decl",
|
||||||
|
"check-operator",
|
||||||
|
"check-separator",
|
||||||
|
"check-type"
|
||||||
|
],
|
||||||
|
"no-output-on-prefix": true,
|
||||||
|
"use-input-property-decorator": true,
|
||||||
|
"use-output-property-decorator": true,
|
||||||
|
"use-host-property-decorator": true,
|
||||||
|
"no-input-rename": true,
|
||||||
|
"no-output-rename": true,
|
||||||
|
"use-life-cycle-interface": true,
|
||||||
|
"use-pipe-transform-interface": true,
|
||||||
|
"component-class-suffix": true,
|
||||||
|
"directive-class-suffix": true
|
||||||
|
}
|
||||||
|
}
|
||||||