Переглянути джерело

The app can now show profile information.

Iñigo Valentin 10 місяців тому
батько
коміт
21373b9462

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+deploy_to_*

+ 1 - 0
backend/.gitignore

@@ -7,6 +7,7 @@ environment/env.*
 
 # Assets
 assets/images/projects/
+assets/images/profile/
 assets/images_scaled/
 
 # Node.js

+ 1 - 1
backend/src/data/data.js

@@ -1,7 +1,7 @@
 const sqlite3 = require('sqlite3');
 
 let data = new sqlite3.Database(process.env.DB , (err) => {
-    if (err) console.log("Error Occurred - " + err.message);
+    if (err) console.log("Error connecting to database " + process.env.DB + err.message);
     else console.log("Conected to database " + process.env.DB);
 });
 

+ 19 - 0
backend/src/data/profileData.js

@@ -0,0 +1,19 @@
+const db = require("./data.js");
+
+let profileData = [];
+
+profileData.getProfile = function(lang = null) {
+    return new Promise((resolve, reject) => {
+        db.get(
+          "SELECT first_name, last_name, image, " + db.langQuery("tagline", lang)
+          + ", " + db.langQuery("bio", lang) + ", " + db.langQuery("description", lang)
+          + " FROM profile LIMIT 1",
+          async (err, row) => {
+            if(err) reject(err);
+            else resolve(row);
+          }
+        );
+    });
+}
+
+module.exports = profileData;

+ 29 - 0
backend/src/routers/assetRouter.js

@@ -43,5 +43,34 @@ router.get("/images/projects/:projectId/:imagePath", async (req, res) => {
     });
 });
 
+router.get("/images/profile/:imagePath", async (req, res) => {
+    var reqpath = req.url.toString().split('?')[0];
+    var file = "./assets" + reqpath.replace(/\/$/, '');
+    console.log("GET PROFILE IMAGE: " + file);
+
+    var scale = parseInt(req.query.w);
+    // Serve a scaled image if width has been specified.
+    if (parseInt(req.query.w) >= 0){
+        var width = 100;
+        for (var i = 100; i < 1000; i += 100){
+            width = i;
+            if (width >= scale) break;
+        }
+        file = file.replace("/assets/images/", "/assets/images_scaled/x" + width + "/");
+    }
+    var type = mime[path.extname(file).slice(1)] || 'text/plain';
+    var s = fs.createReadStream(file);
+    s.on('open', function () {
+        res.setHeader('Content-Type', type);
+        res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
+        s.pipe(res);
+    });
+    s.on('error', function () {
+        res.setHeader('Content-Type', 'text/plain');
+        res.statusCode = 404;
+        res.end('Not found');
+    });
+});
+
 
 module.exports = router;

+ 25 - 0
backend/src/routers/profileRouter.js

@@ -0,0 +1,25 @@
+const express = require("express");
+const cors = require('cors');
+const profileData = require("../data/profileData.js");
+const router = express.Router();
+
+/**
+ * Read (GET) the user profile.
+ * 
+ * Acccpeted request parameters:
+ * 
+ *  - lang: Two letter language code, case insensitive. If the data exists in
+ *          the requested language it will be served, otherwise the default
+ *          language will be used.
+ */
+router.get("/", cors({origin: '*', methods: 'GET'}), async (req, res) => {
+    var lang = process.env.DEFAULT_LANGUAGE;
+    if (
+      req.query.lang
+      && process.env.AVAILABLE_LANGUAGES.split(" ").includes(req.query.lang.toLowerCase())
+    ) lang = req.query.lang.toLowerCase();
+    const data = await profileData.getProfile(lang);
+    res.json(data);
+});
+
+module.exports = router;

+ 2 - 0
backend/src/routers/router.js

@@ -1,9 +1,11 @@
 const express = require('express');
 const projectRouter = require("./projectRouter.js");
+const profileRouter = require("./profileRouter.js");
 const assetRouter = require("./assetRouter.js");
 
 const router = express.Router();
 router.use("/projects", projectRouter);
+router.use("/profile", profileRouter);
 router.use("/assets", assetRouter);
 
 module.exports = router;

+ 1 - 1
frontend/package.json

@@ -1,6 +1,6 @@
 {
   "name": "leather-frontend",
-  "version": "0.0.2",
+  "version": "0.0.3",
   "author": "Iñigo Valentin",
   "authorURL": "https://inigovalentin.com",
   "sourceSite": "Github",

+ 0 - 1
frontend/src/app/app.ts

@@ -1,6 +1,5 @@
 import { Component, ViewEncapsulation, inject } from '@angular/core';
 import { RouterOutlet } from '@angular/router';
-import {  } from '@angular/core';
 import { TranslateService, TranslatePipe, TranslateDirective } from '@ngx-translate/core';
 import { Header } from './header/header';
 import { Footer } from './footer/footer';

+ 1 - 1
frontend/src/app/footer/footer.scss

@@ -12,7 +12,7 @@ footer{
         display: inline-block;
         vertical-align: middle;
         text-align: center;
-        width: 44%;
+        width: calc(44% - 3em);
         margin: 0;
         padding: 0.5em 2em;
         span{display: block;}

+ 9 - 3
frontend/src/app/header/header.scss

@@ -1,8 +1,14 @@
 header{
     text-align: center;
+    
+    h1{
+        margin: 0.2em;
 
-    img{
-        max-height: 7em;
-        max-width: 90%;
+        img{
+            max-height: 7em;
+            max-width: 90%;
+            
+            @media screen and (max-width: 800px){max-height: 5.6em;}
+        }
     }
 }

+ 10 - 0
frontend/src/app/home/home.html

@@ -1,3 +1,13 @@
+<section id='profile'>
+  <img
+    src="{{ apiURL }}{{ profile.image }}"
+    srcset="{{ utilService.generateSrcset(apiURL + profile.image) }}"
+  />
+  <div id="profile-text">
+    <p>{{ profile.bio }}</p>
+    <p>{{ profile.description }}</p>
+  </div>
+</section>
 <section id='catalog'>
   @for (project of projects; track project) {
     <article class="project">

+ 57 - 0
frontend/src/app/home/home.scss

@@ -1,5 +1,62 @@
 @use '../_variables';
 
+section#profile{
+    
+    max-width: 65em;
+    margin: 1em auto;
+    padding:0.5em 1em;
+    
+    img{
+        max-width: 12em;
+        height: 12em;
+        border-radius: 50%;
+        border: 0.3em solid variables.$primary-brown;
+        margin-right: 2em;
+        display: inline-block;
+        vertical-align: middle;
+        z-index: 1;
+        position: relative;
+        
+        @media screen and (max-width: 800px){
+            height: initial;
+            max-height: initial;
+            max-width: calc(30% - 2em);
+            margin-right: 1em;
+            margin-right: 0;
+        }
+    }
+    
+    div#profile-text{
+        max-width: calc(100% - 18em);
+        display: inline-block;
+        height: 10em;
+        max-height: 10em;
+        vertical-align: middle;
+        border-bottom: 0.2em solid variables.$primary-brown;
+        border-top: 0.2em solid variables.$primary-brown;
+        border-top-right-radius: 1em;
+        border-bottom-right-radius: 1em;
+        margin-left: -7em;
+        padding: 0.5em 3em 0.5em 7em;
+        z-index:0;
+        position: relative;
+        background: linear-gradient(90deg, #eeeeee 99%, #ffffff00 100%);
+        
+        @media screen and (max-width: 800px){
+            max-width: 65%;
+            font-size: 90%;
+            border-bottom: 0;
+            border-top: 0;
+            background: initial;
+            margin-left: initial;
+            padding: 0.3em 0.3em 0.3em 0.8em;
+            margin: 0;
+            height: initial;
+            max-height: initial;
+        }
+    }
+}
+
 section#catalog{
     text-align: center;
     font-size: 0;

+ 8 - 1
frontend/src/app/home/home.ts

@@ -2,26 +2,33 @@ import { Component, OnInit, inject } from '@angular/core';
 import { Title } from '@angular/platform-browser';
 import { TranslateService, _ } from '@ngx-translate/core';
 import { ProjectService } from '../service/project-service';
+import { ProfileService } from '../service/profile-service';
 import { UtilService } from '../service/util-service';
 import { ProjectModel } from '../model/project';
+import { ProfileModel } from '../model/profile';
 import { environment } from '../../environments/environment';
 
 @Component({ selector: 'app-home', templateUrl: './home.html', styleUrl: './home.scss'})
 export class Home implements OnInit {
     projects: ProjectModel[] = [];
+    profile: any;
     private intervalId: any;
     apiURL: string = environment.apiUrl;
     utilService: UtilService;
     
     private translate = inject(TranslateService)
 
-    constructor(private projectService: ProjectService, private titleService: Title){
+    constructor(
+      private projectService: ProjectService, private profileService: ProfileService,
+      private titleService: Title
+    ){
         this.utilService = new UtilService()
         this.translate.use("" + localStorage.getItem("language"));
     }
 
     ngOnInit(): void {
         this.projectService.getProjects().subscribe(data => { this.projects = data; });
+        this.profileService.getProfile().subscribe(data => { this.profile = data; });
         this.startInterval();
         this.translate.use("" + localStorage.getItem("language"));
         //this.translate.use

+ 8 - 0
frontend/src/app/model/profile.ts

@@ -0,0 +1,8 @@
+export interface ProfileModel{
+  first_name: string;
+  last_name: string;
+  image: string;
+  tagline: string;
+  bio: string;
+  description: string;
+}

+ 39 - 42
frontend/src/app/project/project.scss

@@ -13,14 +13,18 @@ div#details{
         font-size: 300%;
         border-bottom: 0.2em solid;
         margin-left: 2em;
+        
+        @media only screen and (max-width : 800px){
+            margin-left: initial;
+            margin-bottom: 0.5em;
+            border-bottom: 0.05em solid;
+            text-align: center;
+        }
     }
    
     p{margin: auto 0.2em;}
-}
 
-@media only screen and (max-width : 600px) {
-    div#details{margin: auto 1em;}
-    div#details h2{text-align: center;}
+    @media only screen and (max-width : 600px) {margin: auto 1em;}
 }
 
 div#images{text-align: center;}
@@ -52,15 +56,13 @@ img.img_main{
     max-height: 21em;
     float: left;
     margin: 0.5em 3em 0.5em 0.5em;
-}
 
-@media only screen and (max-width : 600px){
-    img.img_main{
+    @media only screen and (max-width : 800px){
         max-width: 100%;
         max-height: 24em;
         float: none;
         margin: 0.5em auto;
-        display: block;;
+        display: block;
     }
 }
 
@@ -90,6 +92,13 @@ div#gallery{
     transition: opacity 1s ease-in-out;
     text-align: center;
     z-index: 3;
+    
+    @media only screen and (max-width : 800px) {
+        top: 4%;
+        bottom: 2%;
+        left: 2%;
+        right: 2%;
+    }
 
     h3{
         text-align: left;
@@ -108,10 +117,14 @@ div#gallery{
         display: flex;
         max-height: calc(100% - 8em);
         
+        @media only screen and (max-width : 800px) {display: block;}
+        
         div#gallery-flex-img-container{
             flex: 70%;
             padding: 1em;
             text-align: center;
+            
+            @media only screen and (max-width : 800px) {max-height: 60%;}
     
             img#gallery-img, video#gallery-video{
                 
@@ -120,6 +133,13 @@ div#gallery{
                 max-width: 95%;
                 max-height: 100%;
                 margin: auto;
+                
+                @media only screen and (max-width : 800px) {
+                    display: block;
+                    max-height: calc(60% - 2em);
+                    max-width: 95%;
+                    margin: 0.2em auto;
+                }
             }
         }
         
@@ -132,12 +152,23 @@ div#gallery{
             border: 0.1em solid variables.$primary-brown;
             border-radius: 0.3em;
             overflow-y: scroll;
+            
+            @media only screen and (max-width : 800px) {
+                display: block;
+                margin: auto 2em;
+            }
         }
     }
     div#gallery-controls{
         text-align: center;
         margin: 0;
         
+        @media only screen and (max-width : 800px) {
+            position: absolute;
+            bottom: 0;
+            width: 100%;
+        }
+        
         input[type='button']{
             display: inline-block;
             padding: 1em 1.5em;
@@ -145,37 +176,3 @@ div#gallery{
         }
     }
 }
-
-@media only screen and (max-width : 600px) {
-
-    div#gallery{
-        top: 4%;
-        bottom: 2%;
-        left: 2%;
-        right: 2%;
-        
-        div#gallery-flex{
-            display: block;
-        }
-    
-        img#gallery-img{
-            display: block;
-            max-height: calc(60% - 2em);
-            max-width: 95%;
-            margin: 0.2em auto;
-        }
-    
-        p#gallery-text{
-            display: block;
-            max-width: 100%;
-            margin: auto 0.3em;
-            max-height: calc(40% - 2em);
-        }
-
-        div#gallery-controls{
-            position: absolute;
-            bottom: 0;
-            width: 100%;
-        }
-    }
-}

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

@@ -56,7 +56,7 @@ export class Project {
         let cover: HTMLDivElement = <HTMLDivElement> document.getElementById('gallery-cover');
         let gallery: HTMLDivElement = <HTMLDivElement> document.getElementById('gallery');
         cover.style.display = 'block';
-        cover.style.opacity = '0.4';
+        cover.style.opacity = '0.6';
         gallery.style.display = 'block';
         gallery.style.opacity = '1';
         if (index >= 0 && index <= this.maxIndex){
@@ -86,7 +86,8 @@ export class Project {
      */
     gallerySet(){
         let titleText = document.getElementById('img-' + this.curIndex)?.getAttribute("alt") || "";
-        if (titleText != null && titleText.length > 0) titleText = ': ' + titleText;
+        if (titleText != null && titleText.length > 0 && titleText != "" + this.project?.title)
+            titleText = ': ' + titleText;
         else titleText = '';
         let title: HTMLSpanElement = <HTMLSpanElement> document.getElementById('gallery-title');
         let text: HTMLParagraphElement

+ 16 - 0
frontend/src/app/service/profile-service.ts

@@ -0,0 +1,16 @@
+import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { ProfileModel } from '../model/profile';
+import { environment } from '../../environments/environment';
+
+@Injectable({ providedIn: 'root' })
+
+export class ProfileService {
+  private apiUrl = environment.apiUrl + '/profile';
+  constructor(private http: HttpClient) { }
+
+  getProfile(): Observable<ProfileModel[]> {
+    return this.http.get<ProfileModel[]>(this.apiUrl + "?lang=" + localStorage.getItem('language'));
+  }
+}