Browse Source

Project view functionality implemented.

Iñigo Valentin 1 year ago
parent
commit
fe227fa27e
82 changed files with 3962 additions and 407 deletions
  1. 60 88
      backend/inigovalentin/app/controllers/license.controller.js
  2. 120 0
      backend/inigovalentin/app/controllers/project-type.controller.js
  3. 63 88
      backend/inigovalentin/app/controllers/project.controller.js
  4. 24 4
      backend/inigovalentin/app/models/index.js
  5. 4 1
      backend/inigovalentin/app/models/lang.model.js
  6. 1 1
      backend/inigovalentin/app/models/license.model.js
  7. 12 0
      backend/inigovalentin/app/models/project-image.model.js
  8. 11 0
      backend/inigovalentin/app/models/project-type.model.js
  9. 11 0
      backend/inigovalentin/app/models/project-url-type.model.js
  10. 11 0
      backend/inigovalentin/app/models/project-url.model.js
  11. 3 4
      backend/inigovalentin/app/models/project.model.js
  12. 10 0
      backend/inigovalentin/app/models/tag.model.js
  13. 0 10
      backend/inigovalentin/app/models/text.model.js.save
  14. 0 3
      backend/inigovalentin/app/routes/project.routes.js
  15. 173 42
      backend/inigovalentin/app/services/locale.service.js
  16. 4 2
      frontend/inigovalentin/angular.json
  17. BIN
      frontend/inigovalentin/public/favicon.ico
  18. 98 0
      frontend/inigovalentin/public/img/license/CC-0.svg
  19. 92 0
      frontend/inigovalentin/public/img/license/CC-BY-NC-ND.svg
  20. 84 0
      frontend/inigovalentin/public/img/license/CC-BY-NC-SA.svg
  21. 81 0
      frontend/inigovalentin/public/img/license/CC-BY-NC.svg
  22. 84 0
      frontend/inigovalentin/public/img/license/CC-BY-ND.svg
  23. 107 0
      frontend/inigovalentin/public/img/license/CC-BY.svg
  24. 5 0
      frontend/inigovalentin/public/img/license/GPLV3.svg
  25. 488 0
      frontend/inigovalentin/public/img/social/app_store.svg
  26. 17 0
      frontend/inigovalentin/public/img/social/email.svg
  27. 54 0
      frontend/inigovalentin/public/img/social/facebook.svg
  28. 3 0
      frontend/inigovalentin/public/img/social/github.svg
  29. 21 0
      frontend/inigovalentin/public/img/social/google_play.svg
  30. 209 0
      frontend/inigovalentin/public/img/social/google_plus.svg
  31. 37 0
      frontend/inigovalentin/public/img/social/linkedin.svg
  32. 17 0
      frontend/inigovalentin/public/img/social/telegram.svg
  33. 21 0
      frontend/inigovalentin/public/img/social/twitter.svg
  34. 46 0
      frontend/inigovalentin/public/img/social/web.svg
  35. BIN
      frontend/inigovalentin/public/img/tag/android.png
  36. BIN
      frontend/inigovalentin/public/img/tag/angular.png
  37. BIN
      frontend/inigovalentin/public/img/tag/c.png
  38. BIN
      frontend/inigovalentin/public/img/tag/cpp.png
  39. BIN
      frontend/inigovalentin/public/img/tag/css.png
  40. BIN
      frontend/inigovalentin/public/img/tag/html.png
  41. BIN
      frontend/inigovalentin/public/img/tag/ios.png
  42. BIN
      frontend/inigovalentin/public/img/tag/java.png
  43. BIN
      frontend/inigovalentin/public/img/tag/js.png
  44. BIN
      frontend/inigovalentin/public/img/tag/lua.png
  45. BIN
      frontend/inigovalentin/public/img/tag/mariadb.png
  46. BIN
      frontend/inigovalentin/public/img/tag/nodejs.png
  47. BIN
      frontend/inigovalentin/public/img/tag/ogre.png
  48. BIN
      frontend/inigovalentin/public/img/tag/php.png
  49. BIN
      frontend/inigovalentin/public/img/tag/python.png
  50. BIN
      frontend/inigovalentin/public/img/tag/sqlite.png
  51. BIN
      frontend/inigovalentin/public/img/tag/swift.png
  52. BIN
      frontend/inigovalentin/public/img/tag/tag.xcf
  53. BIN
      frontend/inigovalentin/public/img/tag/web.png
  54. 34 0
      frontend/inigovalentin/public/js/help.js
  55. 12 0
      frontend/inigovalentin/public/js/profile.js
  56. 72 0
      frontend/inigovalentin/public/js/project.js
  57. 11 0
      frontend/inigovalentin/public/js/ui.js
  58. 1 0
      frontend/inigovalentin/src/app/app.component.ts
  59. 3 3
      frontend/inigovalentin/src/app/app.routes.ts
  60. 74 0
      frontend/inigovalentin/src/app/components/navbar/navbar.component.css
  61. 10 7
      frontend/inigovalentin/src/app/components/navbar/navbar.component.html
  62. 73 0
      frontend/inigovalentin/src/app/components/project-preview/project-preview.component.css
  63. 21 0
      frontend/inigovalentin/src/app/components/project-preview/project-preview.component.html
  64. 23 0
      frontend/inigovalentin/src/app/components/project-preview/project-preview.component.spec.ts
  65. 19 0
      frontend/inigovalentin/src/app/components/project-preview/project-preview.component.ts
  66. BIN
      frontend/inigovalentin/src/app/components/project/.project.component.css.kate-swp
  67. 340 0
      frontend/inigovalentin/src/app/components/project/project.component.css
  68. 0 1
      frontend/inigovalentin/src/app/components/project/project.component.html
  69. 7 0
      frontend/inigovalentin/src/app/models/license.ts
  70. 6 1
      frontend/inigovalentin/src/app/models/project.ts
  71. 339 0
      frontend/inigovalentin/src/app/pages/project/project.component.css
  72. 91 0
      frontend/inigovalentin/src/app/pages/project/project.component.html
  73. 4 4
      frontend/inigovalentin/src/app/pages/project/project.component.spec.ts
  74. 109 0
      frontend/inigovalentin/src/app/pages/project/project.component.ts
  75. 1 21
      frontend/inigovalentin/src/app/pages/projects/projects.component.html
  76. 15 53
      frontend/inigovalentin/src/app/pages/projects/projects.component.ts
  77. 0 0
      frontend/inigovalentin/src/app/pages/show-project/show-project.component.css
  78. 0 16
      frontend/inigovalentin/src/app/pages/show-project/show-project.component.html
  79. 0 31
      frontend/inigovalentin/src/app/pages/show-project/show-project.component.ts
  80. 5 24
      frontend/inigovalentin/src/app/services/projects.service.ts
  81. 720 3
      frontend/inigovalentin/src/styles.css
  82. 1 0
      frontend/inigovalentin/tsconfig.json

+ 60 - 88
backend/inigovalentin/app/controllers/license.controller.js

@@ -1,17 +1,26 @@
+/**
+ * @file Provides the model and operation for licenses.
+ * @author Inigo Valentin
+ * @since 4.0.0
+ */
+
 const db = require("../models");
 const License = db.licenses;
 const Op = db.Sequelize.Op;
 
 const LocaleService = require('../services/locale.service.js');
-const localeService = new LocaleService();
+const localeService = new LocaleService(db);
 
-// Create and Save a new License
+/**
+ * Create and Save a new license.
+ * 
+ * @param req The received request by the server.
+ * @param res The request to be sent by the server.
+ */
 exports.create = (req, res) => {
     // Validate request
     if (!req.body.title) {
-        res.status(400).send({
-            message: "Content can not be empty!"
-        });
+        res.status(400).send({message: "Content can not be empty!"});
         return;
     }
     
@@ -24,124 +33,87 @@ exports.create = (req, res) => {
     
     // Save License in the database
     License.create(License)
-    .then(data => {
-        res.send(data);
-    })
-    .catch(err => {
-        res.status(500).send({
-            message:
-            err.message || "Some error occurred while creating the License."
-        });
-    });
+    .then(data => {res.send(data);})
+    .catch(err => {res.status(500).send({message: err.message || "Some error occurred while creating the License."});});
 };
 
-// Retrieve all Licenses from the database.
+/**
+ * Retrieve all licenses from the database.
+ * 
+ * @param req The received request by the server.
+ * @param res The request to be sent by the server.
+ */
 exports.findAll = (req, res) => {
-    //const title = req.query.title;
-    
-    //License.findAll({ where: condition })
-    //License.findAll({include: ["titles"]})
-    License.findAll({where: {visible: true,}})
+    License.findAll()
     .then(async data => {
-        data = await localeService.localizeLicenses(data, db);
+        data = await localeService.localizeLicenses(data, req);
         res.send(data);
     })
     .catch(err => {res.status(500).send({message: err.message || "Some error occurred while retrieving Licenses."});});
 };
 
-// Find a single License with an id
+/**
+ * Find a single license by it's ID.
+ * 
+ * @param req The received request by the server.
+ * @param res The request to be sent by the server.
+ */
 exports.findOne = (req, res) => {
     const id = req.params.id;
     License.findByPk(id)
     .then(async data => {
         if (data) {
-            data = await localeService.localizeLicense(data, db);
+            data = await localeService.localizeLicense(data, req);
             res.send(data);
         }
-        else {
-            res.status(404).send({message: `Cannot find License with id=${id}.`});
-        }
+        else res.status(404).send({message: `Cannot find License with id=${id}.`});}
     })
     .catch(err => {res.status(500).send({message: "Error retrieving License with id=" + id});});
 };
 
-// Update a License by the id in the request
+/**
+ * Update a single license by it's ID.
+ * 
+ * @param req The received request by the server.
+ * @param res The request to be sent by the server.
+ */
 exports.update = (req, res) => {
     const id = req.params.id;
-    
-    License.update(req.body, {
-        where: { id: id }
-    })
+    License.update(req.body, {where: {id: id}})
     .then(num => {
-        if (num == 1) {
-            res.send({
-                message: "License was updated successfully."
-            });
-        } else {
-            res.send({
-                message: `Cannot update License with id=${id}. Maybe License was not found or req.body is empty!`
-            });
-        }
+        if (num == 1) res.send({message: "License was updated successfully."});
+        else res.send({message: `Cannot update License with id=${id}. Maybe License was not found or req.body is empty!`});
     })
-    .catch(err => {
-        res.status(500).send({
-            message: "Error updating License with id=" + id
-        });
-    });
+    .catch(err => {res.status(500).send({message: "Error updating License with id=" + id});});
 };
 
-// Delete a License with the specified id in the request
+/**
+ * Delete a single license by it's ID.
+ * 
+ * @param req The received request by the server.
+ * @param res The request to be sent by the server.
+ */
 exports.delete = (req, res) => {
     const id = req.params.id;
-    
-    License.destroy({
-        where: { id: id }
-    })
+    License.destroy({where: {id: id}})
     .then(num => {
-        if (num == 1) {
-            res.send({
-                message: "License was deleted successfully!"
-            });
-        } else {
-            res.send({
-                message: `Cannot delete License with id=${id}. Maybe License was not found!`
-            });
-        }
+        if (num == 1) res.send({message: "License was deleted successfully!"});
+        else res.send({message: `Cannot delete License with id=${id}. Maybe License was not found!`});
     })
-    .catch(err => {
-        res.status(500).send({
-            message: "Could not delete License with id=" + id
-        });
-    });
+    .catch(err => {res.status(500).send({message: "Could not delete License with id=" + id});});
 };
 
-// Delete all Licenses from the database.
+/**
+ * Delete all licenses.
+ * 
+ * @param req The received request by the server.
+ * @param res The request to be sent by the server.
+ */
 exports.deleteAll = (req, res) => {
     License.destroy({
         where: {},
         truncate: false
     })
-    .then(nums => {
-        res.send({ message: `${nums} Licenses were deleted successfully!` });
-    })
-    .catch(err => {
-        res.status(500).send({
-            message:
-            err.message || "Some error occurred while removing all Licenses."
-        });
-    });
-};
-
-// Find all published Licenses
-exports.findAllPublished = (req, res) => {
-    License.findAll({ where: { published: true } })
-    .then(data => {
-        res.send(data);
-    })
-    .catch(err => {
-        res.status(500).send({
-            message:
-            err.message || "Some error occurred while retrieving Licenses."
-        });
-    });
+    .then(nums => {res.send({ message: `${nums} Licenses were deleted successfully!` });})
+    .catch(err => {res.status(500).send({message: err.message || "Some error occurred while removing all Licenses."});});
 };

+ 120 - 0
backend/inigovalentin/app/controllers/project-type.controller.js

@@ -0,0 +1,120 @@
+/**
+ * @file Provides the model and operation for project types.
+ * @author Inigo Valentin
+ * @since 4.0.0
+ */
+
+const db = require("../models");
+const ProjectType = db.project types;
+const Op = db.Sequelize.Op;
+
+const LocaleService = require('../services/locale.service.js');
+const localeService = new LocaleService(db);
+
+/**
+ * Create and Save a new project type.
+ * 
+ * @param req The received request by the server.
+ * @param res The request to be sent by the server.
+ */
+exports.create = (req, res) => {
+    // Validate request
+    if (!req.body.title) {
+        res.status(400).send({message: "Content can not be empty!"});
+        return;
+    }
+    
+    // Create a ProjectType
+    const ProjectType = {
+        title: req.body.title,
+        description: req.body.description,
+        published: req.body.published ? req.body.published : false
+    };
+    
+    // Save ProjectType in the database
+    ProjectType.create(ProjectType)
+    .then(data => {res.send(data);})
+    .catch(err => {res.status(500).send({message: err.message || "Some error occurred while creating the ProjectType."});});
+};
+
+/**
+ * Retrieve all project types from the database.
+ * 
+ * @param req The received request by the server.
+ * @param res The request to be sent by the server.
+ */
+exports.findAll = (req, res) => {
+    ProjectType.findAll()
+    .then(async data => {
+        data = await localeService.localizeProjectTypes(data, req);
+        res.send(data);
+    })
+    .catch(err => {res.status(500).send({message: err.message || "Some error occurred while retrieving ProjectTypes."});});
+};
+
+/**
+ * Find a single project type by it's ID.
+ * 
+ * @param req The received request by the server.
+ * @param res The request to be sent by the server.
+ */
+exports.findOne = (req, res) => {
+    const id = req.params.id;
+    ProjectType.findByPk(id)
+    .then(async data => {
+        if (data) {
+            data = await localeService.localizeProjectType(data, req);
+            res.send(data);
+        }
+        else res.status(404).send({message: `Cannot find ProjectType with id=${id}.`});}
+})
+    .catch(err => {res.status(500).send({message: "Error retrieving ProjectType with id=" + id});});
+    };
+    
+    /**
+     * Update a single project type by it's ID.
+     * 
+     * @param req The received request by the server.
+     * @param res The request to be sent by the server.
+     */
+    exports.update = (req, res) => {
+        const id = req.params.id;
+        ProjectType.update(req.body, {where: {id: id}})
+        .then(num => {
+            if (num == 1) res.send({message: "ProjectType was updated successfully."});
+            else res.send({message: `Cannot update ProjectType with id=${id}. Maybe ProjectType was not found or req.body is empty!`});
+        })
+        .catch(err => {res.status(500).send({message: "Error updating ProjectType with id=" + id});});
+    };
+    
+    /**
+     * Delete a single project type by it's ID.
+     * 
+     * @param req The received request by the server.
+     * @param res The request to be sent by the server.
+     */
+    exports.delete = (req, res) => {
+        const id = req.params.id;
+        ProjectType.destroy({where: {id: id}})
+        .then(num => {
+            if (num == 1) res.send({message: "ProjectType was deleted successfully!"});
+            else res.send({message: `Cannot delete ProjectType with id=${id}. Maybe ProjectType was not found!`});
+        })
+        .catch(err => {res.status(500).send({message: "Could not delete ProjectType with id=" + id});});
+    };
+    
+    /**
+     * Delete all project types.
+     * 
+     * @param req The received request by the server.
+     * @param res The request to be sent by the server.
+     */
+    exports.deleteAll = (req, res) => {
+        ProjectType.destroy({
+            where: {},
+            truncate: false
+        })
+        .then(nums => {res.send({ message: `${nums} ProjectTypes were deleted successfully!` });})
+        .catch(err => {res.status(500).send({message: err.message || "Some error occurred while removing all ProjectTypes."});});
+    };
+    

+ 63 - 88
backend/inigovalentin/app/controllers/project.controller.js

@@ -1,17 +1,26 @@
+/**
+ * @file Provides the model and operation for projects.
+ * @author Inigo Valentin
+ * @since 4.0.0
+ */
+
 const db = require("../models");
 const Project = db.projects;
 const Op = db.Sequelize.Op;
 
 const LocaleService = require('../services/locale.service.js');
-const localeService = new LocaleService();
+const localeService = new LocaleService(db);
 
-// Create and Save a new Project
+/**
+ * Create and Save a new project.
+ * 
+ * @param req The received request by the server.
+ * @param res The request to be sent by the server.
+ */
 exports.create = (req, res) => {
     // Validate request
     if (!req.body.title) {
-        res.status(400).send({
-            message: "Content can not be empty!"
-        });
+        res.status(400).send({ message: "Content can not be empty!" });
         return;
     }
     
@@ -24,49 +33,50 @@ exports.create = (req, res) => {
     
     // Save Project in the database
     Project.create(Project)
-    .then(data => {
-        res.send(data);
-    })
-    .catch(err => {
-        res.status(500).send({
-            message:
-            err.message || "Some error occurred while creating the Project."
-        });
-    });
+    .then(data => {res.send(data);})
+    .catch(err => { res.status(500).send({ message: err.message || "Some error occurred while creating the Project." }); });
 };
 
-// Retrieve all Projects from the database.
+/**
+ * Retrieve all projects from the database.
+ * 
+ * @param req The received request by the server.
+ * @param res The request to be sent by the server.
+ */
 exports.findAll = (req, res) => {
-    //const title = req.query.title;
-    
-    //Project.findAll({ where: condition })
-    //Project.findAll({include: ["titles"]})
-    //Project.findAll({where: {visible: true,},include: db.License})
-    Project.findAll({where: {visible: true,}, include: ["license"], attributes: {exclude: ['licenseId']}})
+    Project.findAll({where: {visible: true,}, include: ["license", "type", "tags", {model: ProjectUrl, include: "type"}], attributes: { exclude: ['licenseId', 'projectTypeId'] }, order: [['idx', 'ASC'], ['id', 'DESC']] })
     .then(async data => {
-        data = await localeService.localizeProjects(data, db);
+        data = await localeService.localizeProjects(data, req);
         res.send(data);
     })
-    .catch(err => {res.status(500).send({message: err.message || "Some error occurred while retrieving Projects."});});
+    .catch(err => { res.status(500).send({message: err.message || "Some error occurred while retrieving Projects."}); });
 };
 
-// Find a single Project with an id
+/**
+ * Find a single License by it's ID.
+ * 
+ * @param req The received request by the server.
+ * @param res The request to be sent by the server.
+ */
 exports.findOne = (req, res) => {
     const id = req.params.id;
-    Project.findByPk(id, {where: {visible: true,}, include: ["license"], attributes: {exclude: ['licenseId']}})
+    Project.findOne({ where: { visible: true, [Op.or]: {permalink: id, id: id }}, include: ["license", "type", "tags", {model: ProjectUrl, include: "type"}, "project-images"], attributes: { exclude: ['licenseId', 'projectTypeId'] } })
+    //Project.findByPk(id, { where: { visible: true }, include: ["license"], attributes: {exclude: ['licenseId'] } })
     .then(async data => {
         if (data) {
-            data = await localeService.localizeProject(data, db);
+            data = await localeService.localizeProject(data, req);
             res.send(data);
         }
-        else {
-            res.status(404).send({message: `Cannot find Project with id=${id}.`});
-        }
+        else res.status(404).send({ message: `Cannot find Project with permalink or id=${id}.` });
     })
-    .catch(err => {res.status(500).send({message: "Error retrieving Project with id=" + id});});
+    .catch(err => { res.status(500).send({ message: "Error retrieving Project with permalink or id=" + id + ": " + err}); });
 };
-
-// Update a Project by the id in the request
+/**
+ * Update a single project by it's ID.
+ * 
+ * @param req The received request by the server.
+ * @param res The request to be sent by the server.
+ */
 exports.update = (req, res) => {
     const id = req.params.id;
     
@@ -74,75 +84,40 @@ exports.update = (req, res) => {
         where: { id: id }
     })
     .then(num => {
-        if (num == 1) {
-            res.send({
-                message: "Project was updated successfully."
-            });
-        } else {
-            res.send({
-                message: `Cannot update Project with id=${id}. Maybe Project was not found or req.body is empty!`
-            });
-        }
+        if (num == 1) res.send({ message: "Project was updated successfully." });
+        else res.send({ message: `Cannot update Project with id=${id}. Maybe Project was not found or req.body is empty!` });
     })
-    .catch(err => {
-        res.status(500).send({
-            message: "Error updating Project with id=" + id
-        });
-    });
+    .catch(err => { res.status(500).send({ message: "Error updating Project with id=" + id }); });
 };
 
-// Delete a Project with the specified id in the request
+/**
+ * Delete a single project by it's ID.
+ * 
+ * @param req The received request by the server.
+ * @param res The request to be sent by the server.
+ */
 exports.delete = (req, res) => {
     const id = req.params.id;
     
-    Project.destroy({
-        where: { id: id }
-    })
+    Project.destroy({ where: { id: id } })
     .then(num => {
-        if (num == 1) {
-            res.send({
-                message: "Project was deleted successfully!"
-            });
-        } else {
-            res.send({
-                message: `Cannot delete Project with id=${id}. Maybe Project was not found!`
-            });
-        }
+        if (num == 1) res.send({ message: "Project was deleted successfully!" });
+        else  res.send({ message: `Cannot delete Project with id=${id}. Maybe Project was not found!` });
     })
-    .catch(err => {
-        res.status(500).send({
-            message: "Could not delete Project with id=" + id
-        });
-    });
+    .catch(err => { res.status(500).send({ message: "Could not delete Project with id=" + id }); });
 };
 
-// Delete all Projects from the database.
+/**
+ * Delete all projects.
+ * 
+ * @param req The received request by the server.
+ * @param res The request to be sent by the server.
+ */
 exports.deleteAll = (req, res) => {
     Project.destroy({
         where: {},
         truncate: false
     })
-    .then(nums => {
-        res.send({ message: `${nums} Projects were deleted successfully!` });
-    })
-    .catch(err => {
-        res.status(500).send({
-            message:
-            err.message || "Some error occurred while removing all Projects."
-        });
-    });
-};
-
-// Find all published Projects
-exports.findAllPublished = (req, res) => {
-    Project.findAll({ where: { published: true } })
-    .then(data => {
-        res.send(data);
-    })
-    .catch(err => {
-        res.status(500).send({
-            message:
-            err.message || "Some error occurred while retrieving Projects."
-        });
-    });
+    .then(nums => { res.send({ message: `${nums} Projects were deleted successfully!` }); })
+    .catch(err => { res.status(500).send({ message: err.message || "Some error occurred while removing all Projects." }); });
 };

+ 24 - 4
backend/inigovalentin/app/models/index.js

@@ -14,16 +14,36 @@ const sequelize = new Sequelize(
 const db = {};
 db.Sequelize = Sequelize;
 db.sequelize = sequelize;
-// 
+
 db.langs = require("./lang.model.js")(sequelize, Sequelize);
 db.texts = require("./text.model.js")(sequelize, Sequelize);
 db.licenses = require("./license.model.js")(sequelize, Sequelize);
 db.projects = require("./project.model.js")(sequelize, Sequelize);
-//db.projects.hasMany(db.texts, {foreignkey: "title", as: "titles" });
-//db.projects.hasOne(db.licenses, {foreignkey: "license", as: "licens" });
-//db.licenses.belongsTo(db.projects, {foreignkey: "license"});
+db.projectTypes = require("./project-type.model.js")(sequelize, Sequelize);
+db.projectUrlTypes = require("./project-url-type.model.js")(sequelize, Sequelize);
+db.projectUrls = require("./project-url.model.js")(sequelize, Sequelize);
+db.projectImages = require("./project-image.model.js")(sequelize, Sequelize);
+db.tags = require("./tag.model.js")(sequelize, Sequelize);
+
 
 db.projects.belongsTo(db.licenses, {foreignKey: 'licenseId', as: "license"});
 db.licenses.hasMany(db.projects);
 
+db.projects.belongsTo(db.projectTypes, {foreignKey: 'projectTypeId', as: "type"});
+db.projectTypes.hasMany(db.projects);
+
+const ProjectTag = sequelize.define("project-tag",{},{ timestamps: false });
+db.projects.belongsToMany(db.tags, { through: ProjectTag });
+db.tags.belongsToMany(db.projects, { through: ProjectTag });
+
+db.projectUrls.belongsTo(db.projectUrlTypes, {foreignKey: 'projectUrlTypeId', as: "type"});
+db.projectUrlTypes.hasMany(db.projectUrls);
+
+db.projectUrls.belongsTo(db.projects, {foreignKey: 'projectId', as: "project"});
+db.projects.hasMany(db.projectUrls);
+
+
+db.projectImages.belongsTo(db.projects, {foreignKey: 'projectId', as: "project"});
+db.projects.hasMany(db.projectImages);
+
 module.exports = db;

+ 4 - 1
backend/inigovalentin/app/models/lang.model.js

@@ -2,8 +2,11 @@ module.exports = (sequelize, Sequelize) => {
     const Lang = sequelize.define("lang", {
       code: {allowNull: false, primaryKey: true, type: Sequelize.STRING(8)},
       name: {type: Sequelize.STRING(64)},
+      priority: {type: Sequelize.INTEGER},
       active: {type: Sequelize.BOOLEAN},
       default: {type: Sequelize.BOOLEAN},
-    });
+    },
+    {timestamps: false}
+    );
     return Lang;
 }

+ 1 - 1
backend/inigovalentin/app/models/license.model.js

@@ -6,7 +6,7 @@ module.exports = (sequelize, Sequelize) => {
       logo: {type: Sequelize.STRING(64)},
       icon: {type: Sequelize.STRING(64)},
     },
-    { timestamps: false }
+    {timestamps: false}
     );
     return License;
 }

+ 12 - 0
backend/inigovalentin/app/models/project-image.model.js

@@ -0,0 +1,12 @@
+module.exports = (sequelize, Sequelize) => {
+    const ProjectImage = sequelize.define("project-image", {
+        id: {allowNull: false, primaryKey: true, type: Sequelize.STRING(32)},
+        projectId: {allowNull: false, type: Sequelize.INTEGER, references: {model: "projects", key: "id"}},
+        idx: {type: Sequelize.SMALLINT}, // TODO REFERENCE
+        image: {type: Sequelize.STRING(128)},
+        alt: {type: Sequelize.STRING(64)},
+    },
+    {timestamps: false}
+    );
+    return ProjectImage;
+}

+ 11 - 0
backend/inigovalentin/app/models/project-type.model.js

@@ -0,0 +1,11 @@
+module.exports = (sequelize, Sequelize) => {
+    ProjectType = sequelize.define("project-type", {
+        id: {allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER},
+        title: {allowNull: false, type: Sequelize.STRING(64), references: {model: "texts", key: "id"}},
+        summary: {type: Sequelize.STRING(512), references: {model: "texts", key: "id"}},
+        icon: {type: Sequelize.STRING(64)},
+    },
+    {timestamps: false}
+    );
+    return ProjectType;
+}

+ 11 - 0
backend/inigovalentin/app/models/project-url-type.model.js

@@ -0,0 +1,11 @@
+module.exports = (sequelize, Sequelize) => {
+    ProjectUrlType = sequelize.define("project-url-type", {
+        id: {allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER},
+        title: {allowNull: false, type: Sequelize.STRING(64), references: {model: "texts", key: "id"}},
+        summary: {type: Sequelize.STRING(512), references: {model: "texts", key: "id"}},
+        logo: {type: Sequelize.STRING(64)},
+    },
+    {timestamps: false}
+    );
+    return ProjectUrlType;
+}

+ 11 - 0
backend/inigovalentin/app/models/project-url.model.js

@@ -0,0 +1,11 @@
+module.exports = (sequelize, Sequelize) => {
+    ProjectUrl = sequelize.define("project-url", {
+        id: {allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER},
+        projectId: {allowNull: false, type: Sequelize.INTEGER, references: {model: "projects", key: "id"}},
+        projectUrlTypeId: {allowNull: false, type: Sequelize.INTEGER, references: {model: "project-url-types", key: "id"}},
+        url: {type: Sequelize.STRING(512)},
+    },
+    {timestamps: false}
+    );
+    return ProjectUrl;
+}

+ 3 - 4
backend/inigovalentin/app/models/project.model.js

@@ -1,17 +1,16 @@
-
 module.exports = (sequelize, Sequelize) => {
     Project = sequelize.define("project", {
       id: {allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER},
       permalink: {type: Sequelize.STRING(64)},
       user: {type: Sequelize.INTEGER}, // TODO REFERENCE
-      idx: {type: Sequelize.SMALLINT}, // TODO REFERENCE
-      type: {type: Sequelize.STRING(8)},
+      idx: {type: Sequelize.SMALLINT},
+      projectTypeId: {type: Sequelize.INTEGER, references: {model: "project-types", key: "id"}},
       title: {allowNull: false, type: Sequelize.STRING(64), references: {model: "texts", key: "id"}},
       logo: {type: Sequelize.STRING(64)},
       header: {allowNull: false, type: Sequelize.STRING(64)},//, references: {model: "texts", key: "id"}},
       text: {type: Sequelize.STRING(64)},//, references: {model: "texts", key: "id"}},
       comment: {type: Sequelize.STRING(64)},//, references: {model: "texts", key: "id"}},
-      licenseId: {type: Sequelize.STRING(32), references: {model: "licenses", key: "id"}}, // TODO REFERENCE
+      licenseId: {type: Sequelize.STRING(32), references: {model: "licenses", key: "id"}},
       visible: {type: Sequelize.BOOLEAN},
     });
     return Project;

+ 10 - 0
backend/inigovalentin/app/models/tag.model.js

@@ -0,0 +1,10 @@
+module.exports = (sequelize, Sequelize) => {
+    const Tag = sequelize.define("tag", {
+        id: {allowNull: false, primaryKey: true, type: Sequelize.STRING(32)},
+        tag: {allowNull: false, type: Sequelize.STRING(64)},
+        icon: {type: Sequelize.STRING(64)},
+    },
+    {timestamps: false}
+    );
+    return Tag;
+}

+ 0 - 10
backend/inigovalentin/app/models/text.model.js.save

@@ -1,10 +0,0 @@
-module.exports = (sequelize, Sequelize) => {
-    const Text = sequelize.define("text", {
-      id: {type: Sequelize.VARCHAR(64), allowNull: false}
-      code: {type: Sequelize.STRING(8), allowNull: false},
-      section: {type: Sequelize.STRING(64)},
-      text: {type: Sequelize.STRING(512)},
-      file: {type: Sequelize.STRING(256)},
-    });
-    return Text;
-}

+ 0 - 3
backend/inigovalentin/app/routes/project.routes.js

@@ -9,9 +9,6 @@ module.exports = app => {
     // Retrieve all Projects
     router.get("/", projects.findAll);
     
-    // Retrieve all published Projects
-    router.get("/published", projects.findAllPublished);
-    
     // Retrieve a single Project with id
     router.get("/:id", projects.findOne);
     

+ 173 - 42
backend/inigovalentin/app/services/locale.service.js

@@ -1,59 +1,190 @@
+/**
+ * @file Provides a service to handle languages and localizations.
+ * @author Inigo Valentin
+ * @since 4.0.0
+ */
+
+/**
+ * Handles localization of elements.
+ */
 class LocaleService {
-    
-    constructor(){}
-    
-    getLanguage(db){
-        // TODO: Implement;
-        return "en"
+
+    /**
+     * Database connection.
+     */
+    #db;
+
+    /**
+     * List of available languages.
+     *
+     * The one at index 0 is the default language.
+     */
+    #availableLanguages = Array();
+
+    /**
+     * Constructor.
+     *
+     * @param db Database connection.
+     * @constructor
+     */
+    constructor(db){
+        this.#db = db;
+        this.#init();
     }
-    
-    async localizeProject(data, db){
-        var result = await db.sequelize.query('SELECT text FROM texts WHERE lang = ? AND id = ?', { replacements: ['en', data.dataValues.title], type: db.sequelize.QueryTypes.SELECT })
+
+    /**
+     * Initializes the service.
+     *
+     * Called automatically from the constructor.
+     */
+    async #init(){
+        var result = await this.#db.sequelize.query('SELECT code FROM langs WHERE active = 1 ORDER by PRIORITY ASC ', { type: this.#db.sequelize.QueryTypes.SELECT })
+        for (const r of result) this.#availableLanguages.push(r.code.toLowerCase().substring(0, 2));
+    }
+
+    /**
+     * Selects the language to use.
+     *
+     * If the language is not in the request, using the GET parameter "lang", the default one set
+     * in the database will be used.
+     *
+     * @param req The request received by the server.
+     * @return Two letter language code to use.
+     */
+    selectLanguage(req){
+        if (!req.query.lang) return this.#availableLanguages[0];
+        var requestedLang = req.query.lang.toLowerCase().substring(0, 2)
+        if (this.#availableLanguages.indexOf(requestedLang) != -1) return requestedLang;
+        return this.#availableLanguages[0];
+    }
+
+    /**
+     * Decodes a text from the database.
+     * 
+     * @param result Result of a query to the table 'texts'. Must have at least the columns 'text' and 'file'.
+     * @return The text.
+     */
+    async #decodeText(key, lang){
+        var result = await this.#db.sequelize.query('SELECT text, file FROM texts WHERE lang = ? AND id = ?', { replacements: [lang, key], type: this.#db.sequelize.QueryTypes.SELECT })
         for (const r of result) {
-            data.dataValues.title = r.text;
-            break;
+            if (r.text) return "" + r.text;
+            if (r.file) return "" + r.file; // TODO: Return file CONTENTS
         }
-        result = await db.sequelize.query('SELECT text FROM texts WHERE lang = ? AND id = ?', { replacements: ['en', data.dataValues.header], type: db.sequelize.QueryTypes.SELECT })
-        for (const r of result) {
-            data.dataValues.header = r.text;
-            break;
+        return "";
+    }
+
+    /**
+     * Localizes a project.
+     *
+     * Localizes all localizable items in a project, using the language provided in the request or
+     * the default one.
+     *
+     * @param data The project object to localize.
+     * @param req The request received by the server.
+     * @return The project object, with all of the fields localized.
+     */
+    async localizeProject(data, req){
+        var lang = this.selectLanguage(req);
+        data.dataValues.title = await this.#decodeText(data.dataValues.title, lang);
+        data.dataValues.header = await this.#decodeText(data.dataValues.header, lang);
+        data.dataValues.text = await this.#decodeText(data.dataValues.text, lang);
+        data.dataValues.comment = await this.#decodeText(data.dataValues.comment, lang);
+        if (data.dataValues.type){
+            data.dataValues.type.title = await this.#decodeText(data.dataValues.type.title, lang);
+            data.dataValues.type.summary = await this.#decodeText(data.dataValues.type.summary, lang);
         }
-        result = await db.sequelize.query('SELECT text FROM texts WHERE lang = ? AND id = ?', { replacements: ['en', data.dataValues.text], type: db.sequelize.QueryTypes.SELECT })
-        for (const r of result) {
-            data.dataValues.text = r.text;
-            break;
+        if (data.dataValues.license){
+            data.dataValues.license.summary = await this.#decodeText(data.dataValues.license.summary, lang);
+            data.dataValues.license.legal = await this.#decodeText(data.dataValues.license.legal, lang);
         }
-        result = await db.sequelize.query('SELECT text FROM texts WHERE lang = ? AND id = ?', { replacements: ['en', data.dataValues.comment], type: db.sequelize.QueryTypes.SELECT })
-        for (const r of result) {
-            data.dataValues.comment = r.text;
-            break;
+        for (var i = 0; i < data.dataValues.tags.length; i ++)
+            data.dataValues.tags[i].tag = await this.#decodeText(data.dataValues.tags[i].tag, lang);
+        for (var i = 0; i < data.dataValues["project-urls"].length; i ++){
+            data.dataValues["project-urls"][i].dataValues.type.dataValues.title = await this.#decodeText(data.dataValues["project-urls"][i].dataValues.type.dataValues.title, lang);
+            data.dataValues["project-urls"][i].dataValues.type.dataValues.summary = await this.#decodeText(data.dataValues["project-urls"][i].dataValues.type.dataValues.summary, lang);
         }
+        for (var i = 0; i < data.dataValues["project-images"].length; i ++)
+            data.dataValues["project-images"][i].dataValues.alt = await this.#decodeText(data.dataValues["project-images"][i].dataValues.alt, lang);
+        //console.log(data.dataValues.projectUrls)
         return data;
     }
-    
-    async localizeProjects(data, db){
-        for (var d of data)
-            d = await this.localizeProject(d, db);
+
+    /**
+     * Localizes a list of projects.
+     *
+     * Localizes all localizable items in all projects, using the language provided in the request or
+     * the default one.
+     *
+     * @param data The project object list to localize.
+     * @param req The request received by the server.
+     * @return The project list, with all of their fields localized.
+     */
+    async localizeProjects(data, req){
+        for (var d of data) d = await this.localizeProject(d, req);
         return data;
     }
-    
-    async localizeLicense(data, db){
-        var result = await db.sequelize.query('SELECT text FROM texts WHERE lang = ? AND id = ?', { replacements: ['en', data.dataValues.summary], type: db.sequelize.QueryTypes.SELECT })
-        for (const r of result) {
-            data.dataValues.summary = r.text;
-            break;
-        }
-        result = await db.sequelize.query('SELECT text FROM texts WHERE lang = ? AND id = ?', { replacements: ['en', data.dataValues.legal], type: db.sequelize.QueryTypes.SELECT })
-        for (const r of result) {
-            data.dataValues.legal = r.text;
-            break;
-        }
+
+    /**
+     * Localizes a license.
+     *
+     * Localizes all localizable items in a license, using the language provided in the request or
+     * the default one.
+     *
+     * @param data The license object to localize.
+     * @param req The request received by the server.
+     * @return The license object, with all of the fields localized.
+     */
+    async localizeLicense(data, req){
+        var lang = this.selectLanguage(req);
+        data.dataValues.summary = await this.#decodeText(data.dataValues.summary, lang);
+        data.dataValues.legal = await this.#decodeText(data.dataValues.legal, lang);
+        return data;
+    }
+
+    /**
+     * Localizes a list of projects licenses.
+     *
+     * Localizes all localizable items in all licenses, using the language provided in the request or
+     * the default one.
+     *
+     * @param data The license object list to localize.
+     * @param req The request received by the server.
+     * @return The license list, with all of their fields localized.
+     */
+    async localizeLicenses(data, req){
+        for (var d of data) d = await this.localizeLicense(d, req);
+        return data;
+    }
+
+    /**
+     * Localizes a project type.
+     *
+     * Localizes all localizable items in a project type, using the language provided in the request or
+     * the default one.
+     *
+     * @param data The project type object to localize.
+     * @param req The request received by the server.
+     * @return The project type object, with all of the fields localized.
+     */
+    async localizeProjectType(data, req){
+        var lang = this.selectLanguage(req);
+        data.dataValues.title = await this.#decodeText(data.dataValues.title, lang);
+        data.dataValues.summary = await this.#decodeText(data.dataValues.summary, lang);
         return data;
     }
     
-    async localizeLicenses(data, db){
-        for (var d of data)
-            d = await this.localizeLicense(d, db);
+    /**
+     * Localizes a list of projects project types.
+     *
+     * Localizes all localizable items in all project types, using the language provided in the request or
+     * the default one.
+     *
+     * @param data The project type object list to localize.
+     * @param req The request received by the server.
+     * @return The project type list, with all of their fields localized.
+     */
+    async localizeProjectTypes(data, req){
+        for (var d of data) d = await this.localizeProjectType(d, req);
         return data;
     }
 

+ 4 - 2
frontend/inigovalentin/angular.json

@@ -23,13 +23,15 @@
             "assets": [
               {
                 "glob": "**/*",
-                "input": "public"
+                "input": "public/"
               }
             ],
             "styles": [
               "src/styles.css"
             ],
-            "scripts": []
+            "scripts": [
+              "public/js/project.js"
+            ]
           },
           "configurations": {
             "production": {

BIN
frontend/inigovalentin/public/favicon.ico


+ 98 - 0
frontend/inigovalentin/public/img/license/CC-0.svg

@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="88px" height="31px" viewBox="-0.5 -0.101 88 31" enable-background="new -0.5 -0.101 88 31" xml:space="preserve">
+<g>
+	<path fill="#FFFFFF" d="M1.803,0.482L84.93,0.631c1.161,0,2.198-0.173,2.198,2.333L87.025,30.52h-87.32V2.862
+		C-0.295,1.626-0.177,0.482,1.803,0.482z"/>
+	<g>
+		<ellipse fill="#FFFFFF" cx="13.887" cy="15.502" rx="11.101" ry="11.174"/>
+	</g>
+	<path d="M23.271,4.061c3.484,2.592,5.754,6.744,5.755,11.44c-0.001,4.272-1.88,8.095-4.842,10.705h62.853V4.061H23.271z"/>
+	<g>
+		<path fill="#FFFFFF" d="M35.739,7.559c0.392,0,0.728,0.059,1.002,0.173c0.276,0.116,0.5,0.268,0.674,0.456
+			c0.173,0.189,0.299,0.405,0.379,0.647c0.079,0.242,0.118,0.494,0.118,0.753c0,0.253-0.039,0.503-0.118,0.749
+			c-0.08,0.244-0.206,0.462-0.379,0.65c-0.174,0.189-0.397,0.341-0.674,0.456c-0.274,0.114-0.61,0.173-1.002,0.173h-1.452v2.267
+			h-1.382V7.559H35.739z M35.36,10.535c0.158,0,0.312-0.012,0.457-0.035c0.147-0.023,0.276-0.069,0.388-0.137
+			c0.112-0.068,0.201-0.164,0.269-0.288s0.101-0.287,0.101-0.487c0-0.2-0.033-0.362-0.101-0.487
+			c-0.067-0.124-0.157-0.221-0.269-0.287c-0.111-0.068-0.24-0.114-0.388-0.138C35.671,8.652,35.518,8.64,35.36,8.64h-1.073v1.896
+			L35.36,10.535L35.36,10.535z"/>
+		<path fill="#FFFFFF" d="M43.751,13.4c-0.476,0.417-1.133,0.625-1.972,0.625c-0.851,0-1.509-0.207-1.976-0.62
+			c-0.466-0.412-0.699-1.052-0.699-1.913V7.559h1.381v3.934c0,0.171,0.016,0.338,0.045,0.505c0.029,0.165,0.091,0.311,0.185,0.439
+			c0.094,0.126,0.225,0.229,0.392,0.309c0.167,0.081,0.392,0.12,0.673,0.12c0.493,0,0.833-0.11,1.021-0.332
+			c0.188-0.222,0.282-0.568,0.282-1.04V7.559h1.382v3.934C44.464,12.348,44.227,12.983,43.751,13.4z"/>
+		<path fill="#FFFFFF" d="M49.07,7.559c0.3,0,0.572,0.027,0.818,0.081c0.244,0.054,0.457,0.14,0.633,0.261
+			c0.177,0.121,0.312,0.282,0.41,0.482c0.096,0.201,0.146,0.45,0.146,0.745c0,0.318-0.072,0.584-0.216,0.796
+			c-0.146,0.212-0.357,0.388-0.639,0.523c0.387,0.112,0.676,0.31,0.865,0.589c0.189,0.281,0.286,0.62,0.286,1.015
+			c0,0.319-0.062,0.595-0.187,0.828c-0.123,0.232-0.289,0.423-0.496,0.571c-0.209,0.148-0.445,0.257-0.713,0.327
+			c-0.269,0.07-0.541,0.105-0.822,0.105h-3.047V7.559H49.07z M48.895,10.119c0.246,0,0.448-0.059,0.607-0.178
+			c0.158-0.118,0.236-0.309,0.236-0.576c0-0.147-0.025-0.269-0.078-0.363c-0.053-0.093-0.123-0.168-0.211-0.221
+			c-0.09-0.053-0.189-0.091-0.305-0.109C49.029,8.65,48.912,8.64,48.789,8.64h-1.294v1.48L48.895,10.119L48.895,10.119
+			L48.895,10.119z M48.975,12.804c0.135,0,0.264-0.014,0.387-0.04c0.123-0.026,0.23-0.072,0.326-0.133
+			c0.092-0.062,0.168-0.147,0.226-0.254c0.056-0.104,0.083-0.241,0.083-0.406c0-0.324-0.092-0.557-0.271-0.695
+			c-0.182-0.138-0.424-0.208-0.723-0.208h-1.505v1.738h1.479v-0.002H48.975z"/>
+		<path fill="#FFFFFF" d="M54.143,7.559v5.156h3.062v1.168H52.76V7.559H54.143z"/>
+		<path fill="#FFFFFF" d="M59.748,7.559v6.324h-1.382V7.559H59.748z"/>
+		<path fill="#FFFFFF" d="M65.451,9.247c-0.082-0.132-0.186-0.249-0.309-0.349c-0.123-0.102-0.263-0.18-0.418-0.236
+			c-0.156-0.057-0.316-0.084-0.488-0.084c-0.312,0-0.574,0.062-0.793,0.183c-0.217,0.12-0.394,0.283-0.525,0.486
+			c-0.136,0.204-0.232,0.436-0.296,0.695c-0.062,0.259-0.093,0.528-0.093,0.806c0,0.267,0.031,0.524,0.093,0.776
+			c0.062,0.251,0.16,0.477,0.296,0.678c0.134,0.201,0.312,0.361,0.525,0.483c0.219,0.12,0.481,0.181,0.793,0.181
+			c0.424,0,0.752-0.13,0.99-0.389c0.236-0.26,0.383-0.602,0.437-1.028H67c-0.034,0.396-0.126,0.753-0.271,1.072
+			c-0.146,0.318-0.342,0.591-0.582,0.815c-0.238,0.225-0.521,0.396-0.845,0.513c-0.323,0.119-0.678,0.178-1.065,0.178
+			c-0.479,0-0.914-0.084-1.297-0.252c-0.385-0.169-0.709-0.398-0.973-0.695c-0.265-0.295-0.468-0.642-0.607-1.04
+			c-0.142-0.399-0.211-0.829-0.211-1.289c0-0.473,0.069-0.911,0.211-1.316c0.141-0.404,0.344-0.758,0.607-1.059
+			c0.264-0.302,0.588-0.536,0.973-0.708c0.384-0.172,0.815-0.258,1.297-0.258c0.348,0,0.676,0.051,0.981,0.15
+			c0.308,0.102,0.583,0.248,0.827,0.44c0.243,0.191,0.443,0.43,0.604,0.712c0.158,0.283,0.259,0.608,0.301,0.975h-1.34
+			C65.586,9.524,65.533,9.377,65.451,9.247z"/>
+		<path fill="#FFFFFF" d="M35.615,16.418c0.405,0,0.782,0.062,1.131,0.192c0.35,0.13,0.651,0.324,0.906,0.585
+			c0.255,0.26,0.455,0.586,0.599,0.975c0.144,0.391,0.216,0.849,0.216,1.371c0,0.463-0.059,0.888-0.176,1.277
+			c-0.118,0.391-0.295,0.727-0.532,1.012c-0.238,0.281-0.534,0.504-0.89,0.668c-0.354,0.16-0.772,0.242-1.254,0.242h-2.71v-6.322
+			H35.615z M35.519,21.572c0.199,0,0.393-0.031,0.581-0.098c0.188-0.062,0.354-0.173,0.502-0.323
+			c0.146-0.151,0.264-0.347,0.352-0.59c0.088-0.241,0.132-0.536,0.132-0.886c0-0.317-0.031-0.606-0.093-0.863
+			c-0.062-0.256-0.162-0.479-0.304-0.659c-0.141-0.183-0.326-0.323-0.559-0.421c-0.231-0.098-0.517-0.146-0.858-0.146h-0.984v3.986
+			H35.519z"/>
+		<path fill="#FFFFFF" d="M39.8,18.289c0.141-0.403,0.344-0.756,0.606-1.059c0.265-0.303,0.589-0.538,0.973-0.709
+			c0.385-0.171,0.816-0.257,1.298-0.257c0.487,0,0.921,0.086,1.303,0.257c0.381,0.171,0.704,0.406,0.969,0.709
+			c0.264,0.303,0.466,0.652,0.605,1.059c0.143,0.404,0.213,0.845,0.213,1.316c0,0.46-0.07,0.891-0.213,1.288
+			c-0.142,0.397-0.344,0.744-0.605,1.04c-0.266,0.295-0.588,0.525-0.969,0.695c-0.382,0.166-0.815,0.252-1.303,0.252
+			c-0.481,0-0.913-0.086-1.298-0.252c-0.384-0.17-0.708-0.4-0.973-0.695c-0.263-0.296-0.466-0.645-0.606-1.04
+			c-0.14-0.397-0.211-0.828-0.211-1.288C39.589,19.134,39.659,18.694,39.8,18.289z M41.062,20.379
+			c0.062,0.252,0.16,0.479,0.295,0.68c0.135,0.2,0.312,0.359,0.527,0.482c0.218,0.121,0.481,0.183,0.792,0.183
+			c0.312,0,0.576-0.062,0.792-0.183c0.218-0.121,0.394-0.281,0.529-0.482c0.134-0.2,0.231-0.428,0.295-0.68
+			c0.062-0.25,0.092-0.508,0.092-0.774c0-0.276-0.03-0.547-0.092-0.806c-0.062-0.262-0.161-0.492-0.295-0.696
+			c-0.136-0.201-0.312-0.365-0.529-0.485c-0.216-0.121-0.48-0.184-0.792-0.184c-0.311,0-0.574,0.062-0.792,0.184
+			c-0.216,0.12-0.393,0.284-0.527,0.485c-0.135,0.204-0.233,0.437-0.295,0.696c-0.062,0.259-0.093,0.527-0.093,0.806
+			C40.97,19.871,41.001,20.129,41.062,20.379z"/>
+		<path fill="#FFFFFF" d="M49.092,16.418l1.471,4.348h0.02l1.393-4.348h1.942v6.322h-1.294v-4.48h-0.02l-1.539,4.48H50l-1.54-4.437
+			h-0.019v4.437h-1.293v-6.322H49.092z"/>
+		<path fill="#FFFFFF" d="M58.764,16.418l2.35,6.322H59.68l-0.476-1.408h-2.351l-0.492,1.408h-1.391l2.377-6.322H58.764z
+			 M58.844,20.297l-0.793-2.322h-0.018l-0.817,2.322H58.844z"/>
+		<path fill="#FFFFFF" d="M63.547,16.418v6.322h-1.382v-6.322H63.547z"/>
+		<path fill="#FFFFFF" d="M66.604,16.418l2.623,4.242h0.018v-4.242h1.294v6.322h-1.384l-2.611-4.234h-0.02v4.234H65.23v-6.322
+			H66.604z"/>
+	</g>
+	<path d="M85.852,0H1.147C0.239,0-0.5,0.744-0.5,1.658v28.969C-0.5,30.834-0.333,31-0.128,31h87.256
+		c0.205,0,0.372-0.166,0.372-0.373V1.658C87.5,0.744,86.762,0,85.852,0z M1.147,0.75h84.705c0.498,0,0.902,0.406,0.902,0.908
+		c0,0,0,20.121,0,28.557H0.245v-8.426c0-8.435,0-20.131,0-20.131C0.245,1.157,0.65,0.75,1.147,0.75z"/>
+	<g>
+		<ellipse fill="#FFFFFF" cx="14.156" cy="15.661" rx="11.004" ry="11.076"/>
+		<path id="text2809_4_" d="M14.22,8.746c-3.862,0-4.834,3.669-4.834,6.779c0,3.111,0.971,6.779,4.834,6.779
+			c3.863,0,4.834-3.67,4.834-6.779C19.054,12.414,18.083,8.746,14.22,8.746z M14.22,11.301c0.157,0,0.3,0.024,0.435,0.06
+			c0.278,0.24,0.414,0.573,0.147,1.038l-2.572,4.76c-0.079-0.603-0.091-1.195-0.091-1.634C12.139,14.155,12.233,11.301,14.22,11.301
+			z M16.146,13.494c0.137,0.731,0.155,1.493,0.155,2.03c0,1.37-0.094,4.223-2.08,4.223c-0.156,0-0.301-0.017-0.435-0.049
+			c-0.025-0.01-0.049-0.019-0.074-0.025c-0.04-0.012-0.084-0.024-0.122-0.041c-0.442-0.188-0.721-0.531-0.319-1.139L16.146,13.494z"
+			/>
+		<path id="path2815_4_" d="M14.195,3.748c-3.245,0-5.98,1.137-8.21,3.422c-1.128,1.135-1.99,2.431-2.589,3.876
+			c-0.585,1.43-0.876,2.921-0.876,4.478c0,1.57,0.291,3.062,0.876,4.479s1.434,2.69,2.548,3.826
+			c1.128,1.121,2.395,1.985,3.802,2.588c1.421,0.59,2.903,0.884,4.449,0.884c1.547,0,3.05-0.304,4.499-0.907
+			c1.448-0.604,2.74-1.471,3.883-2.605c1.101-1.078,1.934-2.317,2.49-3.719c0.571-1.415,0.853-2.932,0.853-4.544
+			c0-1.598-0.281-3.112-0.852-4.528c-0.571-1.429-1.407-2.693-2.507-3.801C20.263,4.895,17.469,3.748,14.195,3.748z M14.244,5.867
+			c2.646,0,4.904,0.944,6.784,2.836c0.906,0.912,1.6,1.954,2.073,3.119c0.473,1.164,0.713,2.398,0.713,3.703
+			c0,2.707-0.92,4.952-2.744,6.746c-0.948,0.927-2.012,1.638-3.196,2.128c-1.17,0.489-2.375,0.732-3.63,0.732
+			c-1.268,0-2.481-0.239-3.638-0.717c-1.156-0.489-2.193-1.191-3.113-2.104c-0.92-0.925-1.629-1.97-2.13-3.135
+			c-0.487-1.178-0.738-2.391-0.738-3.653c0-1.276,0.251-2.497,0.738-3.662c0.501-1.178,1.211-2.235,2.13-3.175
+			C9.317,6.809,11.57,5.867,14.244,5.867z"/>
+	</g>
+</g>
+</svg>

File diff suppressed because it is too large
+ 92 - 0
frontend/inigovalentin/public/img/license/CC-BY-NC-ND.svg


File diff suppressed because it is too large
+ 84 - 0
frontend/inigovalentin/public/img/license/CC-BY-NC-SA.svg


File diff suppressed because it is too large
+ 81 - 0
frontend/inigovalentin/public/img/license/CC-BY-NC.svg


File diff suppressed because it is too large
+ 84 - 0
frontend/inigovalentin/public/img/license/CC-BY-ND.svg


File diff suppressed because it is too large
+ 107 - 0
frontend/inigovalentin/public/img/license/CC-BY.svg


File diff suppressed because it is too large
+ 5 - 0
frontend/inigovalentin/public/img/license/GPLV3.svg


File diff suppressed because it is too large
+ 488 - 0
frontend/inigovalentin/public/img/social/app_store.svg


+ 17 - 0
frontend/inigovalentin/public/img/social/email.svg

@@ -0,0 +1,17 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
+  <defs id="defs3051">
+    <style type="text/css" id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#4d4d4d;
+      }
+      </style>
+  </defs>
+  <g
+     transform="translate(-421.71429,-525.79074)">
+    <path
+       style="opacity:1;fill:currentColor;fill-opacity:1;stroke:none" 
+       d="m 422.71429,528.79074 0,1 0,14 0,0.41406 0,0.58594 1,0 19,0 0,-0.58594 0,-0.41406 0,-1 0,-14 -0.0488,0 -0.95117,0 -17.53711,0 -0.46289,0 -1,0 z m 1,1 0.0488,0 5.97461,5.97656 -6.02344,6.02344 0,-12 z m 1.46289,0 15.07422,0 -7.53711,7.53711 -7.53711,-7.53711 z m 16.48828,0 0.0488,0 0,12 -6.02344,-6.02344 5.97461,-5.97656 z m -11.2207,6.68359 2.26953,2.26758 2.26953,-2.26758 6.26758,6.26758 0.46289,0.46289 0,0.58594 -18,0 0,-0.58594 0.46289,-0.46289 6.26758,-6.26758 z"
+       id="rect4144" 
+        class="ColorScheme-Text"/>
+  </g>
+</svg>

+ 54 - 0
frontend/inigovalentin/public/img/social/facebook.svg

@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="256" height="256" id="svg2" sodipodi:version="0.32" inkscape:version="0.47 r22583" sodipodi:docname="facebook.svg" version="1.0" inkscape:output_extension="org.inkscape.output.svg.inkscape">
+  <title id="title2910">facebook web</title>
+  <defs id="defs4">
+    <linearGradient inkscape:collect="always" id="linearGradient2555">
+      <stop style="stop-color: rgb(255, 255, 255); stop-opacity: 1;" offset="0" id="stop2557"/>
+      <stop style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="1" id="stop2559"/>
+    </linearGradient>
+    <linearGradient inkscape:collect="always" xlink:href="#linearGradient2555" id="linearGradient2449" gradientUnits="userSpaceOnUse" gradientTransform="matrix(-0.5914584,0,0,0.5914584,210.02161,142.23241)" x1="-344.15295" y1="274.711" x2="-395.84943" y2="425.39993"/>
+  </defs>
+  <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.98994949" inkscape:cx="204.60201" inkscape:cy="64.459739" inkscape:document-units="px" inkscape:current-layer="layer1" inkscape:window-width="682" inkscape:window-height="762" inkscape:window-x="1" inkscape:window-y="193" showgrid="false" inkscape:window-maximized="0"/>
+  <metadata id="metadata7">
+    <rdf:RDF>
+      <cc:Work rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+        <dc:title>facebook web</dc:title>
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>User:ZyMOS</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <dc:subject>
+          <rdf:Bag/>
+        </dc:subject>
+        <cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/>
+        <dc:description/>
+        <dc:contributor>
+          <cc:Agent>
+            <dc:title/>
+          </cc:Agent>
+        </dc:contributor>
+        <dc:publisher>
+          <cc:Agent>
+            <dc:title>Open Icon Library</dc:title>
+          </cc:Agent>
+        </dc:publisher>
+      </cc:Work>
+      <cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/">
+        <cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
+        <cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
+        <cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
+      </cc:License>
+    </rdf:RDF>
+  </metadata>
+  <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(-373.642,-318.344)">
+    <rect inkscape:export-ydpi="7.7063322" inkscape:export-xdpi="7.7063322" inkscape:export-filename="C:\Documents and Settings\Molumen\Desktop\path3511111.png" transform="scale(-1,1)" ry="35.487503" rx="35.487503" y="328.34399" x="-619.64203" height="236" width="236" id="rect1942" style="fill:#0037c1;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.875;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.875, 1.75;stroke-dashoffset:0;stroke-opacity:1"/>
+    <path inkscape:export-ydpi="7.7063322" inkscape:export-xdpi="7.7063322" inkscape:export-filename="C:\Documents and Settings\Molumen\Desktop\path3511111.png" sodipodi:nodetypes="ccccsssc" id="path1950" d="M 557.29062,338.43314 L 445.99327,338.43314 C 416.53255,338.43314 392.81507,362.34527 392.81507,392.04777 L 392.81507,500.64007 C 393.76867,523.8254 397.43678,509.16812 404.41887,483.49194 C 412.53354,453.65102 438.96056,427.56963 471.144,408.02312 C 495.7086,393.10398 523.20395,383.5772 573.25282,382.6709 C 601.63697,382.15694 599.13112,345.83025 557.29062,338.43314 z" style="opacity:0.30042916;fill:url(#linearGradient2449);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.875;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.875, 1.75;stroke-dashoffset:0;stroke-opacity:1"/>
+    <g id="Layer_3" transform="matrix(1.7982054,0,0,1.8129643,376.63343,326.35613)">
+      <path d="M 93.376,117.012 L 72.203,117.012 L 72.203,65.767 L 61.625,65.767 L 61.625,48.107 L 72.203,48.107 L 72.203,37.504 C 72.203,23.097 78.176,14.53 95.146,14.53 L 109.274,14.53 L 109.274,32.192 L 100.443,32.192 C 93.837,32.192 93.4,34.66 93.4,39.266 L 93.376,48.105 L 109.374,48.105 L 107.502,65.765 L 93.376,65.765 L 93.376,117.012 z" id="path3336" style="fill:#ffffff"/>
+    </g>
+  </g>
+</svg>

File diff suppressed because it is too large
+ 3 - 0
frontend/inigovalentin/public/img/social/github.svg


+ 21 - 0
frontend/inigovalentin/public/img/social/google_play.svg

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1"
+	 id="Layer_2" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" sodipodi:docname="Google_Play_logo.svg" inkscape:version="0.48.4 r9939"
+	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="30 336.7 120.9 129.2"
+	 style="enable-background:new 30 336.7 120.9 129.2;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#FFD400;}
+	.st1{fill:#FF3333;}
+	.st2{fill:#48FF48;}
+	.st3{fill:#3BCCFF;}
+</style>
+<path id="path16" inkscape:connector-curvature="0" class="st0" d="M119.2,421.2c15.3-8.4,27-14.8,28-15.3c3.2-1.7,6.5-6.2,0-9.7
+	c-2.1-1.1-13.4-7.3-28-15.3l-20.1,20.2L119.2,421.2z"/>
+<path id="path23" inkscape:connector-curvature="0" class="st1" d="M99.1,401.1l-64.2,64.7c1.5,0.2,3.2-0.2,5.2-1.3
+	c4.2-2.3,48.8-26.7,79.1-43.3L99.1,401.1L99.1,401.1z"/>
+<path id="path30" inkscape:connector-curvature="0" class="st2" d="M99.1,401.1l20.1-20.2c0,0-74.6-40.7-79.1-43.1
+	c-1.7-1-3.6-1.3-5.3-1L99.1,401.1z"/>
+<path id="path37" inkscape:connector-curvature="0" class="st3" d="M99.1,401.1l-64.3-64.3c-2.6,0.6-4.8,2.9-4.8,7.6
+	c0,7.5,0,107.5,0,113.8c0,4.3,1.7,7.4,4.9,7.7L99.1,401.1z"/>
+</svg>

+ 209 - 0
frontend/inigovalentin/public/img/social/google_plus.svg

@@ -0,0 +1,209 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="300.537px" height="300px" viewBox="0 0 300.537 300" enable-background="new 0 0 300.537 300" xml:space="preserve">
+<g id="Background_2_">
+</g>
+<g id="Circle_1_">
+	<g id="Ellipse_1__x2B__Shape_1_2_" enable-background="new    ">
+		<defs>
+			<circle id="SVGID_1_" enable-background="new    " cx="150.269" cy="150" r="147.47"/>
+		</defs>
+		<clipPath id="SVGID_2_">
+			<use xlink:href="#SVGID_1_"  overflow="visible"/>
+		</clipPath>
+		<g transform="matrix(1 0 0 1 -1.891906e-007 -5.580137e-008)" clip-path="url(#SVGID_2_)">
+			
+				<image overflow="visible" width="560" height="559" id="Ellipse_1__x2B__Shape_1_xA0_影像" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAjEAAAIxCAYAAAC8b+n0AAAACXBIWXMAABTqAAAU6gFxlYZXAAAA
+GXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAJmlJREFUeNrs3Q1640Z2sG2U1J5x
+9pUFZEdJFpp1uNvdJEJQkq2WpRZ/UMA5de57vrlmvuSNLBVJ1MNCAZgmAICE2v/913/+z+k//9tQ
+AACJ/O+DMQAAMhIxAICIAQAQMQAAIgYAEDEAACIGAEDEAAAiBgBAxAAAiBgAABEDAIgYAAARAwAg
+YgAAEQMAIGIAAEQMACBiAABEDACAiAEAEDEAgIgBABAxAAAiBgAQMQAAIgYAQMQAACIGAEDEAACI
+GAAAEQMAiBgAABEDACBiAAARAwAgYgAARAwAIGIAAEQMAICIAQAQMQCAiAEAEDEAACIGABAxAAAi
+BgBAxAAAIgYAQMQAAIgYAAARAwCIGAAAEQMAIGIAABEDACBiAABEDAAgYgAARAwAgIgBABAxAICI
+AQAQMQAAIgYAEDEAACIGAEDEAACIGABAxAAAiBgAABEDAIgYAAARAwAgYgAAEQMAIGIAAEQMAICI
+AQBEDACAiAEAEDEAgIgBABAxAAAiBgAQMQAAIgYAQMQAAIgYAEDEAACIGAAAEQMAiBgAABEDACBi
+AAARAwAgYgAARAwAgIgBAEQMAICIAQAQMQCAiAEAEDEAACIGABAxAAAiBgBAxAAAiBgAQMQAAIgY
+AAARAwCIGAAAEQMAIGIAABEDACBiAABEDACAiAEARAwAgIgBABAxAICIAQAQMQAAIgYAEDEAACIG
+AEDEAACIGABAxAAAiBgAABEDAIgYAAARAwAgYgAARAwAIGIAAEQMAICIAQBEDACAiAEAEDEAgIgB
+ABAxAAAiBgBAxAAAIgYAQMQAAIgYAEDEAACIGAAAEQMAiBgAABEDACBiAABEDAAgYgAARAwAgIgB
+AEQMAICIAQAQMQCAiAEAEDEAACIGAEDEAAAiBgBAxAAAiBgAQMQAAIgYAAARAwCIGAAAEQMAIGIA
+AEQMACBiAABEDACAiAEARAwAgIgBABAxAICIAQAQMQAAIgYAQMQAACIGAEDEAABc7YshgL7ml3/P
+89//s/nlfzc//+/e/L+dXv0PXv3P5jf//69/1qK1Nj2007eT5T+np//+eP7vT/+75uUARAyIis+i
+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" transform="matrix(0.5295 0 0 0.5295 2.0034 2)">
+			</image>
+		</g>
+	</g>
+</g>
+<g id="Logo_1_">
+	<g id="G_x2B_">
+		<g>
+			<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M173.723,136.102h-25.044h-33.227v24.653h33.227
+				c-4.974,13.099-17.637,22.412-32.48,22.412c-19.186,0-34.74-15.554-34.74-34.739s15.554-34.739,34.74-34.739
+				c8.548,0,16.369,3.096,22.42,8.217l17.394-17.032c-10.496-9.601-24.468-15.466-39.814-15.466
+				c-32.596,0-59.02,26.424-59.02,59.019c0,32.596,26.424,59.02,59.02,59.02c32.595,0,59.019-26.424,59.019-59.02
+				c0-4.228-0.448-8.35-1.293-12.326H173.723z M222.657,137.968v-20.544h-14.941v20.544h-20.545v14.942h20.545v20.544h14.941V152.91
+				h20.545v-14.942H222.657z"/>
+		</g>
+	</g>
+</g>
+</svg>

+ 37 - 0
frontend/inigovalentin/public/img/social/linkedin.svg

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 15.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1"
+	 id="svg2" sodipodi:version="0.32" inkscape:version="0.47 r22583" sodipodi:docname="linkedin.svg" inkscape:output_extension="org.inkscape.output.svg.inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"
+	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="256px" height="256px"
+	 viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
+<sodipodi:namedview  showgrid="false" inkscape:window-maximized="0" pagecolor="#ffffff" bordercolor="#666666" id="base" borderopacity="1.0" inkscape:cx="138.80354" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.24748737" inkscape:cy="94.798647" inkscape:document-units="px" inkscape:current-layer="layer1" inkscape:window-width="853" inkscape:window-height="674" inkscape:window-x="1" inkscape:window-y="281">
+	</sodipodi:namedview>
+<g id="layer1" transform="translate(-373.642,-318.344)" inkscape:label="Layer 1" inkscape:groupmode="layer">
+	
+		<path id="rect1942" inkscape:export-filename="C:\Documents and Settings\Molumen\Desktop\path3511111.png" inkscape:export-xdpi="7.7063322" inkscape:export-ydpi="7.7063322" fill="#1D8CB5" d="
+		M583.659,328.849H419.625c-19.599,0-35.487,15.888-35.487,35.487v164.015c0,19.599,15.888,35.486,35.487,35.486h164.033
+		c19.599,0,35.487-15.888,35.487-35.486V364.337C619.146,344.738,603.257,328.849,583.659,328.849z"/>
+	
+		<linearGradient id="path1950_1_" gradientUnits="userSpaceOnUse" x1="-808.8727" y1="199.8918" x2="-860.5692" y2="49.2029" gradientTransform="matrix(-0.5915 0 0 -0.5915 -34.5417 412.5453)">
+		<stop  offset="0" style="stop-color:#FFFFFF"/>
+		<stop  offset="1" style="stop-color:#FFFFFF;stop-opacity:0"/>
+	</linearGradient>
+	
+		<path id="path1950" inkscape:export-filename="C:\Documents and Settings\Molumen\Desktop\path3511111.png" inkscape:export-xdpi="7.7063322" sodipodi:nodetypes="ccccsssc" inkscape:export-ydpi="7.7063322" opacity="0.7811" fill="url(#path1950_1_)" enable-background="new    " d="
+		M557.056,338.895H446.227c-29.337,0-52.955,23.81-52.955,53.385v108.127c0.95,23.087,4.603,8.491,11.555-17.075
+		c8.081-29.713,34.396-55.682,66.445-75.145c24.461-14.855,51.84-24.341,101.679-25.244
+		C601.216,382.432,598.721,346.26,557.056,338.895z"/>
+</g>
+<g id="layer1_1_" transform="translate(-200.55198,-393.96227)" inkscape:label="Layer 1" inkscape:groupmode="layer">
+	<g id="g3019" transform="matrix(1.018827,0,0,-1.018827,170.5996,498.03288)">
+		<path id="path28_1_" inkscape:connector-curvature="0" fill="#FFFFFF" d="M109.495-101.774V2.533H74.828v-104.307H109.495z
+			 M92.168,16.775c12.081,0,19.612,8.01,19.612,18.021c-0.224,10.233-7.531,18.021-19.385,18.021
+			c-11.864,0-19.615-7.788-19.615-18.021c0-10.011,7.521-18.021,19.159-18.021H92.168L92.168,16.775z"/>
+		<path id="path30_1_" inkscape:connector-curvature="0" fill="#FFFFFF" d="M128.681-101.774h34.673v58.249
+			c0,3.119,0.226,6.235,1.143,8.462c2.5,6.232,8.209,12.681,17.784,12.681c12.547,0,17.562-9.568,17.562-23.588v-55.804h34.666
+			v59.81c0,32.039-17.1,46.946-39.913,46.946c-18.701,0-26.915-10.45-31.476-17.571h0.234V2.533h-34.673
+			C129.141-7.253,128.681-101.774,128.681-101.774L128.681-101.774z"/>
+	</g>
+</g>
+</svg>

+ 17 - 0
frontend/inigovalentin/public/img/social/telegram.svg

@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 240 240">
+<defs>
+	<linearGradient id="b" x1="0.6667" y1="0.1667" x2="0.4167" y2="0.75">
+		<stop stop-color="#37aee2" offset="0"/>
+		<stop stop-color="#1e96c8" offset="1"/>
+	</linearGradient>
+	<linearGradient id="w" x1="0.6597" y1="0.4369" x2="0.8512" y2="0.8024">
+		<stop stop-color="#eff7fc" offset="0"/>
+		<stop stop-color="#fff" offset="1"/>
+	</linearGradient>
+</defs>
+<circle cx="120" cy="120" r="120" fill="url(#b)"/>
+<path fill="#c8daea" d="m98 175c-3.8876 0-3.227-1.4679-4.5678-5.1695L82 132.2059 170 80"/>
+<path fill="#a9c9dd" d="m98 175c3 0 4.3255-1.372 6-3l16-15.558-19.958-12.035"/>
+<path fill="url(#w)" d="m100.04 144.41 48.36 35.729c5.5185 3.0449 9.5014 1.4684 10.876-5.1235l19.685-92.763c2.0154-8.0802-3.0801-11.745-8.3594-9.3482l-115.59 44.571c-7.8901 3.1647-7.8441 7.5666-1.4382 9.528l29.663 9.2583 68.673-43.325c3.2419-1.9659 6.2173-0.90899 3.7752 1.2584"/>
+</svg>

+ 21 - 0
frontend/inigovalentin/public/img/social/twitter.svg

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 20.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 400 400" style="enable-background:new 0 0 400 400;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#1DA1F2;}
+	.st1{fill:#FFFFFF;}
+</style>
+<g id="Dark_Blue">
+	<path class="st0" d="M350,400H50c-27.6,0-50-22.4-50-50V50C0,22.4,22.4,0,50,0h300c27.6,0,50,22.4,50,50v300
+		C400,377.6,377.6,400,350,400z"/>
+</g>
+<g id="Logo__x2014__FIXED">
+	<path class="st1" d="M153.6,301.6c94.3,0,145.9-78.2,145.9-145.9c0-2.2,0-4.4-0.1-6.6c10-7.2,18.7-16.3,25.6-26.6
+		c-9.2,4.1-19.1,6.8-29.5,8.1c10.6-6.3,18.7-16.4,22.6-28.4c-9.9,5.9-20.9,10.1-32.6,12.4c-9.4-10-22.7-16.2-37.4-16.2
+		c-28.3,0-51.3,23-51.3,51.3c0,4,0.5,7.9,1.3,11.7c-42.6-2.1-80.4-22.6-105.7-53.6c-4.4,7.6-6.9,16.4-6.9,25.8
+		c0,17.8,9.1,33.5,22.8,42.7c-8.4-0.3-16.3-2.6-23.2-6.4c0,0.2,0,0.4,0,0.7c0,24.8,17.7,45.6,41.1,50.3c-4.3,1.2-8.8,1.8-13.5,1.8
+		c-3.3,0-6.5-0.3-9.6-0.9c6.5,20.4,25.5,35.2,47.9,35.6c-17.6,13.8-39.7,22-63.7,22c-4.1,0-8.2-0.2-12.2-0.7
+		C97.7,293.1,124.7,301.6,153.6,301.6"/>
+</g>
+</svg>

+ 46 - 0
frontend/inigovalentin/public/img/social/web.svg

@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 490 490" style="enable-background:new 0 0 490 490;" xml:space="preserve">
+<g>
+	<path d="M245,490c135.31,0,245-109.689,245-245C490,109.69,380.31,0,245,0S0,109.69,0,245C0,380.311,109.69,490,245,490z
+		 M245,30.625c118.206,0,214.375,96.169,214.375,214.375c0,118.207-96.169,214.375-214.375,214.375S30.625,363.207,30.625,245
+		C30.625,126.794,126.794,30.625,245,30.625z"/>
+	<polygon points="128.297,222.899 146.077,289.622 165.188,289.622 186.871,200.378 168.732,200.378 155.034,262.72 
+		139.378,200.378 117.949,200.378 101.635,261.673 88.177,200.378 69.724,200.378 91.033,289.622 110.577,289.622 	"/>
+	<polygon points="228.262,289.622 245.982,222.899 263.762,289.622 282.873,289.622 304.556,200.378 286.417,200.378 
+		272.719,262.72 257.063,200.378 235.634,200.378 219.32,261.673 205.862,200.378 187.409,200.378 208.718,289.622 	"/>
+	<polygon points="345.947,289.622 363.667,222.899 381.447,289.622 400.558,289.622 422.241,200.378 404.102,200.378 
+		390.404,262.72 374.748,200.378 353.319,200.378 337.005,261.673 323.547,200.378 305.094,200.378 326.403,289.622 	"/>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

BIN
frontend/inigovalentin/public/img/tag/android.png


BIN
frontend/inigovalentin/public/img/tag/angular.png


BIN
frontend/inigovalentin/public/img/tag/c.png


BIN
frontend/inigovalentin/public/img/tag/cpp.png


BIN
frontend/inigovalentin/public/img/tag/css.png


BIN
frontend/inigovalentin/public/img/tag/html.png


BIN
frontend/inigovalentin/public/img/tag/ios.png


BIN
frontend/inigovalentin/public/img/tag/java.png


BIN
frontend/inigovalentin/public/img/tag/js.png


BIN
frontend/inigovalentin/public/img/tag/lua.png


BIN
frontend/inigovalentin/public/img/tag/mariadb.png


BIN
frontend/inigovalentin/public/img/tag/nodejs.png


BIN
frontend/inigovalentin/public/img/tag/ogre.png


BIN
frontend/inigovalentin/public/img/tag/php.png


BIN
frontend/inigovalentin/public/img/tag/python.png


BIN
frontend/inigovalentin/public/img/tag/sqlite.png


BIN
frontend/inigovalentin/public/img/tag/swift.png


BIN
frontend/inigovalentin/public/img/tag/tag.xcf


BIN
frontend/inigovalentin/public/img/tag/web.png


+ 34 - 0
frontend/inigovalentin/public/js/help.js

@@ -0,0 +1,34 @@
+/**
+ * Opens or closes a section in the help page.
+ *
+ * If the currently openend section is toggled, it will just close. If another onw is toggled, it
+ * will open, and any other currently open sections will be closed.
+ *
+ * @param id Id of the section to toggle.
+ */
+function toggleHelp(id){
+    var helps = document.getElementsByClassName("help");
+    for (var i = 0; i < helps.length; i++) {
+        if (helps[i].id == id){
+            if (helps[i].classList.contains("selected")) helps[i].classList.remove("selected");
+            else helps[i].classList.add("selected");
+        }
+        else helps[i].classList.remove("selected");
+    }
+}
+
+/**
+ * Hiddes the privacy policy.
+ */
+function closePolicy(){
+    document.getElementById('policy_cover').style.display = 'none';
+    document.getElementById('policy').style.display = 'none';
+}
+
+/**
+ * Shows the privacy policy.
+ */
+function openPolicy(){
+    document.getElementById('policy_cover').style.display = 'block';
+    document.getElementById('policy').style.display = 'block';
+}

+ 12 - 0
frontend/inigovalentin/public/js/profile.js

@@ -0,0 +1,12 @@
+/**
+ * Shows the buttons for all language CVs.
+ *
+ * It also hides the normal CV and the 'Other languages' buttons.
+ */
+function showAllCvs(){
+    document.getElementById('cv_main').style.display = 'none';
+    document.getElementById('cv_other_langs').style.display = 'none';
+    var btns = document.getElementsByClassName('cv_lang');
+    for (var i = 0; i < btns.length; i ++)
+        btns.item(i).style.display = 'inline-block';
+}

+ 72 - 0
frontend/inigovalentin/public/js/project.js

@@ -0,0 +1,72 @@
+/**
+ * Current image ID for the image viewer.
+ */
+let curImage = 0;
+
+/**
+ * Total number of images.
+ */
+let totalImages = -1;
+
+/**
+ * Shows an image in the image viewer.
+ *
+ * Loads the image and text from the image preview.
+ *
+ * @param element The clicked img element.
+ */
+function showImage(element){
+  curImage = Number(element.id.substring(4));
+  document.getElementById('image_viewer_cover').style.display = 'block';
+  document.getElementById('image_viewer_cover').style.opacity = '1';
+  loadImage(element);
+  document.getElementById('image_viewer').style.display = 'block';
+  if (totalImages == -1){
+    console.log("RECALC TOTAL");
+    totalImages = document.getElementById("image_reel").children.length;
+    console.log("RECALC TOTAL: " + totalImages);
+  }
+}
+
+/**
+ * Closes the image viewew.
+ */
+function closeImage(){
+    document.getElementById('image_viewer_cover').style.display = 'none';
+    document.getElementById('image_viewer_cover').style.opacity = '0';
+    document.getElementById('image_viewer').style.display = 'none';
+}
+
+/**
+ * Shows the next image in the image viewer.
+ */
+function nextImage(){
+    curImage = curImage + 1;
+    if (curImage > totalImages) curImage = 0;
+    loadImage(document.getElementById('img_' + curImage));
+}
+
+/**
+ * Shows the previous image in the image viewer.
+ */
+function prevImage(){
+    curImage = curImage - 1;
+    if (curImage < 0) curImage = totalImages;
+    loadImage(document.getElementById('img_' + curImage));
+}
+
+/**
+ * Shows an image in the image viewer.
+ *
+ * Dont call manually, use {@see showImage} instead.
+ *
+ * @param element The clicked img element.
+ */
+function loadImage(element){
+    document.getElementById('image_viewer_title').innerHTML = element.title;
+    document.getElementById('image_viewer_image').src = element.src;
+    document.getElementById('image_viewer_image').srcset = element.srcset;
+    document.getElementById('image_viewer_image').alt = element.alt;
+    document.getElementById('image_viewer_image').title = element.title;
+    console.log("LOADED: " + curImage + "/" + totalImages);
+}

+ 11 - 0
frontend/inigovalentin/public/js/ui.js

@@ -0,0 +1,11 @@
+function showCover(visible){
+    var cover = document.getElementById('cover');
+    if (visible){
+        cover.style.display = 'block';
+        cover.style.opacity = 1;
+    }
+    else{
+        cover.style.display = 'none';
+        cover.style.opacity = 0;
+    }
+}

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

@@ -2,6 +2,7 @@ import { Component } from '@angular/core';
 import { RouterOutlet } from '@angular/router';
 import { NavbarComponent } from './components/navbar/navbar.component';
 import { FooterComponent } from './components/footer/footer.component';
+import { ProjectPreviewComponent } from './components/project-preview/project-preview.component';
 
 @Component({
   selector: 'app-root',

+ 3 - 3
frontend/inigovalentin/src/app/app.routes.ts

@@ -1,7 +1,7 @@
 import { Routes } from '@angular/router';
 import { HomeComponent } from './pages/home/home.component';
 import { ProjectsComponent } from './pages/projects/projects.component';
-import { ShowProjectComponent } from './pages/show-project/show-project.component';
+import { ProjectComponent } from './pages/project/project.component';
 
 export const routes: Routes = [
   {
@@ -9,8 +9,8 @@ export const routes: Routes = [
     component: ProjectsComponent,
   },
   {
-    path: 'show-project/:projectId',
-    component: ShowProjectComponent,
+    path: 'projects/:projectId',
+    component: ProjectComponent,
   },
   {
     path: '',

+ 74 - 0
frontend/inigovalentin/src/app/components/navbar/navbar.component.css

@@ -0,0 +1,74 @@
+header{
+  background-color: #000000;
+  width: 100%;
+  text-align: center;
+}
+
+header div#logo{
+  color: #ffffff;
+  font-size: 4em;
+  font-family: 'Mono';
+}
+header nav{
+  width: 100%;
+}
+header nav a{
+  font-size: 1.5em;
+  margin: 0.5em;
+  color: #ffffff;
+  text-decoration: none;
+  position: relative;
+}
+header nav a:hover{
+  color: #ffffff;
+}
+header nav a:before {
+  content: " ";
+  position: absolute;
+  width: 100%;
+  height: 0.1em;
+  bottom: -0.1em;
+  left: 0;
+  background-color: #ffffff;
+  visibility: hidden;
+  transform: scaleX(0);
+  transition: all 0.3s ease-in-out 0s;
+}
+header nav a:hover:before {
+  visibility: visible;
+  transform: scaleX(1);
+}
+header nav a img{
+  height: 1.5em;
+  vertical-align: bottom;
+}
+@media screen and (max-width : 800px){
+  header div#logo{
+    /* Adjusted for the text 'inigoValentin' */
+    font-size: 12vw;
+  }
+  header nav{
+    overflow: hidden;
+    white-space: nowrap;
+    width: 95%;
+    margin: auto;
+  }
+  header nav a{
+    display: inline-block;
+    width: 30%;
+    margin: 0;
+    padding: 0.5em 0;
+    text-align: center;
+    color: #ffffff;
+    text-decoration: none;
+    border: 0.1em solid #aaaaaa;
+    background-color: #222222;
+    font-size: 4vw;
+  }
+  header nav a img{
+    height: 1em;
+    width: 1em;
+    vertical-align: middle;
+    margin-right: 0.2em;
+  }
+}

+ 10 - 7
frontend/inigovalentin/src/app/components/navbar/navbar.component.html

@@ -1,7 +1,10 @@
-<nav class="fixed top-0 w-full h-12 flex bg-slate-700 text-white justify-between z-50">
-  <div class="flex items-center gap-10 px-10">
-    <img class="w-9" src="logo.svg" />
-    <a routerLink="/">Home</a>
-    <a routerLink="/projects">Projects</a>
-  </div>
-</nav>
+<header>
+    <div id='logo'>
+        inigoValentin
+    </div>
+    <nav>
+        <a routerLink="/">Home</a>
+        <a routerLink="/profile">Me</a>
+        <a routerLink="/projects">Projects</a>
+    </nav>
+</header>

+ 73 - 0
frontend/inigovalentin/src/app/components/project-preview/project-preview.component.css

@@ -0,0 +1,73 @@
+article.project{
+  position: relative;
+  margin: 1em;
+  text-align: left;
+  border-radius: 0.6em;
+  padding: 1.5em 1em;
+  border: 0.1em solid #000000;
+  border-radius: 1em;
+  background-color: #dddddd;
+}
+@media screen and (max-width : 800px){
+  article.project{
+    margin: 0.3em auto;
+    padding: 0.3em;
+  }
+}
+
+@media screen and (max-width : 800px){
+  article.project table td.logo{
+    vertical-align: top;
+  }
+}
+
+article.project h3{
+  font-size: 200%;
+  margin-top: 0;
+}
+
+article.project td.project_details{
+  vertical-align: top;
+  padding-left: 1em;
+}
+@media screen and (max-width : 800px){
+  article.project td.project_details{
+    padding-left: 0.3em;
+  }
+  article.project td.project_details h4{
+    margin-bottom: 0.2em;
+  }
+}
+
+article.project table td{
+  vertical-align: top;
+}
+
+article.project img.logo{
+  padding: 0.5em;
+  max-width: 9em;
+  max-height: 9em;
+  background-color: #ffffff;
+  box-shadow: 0 0 0 0.1em #000000, 0 0 0 0.3em #66bf38, 0 0 0 0.4em #000000, inset 0 0 0.5em #00220077;
+  border-radius: 1em;
+}
+@media screen and (max-width : 1000px){
+  article.project img.logo{
+    padding: 0.2em;
+    max-width: 13vw;
+    max-height: 13vw;
+    border-radius: 0.5em;
+    box-shadow: 0 0 0 0.05em #000000, 0 0 0 0.15em #66bf38, 0 0 0 0.2em #000000, inset 0 0 0.25em #00220077;
+  }
+}
+
+article.project td.project_details div.tags{
+  text-align: right;
+}
+
+article.project td.project_details div.tags img{
+  max-width: 3em;
+  max-height: 3em;
+  border: 0.15em solid #000000;
+  border-radius: 50%;
+}

+ 21 - 0
frontend/inigovalentin/src/app/components/project-preview/project-preview.component.html

@@ -0,0 +1,21 @@
+<article class='project'>
+  <table>
+    <tr>
+      <td ngIf="project.logo" class='logo'>
+        <a routerLink="/projects/{{project.permalink}}">
+          <img class="logo" src="./img/project/{{project.logo}}" alt={{project.title}} />
+        </a>
+      </td>
+      <td class='project_details'>
+        <h4 class='project_name'>{{project.title}}</h4>
+        <h5 class='project_header'>{{project.header}}</h5>
+        <div class='project_text' [innerHTML]=project.text></div>
+        <div class='tags'>
+          <span *ngFor="let tag of project.tags">
+            <img src="./img/tag/{{tag.icon}}" alt="{{tag.tag}}" title="{{tag.tag}}"/>
+          </span>
+        </div>
+      </td>
+    </tr>
+  </table>
+</article>

+ 23 - 0
frontend/inigovalentin/src/app/components/project-preview/project-preview.component.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ProjecPreviewComponent } from './projec-preview.component';
+
+describe('ProjecPreviewComponent', () => {
+  let component: ProjecPreviewComponent;
+  let fixture: ComponentFixture<ProjecPreviewComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [ProjecPreviewComponent]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(ProjecPreviewComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 19 - 0
frontend/inigovalentin/src/app/components/project-preview/project-preview.component.ts

@@ -0,0 +1,19 @@
+import { Component, Injectable, inject, Input } from '@angular/core';
+import { RouterLink, RouterOutlet } from '@angular/router';
+import { CommonModule, SlicePipe, NgFor } from '@angular/common';
+import { Project } from '../../models/project';
+
+@Component({
+  selector: 'app-project-preview',
+  standalone: true,
+  imports: [SlicePipe, NgFor, RouterLink, RouterOutlet],
+  templateUrl: './project-preview.component.html',
+  styleUrl: './project-preview.component.css'
+})
+
+export class ProjectPreviewComponent {
+  title = 'project';
+  errorMessage!: string;
+  @Input() project!: Project;
+  constructor() {}
+}

BIN
frontend/inigovalentin/src/app/components/project/.project.component.css.kate-swp


+ 340 - 0
frontend/inigovalentin/src/app/components/project/project.component.css

@@ -0,0 +1,340 @@
+section{
+  text-align: center;
+  width: 90%;
+  margin: auto;
+}
+section h3{
+  text-align: left;
+}
+div.info_summary{
+  display: inline-block;
+  width: 72%;
+  margin: 0;
+  vertical-align: top;
+  padding: 0;
+  min-height: 13em;
+  text-align: left;
+}
+@media screen and (max-width : 800px){
+  div.info_summary{
+    display: block;
+    width: 100%;
+    margin: 0;
+    padding: 0;
+  }
+}
+
+div.info_summary img.logo{
+  padding: 0.5em;
+  max-width: 100%;
+  max-height: 18em;
+  border-radius: 1em;
+  float: left;
+  margin: 0 2em 1em 0;
+}
+@media screen and (max-width : 800px){
+  div.info_summary img.logo{
+    margin: 0 2em 1em 0.4em;
+    padding: 0.2em;
+    width: 20vw;
+    height: 20vw;
+  }
+}
+
+div.info_summary h4{
+  margin: 0.5em;
+  font-style: italic;
+}
+
+div.info_summary div.text{
+  margin: 2em;
+  min-height: 7em;
+}
+@media screen and (max-width : 800px){
+  div.info_summary div.text{
+    margin: 0;
+    min-height: 20vw;
+  }
+}
+div.comment{
+  font-style: italic;
+  border: 0.1em solid .333;
+  padding: 1.5em;
+  box-shadow: 0 0 0.5em .111;
+  margin: 1em auto;
+  max-width: 55em;
+  background: .dbffdb;
+  rotate: -1deg;
+  border-radius: 0.2em;
+  line-height: 1.6em;
+  text-align: left;
+  display: inline-block;
+}
+@media screen and (max-width : 800px){
+  div.comment{
+    rotate: 0deg;
+    margin: 0.5em;
+    padding: 0.5em;
+    box-shadow: 0 0 0;
+  }
+}
+div.comment div.comment_title{
+  font-family: 'Handwritten';
+  display: block;
+  width: 3em;
+  font-size: 180%;
+  float: left;
+  line-height: 1;
+  padding: 0.5em 0 0 0.5em;
+  margin: 0 0.2em 0 -0.5em;
+  rotate: -36deg;
+}
+@media screen and (max-width : 800px){
+  div.comment div.comment_title{
+    font-size: 110%;
+    margin: 0 0.5em 0 0;
+    line-height: 1;
+  }
+}
+div.info_details{
+  display: inline-block;
+  width: 22%;
+  vertical-align: top;
+  margin: 0;
+  border-left: 0.1em solid .aacaaa;
+  /*background: linear-gradient(to right, .e5f0e500 0%, .e5f0e5ff 20%, .e5f0e5ff 80%, .e5f0e500 100%); */
+  background: linear-gradient(to right, .e5f0e5ff 0%, .e5f0e5cc 30%, .e5f0e500 100%);
+  padding: 1em;
+  text-align: center;
+}
+@media screen and (max-width : 800px){
+  div.info_details{
+    text-align: center;
+    display: block;
+    width: 85%;
+    margin: 0.5em auto;
+    border-left: 0.1em solid .aacaaa;
+    border-right: 0.1em solid .aacaaa;
+    border-top: 0.1em solid .aacaaa;
+    border-bottom: 0.1em solid .aacaaa;
+    border-radius: 0.5em;
+    /*background: linear-gradient(to right, .e5f0e500 0%, .e5f0e5ff 20%, .e5f0e5ff 80%, .e5f0e500 100%);*/
+    padding: 0.5em;
+  }
+}
+
+div.info_details div.details{
+  margin: 0.5em 1.5em 0.5em 1.5em;
+}
+@media screen and (max-width : 800px){
+  div.info_details div.details{
+    margin: 0.5em 0;
+    text-align: left;
+  }
+}
+
+div.info_details div.details hr.details_separator{
+  margin: 0.7em -0.5em 0.7em -0.5em;
+  border: 0;
+  color: .aacaaa;
+  background-color: .aacaaa;
+  border-top: 0.1em solid .aacaaa;
+}
+@media screen and (max-width : 800px){
+  div.info_details div.details hr.details_separator{
+    margin: 0.5em;
+  }
+}
+
+div.info_details div.links{
+  padding-top: 0.5em;
+  text-align: center;
+}
+
+div.info_details div.links a.link{
+  display: block;
+  margin-bottom: 1em;
+  text-align: left;
+}
+div.info_details div.links a.link img.link{
+  vertical-align: middle;
+  width: 1.5em;
+  height: 1.5em;
+  margin-right: 0.5em;
+}
+
+div.info_details div.tags{
+  text-align: left;
+  color: .00000088;
+  font-style: italic;
+  font-size: 85%;
+}
+
+div.info_details div.tags span{
+  font-weight: bold;
+}
+
+div.info_details div.license{
+  text-align: left;
+  color: .00000088;
+  font-style: italic;
+  font-size: 85%;
+  font-weight: bold;
+}
+
+
+div.info_details div.license img{
+  max-width: 8em;
+  max-height: 1.5em;
+  vertical-align: middle;
+  text-align: center;
+  margin-left: 1em;
+}
+
+
+/* The image reel */
+div.image_reel{
+  display: block;
+  margin: auto;
+}
+
+div.image_reel div.img{
+  display:inline-block;
+  vertical-align: top;
+  max-width: 20%;
+  max-height: 25em;
+  text-align: center;
+  background: .00000020;
+  border: 0.1em solid .aacaaa;
+  border-radius: 0.5em;
+  padding: 0.1em;
+  margin: 0.3em;
+  box-shadow: inset 0 0 0.5em .aacaaa;
+}
+
+div.image_reel div.img img{
+  display: block;
+  cursor: pointer;
+  margin: 0.5em auto 0.2em auto;
+  max-width: calc(100% - 0.5em);
+  max-height: 22em;
+  border: 0.1em solid .000000;
+  background: .000000;
+}
+div.image_reel div.img h5{
+  display: block;
+  margin: 0 auto 0.3em auto;
+}
+
+/* The image viewer. */
+div.image_viewer_cover{
+  position: fixed;
+  opacity: 0;
+  transition: all 0.7s;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 99;
+  background-color: .00000077;
+  display: none;
+}
+
+section.image_viewer{
+  position: fixed;
+  top: 15%;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 70em;
+  height: 53em;
+  z-index: 100;
+  display: none;
+}
+
+@media screen and (max-width : 800px){
+  section.image_viewer{
+    width: 85%;
+    height: 80vh;
+    left: 5%;
+    top: 10vh;
+    transform: translateX(0%);
+  }
+}
+
+section.image_viewer h3 div.image_viewer_close{
+  float: right;
+  cursor: pointer;
+}
+
+section.image_viewer h3 div.image_viewer_close img{
+  height: 1.5em;
+  width: 1.5em;
+}
+section.image_viewer h3 div.image_viewer_close img:hover{
+  filter: invert(70%);
+}
+
+section.image_viewer table.image_viewer_container{
+  width: 100%;
+  height: calc(100% - 4em);
+  border-collapse: collapse;
+}
+@media screen and (max-width : 800px){
+  section.image_viewer table.image_viewer_container{
+    max-height: 60vh;
+  }
+}
+
+section.image_viewer table.image_viewer_container td.control{
+  width: 4em;
+  text-align: center;
+}
+
+@media screen and (max-width : 900px){
+  section.image_viewer table.image_viewer_container td.control{
+    display: none;
+  }
+}
+
+section.image_viewer table.image_viewer_container td.control img{
+  width: 4em;
+  cursor: pointer;
+  transition: all 0.5s;
+}
+section.image_viewer table.image_viewer_container td.control img:hover{
+  filter: invert(70%);
+}
+
+section.image_viewer table.image_viewer_container td.image{
+  text-align: center;
+}
+
+section.image_viewer table.image_viewer_container td.image img{
+  max-width: 100%;
+  max-height: 50em;
+  border: 0.1em solid .000000;
+  background: .000000;
+}
+
+@media screen and (max-width : 800px){
+  section.image_viewer table.image_viewer_container td.image img{
+    max-width: 100%;
+    max-height: 60vh;
+  }
+}
+
+section.image_viewer div.secondary_controls{
+  display: none;
+}
+@media screen and (max-width : 900px){
+  section.image_viewer div.secondary_controls{
+    display: block;
+  }
+}
+
+section.image_viewer div.secondary_controls img{
+  width: 5em;
+  height: 5em;
+  margin: 0 3em;
+  cursor: pointer;
+}

+ 0 - 1
frontend/inigovalentin/src/app/components/project/project.component.html

@@ -1,4 +1,3 @@
-PROJECT
 <a
   routerLink="/show-project/{{ project.id }}"
   class="w-full min-w-[230px] h-90 overflow-hidden block rounded relative hover:scale-105 transition-all">

+ 7 - 0
frontend/inigovalentin/src/app/models/license.ts

@@ -0,0 +1,7 @@
+export interface License {
+  id: number;
+  summary: string;
+  legal: number;
+  logo: string;
+  icon: number;
+}

+ 6 - 1
frontend/inigovalentin/src/app/models/project.ts

@@ -1,3 +1,5 @@
+import { License } from './license';
+
 export interface Project {
   id: number;
   permalink: string;
@@ -9,5 +11,8 @@ export interface Project {
   header: number;
   text: string;
   comment: string;
-  license?: any[];
+  license?: License;
+  tags?: any[];
+  "project-urls": any[];
+  "project-images": any[];
 }

+ 339 - 0
frontend/inigovalentin/src/app/pages/project/project.component.css

@@ -0,0 +1,339 @@
+section{
+  text-align: center;
+  width: 90%;
+  margin: auto;
+}
+section h3{
+  text-align: left;
+}
+div.info_summary{
+  display: inline-block;
+  width: 72%;
+  margin: 0;
+  vertical-align: top;
+  padding: 0;
+  min-height: 13em;
+  text-align: left;
+}
+@media screen and (max-width : 800px){
+  div.info_summary{
+    display: block;
+    width: 100%;
+    margin: 0;
+    padding: 0;
+  }
+}
+
+div.info_summary img.logo{
+  padding: 0.5em;
+  max-width: 100%;
+  max-height: 18em;
+  border-radius: 1em;
+  float: left;
+  margin: 0 2em 1em 0;
+}
+@media screen and (max-width : 800px){
+  div.info_summary img.logo{
+    margin: 0 2em 1em 0.4em;
+    padding: 0.2em;
+    width: 20vw;
+    height: 20vw;
+  }
+}
+
+div.info_summary h4{
+  margin: 0.5em;
+  font-style: italic;
+}
+
+div.info_summary div.text{
+  margin: 2em;
+  min-height: 7em;
+}
+@media screen and (max-width : 800px){
+  div.info_summary div.text{
+    margin: 0;
+    min-height: 20vw;
+  }
+}
+div.comment{
+  font-style: italic;
+  border: 0.1em solid .333;
+  padding: 1.5em;
+  box-shadow: 0 0 0.5em .111;
+  margin: 1em auto;
+  max-width: 55em;
+  background: .dbffdb;
+  rotate: -1deg;
+  border-radius: 0.2em;
+  line-height: 1.6em;
+  text-align: left;
+  display: inline-block;
+}
+@media screen and (max-width : 800px){
+  div.comment{
+    rotate: 0deg;
+    margin: 0.5em;
+    padding: 0.5em;
+    box-shadow: 0 0 0;
+  }
+}
+div.comment div.comment_title{
+  font-family: 'Handwritten';
+  display: block;
+  width: 3em;
+  font-size: 180%;
+  float: left;
+  line-height: 1;
+  padding: 0.5em 0 0 0.5em;
+  margin: 0 0.2em 0 -0.5em;
+  rotate: -36deg;
+}
+@media screen and (max-width : 800px){
+  div.comment div.comment_title{
+    font-size: 110%;
+    margin: 0 0.5em 0 0;
+    line-height: 1;
+  }
+}
+div.info_details{
+  display: inline-block;
+  width: 22%;
+  vertical-align: top;
+  margin: 0;
+  border-left: 0.1em solid .aacaaa;
+  /*background: linear-gradient(to right, .e5f0e500 0%, .e5f0e5ff 20%, .e5f0e5ff 80%, .e5f0e500 100%); */
+  background: linear-gradient(to right, .e5f0e5ff 0%, .e5f0e5cc 30%, .e5f0e500 100%);
+  padding: 1em;
+  text-align: center;
+}
+@media screen and (max-width : 800px){
+  div.info_details{
+    text-align: center;
+    display: block;
+    width: 85%;
+    margin: 0.5em auto;
+    border-left: 0.1em solid .aacaaa;
+    border-right: 0.1em solid .aacaaa;
+    border-top: 0.1em solid .aacaaa;
+    border-bottom: 0.1em solid .aacaaa;
+    border-radius: 0.5em;
+    /*background: linear-gradient(to right, .e5f0e500 0%, .e5f0e5ff 20%, .e5f0e5ff 80%, .e5f0e500 100%);*/
+    padding: 0.5em;
+  }
+}
+
+div.info_details div.details{
+  margin: 0.5em 1.5em 0.5em 1.5em;
+}
+@media screen and (max-width : 800px){
+  div.info_details div.details{
+    margin: 0.5em 0;
+    text-align: left;
+  }
+}
+
+div.info_details div.details hr.details_separator{
+  margin: 0.7em -0.5em 0.7em -0.5em;
+  border: 0;
+  color: .aacaaa;
+  background-color: .aacaaa;
+  border-top: 0.1em solid .aacaaa;
+}
+@media screen and (max-width : 800px){
+  div.info_details div.details hr.details_separator{
+    margin: 0.5em;
+  }
+}
+
+div.info_details ul.urls{
+  padding-top: 0.5em;
+  text-align: center;
+  border-bottom: 0.1em solid #000000;
+  list-style-type: none;
+}
+
+div.info_details ul.urls a.url{
+  display: block;
+  margin-bottom: 1em;
+  text-align: left;
+}
+div.info_details ul.urls a.url img.url{
+  vertical-align: middle;
+  width: 1.8em;
+  height: 1.8em;
+  margin-right: 0.5em;
+}
+
+div.info_details div.tags{
+  border-bottom: 0.1em solid #000000;
+}
+
+div.info_details div.tags img{
+  max-width: 3em;
+  max-height: 3em;
+  border: 0.15em solid #000000;
+  border-radius: 50%;
+  margin: auto 0.3em;
+}
+
+div.info_details div.license{
+  color: #00000088;
+}
+
+
+div.info_details div.license img{
+  max-width: 8em;
+  max-height: 1.5em;
+  vertical-align: middle;
+  text-align: center;
+  margin-left: 1em;
+}
+
+
+/* The image reel */
+div#image_reel{
+  display: block;
+  margin: auto;
+}
+
+div#image_reel div.img{
+  display:inline-block;
+  vertical-align: top;
+  max-width: 20%;
+  max-height: 25em;
+  text-align: center;
+  background: .00000020;
+  border: 0.1em solid .aacaaa;
+  border-radius: 0.5em;
+  padding: 0.1em;
+  margin: 0.3em;
+  box-shadow: inset 0 0 0.5em .aacaaa;
+}
+
+div#image_reel div.img img{
+  display: block;
+  cursor: pointer;
+  margin: 0.5em auto 0.2em auto;
+  max-width: calc(100% - 0.5em);
+  max-height: 22em;
+  border: 0.1em solid .000000;
+  background: .000000;
+}
+div#image_reel div.img h5{
+  display: block;
+  margin: 0 auto 0.3em auto;
+}
+
+/* The image viewer. */
+div#image_viewer_cover{
+  position: fixed;
+  opacity: 0;
+  transition: all 0.7s;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 99;
+  background-color: .00000077;
+  display: none;
+}
+
+section#image_viewer{
+  position: fixed;
+  top: 15%;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 70em;
+  height: 53em;
+  z-index: 100;
+  display: none;
+}
+
+@media screen and (max-width : 800px){
+  section#image_viewer{
+    width: 85%;
+    height: 80vh;
+    left: 5%;
+    top: 10vh;
+    transform: translateX(0%);
+  }
+}
+
+section#image_viewer h3 div#image_viewer_close{
+  float: right;
+  cursor: pointer;
+}
+
+section#image_viewer h3 div#image_viewer_close img{
+  height: 1.5em;
+  width: 1.5em;
+}
+section#image_viewer h3 div#image_viewer_close img:hover{
+  filter: invert(70%);
+}
+
+section#image_viewer table#image_viewer_container{
+  width: 100%;
+  height: calc(100% - 4em);
+  border-collapse: collapse;
+}
+@media screen and (max-width : 800px){
+  section#image_viewer table#image_viewer_container{
+    max-height: 60vh;
+  }
+}
+
+section#image_viewer table#image_viewer_container td.control{
+  width: 4em;
+  text-align: center;
+}
+
+@media screen and (max-width : 900px){
+  section#image_viewer table#image_viewer_container td.control{
+    display: none;
+  }
+}
+
+section#image_viewer table#image_viewer_container td.control img{
+  width: 4em;
+  cursor: pointer;
+  transition: all 0.5s;
+}
+section#image_viewer table#image_viewer_container td.control img:hover{
+  filter: invert(70%);
+}
+
+section#image_viewer table#image_viewer_container td.image{
+  text-align: center;
+}
+
+section#image_viewer table#image_viewer_container td.image img{
+  max-width: 100%;
+  max-height: 50em;
+  border: 0.1em solid .000000;
+  background: .000000;
+}
+
+@media screen and (max-width : 800px){
+  section#image_viewer table#image_viewer_container td.image img{
+    max-width: 100%;
+    max-height: 60vh;
+  }
+}
+
+section#image_viewer div#secondary_controls{
+  display: none;
+}
+@media screen and (max-width : 900px){
+  section#image_viewer div#secondary_controls{
+    display: block;
+  }
+}
+
+section#image_viewer div#secondary_controls img{
+  width: 5em;
+  height: 5em;
+  margin: 0 3em;
+  cursor: pointer;
+}

+ 91 - 0
frontend/inigovalentin/src/app/pages/project/project.component.html

@@ -0,0 +1,91 @@
+<script type="text/javascript" src="./js/project.js"></script>
+@if(projectObs$ | async; as project){
+  <section>
+    <h3>{{ project.title }}</h3>
+    <article>
+      <div class='info_summary'>
+        @if(project.logo){
+          <img class='logo' src="./img/project/{{project.logo}}" alt={{project.title}}/>
+        }
+        <h4>{{ project.header }}</h4>
+        <div class='text' [innerHTML]=project.text></div>
+      </div>
+      <div class='info_details'>
+        @if(project['project-urls']){
+          <ul class='urls'>
+            <li *ngFor="let url of project['project-urls']">
+              <a href="{{ url.url }}" title="{{ url.type.summary }}" target="_blank" class="url">
+                <img src="./img/social/{{url.type.logo}}" alt="{{url.type.title}}" class="url"/>
+                {{ url.type.title }}
+              </a>
+            </li>
+          </ul>
+        }
+        @if(project.tags){
+          <div class='tags'>
+            <span *ngFor="let tag of project.tags">
+              <img src="./img/tag/{{tag.icon}}" alt="{{tag.tag}}" title="{{tag.tag}}"/>
+            </span>
+          </div>
+        }
+        @if(project.license){
+          <div class='license'>
+            License:
+            <img src="./img/license/{{project.license.logo}}" alt="{{project.license.legal}}" title="{{project.license.legal}}"/>
+          </div>
+        }
+      </div>
+      @if(project.comment){
+        <div class='comment'>
+          <div class='comment_title'>
+              TODO NOTES
+          </div>
+          <div class="comment_text" [innerHTML]=project.comment></div>
+        </div>
+      }
+      @if(project['project-images']){
+        <div id='image_reel'>
+          <div *ngFor="let img of project['project-images']; let i = index" class="img">
+            <img id="img_{{ i }}" src="./img/project/{{img.image}}" alt="{{img.alt}}" title="{{img.alt}}" onClick="showImage(this);"/>
+            <h5>{{img.alt}}</h5>
+          </div>
+        </div>
+      }
+    </article>
+  </section>
+  <div id='image_viewer_cover' onclick='closeImage();'></div>
+  <section id='image_viewer'>
+    <h3>
+      <span id='image_viewer_title'></span>
+      <div id='image_viewer_close'>
+          <a onClick='closeImage();'>
+              <img src='https://v-gears.inigovalentin.com/img/close.svg'/>
+          </a>
+      </div>
+    </h3>
+    <table id='image_viewer_container'>
+        <td class='control'>
+            <a onClick='prevImage();'>
+                <img src='https://v-gears.inigovalentin.com/img/prev.svg'/>
+            </a>
+        </td>
+        <td class='image'>
+            <img id='image_viewer_image'/>
+        </td>
+        <td class='control'>
+            <a onClick='nextImage();'>
+                <img src='https://v-gears.inigovalentin.com/img/next.svg'/>
+            </a>
+        </td>
+    </table>
+    <div id='secondary_controls'>
+        <a onClick='prevImage();'>
+            <img src='https://v-gears.inigovalentin.com/img/prev.svg'/>
+        </a>
+        <a onClick='nextImage();'>
+            <img src='https://v-gears.inigovalentin.com/img/next.svg'/>
+        </a>
+    </div>
+  </section>
+}
+

+ 4 - 4
frontend/inigovalentin/src/app/pages/show-project/show-project.component.spec.ts → frontend/inigovalentin/src/app/pages/project/project.component.spec.ts

@@ -1,18 +1,18 @@
 import { ComponentFixture, TestBed } from '@angular/core/testing';
 
-import { ShowProjectComponent } from './show-project.component';
+import { ProjectComponent } from './project.component';
 
-describe('ShowProjectComponent', () => {
+describe('ProjectComponent', () => {
   let component: ShowProjectComponent;
   let fixture: ComponentFixture<ShowProjectComponent>;
 
   beforeEach(async () => {
     await TestBed.configureTestingModule({
-      imports: [ShowProjectComponent]
+      imports: [ProjectComponent]
     })
     .compileComponents();
 
-    fixture = TestBed.createComponent(ShowProjectComponent);
+    fixture = TestBed.createComponent(ProjectComponent);
     component = fixture.componentInstance;
     fixture.detectChanges();
   });

+ 109 - 0
frontend/inigovalentin/src/app/pages/project/project.component.ts

@@ -0,0 +1,109 @@
+import { Component, Input, inject } from '@angular/core';
+import { ProjectsService } from '../../services/projects.service';
+import { AsyncPipe, CurrencyPipe, DatePipe, NgFor, NgIf } from '@angular/common';
+import { ActivatedRoute } from '@angular/router';
+import { map } from 'rxjs/operators';
+import { Observable } from 'rxjs';
+import { Project } from '../../models/project';
+declare function showImage(element: any): any;
+declare function nextImage(): any;
+declare function prevImage(): any;
+declare function closeImage(): any;
+declare var curImage: number;
+declare var totalImages: number;
+
+@Component({
+  selector: 'app-project',
+  standalone: true,
+  imports: [AsyncPipe, DatePipe, CurrencyPipe, NgIf, NgFor],
+  templateUrl: './project.component.html',
+  styleUrl: './project.component.css'
+})
+export class ProjectComponent {
+
+  /*@Input() projectId: string = '';
+  private projectsService = inject(ProjectsService);
+  public projectObs$! : Observable<Project>;
+  private activatedRouter = inject(ActivatedRoute);*/
+
+  title = 'project';
+  projectObs$!: Observable<Project>;
+  errorMessage!: string;
+  private activatedRouter = inject(ActivatedRoute);
+
+  constructor(private projects_service: ProjectsService) {}
+
+  /*ngOnInit(){
+    this.activatedRouter.params.pipe(map((p)=> p['projectId'])).subscribe((id)=>{
+      //this.projectObs$ = this.projectsService.fetchProjectById(id);
+
+    })
+  }*/
+  ngOnInit() {
+    this.activatedRouter.params.pipe(map((p)=> p['projectId'])).subscribe((id)=>{
+        this.projectObs$! = this.projects_service.getProject(id);
+    });
+  }
+
+  /**
+   * Current image ID for the image viewer.
+   */
+  //let curImage = 0;
+
+  /**
+   * Shows an image in the image viewer.
+   *
+   * Loads the image and text from the image preview.
+   *
+   * @param element The clicked img element.
+   */
+   /* showImage(element: any): any{
+    var curImage: number = Number(element.id.substring(4));
+    document.getElementById('image_viewer_cover')!.style.display = 'block';
+    document.getElementById('image_viewer_cover')!.style.opacity = '1';
+    //loadImage(element);
+    document.getElementById('image_viewer')!.style.display = 'block';
+  }*/
+
+  /**
+   * Closes the image viewew.
+   */
+  /*function closeImage(){
+    document.getElementById('image_viewer_cover').style.display = 'none';
+    document.getElementById('image_viewer_cover').style.opacity = '0';
+    document.getElementById('image_viewer').style.display = 'none';
+  }*/
+
+  /**
+   * Shows the next image in the image viewer.
+   */
+  /*function nextImage(){
+    curImage = curImage + 1;
+    if (curImage > maxImages) curImage = 0;
+    loadImage(document.getElementById('img_' + curImage));
+  }*/
+
+  /**
+   * Shows the previous image in the image viewer.
+   */
+  /*function prevImage(){
+    curImage = curImage - 1;
+    if (curImage < 0) curImage = maxImages;
+    loadImage(document.getElementById('img_' + curImage));
+  }*/
+
+  /**
+   * Shows an image in the image viewer.
+   *
+   * Dont call manually, use {@see showImage} instead.
+   *
+   * @param element The clicked img element.
+   */
+  /*function loadImage(element){
+    document.getElementById('image_viewer_title').innerHTML = element.title;
+    document.getElementById('image_viewer_image').src = element.src;
+    document.getElementById('image_viewer_image').srcset = element.srcset;
+    document.getElementById('image_viewer_image').alt = element.alt;
+    document.getElementById('image_viewer_image').title = element.title;
+  }*/
+}

+ 1 - 21
frontend/inigovalentin/src/app/pages/projects/projects.component.html

@@ -1,21 +1 @@
-<p>projects works!</p>
-<br/>
-<br/><br/><br/><br/><br/><br/>
-<h1>PROJECTS</h1>
-
-
-
-
-<table>
-  <tr>
-    <th>id</th>
-    <th>title</th>
-    <th>header</th>
-  </tr>
-  <tr *ngFor="let project of projects">
-    <td>{{project.id}}</td>
-    <td>{{project.title}}</td>
-    <td>{{project.header}}</td>
-  </tr>
-  <tr>
-</table>
+<app-project-preview *ngFor="let project of projects" [project]="project"></app-project-preview>

+ 15 - 53
frontend/inigovalentin/src/app/pages/projects/projects.component.ts

@@ -1,63 +1,20 @@
 import { Component, DestroyRef, inject, Input } from '@angular/core';
-import { ProjectsService } from '../../services/projects.service';
-import { AsyncPipe } from '@angular/common';
+import { HttpClient } from '@angular/common/http';
+import { CommonModule, AsyncPipe, NgFor } from '@angular/common';
 import { Project } from '../../models/project';
+import { ProjectsService } from '../../services/projects.service';
 import { ProjectComponent } from '../../components/project/project.component';
-import { HttpClient } from '@angular/common/http';
-import { CommonModule } from '@angular/common';
-import { NgFor } from '@angular/common';
+import { ProjectPreviewComponent } from '../../components/project-preview/project-preview.component';
+
+declare function showImage(element: any): any
 
 @Component({
   selector: 'app-projects',
   standalone: true,
-  imports: [ProjectComponent, NgFor],
+  imports: [ProjectComponent, NgFor, ProjectPreviewComponent],
   templateUrl: './projects.component.html',
   styleUrl: './projects.component.css'
 })
-/*export class ProjectsComponent {
-  private projectsService  =  inject(ProjectsService);
-  private pageNumber  =  1;
-  private destroyRef  =  inject(DestroyRef)
-  public projectsObs$  =  this.projectsService.fetchAll(this.pageNumber);
-  public projectsResults: Project[] = [];
-  @Input() project!: Project;
-}*/
-
-
-/*export class ProjectsComponent {
-  private projectsService = inject(ProjectsService);
-  private pageNumber = 1;
-  private destroyRef  =  inject(DestroyRef)
-  public projectsObs$ = this.projectsService.fetchAll(this.pageNumber);
-  public projectsResults: Project[] = [];
-
-
-  ngOnInit(){
-    this.projectsObs$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => {
-      this.projectsResults = data.results;
-    });
-  }
-
-  onScroll(): void {
-    this.pageNumber++;
-    console.log("scrolled!!");
-
-    this.projectsObs$ = this.projectsService.fetchAll(this.pageNumber);
-    this.projectsObs$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data) => {
-      this.projectsResults = this.projectsResults.concat(data.results);
-    });
-
-  }
-
-}*/
-
-/*export class ProjectsComponent {
-  data$ = this.http.get('http://localhost/API/projects/').pipe(
-    map(data => ({ state: "loaded", data })),
-    catchError(error => of({ state: "error", error })),
-    startWith({state: "loading"})
-  );
-}*/
 
 export class ProjectsComponent {
   title = 'projects';
@@ -72,9 +29,14 @@ export class ProjectsComponent {
         this.projects = projects;
         console.log(this.projects);
       },
-      error: (error) => {
-        this.errorMessage = error;
-      },
+      error: (error) => {this.errorMessage = error;},
     });
+    let node = document.createElement('script');
+    node.src = './js/project.j';
+    node.type = 'text/javascript';
+    node.async = true;
+    node.charset = 'utf-8';
+    document.getElementsByTagName('head')[0].appendChild(node);
+    console.log("LOAD!");
   }
 }

+ 0 - 0
frontend/inigovalentin/src/app/pages/show-project/show-project.component.css


+ 0 - 16
frontend/inigovalentin/src/app/pages/show-project/show-project.component.html

@@ -1,16 +0,0 @@
-<div class="relative h-full">
-  <p class="text-white">Hello</p>
-  @if(projectObs$ | async; as project){
-
-  <div class="text-white">
-    <div class="container mx-auto flex flex-row">
-      <div class="p-3 flex flex-col gap-7">
-        <h2 class="text-ellipsis text-2xl border-b font-semibold">
-          {{ project.title }}
-        </h2>
-        <p class="border-b">{{ project.header }}</p>
-      </div>
-    </div>
-  </div>
-  }
-</div>

+ 0 - 31
frontend/inigovalentin/src/app/pages/show-project/show-project.component.ts

@@ -1,31 +0,0 @@
-import { Component, Input, inject } from '@angular/core';
-import { ProjectsService } from '../../services/projects.service';
-import { AsyncPipe, CurrencyPipe, DatePipe } from '@angular/common';
-import { ActivatedRoute } from '@angular/router';
-import { map } from 'rxjs/operators';
-import { Observable } from 'rxjs';
-import { Project } from '../../models/project';
-
-@Component({
-  selector: 'app-show-project',
-  standalone: true,
-  imports: [AsyncPipe, DatePipe, CurrencyPipe],
-  templateUrl: './show-project.component.html',
-  styleUrl: './show-project.component.css'
-})
-export class ShowProjectComponent {
-
-  @Input() projectId: string = '';
-  private projectsService = inject(ProjectsService);
-  public projectObs$! : Observable<Project>;
-  private activatedRouter = inject(ActivatedRoute);
-
-  ngOnInit(){
-  console.log("TEST");
-    this.activatedRouter.params.pipe(map((p)=> p['projectId'])).subscribe((id)=>{
-      console.log(id);
-      //this.projectObs$ = this.projectsService.fetchProjectById(id);
-
-    })
-  }
-}

+ 5 - 24
frontend/inigovalentin/src/app/services/projects.service.ts

@@ -1,14 +1,6 @@
-/*import { Injectable, inject } from '@angular/core';
-import { environment } from '../../environments/environment';
-import { HttpClient } from '@angular/common/http';
-import { Project } from '../models/project';
-import { Projects } from '../models/projects';
-import { map } from 'rxjs';
-*/
-
+import { Injectable } from '@angular/core';
 import { HttpClient } from '@angular/common/http';
 import { Observable } from 'rxjs';
-import { Injectable } from '@angular/core';
 import { Projects } from '../models/projects';
 import { Project } from '../models/project';
 
@@ -17,28 +9,17 @@ import { Project } from '../models/project';
 })
 export class ProjectsService {
 
-  /*private apiUrl = 'http://localhost:8080/API/';
-  private httpClient = inject(HttpClient);
-  constructor() { console.log("PROJECTS SERVICE");}
-
-  fetchProjectById(id: string) {
-    console.log("FETCH BY ID: " + id);
-    return this.httpClient.get<Project>(`${this.apiUrl}/projects/${id}`)
-  }
-
-  fetchAll(pageNumber: number) {
-    console.log("FETCH ALL: " + pageNumber);
-    return this.httpClient.get<Projects>(`${this.apiUrl}/projects/`)
-  }*/
-
   apiUrl = 'http://localhost:8080/api/projects/';
 
   constructor(private http: HttpClient) {}
 
   getAllProjects(): Observable<Project[]> {
-    console.log("GETALL");
     return this.http.get<Project[]>(this.apiUrl);
   }
 
+  getProject(permalink: String): Observable<Project> {
+    return this.http.get<Project>(this.apiUrl + permalink);
+  }
+
 
 }

+ 720 - 3
frontend/inigovalentin/src/styles.css

@@ -1,4 +1,721 @@
 /* You can add global styles to this file, and also import other style files */
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
+/* Website fonts. */
+@font-face {
+  font-family: 'Nunito';
+  src:  url('/fonts/Nunito-Regular.ttf');
+}
+@font-face {
+  font-family: 'Mono';
+  src:  url('/fonts/NotoSansMono-Regular.ttf');
+}
+@font-face {
+  font-family: 'Mono';
+  src:  url('/fonts/NotoSansMono-Bold.ttf');
+  font-weight: bold;
+}
+@font-face {
+  font-family: 'Handwritten';
+  src:  url('/fonts/Handwritten.ttf');
+}
+
+/* Elements to be shown only in large screens. */
+.desktop{
+  display: initial;
+}
+@media screen and (max-width : 800px){
+  .desktop{
+    display: none;
+  }
+}
+
+/* Elements to be shown only in small screens. */
+.mobile{
+  display: none;
+}
+@media screen and (max-width : 800px){
+  .mobile{
+    display: initial !important;
+  }
+}
+
+/* Styles for the body of the page. */
+html {
+  height: 100%;
+}
+
+body{
+  height: 100%;
+  padding: 0;
+  margin: 0;
+  background-color: #000000;
+  font-size: 110%;
+  min-height: 100vh;
+  flex-direction: column;
+  display: flex;
+}
+@media screen and (max-width : 800px){
+  body{
+    font-size: 120%;
+  }
+}
+
+/* Style for all links. */
+a{
+  color: #555555;
+  text-shadow: 0 0 0.5em #7777ff;
+  text-decoration: none;
+}
+
+p{
+  text-indent: 1em;
+}
+
+/* Styles for elements that look and act as links. */
+.fake_a{
+  color: #555555;
+  text-shadow: 0 0 0.5em #7777ff;
+  text-decoration: none;
+  cursor: pointer;
+}
+
+
+/* Bold elements. */
+.bold{
+  font-weight: bold;
+}
+
+/* Elements that look and act as buttons or links.*/
+.pointer {
+  cursor: pointer;
+}
+
+/* Hidden elements. */
+.hidden{
+  display: none;
+}
+
+img.slider{
+  width: 1em;
+  height: 1em;
+  transition: all .3s ease-in-out;
+}
+
+div#cover{
+  display: none;
+  background-color: #000000bb;
+  opacity: 0;
+  transition: opacity .3s ease-in-out;
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+
+/* The contact form */
+div#contact_bg{
+  display: none;
+  background: #000000;
+  opacity: 0.6;
+  transition: all .4s ease-in-out;
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+section#contact{
+  display: none;
+  position: fixed;
+  top: 20%;
+  width: 60%;
+  margin: auto;
+  left: 20%;
+}
+@media screen and (max-width : 800px){
+  section#contact{
+    width: 90%;
+    top: 15%;
+    left: 5%;
+  }
+}
+section#contact h3 img#contact_close{
+  float:right;
+  width: 1em;
+  height: 1em;
+}
+section#contact div.contact_buttons{
+  text-align: center;
+}
+section#contact div.contact_buttons input{
+  margin: 1em;
+}
+@media screen and (max-width : 800px){
+  section#contact div.contact_buttons input{
+    margin: 0.3em;
+  }
+}
+section#contact div#contact_menu{
+  height: 18em;
+}
+section#contact div#contact_menu table{
+  width: 100%;
+}
+section#contact div#contact_menu table td.label{
+  width: 8em;
+  text-align: right;
+  padding-right: 1em;
+  vertical-align: top;
+}
+section#contact div#contact_menu table input[type=text]{
+  width: 18em;
+}
+@media screen and (max-width : 800px){
+  section#contact div#contact_menu table td.label{
+    width: initial;
+    padding-right: 0.5em;
+    padding-bottom: 1em;
+  }
+  section#contact div#contact_menu table input[type=text]{
+    width: 90%;
+  }
+}
+section#contact div#contact_menu table textarea{
+  width: 90%;
+  height: 5em;
+  margin: auto;
+}
+section#contact div#contact_menu span.contact_error {
+  font-weight: bold;
+  color: #581010;
+  background-color: #E2AAAA;
+  border: 0.2em solid #E02323;
+  border-radius: 0.429em;
+  padding: 0.2em 0.5em;
+  text-shadow: 0 0 0.15em #0003;
+  cursor: pointer;
+  font-size: 80%;
+  margin-top: -0.5em;
+  margin-left: 1em;
+  transition: all .2s ease-in-out;
+}
+section#contact div#contact_progress{
+  height: 18em;
+  text-align: center;
+}
+section#contact div#contact_progress img{
+  margin-top: 6em;
+  width: 3em;
+  height: 3em;
+  animation: contact_progress 4s infinite linear;
+}
+@keyframes contact_progress {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(359deg);
+  }
+}
+section#contact div#contact_confirmation{
+  height: 18em;
+  text-align: center;
+}
+section#contact div#contact_confirmation p{
+  padding-top: 2em;
+  padding-bottom: 2em;
+  font-size: 150%;
+  font-weight: bold;
+}
+main{
+  background-color: #000000;
+  width: 100%;
+  max-width: 90em;
+  margin: 2em auto;
+  padding: 0;
+  text-align: center;
+  flex: 1;
+}
+@media screen and (max-width : 800px){
+  main{
+    width: 100%;
+    margin: 1em auto;
+  }
+}
+
+section{
+  text-align: left;
+  margin: 0.5em;
+  padding: 1em;
+  background-color: #f0f0f0;
+  border: 0.1em solid #003300;
+  overflow: hidden;
+  border-top-left-radius: 0.5em;
+  border-top-right-radius: 0.5em;
+  border-bottom-left-radius: 1.5em;
+  border-bottom-right-radius: 1.5em;
+}
+
+h3{
+  position: relative;
+  font-weight: bold;
+  color: #ffffff;
+  background-color: #60ae29;
+  border-bottom: 0.1em solid #003300;
+  font-size: 150%;
+  padding: 0.5em 2em 0.2em 3em;
+  margin: -0.8em -1em 1em -1em;
+  font-variant: small-caps;
+  font-family: 'Mono'
+}
+
+h3:before {
+  content: " /";
+  opacity: 0.3;
+}
+
+@media screen and (max-width : 800px){
+  h3{
+    font-size: 120%;
+    padding: 1em 2em 0.2em 1.5em;
+    margin-top: -1.6em;
+  }
+}
+
+/* .entry is a style only class to show content. Must be allways children os a .lighted element */
+section article{
+  border: 0.1em solid #aaffaa00;
+  border-radius: 0.3em;
+  padding: 0.5em;
+  margin: 0.5em;
+  transition: all .4s ease-in-out;
+}
+@media screen and (max-width : 800px){
+  section article{
+    border-radius: 0.3em;
+    padding: 0.2em;
+    margin: 0.2em;
+  }
+}
+h4{
+  font-variant: small-caps;
+  font-weight: bold;
+  font-family: 'Mono';
+  text-decoration-color: #33553377;
+  font-size: 140%;
+  margin: 0.2em auto 1em auto;
+}
+@media screen and (max-width : 800px){
+  h4{
+    font-size: 120%;
+  }
+}
+
+section section_list:hover{
+  background-color: #efffef;
+  border: 0.1em solid #aaffaaff;
+}
+
+
+
+/* Style for the mathces in search results */
+span.result_counter{
+  margin-left: 1.2em;
+  margin-bottom: 0.5em;
+  font-weight: bold;
+  font-style: italic;
+}
+
+/* Shown when no records on database */
+div.unavailable{
+  margin-left: 4em;
+  font-style: italic;
+}
+
+
+/**********************************
+ * Styles to be applied to some   *
+ * horizontally scrollable        *
+ * elements                       *
+ **********************************/
+
+/* Horizontall scrollbar that apears when a project has lots of renders */
+.slider::-webkit-scrollbar{
+  width: 0.5em;
+  height: 0.25em;
+  background-color: #444444;
+}
+.slider::-webkit-scrollbar-thumb{
+  background-color: #cccccc;
+  border-radius: 0.714em;
+}
+.slider::-webkit-scrollbar-thumb:hover{
+  background-color: #cccccc;
+  border: 0.143em solid #000000;
+}
+.slider::-webkit-scrollbar-thumb:active{
+  background-color: #cccccc;
+  border: 0.143em solid #000000;
+}
+
+/* Icons for sharing page in a social network */
+img.social_icon{
+  width: 3em;
+  height: 3em;
+}
+
+/* Elements that should have the cursor as pointer */
+.pointer{
+  cursor: pointer;
+}
+
+
+
+/**********************************
+ * Styles for diverse input       *
+ * elements. Type-assigned        *
+ **********************************/
+
+/* Font for all the input elements */
+input{
+  font-family: 'Mono';
+}
+
+/* Style for the "submit" type elements in forms */
+input[type=submit]{
+  font-weight: bold;
+  color: #cccccc;
+  background-color: #555555;
+  border: 0.143em solid #bbbbbb;
+  border-radius: 0.429em;
+  padding: 0.5em;
+  text-shadow: 0 0 0.75em #000000;
+  cursor: pointer;
+}
+
+/* Style forr buttons */
+input[type=button], input[type=submit]{
+  cursor: pointer;
+  display: inline-block;
+  color: #dddddd;
+  background-color: #278c3a;
+  border: 0.143em solid #66bf38;
+  border-radius: 0.429em;
+  padding: 0.5em 0.8em;
+  text-shadow: 0 0 0.75em #000000;
+  text-decoration: none;
+  font-family: 'Nunito';
+  font-weight: bold;
+  font-variant: small-caps;
+  letter-spacing: 0.07em;
+  font-size: 110%;
+  box-shadow: inset 0 0 0.2em #082400;
+  text-transform: capitalize;
+}
+@media screen and (max-width : 800px){
+  input[type=button], input[type=submit]{
+    /*font-size: 120%;
+     *        padding: 0.9em 1.4em;*/
+  }
+}
+input[type=button]:hover{
+  color: #ffffff;
+  background-color: #34b94d;
+  box-shadow: inset 0 0 0.1em #000;
+  border: 0.143em solid #285710;
+}
+
+/* Style for textboxes */
+input[type=text]{
+  font-weight: bold;
+  color: #224422;
+  background-color: #bbccbb;
+  border: 0.286em solid #ddeedd;
+  border-color: #dddddd;
+  border-radius: 0.429em;
+  padding: 0.25em;
+  text-shadow: 0 0 0.5em #ffffff;
+}
+
+input[type=checkbox]{
+  width: 1em;
+  height: 1em;
+  border-radius: 0.2em;
+  border: 0.063em solid #005500;
+}
+
+/* Style for textareas */
+textarea{
+  width: 80%;
+  color: #224422;
+  background-color: #bbccbb;
+  border: 0.286em solid #ddeedd;
+  border-color: #dddddd;
+  border-radius: 0.429em;
+  padding: 0.25em;
+  text-shadow: 0 0 0.5em #ffffff;
+  resize: none;
+}
+
+/* Style for selects */
+select{
+  font-weight: bold;
+  color: #444444;
+  background-color: #bbbbbb;
+  border: 0.286em solid #dddddd;
+  border-radius: 0.286em;
+  padding: 0.25em;
+  text-shadow: 0 0 0.5em #ffffff;
+}
+
+/* Style for placeholders in input fields */
+::-webkit-input-placeholder { /* WebKit browsers */
+  color: #777777;
+}
+:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
+  color: #777777;
+}
+::-moz-placeholder { /* Mozilla Firefox 19+ */
+  color: #777777;
+}
+:-ms-input-placeholder { /* Internet Explorer 10+ */
+  color: #777777;
+}
+
+/* Style-only class user to indicate that a field is incorrect. Ususally this class is added to an element with js */
+.error{
+  border-color: #ffaaaa;
+}
+
+/* Style for the textbox input when the entered text is not the expected one */
+input[type=text].error{
+  border-color: #ffaaaa;
+}
+
+/* Style for the textared input when the entered text is not the expected one */
+textarea.error{
+  border-color: #ffaaaa;
+}
+
+/* Style for buttons that act as links */
+a.a_button{
+  display: inline-block;
+  color: #dddddd;
+  background-color: #278c3a;
+  border: 0.143em solid #66bf38;
+  border-radius: 0.429em;
+  padding: 0.5em 0.8em;
+  text-shadow: 0 0 0.75em #000000;
+  text-decoration: none;
+  font-family: 'Mono';
+  font-weight: bold;
+  font-variant: small-caps;
+  letter-spacing: 0.07em;
+  font-size: 110%;
+  box-shadow: inset 0 0 0.2em #082400;
+  text-transform: capitalize;
+  transition: all .3s ease-in-out;
+}
+@media screen and (max-width : 800px){
+  a.a_button{
+    font-size: 100%;
+    padding: 0.3em 0.2em;
+  }
+}
+a.a_button:hover{
+  color: #ffffff;
+  background-color: #34b94d;
+  box-shadow: inset 0 0 0.1em #000;
+  border: 0.143em solid #285710;
+}
+
+/**********************************
+ * Styles for comment-related     *
+ * elements                       *
+ **********************************/
+
+/* Contains fields to identify the user before the comment can be posted. Hidden by css, must be displayed with js */
+div#identification_form{
+  display: none;
+}
+
+/* Contain a comment, with the details and the text */
+div.comment{
+  background-color: #efffef;
+  border: 0.1em solid #aaffaa;
+  border-radius: 0.3em;
+  padding: 0.5em;
+  margin: 0.5em;
+}
+
+/* User who posted the coment */
+div.comment h4.comment_user{
+  margin-top: 0.417em;
+  margin-bottom: 0;
+}
+
+div.comment h4.comment_user span.comment_user_registered{
+  font-weight: bold;
+  font-size: 110%;
+  color: #005500;
+}
+
+/* Style for the comment's images */
+img.comment_image{
+  border: 0.077em solid black;
+  max-height: 6em;
+  max-width: 60%;
+  display: block;
+  border: 0.133em solid black;
+  margin-left: 0.938em;
+  margin-bottom: 0.938em;
+  box-shadow: 0 0 1em #444444;
+}
+
+/* Paragraph with the comment text */
+p.comment_text{
+  width: auto;
+  display: block;
+  margin-top: 0.938em;
+  margin-left: 0.938em;
+}
+
+span.comment_date{
+  font-size: 80%;
+  font-style: italic;
+}
+
+/* Input field for the comment text */
+textarea#new_comment_text{
+  width: 98%;
+}
+
+/* Input field for the comment user */
+input#new_comment_user{
+  width: 47%;
+}
+
+/* Input field for the comment email */
+input#new_comment_email{
+  width: 47%;
+}
+
+/* Styles so the input field to upload an image with the comment looks like a button */
+label.image_upload {
+  font-weight: bold;
+  color: #cccccc;
+  background-color: #555555;
+  border: 0.067em solid #bbbbbb;
+  border-radius: 0.2em;
+  padding: 0.5em;
+  text-shadow: 0 0 0.750em #000000;
+  font-size: 0.813em; /*Same as buttons*/
+  font-family: 'Nunito';
+}
+
+label.image_upload input {
+  width: 0 !important;
+  opacity: 0 !important;
+  overflow: hidden !important;
+}
+
+/* Previsualization for the upload image in a comment */
+img#new_comment_image_preview{
+  max-height: 7.692em;
+  max-width: 11.538em;
+}
+
+
+/**********************************
+ * Styles for the social buttons. *
+ **********************************/
+
+a.share img.share{
+  display: inline-block;
+  vertical-align: top;
+  overflow: hidden;
+  width: 1.6em;
+  height: 1.6em;
+  border: 0.1em solid #003300;
+  border-radius: 2em;
+  box-shadow: 0 0 0.3em #005500;
+  margin: 0.2em;
+  transition: all .3s ease-in-out;
+}
+
+a.share img.share:hover{
+  box-shadow: 0 0 0.7em #005500;
+  border-radius: 0.7em;
+}
+
+span.technology{
+  display: inline-block;
+}
+
+span.technology img.technology_logo{
+  display: inline-block;
+  position:relative;
+  vertical-align: middle;
+  border-radius: 50%;
+  border-width: 0.2em;
+  z-index: 2;
+  background-color: #ffffff;
+  border-style: solid;
+  border-color: #000000;
+  height: 1.5em;
+  width: 1.5em;
+}
+span.technology span.technology_name{
+  display: inline-block;
+  position:relative;
+  vertical-align: middle;
+  border-radius: 15%;
+  border-width: 0.2em;
+  border-style: solid;
+  padding: 0.1em 0.2em 0.1em 1.1em;
+  z-index: 1;
+  background-color: #ffffff;
+  border-color: #000000;
+  margin-left: -1.3em;
+  font-size: 70%;
+  font-weight: bold;
+}
+
+span.technology_1 span.technology_name{
+  background-color: #f4a4a4;
+}
+span.technology_2 span.technology_name{
+  background-color: #ffc898;
+}
+span.technology_3 span.technology_name{
+  background-color: #a4c3f4;
+}
+span.technology_4 span.technology_name{
+  background-color: #f4a4e4;
+}
+span.technology_5 span.technology_name{
+  background-color: #a4f4a9;
+}
+span.technology_6 span.technology_name{
+  background-color: #f4e8a4;
+}
+
+
+/* Project icons */
+img.project_image{
+  padding: 0.5em;
+  max-width: 9em;
+  max-height: 9em;
+  background-color: #ffffff;
+  box-shadow: 0 0 0 0.1em #000000, 0 0 0 0.3em #66bf38, 0 0 0 0.4em #000000, inset 0 0 0.5em #00220077;
+  border-radius: 1em;
+}
+@media screen and (max-width : 1000px){
+  img.project_image{
+    padding: 0.2em;
+    max-width: 13vw;
+    max-height: 13vw;
+    border-radius: 0.5em;
+    box-shadow: 0 0 0 0.05em #000000, 0 0 0 0.15em #66bf38, 0 0 0 0.2em #000000, inset 0 0 0.25em #00220077;
+  }
+}

+ 1 - 0
frontend/inigovalentin/tsconfig.json

@@ -4,6 +4,7 @@
   "compileOnSave": false,
   "compilerOptions": {
     "outDir": "./dist/out-tsc",
+    "allowJs": true,
     "strict": true,
     "noImplicitOverride": true,
     "noPropertyAccessFromIndexSignature": true,

Some files were not shown because too many files changed in this diff