From d9169d18b312dea3842197fd501084ff605d4522 Mon Sep 17 00:00:00 2001 From: Hugo SUBTIL <ext.sopra.husubtil@grandlyon.com> Date: Wed, 10 Mar 2021 10:50:40 +0100 Subject: [PATCH] Feat/post tags --- src/app/app-routing.module.ts | 2 +- src/app/header/header.component.html | 12 +- .../post-card/post-card.component.ts | 2 +- .../post-details/post-details.component.ts | 2 +- .../post-header/post-header.component.html | 76 ++++++++- .../post-header/post-header.component.scss | 150 ++++++++++++++++++ .../post-header/post-header.component.ts | 97 ++++++++++- .../post-list/post-list.component.html | 65 ++++++-- .../post-list/post-list.component.scss | 21 ++- .../post-list/post-list.component.ts | 123 ++++++++++++-- .../post-modal-filters.component.html | 34 ++++ .../post-modal-filters.component.scss | 132 +++++++++++++++ .../post-modal-filters.component.spec.ts | 25 +++ .../post-modal-filters.component.ts | 72 +++++++++ src/app/post/components/utils/NewsUtils.ts | 11 ++ src/app/post/enum/tag.enum.ts | 6 + src/app/post/enum/typeModalNews.enum.ts | 4 + src/app/post/models/tag.model.ts | 6 + src/app/post/models/tagWithMeta.model.ts | 7 + src/app/post/news.component.html | 3 +- src/app/post/news.component.ts | 6 + src/app/post/post-routing.module.ts | 4 + src/app/post/post.module.ts | 12 +- src/app/post/resolvers/tags.resolver.ts | 21 +++ src/app/post/services/post.service.ts | 10 +- src/app/profile/services/profile.service.ts | 1 - .../svg-icon/svg-icon.component.scss | 12 ++ src/assets/ico/sprite.svg | 22 +++ src/assets/scss/_layout.scss | 2 +- src/index.html | 2 - 30 files changed, 888 insertions(+), 54 deletions(-) create mode 100644 src/app/post/components/post-modal-filters/post-modal-filters.component.html create mode 100644 src/app/post/components/post-modal-filters/post-modal-filters.component.scss create mode 100644 src/app/post/components/post-modal-filters/post-modal-filters.component.spec.ts create mode 100644 src/app/post/components/post-modal-filters/post-modal-filters.component.ts create mode 100644 src/app/post/components/utils/NewsUtils.ts create mode 100644 src/app/post/enum/typeModalNews.enum.ts create mode 100644 src/app/post/models/tagWithMeta.model.ts create mode 100644 src/app/post/resolvers/tags.resolver.ts diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 9b85e5e1b..b8a27b65e 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -88,7 +88,7 @@ const routes: Routes = [ canDeactivate: [DeactivateGuard], }, { - path: 'posts', + path: 'news', loadChildren: () => import('./post/post.module').then((m) => m.PostModule), }, { diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index 9d31ad373..7fc0ea9a6 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -9,14 +9,10 @@ <app-svg-icon (click)="openMenu()" [type]="'ico'" [icon]="'menu'" [iconClass]="'icon-32'"></app-svg-icon> </div> <div fxLayout="row" class="right-header" fxLayoutAlign="center center" fxLayoutGap="3vw"> - <a routerLink="/home" [routerLinkActive]="'active'" i18n>Les acteurs</a> + <a routerLink="/news" [routerLinkActive]="'active'" i18n>Actualités</a> + <a routerLink="/home" [routerLinkActive]="'active'" i18n>Cartographie de acteurs</a> <a routerLink="/about" [routerLinkActive]="'active'" i18n>Qui sommes-nous ?</a> - <!-- <a routerLink="/news" [routerLinkActive]="'active'" i18n>Actualités</a> --> - <!-- <a routerLink="/resources" [routerLinkActive]="'active'" i18n>Ressources</a> --> <a *ngIf="isAdmin" routerLink="/admin" [routerLinkActive]="'active'">Administration</a> - <!-- <a *ngIf="isLoggedIn" routerLink="/profile" [routerLinkActive]="'active'" fxLayout="row" fxLayoutGap="1.5vh"> - <app-svg-icon [type]="'ico'" [iconClass]="'icon-32'" [icon]="'user'" [iconColor]="'currentColor'"></app-svg-icon> - </a> --> <button *ngIf="isLoggedIn" class="red" routerLink="/profile" [routerLinkActive]="'active'"> {{ displayName }} </button> @@ -32,7 +28,8 @@ <div (click)="closeMenu()" class="ico-close-details"></div> </div> <div fxLayout="column" class="right-header" fxLayoutAlign="none baseline" fxLayoutGap="5vw"> - <a routerLink="/home" [routerLinkActive]="'active'" (click)="closeMenu()" i18n>Les acteurs</a> + <a routerLink="/news" [routerLinkActive]="'active'" (click)="closeMenu()" i18n>Actualités</a> + <a routerLink="/home" [routerLinkActive]="'active'" (click)="closeMenu()" i18n>Cartographie de acteurs</a> <a routerLink="/about" [routerLinkActive]="'active'" i18n>Qui sommes-nous ?</a> <a *ngIf="isAdmin" routerLink="/admin" [routerLinkActive]="'active'" (click)="closeMenu()">Administration</a> </div> @@ -47,7 +44,6 @@ </div> <app-signup-modal *ngIf="displaySignUp" [openned]="isPopUpOpen" (closed)="closeSignUpModal($event)"></app-signup-modal> -<!-- <app-signin-modal *ngIf="!displaySignUp" [openned]="isPopUpOpen" (closed)="closeSignInModal()"></app-signin-modal> --> <ng-template #customTitle> <img class="desktop-show logo-grand-lyon" src="/assets/logos/resin.svg" alt /> diff --git a/src/app/post/components/post-card/post-card.component.ts b/src/app/post/components/post-card/post-card.component.ts index e97a5fe74..78970f17f 100644 --- a/src/app/post/components/post-card/post-card.component.ts +++ b/src/app/post/components/post-card/post-card.component.ts @@ -16,6 +16,6 @@ export class PostCardComponent implements OnInit { ngOnInit(): void {} public showDetails(post: Post): void { - this.router.navigateByUrl('posts/details/' + post.id, { state: { data: post } }); + this.router.navigateByUrl('news/details/' + post.id, { state: { data: post } }); } } diff --git a/src/app/post/components/post-details/post-details.component.ts b/src/app/post/components/post-details/post-details.component.ts index 31f7b0f62..797763328 100644 --- a/src/app/post/components/post-details/post-details.component.ts +++ b/src/app/post/components/post-details/post-details.component.ts @@ -31,6 +31,6 @@ export class PostDetailsComponent implements OnInit { } public backToPosts(): void { - this.router.navigateByUrl('/posts'); + this.router.navigateByUrl('/news'); } } diff --git a/src/app/post/components/post-header/post-header.component.html b/src/app/post/components/post-header/post-header.component.html index 6d9a4faab..6b48f6d33 100644 --- a/src/app/post/components/post-header/post-header.component.html +++ b/src/app/post/components/post-header/post-header.component.html @@ -1 +1,75 @@ -<div class="header-container">post-header works!</div> +<div class="header-container"> + <div class="section-container" fxLayout="column" fxLayoutAlign="space-between"> + <h1>Actualités</h1> + <div fxLayout="row" fxLayoutGap="5px" fxLayoutAlign="space-between flex-end" class="overflow"> + <div fxLayout="row" class="row-mobile"> + <div fxLayout="row" fxLayoutAlign="center center" *ngFor="let tag of tags.others"> + <span + class="tag-button" + tabindex="0" + (click)="activateTag(tag)" + (keydown.enter)="activateTag(tag)" + [ngClass]="{ active: tag.slug === mainActiveTag.slug }" + >{{ tag.name }}</span + > + </div> + </div> + <div + class="btnSection" + fxLayout="row" + fxLayoutAlign="space-between center" + fxLayoutGap="16px" + (clickOutside)="closeModal()" + > + <button + type="button" + fxLayout="row" + [ngClass]="{ + selected: modalTypeOpened === TypeModal.public, + containCheckedFilters: checkedPublicTags.length > 0 + }" + fxLayoutAlign="space-between center" + (click)="openModal(TypeModal.public)" + > + <span class="text">Tout public</span> + <app-svg-icon + class="icon" + [type]="'ico'" + [iconClass]="modalTypeOpened === TypeModal.public ? 'white' : 'grey-1'" + [icon]="'news-public'" + ></app-svg-icon> + <div class="arrow"></div> + </button> + <button + class="btn-filter-no-margin" + type="button" + fxLayout="row" + [ngClass]="{ + selected: modalTypeOpened === TypeModal.location, + containCheckedFilters: checkedLocationTags.length > 0 + }" + fxLayoutAlign="space-between center" + (click)="openModal(TypeModal.location)" + > + <span class="text">Toutes les communes</span> + <app-svg-icon + class="icon" + [type]="'ico'" + [iconClass]="modalTypeOpened === TypeModal.location ? 'white' : 'grey-1'" + [icon]="'news-location'" + ></app-svg-icon> + <div class="arrow"></div> + </button> + <div *ngIf="modalTypeOpened"> + <app-post-modal-filters + [modalType]="modalTypeOpened" + [tags]="getModalData()" + [inputCheckedTags]="getCheckedModalData()" + (searchEvent)="filter($event)" + (closeEvent)="closeModal()" + ></app-post-modal-filters> + </div> + </div> + </div> + </div> +</div> diff --git a/src/app/post/components/post-header/post-header.component.scss b/src/app/post/components/post-header/post-header.component.scss index 526d11201..46d4f71b1 100644 --- a/src/app/post/components/post-header/post-header.component.scss +++ b/src/app/post/components/post-header/post-header.component.scss @@ -1,7 +1,157 @@ @import '../../../../assets/scss/color'; +@import '../../../../assets/scss/buttons'; +@import '../../../../assets/scss/typography'; +@import '../../../../assets/scss/breakpoint'; @import '../../../../assets/scss/layout'; +h1 { + margin-bottom: 0px; +} + .header-container { height: #{$header-post-height}; background: $white; } + +.section-container { + height: 100%; +} + +.btnSection { + padding: 16px 0 0px 0; + button { + background: $white; + height: 40px; + width: 100%; + border: 1px solid $grey-4; + padding: 3px 16px 3px 16px; + outline: none; + border-radius: 4px; + cursor: pointer; + white-space: nowrap; + @include btn-normal; + .arrow { + background-color: transparent; + border-bottom: 1px solid $grey-2; + border-right: 1px solid $grey-2; + transform: translateY(-25%) rotate(45deg); + margin: 0 5px 0 10px; + height: 7px; + width: 7px; + } + &:focus { + border-color: $blue-hover; + } + } + .selected { + background-color: $secondary-color; + border-color: $secondary-color !important; + color: $white; + .arrow { + background-color: transparent; + border-bottom: 1px solid $white; + border-right: 1px solid $white; + transform: translateY(25%) rotate(-135deg); + margin: 0 5px 0 10px; + height: 7px; + width: 7px; + } + } + .containCheckedFilters { + border-color: $secondary-color; + } + .icon { + display: none !important; + } + @media #{$desktop} { + button { + width: 90px; + } + .text { + display: none !important; + } + .icon { + display: inherit !important; + } + } + @media #{$tablet} { + button { + display: none !important; + } + } +} + +// .btnSection { +// padding: 16px 0 0px 0; +// button { +// background: $white; +// height: 40px; +// width: 210px; +// border: 1px solid $grey-4; +// padding: 3px 16px 3px 16px; +// outline: none; +// border-radius: 4px; +// cursor: pointer; +// white-space: nowrap; +// @include btn-normal; +// .arrow { +// background-color: transparent; +// border-bottom: 1px solid $grey-2; +// border-right: 1px solid $grey-2; +// transform: translateY(-25%) rotate(45deg); +// margin: 0 5px 0 10px; +// height: 7px; +// width: 7px; +// } +// &:focus { +// border-color: $blue-hover; +// } +// } +// .selected { +// border-color: $primary-color !important; +// color: inherit; +// .arrow { +// background-color: transparent; +// border-bottom: 1px solid $primary-color; +// border-right: 1px solid $primary-color; +// transform: translateY(25%) rotate(-135deg); +// margin: 0 5px 0 10px; +// height: 7px; +// width: 7px; +// } +// } +// .containCheckedFilters { +// border-color: $secondary-color; +// } +// } + +.tag-button { + padding: 8px 10px; + @include cn-regular-16; + cursor: pointer; + white-space: nowrap; + &.active { + background-color: $secondary-color; + color: $white; + } + &:focus { + outline-color: $secondary-color; + } +} + +// Remove margin right on filter pop-up trigger +.btn-filter-no-margin { + margin-right: 0 !important; +} + +.row-mobile { + @media #{$tablet} { + width: 100%; + justify-content: space-between; + } +} +.overflow { + @media #{$tablet} { + overflow-y: scroll; + } +} diff --git a/src/app/post/components/post-header/post-header.component.ts b/src/app/post/components/post-header/post-header.component.ts index 4927d262b..1cbc7f338 100644 --- a/src/app/post/components/post-header/post-header.component.ts +++ b/src/app/post/components/post-header/post-header.component.ts @@ -1,15 +1,106 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { Tag } from '../../models/tag.model'; +import { TagWithMeta } from '../../models/tagWithMeta.model'; +import * as _ from 'lodash'; +import { TypeModalNews } from '../../enum/typeModalNews.enum'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TagEnum } from '../../enum/tag.enum'; +import { parseSlugToTag } from '../utils/NewsUtils'; @Component({ selector: 'app-post-header', templateUrl: './post-header.component.html', - styleUrls: ['./post-header.component.scss'] + styleUrls: ['./post-header.component.scss'], }) export class PostHeaderComponent implements OnInit { + public modalTypeOpened: TypeModalNews; + public tags: TagWithMeta; + public mainActiveTag: Tag = new Tag({ slug: TagEnum.aLaUne }); + public tagEnum = TagEnum; - constructor() { } + public checkedPublicTags: Tag[] = []; + public checkedLocationTags: Tag[] = []; + + constructor(private route: ActivatedRoute, private router: Router) {} ngOnInit(): void { + this.route.data.subscribe((data) => { + if (data.tags) { + this.tags = data.tags; + } + }); + + this.route.queryParams.subscribe((queryParams) => { + if (queryParams.mainTag) { + this.mainActiveTag = new Tag({ slug: queryParams.mainTag }); + } + if (queryParams.publicTags) { + this.checkedPublicTags = parseSlugToTag(queryParams.publicTags); + } + if (queryParams.locationTags) { + this.checkedLocationTags = parseSlugToTag(queryParams.locationTags); + } + }); + } + + // Open the modal and display the list according to the right filter button + public openModal(modalType: TypeModalNews): void { + // if modal already opened, reset type + if (this.modalTypeOpened === modalType) { + this.closeModal(); + } else if (this.modalTypeOpened !== modalType) { + this.modalTypeOpened = modalType; + } + } + + public closeModal(): void { + this.modalTypeOpened = undefined; + } + + // Accessor to template angular. + public get TypeModal(): typeof TypeModalNews { + return TypeModalNews; + } + + public getModalData(): Tag[] { + if (this.modalTypeOpened === this.TypeModal.public) { + return this.tags.public; + } + return this.tags.commune; + } + + public getCheckedModalData(): Tag[] { + if (this.modalTypeOpened === this.TypeModal.public) { + return this.checkedPublicTags; + } + return this.checkedLocationTags; + } + + public activateTag(tag: Tag): void { + this.mainActiveTag = tag; + this.setQueryParam(); } + public filter(data: Tag[]): void { + if (this.modalTypeOpened === this.TypeModal.public) { + this.checkedPublicTags = data; + } else { + this.checkedLocationTags = data; + } + + this.setQueryParam(); + this.closeModal(); + } + + private setQueryParam(): void { + this.router.navigate([], { + relativeTo: this.route, + queryParams: { + mainTag: this.mainActiveTag.slug, + publicTags: this.checkedPublicTags.map((tag) => tag.slug), + locationTags: this.checkedLocationTags.map((tag) => tag.slug), + }, + queryParamsHandling: 'merge', + }); + } } diff --git a/src/app/post/components/post-list/post-list.component.html b/src/app/post/components/post-list/post-list.component.html index 190d108eb..edb7da66c 100644 --- a/src/app/post/components/post-list/post-list.component.html +++ b/src/app/post/components/post-list/post-list.component.html @@ -1,6 +1,21 @@ <div class="section-container" fxLayout="row" fxLayoutGap="32px"> <div fxLayout="column" class="list-container" fxLayoutGap="16px"> - <div fxLayout="column"> + <div fxLayout="column" *ngIf="displayTags()"> + <div fxLayout="row wrap" fxLayoutAlign="none center" fxLayoutGap="8px"> + <div + fxLayout="row" + fxLayoutAlign="start center" + fxLayoutGap="9px" + *ngFor="let filter of filters | slice: 1" + class="tag" + (click)="removeTag(filter)" + > + <p>{{ filter.slug | titlecase }}</p> + <app-svg-icon [type]="'ico'" [iconColor]="'currentColor'" [icon]="'cancel'"></app-svg-icon> + </div> + </div> + </div> + <div fxLayout="column" *ngIf="isALaUneTag() && !displayTags()"> <div fxLayout="row" class="row-border" fxLayoutAlign="space-between center"> <h2>à la une</h2> <app-button @@ -16,18 +31,33 @@ <div class="background-project-container"> <div class="project-content mobile" fxLayout="column"> <h2>appels à projets</h2> - <app-post-card - [post]="news" - [class]="'project'" - [ngClass]="{ 'last-child': last }" - *ngFor="let news of projectsNew; let last = last" - ></app-post-card> + <div *ngIf="projectsNew.length !== 0"> + <app-post-card + [post]="news" + [class]="'project'" + [ngClass]="{ 'last-child': last }" + *ngFor="let news of projectsNew; let last = last" + ></app-post-card> + </div> + <div *ngIf="projectsNew.length === 0"> + <p>Aucun appels à projet pour le moment.</p> + </div> </div> </div> </div> <div fxLayout="column"> - <div fxLayout="row" class="row-border otherNews"> - <h2>autres actualités</h2> + <div fxLayout="row" class="row-border" fxLayoutAlign="space-between center"> + <h2 [ngClass]="{ 'padding-16-Top': isALaUneTag() && !displayTags() }">{{ getDisplayedTag() }}</h2> + <app-button + *ngIf="displayTags() || !isALaUneTag()" + [type]="'button'" + [style]="'buttonWithHash'" + [text]="'Publier votre actu'" + (action)="publishNews()" + ></app-button> + </div> + <div *ngIf="leftColumnPosts.length <= 0" fxLayout="column"> + <p>Aucun résultat ne correspond a votre recherche.</p> </div> <div fxLayout="row" fxLayoutGap="33px"> <div fxLayout="column" class="columnPosts"> @@ -47,12 +77,17 @@ <div class="project-content" fxLayout="column"> <app-svg-icon [iconClass]="'icon-80'" [iconColor]="'inherit'" [type]="'post'" [icon]="'appels'"></app-svg-icon> <h2>appels à projets</h2> - <app-post-card - [post]="news" - [class]="'project'" - [ngClass]="{ 'last-child': last }" - *ngFor="let news of projectsNew; let last = last" - ></app-post-card> + <div *ngIf="projectsNew.length !== 0"> + <app-post-card + [post]="news" + [class]="'project'" + [ngClass]="{ 'last-child': last }" + *ngFor="let news of projectsNew; let last = last" + ></app-post-card> + </div> + <div *ngIf="projectsNew.length === 0"> + <p>Aucun appels à projet pour le moment.</p> + </div> </div> </div> </div> diff --git a/src/app/post/components/post-list/post-list.component.scss b/src/app/post/components/post-list/post-list.component.scss index 27a518b91..ea2f433a9 100644 --- a/src/app/post/components/post-list/post-list.component.scss +++ b/src/app/post/components/post-list/post-list.component.scss @@ -6,12 +6,11 @@ .section-container { background: $grey-6; margin-top: 40px; + min-height: 68vh; + width: 100%; .row-border { border-bottom: 1px dashed $grey-4; padding-bottom: 16px; - &.otherNews { - padding-top: 24px; - } } h2 { font-style: italic !important; @@ -100,3 +99,19 @@ h2 { } } } +.tag { + background: $grey-3; + color: $white; + border-radius: 20px; + cursor: pointer; + margin-bottom: 10px; + padding: 0 20px; + p { + margin-top: 0; + margin-bottom: 0; + } +} + +.padding-16-Top { + padding-top: 16px; +} diff --git a/src/app/post/components/post-list/post-list.component.ts b/src/app/post/components/post-list/post-list.component.ts index 807c2b9c2..3ec4088c5 100644 --- a/src/app/post/components/post-list/post-list.component.ts +++ b/src/app/post/components/post-list/post-list.component.ts @@ -3,8 +3,12 @@ import { WindowScrollService } from '../../../shared/service/windowScroll.servic import { TagEnum } from '../../enum/tag.enum'; import { Pagination } from '../../models/pagination.model'; import { Post } from '../../models/post.model'; +import { Tag } from '../../models/tag.model'; import { PostWithMeta } from '../../models/postWithMeta.model'; import { PostService } from '../../services/post.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import * as _ from 'lodash'; +import { parseSlugToTag } from '../utils/NewsUtils'; @Component({ selector: 'app-post-list', @@ -12,7 +16,24 @@ import { PostService } from '../../services/post.service'; styleUrls: ['./post-list.component.scss'], }) export class PostListComponent implements OnInit { - constructor(private postService: PostService, private windowScrollService: WindowScrollService) { + public selectedMainTagSlug = ''; + public selectedLocationTagSlug = []; + public selectedPublicTagsSlug = []; + public filters: Tag[]; + public postsMobileView: Post[] = []; + public leftColumnPosts: Post[] = []; + public rightColumnPosts: Post[] = []; + public projectsNew: Post[] = []; + public bigNews: Post; + public pagination: Pagination; + public isLoading = false; + + constructor( + private postService: PostService, + private windowScrollService: WindowScrollService, + private route: ActivatedRoute, + private router: Router + ) { this.windowScrollService.scrollY$.subscribe((evt: any) => { if (evt && evt.target.offsetHeight + evt.target.scrollTop >= evt.target.scrollHeight - 200) { if (!this.isLoading) { @@ -21,28 +42,86 @@ export class PostListComponent implements OnInit { } }); } - public postsMobileView: Post[] = []; - public leftColumnPosts: Post[] = []; - public rightColumnPosts: Post[] = []; - public projectsNew: Post[] = []; - public bigNews: Post; - public pagination: Pagination; - public isLoading = false; ngOnInit(): void { this.isLoading = true; - this.postService.getPosts(1).subscribe((news) => { - this.setNews(news); + // Init APP news list + this.postService.getPosts(1, [TagEnum.appels]).subscribe((news) => { + let projectNews = news.posts.map((news) => (news = this.addAuthorToPost(news))); + this.projectsNew = projectNews; }); this.postService.getPosts(1, [TagEnum.aLaUne]).subscribe((news) => { this.bigNews = this.addAuthorToPost(news.posts[0]); }); - this.postService.getPosts(1, [TagEnum.appels]).subscribe((news) => { - let projectNews = news.posts.map((news) => (news = this.addAuthorToPost(news))); - this.projectsNew = projectNews; + this.route.queryParams.subscribe((queryParams) => { + // If main tag is in route, set it + if (queryParams.mainTag) { + this.selectedMainTagSlug = queryParams.mainTag; + this.selectedPublicTagsSlug = parseSlugToTag(queryParams.publicTags); + this.selectedLocationTagSlug = parseSlugToTag(queryParams.locationTags); + // Set filters for search and display + this.filters = [ + new Tag({ slug: queryParams.mainTag }), + ...this.selectedLocationTagSlug, + ...this.selectedPublicTagsSlug, + ]; + // Apply search + this.getPosts(this.filters); + } else { + // Init default news list + this.postService.getPosts(1).subscribe((news) => { + this.setNews(news); + }); + } }); } + public getPosts(filters?: Tag[]): void { + // Parse filter + let parsedFilters = null; + if (filters) { + parsedFilters = filters.map((filter) => { + return filter.slug; + }); + + // remove 'a la une' filter + parsedFilters = parsedFilters.filter((item) => { + return item !== TagEnum.aLaUne; + }); + + if (parsedFilters.length <= 0) { + parsedFilters = null; + } + } + + // Reset posts + this.resetPosts(); + + this.postService.getPosts(1, parsedFilters).subscribe((news) => { + this.setNews(news); + }); + } + + public getDisplayedTag(): string { + if (!this.isALaUneTag()) { + return this.selectedMainTagSlug; + } + return 'autres actualités'; + } + + public isALaUneTag(): boolean { + if (!this.filters || this.filters[0].slug === TagEnum.aLaUne) { + return true; + } + return false; + } + + public resetPosts(): void { + this.leftColumnPosts = []; + this.rightColumnPosts = []; + this.postsMobileView = []; + } + public publishNews(): void {} //Transform excerpt post to have a custom author. @@ -78,4 +157,22 @@ export class PostListComponent implements OnInit { }); this.isLoading = false; } + + public removeTag(tagToRemove: Tag): void { + _.remove(this.selectedPublicTagsSlug, { slug: tagToRemove.slug }); + _.remove(this.selectedLocationTagSlug, { slug: tagToRemove.slug }); + this.router.navigate([], { + relativeTo: this.route, + queryParams: { + mainTag: this.selectedMainTagSlug, + publicTags: this.selectedPublicTagsSlug.map((tag) => tag.slug), + locationTags: this.selectedLocationTagSlug.map((tag) => tag.slug), + }, + queryParamsHandling: 'merge', + }); + } + + public displayTags(): boolean { + return this.selectedLocationTagSlug.length > 0 || this.selectedPublicTagsSlug.length > 0; + } } diff --git a/src/app/post/components/post-modal-filters/post-modal-filters.component.html b/src/app/post/components/post-modal-filters/post-modal-filters.component.html new file mode 100644 index 000000000..7ee54d56c --- /dev/null +++ b/src/app/post/components/post-modal-filters/post-modal-filters.component.html @@ -0,0 +1,34 @@ +<div *ngIf="modalType" fxLayout="column" fxLayoutAlign="space-between" [ngClass]="['modal', 'modal' + getModalType()]"> + <div class="body-wrap" fxLayout="column" fxLayoutAlign="space-between"> + <div class="titleFilter" fxLayout="row" fxLayoutAlign="space-between"> + <span>Filtres</span> + <div (click)="closeModal()" class="ico-close-details"></div> + </div> + <div class="contentModal" fxLayout="row wrap" fxLayoutAlign="flex-start" *ngIf="tags.length > 0"> + <div class="blockFiltre"> + <ul class="blockLigne"> + <div fxLayout="row" class="ligneFiltre" *ngFor="let tag of tags"> + <li class="checkbox"> + <div class="checkboxItem"> + <label> + <input + type="checkbox" + [checked]="getIndex(checkedTags, tag.slug) > -1" + [value]="tag.id" + (change)="onCheckboxChange($event, tag)" + /> + <span class="customCheck"></span> + <div class="label">{{ tag.name }}</div> + </label> + </div> + </li> + </div> + </ul> + </div> + </div> + <div class="footer" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="3vw"> + <a (click)="clearFilters()" tabindex="0">Effacer</a> + <app-button [style]="'button'" [text]="'Appliquer'" (click)="emit(checkedTags)"></app-button> + </div> + </div> +</div> diff --git a/src/app/post/components/post-modal-filters/post-modal-filters.component.scss b/src/app/post/components/post-modal-filters/post-modal-filters.component.scss new file mode 100644 index 000000000..c897edd1c --- /dev/null +++ b/src/app/post/components/post-modal-filters/post-modal-filters.component.scss @@ -0,0 +1,132 @@ +@import '../../../../assets/scss/icons'; +@import '../../../../assets/scss/color'; +@import '../../../../assets/scss/typography'; +@import '../../../../assets/scss/breakpoint'; +@import '../../../../assets/scss/shapes'; +@import '../../../../assets/scss/hyperlink'; +@import '../../../../assets/scss/z-index'; + +.modallocation { + margin-left: -341px; +} +.modalpublic { + @media #{$desktop} { + margin-left: -446px; + } + margin-left: -566px; +} +.modal { + max-width: 341px; + width: 94%; + z-index: $modal-z-index !important; + position: absolute; + border-radius: 6px; + margin-top: 24px; + @media #{$large-phone} { + height: 100%; + max-height: auto; + max-width: auto; + width: 100%; + position: fixed; + top: 0; + left: 0; + border: none; + padding: 0; + } + @include background-hash($grey-2); + border: 1px solid $grey-4; + ::-webkit-scrollbar { + width: 16px; + } + ::-webkit-scrollbar-track { + background: $grey-6; + } + ::-webkit-scrollbar-thumb { + background: $grey; + border-radius: 6px; + } + .body-wrap { + @media #{$large-phone} { + height: 100vh; + height: -webkit-fill-available; + } + .titleFilter { + display: none !important; + margin: 27px 25px 0px 25px; + @include cn-bold-26; + @media #{$large-phone} { + display: flex !important; + } + } + } + .contentModal { + overflow-y: auto; + max-width: 1100px; + border-bottom: 1px solid $grey; + margin-bottom: 16px; + max-height: 40vh; + @media #{$large-phone} { + max-height: none; + height: 100%; + } + .blockFiltre { + width: 100%; + margin: 0 32px; + padding: 40px 0; + border-bottom: 1px dashed $grey-4; + + &:last-child { + padding-bottom: 32px; + border-bottom: none; + } + @media #{$large-phone} { + margin: 0 18px; + padding: 25px 0; + } + } + .blockLigne { + padding-left: 0; + -moz-column-count: 1; + -moz-column-gap: 46px; + -webkit-column-count: 1; + -webkit-column-gap: 46px; + column-count: 1; + column-gap: 46px; + margin: 0px; + @media #{$large-phone} { + -moz-column-count: 1; + -webkit-column-count: 1; + column-count: 1; + } + } + .ligneFiltre { + padding: 5px 0; + } + h4 { + @include cn-bold-16; + line-height: 17px; + text-transform: uppercase; + color: $grey-3; + display: flex; + align-items: center; + margin-top: 0; + margin-bottom: 9px; + } + .nbResult { + @include cn-regular-14; + line-height: 16px; + color: $grey-3; + padding-top: 3px; + } + label { + @include cn-regular-14; + } + } + .footer { + margin: 0px 20px 16px 0; + height: 32px; + } +} +a { + @include hyperlink; +} diff --git a/src/app/post/components/post-modal-filters/post-modal-filters.component.spec.ts b/src/app/post/components/post-modal-filters/post-modal-filters.component.spec.ts new file mode 100644 index 000000000..601a25abe --- /dev/null +++ b/src/app/post/components/post-modal-filters/post-modal-filters.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PostModalFiltersComponent } from './post-modal-filters.component'; + +describe('PostModalFiltersComponent', () => { + let component: PostModalFiltersComponent; + let fixture: ComponentFixture<PostModalFiltersComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ PostModalFiltersComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PostModalFiltersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/post/components/post-modal-filters/post-modal-filters.component.ts b/src/app/post/components/post-modal-filters/post-modal-filters.component.ts new file mode 100644 index 000000000..6624c5d0e --- /dev/null +++ b/src/app/post/components/post-modal-filters/post-modal-filters.component.ts @@ -0,0 +1,72 @@ +import { Component, EventEmitter, Input, OnInit, Output, SimpleChanges } from '@angular/core'; +import { Tag } from '../../models/tag.model'; +import { TypeModalNews } from '../../enum/typeModalNews.enum'; +import { OnChanges } from '@angular/core'; + +@Component({ + selector: 'app-post-modal-filters', + templateUrl: './post-modal-filters.component.html', + styleUrls: ['./post-modal-filters.component.scss'], +}) +export class PostModalFiltersComponent implements OnInit, OnChanges { + @Input() public modalType: TypeModalNews; + @Input() public tags: Tag[]; + @Output() searchEvent = new EventEmitter(); + @Output() closeEvent = new EventEmitter(); + // Checkbox variable + @Input() public inputCheckedTags: Tag[] = []; + public checkedTags: Tag[] = []; + + constructor() {} + + ngOnChanges(changes: SimpleChanges): void { + if (changes.inputCheckedTags) { + this.checkedTags = this.inputCheckedTags; + } + } + + ngOnInit(): void { + this.checkedTags = this.inputCheckedTags; + } + + // Management of the checkbox event (Check / Uncheck) + public onCheckboxChange(event, tag: Tag): void { + if (event.target.checked) { + this.checkedTags.push(tag); + } else { + // Check if the module is present in the list and remove it + if (this.getIndex(this.checkedTags, tag.slug) > -1) { + this.checkedTags.splice(this.getIndex(this.checkedTags, tag.slug), 1); + } + } + } + + // Clear only filters in the current modal + public clearFilters(): void { + this.checkedTags = []; + this.searchEvent.emit(this.checkedTags); + } + + public getModalType(): string { + switch (this.modalType) { + case TypeModalNews.location: + return 'location'; + case TypeModalNews.public: + return 'public'; + default: + return ''; + } + } + + public emit(data: Tag[]): void { + this.searchEvent.emit(data); + } + + public getIndex(array: Tag[], slug: string): number { + return array.findIndex((tag: Tag) => tag.slug === slug); + } + + public closeModal(): void { + this.closeEvent.emit(); + } +} diff --git a/src/app/post/components/utils/NewsUtils.ts b/src/app/post/components/utils/NewsUtils.ts new file mode 100644 index 000000000..4bb4eea6b --- /dev/null +++ b/src/app/post/components/utils/NewsUtils.ts @@ -0,0 +1,11 @@ +import { Tag } from '../../models/tag.model'; + +export function parseSlugToTag(data: Tag[] | string): Tag[] { + let otherTags = []; + if (Array.isArray(data)) { + otherTags = data.map((slug) => new Tag({ slug: slug })); + } else if (data) { + otherTags = [new Tag({ slug: data })]; + } + return otherTags; +} diff --git a/src/app/post/enum/tag.enum.ts b/src/app/post/enum/tag.enum.ts index 0b6c049d9..8b95ae41b 100644 --- a/src/app/post/enum/tag.enum.ts +++ b/src/app/post/enum/tag.enum.ts @@ -1,4 +1,10 @@ export enum TagEnum { aLaUne = 'a-la-une', appels = 'appels', + projets = 'projets', + formations = 'formations', + infos = 'infos', + dossiers = 'dossiers', + etudes = 'etudes', + ressources = 'ressources', } diff --git a/src/app/post/enum/typeModalNews.enum.ts b/src/app/post/enum/typeModalNews.enum.ts new file mode 100644 index 000000000..d15c4e702 --- /dev/null +++ b/src/app/post/enum/typeModalNews.enum.ts @@ -0,0 +1,4 @@ +export enum TypeModalNews { + public = 1, + location, +} diff --git a/src/app/post/models/tag.model.ts b/src/app/post/models/tag.model.ts index df72a66b1..53b77b9e0 100644 --- a/src/app/post/models/tag.model.ts +++ b/src/app/post/models/tag.model.ts @@ -1,4 +1,10 @@ export class Tag { + id: string; name: string; slug: string; + description: string; // Description is used to ut categories on tags + + constructor(obj?: any) { + Object.assign(this, obj); + } } diff --git a/src/app/post/models/tagWithMeta.model.ts b/src/app/post/models/tagWithMeta.model.ts new file mode 100644 index 000000000..6b28aa489 --- /dev/null +++ b/src/app/post/models/tagWithMeta.model.ts @@ -0,0 +1,7 @@ +import { Tag } from './tag.model'; + +export class TagWithMeta { + commune: Tag[]; + public: Tag[]; + others: Tag[]; +} diff --git a/src/app/post/news.component.html b/src/app/post/news.component.html index ba58f058b..93d255936 100644 --- a/src/app/post/news.component.html +++ b/src/app/post/news.component.html @@ -1,4 +1,5 @@ -<app-post-header></app-post-header> +<app-post-header (filterTags)="setFilters($event)"></app-post-header> <div class="section-container"> + <!-- <app-post-list [filters]="filters"></app-post-list> --> <router-outlet></router-outlet> </div> diff --git a/src/app/post/news.component.ts b/src/app/post/news.component.ts index ce2f45998..40e237776 100644 --- a/src/app/post/news.component.ts +++ b/src/app/post/news.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit } from '@angular/core'; +import { Tag } from './models/tag.model'; @Component({ selector: 'app-news', @@ -6,7 +7,12 @@ import { Component, OnInit } from '@angular/core'; styleUrls: ['./news.component.scss'], }) export class NewsComponent implements OnInit { + public filters: Tag[]; constructor() {} ngOnInit(): void {} + + public setFilters(tags: Tag[]): void { + this.filters = tags; + } } diff --git a/src/app/post/post-routing.module.ts b/src/app/post/post-routing.module.ts index e6f6a3e00..c95c0ba7c 100644 --- a/src/app/post/post-routing.module.ts +++ b/src/app/post/post-routing.module.ts @@ -3,11 +3,15 @@ import { Routes, RouterModule } from '@angular/router'; import { PostDetailsComponent } from './components/post-details/post-details.component'; import { PostListComponent } from './components/post-list/post-list.component'; import { NewsComponent } from './news.component'; +import { TagResolver } from './resolvers/tags.resolver'; const routes: Routes = [ { path: '', component: NewsComponent, + resolve: { + tags: TagResolver, + }, children: [ { path: '', diff --git a/src/app/post/post.module.ts b/src/app/post/post.module.ts index d51f1291c..e259e528b 100644 --- a/src/app/post/post.module.ts +++ b/src/app/post/post.module.ts @@ -7,9 +7,19 @@ import { PostListComponent } from './components/post-list/post-list.component'; import { PostDetailsComponent } from './components/post-details/post-details.component'; import { SharedModule } from '../shared/shared.module'; import { PostCardComponent } from './components/post-card/post-card.component'; +import { PostModalFiltersComponent } from './components/post-modal-filters/post-modal-filters.component'; +import { TagResolver } from './resolvers/tags.resolver'; @NgModule({ - declarations: [NewsComponent, PostHeaderComponent, PostListComponent, PostDetailsComponent, PostCardComponent], + declarations: [ + NewsComponent, + PostHeaderComponent, + PostListComponent, + PostDetailsComponent, + PostCardComponent, + PostModalFiltersComponent, + ], imports: [CommonModule, PostRoutingModule, SharedModule], + providers: [TagResolver], }) export class PostModule {} diff --git a/src/app/post/resolvers/tags.resolver.ts b/src/app/post/resolvers/tags.resolver.ts new file mode 100644 index 000000000..6f92f989e --- /dev/null +++ b/src/app/post/resolvers/tags.resolver.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, Router } from '@angular/router'; +import { Observable } from 'rxjs'; +import { map, catchError } from 'rxjs/operators'; +import { TagWithMeta } from '../models/tagWithMeta.model'; +import { PostService } from '../services/post.service'; + +@Injectable() +export class TagResolver implements Resolve<TagWithMeta> { + constructor(private postService: PostService, private router: Router) {} + + resolve(): Observable<TagWithMeta> { + return this.postService.getTags().pipe( + map((res) => res), + catchError(() => { + this.router.navigate(['/home']); + return new Observable<TagWithMeta>(); + }) + ); + } +} diff --git a/src/app/post/services/post.service.ts b/src/app/post/services/post.service.ts index d70e18881..b0fc02fb2 100644 --- a/src/app/post/services/post.service.ts +++ b/src/app/post/services/post.service.ts @@ -5,6 +5,8 @@ import { map } from 'rxjs/operators'; import { Post } from '../models/post.model'; import { TagEnum } from '../enum/tag.enum'; import { PostWithMeta } from '../models/postWithMeta.model'; +import { Tag } from '../models/tag.model'; +import { TagWithMeta } from '../models/tagWithMeta.model'; @Injectable({ providedIn: 'root', @@ -37,14 +39,18 @@ export class PostService { tags.forEach((tag, index) => { tagsString += tag; if (index != tags.length - 1) { - tagsString += ','; + tagsString += '+tags:'; } }); return this.http - .get<PostWithMeta>(`${this.baseUrl}?include=tags,authors&filter=tag:[${tagsString}]`) + .get<PostWithMeta>(`${this.baseUrl}?include=tags,authors&filter=tags:${encodeURIComponent(tagsString)}`) .pipe(map((item: PostWithMeta) => new PostWithMeta(item))); } + public getTags(): Observable<TagWithMeta> { + return this.http.get<TagWithMeta>(`${this.baseUrl}/tags`); + } + private addAuthorToPost(post: Post): Post { post.author = post.excerpt; post.excerpt = post.html.replace(/<[^>]*>/g, ''); diff --git a/src/app/profile/services/profile.service.ts b/src/app/profile/services/profile.service.ts index 370137312..2f044a5f5 100644 --- a/src/app/profile/services/profile.service.ts +++ b/src/app/profile/services/profile.service.ts @@ -40,7 +40,6 @@ export class ProfileService { if (!this.currentProfile) { return false; } - console.log(this.currentProfile.pendingStructuresLink); return this.currentProfile.pendingStructuresLink.includes(idStructure); } diff --git a/src/app/shared/components/svg-icon/svg-icon.component.scss b/src/app/shared/components/svg-icon/svg-icon.component.scss index 0d40e293b..ceb7b8dec 100644 --- a/src/app/shared/components/svg-icon/svg-icon.component.scss +++ b/src/app/shared/components/svg-icon/svg-icon.component.scss @@ -3,6 +3,10 @@ display: inline-block; height: 2em; width: 1.5em; + &.icon-28 { + width: 28px; + height: 28px; + } &.icon-32 { width: 2em; } @@ -23,6 +27,14 @@ opacity: 0.8; } } + &.white { + fill: $white; + stroke: $white; + path { + stroke: $white; + fill: $white; + } + } &.grey { fill: $grey-3; stroke: $grey-3; diff --git a/src/assets/ico/sprite.svg b/src/assets/ico/sprite.svg index 748235c0a..08a36d469 100644 --- a/src/assets/ico/sprite.svg +++ b/src/assets/ico/sprite.svg @@ -204,6 +204,28 @@ <rect x="5" y="21" width="23" height="2" rx="1" fill="#333333"/> </symbol> +<symbol id="news-location" viewBox="0 0 29 31" xmlns="http://www.w3.org/2000/svg"> +<path d="M13.8789 7.40777L13.8789 1.18555" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M13.3789 3.44639V1.86578C13.3665 1.85795 13.3533 1.84995 13.3392 1.84189C13.2087 1.76732 13.0017 1.68555 12.7122 1.68555C12.4228 1.68555 12.2158 1.76732 12.0853 1.84189C12.0712 1.84995 12.058 1.85795 12.0456 1.86578V3.44639C12.2373 3.38974 12.4598 3.35221 12.7122 3.35221C12.9647 3.35221 13.1872 3.38974 13.3789 3.44639Z"/> +<path d="M11.0469 2.81338V4.39398C11.0345 4.40181 11.0212 4.40981 11.0071 4.41787C10.8766 4.49245 10.6696 4.57422 10.3802 4.57422C10.0908 4.57422 9.88378 4.49245 9.75328 4.41787C9.73917 4.40981 9.72592 4.40181 9.71354 4.39398V2.81338C9.90529 2.87003 10.1278 2.90755 10.3802 2.90755C10.6326 2.90755 10.8551 2.87003 11.0469 2.81338Z"/> +<path d="M4.34813 13.1299L14.2096 7.21298L24.0711 13.1299H4.34813Z"/> +<path d="M3.04297 14.1299H25.3763V14.1854C25.3763 14.4616 25.1524 14.6854 24.8763 14.6854H3.54297C3.26683 14.6854 3.04297 14.4616 3.04297 14.1854V14.1299Z" stroke-width="0.777778"/> +<path d="M3.04297 27.3521H25.3763V27.9076H3.04297V27.3521Z" stroke-width="0.777778"/> +<path d="M0.710938 29.6855H27.7109V30.2411H0.710938V29.6855Z" stroke-width="0.777778"/> +<rect x="5.37891" y="16.4634" width="1.33333" height="9.11111"/> +<rect x="10.8203" y="16.4634" width="1.33333" height="9.11111"/> +<rect x="16.2656" y="16.4634" width="1.33333" height="9.11111"/> +<rect x="21.7109" y="16.4634" width="1.33333" height="9.11111"/> +</symbol> + +<symbol id="news-public" viewBox="0 0 29 26" xmlns="http://www.w3.org/2000/svg"> +<path d="M15.8488 19.9496C16.7844 19.3911 17.4109 18.3687 17.4109 17.2C17.4109 15.4327 15.9782 14 14.2109 14C12.4436 14 11.0109 15.4327 11.0109 17.2C11.0109 18.3687 11.6375 19.3911 12.573 19.9496C11.1806 20.5754 10.2109 21.9745 10.2109 23.6V26H18.2109V23.6C18.2109 21.9745 17.2413 20.5754 15.8488 19.9496Z" /> +<path d="M5.84883 19.9496C6.78438 19.3911 7.41094 18.3687 7.41094 17.2C7.41094 15.4327 5.97825 14 4.21094 14C2.44363 14 1.01094 15.4327 1.01094 17.2C1.01094 18.3687 1.6375 19.3911 2.57304 19.9496C1.18059 20.5754 0.210938 21.9745 0.210938 23.6V26H8.21094V23.6C8.21094 21.9745 7.24128 20.5754 5.84883 19.9496Z" /> +<path d="M10.8488 5.94963C11.7844 5.39114 12.4109 4.36874 12.4109 3.2C12.4109 1.43269 10.9782 0 9.21094 0C7.44363 0 6.01094 1.43269 6.01094 3.2C6.01094 4.36874 6.6375 5.39114 7.57304 5.94963C6.18059 6.57538 5.21094 7.97445 5.21094 9.6V12H13.2109V9.6C13.2109 7.97445 12.2413 6.57538 10.8488 5.94963Z" /> +<path d="M25.8488 19.9496C26.7844 19.3911 27.4109 18.3687 27.4109 17.2C27.4109 15.4327 25.9782 14 24.2109 14C22.4436 14 21.0109 15.4327 21.0109 17.2C21.0109 18.3687 21.6375 19.3911 22.573 19.9496C21.1806 20.5754 20.2109 21.9745 20.2109 23.6V26H28.2109V23.6C28.2109 21.9745 27.2413 20.5754 25.8488 19.9496Z" /> +<path d="M20.8488 5.94963C21.7844 5.39114 22.4109 4.36874 22.4109 3.2C22.4109 1.43269 20.9782 0 19.2109 0C17.4436 0 16.0109 1.43269 16.0109 3.2C16.0109 4.36874 16.6375 5.39114 17.573 5.94963C16.1806 6.57538 15.2109 7.97445 15.2109 9.6V12H23.2109V9.6C23.2109 7.97445 22.2413 6.57538 20.8488 5.94963Z"/> +</symbol> + <symbol id="calendar" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"> <path d="M8 10H5V13H8V10Z" fill="#333333"/> diff --git a/src/assets/scss/_layout.scss b/src/assets/scss/_layout.scss index 2a4a34f6e..3da7ec582 100644 --- a/src/assets/scss/_layout.scss +++ b/src/assets/scss/_layout.scss @@ -3,4 +3,4 @@ $footer-height: 56px; $header-height-phone: 70px; $footer-height-phone: 75px; $progressBar-height: 50px; -$header-post-height: 180px; +$header-post-height: 140px; diff --git a/src/index.html b/src/index.html index 17a60d8dc..7a2041e97 100644 --- a/src/index.html +++ b/src/index.html @@ -11,8 +11,6 @@ content="Plateforme à destination des acteurs de l'inclusion numérique de la métropole de Lyon" /> <meta property="og:image" content="https://resin.grandlyon.com/assets/logos/logo_1200.svg" /> - > - <link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="stylesheet" href="https://openlayers.org/en/v4.6.5/css/ol.css" type="text/css" /> <link -- GitLab