Modifification et suppression de ses propre posts (CRUD ! :D)
This commit is contained in:
parent
06d8bc2b86
commit
15ec7f68d3
@ -0,0 +1,18 @@
|
|||||||
|
div {
|
||||||
|
margin-top: 2em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 50em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap:1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button {
|
||||||
|
align-self: center;
|
||||||
|
}
|
@ -1 +1,27 @@
|
|||||||
<p>post-form works!</p>
|
<div>
|
||||||
|
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||||
|
<label for="title">Titre du post</label>
|
||||||
|
<input [(ngModel)]="title" id="title" type="text" pInputText formControlName="title"/>
|
||||||
|
<label for="category">Catégorie du post</label>
|
||||||
|
<input [(ngModel)]="category" pInputText id="category" formControlName="category" type="text"/>
|
||||||
|
<label for="desc">Description du post</label>
|
||||||
|
<textarea [(ngModel)]="description" formControlName="description" id="desc" pInputTextarea></textarea>
|
||||||
|
<label>Image descriptive du post</label>
|
||||||
|
<p-fileUpload accept="image/*"
|
||||||
|
maxFileSize="1000000"
|
||||||
|
[showUploadButton]="false"
|
||||||
|
[showCancelButton]="false"
|
||||||
|
chooseLabel="Sélectionner une image"
|
||||||
|
(onSelect)="onSelected($event)">
|
||||||
|
<ng-template pTemplate="content">
|
||||||
|
</ng-template>
|
||||||
|
</p-fileUpload>
|
||||||
|
<label>Contenu du post</label>
|
||||||
|
<p-editor [(ngModel)]="body" formControlName="body" [style]="{ height: '320px' }"/>
|
||||||
|
@if (isUpdateMode) {
|
||||||
|
<p-button class="send-button" type="submit" label="Mettre à jour"></p-button>
|
||||||
|
} @else {
|
||||||
|
<p-button class="send-button" type="submit" label="Ajouter"></p-button>
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
@ -1,13 +1,155 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, Input, OnDestroy } from '@angular/core';
|
||||||
|
import { HeaderComponent } from '../../components/header/header.component';
|
||||||
|
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
|
import { InputTextareaModule } from 'primeng/inputtextarea';
|
||||||
|
import { FileSelectEvent, FileUploadModule } from 'primeng/fileupload';
|
||||||
|
import {mergeMap, Subscription} from 'rxjs';
|
||||||
|
import { PostService } from '../../services/post.service';
|
||||||
|
import { CookieService } from 'ngx-cookie-service';
|
||||||
|
import { MessageService } from 'primeng/api';
|
||||||
|
import { EditorModule } from 'primeng/editor';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import {Author} from '../../models/author';
|
||||||
|
import {AuthorService} from '../../services/author.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-post-form',
|
selector: 'app-post-form',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [],
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
InputTextModule,
|
||||||
|
InputTextareaModule,
|
||||||
|
FileUploadModule,
|
||||||
|
EditorModule,
|
||||||
|
HeaderComponent
|
||||||
|
],
|
||||||
templateUrl: './post-form.component.html',
|
templateUrl: './post-form.component.html',
|
||||||
styleUrl: './post-form.component.css'
|
styleUrls: ['./post-form.component.css']
|
||||||
})
|
})
|
||||||
export class PostFormComponent {
|
export class PostFormComponent implements OnDestroy {
|
||||||
|
@Input() postId: bigint | undefined;
|
||||||
|
@Input() isUpdateMode: boolean = false;
|
||||||
|
@Input() actualAuthor: Author | undefined;
|
||||||
|
@Input() title: string = '';
|
||||||
|
@Input() category: string = '';
|
||||||
|
@Input() description: string = '';
|
||||||
|
@Input() body: string = '';
|
||||||
|
subs: Subscription[] = [];
|
||||||
|
form: FormGroup;
|
||||||
|
uploadedFile: File | undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
|
private postService: PostService,
|
||||||
|
private authorService: AuthorService,
|
||||||
|
private cookieService: CookieService,
|
||||||
|
private messageService: MessageService,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
this.form = this.formBuilder.group({
|
||||||
|
description: ['', [Validators.required, Validators.maxLength(512)]],
|
||||||
|
title: ['', [Validators.required, Validators.maxLength(50)]],
|
||||||
|
body: ['', [Validators.required]],
|
||||||
|
category: ['', [Validators.required, Validators.maxLength(50)]],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.isUpdateMode && this.postId) {
|
||||||
|
this.loadPostData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPostData(): void {
|
||||||
|
if (this.postId) {
|
||||||
|
this.subs.push(
|
||||||
|
this.postService.getPost(this.postId).subscribe({
|
||||||
|
next: (post) => {
|
||||||
|
this.form.patchValue({
|
||||||
|
title: post.title,
|
||||||
|
description: post.description,
|
||||||
|
body: post.body,
|
||||||
|
category: post.category
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.failureMessage('Erreur', `Impossible de charger le post : ${err.message}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelected(event: FileSelectEvent): void {
|
||||||
|
if (event.currentFiles && event.currentFiles.length > 0) {
|
||||||
|
this.uploadedFile = event.currentFiles[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit(): void {
|
||||||
|
if (this.form.valid && this.uploadedFile) {
|
||||||
|
const formData = this.form.value;
|
||||||
|
const postData: any = {
|
||||||
|
title: formData.title,
|
||||||
|
description: formData.description,
|
||||||
|
body: formData.body,
|
||||||
|
category: formData.category
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.isUpdateMode && this.postId) {
|
||||||
|
this.subs.push(
|
||||||
|
this.postService.updatePost(this.postId, postData, this.cookieService.get('token')).pipe(
|
||||||
|
mergeMap((_) => {
|
||||||
|
return this.postService.changeIllustration(this.postId, this.uploadedFile, this.cookieService.get('token'));
|
||||||
|
})
|
||||||
|
).subscribe({
|
||||||
|
next: (_) => this.successMessage('Succès', 'Post créé avec succès'),
|
||||||
|
error: (err) => this.failureMessage('Erreur', err.message)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.subs.push(
|
||||||
|
this.postService.createPost(postData, this.cookieService.get("token")).pipe(
|
||||||
|
mergeMap(post =>
|
||||||
|
this.authorService.attributePost(this.actualAuthor?.id, post.id, this.cookieService.get("token")).pipe(
|
||||||
|
mergeMap((_) =>
|
||||||
|
this.postService.changeIllustration(post.id, this.uploadedFile, this.cookieService.get("token"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.router.navigate(['/']).then(() => {
|
||||||
|
this.successMessage('Succès', 'Post créé avec succès')
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (err) => this.failureMessage('Erreur', err.message)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
successMessage(summary: string, detail: string): void {
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary,
|
||||||
|
detail,
|
||||||
|
life: 3000,
|
||||||
|
closable: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
failureMessage(summary: string, detail: string): void {
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary,
|
||||||
|
detail,
|
||||||
|
life: 3000,
|
||||||
|
closable: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach((sub) => sub.unsubscribe());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<app-header></app-header>
|
<app-header></app-header>
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
|
@if (posts.length) {
|
||||||
@for (post of posts; track post.id) {
|
@for (post of posts; track post.id) {
|
||||||
<app-post-home [illustration]="post.illustration"
|
<app-post-home [illustration]="post.illustration"
|
||||||
[date]="post.publicationDate"
|
[date]="post.publicationDate"
|
||||||
@ -9,4 +10,7 @@
|
|||||||
[username]="'Guams'"
|
[username]="'Guams'"
|
||||||
[description]="post.description"/>
|
[description]="post.description"/>
|
||||||
}
|
}
|
||||||
|
} @else {
|
||||||
|
<h1>Aucun post n'a été créé pour l'instant</h1>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
18
src/app/pages/login/login.component.css
Normal file
18
src/app/pages/login/login.component.css
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.form-container {
|
||||||
|
margin-top: 2em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 50em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap:1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button {
|
||||||
|
align-self: center;
|
||||||
|
}
|
@ -1,14 +1,18 @@
|
|||||||
<app-header></app-header>
|
<app-header></app-header>
|
||||||
|
<div class="form-container">
|
||||||
|
<div class="form">
|
||||||
<label for="username">Nom d'utilisateur</label>
|
<label for="username">Nom d'utilisateur</label>
|
||||||
<input type="text" id="username" placeholder="Jean Zay" pInputText [(ngModel)]="name"/>
|
<input type="text" id="username" placeholder="Jean Zay" pInputText [(ngModel)]="name"/>
|
||||||
<label for="password">Mot de passe</label>
|
<label for="password">Mot de passe</label>
|
||||||
<input type="password" id="password" placeholder="motDePasseTrèsSecret" pInputText [(ngModel)]="password"/>
|
<input type="password" id="password" placeholder="motDePasseTrèsSecret" pInputText [(ngModel)]="password"/>
|
||||||
<label for="confirm-password">Confirmez le mot de passe</label>
|
<label for="confirm-password">Confirmez le mot de passe</label>
|
||||||
<input type="password" id="confirm-password" placeholder="motDePasseTrèsSecret" pInputText [(ngModel)]="confirmPassword" (keyup.enter)="sendLogins()" />
|
<input type="password" id="confirm-password" placeholder="motDePasseTrèsSecret" pInputText
|
||||||
|
[(ngModel)]="confirmPassword" (keyup.enter)="sendLogins()"/>
|
||||||
<p-button
|
<p-button
|
||||||
|
class="send-button"
|
||||||
label="Se connecter"
|
label="Se connecter"
|
||||||
icon="pi pi-check"
|
icon="pi pi-check"
|
||||||
(onClick)="sendLogins()"
|
(onClick)="sendLogins()"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
{{ actualAuthor | json }}
|
</div>
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
:host ::ng-deep .p-dialog {
|
||||||
|
width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-dialog {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3em;
|
||||||
|
}
|
@ -25,7 +25,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<p-button icon="pi pi-eye" (click)="openDialog(previewDialogVisibility, rowIndex)" severity="info"
|
<p-button icon="pi pi-eye" (click)="openDialog(previewDialogVisibility, rowIndex)" severity="info"
|
||||||
label="Prévisualiser"/>
|
label="Prévisualiser"/>
|
||||||
<p-dialog header='Prévisualisation de "{{ post.title }}"' [modal]="true"
|
<p-dialog class="preview-dialog" header='Prévisualisation de "{{ post.title }}"' [modal]="true"
|
||||||
[(visible)]="previewDialogVisibility[rowIndex]">
|
[(visible)]="previewDialogVisibility[rowIndex]">
|
||||||
<app-post-home [title]="post.title"
|
<app-post-home [title]="post.title"
|
||||||
[description]="post.description"
|
[description]="post.description"
|
||||||
@ -38,6 +38,13 @@
|
|||||||
<p-button icon="pi pi-pencil" (click)="openDialog(updateDialogVisibility, rowIndex)" severity="warning"
|
<p-button icon="pi pi-pencil" (click)="openDialog(updateDialogVisibility, rowIndex)" severity="warning"
|
||||||
label="Modifier"/>
|
label="Modifier"/>
|
||||||
<p-dialog header='Modifier "{{ post.title }}"' [modal]="true" [(visible)]="updateDialogVisibility[rowIndex]">
|
<p-dialog header='Modifier "{{ post.title }}"' [modal]="true" [(visible)]="updateDialogVisibility[rowIndex]">
|
||||||
|
<app-post-form [actualAuthor]="actualAuthor"
|
||||||
|
[postId]="post.id"
|
||||||
|
[isUpdateMode]="true"
|
||||||
|
[title]="post.title"
|
||||||
|
[category]="post.category"
|
||||||
|
[description]="post.description"
|
||||||
|
[body]="post.body"/>
|
||||||
</p-dialog>
|
</p-dialog>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@ -45,6 +52,7 @@
|
|||||||
label="Supprimer"/>
|
label="Supprimer"/>
|
||||||
<p-dialog header='Êtes-vous sur de bien vouloir supprimer "{{ post.title }}"' [modal]="true"
|
<p-dialog header='Êtes-vous sur de bien vouloir supprimer "{{ post.title }}"' [modal]="true"
|
||||||
[(visible)]="deleteDialogVisibility[rowIndex]">
|
[(visible)]="deleteDialogVisibility[rowIndex]">
|
||||||
|
<div class="delete-dialog">
|
||||||
<p-button label="Annuler"
|
<p-button label="Annuler"
|
||||||
icon="pi pi-times"
|
icon="pi pi-times"
|
||||||
severity="info"
|
severity="info"
|
||||||
@ -52,6 +60,7 @@
|
|||||||
<p-button (click)="deletePost(post.id, rowIndex)"
|
<p-button (click)="deletePost(post.id, rowIndex)"
|
||||||
label="Oui" icon="pi pi-trash"
|
label="Oui" icon="pi pi-trash"
|
||||||
severity="danger"/>
|
severity="danger"/>
|
||||||
|
</div>
|
||||||
</p-dialog>
|
</p-dialog>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -13,6 +13,7 @@ import {DialogModule} from 'primeng/dialog';
|
|||||||
import {InputTextModule} from 'primeng/inputtext';
|
import {InputTextModule} from 'primeng/inputtext';
|
||||||
import {PostHomeComponent} from '../../components/post-home/post-home.component';
|
import {PostHomeComponent} from '../../components/post-home/post-home.component';
|
||||||
import {PostService} from '../../services/post.service';
|
import {PostService} from '../../services/post.service';
|
||||||
|
import {PostFormComponent} from "../../components/post-form/post-form.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-my-posts',
|
selector: 'app-my-posts',
|
||||||
@ -24,7 +25,8 @@ import {PostService} from '../../services/post.service';
|
|||||||
DatePipe,
|
DatePipe,
|
||||||
DialogModule,
|
DialogModule,
|
||||||
InputTextModule,
|
InputTextModule,
|
||||||
PostHomeComponent
|
PostHomeComponent,
|
||||||
|
PostFormComponent
|
||||||
],
|
],
|
||||||
templateUrl: './my-posts.component.html',
|
templateUrl: './my-posts.component.html',
|
||||||
styleUrl: './my-posts.component.css'
|
styleUrl: './my-posts.component.css'
|
||||||
@ -35,14 +37,14 @@ export class MyPostsComponent implements OnDestroy {
|
|||||||
updateDialogVisibility: boolean[] = [];
|
updateDialogVisibility: boolean[] = [];
|
||||||
deleteDialogVisibility: boolean[] = [];
|
deleteDialogVisibility: boolean[] = [];
|
||||||
posts: Post[] = [];
|
posts: Post[] = [];
|
||||||
concernedAuthor: Author | undefined;
|
actualAuthor: Author | undefined;
|
||||||
|
|
||||||
|
|
||||||
constructor(private cookieService: CookieService,
|
constructor(private cookieService: CookieService,
|
||||||
private postService: PostService,
|
private postService: PostService,
|
||||||
private authorService: AuthorService,
|
private authorService: AuthorService,
|
||||||
private messageService: MessageService) {
|
private messageService: MessageService) {
|
||||||
this.concernedAuthor = this.cookieService.get('author') ? JSON.parse(this.cookieService.get('author')) : undefined;
|
this.actualAuthor = this.cookieService.get('author') ? JSON.parse(this.cookieService.get('author')) : undefined;
|
||||||
this.updatePosts();
|
this.updatePosts();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +60,7 @@ export class MyPostsComponent implements OnDestroy {
|
|||||||
|
|
||||||
updatePosts(): void {
|
updatePosts(): void {
|
||||||
if (this.cookieService.get('token')) {
|
if (this.cookieService.get('token')) {
|
||||||
this.authorService.getAuthorsPosts(this.concernedAuthor?.id, this.cookieService.get('token')).subscribe({
|
this.authorService.getAuthorsPosts(this.actualAuthor?.id, this.cookieService.get('token')).subscribe({
|
||||||
next: posts => this.posts = posts,
|
next: posts => this.posts = posts,
|
||||||
error: error => this.failureMessage("Erreur", error.message),
|
error: error => this.failureMessage("Erreur", error.message),
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,2 @@
|
|||||||
<app-header></app-header>
|
<app-header></app-header>
|
||||||
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
<app-post-form [actualAuthor]="actualAuthor"/>
|
||||||
<label for="title">Titre du post</label>
|
|
||||||
<input id="title" type="text" pInputText formControlName="title"/>
|
|
||||||
<label for="category">Catégorie du post</label>
|
|
||||||
<input pInputText id="category" formControlName="category" type="text"/>
|
|
||||||
<label for="desc">Description du post</label>
|
|
||||||
<textarea formControlName="description" id="desc" pInputTextarea></textarea>
|
|
||||||
<label>Image descriptive du post</label>
|
|
||||||
<p-fileUpload accept="image/*"
|
|
||||||
maxFileSize="1000000"
|
|
||||||
[showUploadButton]="false"
|
|
||||||
[showCancelButton]="false"
|
|
||||||
chooseLabel="Sélectionner une image"
|
|
||||||
(onSelect)="onSelected($event)">
|
|
||||||
<ng-template pTemplate="content">
|
|
||||||
</ng-template>
|
|
||||||
</p-fileUpload>
|
|
||||||
<p-editor formControlName="body" [style]="{ height: '320px' }"/>
|
|
||||||
<p-button type="submit" label="envoyer" ></p-button>
|
|
||||||
</form>
|
|
||||||
|
@ -13,6 +13,7 @@ import {EditorModule} from 'primeng/editor';
|
|||||||
import {AuthorService} from '../../services/author.service';
|
import {AuthorService} from '../../services/author.service';
|
||||||
import {Author} from '../../models/author';
|
import {Author} from '../../models/author';
|
||||||
import {Router} from '@angular/router';
|
import {Router} from '@angular/router';
|
||||||
|
import {PostFormComponent} from '../../components/post-form/post-form.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-new-post',
|
selector: 'app-new-post',
|
||||||
@ -23,7 +24,8 @@ import {Router} from '@angular/router';
|
|||||||
InputTextModule,
|
InputTextModule,
|
||||||
InputTextareaModule,
|
InputTextareaModule,
|
||||||
FileUploadModule,
|
FileUploadModule,
|
||||||
EditorModule
|
EditorModule,
|
||||||
|
PostFormComponent
|
||||||
],
|
],
|
||||||
templateUrl: './new-post.component.html',
|
templateUrl: './new-post.component.html',
|
||||||
styleUrl: './new-post.component.css'
|
styleUrl: './new-post.component.css'
|
||||||
|
0
src/app/pages/register/register.component.css
Normal file
0
src/app/pages/register/register.component.css
Normal file
@ -27,6 +27,16 @@ export class PostService {
|
|||||||
return this.httpClient.delete(`${this.url}/${id}`, httpOptions);
|
return this.httpClient.delete(`${this.url}/${id}`, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updatePost(postId: bigint, postData: any, token: string) {
|
||||||
|
const httpOptions = {
|
||||||
|
headers: new HttpHeaders({
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
})
|
||||||
|
};
|
||||||
|
return this.httpClient.put(`${this.url}/${postId}`, postData, httpOptions);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
createPost(post: any, token: string | undefined): Observable<Post> {
|
createPost(post: any, token: string | undefined): Observable<Post> {
|
||||||
const httpOptions = {
|
const httpOptions = {
|
||||||
headers: new HttpHeaders({
|
headers: new HttpHeaders({
|
||||||
@ -36,7 +46,7 @@ export class PostService {
|
|||||||
return this.httpClient.post<Post>(this.url, post, httpOptions);
|
return this.httpClient.post<Post>(this.url, post, httpOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
changeIllustration(id: bigint, image: File | undefined, token: string): Observable<Post> {
|
changeIllustration(id: bigint | undefined, image: File | undefined, token: string): Observable<Post> {
|
||||||
if (image) {
|
if (image) {
|
||||||
const formData: FormData = new FormData();
|
const formData: FormData = new FormData();
|
||||||
formData.append('illustration', image);
|
formData.append('illustration', image);
|
||||||
|
Loading…
Reference in New Issue
Block a user