Selaa lähdekoodia

Improvements for image viewing and gallery screen and controls. Also, images and all other info are loaded in the right language.

Iñigo Valentin 1 kuukausi sitten
vanhempi
sitoutus
3c93cfbdb0

+ 2 - 2
frontend/src/app/app.scss

@@ -9,14 +9,14 @@ body{
 }
 
 app-root {
-  height: 100vh;
+  min-height: 100vh;
   display: flex;
   flex-direction: column;
   
   main{
     flex-grow: 1;
     max-width: 80em;
-    margin: auto;
+    margin: 0 auto;
   }
 }
 

+ 5 - 38
frontend/src/app/app.ts

@@ -6,63 +6,30 @@ import { Meta } from '@angular/platform-browser';
 import { author, languages} from '../../package.json';
 import { Header } from './header/header';
 import { Footer } from './footer/footer';
+import { LanguageService } from './service/language-service';
 
 
 @Component({
   selector: 'app-root',
   imports: [RouterOutlet, Header, Footer],
   templateUrl: './app.html',
-  styleUrl: './app.scss',
+  styleUrls: ['./app.scss'],
   encapsulation: ViewEncapsulation.None,
 })
 export class App {
     
     private translate = inject(TranslateService);
-    private cookieService = inject(SsrCookieService);
+    private languageService = inject(LanguageService);
     
     author: string = author;
     private languages: any = languages;
 
-    /**
-     * Selects the best language to serve the page in.
-     * 
-     * It considers available languages, browser languages, query parameters, local storage...
-     * 
-     * @return Two letter language code of the language to show the page in.
-     */
-    private selectLanguage(): string{
-        
-        let available: String[] = this.languages.available.split("|");
-        // If the language is set as a request parameter and its valid, use it.
-        // TODO: Reimplement in SSR
-        //const queryLang = new URLSearchParams(window.location.search).get('lang');
-        //if (queryLang && available.indexOf(queryLang.substring(0, 2).toLowerCase()) != -1)
-        //    return queryLang.substring(0, 2).toLowerCase();
-
-        // If the language has been previously set and a valid one is its in local storage, done.
-        if (
-          this.cookieService.check('language')
-          && available.indexOf(this.cookieService.get('language')) != -1
-        )return "" + this.cookieService.get('language');
-        
-        // If the language is not set, loop the browser accepted languages.
-        // When there is a match with the app available languages, return it.
-        // TODO: Reimplement in SSR
-        /*for (let l of navigator.languages){
-            if ( l.length >= 2 && available.indexOf(l.substring(0, 2).toLowerCase()) != -1)
-                return l.substring(0, 2).toLowerCase();
-        }*/
-        
-        // If everything else failed, return the default language.
-        return this.languages.default;
-    }
-    
     constructor(private metaService: Meta, private route: ActivatedRoute){
         this.translate.addLangs(this.languages.available.split("|"));
         this.translate.setFallbackLang(this.languages.default);
         // Detect the best language.
-        const lang: string = this.selectLanguage()
-        this.translate.use(lang);        
+        const lang: string = this.languageService.resolveInitialLanguage();
+        this.languageService.applyLanguage(lang);
         // Set meta tags
         this.metaService.addTag({ property: 'author', author });
 

+ 5 - 7
frontend/src/app/footer/footer.ts

@@ -1,10 +1,10 @@
 import { Component, inject } from '@angular/core';
 import { version, author, authorURL, sourceSite, sourceURL, license } from '../../../package.json';
 import { TranslateService, TranslatePipe } from '@ngx-translate/core';
-import { SsrCookieService } from 'ngx-cookie-service-ssr';
+import { LanguageService } from '../service/language-service';
 
 @Component({
-    selector: 'app-footer', templateUrl: './footer.html', styleUrl: './footer.scss',
+    selector: 'app-footer', templateUrl: './footer.html', styleUrls: ['./footer.scss'],
     standalone: true, imports: [TranslatePipe]
  })
 export class Footer{
@@ -16,7 +16,7 @@ export class Footer{
     license: string = license;
     currentLanguage: string; 
     
-    private cookieService = inject(SsrCookieService)
+    private languageService = inject(LanguageService);
     
     languages = [
         { code: 'es', name: 'Español' },
@@ -27,13 +27,11 @@ export class Footer{
     private translate = inject(TranslateService)
     
     constructor(){
-        this.currentLanguage = this.translate.getCurrentLang();
+        this.currentLanguage = this.languageService.getRequestLanguage();
     }
     
     switchLanguage(languageCode: string): void {
-        this.currentLanguage = languageCode;
-        this.cookieService.set('language', languageCode);
-        this.translate.use(languageCode);
+        this.currentLanguage = this.languageService.applyLanguage(languageCode);
         setTimeout(location.reload.bind(location), 100);
     }
 

+ 1 - 1
frontend/src/app/profile/profile.html

@@ -9,7 +9,7 @@
       }
       <div id="profile-text">
         @for (t of profile.texts; track t) {
-          <p>{{ t.content }}</p>
+          <p [innerHTML]="t.content"></p>
         }
       </div>
     </div>

+ 59 - 20
frontend/src/app/project/project.scss

@@ -29,12 +29,15 @@ section#details{
 section.children{
     margin-left: 4em;
     margin-right: 4em;
+
+    @media only screen and (max-width : 600px) {margin: auto 1em;}
+
     h3{
         font-size: 150%;
         border-bottom: 0.2em solid;
         margin-left: -2em;
         margin-right: -2em;
-        
+
         @media only screen and (max-width : 800px){
             margin-left: 0;
             margin-right: 0;
@@ -42,6 +45,8 @@ section.children{
             border-bottom: 0.03em solid;
             text-align: center;
         }
+        
+        
     }
     section.children-images{
         text-align: center;
@@ -60,6 +65,11 @@ img.img, video.img{
     cursor: pointer;
     position: relative;
     display: inline-block;
+    @media only screen and (max-width : 600px) {
+        max-width: calc(50% - 1.5em);
+        margin: 0.2em;
+        border-radius: 0.2em;
+    }
 }
 
 img.video_play{
@@ -103,6 +113,7 @@ div#gallery-cover{
 div#gallery{
     display: none;
     position: fixed;
+    flex-direction: column;
     top: 5%;
     left: 10%;
     right: 10%;
@@ -112,6 +123,7 @@ div#gallery{
     border-radius: 0.4em;
     transition: opacity 1s ease-in-out;
     text-align: center;
+    overflow: hidden;
     z-index: 3;
     
     @media only screen and (max-width : 800px) {
@@ -125,7 +137,7 @@ div#gallery{
         background-color: variables.$background-off-white;
         display: none;
         display: block;
-        height: calc(100% - 4em);
+        height: calc(100% - 3.7em);
         width: 100%;
         position: absolute;
         z-index: 6;
@@ -134,29 +146,40 @@ div#gallery{
     }
 
     h3{
+        flex: 0 0 auto;
         text-align: left;
         font-size: 140%;
         margin: 0.6em;
-        max-height: 4em;
         border-bottom: 0.05em solid variables.$primary-brown;
 
         input[type='button']{
-            float: right;
-            padding: 0.3em 0.6em;
-            margin: -0.3em;
+            position: absolute;
+            right: 0.2em;
+            top: 0.2em;
+            font-size: 130%;
+            @media only screen and (max-width : 800px) {
+                right: 0.15em;
+                top: 0.15em;
+                font-size: 90%;
+            }
         }
+        
     }
     
     div#gallery-content{
-        position: absolute;
-        height: calc(100% - 4em);
+        display: flex;
+        flex-direction: column;
+        flex: 1 1 auto;
+        min-height: 0;
         width: 100%;
     }
     
     div#gallery-flex{
         display: flex;
         flex-direction: row;
-        height: calc(100% - 4em);
+        flex: 1 1 auto;
+        min-height: 0;
+        width: 100%;
         
         @media only screen and (max-width : 800px) {
             flex-direction: column;
@@ -166,20 +189,33 @@ div#gallery{
             flex: 70%;
             padding: 1em;
             text-align: center;
+            min-height: 0;
+            display: flex;
+            align-items: center;
+            justify-content: center;
             
-            @media only screen and (max-width : 800px) {max-height: 60%;}
+            @media only screen and (max-width : 800px) {
+                flex: 1 1 auto;
+                max-height: none;
+                padding: 0.35em 1em 0.2em;
+                align-items: stretch;
+                justify-content: stretch;
+            }
     
             img#gallery-img, video#gallery-video{
                 
-                border: 0.1em solid variables.$primary-brown;
                 border-radius: 0.3em;
-                max-width: 95%;
+                max-width: 100%;
                 max-height: 100%;
-                margin: auto;
+                margin: 0;
+                display: block;
                 
                 @media only screen and (max-width : 800px) {
-                    max-width: 95%;
-                    margin: 0.2em auto;
+                    width: 100%;
+                    height: 100%;
+                    max-width: none;
+                    max-height: none;
+                    object-fit: contain;
                 }
             }
         }
@@ -195,26 +231,29 @@ div#gallery{
             overflow-y: scroll;
             
             @media only screen and (max-width : 800px) {
-                margin: auto 2em;
+                margin: 0.2em 1em 0;
+                flex: 0 0 auto;
+                max-height: none;
+                overflow-y: visible;
             }
         }
     }
     div#gallery-controls{
+        flex: 0 0 auto;
         text-align: center;
         margin: 0;
+        padding-bottom: 0.2em;
         
         @media only screen and (max-width : 800px) {
-            position: absolute;
-            bottom: 0;
             width: 100%;
-            @media only screen and (max-width : 800px){display: flex}
+            display: flex;
         }
         
         input[type='button']{
             display: inline-block;
             padding: 1em 1.5em;
             margin: 0.3em 1em;
-            @media only screen and (max-width : 800px){flex: 50%}
+            @media only screen and (max-width : 800px){flex: 1 1 50%}
         }
     }
 }

+ 2 - 2
frontend/src/app/project/project.ts

@@ -47,7 +47,7 @@ export class Project {
 
               if (this.project.images.length === 0) {
                   for (const child of this.project.children) {
-                      const childFirstImage = child.images.shift();
+                      const childFirstImage = child.images[0];
                       if (childFirstImage) this.project.images.push(childFirstImage);
                   }
               }
@@ -125,7 +125,7 @@ export class Project {
         }
         cover.style.display = 'block';
         cover.style.opacity = '0.6';
-        gallery.style.display = 'block';
+        gallery.style.display = 'flex';
         gallery.style.opacity = '1';
         if (parsedIndex >= 1 && parsedIndex <= this.maxIndex){
             this.curIndex = parsedIndex;

+ 56 - 0
frontend/src/app/service/language-service.ts

@@ -0,0 +1,56 @@
+import { Injectable, inject } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+import { SsrCookieService } from 'ngx-cookie-service-ssr';
+import { languages } from '../../../package.json';
+
+@Injectable({ providedIn: 'root' })
+export class LanguageService {
+  private translate = inject(TranslateService);
+  private cookieService = inject(SsrCookieService);
+
+  private availableLanguages: string[] = languages.available.split('|');
+  private defaultLanguage: string = languages.default;
+
+  private isAvailableLanguage(lang: string | null | undefined): lang is string {
+    return !!lang && this.availableLanguages.indexOf(lang) !== -1;
+  }
+
+  resolveInitialLanguage(): string {
+    const cookieLanguage = this.cookieService.get('language');
+    if (this.isAvailableLanguage(cookieLanguage)) {
+      return cookieLanguage;
+    }
+
+    return this.defaultLanguage;
+  }
+
+  getRequestLanguage(): string {
+    const cookieLanguage = this.cookieService.get('language');
+    if (this.isAvailableLanguage(cookieLanguage)) {
+      return cookieLanguage;
+    }
+
+    const currentLanguage = this.translate.getCurrentLang();
+    if (this.isAvailableLanguage(currentLanguage)) {
+      return currentLanguage;
+    }
+
+    const fallbackLanguage = this.translate.getFallbackLang();
+    if (this.isAvailableLanguage(fallbackLanguage)) {
+      return fallbackLanguage;
+    }
+
+    return this.defaultLanguage;
+  }
+
+  applyLanguage(language: string): string {
+    const selectedLanguage = this.isAvailableLanguage(language)
+      ? language
+      : this.defaultLanguage;
+
+    this.cookieService.set('language', selectedLanguage);
+    this.translate.use(selectedLanguage);
+
+    return selectedLanguage;
+  }
+}

+ 4 - 3
frontend/src/app/service/profile-service.ts

@@ -1,20 +1,21 @@
 import { Injectable, inject } from '@angular/core';
-import { TranslateService } from '@ngx-translate/core';
 import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
 import { ProfileModel } from '../model/profile';
 import { environment } from '../../environments/environment';
+import { LanguageService } from './language-service';
 
 @Injectable({ providedIn: 'root' })
 
 export class ProfileService {
   private apiUrl = environment.apiUrl + '/profile';
-  private translate = inject(TranslateService);
+  private languageService = inject(LanguageService);
   constructor(private http: HttpClient) { }
 
   getProfile(images: any, texts: string): Observable<ProfileModel[]> {
+    const lang = this.languageService.getRequestLanguage();
     return this.http.get<ProfileModel[]>(
-      this.apiUrl + "?lang=" + this.translate.getCurrentLang()
+      this.apiUrl + "?lang=" + lang
       + "&images=" + images + "&texts=" + texts
     );
   }

+ 14 - 9
frontend/src/app/service/project-service.ts

@@ -1,35 +1,40 @@
 import { Injectable, inject } from '@angular/core';
 import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
-import { TranslateService } from '@ngx-translate/core';
 import { ProjectModel } from '../model/project';
 import { ProjectImageModel } from '../model/project-image';
 import { environment } from '../../environments/environment';
+import { LanguageService } from './language-service';
 
 @Injectable({ providedIn: 'root' })
 
 export class ProjectService {
   private apiUrl = environment.apiUrl + '/projects';
   constructor(private http: HttpClient) { }
-  
-  private translate = inject(TranslateService);
+  private languageService = inject(LanguageService);
 
   getProjects(images: any): Observable<ProjectModel[]> {
-    var paramImages: string = "";
-    if (images == true || parseInt(images) >= 0)
-        paramImages = "&images=" + images;
-    return this.http.get<ProjectModel[]>(this.apiUrl + "?lang=" + this.translate.getCurrentLang());
+    const lang = this.languageService.getRequestLanguage();
+    let url = this.apiUrl + "?lang=" + lang;
+
+    if (images === true || parseInt(images) >= 0) {
+      url += "&images=" + images;
+    }
+
+    return this.http.get<ProjectModel[]>(url);
   }
 
   getProject(id: string): Observable<ProjectModel> {
+    const lang = this.languageService.getRequestLanguage();
     return this.http.get<ProjectModel>(
-      `${this.apiUrl}/${id}` + "?lang=" + this.translate.getCurrentLang()
+      `${this.apiUrl}/${id}` + "?lang=" + lang
     );
   }
   
   getProjectRandomImage(id: string): Observable<ProjectImageModel> {
+    const lang = this.languageService.getRequestLanguage();
     return this.http.get<ProjectImageModel>(
-      `${this.apiUrl}/${id}/images/random` + "?lang=" + this.translate.getCurrentLang()
+      `${this.apiUrl}/${id}/images/random` + "?lang=" + lang
     );
   }
   

+ 6 - 0
package-lock.json

@@ -0,0 +1,6 @@
+{
+  "name": "leather-web",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {}
+}