Add create-update post component.
This commit is contained in:
@@ -17,6 +17,9 @@ import javax.persistence.OneToMany;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import org.codiki.core.entities.dto.CategoryDTO;
|
||||
import org.codiki.core.entities.dto.View;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
|
||||
@Entity
|
||||
@Table(name="category")
|
||||
@@ -29,8 +32,10 @@ public class Category implements Serializable {
|
||||
/* ******************* */
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@JsonView(View.PostDTO.class)
|
||||
private Long id;
|
||||
|
||||
@JsonView(View.PostDTO.class)
|
||||
private String name;
|
||||
|
||||
/* ******************* */
|
||||
|
||||
@@ -120,36 +120,32 @@ public class PostController {
|
||||
|
||||
@JsonView(View.PostDTO.class)
|
||||
@PostMapping("/preview")
|
||||
public PostDTO preview(@RequestBody final PostDTO pPost) {
|
||||
final PostDTO result = new PostDTO();
|
||||
|
||||
result.setTitle(pPost.getTitle());
|
||||
result.setImage(pPost.getImage() == null || "".equals(pPost.getImage())
|
||||
public Post preview(@RequestBody final Post pPost) {
|
||||
pPost.setImage(pPost.getImage() == null || "".equals(pPost.getImage())
|
||||
? "https://news-cdn.softpedia.com/images/news2/this-is-the-default-wallpaper-of-the-gnome-3-20-desktop-environment-500743-2.jpg"
|
||||
: pPost.getImage());
|
||||
result.setDescription(pPost.getDescription());
|
||||
result.setText(parserService.parse(pPost.getText()));
|
||||
pPost.setText(parserService.parse(pPost.getText()));
|
||||
|
||||
return result;
|
||||
return pPost;
|
||||
}
|
||||
|
||||
@JsonView(View.PostDTO.class)
|
||||
@PostMapping("/")
|
||||
public PostDTO insert(@RequestBody final PostDTO pPost, final HttpServletRequest pRequest,
|
||||
public Post insert(@RequestBody final PostDTO pPost, final HttpServletRequest pRequest,
|
||||
final HttpServletResponse pResponse, final Principal pPrincipal) {
|
||||
PostDTO result = null;
|
||||
Post result = null;
|
||||
|
||||
Optional<Post> postCreated = postService.insert(pPost, pRequest, pResponse, pPrincipal);
|
||||
|
||||
if(postCreated.isPresent()) {
|
||||
result = new PostDTO(postCreated.get());
|
||||
result = postCreated.get();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@PutMapping("/")
|
||||
public void update(@RequestBody final PostDTO pPost, final HttpServletRequest pRequest,
|
||||
public void update(@RequestBody final Post pPost, final HttpServletRequest pRequest,
|
||||
final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
|
||||
postService.update(pPost, pRequest, pResponse, pPrincipal);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class PostService {
|
||||
return result;
|
||||
}
|
||||
|
||||
public void update(final PostDTO pPost, final HttpServletRequest pRequest,
|
||||
public void update(final Post pPost, final HttpServletRequest pRequest,
|
||||
final HttpServletResponse pResponse, final Principal pPrincipal) throws IOException {
|
||||
final Optional<User> connectedUser = userService.getUserByPrincipal(pPrincipal);
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import { ProfilEditionComponent } from './account-settings/profil-edition/profil
|
||||
import { PostComponent } from './posts/post.component';
|
||||
import { NotFoundComponent } from './not-found/not-found.component';
|
||||
import { ByCategoryComponent } from './posts/byCategory/by-category.component';
|
||||
import { CreateUpdatePostComponent } from './posts/create-update/create-update-post.component';
|
||||
|
||||
// Reusable components
|
||||
import { PostCardComponent } from './core/post-card/post-card.component';
|
||||
@@ -45,6 +46,7 @@ import { ChangePasswordService } from './account-settings/change-password/change
|
||||
import { ProfilEditionService } from './account-settings/profil-edition/profil-edition.service';
|
||||
import { PostService } from './posts/post.service';
|
||||
import { ByCategoryService } from './posts/byCategory/by-category.service';
|
||||
import { CreateUpdatePostService } from './posts/create-update/create-update-post.service';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@@ -61,7 +63,8 @@ import { ByCategoryService } from './posts/byCategory/by-category.service';
|
||||
ProfilEditionComponent,
|
||||
PostComponent,
|
||||
NotFoundComponent,
|
||||
ByCategoryComponent
|
||||
ByCategoryComponent,
|
||||
CreateUpdatePostComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -86,6 +89,7 @@ import { ByCategoryService } from './posts/byCategory/by-category.service';
|
||||
ProfilEditionService,
|
||||
PostService,
|
||||
ByCategoryService,
|
||||
CreateUpdatePostService,
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: UnauthorizedInterceptor, multi: true }
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ChangePasswordComponent } from './account-settings/change-password/chan
|
||||
import { ProfilEditionComponent } from './account-settings/profil-edition/profil-edition.component';
|
||||
import { PostComponent } from './posts/post.component';
|
||||
import { ByCategoryComponent } from './posts/byCategory/by-category.component';
|
||||
import { CreateUpdatePostComponent } from './posts/create-update/create-update-post.component';
|
||||
|
||||
export const appRoutes: Routes = [
|
||||
{ path: '', redirectTo: '/home', pathMatch: 'full' },
|
||||
@@ -21,7 +22,9 @@ export const appRoutes: Routes = [
|
||||
{ path: 'accountSettings', component: AccountSettingsComponent, canActivate: [AuthGuard] },
|
||||
{ path: 'changePassword', component: ChangePasswordComponent, canActivate: [AuthGuard] },
|
||||
{ path: 'profilEdit', component: ProfilEditionComponent, canActivate: [AuthGuard] },
|
||||
{ path: 'posts/new', component: CreateUpdatePostComponent, canActivate: [AuthGuard] },
|
||||
{ path: 'posts/:postKey', component: PostComponent },
|
||||
{ path: 'posts/byCategory/:categoryId', component: ByCategoryComponent},
|
||||
{ path: 'posts/update/:postKey', component: CreateUpdatePostComponent, canActivate: [AuthGuard] },
|
||||
{ path: '**', redirectTo: '/home' }
|
||||
];
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
<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 mdbInputDirective
|
||||
id="title"
|
||||
name="title"
|
||||
type="text"
|
||||
class="form-control"
|
||||
[(ngModel)]="model.title"
|
||||
#title="ngModel"
|
||||
data-error="Veuillez saisir un titre d'article"
|
||||
[validateSuccess]="false"
|
||||
required />
|
||||
<label for="title">Titre de l'article</label>
|
||||
</div>
|
||||
<div class="md-form">
|
||||
<input mdbInputDirective
|
||||
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"
|
||||
[validateSuccess]="false"
|
||||
required />
|
||||
<label for="image">Image de l'article</label>
|
||||
</div>
|
||||
<div class="md-form">
|
||||
<input mdbInputDirective
|
||||
id="description"
|
||||
name="description"
|
||||
type="text"
|
||||
class="form-control"
|
||||
[(ngModel)]="model.description"
|
||||
#description="ngModel"
|
||||
data-error="Veuillez saisir la description de l'article"
|
||||
[validateSuccess]="false"
|
||||
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" [compareWith]="compareCategories" 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><b>H1</b></button>
|
||||
<button type="button"
|
||||
class="btn btn-floating waves-light"
|
||||
(click)="injectHeader('h2')"
|
||||
mdbTooltip="Titre 2"
|
||||
placement="bottom"
|
||||
mdbRippleRadius><b>H2</b></button>
|
||||
<button type="button"
|
||||
class="btn btn-floating waves-light"
|
||||
(click)="injectHeader('h3')"
|
||||
mdbTooltip="Titre 3"
|
||||
placement="bottom"
|
||||
mdbRippleRadius><b>H3</b></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 mdbInputDirective
|
||||
id="text"
|
||||
name="text"
|
||||
type="text"
|
||||
class="md-textarea form-control"
|
||||
[(ngModel)]="model.text"
|
||||
#text="ngModel"
|
||||
data-error="Veuillez saisir le contenu de l'article"
|
||||
[validateSuccess]="false"
|
||||
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="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 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 mdbInputDirective
|
||||
id="codeTmp"
|
||||
name="codeTmp"
|
||||
type="text"
|
||||
class="md-textarea form-control"
|
||||
[(ngModel)]="codeTmp"
|
||||
data-error="Veuillez écrire ou coller votre extrait de code dans cette zone de saisie"
|
||||
[validateSuccess]="false"
|
||||
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>
|
||||
@@ -0,0 +1,234 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
height: 250px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
import { Component, OnInit, SecurityContext, ViewChild } from '@angular/core';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { Router, ActivatedRoute, RoutesRecognized, NavigationEnd } 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, pairwise } from 'rxjs/operators';
|
||||
|
||||
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']);
|
||||
}
|
||||
});
|
||||
|
||||
// FIXME: The message isn't shown and the method ngOnInit is too much called, also during others components navigation.
|
||||
this.router.events.pipe(filter(e => e instanceof RoutesRecognized), pairwise()).subscribe((events: any) => {
|
||||
if (events[0].urlAfterRedirects === '/posts/new') {
|
||||
this.setMessage('Article créé.', false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 `/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();
|
||||
}
|
||||
|
||||
compareCategories(cat1: Category, cat2: Category): boolean {
|
||||
return cat1 && cat2 ? cat1.id === cat2.id : cat1 === cat2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpEvent, HttpRequest } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Post, Category, Image } from '../../core/entities';
|
||||
|
||||
@Injectable()
|
||||
export class CreateUpdatePostService {
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
processPreview(post: Post): Observable<Post> {
|
||||
return this.http.post<Post>(`/api/posts/preview`, post);
|
||||
}
|
||||
|
||||
getCategories(): Observable<Array<Category>> {
|
||||
return this.http.get<Array<Category>>(`/api/categories/`);
|
||||
}
|
||||
|
||||
addPost(post: Post): Observable<Post> {
|
||||
return this.http.post<Post>(`/api/posts/`, post);
|
||||
}
|
||||
|
||||
updatePost(post: Post): Observable<Post> {
|
||||
return this.http.put<Post>(`/api/posts/`, post);
|
||||
}
|
||||
|
||||
getPost(postKey: string): Observable<Post> {
|
||||
return this.http.get<Post>(`/api/posts/${postKey}/source`);
|
||||
}
|
||||
|
||||
getImages(): Observable<Array<Image>> {
|
||||
return this.http.get<Array<Image>>(`/api/images/myImages`);
|
||||
}
|
||||
|
||||
uploadPicture(file: File): Observable<HttpEvent<{}>> {
|
||||
const formData: FormData = new FormData();
|
||||
|
||||
formData.append('file', file);
|
||||
|
||||
return this.http.request(new HttpRequest(
|
||||
'POST', '/api/images', formData, {
|
||||
reportProgress: true,
|
||||
responseType: 'text'
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
getImageDetails(imageLink: string): Observable<Image> {
|
||||
return this.http.get<Image>(`/api/images/${imageLink}/details`);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
<a *ngIf="owned" class="btn-card-floating waves-light white-text"
|
||||
routerLink="/posts/update/{{post.key}}">
|
||||
<i class="fa fa-pencil"></i>
|
||||
<i class="fa fa-pen"></i>
|
||||
</a>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
<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> -->
|
||||
<script type="text/javascript" src="./assets/js/prism.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user