diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index 06ac5c2..7edd313 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -1,5 +1,5 @@
- +
- \ No newline at end of file + diff --git a/frontend/src/app/app.component.scss b/frontend/src/app/app.component.scss index cb1b034..a1fbbc1 100644 --- a/frontend/src/app/app.component.scss +++ b/frontend/src/app/app.component.scss @@ -1,14 +1,14 @@ :host { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; + flex: 1; + + app-header { + width: 100%; + } + + main { flex: 1; - - app-header { - width: 100%; - } - - main { - flex: 1; - padding: 1em 0; - } -} \ No newline at end of file + padding: 1em 0; + } +} diff --git a/frontend/src/app/app.component.spec.ts b/frontend/src/app/app.component.spec.ts index 0e07b72..82308c0 100644 --- a/frontend/src/app/app.component.spec.ts +++ b/frontend/src/app/app.component.spec.ts @@ -1,5 +1,5 @@ -import { TestBed } from '@angular/core/testing'; -import { AppComponent } from './app.component'; +import {TestBed} from '@angular/core/testing'; +import {AppComponent} from './app.component'; describe('AppComponent', () => { beforeEach(async () => { diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 0c0a0a2..c78ef51 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,18 +1,17 @@ - -import { Component } from '@angular/core'; -import { RouterOutlet } from '@angular/router'; -import { HeaderComponent } from './components/header/header.component'; -import { FooterComponent } from './components/footer/footer.component'; +import {Component} from '@angular/core'; +import {RouterOutlet} from '@angular/router'; +import {HeaderComponent} from './components/header/header.component'; +import {FooterComponent} from './components/footer/footer.component'; @Component({ - selector: 'app-root', - imports: [ + selector: 'app-root', + imports: [ RouterOutlet, HeaderComponent, FooterComponent -], - templateUrl: './app.component.html', - styleUrl: './app.component.scss' + ], + templateUrl: './app.component.html', + styleUrl: './app.component.scss' }) export class AppComponent { title = 'codiki-ng'; diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index d003db6..c3568ca 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,11 +1,11 @@ -import { ApplicationConfig, inject, provideAppInitializer } from '@angular/core'; -import { provideRouter, withRouterConfig } from '@angular/router'; +import {ApplicationConfig, inject, provideAppInitializer} from '@angular/core'; +import {provideRouter, withRouterConfig} from '@angular/router'; -import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; -import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; -import { routes } from './app.routes'; -import { JwtInterceptor } from './core/interceptor/jwt.interceptor'; -import { AuthenticationService } from './core/service/authentication.service'; +import {HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi} from '@angular/common/http'; +import {provideAnimationsAsync} from '@angular/platform-browser/animations/async'; +import {routes} from './app.routes'; +import {JwtInterceptor} from './core/interceptor/jwt.interceptor'; +import {AuthenticationService} from './core/service/authentication.service'; export const appConfig: ApplicationConfig = { providers: [ @@ -15,13 +15,13 @@ export const appConfig: ApplicationConfig = { paramsInheritanceStrategy: 'always', onSameUrlNavigation: 'reload' }) - ), + ), provideAnimationsAsync(), provideHttpClient(withInterceptorsFromDi()), - { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, + {provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true}, provideAppInitializer(() => { - const initializerFn = ((authenticationService: AuthenticationService) => () => authenticationService.startAuthenticationCheckingProcess())(inject(AuthenticationService)); - return initializerFn(); - }) + const initializerFn = ((authenticationService: AuthenticationService) => () => authenticationService.startAuthenticationCheckingProcess())(inject(AuthenticationService)); + return initializerFn(); + }) ] }; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 40eb449..c00ad23 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,43 +1,43 @@ -import { Routes } from '@angular/router'; -import { alreadyAuthenticatedGuard } from './core/guard/already-authenticated.guard'; +import {Routes} from '@angular/router'; +import {alreadyAuthenticatedGuard} from './core/guard/already-authenticated.guard'; export const routes: Routes = [ - { - path: 'login', - loadComponent: () => import('./pages/login/login.component').then(module => module.LoginComponent), - canActivate: [alreadyAuthenticatedGuard] - }, - { - path: 'signin', - loadComponent: () => import('./pages/signin/signin.component').then(module => module.SigninComponent), - canActivate: [alreadyAuthenticatedGuard] - }, - { - path: 'disconnect', - loadComponent: () => import('./pages/disconnection/disconnection.component').then(module => module.DisconnectionComponent) - }, - { - path: 'publications/new', - loadChildren: () => import('./pages/publication-creation/publication-creation.routes').then(module => module.ROUTES) - }, - { - path: 'publications/:publicationId', - loadComponent: () => import('./pages/publication/publication.component').then(module => module.PublicationComponent) - }, - { - path: 'publications/:publicationId/edit', - loadChildren: () => import('./pages/publication-update/publication-update.routes').then(module => module.ROUTES) - }, - { - path: 'publications', - loadComponent: () => import('./pages/search-publications/search-publications.component').then(module => module.SearchPublicationsComponent) - }, - { - path: 'my-publications', - loadChildren: () => import('./pages/my-publications/my-publications.routes').then(module => module.ROUTES) - }, - { - path: '**', - loadComponent: () => import('./pages/home/home.component').then(module => module.HomeComponent) - } + { + path: 'login', + loadComponent: () => import('./pages/login/login.component').then(module => module.LoginComponent), + canActivate: [alreadyAuthenticatedGuard] + }, + { + path: 'signin', + loadComponent: () => import('./pages/signin/signin.component').then(module => module.SigninComponent), + canActivate: [alreadyAuthenticatedGuard] + }, + { + path: 'disconnect', + loadComponent: () => import('./pages/disconnection/disconnection.component').then(module => module.DisconnectionComponent) + }, + { + path: 'publications/new', + loadChildren: () => import('./pages/publication-creation/publication-creation.routes').then(module => module.ROUTES) + }, + { + path: 'publications/:publicationId', + loadComponent: () => import('./pages/publication/publication.component').then(module => module.PublicationComponent) + }, + { + path: 'publications/:publicationId/edit', + loadChildren: () => import('./pages/publication-update/publication-update.routes').then(module => module.ROUTES) + }, + { + path: 'publications', + loadComponent: () => import('./pages/search-publications/search-publications.component').then(module => module.SearchPublicationsComponent) + }, + { + path: 'my-publications', + loadChildren: () => import('./pages/my-publications/my-publications.routes').then(module => module.ROUTES) + }, + { + path: '**', + loadComponent: () => import('./pages/home/home.component').then(module => module.HomeComponent) + } ]; diff --git a/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.html b/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.html index 74b0190..1157116 100644 --- a/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.html +++ b/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.html @@ -1,10 +1,10 @@ -

{{title}}

-

{{description}}

+

{{ title }}

+

{{ description }}

\ No newline at end of file + + + diff --git a/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.scss b/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.scss index 23e6e87..62d7b50 100644 --- a/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.scss +++ b/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.scss @@ -1,12 +1,12 @@ :host { - display: flex; - flex-direction: column; - text-align: center; - padding: 1em; + display: flex; + flex-direction: column; + text-align: center; + padding: 1em; - footer { - display: flex; - flex-direction: row; - justify-content: space-between; - } -} \ No newline at end of file + footer { + display: flex; + flex-direction: row; + justify-content: space-between; + } +} diff --git a/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.ts b/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.ts index 71b3fef..f8bcd56 100644 --- a/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.ts +++ b/frontend/src/app/components/confirmation-dialog/confirmation-dialog.component.ts @@ -1,35 +1,35 @@ -import { Component, inject, Input } from "@angular/core"; -import { MatRippleModule } from "@angular/material/core"; -import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import {Component, inject} from "@angular/core"; +import {MatRippleModule} from "@angular/material/core"; +import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog"; export interface ConfirmationDialogData { - title: string; - description: string; + title: string; + description: string; } @Component({ - selector: 'app-confirmation-dialog', - templateUrl: './confirmation-dialog.component.html', - styleUrl: './confirmation-dialog.component.scss', - imports: [MatRippleModule] + selector: 'app-confirmation-dialog', + templateUrl: './confirmation-dialog.component.html', + styleUrl: './confirmation-dialog.component.scss', + imports: [MatRippleModule] }) export class ConfirmationDialog { - private readonly dialogRef = inject(MatDialogRef); - data: ConfirmationDialogData = inject(MAT_DIALOG_DATA); + private readonly dialogRef = inject(MatDialogRef); + data: ConfirmationDialogData = inject(MAT_DIALOG_DATA); - get title(): string { - return this.data.title; - } + get title(): string { + return this.data.title; + } - get description(): string { - return this.data.description; - } + get description(): string { + return this.data.description; + } - closeAndValidate(): void { - this.dialogRef.close(true); - } + closeAndValidate(): void { + this.dialogRef.close(true); + } - closeDialog(): void { - this.dialogRef.close(false); - } -} \ No newline at end of file + closeDialog(): void { + this.dialogRef.close(false); + } +} diff --git a/frontend/src/app/components/footer/footer.component.html b/frontend/src/app/components/footer/footer.component.html index f025fd8..65a8b05 100644 --- a/frontend/src/app/components/footer/footer.component.html +++ b/frontend/src/app/components/footer/footer.component.html @@ -1,14 +1,14 @@
- © - 2016 - 2026 All rights reserved - - - 2.2 - - favorite - + © + 2016 - 2026 All rights reserved + - + 2.2 + + favorite +
- menu_book - - - Development realised by Florian THIERRY + menu_book + - + Development realised by Florian THIERRY
diff --git a/frontend/src/app/components/footer/footer.component.scss b/frontend/src/app/components/footer/footer.component.scss index 1784222..16d10cb 100644 --- a/frontend/src/app/components/footer/footer.component.scss +++ b/frontend/src/app/components/footer/footer.component.scss @@ -1,32 +1,33 @@ :host { - background-color: #3f51b5; - color: rgba(255,255,255,.6); + background-color: #3f51b5; + color: rgba(255, 255, 255, .6); + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; + padding: .5em; + font-size: 1.1em; + + div { display: flex; flex-direction: row; - justify-content: space-around; align-items: center; - padding: .5em; - font-size: 1.1em; + gap: .2em; - div { - display: flex; - flex-direction: row; - align-items: center; - gap: .2em; - - .copy-left { - transform: rotate(180deg); - } - a { - text-decoration: none; - color: rgba(255,255,255,.6); - } - - mat-icon { - font-size: 1em; - display: flex; - justify-content: center; - align-items: center; - } + .copy-left { + transform: rotate(180deg); } + + a { + text-decoration: none; + color: rgba(255, 255, 255, .6); + } + + mat-icon { + font-size: 1em; + display: flex; + justify-content: center; + align-items: center; + } + } } diff --git a/frontend/src/app/components/footer/footer.component.ts b/frontend/src/app/components/footer/footer.component.ts index 0e62bc6..bbf699b 100644 --- a/frontend/src/app/components/footer/footer.component.ts +++ b/frontend/src/app/components/footer/footer.component.ts @@ -1,12 +1,13 @@ -import { Component } from '@angular/core'; -import { MatIconModule } from '@angular/material/icon'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { RouterModule } from '@angular/router'; +import {Component} from '@angular/core'; +import {MatIconModule} from '@angular/material/icon'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {RouterModule} from '@angular/router'; @Component({ - selector: 'app-footer', - imports: [MatIconModule, MatTooltipModule, RouterModule], - templateUrl: './footer.component.html', - styleUrl: './footer.component.scss' + selector: 'app-footer', + imports: [MatIconModule, MatTooltipModule, RouterModule], + templateUrl: './footer.component.html', + styleUrl: './footer.component.scss' }) -export class FooterComponent {} +export class FooterComponent { +} diff --git a/frontend/src/app/components/header/header.component.html b/frontend/src/app/components/header/header.component.html index 17a4b46..3f21547 100644 --- a/frontend/src/app/components/header/header.component.html +++ b/frontend/src/app/components/header/header.component.html @@ -1,44 +1,44 @@
- - - logo - Codiki - + + + logo + Codiki +
- +
- @if (isAuthenticated) { - - - - - } @else { - Login - } + @if (isAuthenticated) { + + + + + } @else { + Login + }
- \ No newline at end of file + diff --git a/frontend/src/app/components/header/header.component.scss b/frontend/src/app/components/header/header.component.scss index a17213b..6ef4a16 100644 --- a/frontend/src/app/components/header/header.component.scss +++ b/frontend/src/app/components/header/header.component.scss @@ -1,148 +1,148 @@ $headerHeight: 3.5em; :host { + display: flex; + flex-direction: row; + justify-content: space-between; + background-color: #3f51b5; + color: white; + position: relative; + height: $headerHeight; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .16), 0 2px 10px 0 rgba(0, 0, 0, .12); + + div { display: flex; flex-direction: row; - justify-content: space-between; - background-color: #3f51b5; - color: white; + justify-content: center; position: relative; height: $headerHeight; - box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12); - div { + &.left { + position: absolute; + top: 0; + left: 0; + align-items: center; + gap: 1em; + padding: 0 1em; + z-index: 2; + + a { display: flex; flex-direction: row; justify-content: center; - position: relative; - height: $headerHeight; + align-items: center; + color: white; + text-decoration: none; + gap: .5em; - &.left { - position: absolute; - top: 0; - left: 0; - align-items: center; - gap: 1em; - padding: 0 1em; - z-index: 2; - - a { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - color: white; - text-decoration: none; - gap: .5em; - - img { - $imageSize: 2em; - width: $imageSize; - height: $imageSize; - } - - .title { - font-size: 1.5em; - display: none; - - @media screen and (min-width: 600px) { - display: block; - } - } - } + img { + $imageSize: 2em; + width: $imageSize; + height: $imageSize; } - &.middle { - flex: 1; - $borderRadiusValue: 10em; - position: relative; - transition: max-width .2s ease-in-out; - display: flex; - justify-content: center; - align-items: center; - z-index: 1; - - app-publications-search-bar { - width: 100%; - max-width: 12em; + .title { + font-size: 1.5em; + display: none; - @media screen and (min-width: 435px) { - max-width: 16em; - } - - @media screen and (min-width: 500px) { - max-width: 20em; - } - - @media screen and (min-width: 700px) { - max-width: 24em; - } - - @media screen and (min-width: 800px) { - max-width: 32em; - } - - @media screen and (min-width: 900px) { - max-width: 38em; - } - - @media screen and (min-width: 1000px) { - max-width: 45em; - } - - @media screen and (min-width: 1100px) { - max-width: 50em; - } - } - } - - &.right { - position: absolute; - top: 0; - right: 0; - z-index: 2; - margin-right: .5em; - - a, button { - margin: .5em; - } + @media screen and (min-width: 600px) { + display: block; + } } + } } + + &.middle { + flex: 1; + $borderRadiusValue: 10em; + position: relative; + transition: max-width .2s ease-in-out; + display: flex; + justify-content: center; + align-items: center; + z-index: 1; + + app-publications-search-bar { + width: 100%; + max-width: 12em; + + @media screen and (min-width: 435px) { + max-width: 16em; + } + + @media screen and (min-width: 500px) { + max-width: 20em; + } + + @media screen and (min-width: 700px) { + max-width: 24em; + } + + @media screen and (min-width: 800px) { + max-width: 32em; + } + + @media screen and (min-width: 900px) { + max-width: 38em; + } + + @media screen and (min-width: 1000px) { + max-width: 45em; + } + + @media screen and (min-width: 1100px) { + max-width: 50em; + } + } + } + + &.right { + position: absolute; + top: 0; + right: 0; + z-index: 2; + margin-right: .5em; + + a, button { + margin: .5em; + } + } + } } app-side-menu { - height: 100%; + height: 100%; } .authenticated-user-menu { + display: flex; + flex-direction: column; + padding: 0.2em 0; + + a { + flex: 1; display: flex; - flex-direction: column; - padding: 0.2em 0; + flex-direction: row; + align-items: center; + text-decoration: none; + background-color: white; + color: black; + padding: 1em; + gap: .5em; + transition: background-color .2s ease-in-out, color .2s ease-in-out; - a { - flex: 1; - display: flex; - flex-direction: row; - align-items: center; - text-decoration: none; - background-color: white; - color: black; - padding: 1em; - gap: .5em; - transition: background-color .2s ease-in-out, color .2s ease-in-out; - - &:hover { - background-color: #5c6bc0; - color: white; - } - - &.disconnection { - color: #D50000; - - &:hover { - background-color: #E53935; - color: white; - } - } + &:hover { + background-color: #5c6bc0; + color: white; } -} \ No newline at end of file + + &.disconnection { + color: #D50000; + + &:hover { + background-color: #E53935; + color: white; + } + } + } +} diff --git a/frontend/src/app/components/header/header.component.ts b/frontend/src/app/components/header/header.component.ts index b8c7df3..3dc4184 100644 --- a/frontend/src/app/components/header/header.component.ts +++ b/frontend/src/app/components/header/header.component.ts @@ -1,19 +1,18 @@ - -import { Component, inject } from '@angular/core'; -import { FormControl, ReactiveFormsModule } from '@angular/forms'; -import { MatButtonModule } from '@angular/material/button'; -import { MatRippleModule } from '@angular/material/core'; -import { MatIconModule } from '@angular/material/icon'; -import { MatMenuModule } from '@angular/material/menu'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { RouterModule } from '@angular/router'; -import { AuthenticationService } from '../../core/service/authentication.service'; -import { PublicationsSearchBarComponent } from '../publications-search-bar/publications-search-bar.component'; -import { SideMenuComponent } from '../side-menu/side-menu.component'; +import {Component, inject} from '@angular/core'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {MatButtonModule} from '@angular/material/button'; +import {MatRippleModule} from '@angular/material/core'; +import {MatIconModule} from '@angular/material/icon'; +import {MatMenuModule} from '@angular/material/menu'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {RouterModule} from '@angular/router'; +import {AuthenticationService} from '../../core/service/authentication.service'; +import {PublicationsSearchBarComponent} from '../publications-search-bar/publications-search-bar.component'; +import {SideMenuComponent} from '../side-menu/side-menu.component'; @Component({ - selector: 'app-header', - imports: [ + selector: 'app-header', + imports: [ MatButtonModule, MatIconModule, MatMenuModule, @@ -23,9 +22,9 @@ import { SideMenuComponent } from '../side-menu/side-menu.component'; ReactiveFormsModule, RouterModule, SideMenuComponent -], - templateUrl: './header.component.html', - styleUrl: './header.component.scss' + ], + templateUrl: './header.component.html', + styleUrl: './header.component.scss' }) export class HeaderComponent { private authenticationService = inject(AuthenticationService); diff --git a/frontend/src/app/components/publication-edition/code-block-dialog/code-block-dialog.component.html b/frontend/src/app/components/publication-edition/code-block-dialog/code-block-dialog.component.html index abd2006..cde3d87 100644 --- a/frontend/src/app/components/publication-edition/code-block-dialog/code-block-dialog.component.html +++ b/frontend/src/app/components/publication-edition/code-block-dialog/code-block-dialog.component.html @@ -4,34 +4,34 @@ matTooltip="Close" matRipple i18n-matTooltip> - close + close
-

Add a code block

+

Add a code block

-
- - Programming language - - @for(programmingLanguage of programmingLanguages; track programmingLanguage) { - - {{programmingLanguage.label}} - - } - - - - Code block - - -
-
- - -
+
+ + Programming language + + @for (programmingLanguage of programmingLanguages; track programmingLanguage) { + + {{ programmingLanguage.label }} + + } + + + + Code block + + +
+
+ + +
diff --git a/frontend/src/app/components/publication-edition/code-block-dialog/code-block-dialog.component.scss b/frontend/src/app/components/publication-edition/code-block-dialog/code-block-dialog.component.scss index e21d3e8..9f8e59f 100644 --- a/frontend/src/app/components/publication-edition/code-block-dialog/code-block-dialog.component.scss +++ b/frontend/src/app/components/publication-edition/code-block-dialog/code-block-dialog.component.scss @@ -1,30 +1,30 @@ :host { + display: flex; + flex-direction: column; + padding: 1em; + gap: 1em; + position: relative; + max-height: 90vh; + + header { + flex: 1; display: flex; - flex-direction: column; - padding: 1em; - gap: 1em; - position: relative; - max-height: 90vh; + flex-direction: row; + justify-content: center; + align-items: center; + } - header { - flex: 1; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - } + form { + div { + &.form-content { + mat-form-field { + width: 100%; - form { - div { - &.form-content { - mat-form-field { - width: 100%; - - textarea { - height: 30vh; - } - } - } + textarea { + height: 30vh; + } } + } } -} \ No newline at end of file + } +} diff --git a/frontend/src/app/components/publication-edition/code-block-dialog/code-block-dialog.component.ts b/frontend/src/app/components/publication-edition/code-block-dialog/code-block-dialog.component.ts index 3453a45..a2d7c8d 100644 --- a/frontend/src/app/components/publication-edition/code-block-dialog/code-block-dialog.component.ts +++ b/frontend/src/app/components/publication-edition/code-block-dialog/code-block-dialog.component.ts @@ -1,119 +1,119 @@ -import { Component, inject } from "@angular/core"; -import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms"; -import { MatRippleModule } from "@angular/material/core"; -import { MatDialogRef } from "@angular/material/dialog"; -import { MatFormFieldModule } from "@angular/material/form-field"; -import { MatIcon } from "@angular/material/icon"; -import { MatInputModule } from "@angular/material/input"; -import { MatSelectModule } from '@angular/material/select'; -import { MatTooltip } from "@angular/material/tooltip"; +import {Component, inject} from "@angular/core"; +import {FormBuilder, FormControl, ReactiveFormsModule, Validators} from "@angular/forms"; +import {MatRippleModule} from "@angular/material/core"; +import {MatDialogRef} from "@angular/material/dialog"; +import {MatFormFieldModule} from "@angular/material/form-field"; +import {MatIcon} from "@angular/material/icon"; +import {MatInputModule} from "@angular/material/input"; +import {MatSelectModule} from '@angular/material/select'; +import {MatTooltip} from "@angular/material/tooltip"; export interface ProgramingLanguage { - code: string; - label: string; + code: string; + label: string; } export const PROGRAMMING_LANGUAGES: ProgramingLanguage[] = [ - { - code: 'bash', - label: 'Bash' - }, - { - code: 'c', - label: 'C' - }, - { - code: 'cpp', - label: 'C++' - }, - { - code: 'cs', - label: 'C#' - }, - { - code: 'lua', - label: 'Lua' - }, - { - code: 'java', - label: 'Java' - }, - { - code: 'json5', - label: 'JSON' - }, - { - code: 'kt', - label: 'Kotlin' - }, - { - code: 'markup', - label: 'html/xml' - }, - { - code: 'php', - label: 'PHP' - }, - { - code: 'plsql', - label: 'PL/SQL' - }, - { - code: 'python', - label: 'Python' - }, - { - code: 'powershell', - label: 'PowerShell' - }, - { - code: 'rust', - label: 'Rust' - }, - { - code: 'sql', - label: 'SQL' - }, - { - code: 'ts', - label: 'Typescript' - }, - { - code: 'yml', - label: 'YAML' - }, + { + code: 'bash', + label: 'Bash' + }, + { + code: 'c', + label: 'C' + }, + { + code: 'cpp', + label: 'C++' + }, + { + code: 'cs', + label: 'C#' + }, + { + code: 'lua', + label: 'Lua' + }, + { + code: 'java', + label: 'Java' + }, + { + code: 'json5', + label: 'JSON' + }, + { + code: 'kt', + label: 'Kotlin' + }, + { + code: 'markup', + label: 'html/xml' + }, + { + code: 'php', + label: 'PHP' + }, + { + code: 'plsql', + label: 'PL/SQL' + }, + { + code: 'python', + label: 'Python' + }, + { + code: 'powershell', + label: 'PowerShell' + }, + { + code: 'rust', + label: 'Rust' + }, + { + code: 'sql', + label: 'SQL' + }, + { + code: 'ts', + label: 'Typescript' + }, + { + code: 'yml', + label: 'YAML' + }, ]; @Component({ - selector: 'app-code-block-dialog', - templateUrl: './code-block-dialog.component.html', - styleUrl: './code-block-dialog.component.scss', - imports: [ - MatFormFieldModule, - MatIcon, - MatInputModule, - MatRippleModule, - MatSelectModule, - MatTooltip, - ReactiveFormsModule, - ] + selector: 'app-code-block-dialog', + templateUrl: './code-block-dialog.component.html', + styleUrl: './code-block-dialog.component.scss', + imports: [ + MatFormFieldModule, + MatIcon, + MatInputModule, + MatRippleModule, + MatSelectModule, + MatTooltip, + ReactiveFormsModule, + ] }) export class CodeBlockDialog { - private readonly dialogRef = inject(MatDialogRef); - private formBuilder = inject(FormBuilder); - programmingLanguages = PROGRAMMING_LANGUAGES; - formGroup = this.formBuilder.group({ - programmingLanguage: new FormControl('', Validators.required), - codeBlock: new FormControl('', Validators.required) - }); + private readonly dialogRef = inject(MatDialogRef); + private formBuilder = inject(FormBuilder); + programmingLanguages = PROGRAMMING_LANGUAGES; + formGroup = this.formBuilder.group({ + programmingLanguage: new FormControl('', Validators.required), + codeBlock: new FormControl('', Validators.required) + }); - closeAndValidate(): void { - if (this.formGroup.valid) { - this.dialogRef.close(this.formGroup.value); - } + closeAndValidate(): void { + if (this.formGroup.valid) { + this.dialogRef.close(this.formGroup.value); } + } - closeDialog(): void { - this.dialogRef.close(); - } -} \ No newline at end of file + closeDialog(): void { + this.dialogRef.close(); + } +} diff --git a/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.component.html b/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.component.html index 88dab07..9d4b3c0 100644 --- a/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.component.html +++ b/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.component.html @@ -4,40 +4,41 @@ matTooltip="Close" matRipple i18n-matTooltip> - close + close
-

Select an illustration

+

Select an illustration

- @if (isLoading()) { -

Pictures loading...

- + @if (isLoading()) { +

Pictures loading...

+ + } @else { + @if (pictures.length) { + @for (picture of pictures; track picture) { + + } } @else { - @if (pictures.length) { - @for(picture of pictures; track picture) { - - } - } @else { -

There is no any picture.

- } +

There is no any picture.

} + }
- - - + + +
diff --git a/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.component.scss b/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.component.scss index d5ead41..0d0d121 100644 --- a/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.component.scss +++ b/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.component.scss @@ -1,83 +1,83 @@ :host { + display: flex; + flex-direction: column; + padding: 1em; + gap: 1em; + position: relative; + max-height: 90vh; + + header { + flex: 1; display: flex; - flex-direction: column; - padding: 1em; + flex-direction: row; + justify-content: center; + align-items: center; + } + + .picture-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-items: center; gap: 1em; - position: relative; - max-height: 90vh; + max-height: 30em; + overflow-y: auto; + min-height: 10em; + padding: .5em 0; - header { - flex: 1; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; + img { + width: 15em; + height: 10em; + object-fit: cover; + border-radius: 1em; + opacity: .9; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .16), 0 2px 10px 0 rgba(0, 0, 0, .12); + transition: opacity .2s ease-in-out, box-shadow .2s ease-in-out; + + &:hover { + cursor: pointer; + opacity: 1; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .32), 0 2px 10px 0 rgba(0, 0, 0, .24); + } + } + } + + footer { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + button { + padding: .8em 1.2em; + border-radius: 10em; + border: none; + background-color: #3f51b5; + color: white; + transition: background-color .2s ease-in-out; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + + &:hover { + background-color: #5b6ed8; + } + + &.secondary { + color: #3f51b5; + background-color: white; + + &:hover { + background-color: #f2f4ff; + cursor: pointer; + } + } } - .picture-container { - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: center; - align-items: center; - gap: 1em; - max-height: 30em; - overflow-y: auto; - min-height: 10em; - padding: .5em 0; - - img { - width: 15em; - height: 10em; - object-fit: cover; - border-radius: 1em; - opacity: .9; - box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12); - transition: opacity .2s ease-in-out, box-shadow .2s ease-in-out; - - &:hover { - cursor: pointer; - opacity: 1; - box-shadow: 0 2px 5px 0 rgba(0,0,0,.32),0 2px 10px 0 rgba(0,0,0,.24); - } - } + input[type=file] { + display: none; } - - footer { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - - button { - padding: .8em 1.2em; - border-radius: 10em; - border: none; - background-color: #3f51b5; - color: white; - transition: background-color .2s ease-in-out; - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - - &:hover { - background-color: #5b6ed8; - } - - &.secondary { - color: #3f51b5; - background-color: white; - - &:hover { - background-color: #f2f4ff; - cursor: pointer; - } - } - } - - input[type=file] { - display: none; - } - } -} \ No newline at end of file + } +} diff --git a/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.component.ts b/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.component.ts index 25ad430..bbf8eac 100644 --- a/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.component.ts +++ b/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.component.ts @@ -1,74 +1,74 @@ import {Component, inject, OnInit, signal} from "@angular/core"; -import { Picture } from "../../../core/rest-services/picture/model/picture"; +import {Picture} from "../../../core/rest-services/picture/model/picture"; import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; -import { MatSnackBar } from "@angular/material/snack-bar"; -import { PictureRestService } from "../../../core/rest-services/picture/picture.rest-service"; -import { MatIcon } from "@angular/material/icon"; -import { MatDialogRef } from "@angular/material/dialog"; +import {MatSnackBar} from "@angular/material/snack-bar"; +import {PictureRestService} from "../../../core/rest-services/picture/picture.rest-service"; +import {MatIcon} from "@angular/material/icon"; +import {MatDialogRef} from "@angular/material/dialog"; import {MatRippleModule} from '@angular/material/core'; -import { MatTooltip } from "@angular/material/tooltip"; +import {MatTooltip} from "@angular/material/tooltip"; @Component({ - selector: 'app-picture-selection', - templateUrl: './picture-selection-dialog.component.html', - styleUrl: './picture-selection-dialog.component.scss', - imports: [ - MatIcon, - MatRippleModule, - MatProgressSpinnerModule, - MatTooltip - ] + selector: 'app-picture-selection', + templateUrl: './picture-selection-dialog.component.html', + styleUrl: './picture-selection-dialog.component.scss', + imports: [ + MatIcon, + MatRippleModule, + MatProgressSpinnerModule, + MatTooltip + ] }) export class PictureSelectionDialog implements OnInit { - private readonly pictureRestService = inject(PictureRestService); - private readonly snackBar = inject(MatSnackBar); - private readonly dialogRef = inject(MatDialogRef); + private readonly pictureRestService = inject(PictureRestService); + private readonly snackBar = inject(MatSnackBar); + private readonly dialogRef = inject(MatDialogRef); - isLoading = signal(false); - isLoaded = signal(false); - pictures: Picture[] = []; + isLoading = signal(false); + isLoaded = signal(false); + pictures: Picture[] = []; - ngOnInit(): void { - this.isLoading.set(true); - this.pictureRestService.getAllOfCurrentUser() - .then(pictures => { - this.pictures = pictures; - }) - .catch(error => { - if (error.status === 401) { - this.dialogRef.close(); - } else { - const errorMessage = $localize`An error occurred while loading pictures.`; - console.error(errorMessage, error); - this.snackBar.open(errorMessage, $localize`Close`, { duration: 5000 }); - } - }) - .finally(() => { - this.isLoading.set(false); - this.isLoaded.set(true); - }); - } - - selectPicture(picture: Picture): void { - this.dialogRef.close(picture.id); - } - - closeDialog(): void { - this.dialogRef.close(); - } - - uploadPicture(fileSelectionEvent: any): void { - const pictureFile = fileSelectionEvent.target.files[0]; - if (pictureFile) { - this.pictureRestService.uploadPicture(pictureFile) - .then(pictureId => { - this.dialogRef.close(pictureId); - }) - .catch(error => { - const errorMessage = $localize`A technical error occurred while uploading your picture.`; - console.error(errorMessage, error); - this.snackBar.open(errorMessage, $localize`Close`, { duration: 5000 }); - }); + ngOnInit(): void { + this.isLoading.set(true); + this.pictureRestService.getAllOfCurrentUser() + .then(pictures => { + this.pictures = pictures; + }) + .catch(error => { + if (error.status === 401) { + this.dialogRef.close(); + } else { + const errorMessage = $localize`An error occurred while loading pictures.`; + console.error(errorMessage, error); + this.snackBar.open(errorMessage, $localize`Close`, {duration: 5000}); } + }) + .finally(() => { + this.isLoading.set(false); + this.isLoaded.set(true); + }); + } + + selectPicture(picture: Picture): void { + this.dialogRef.close(picture.id); + } + + closeDialog(): void { + this.dialogRef.close(); + } + + uploadPicture(fileSelectionEvent: any): void { + const pictureFile = fileSelectionEvent.target.files[0]; + if (pictureFile) { + this.pictureRestService.uploadPicture(pictureFile) + .then(pictureId => { + this.dialogRef.close(pictureId); + }) + .catch(error => { + const errorMessage = $localize`A technical error occurred while uploading your picture.`; + console.error(errorMessage, error); + this.snackBar.open(errorMessage, $localize`Close`, {duration: 5000}); + }); } + } } diff --git a/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.service.ts b/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.service.ts index f7d5673..29f0567 100644 --- a/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.service.ts +++ b/frontend/src/app/components/publication-edition/picture-selection-dialog/picture-selection-dialog.service.ts @@ -1,24 +1,24 @@ -import { inject, Injectable } from "@angular/core"; -import { PictureRestService } from "../../../core/rest-services/picture/picture.rest-service"; -import { MatSnackBar } from "@angular/material/snack-bar"; -import { MatDialogRef } from "@angular/material/dialog"; -import { PictureSelectionDialog } from "./picture-selection-dialog.component"; +import {inject, Injectable} from "@angular/core"; +import {PictureRestService} from "../../../core/rest-services/picture/picture.rest-service"; +import {MatSnackBar} from "@angular/material/snack-bar"; +import {MatDialogRef} from "@angular/material/dialog"; +import {PictureSelectionDialog} from "./picture-selection-dialog.component"; @Injectable() export class PictureSelectionDialogService { - private pictureRestService = inject(PictureRestService); - private snackBar = inject(MatSnackBar); - private readonly dialogRef = inject(MatDialogRef); + private pictureRestService = inject(PictureRestService); + private snackBar = inject(MatSnackBar); + private readonly dialogRef = inject(MatDialogRef); - uploadPicture(pictureFile: File): void { - this.pictureRestService.uploadPicture(pictureFile) - .then(pictureId => { - this.dialogRef.close(pictureId); - }) - .catch(error => { - const errorMessage = $localize`An error occured while uploading a picture...`; - console.error(errorMessage, error); - this.snackBar.open(errorMessage, $localize`Close`, { duration: 5000 }); - }); - } -} \ No newline at end of file + uploadPicture(pictureFile: File): void { + this.pictureRestService.uploadPicture(pictureFile) + .then(pictureId => { + this.dialogRef.close(pictureId); + }) + .catch(error => { + const errorMessage = $localize`An error occured while uploading a picture...`; + console.error(errorMessage, error); + this.snackBar.open(errorMessage, $localize`Close`, {duration: 5000}); + }); + } +} diff --git a/frontend/src/app/components/publication-edition/publication-edition.component.html b/frontend/src/app/components/publication-edition/publication-edition.component.html index 0f59725..8a2d8c3 100644 --- a/frontend/src/app/components/publication-edition/publication-edition.component.html +++ b/frontend/src/app/components/publication-edition/publication-edition.component.html @@ -1,132 +1,133 @@
-
-

{{title}}

-
+
+

{{ title() }}

+
- - -
-
-
- - Title - - - - Description - - - - Category - - @for (category of categories$ | async; track category) { - - {{ category.name }} - - } - - -
- -
- -
-
- -
- - - - - - - -
- - Content - - -
-
- - -
- @if ((isPreviewing$ | async) === true) { -
-

Preview is loading...

- -
- } @else { - -
-

{{ publication.title }}

-

{{ publication.description }}

-
-
+ + +
+
+
+ + Title + + + + Description + + + + Category + + @for (category of categories$ | async; track category) { + + {{ category.name }} + } -
- - -
- Save - -
- \ No newline at end of file + + +
+ +
+ +
+
+ +
+ + + + + + + +
+ + Content + + +
+
+ + +
+ @if (isPreviewing()) { +
+

Preview is loading...

+ +
+ } @else { + +
+

{{ publication().title }}

+

{{ publication().description }}

+
+
+ } +
+
+
+
+ Save + +
+ diff --git a/frontend/src/app/components/publication-edition/publication-edition.component.scss b/frontend/src/app/components/publication-edition/publication-edition.component.scss index 9f564de..83a06e7 100644 --- a/frontend/src/app/components/publication-edition/publication-edition.component.scss +++ b/frontend/src/app/components/publication-edition/publication-edition.component.scss @@ -1,170 +1,170 @@ :host { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; - form { - margin: 1em; - max-width: 80em; - width: 90%; - border-radius: .5em; - box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12); + form { + margin: 1em; + max-width: 80em; + width: 90%; + border-radius: .5em; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .16), 0 2px 10px 0 rgba(0, 0, 0, .12); - & > header { - padding: 2em; - background-color: #3f51b5; - color: white; - border-radius: .5em .5em 0 0; + & > header { + padding: 2em; + background-color: #3f51b5; + color: white; + border-radius: .5em .5em 0 0; - h1 { - font-size: 2em; - margin-bottom: .5em; - } - } - - footer { - padding: 2em; - display: flex; - flex-direction: row-reverse; - justify-content: space-between; - align-items: center; - } + h1 { + font-size: 2em; + margin-bottom: .5em; + } } + + footer { + padding: 2em; + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + align-items: center; + } + } } .form-content { - padding: 2em; - padding-bottom: 0; + padding: 2em; + padding-bottom: 0; + display: flex; + flex-direction: column; + gap: .5em; + + mat-form-field { + textarea { + height: 20em; + } + } + + .first-part { display: flex; - flex-direction: column; + flex-direction: column-reverse; + gap: 1em; + + @media screen and (min-width: 600px) { + flex-direction: row; + + div { + flex: 1 0; + + &.picture-container { + max-width: 20em; + + img { + max-height: 15em; + max-width: 20em; + } + } + } + } + + div { + flex: 1 0 50%; + display: flex; + flex-direction: column; + justify-content: center; + + &.picture-container { + img { + flex: 1; + object-fit: cover; + width: 100%; + cursor: pointer; + border-radius: 1em; + opacity: .9; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .16), 0 2px 10px 0 rgba(0, 0, 0, .12); + transition: opacity .2s ease-in-out, box-shadow .2s ease-in-out; + + &:hover { + cursor: pointer; + opacity: 1; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .32), 0 2px 10px 0 rgba(0, 0, 0, .24); + } + } + } + } + } + + .actions { + display: flex; + flex-direction: row; gap: .5em; - mat-form-field { - textarea { - height: 20em; - } - } - - .first-part { - display: flex; - flex-direction: column-reverse; - gap: 1em; - - @media screen and (min-width: 600px) { - flex-direction: row; - - div { - flex: 1 0; - - &.picture-container { - max-width: 20em; - - img { - max-height: 15em; - max-width: 20em; - } - } - } - } - - div { - flex: 1 0 50%; - display: flex; - flex-direction: column; - justify-content: center; - - &.picture-container { - img { - flex: 1; - object-fit: cover; - width: 100%; - cursor: pointer; - border-radius: 1em; - opacity: .9; - box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12); - transition: opacity .2s ease-in-out, box-shadow .2s ease-in-out; - - &:hover { - cursor: pointer; - opacity: 1; - box-shadow: 0 2px 5px 0 rgba(0,0,0,.32),0 2px 10px 0 rgba(0,0,0,.24); - } - } - } - } - } - - .actions { - display: flex; - flex-direction: row; - gap: .5em; - - button { - padding: 0; - border-radius: 10em; - border: none; - background-color: #3f51b5; - color: white; - transition: background-color .2s ease-in-out; - display: flex; - justify-content: center; - align-items: center; - width: 3em; - height: 3em; - box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12); - font-weight: bold; - - &:hover { - background-color: #5b6ed8; - cursor: pointer; - } - - &:disabled { - background-color: #5f6aa6; - cursor: not-allowed; - } - } + button { + padding: 0; + border-radius: 10em; + border: none; + background-color: #3f51b5; + color: white; + transition: background-color .2s ease-in-out; + display: flex; + justify-content: center; + align-items: center; + width: 3em; + height: 3em; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .16), 0 2px 10px 0 rgba(0, 0, 0, .12); + font-weight: bold; + + &:hover { + background-color: #5b6ed8; + cursor: pointer; + } + + &:disabled { + background-color: #5f6aa6; + cursor: not-allowed; + } } + } } .preview { + display: flex; + flex-direction: column; + max-height: 80vh; + overflow-y: auto; + + .preview-loading { display: flex; flex-direction: column; - max-height: 80vh; - overflow-y: auto; + align-items: center; + } - .preview-loading { - display: flex; - flex-direction: column; - align-items: center; + .illustration { + flex: 1; + height: 12em; + object-fit: cover; + transition: height .2s ease-in-out; + + @media screen and (min-width: 450px) { + height: 15em; } - .illustration { - flex: 1; - height: 12em; - object-fit: cover; - transition: height .2s ease-in-out; - - @media screen and (min-width: 450px) { - height: 15em; - } - - @media screen and (min-width: 600px) { - height: 20em; - } - - @media screen and (min-width: 750px) { - height: 25em; - } + @media screen and (min-width: 600px) { + height: 20em; } - header { - padding: 2em; + @media screen and (min-width: 750px) { + height: 25em; } + } - main { - padding: 2em; - text-align: justify; - } -} \ No newline at end of file + header { + padding: 2em; + } + + main { + padding: 2em; + text-align: justify; + } +} diff --git a/frontend/src/app/components/publication-edition/publication-edition.component.ts b/frontend/src/app/components/publication-edition/publication-edition.component.ts index 4eacd02..b266dc6 100644 --- a/frontend/src/app/components/publication-edition/publication-edition.component.ts +++ b/frontend/src/app/components/publication-edition/publication-edition.component.ts @@ -1,148 +1,135 @@ -import { CommonModule, Location } from "@angular/common"; -import { Component, EventEmitter, inject, Input, OnChanges, OnDestroy, Output } from "@angular/core"; -import { FormGroup, ReactiveFormsModule } from "@angular/forms"; -import { MatDialogModule } from "@angular/material/dialog"; -import { MatIconModule } from "@angular/material/icon"; -import { MatInputModule } from "@angular/material/input"; -import { MatProgressSpinnerModule } from "@angular/material/progress-spinner"; -import { MatSelectModule } from "@angular/material/select"; -import { MatTabsModule } from "@angular/material/tabs"; -import { MatTooltipModule } from "@angular/material/tooltip"; -import { map, Observable, of, Subscription } from "rxjs"; -import { Category } from "../../core/rest-services/category/model/category"; -import { Publication } from "../../core/rest-services/publications/model/publication"; -import { CategoryService } from "../../core/service/category.service"; -import { SubmitButtonComponent } from "../submit-button/submit-button.component"; -import { PictureSelectionDialog } from "./picture-selection-dialog/picture-selection-dialog.component"; -import { PublicationEditionService } from "./publication-edition.service"; -import { MatRippleModule } from "@angular/material/core"; +import {CommonModule, Location} from "@angular/common"; +import {Component, effect, inject, input, output, signal} from "@angular/core"; +import {FormGroup, ReactiveFormsModule} from "@angular/forms"; +import {MatDialogModule} from "@angular/material/dialog"; +import {MatIconModule} from "@angular/material/icon"; +import {MatInputModule} from "@angular/material/input"; +import {MatProgressSpinnerModule} from "@angular/material/progress-spinner"; +import {MatSelectModule} from "@angular/material/select"; +import {MatTabsModule} from "@angular/material/tabs"; +import {MatTooltipModule} from "@angular/material/tooltip"; +import {map, Observable} from "rxjs"; +import {Category} from "../../core/rest-services/category/model/category"; +import {DEFAULT_PUBLICATION, Publication} from "../../core/rest-services/publications/model/publication"; +import {CategoryService} from "../../core/service/category.service"; +import {SubmitButtonComponent} from "../submit-button/submit-button.component"; +import {PublicationEditionService} from "./publication-edition.service"; +import {MatRippleModule} from "@angular/material/core"; @Component({ - selector: 'app-publication-edition', - templateUrl: './publication-edition.component.html', - styleUrl: './publication-edition.component.scss', - imports: [ - CommonModule, - MatDialogModule, - MatIconModule, - MatInputModule, - MatRippleModule, - MatProgressSpinnerModule, - MatSelectModule, - MatTabsModule, - MatTooltipModule, - ReactiveFormsModule, - SubmitButtonComponent - ], - providers: [PublicationEditionService] + selector: 'app-publication-edition', + templateUrl: './publication-edition.component.html', + styleUrl: './publication-edition.component.scss', + imports: [ + CommonModule, + MatDialogModule, + MatIconModule, + MatInputModule, + MatRippleModule, + MatProgressSpinnerModule, + MatSelectModule, + MatTabsModule, + MatTooltipModule, + ReactiveFormsModule, + SubmitButtonComponent + ], + providers: [PublicationEditionService] }) -export class PublicationEditionComponent implements OnChanges, OnDestroy { - @Input() - publication!: Publication; - @Input() - title!: string; - @Input() - isSaving$: Observable = of(false); - @Output() - publicationSave = new EventEmitter(); +export class PublicationEditionComponent { + readonly #categoryService = inject(CategoryService); + readonly #location = inject(Location); + readonly #publicationEditionService = inject(PublicationEditionService); - publicationInEdition!: Publication; - private readonly categoryService = inject(CategoryService); - private readonly location = inject(Location); - private readonly publicationEditionService = inject(PublicationEditionService); - private subscriptions: Subscription[] = []; + publication = input.required(); + title = input.required(); + isSaving = input.required(); + publicationSave = output(); - get publicationEditionForm(): FormGroup { - return this.publicationEditionService.publicationEditionForm; + isLoading = this.#publicationEditionService.isLoading; + isPreviewing = this.#publicationEditionService.isPreviewing; + publicationInEdition = signal(DEFAULT_PUBLICATION); + + constructor() { + effect(() => { + let publication = this.publication(); + const publicationInEdition = this.publicationInEdition(); + if (!publicationInEdition || publicationInEdition !== publication) { + this.publicationInEdition.set(publication); + this.#publicationEditionService.init(publicationInEdition); + } + }); + } + + get publicationEditionForm(): FormGroup { + return this.#publicationEditionService.publicationEditionForm; + } + + get categories$(): Observable { + return this.#categoryService.categories$ + .pipe( + map(categories => + categories.filter(category => category.subCategories.length == 0) + .sort(this.byNameAscComparator()) + ) + ); + } + + private byNameAscComparator(): (categoryA: Category, categoryB: Category) => number { + return (categoryA, categoryB) => this.compareStrings(categoryA.name, categoryB.name); + } + + private compareStrings(stringA: string, stringB: string): number { + if (stringA < stringB) { + return -1; } - - get isLoading$(): Observable { - return this.publicationEditionService.isLoading$; + if (stringA > stringB) { + return 1; } + return 0; + } - get isPreviewing$(): Observable { - return this.publicationEditionService.isPreviewing$; + goPreviousLocation(): void { + this.#location.back(); + } + + insertTitle(titleNumber: number): void { + this.#publicationEditionService.insertTitle(titleNumber); + } + + selectAPicture(): void { + this.#publicationEditionService.selectAPicture(); + } + + insertLink(): void { + this.#publicationEditionService.insertLink(); + } + + displayCodeBlockDialog(): void { + this.#publicationEditionService.displayCodeBlockDialog(); + } + + displayPictureSectionDialog(): void { + this.#publicationEditionService.displayPictureSectionDialog(); + } + + updateCursorPosition(event: KeyboardEvent | MouseEvent): void { + if (event.target) { + const textarea = event.target as HTMLTextAreaElement; + + const positionStart = textarea.selectionStart; + const positionEnd = textarea.selectionEnd; + + this.#publicationEditionService.editCursorPosition(positionStart, positionEnd); } + } - get categories$(): Observable { - return this.categoryService.categories$ - .pipe( - map(categories => - categories.filter(category => category.subCategories.length == 0) - .sort(this.byNameAscComparator()) - ) - ); - } - - private byNameAscComparator(): (categoryA: Category, categoryB: Category) => number { - return (categoryA, categoryB) => this.compareStrings(categoryA.name, categoryB.name); - } - - private compareStrings(stringA: string, stringB: string): number { - if (stringA < stringB) { - return -1; - } - if (stringA > stringB) { - return 1; - } - return 0; - } - - ngOnChanges(): void { - this.ngOnDestroy(); - - if (!this.publicationInEdition || this.publicationInEdition !== this.publication) { - this.publicationInEdition = this.publication; - this.publicationEditionService.init(this.publicationInEdition); - } - } - - ngOnDestroy(): void { - this.subscriptions.forEach(subscription => subscription?.unsubscribe()); - } - - goPreviousLocation(): void { - this.location.back(); - } - - insertTitle(titleNumber: number): void { - this.publicationEditionService.insertTitle(titleNumber); - } - - selectAPicture(): void { - this.publicationEditionService.selectAPicture(); - } - - insertLink(): void { - this.publicationEditionService.insertLink(); - } - - displayCodeBlockDialog(): void { - this.publicationEditionService.displayCodeBlockDialog(); - } - - displayPictureSectionDialog(): void { - this.publicationEditionService.displayPictureSectionDialog(); - } - - updateCursorPosition(event: KeyboardEvent | MouseEvent): void { - if (event.target) { - const textarea = event.target as HTMLTextAreaElement; - - const positionStart = textarea.selectionStart; - const positionEnd = textarea.selectionEnd; - - this.publicationEditionService.editCursorPosition(positionStart, positionEnd); - } - } - - save(): void { - this.publicationSave.emit(this.publicationEditionService.editedPublication); - } - - onTabChange(tabSelectedIndex: number): void { - if (tabSelectedIndex === 1) { - this.publicationEditionService.loadPreview(); - } + save(): void { + this.publicationSave.emit(this.#publicationEditionService.editedPublication); + } + + onTabChange(tabSelectedIndex: number): void { + if (tabSelectedIndex === 1) { + this.#publicationEditionService.loadPreview(); } + } } diff --git a/frontend/src/app/components/publication-edition/publication-edition.service.ts b/frontend/src/app/components/publication-edition/publication-edition.service.ts index 36ccd58..0d01f46 100644 --- a/frontend/src/app/components/publication-edition/publication-edition.service.ts +++ b/frontend/src/app/components/publication-edition/publication-edition.service.ts @@ -1,312 +1,287 @@ -import { Location } from "@angular/common"; -import { inject, Injectable, OnDestroy } from "@angular/core"; -import { MatDialog } from "@angular/material/dialog"; -import { MatSnackBar } from "@angular/material/snack-bar"; -import { ActivatedRoute } from "@angular/router"; -import { BehaviorSubject, debounceTime, distinctUntilChanged, Observable, Subscription } from "rxjs"; -import { Publication } from "../../core/rest-services/publications/model/publication"; -import { PublicationRestService } from "../../core/rest-services/publications/publication.rest-service"; -import { copy } from "../../core/utils/ObjectUtils"; -import { CodeBlockDialog } from "./code-block-dialog/code-block-dialog.component"; -import { PictureSelectionDialog } from "./picture-selection-dialog/picture-selection-dialog.component"; -import { PreviewContentRequest } from "../../core/rest-services/publications/model/preview"; -import { FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms"; +import {Location} from "@angular/common"; +import {inject, Injectable, OnDestroy, Signal, signal} from "@angular/core"; +import {MatDialog} from "@angular/material/dialog"; +import {MatSnackBar} from "@angular/material/snack-bar"; +import {ActivatedRoute} from "@angular/router"; +import {debounceTime, distinctUntilChanged, Subscription} from "rxjs"; +import {DEFAULT_PUBLICATION, Publication} from "../../core/rest-services/publications/model/publication"; +import {PublicationRestService} from "../../core/rest-services/publications/publication.rest-service"; +import {copy} from "../../core/utils/ObjectUtils"; +import {CodeBlockDialog} from "./code-block-dialog/code-block-dialog.component"; +import {PictureSelectionDialog} from "./picture-selection-dialog/picture-selection-dialog.component"; +import {PreviewContentRequest} from "../../core/rest-services/publications/model/preview"; +import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms"; declare let Prism: any; export class CursorPosition { - start: number; - end: number; - selectedCharacters: number; + start: number; + end: number; + selectedCharacters: number; - constructor(start: number, end: number) { - this.start = start; - this.end = end; - this.selectedCharacters = end - start; - } + constructor(start: number, end: number) { + this.start = start; + this.end = end; + this.selectedCharacters = end - start; + } } export interface PublicationEditionState { - publication: Publication; - cursorPosition: CursorPosition; + publication: Publication; + cursorPosition: CursorPosition; } -const DEFAULT_PUBLICATION: Publication = { - id: '', - key: '', - title: '', - text: '', - parsedText: '', - description: '', - creationDate: new Date(), - illustrationId: '', - categoryId: '', - author: { - id: '', - name: '', - image: '' - } -}; - const DEFAULT_CURSOR_POSITION = new CursorPosition(0, 0); const DEFAULT_STATE: PublicationEditionState = { - publication: DEFAULT_PUBLICATION, - cursorPosition: DEFAULT_CURSOR_POSITION + publication: DEFAULT_PUBLICATION, + cursorPosition: DEFAULT_CURSOR_POSITION }; @Injectable() export class PublicationEditionService implements OnDestroy { - private readonly activatedRoute = inject(ActivatedRoute); - private readonly dialog = inject(MatDialog); - private readonly formBuilder = inject(FormBuilder); - private readonly location = inject(Location); - private readonly publicationRestService = inject(PublicationRestService); - private readonly snackBar = inject(MatSnackBar); + readonly #activatedRoute = inject(ActivatedRoute); + readonly #dialog = inject(MatDialog); + readonly #formBuilder = inject(FormBuilder); + readonly #location = inject(Location); + readonly #publicationRestService = inject(PublicationRestService); + readonly #snackBar = inject(MatSnackBar); - private isLoadingSubject = new BehaviorSubject(false); - private stateSubject = new BehaviorSubject(copy(DEFAULT_STATE)); - private subscriptions: Subscription[] = []; - private isSavingSubject = new BehaviorSubject(false); - private isPreviewingSubject = new BehaviorSubject(false); + #isLoading = signal(false); + #state = signal(copy(DEFAULT_STATE)); + #isSaving = signal(false); + #isPreviewing = signal(false); + #subscriptions: Subscription[] = []; - publicationEditionForm: FormGroup = this.formBuilder.group({ - title: new FormControl('', [Validators.required]), - description: new FormControl('', [Validators.required]), - text: new FormControl('', [Validators.required]), - illustrationId: new FormControl('', [Validators.required]), - categoryId: new FormControl('', [Validators.required]) + publicationEditionForm: FormGroup = this.#formBuilder.group({ + title: new FormControl('', [Validators.required]), + description: new FormControl('', [Validators.required]), + text: new FormControl('', [Validators.required]), + illustrationId: new FormControl('', [Validators.required]), + categoryId: new FormControl('', [Validators.required]) + }); + + ngOnDestroy(): void { + this.#subscriptions.forEach(subscription => subscription.unsubscribe()); + } + + #updateForm(): void { + const state = this.#state(); + const publication = state.publication; + + this.publicationEditionForm.controls['title'].setValue(publication.title); + this.publicationEditionForm.controls['description'].setValue(publication.description); + this.publicationEditionForm.controls['text'].setValue(publication.text); + this.publicationEditionForm.controls['illustrationId'].setValue(publication.illustrationId); + this.publicationEditionForm.controls['categoryId'].setValue(publication.categoryId); + } + + get isLoading(): Signal { + return this.#isLoading.asReadonly(); + } + + get isSaving(): Signal { + return this.#isSaving.asReadonly(); + } + + get isPreviewing(): Signal { + return this.#isPreviewing.asReadonly(); + } + + get state(): Signal { + return this.#state.asReadonly(); + } + + get editedPublication(): Publication { + return this.#state().publication; + } + + loadPublication(): void { + this.#isLoading.set(true); + + this.#activatedRoute.paramMap.subscribe(params => { + const publicationId = params.get('publicationId'); + if (publicationId == undefined) { + this.#snackBar.open($localize`A technical error occurred while loading publication data.`, $localize`Close`, {duration: 5000}); + this.#location.back(); + } else { + this.#publicationRestService.getById(publicationId) + .then(publication => { + const state = this.#state(); + state.publication = publication; + this.#state.set(state); + }) + .catch(error => { + const errorMessage = $localize`A technical error occurred while loading publication data.`; + this.#snackBar.open(errorMessage, $localize`Close`, {duration: 5000}); + console.error(errorMessage, error) + }) + .finally(() => this.#isLoading.set(false)); + } }); + } - ngOnDestroy(): void { - this.subscriptions.forEach(subscription => subscription.unsubscribe()); - } + init(publication: Publication): void { + const state = this.#state(); + state.publication = publication; + this.#state.set(state); + this.#updateForm(); - private get _state(): PublicationEditionState { - return this.stateSubject.value; - } - - private _save(state: PublicationEditionState): void { - this.stateSubject.next(state); - } - - private _updateForm(): void { - const state = this._state; + const formValueChangesSubscription = this.publicationEditionForm.valueChanges + .pipe( + debounceTime(200), + distinctUntilChanged() + ) + .subscribe(formValue => { + const state = this.#state(); const publication = state.publication; - this.publicationEditionForm.controls['title'].setValue(publication.title); - this.publicationEditionForm.controls['description'].setValue(publication.description); - this.publicationEditionForm.controls['text'].setValue(publication.text); - this.publicationEditionForm.controls['illustrationId'].setValue(publication.illustrationId); - this.publicationEditionForm.controls['categoryId'].setValue(publication.categoryId); - } + publication.title = formValue.title; + publication.description = formValue.description; + publication.categoryId = formValue.categoryId; + publication.text = formValue.text; - get isLoading$(): Observable { - return this.isLoadingSubject.asObservable(); - } + this.#state.set(state); + }); + this.#subscriptions.push(formValueChangesSubscription); + } - get isSaving$(): Observable { - return this.isSavingSubject.asObservable(); - } + private editIllustrationId(pictureId: string): void { + const state = this.#state(); + state.publication.illustrationId = pictureId + this.#state.set(state); + } - get isPreviewing$(): Observable { - return this.isPreviewingSubject.asObservable(); - } + displayPictureSectionDialog(): void { + const dialogRef = this.#dialog.open(PictureSelectionDialog); - get state$(): Observable { - return this.stateSubject.asObservable(); - } - - get editedPublication(): Publication { - return this._state.publication; - } - - loadPublication(): void { - this.isLoadingSubject.next(true); - - this.activatedRoute.paramMap.subscribe(params => { - const publicationId = params.get('publicationId'); - if (publicationId == undefined) { - this.snackBar.open($localize`A technical error occurred while loading publication data.`, $localize`Close`, { duration: 5000 }); - this.location.back(); - } else { - this.publicationRestService.getById(publicationId) - .then(publication => { - const state = this._state; - state.publication = publication; - this.stateSubject.next(state); - }) - .catch(error => { - const errorMessage = $localize`A technical error occurred while loading publication data.`; - this.snackBar.open(errorMessage, $localize`Close`, {duration: 5000}); - console.error(errorMessage, error) - }) - .finally(() => this.isLoadingSubject.next(false)); - } - }); - } - - init(publication: Publication): void { - const state = this._state; - state.publication = publication; - this.stateSubject.next(state); - this._updateForm(); - - const formValueChangesSubscription = this.publicationEditionForm.valueChanges - .pipe( - debounceTime(200), - distinctUntilChanged() - ) - .subscribe(formValue => { - const state = this._state; - const publication = state.publication; - - publication.title = formValue.title; - publication.description = formValue.description; - publication.categoryId = formValue.categoryId; - publication.text = formValue.text; - - this._save(state); - }) - this.subscriptions.push(formValueChangesSubscription); - } - - private editIllustrationId(pictureId: string): void { - const state = this._state; - state.publication.illustrationId = pictureId - this._save(state); - } - - displayPictureSectionDialog(): void { - const dialogRef = this.dialog.open(PictureSelectionDialog); - - const afterDialogCloseSubscription = dialogRef.afterClosed() - .subscribe(newPictureId => { - if (newPictureId) { - this.editIllustrationId(newPictureId); - } - }); - this.subscriptions.push(afterDialogCloseSubscription); - } - - displayCodeBlockDialog(): void { - const dialogRef = this.dialog.open(CodeBlockDialog, { width: '60em' }); - - const afterDialogCloseSubscription = dialogRef.afterClosed() - .subscribe(codeBlockWithLanguage => { - if (codeBlockWithLanguage) { - this.insertCodeBlock(codeBlockWithLanguage.programmingLanguage, codeBlockWithLanguage.codeBlock); - } - }); - this.subscriptions.push(afterDialogCloseSubscription); - } - - editCursorPosition(positionStart: number, positionEnd: number): void { - const state = this._state; - - state.cursorPosition.start = positionStart; - state.cursorPosition.end = positionEnd; - - this._save(state); - } - - insertTitle(titleNumber: number): void { - if (titleNumber >= 1 && titleNumber <= 3) { - const state = this._state; - - const publication = state.publication; - - const publicationTextLeftPart = publication.text.substring(0, state.cursorPosition.start); - const publicationTextMiddlePart = publication.text.substring(state.cursorPosition.start, state.cursorPosition.end); - const publicationTextRightPart = publication.text.substring(state.cursorPosition.end); - const textWithTags = `${publicationTextLeftPart}[h${titleNumber}]${publicationTextMiddlePart}[/h${titleNumber}]${publicationTextRightPart}`; - - publication.text = textWithTags; - - this._save(state); - this._updateForm(); - } else { - console.error(`Bad value for parameter of function 'insertTitle': '${titleNumber}'.`); + const afterDialogCloseSubscription = dialogRef.afterClosed() + .subscribe(newPictureId => { + if (newPictureId) { + this.editIllustrationId(newPictureId); } + }); + this.#subscriptions.push(afterDialogCloseSubscription); + } + + displayCodeBlockDialog(): void { + const dialogRef = this.#dialog.open(CodeBlockDialog, {width: '60em'}); + + const afterDialogCloseSubscription = dialogRef.afterClosed() + .subscribe(codeBlockWithLanguage => { + if (codeBlockWithLanguage) { + this.insertCodeBlock(codeBlockWithLanguage.programmingLanguage, codeBlockWithLanguage.codeBlock); + } + }); + this.#subscriptions.push(afterDialogCloseSubscription); + } + + editCursorPosition(positionStart: number, positionEnd: number): void { + const state = this.#state(); + + state.cursorPosition.start = positionStart; + state.cursorPosition.end = positionEnd; + + this.#state.set(state); + } + + insertTitle(titleNumber: number): void { + if (titleNumber >= 1 && titleNumber <= 3) { + const state = this.#state(); + + const publication = state.publication; + + const publicationTextLeftPart = publication.text.substring(0, state.cursorPosition.start); + const publicationTextMiddlePart = publication.text.substring(state.cursorPosition.start, state.cursorPosition.end); + const publicationTextRightPart = publication.text.substring(state.cursorPosition.end); + const textWithTags = `${publicationTextLeftPart}[h${titleNumber}]${publicationTextMiddlePart}[/h${titleNumber}]${publicationTextRightPart}`; + + publication.text = textWithTags; + + this.#state.set(state); + this.#updateForm(); + } else { + console.error(`Bad value for parameter of function 'insertTitle': '${titleNumber}'.`); } + } - selectAPicture(): void { - const dialogRef = this.dialog.open(PictureSelectionDialog); + selectAPicture(): void { + const dialogRef = this.#dialog.open(PictureSelectionDialog); - const afterDialogCloseSubscription = dialogRef.afterClosed() - .subscribe(newPictureId => { - if (newPictureId) { - this.insertPicture(newPictureId); - } - }); - this.subscriptions.push(afterDialogCloseSubscription); - } + const afterDialogCloseSubscription = dialogRef.afterClosed() + .subscribe(newPictureId => { + if (newPictureId) { + this.insertPicture(newPictureId); + } + }); + this.#subscriptions.push(afterDialogCloseSubscription); + } - insertPicture(pictureId: string): void { - const state = this._state; + insertPicture(pictureId: string): void { + const state = this.#state(); - const publication = state.publication; + const publication = state.publication; - const publicationTextLeftPart = publication.text.substring(0, state.cursorPosition.start); - const publicationTextRightPart = publication.text.substring(state.cursorPosition.start); - const textWithTags = `${publicationTextLeftPart}[img src="/api/pictures/${pictureId}" /]${publicationTextRightPart}`; + const publicationTextLeftPart = publication.text.substring(0, state.cursorPosition.start); + const publicationTextRightPart = publication.text.substring(state.cursorPosition.start); + const textWithTags = `${publicationTextLeftPart}[img src="/api/pictures/${pictureId}" /]${publicationTextRightPart}`; - publication.text = textWithTags; + publication.text = textWithTags; - this._save(state); - this._updateForm(); - } + this.#state.set(state); + this.#updateForm(); + } - insertLink(): void { - const state = this._state; + insertLink(): void { + const state = this.#state(); - const publication = state.publication; + const publication = state.publication; - const publicationTextLeftPart = publication.text.substring(0, state.cursorPosition.start); - const publicationTextMiddlePart = publication.text.substring(state.cursorPosition.start, state.cursorPosition.end); - const publicationTextRightPart = publication.text.substring(state.cursorPosition.end); - const textWithTags = `${publicationTextLeftPart}[link href="" txt="${publicationTextMiddlePart}" /]${publicationTextRightPart}`; + const publicationTextLeftPart = publication.text.substring(0, state.cursorPosition.start); + const publicationTextMiddlePart = publication.text.substring(state.cursorPosition.start, state.cursorPosition.end); + const publicationTextRightPart = publication.text.substring(state.cursorPosition.end); + const textWithTags = `${publicationTextLeftPart}[link href="" txt="${publicationTextMiddlePart}" /]${publicationTextRightPart}`; - publication.text = textWithTags; + publication.text = textWithTags; - this._save(state); - this._updateForm(); - } + this.#state.set(state); + this.#updateForm(); + } - private insertCodeBlock(programmingLanguage: string, codeBlock: string): void { - const state = this._state; + private insertCodeBlock(programmingLanguage: string, codeBlock: string): void { + const state = this.#state(); - const publication = state.publication; + const publication = state.publication; - const publicationTextLeftPart = publication.text.substring(0, state.cursorPosition.start); - const publicationTextRightPart = publication.text.substring(state.cursorPosition.start); - const codeBlockInstruction = `\n[code lg="${programmingLanguage}"]\n${codeBlock}\n[/code]\n\n`; - const textWithTags = `${publicationTextLeftPart}${codeBlockInstruction}${publicationTextRightPart}`; + const publicationTextLeftPart = publication.text.substring(0, state.cursorPosition.start); + const publicationTextRightPart = publication.text.substring(state.cursorPosition.start); + const codeBlockInstruction = `\n[code lg="${programmingLanguage}"]\n${codeBlock}\n[/code]\n\n`; + const textWithTags = `${publicationTextLeftPart}${codeBlockInstruction}${publicationTextRightPart}`; - publication.text = textWithTags; + publication.text = textWithTags; - this._save(state); - this._updateForm(); - } + this.#state.set(state); + this.#updateForm(); + } - loadPreview(): void { - const state = this._state; + loadPreview(): void { + const state = this.#state(); - this.isPreviewingSubject.next(true); - const request: PreviewContentRequest = { - text: state.publication.text - }; - this.publicationRestService.preview(request) - .then(response => { - state.publication.parsedText = response.text; - this._save(state); - setTimeout(() => Prism.highlightAll(), 1000); - }) - .catch(error => { - console.error(error); - }) - .finally(() => { - this.isPreviewingSubject.next(false); - }); - } -} \ No newline at end of file + this.#isPreviewing.set(true); + const request: PreviewContentRequest = { + text: state.publication.text + }; + this.#publicationRestService.preview(request) + .then(response => { + state.publication.parsedText = response.text; + this.#state.set(state); + setTimeout(() => Prism.highlightAll(), 1000); + }) + .catch(error => { + console.error(error); + }) + .finally(() => { + this.#isPreviewing.set(false); + }); + } +} diff --git a/frontend/src/app/components/publication-list/publication-list.component.html b/frontend/src/app/components/publication-list/publication-list.component.html index 7986926..084981d 100644 --- a/frontend/src/app/components/publication-list/publication-list.component.html +++ b/frontend/src/app/components/publication-list/publication-list.component.html @@ -1,16 +1,16 @@ -@for(publication of publications(); track publication.id) { - - -
-

{{publication.title}}

-

{{publication.description}}

-
-
+ } diff --git a/frontend/src/app/components/publication-list/publication-list.component.scss b/frontend/src/app/components/publication-list/publication-list.component.scss index a43901b..45670db 100644 --- a/frontend/src/app/components/publication-list/publication-list.component.scss +++ b/frontend/src/app/components/publication-list/publication-list.component.scss @@ -1,87 +1,87 @@ $cardBorderRadius: .5em; :host { + display: flex; + flex-direction: column; + gap: 2em; + max-width: 50em; + width: 90%; + margin: auto; + + .publication { display: flex; flex-direction: column; - gap: 2em; - max-width: 50em; - width: 90%; - margin: auto; + border-radius: $cardBorderRadius; + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .16), 0 2px 10px 0 rgba(0, 0, 0, .12); + transition: box-shadow .2s ease-in-out; + text-decoration: none; + color: black; + background-color: #ffffff; - .publication { - display: flex; - flex-direction: column; - border-radius: $cardBorderRadius; - box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),0 2px 10px 0 rgba(0,0,0,.12); - transition: box-shadow .2s ease-in-out; - text-decoration: none; - color: black; - background-color: #ffffff; - - &:hover { - box-shadow: 0 4px 8px 0 rgba(0,0,0,.24),0 4px 14px 0 rgba(0,0,0,.16); - } - - img { - object-fit: cover; - height: 15em; - border-radius: $cardBorderRadius $cardBorderRadius 0 0; - transition: height .2s ease-in-out; - - @media screen and (min-width: 450px) { - height: 20em; - } - - @media screen and (min-width: 600px) { - height: 25em; - } - - @media screen and (min-width: 750px) { - height: 32em; - } - } - - .body { - display: flex; - flex-direction: column; - padding: 1.5em 2em; - - h1 { - font-size: 1.8em; - margin-bottom: .5em; - } - - h2 { - font-size: 1em; - line-height: 1.4em; - margin: 0; - color: #747373; - font-weight: 400; - } - } - - .footer { - display: flex; - flex-direction: row; - align-items: center; - background-color: #f0f0f0; - border-radius: 0 0 $cardBorderRadius $cardBorderRadius; - padding: 1em 2em; - gap: 1em; - color: #6c757d; - - img { - $imageSize: 4em; - border-radius: 10em; - width: $imageSize; - height: $imageSize; - object-fit: cover; - } - - .publication-date { - font-style: italic; - color: #bdbdbd; - } - } + &:hover { + box-shadow: 0 4px 8px 0 rgba(0, 0, 0, .24), 0 4px 14px 0 rgba(0, 0, 0, .16); } -} \ No newline at end of file + + img { + object-fit: cover; + height: 15em; + border-radius: $cardBorderRadius $cardBorderRadius 0 0; + transition: height .2s ease-in-out; + + @media screen and (min-width: 450px) { + height: 20em; + } + + @media screen and (min-width: 600px) { + height: 25em; + } + + @media screen and (min-width: 750px) { + height: 32em; + } + } + + .body { + display: flex; + flex-direction: column; + padding: 1.5em 2em; + + h1 { + font-size: 1.8em; + margin-bottom: .5em; + } + + h2 { + font-size: 1em; + line-height: 1.4em; + margin: 0; + color: #747373; + font-weight: 400; + } + } + + .footer { + display: flex; + flex-direction: row; + align-items: center; + background-color: #f0f0f0; + border-radius: 0 0 $cardBorderRadius $cardBorderRadius; + padding: 1em 2em; + gap: 1em; + color: #6c757d; + + img { + $imageSize: 4em; + border-radius: 10em; + width: $imageSize; + height: $imageSize; + object-fit: cover; + } + + .publication-date { + font-style: italic; + color: #bdbdbd; + } + } + } +} diff --git a/frontend/src/app/components/publication-list/publication-list.component.ts b/frontend/src/app/components/publication-list/publication-list.component.ts index 43b2d91..b18cdda 100644 --- a/frontend/src/app/components/publication-list/publication-list.component.ts +++ b/frontend/src/app/components/publication-list/publication-list.component.ts @@ -1,16 +1,15 @@ -import {Component, input, Input} from "@angular/core"; -import { Publication } from "../../core/rest-services/publications/model/publication"; -import { Observable } from "rxjs"; -import { CommonModule } from "@angular/common"; -import { RouterModule } from "@angular/router"; -import { MatTooltipModule } from "@angular/material/tooltip"; +import {Component, input} from "@angular/core"; +import {Publication} from "../../core/rest-services/publications/model/publication"; +import {CommonModule} from "@angular/common"; +import {RouterModule} from "@angular/router"; +import {MatTooltipModule} from "@angular/material/tooltip"; @Component({ - selector: 'app-publication-list', - templateUrl: './publication-list.component.html', - styleUrl: './publication-list.component.scss', - imports: [CommonModule, RouterModule, MatTooltipModule] + selector: 'app-publication-list', + templateUrl: './publication-list.component.html', + styleUrl: './publication-list.component.scss', + imports: [CommonModule, RouterModule, MatTooltipModule] }) export class PublicationListComponent { - publications = input.required(); + publications = input.required(); } diff --git a/frontend/src/app/components/publications-search-bar/publications-search-bar.component.html b/frontend/src/app/components/publications-search-bar/publications-search-bar.component.html index 67c8ec2..0bf8ff4 100644 --- a/frontend/src/app/components/publications-search-bar/publications-search-bar.component.html +++ b/frontend/src/app/components/publications-search-bar/publications-search-bar.component.html @@ -1,6 +1,6 @@
- - -
\ No newline at end of file + + + diff --git a/frontend/src/app/components/publications-search-bar/publications-search-bar.component.scss b/frontend/src/app/components/publications-search-bar/publications-search-bar.component.scss index fb10d3f..72b683d 100644 --- a/frontend/src/app/components/publications-search-bar/publications-search-bar.component.scss +++ b/frontend/src/app/components/publications-search-bar/publications-search-bar.component.scss @@ -1,38 +1,38 @@ :host { - $borderRadiusValue: 10em; - position: relative; - flex-direction: row; - align-items: center; + $borderRadiusValue: 10em; + position: relative; + flex-direction: row; + align-items: center; - form { - display: flex; + form { + display: flex; - input { - flex: 1; - border-radius: $borderRadiusValue; - background-color: white; - border: solid 1px #ddd; - padding: .2em 2.7em .2em 1em; - height: 2em; - width: 100%; - } - - button { - position: absolute; - display: flex; - align-items: center; - border-radius: $borderRadiusValue; - background-color: white; - border: none; - top: 0; - right: 0; - color: #aaaaaa; - padding: .3em; - - &:hover { - background-color: #eee; - cursor: pointer; - } - } + input { + flex: 1; + border-radius: $borderRadiusValue; + background-color: white; + border: solid 1px #ddd; + padding: .2em 2.7em .2em 1em; + height: 2em; + width: 100%; } -} \ No newline at end of file + + button { + position: absolute; + display: flex; + align-items: center; + border-radius: $borderRadiusValue; + background-color: white; + border: none; + top: 0; + right: 0; + color: #aaaaaa; + padding: .3em; + + &:hover { + background-color: #eee; + cursor: pointer; + } + } + } +} diff --git a/frontend/src/app/components/publications-search-bar/publications-search-bar.component.ts b/frontend/src/app/components/publications-search-bar/publications-search-bar.component.ts index 7880844..58ad511 100644 --- a/frontend/src/app/components/publications-search-bar/publications-search-bar.component.ts +++ b/frontend/src/app/components/publications-search-bar/publications-search-bar.component.ts @@ -1,36 +1,36 @@ -import { Component, inject } from "@angular/core"; -import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from "@angular/forms"; -import { MatRippleModule } from "@angular/material/core"; -import { MatIconModule } from "@angular/material/icon"; -import { Router } from "@angular/router"; +import {Component, inject} from "@angular/core"; +import {FormBuilder, FormControl, ReactiveFormsModule, Validators} from "@angular/forms"; +import {MatRippleModule} from "@angular/material/core"; +import {MatIconModule} from "@angular/material/icon"; +import {Router} from "@angular/router"; @Component({ - selector: 'app-publications-search-bar', - templateUrl: './publications-search-bar.component.html', - styleUrl: './publications-search-bar.component.scss', - imports: [ - MatIconModule, - MatRippleModule, - ReactiveFormsModule - ], - providers: [] + selector: 'app-publications-search-bar', + templateUrl: './publications-search-bar.component.html', + styleUrl: './publications-search-bar.component.scss', + imports: [ + MatIconModule, + MatRippleModule, + ReactiveFormsModule + ], + providers: [] }) export class PublicationsSearchBarComponent { - private formBuilder = inject(FormBuilder); - private router = inject(Router); - formGroup = this.formBuilder.group({ - criteria: new FormControl('', [Validators.required]) - }); + private formBuilder = inject(FormBuilder); + private router = inject(Router); + formGroup = this.formBuilder.group({ + criteria: new FormControl('', [Validators.required]) + }); - searchPublications(): void { - const query = this.formGroup.controls.criteria.value + searchPublications(): void { + const query = this.formGroup.controls.criteria.value - if (query?.trim()) { - const queryParams = { 'query' : this.formGroup.controls.criteria.value ?? '' } - this.router.navigate(['/publications'], { queryParams }); - } else { - this.router.navigate(['/home']); - } + if (query?.trim()) { + const queryParams = {'query': this.formGroup.controls.criteria.value ?? ''} + this.router.navigate(['/publications'], {queryParams}); + } else { + this.router.navigate(['/home']); } -} \ No newline at end of file + } +} diff --git a/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.html b/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.html index 08fe4e9..8a83dab 100644 --- a/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.html +++ b/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.html @@ -1,18 +1,18 @@ -@for(category of categories$ | async; track category) { -
-
- {{category.name}} - chevron_right -
-
- @for(subCategory of category.subCategories; track subCategory) { - - {{subCategory.name}} - - } -
+@for (category of categories$ | async; track category) { +
+
+ {{ category.name }} + chevron_right
-} \ No newline at end of file +
+ @for (subCategory of category.subCategories; track subCategory) { + + {{ subCategory.name }} + + } +
+
+} diff --git a/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.scss b/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.scss index bcd6a8f..df29967 100644 --- a/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.scss +++ b/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.scss @@ -1,57 +1,57 @@ :host { - display: flex; - flex-direction: column; + display: flex; + flex-direction: column; - .category { + .category { + transition: background-color .2s ease-in-out; + + &:hover { + cursor: pointer; + background-color: #5c6bc0; + } + + .category-header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: .5em 1em; + + mat-icon { + transition: transform .2s ease-in-out; + } + } + + &.openned { + .category-header { + mat-icon { + transform: rotate(90deg); + } + } + + .sub-category-container { + max-height: none; + } + } + + .sub-category-container { + display: flex; + flex-direction: column; + overflow: hidden; + max-height: 0; + transition: max-height .2s ease-in-out; + background-color: #303f9f; + + .sub-category { + padding: .5em 1em .5em 2em; + text-decoration: none; + color: inherit; transition: background-color .2s ease-in-out; &:hover { - cursor: pointer; - background-color: #5c6bc0; - } - - .category-header { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - padding: .5em 1em; - - mat-icon { - transition: transform .2s ease-in-out; - } - } - - &.openned { - .category-header { - mat-icon { - transform: rotate(90deg); - } - } - - .sub-category-container { - max-height: none; - } - } - - .sub-category-container { - display: flex; - flex-direction: column; - overflow: hidden; - max-height: 0; - transition: max-height .2s ease-in-out; - background-color: #303f9f; - - .sub-category { - padding: .5em 1em .5em 2em; - text-decoration: none; - color: inherit; - transition: background-color .2s ease-in-out; - - &:hover { - background-color: #5c6bc0; - } - } + background-color: #5c6bc0; } + } } + } } diff --git a/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.ts b/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.ts index b0e8603..226a9c2 100644 --- a/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.ts +++ b/frontend/src/app/components/side-menu/categories-menu/categories-menu.component.ts @@ -1,19 +1,19 @@ -import { CommonModule } from "@angular/common"; -import { Component, EventEmitter, inject, OnInit, Output } from "@angular/core"; -import { MatIconModule } from "@angular/material/icon"; -import { DisplayableCategory, SideMenuService } from "../side-menu.service"; -import { Observable } from "rxjs"; -import { RouterModule } from "@angular/router"; +import {CommonModule} from "@angular/common"; +import {Component, EventEmitter, inject, OnInit, Output} from "@angular/core"; +import {MatIconModule} from "@angular/material/icon"; +import {DisplayableCategory, SideMenuService} from "../side-menu.service"; +import {Observable} from "rxjs"; +import {RouterModule} from "@angular/router"; @Component({ - selector: 'app-categories-menu', - templateUrl: './categories-menu.component.html', - imports: [ - CommonModule, - RouterModule, - MatIconModule - ], - styleUrl: './categories-menu.component.scss' + selector: 'app-categories-menu', + templateUrl: './categories-menu.component.html', + imports: [ + CommonModule, + RouterModule, + MatIconModule + ], + styleUrl: './categories-menu.component.scss' }) export class CategoriesMenuComponent implements OnInit { private sideMenuService = inject(SideMenuService); @@ -37,10 +37,10 @@ export class CategoriesMenuComponent implements OnInit { } else { const categoriesDivs = document.getElementsByClassName('category-header'); Array.from(categoriesDivs) - .map(category => category as HTMLElement) + .map(category => category as HTMLElement) .forEach(categoryDiv => this.closeAccordion(categoryDiv)); - - const categoryDiv = document.getElementById(`category-${category.id}`); + + const categoryDiv = document.getElementById(`category-${category.id}`); if (categoryDiv) { this.openAccordion(categoryDiv); } @@ -58,4 +58,4 @@ export class CategoriesMenuComponent implements OnInit { const divContent = categoryDiv?.nextElementSibling as HTMLElement; divContent.style.maxHeight = `${divContent.scrollHeight}px`; } -} \ No newline at end of file +} diff --git a/frontend/src/app/components/side-menu/side-menu.component.html b/frontend/src/app/components/side-menu/side-menu.component.html index 34cf202..583647e 100644 --- a/frontend/src/app/components/side-menu/side-menu.component.html +++ b/frontend/src/app/components/side-menu/side-menu.component.html @@ -1,19 +1,19 @@ -