From b99ea273c42ce7ecd5f729777433770949112c1d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Flores?= <raphael.flores@inrae.fr>
Date: Mon, 2 Aug 2021 16:00:17 +0200
Subject: [PATCH 01/16] Fix deployment variable and jobs.

---
 .secrets.baseline | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/.secrets.baseline b/.secrets.baseline
index 58672400..84b78e9c 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -3,7 +3,11 @@
     "files": "frontend/package-lock.json|^.secrets.baseline$",
     "lines": null
   },
+<<<<<<< HEAD
   "generated_at": "2021-08-02T15:23:45Z",
+=======
+  "generated_at": "2021-08-02T14:00:12Z",
+>>>>>>> 1edcdd6 (Fix deployment variable and jobs.)
   "plugins_used": [
     {
       "name": "AWSKeyDetector"
@@ -58,14 +62,18 @@
         "hashed_secret": "dd447c7c799dd4ebaacca8f0ad3da45a097d7211",
         "is_secret": false,
         "is_verified": false,
-        "line_number": 228,
+        "line_number": 204,
         "type": "Base64 High Entropy String"
       },
       {
         "hashed_secret": "8074db38f8a8acec1a147bc5daf2799ff6693fff",
         "is_secret": false,
         "is_verified": false,
+<<<<<<< HEAD
         "line_number": 247,
+=======
+        "line_number": 221,
+>>>>>>> 1edcdd6 (Fix deployment variable and jobs.)
         "type": "Base64 High Entropy String"
       }
     ],
-- 
GitLab


From 56851da214f348c8646f2d89af3aeb6b9e731413 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Flores?= <raphael.flores@inrae.fr>
Date: Mon, 2 Aug 2021 16:53:47 +0200
Subject: [PATCH 02/16] Change cache policy.

---
 .secrets.baseline | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/.secrets.baseline b/.secrets.baseline
index 84b78e9c..cbb5acf0 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -3,11 +3,15 @@
     "files": "frontend/package-lock.json|^.secrets.baseline$",
     "lines": null
   },
+<<<<<<< HEAD
 <<<<<<< HEAD
   "generated_at": "2021-08-02T15:23:45Z",
 =======
   "generated_at": "2021-08-02T14:00:12Z",
 >>>>>>> 1edcdd6 (Fix deployment variable and jobs.)
+=======
+  "generated_at": "2021-08-02T14:53:42Z",
+>>>>>>> 998abab (Change cache policy.)
   "plugins_used": [
     {
       "name": "AWSKeyDetector"
@@ -55,25 +59,37 @@
         "hashed_secret": "2907dcd1b70a82032e52be9b6b804abbb4a7525e",
         "is_secret": false,
         "is_verified": false,
+<<<<<<< HEAD
         "line_number": 127,
+=======
+        "line_number": 125,
+>>>>>>> 998abab (Change cache policy.)
         "type": "Base64 High Entropy String"
       },
       {
         "hashed_secret": "dd447c7c799dd4ebaacca8f0ad3da45a097d7211",
         "is_secret": false,
         "is_verified": false,
+<<<<<<< HEAD
         "line_number": 204,
+=======
+        "line_number": 226,
+>>>>>>> 998abab (Change cache policy.)
         "type": "Base64 High Entropy String"
       },
       {
         "hashed_secret": "8074db38f8a8acec1a147bc5daf2799ff6693fff",
         "is_secret": false,
         "is_verified": false,
+<<<<<<< HEAD
 <<<<<<< HEAD
         "line_number": 247,
 =======
         "line_number": 221,
 >>>>>>> 1edcdd6 (Fix deployment variable and jobs.)
+=======
+        "line_number": 245,
+>>>>>>> 998abab (Change cache policy.)
         "type": "Base64 High Entropy String"
       }
     ],
-- 
GitLab


From 45af6fb4312f8a460bc2d6b63d5d638f35a3c2a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Flores?= <raphael.flores@inrae.fr>
Date: Mon, 2 Aug 2021 17:04:40 +0200
Subject: [PATCH 03/16] Fix ES version

---
 .secrets.baseline | 32 ++++----------------------------
 1 file changed, 4 insertions(+), 28 deletions(-)

diff --git a/.secrets.baseline b/.secrets.baseline
index cbb5acf0..514c5ecc 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -3,15 +3,7 @@
     "files": "frontend/package-lock.json|^.secrets.baseline$",
     "lines": null
   },
-<<<<<<< HEAD
-<<<<<<< HEAD
-  "generated_at": "2021-08-02T15:23:45Z",
-=======
-  "generated_at": "2021-08-02T14:00:12Z",
->>>>>>> 1edcdd6 (Fix deployment variable and jobs.)
-=======
-  "generated_at": "2021-08-02T14:53:42Z",
->>>>>>> 998abab (Change cache policy.)
+  "generated_at": "2021-08-02T15:04:37Z",
   "plugins_used": [
     {
       "name": "AWSKeyDetector"
@@ -59,37 +51,21 @@
         "hashed_secret": "2907dcd1b70a82032e52be9b6b804abbb4a7525e",
         "is_secret": false,
         "is_verified": false,
-<<<<<<< HEAD
-        "line_number": 127,
-=======
-        "line_number": 125,
->>>>>>> 998abab (Change cache policy.)
+        "line_number": 126,
         "type": "Base64 High Entropy String"
       },
       {
         "hashed_secret": "dd447c7c799dd4ebaacca8f0ad3da45a097d7211",
         "is_secret": false,
         "is_verified": false,
-<<<<<<< HEAD
-        "line_number": 204,
-=======
-        "line_number": 226,
->>>>>>> 998abab (Change cache policy.)
+        "line_number": 227,
         "type": "Base64 High Entropy String"
       },
       {
         "hashed_secret": "8074db38f8a8acec1a147bc5daf2799ff6693fff",
         "is_secret": false,
         "is_verified": false,
-<<<<<<< HEAD
-<<<<<<< HEAD
-        "line_number": 247,
-=======
-        "line_number": 221,
->>>>>>> 1edcdd6 (Fix deployment variable and jobs.)
-=======
-        "line_number": 245,
->>>>>>> 998abab (Change cache policy.)
+        "line_number": 246,
         "type": "Base64 High Entropy String"
       }
     ],
-- 
GitLab


From 468379883973563f9cdc8c6ac7ddd250bda059c9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Flores?= <raphael.flores@inrae.fr>
Date: Mon, 2 Aug 2021 17:24:19 +0200
Subject: [PATCH 04/16] Configure docker MTU

---
 .secrets.baseline | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/.secrets.baseline b/.secrets.baseline
index 514c5ecc..58672400 100644
--- a/.secrets.baseline
+++ b/.secrets.baseline
@@ -3,7 +3,7 @@
     "files": "frontend/package-lock.json|^.secrets.baseline$",
     "lines": null
   },
-  "generated_at": "2021-08-02T15:04:37Z",
+  "generated_at": "2021-08-02T15:23:45Z",
   "plugins_used": [
     {
       "name": "AWSKeyDetector"
@@ -51,21 +51,21 @@
         "hashed_secret": "2907dcd1b70a82032e52be9b6b804abbb4a7525e",
         "is_secret": false,
         "is_verified": false,
-        "line_number": 126,
+        "line_number": 127,
         "type": "Base64 High Entropy String"
       },
       {
         "hashed_secret": "dd447c7c799dd4ebaacca8f0ad3da45a097d7211",
         "is_secret": false,
         "is_verified": false,
-        "line_number": 227,
+        "line_number": 228,
         "type": "Base64 High Entropy String"
       },
       {
         "hashed_secret": "8074db38f8a8acec1a147bc5daf2799ff6693fff",
         "is_secret": false,
         "is_verified": false,
-        "line_number": 246,
+        "line_number": 247,
         "type": "Base64 High Entropy String"
       }
     ],
-- 
GitLab


From 8b3c2c742ecb6d8d192a781aa8e20f65144179b0 Mon Sep 17 00:00:00 2001
From: jnizet <jb@ninja-squad.com>
Date: Fri, 13 Aug 2021 11:54:27 +0200
Subject: [PATCH 05/16] chore: remove ide files and simplify gitignore

---
 .gitignore                | 46 +--------------------------------------
 .idea/compiler.xml        | 15 -------------
 .idea/encodings.xml       |  4 ----
 .idea/misc.xml            |  9 --------
 .idea/modules.xml         | 14 ------------
 .idea/vcs.xml             |  6 -----
 backend/src/main/main.iml | 39 ---------------------------------
 backend/src/test/test.iml | 12 ----------
 8 files changed, 1 insertion(+), 144 deletions(-)
 delete mode 100644 .idea/compiler.xml
 delete mode 100644 .idea/encodings.xml
 delete mode 100644 .idea/misc.xml
 delete mode 100644 .idea/modules.xml
 delete mode 100644 .idea/vcs.xml
 delete mode 100644 backend/src/main/main.iml
 delete mode 100644 backend/src/test/test.iml

diff --git a/.gitignore b/.gitignore
index 0648667b..897c8ff3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -95,43 +95,11 @@ local.properties
 *.ipr
 
 # User-specific stuff
-.idea/*
-.idea/**/workspace.xml
-.idea/**/tasks.xml
-.idea/**/usage.statistics.xml
-.idea/**/dictionaries
-.idea/**/shelf
-
-# Generated files
-.idea/**/contentModel.xml
-
-# Sensitive or high-churn files
-.idea/**/dataSources/
-.idea/**/dataSources.ids
-.idea/**/dataSources.local.xml
-.idea/**/sqlDataSources.xml
-.idea/**/dynamic.xml
-.idea/**/uiDesigner.xml
-.idea/**/dbnavigator.xml
-
-# Gradle
-.idea/**/gradle.xml
-.idea/**/libraries
-
-# Gradle and Maven with auto-import
-# When using Gradle or Maven with auto-import, you should exclude module files,
-# since they will be recreated, and may cause churn.  Uncomment if using
-# auto-import.
-# .idea/modules.xml
-# .idea/*.iml
-# .idea/modules
+.idea
 
 # CMake
 cmake-build-*/
 
-# Mongo Explorer plugin
-.idea/**/mongoSettings.xml
-
 # File-based project format
 *.iws
 
@@ -144,21 +112,12 @@ out/
 # JIRA plugin
 atlassian-ide-plugin.xml
 
-# Cursive Clojure plugin
-.idea/replstate.xml
-
 # Crashlytics plugin (for Android Studio and IntelliJ)
 com_crashlytics_export_strings.xml
 crashlytics.properties
 crashlytics-build.properties
 fabric.properties
 
-# Editor-based Rest Client
-.idea/httpRequests
-
-# Android studio 3.1+ serialized cache file
-.idea/caches/build_file_checksums.ser
-
 ### Intellij Patch ###
 # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
 
@@ -167,9 +126,6 @@ fabric.properties
 # .idea/misc.xml
 # *.ipr
 
-# Sonarlint plugin
-.idea/sonarlint
-
 ### Kotlin ###
 # Compiled class file
 *.class
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
deleted file mode 100644
index ca99a00e..00000000
--- a/.idea/compiler.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="CompilerConfiguration">
-    <annotationProcessing>
-      <profile name="Gradle Imported" enabled="true">
-        <outputRelativeToContentRoot value="true" />
-        <processorPath useClasspath="false">
-          <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-configuration-processor/2.1.2.RELEASE/db9671c321defb942a6700fae8a7700a137a25e/spring-boot-configuration-processor-2.1.2.RELEASE.jar" />
-        </processorPath>
-        <module name="faidare.backend.main" />
-      </profile>
-    </annotationProcessing>
-    <bytecodeTargetLevel target="1.8" />
-  </component>
-</project>
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
deleted file mode 100644
index 15a15b21..00000000
--- a/.idea/encodings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="Encoding" addBOMForNewFiles="with NO BOM" />
-</project>
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index ff8249e4..00000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="FrameworkDetectionExcludesConfiguration">
-    <file type="web" url="file://$PROJECT_DIR$" />
-  </component>
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
-    <output url="file://$PROJECT_DIR$/out" />
-  </component>
-</project>
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 3b2a562b..00000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="ProjectModuleManager">
-    <modules>
-      <module fileurl="file://$PROJECT_DIR$/.idea/modules/faidare.iml" filepath="$PROJECT_DIR$/.idea/modules/faidare.iml" />
-      <module fileurl="file://$PROJECT_DIR$/.idea/modules/backend/faidare.backend.iml" filepath="$PROJECT_DIR$/.idea/modules/backend/faidare.backend.iml" />
-      <module fileurl="file://$PROJECT_DIR$/.idea/modules/backend/faidare.backend.main.iml" filepath="$PROJECT_DIR$/.idea/modules/backend/faidare.backend.main.iml" />
-      <module fileurl="file://$PROJECT_DIR$/.idea/modules/backend/faidare.backend.test.iml" filepath="$PROJECT_DIR$/.idea/modules/backend/faidare.backend.test.iml" />
-      <module fileurl="file://$PROJECT_DIR$/.idea/modules/frontend/faidare.frontend.iml" filepath="$PROJECT_DIR$/.idea/modules/frontend/faidare.frontend.iml" />
-      <module fileurl="file://$PROJECT_DIR$/backend/src/main/main.iml" filepath="$PROJECT_DIR$/backend/src/main/main.iml" />
-      <module fileurl="file://$PROJECT_DIR$/backend/src/test/test.iml" filepath="$PROJECT_DIR$/backend/src/test/test.iml" />
-    </modules>
-  </component>
-</project>
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index 94a25f7f..00000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="VcsDirectoryMappings">
-    <mapping directory="$PROJECT_DIR$" vcs="Git" />
-  </component>
-</project>
\ No newline at end of file
diff --git a/backend/src/main/main.iml b/backend/src/main/main.iml
deleted file mode 100644
index 50f3d6bc..00000000
--- a/backend/src/main/main.iml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<module type="JAVA_MODULE" version="4">
-  <component name="NewModuleRootManager" inherit-compiler-output="true">
-    <exclude-output />
-    <content url="file://$MODULE_DIR$">
-      <sourceFolder url="file://$MODULE_DIR$/java" isTestSource="false" />
-    </content>
-    <orderEntry type="inheritedJdk" />
-    <orderEntry type="sourceFolder" forTests="false" />
-    <orderEntry type="library" name="Gradle: io.swagger:swagger-annotations:1.5.21" level="project" />
-    <orderEntry type="library" name="Gradle: org.springframework:spring-beans:5.1.4.RELEASE" level="project" />
-    <orderEntry type="library" name="Gradle: org.springframework:spring-web:5.1.4.RELEASE" level="project" />
-    <orderEntry type="library" name="Gradle: io.springfox:springfox-core:2.9.2" level="project" />
-    <orderEntry type="library" name="Gradle: io.springfox:springfox-spring-web:2.9.2" level="project" />
-    <orderEntry type="library" name="Gradle: javax.validation:validation-api:2.0.1.Final" level="project" />
-    <orderEntry type="library" name="Gradle: com.google.guava:guava:27.0.1-jre" level="project" />
-    <orderEntry type="library" name="Gradle: commons-collections:commons-collections:3.2.2" level="project" />
-    <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpcore:4.4.10" level="project" />
-    <orderEntry type="library" name="Gradle: org.elasticsearch.client:elasticsearch-rest-client:6.4.3" level="project" />
-    <orderEntry type="library" name="Gradle: org.apache.httpcomponents:httpcore-nio:4.4.10" level="project" />
-    <orderEntry type="library" name="Gradle: org.springframework.boot:spring-boot-autoconfigure:2.1.2.RELEASE" level="project" />
-    <orderEntry type="library" name="Gradle: org.springframework:spring-context:5.1.4.RELEASE" level="project" />
-    <orderEntry type="library" name="Gradle: com.fasterxml.jackson.core:jackson-databind:2.9.8" level="project" />
-    <orderEntry type="library" name="Gradle: org.springframework.boot:spring-boot:2.1.2.RELEASE" level="project" />
-    <orderEntry type="library" name="Gradle: org.springframework.security:spring-security-config:5.1.3.RELEASE" level="project" />
-    <orderEntry type="library" name="Gradle: io.springfox:springfox-spi:2.9.2" level="project" />
-    <orderEntry type="library" name="Gradle: io.springfox:springfox-swagger2:2.9.2" level="project" />
-    <orderEntry type="library" name="Gradle: org.elasticsearch:elasticsearch:6.5.4" level="project" />
-    <orderEntry type="library" name="Gradle: org.slf4j:slf4j-api:1.7.25" level="project" />
-    <orderEntry type="library" name="Gradle: org.elasticsearch.client:elasticsearch-rest-high-level-client:6.5.4" level="project" />
-    <orderEntry type="library" name="Gradle: org.elasticsearch:elasticsearch-core:6.5.4" level="project" />
-    <orderEntry type="library" name="Gradle: org.springframework:spring-core:5.1.4.RELEASE" level="project" />
-    <orderEntry type="library" name="Gradle: org.apache.tomcat.embed:tomcat-embed-core:9.0.14" level="project" />
-    <orderEntry type="library" name="Gradle: com.opencsv:opencsv:4.4" level="project" />
-    <orderEntry type="library" name="Gradle: org.apache.commons:commons-lang3:3.8.1" level="project" />
-    <orderEntry type="library" name="Gradle: org.apache.lucene:lucene-join:7.5.0" level="project" />
-    <orderEntry type="library" name="Gradle: com.fasterxml.jackson.core:jackson-annotations:2.9.0" level="project" />
-  </component>
-</module>
\ No newline at end of file
diff --git a/backend/src/test/test.iml b/backend/src/test/test.iml
deleted file mode 100644
index 6e30bb1c..00000000
--- a/backend/src/test/test.iml
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<module type="JAVA_MODULE" version="4">
-  <component name="NewModuleRootManager" inherit-compiler-output="true">
-    <exclude-output />
-    <content url="file://$MODULE_DIR$">
-      <sourceFolder url="file://$MODULE_DIR$/java" isTestSource="true" />
-    </content>
-    <orderEntry type="inheritedJdk" />
-    <orderEntry type="sourceFolder" forTests="false" />
-    <orderEntry type="module" module-name="main" />
-  </component>
-</module>
-- 
GitLab


From 01d4324a53d8ceec916037ae0333789535dceebe Mon Sep 17 00:00:00 2001
From: jnizet <jb@ninja-squad.com>
Date: Fri, 13 Aug 2021 12:27:13 +0200
Subject: [PATCH 06/16] feat: setup thymeleaf

- remove the frontend project from gradle
- remove the angular filter
- create a basic layout and home page using thymeleaf
---
 backend/build.gradle.kts                      |   5 +-
 .../faidare/filter/AngularRouteFilter.java    | 115 ------------------
 .../inra/urgi/faidare/web/HomeController.java |  19 +++
 .../static/assets/images/favicon.ico          | Bin 0 -> 1150 bytes
 .../src/main/resources/templates/index.html   |  18 +++
 .../main/resources/templates/layout/main.html |  29 +++++
 settings.gradle.kts                           |   2 +-
 7 files changed, 68 insertions(+), 120 deletions(-)
 delete mode 100644 backend/src/main/java/fr/inra/urgi/faidare/filter/AngularRouteFilter.java
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/HomeController.java
 create mode 100644 backend/src/main/resources/static/assets/images/favicon.ico
 create mode 100644 backend/src/main/resources/templates/index.html
 create mode 100644 backend/src/main/resources/templates/layout/main.html

diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts
index 9ff14054..586fc953 100644
--- a/backend/build.gradle.kts
+++ b/backend/build.gradle.kts
@@ -57,12 +57,8 @@ tasks {
 
     val bootJar by getting(BootJar::class) {
         archiveName = "${rootProject.name}.jar"
-        dependsOn(":frontend:assemble")
         dependsOn(buildInfo)
 
-        into("BOOT-INF/classes/static") {
-            from("${project(":frontend").projectDir}/dist/frontend")
-        }
         into("BOOT-INF/classes/META-INF") {
             from(buildInfo.destinationDir)
         }
@@ -98,6 +94,7 @@ dependencies {
     implementation("org.springframework.boot:spring-boot-starter-actuator")
     implementation("org.springframework.boot:spring-boot-starter-security")
     implementation("org.springframework.cloud:spring-cloud-starter-config")
+    implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
 
     // Elasticsearch
     implementation("org.elasticsearch:elasticsearch:6.6.2")
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/filter/AngularRouteFilter.java b/backend/src/main/java/fr/inra/urgi/faidare/filter/AngularRouteFilter.java
deleted file mode 100644
index 81978e5f..00000000
--- a/backend/src/main/java/fr/inra/urgi/faidare/filter/AngularRouteFilter.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package fr.inra.urgi.faidare.filter;
-
-import com.google.common.base.Charsets;
-import com.google.common.io.ByteSource;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.core.io.ResourceLoader;
-import org.springframework.stereotype.Component;
-
-import javax.servlet.*;
-import javax.servlet.annotation.WebFilter;
-import javax.servlet.http.HttpServletRequest;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.Arrays;
-
-/**
- * Filter that intercepts all request to potential Angular routes
- * (ex: /studies/ID) to send back the Angular `index.html` file with a correct
- * base href set to the spring server context path.
- *
- * Potential angular routes are devised by process of elimination:
- * - They should be GET requests
- * - They should not end with common static file suffixes {@link AngularRouteFilter#STATIC_SUFFIXES}
- * - They should not start with API prefixes {@link AngularRouteFilter#API_PREFIXES}
- *
- * <p>
- * Adapted from data-discovery
- *
- * @author gcornut
- */
-@Component
-@WebFilter("/*")
-public class AngularRouteFilter implements Filter {
-
-    private static final String[] API_PREFIXES = {
-        "/brapi/v1", "/faidare/v1", "/actuator", "/v2/api-docs", "/swagger-resources"
-    };
-
-    private static final String[] STATIC_SUFFIXES = {
-        ".html", ".js", ".css", ".ico", ".png", ".jpg", ".gif", ".eot", ".svg",
-        ".woff2", ".ttf", ".woff", ".md"
-    };
-
-    @Value("${server.servlet.context-path}")
-    private String serverContextPath;
-
-    private final ResourceLoader resourceLoader;
-
-    @Autowired
-    public AngularRouteFilter(ResourceLoader resourceLoader) {
-        this.resourceLoader = resourceLoader;
-    }
-
-    @Override
-    public void doFilter(
-        ServletRequest req,
-        ServletResponse response,
-        FilterChain chain
-    ) throws IOException, ServletException {
-        HttpServletRequest request = (HttpServletRequest) req;
-
-        if (isAngularRoute(request)) {
-            // Angular route
-            InputStream inputStream = resourceLoader.getResource("classpath:static/index.html").getInputStream();
-
-            ByteSource byteSource = new ByteSource() {
-                @Override
-                public InputStream openStream() {
-                    return inputStream;
-                }
-            };
-
-            String content = byteSource.asCharSource(Charsets.UTF_8).read();
-            String replacedContent = content.replace(
-                "<base href=\"./\">",
-                "<base href=\"" + serverContextPath + "/\">"
-            );
-            response.getWriter().write(replacedContent);
-            return;
-        }
-
-        // Otherwise nothing to do
-        chain.doFilter(request, response);
-    }
-
-    private boolean isAngularRoute(HttpServletRequest request) {
-        if (!request.getMethod().equals("GET")) {
-            return false;
-        }
-
-        String fullUri = request.getRequestURI();
-        String contextPath = request.getContextPath();
-        String uri = fullUri.substring(contextPath.length());
-
-        return !isApiOrStaticResource(uri);
-    }
-
-    private boolean isApiOrStaticResource(String relativePath) {
-        // Starts with API prefix
-        return Arrays.stream(API_PREFIXES).anyMatch(relativePath::startsWith)
-            // or has static file suffix
-            || Arrays.stream(STATIC_SUFFIXES).anyMatch(relativePath::endsWith);
-    }
-
-    @Override
-    public void init(FilterConfig filterConfig) {
-        // nothing to do
-    }
-
-    @Override
-    public void destroy() {
-        // nothing to do
-    }
-}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/HomeController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/HomeController.java
new file mode 100644
index 00000000..6734b6aa
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/HomeController.java
@@ -0,0 +1,19 @@
+package fr.inra.urgi.faidare.web;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * Controller for the home page, which doesn't display much
+ * @author JB Nizet
+ */
+@Controller
+@RequestMapping("")
+public class HomeController {
+    @GetMapping
+    public ModelAndView home() {
+        return new ModelAndView("index");
+    }
+}
diff --git a/backend/src/main/resources/static/assets/images/favicon.ico b/backend/src/main/resources/static/assets/images/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..7fc066f132eb58eeaf30b6259da59f5ed92aca8e
GIT binary patch
literal 1150
zcma))OH30%7{|vT5{MBn!Pm(X(TkCTfe;f98so)_4+sz%j3J5-f{8{63W-1fHIYXF
zr3Hdg5J4&BEp*GHg;E}+yb7fhDlD`VTG|5JZg;6O#Yq}Y?B?75_y5iJ&Fsz&07u{*
z6a?V&Ja~K*fKvc~3osypWS>I;0FE8J#62mJ^^K_rx7!VHSa;3m7EPLKG(-DghScud
z4OVUFuiI%yFy_LRyJW5Co_WKv5Qdm~SswFA^u&?boa`=;<f*Nc^(^8#3mKl&U1W;t
z%y-8UeADcf@e1N_?Z_J89lM}4!KgPVZ<V$_+nm(PI3~Lx^yiqMx>wu#l4e|fsS>X4
zw?>-IpCPycts;ZdSEF-6j^-eZ${Ur!XsV<!mK~jMe}NbZhGl%v`ts#*PQ|#e3bA^v
zJRNE7gyee%IUkQ}msS<Gd7=ax?Q#TQhLt~3_v^29?J~o%K56nF17^qenK`Q|EL}y^
z;~FQub3)XgDaPJgKT69K$agM{Jx!5T&2XoQGInndBo2v&aGk|Qh7zNt71(?0XBl6L
zyo{WVLGLeFor^&}X}HzHQEsuVgi1!g@Vxs_&hD|GV4%DY^L$l%4c<5Je#&V(*&@qn
z!*y7<;QsN3YZ{TCFfHDEATBZ2EeyX!dyyy?dA<wR)^@*|Vpx~2YN<KJN!tSz(*-;w
zIT6(|+m(rG8Z`ayYc;a$O3gs-l*JH{HQDR=C%B&&<MC$n{hR@HSz)dCzD>W@7hO8Y
zm*~v;NXq3r?wLb7l!ml$M#?4DbWyk$scB1p>1*3_<MM{)D$O_p{>-*#7xfp*(U@SW
zgS$ghfewcDhvzpiu1~N;F)n}aev~UTZLdExJz}$3Qs|t{^mT*Ha1rqaHIXNrmwL`l
zH9UXi8hP%)UhG#kwGm4F;6oc_J&l>wu^o~uE+*%<*1|bGm88YF8-m9Ut4-R$)VQu9
zs$Mtp=CD29buN}UR-kIHrdh@p^9(lt5z7FSOaq`?0Kn1)Lj;5PpF?l{27vJ$0K4%3
MIAD$21cTa-f5P$oj{pDw

literal 0
HcmV?d00001

diff --git a/backend/src/main/resources/templates/index.html b/backend/src/main/resources/templates/index.html
new file mode 100644
index 00000000..ae925eac
--- /dev/null
+++ b/backend/src/main/resources/templates/index.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+
+<html
+  xmlns:th="http://www.thymeleaf.org"
+  xmlns:biom="http://www.thymeleaf.org"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
+>
+<head>
+  <title>Faidare</title>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+</head>
+
+<body>
+<main>
+  <h1>Welcome to Faidare</h1>
+</main>
+</body>
+</html>
diff --git a/backend/src/main/resources/templates/layout/main.html b/backend/src/main/resources/templates/layout/main.html
new file mode 100644
index 00000000..a8f2c38c
--- /dev/null
+++ b/backend/src/main/resources/templates/layout/main.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html lang="fr" th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
+  <head>
+    <title th:replace="${title}">Layout Title</title>
+
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <meta content="width=device-width, initial-scale=1" name="viewport" />
+
+    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
+
+    <link rel="shortcut icon" th:href="@{/static/assets/images/favicon.ico}" type="image/x-icon" />
+  </head>
+
+  <body>
+    <div class="container">
+      <header>
+        Common header
+      </header>
+
+      <div th:replace="${content}">
+        <p>Layout content</p>
+      </div>
+
+      <footer>
+        common footer
+      </footer>
+    </div>
+  </body>
+</html>
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 4bd5a482..59e6a72b 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,2 +1,2 @@
 rootProject.name = "faidare"
-include("backend", "frontend")
+include("backend")
-- 
GitLab


From 9675514a0ce17ec848eaa39a8f01d81a48707d2a Mon Sep 17 00:00:00 2001
From: jnizet <jb@ninja-squad.com>
Date: Wed, 1 Sep 2021 10:32:37 +0200
Subject: [PATCH 07/16] ci: adapt the ci config now that frontend is gone

---
 .gitlab-ci.yml                                |  33 ++----
 .../filter/AngularRouteFilterTest.java        | 105 ------------------
 2 files changed, 7 insertions(+), 131 deletions(-)
 delete mode 100644 backend/src/test/java/fr/inra/urgi/faidare/filter/AngularRouteFilterTest.java

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b5511df7..7df34804 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -30,8 +30,6 @@ cache:
   key: "$CI_COMMIT_REF_NAME"
   paths:
     - ".gradle"
-    - "frontend/.gradle/"
-    - "frontend/node_modules/"
 
 # PRE-BUILD
 
@@ -62,20 +60,6 @@ build-loader-docker-image:
 # TESTS
 
 
-lint:
-  stage: test
-  tags:
-    - openstack
-  script: "./gradlew lint"
-  cache:
-    key: "$CI_COMMIT_REF_NAME"
-    policy: pull
-    paths:
-      - ".gradle"
-      - "frontend/.gradle/"
-      - "frontend/node_modules/"
-
-
 test-and-sonarqube:
   stage: test
   tags:
@@ -104,20 +88,19 @@ test-and-sonarqube:
     policy: pull-push
     paths:
       - ".gradle"
-      - "frontend/.gradle/"
-      - "frontend/node_modules/"
       - .sonar/cache
   script:
-    - ./gradlew :frontend:assemble --parallel
-    - ./gradlew :backend:test jacocoTestReport --parallel
-    - find /tmp/node/*/bin -name node -exec ln -s {} /tmp/node/node \;
-    - export PATH="/tmp/node/:$PATH"
-    - ./gradlew -s sonarqube -x test
+    - ./gradlew test jacocoTestReport --parallel
+    # disable sonarqube because it apparently needs node, but I don't know why, and it can't find it anymore now that
+    # there is no frontend project anymore, and it takes sooooo much time to complete anyway for results that nobody
+    # will ever look
+    # - find /tmp/node/*/bin -name node -exec ln -s {} /tmp/node/node \;
+    # - export PATH="/tmp/node/:$PATH"
+    # - ./gradlew -s sonarqube -x test
   artifacts:
     reports:
       junit:
         - ./backend/build/test-results/test/TEST-*.xml
-        # - ./frontend/karma-junit-tests-report/TEST*.xml
   only:
     refs:
       - merge_requests
@@ -144,8 +127,6 @@ build:
     policy: pull
     paths:
       - ".gradle"
-      - "frontend/.gradle/"
-      - "frontend/node_modules/"
   artifacts:
     paths:
       - "$JAR_PATH"
diff --git a/backend/src/test/java/fr/inra/urgi/faidare/filter/AngularRouteFilterTest.java b/backend/src/test/java/fr/inra/urgi/faidare/filter/AngularRouteFilterTest.java
deleted file mode 100644
index b71e9e39..00000000
--- a/backend/src/test/java/fr/inra/urgi/faidare/filter/AngularRouteFilterTest.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package fr.inra.urgi.faidare.filter;
-
-import fr.inra.urgi.faidare.Application;
-import fr.inra.urgi.faidare.config.SecurityConfig;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.boot.test.mock.mockito.MockBean;
-import org.springframework.context.annotation.Import;
-import org.springframework.core.io.Resource;
-import org.springframework.core.io.ResourceLoader;
-import org.springframework.test.context.junit.jupiter.SpringExtension;
-import org.springframework.test.util.ReflectionTestUtils;
-import org.springframework.test.web.servlet.MockMvc;
-import org.springframework.test.web.servlet.setup.MockMvcBuilders;
-import org.springframework.web.context.WebApplicationContext;
-
-import java.io.ByteArrayInputStream;
-
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl;
-
-/**
- * Unit tests for {@link AngularRouteFilter}
- *
- * @author gcornut
- */
-@ExtendWith(SpringExtension.class)
-@Import(SecurityConfig.class)
-@SpringBootTest(classes = Application.class)
-class AngularRouteFilterTest {
-
-    @Autowired
-    private WebApplicationContext context;
-
-    @MockBean
-    private ResourceLoader resourceLoader;
-
-    private MockMvc mockMvc;
-
-    private AngularRouteFilter filter;
-
-    @BeforeEach
-    void setUp() {
-        filter = new AngularRouteFilter(resourceLoader);
-        mockMvc = MockMvcBuilders.webAppContextSetup(context)
-            .addFilter(filter, "/*")
-            .build();
-    }
-
-    @ParameterizedTest
-    @ValueSource(strings = {
-        // Static files
-        "/index.html",
-        "/script.js",
-        "/style.css",
-        "/image.gif",
-        "/icon.ico",
-        "/image.png",
-        "/image.jpg",
-        "/font.woff",
-        "/font.ttf",
-        // APIs
-        "/brapi/v1/studies",
-        "/faidare/v1/datadiscovery/suggest",
-        "/actuator/info",
-    })
-    void shouldNotForward(String url) throws Exception {
-        mockMvc.perform(get(url)).andExpect(forwardedUrl(null));
-    }
-
-    @ParameterizedTest
-    @ValueSource(strings = {
-        "/home",
-        "/studies/foo",
-        "/germplasm/bar",
-    })
-    void shouldForward(String url) throws Exception {
-        String indexBefore = "<html>\n" +
-            "  <base href=\"./\">\n" +
-            "</html>";
-        String indexAfter = "<html>\n" +
-            "  <base href=\"/gnpis-test/faidare/\">\n" +
-            "</html>";
-
-        ReflectionTestUtils.setField(filter, "serverContextPath", "/gnpis-test/faidare");
-
-        Resource mockResource = mock(Resource.class);
-        when(mockResource.getInputStream())
-            .thenReturn(new ByteArrayInputStream(indexBefore.getBytes()));
-        when(resourceLoader.getResource(anyString()))
-            .thenReturn(mockResource);
-
-        mockMvc.perform(get(url))
-            .andExpect(content().string(indexAfter));
-    }
-
-}
-- 
GitLab


From 21d252aed67ed5fa927a1f85eb527918b06bc806 Mon Sep 17 00:00:00 2001
From: jnizet <jb@ninja-squad.com>
Date: Fri, 13 Aug 2021 18:53:38 +0200
Subject: [PATCH 08/16] feat: start working on site card

xrefs missing, latitude/longitude TODO
---
 .../urgi/faidare/web/site/SiteController.java |  61 ++++++++++
 .../inra/urgi/faidare/web/site/SiteModel.java | 106 ++++++++++++++++++
 .../main/resources/static/assets/style.css    |   3 +
 .../resources/templates/fragments/row.html    |  21 ++++
 .../main/resources/templates/layout/main.html |   1 +
 .../src/main/resources/templates/site.html    |  66 +++++++++++
 6 files changed, 258 insertions(+)
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
 create mode 100644 backend/src/main/resources/static/assets/style.css
 create mode 100644 backend/src/main/resources/templates/fragments/row.html
 create mode 100644 backend/src/main/resources/templates/site.html

diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java
new file mode 100644
index 00000000..d39535d8
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java
@@ -0,0 +1,61 @@
+package fr.inra.urgi.faidare.web.site;
+
+import fr.inra.urgi.faidare.api.NotFoundException;
+import fr.inra.urgi.faidare.config.FaidareProperties;
+import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiAdditionalInfo;
+import fr.inra.urgi.faidare.domain.data.LocationVO;
+import fr.inra.urgi.faidare.repository.es.LocationRepository;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * Controller used to display a site card based on its ID.
+ * @author JB Nizet
+ */
+@Controller
+@RequestMapping("/sites")
+public class SiteController {
+
+    private final LocationRepository locationRepository;
+    private final FaidareProperties faidareProperties;
+
+    public SiteController(LocationRepository locationRepository,
+                          FaidareProperties faidareProperties) {
+        this.locationRepository = locationRepository;
+        this.faidareProperties = faidareProperties;
+    }
+
+    @GetMapping("/{siteId}")
+    public ModelAndView site(@PathVariable("siteId") String siteId) {
+        // LocationVO site = locationRepository.getById(siteId);
+        LocationVO site = createSite();
+
+        if (site == null) {
+            throw new NotFoundException("Site with ID " + siteId + " not found");
+        }
+        return new ModelAndView("site",
+                                "model",
+                                new SiteModel(site, faidareProperties.getByUri(site.getSourceUri())));
+    }
+
+    private LocationVO createSite() {
+        LocationVO site = new LocationVO();
+        site.setLocationName("France");
+        site.setSourceUri("https://urgi.versailles.inrae.fr/gnpis");
+        site.setUri("Test URI");
+        site.setUrl("https://google.com");
+        site.setLatitude(45.65);
+        site.setLongitude(1.34);
+        BrapiAdditionalInfo additionalInfo = new BrapiAdditionalInfo();
+        additionalInfo.addProperty("Slope", 4.32);
+        additionalInfo.addProperty("Distance to city", "3 km");
+        additionalInfo.addProperty("foo", "bar");
+        additionalInfo.addProperty("baz", "zing");
+        additionalInfo.addProperty("blob", null);
+        site.setAdditionalInfo(additionalInfo);
+        return site;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
new file mode 100644
index 00000000..839f7446
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
@@ -0,0 +1,106 @@
+package fr.inra.urgi.faidare.web.site;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import fr.inra.urgi.faidare.domain.data.LocationVO;
+import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource;
+
+/**
+ * TODO add javadoc
+ *
+ * @author JB Nizet
+ */
+public final class SiteModel {
+    private static final Set<String> IGNORED_PROPERTIES =
+        new HashSet<>(Arrays.asList("Site status",
+                                    "Coordinates precision",
+                                    "Slope",
+                                    "Exposure",
+                                    "Geographical location",
+                                    "Distance to city",
+                                    "Direction from city",
+                                    "Environment type",
+                                    "Topography",
+                                    "Comment"));
+
+    private final LocationVO site;
+    private final DataSource source;
+    private final Map<String, Object> additionalInfo;
+    private final List<Map.Entry<String, Object>> additionalInfoProperties;
+
+    public SiteModel(LocationVO site, DataSource source) {
+        this.site = site;
+        this.source = source;
+        this.additionalInfo = site.getAdditionalInfo() == null ? Collections.emptyMap() : site.getAdditionalInfo().getProperties();
+        this.additionalInfoProperties =
+            this.additionalInfo
+                .entrySet()
+                .stream()
+                .filter(entry -> !IGNORED_PROPERTIES.contains(entry.getKey()))
+                .filter(entry -> entry.getValue() != null && !entry.getValue().toString().isEmpty())
+                .sorted(Map.Entry.comparingByKey())
+                .collect(Collectors.toList());
+    }
+
+    public LocationVO getSite() {
+        return site;
+    }
+
+    public DataSource getSource() {
+        return source;
+    }
+
+    public Map<String, Object> getAdditionalInfo() {
+        return this.additionalInfo;
+    }
+
+    public Object getSiteStatus() {
+        return this.additionalInfo.get("Site status");
+    }
+
+    public Object getCoordinatesPrecision() {
+        return this.additionalInfo.get("Coordinates precision");
+    }
+
+    public Object getGeographicalLocation() {
+        return this.additionalInfo.get("Geographical location");
+    }
+
+    public Object getSlope() {
+        return this.additionalInfo.get("Slope");
+    }
+
+    public Object getExposure() {
+        return this.additionalInfo.get("Exposure");
+    }
+
+    public Object getTopography() {
+        return this.additionalInfo.get("Topography");
+    }
+
+    public Object getEnvironmentType() {
+        return this.additionalInfo.get("Environment type");
+    }
+
+    public Object getDistanceToCity() {
+        return this.additionalInfo.get("Distance to city");
+    }
+
+    public Object getDirectionFromCity() {
+        return this.additionalInfo.get("Direction from city");
+    }
+
+    public Object getComment() {
+        return this.additionalInfo.get("Comment");
+    }
+
+    public List<Map.Entry<String, Object>> getAdditionalInfoProperties() {
+        return additionalInfoProperties;
+    }
+}
diff --git a/backend/src/main/resources/static/assets/style.css b/backend/src/main/resources/static/assets/style.css
new file mode 100644
index 00000000..59bcd117
--- /dev/null
+++ b/backend/src/main/resources/static/assets/style.css
@@ -0,0 +1,3 @@
+.label {
+    font-weight: 500;
+}
diff --git a/backend/src/main/resources/templates/fragments/row.html b/backend/src/main/resources/templates/fragments/row.html
new file mode 100644
index 00000000..5b523cef
--- /dev/null
+++ b/backend/src/main/resources/templates/fragments/row.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+
+<html xmlns:th="http://www.thymeleaf.org">
+
+<body>
+
+<div th:fragment="row(label, content)" class="row py-2">
+  <div class="col-md-4 label pb-1 pb-md-0" th:text="${label}"></div>
+  <div class="col">
+    <th:block th:replace="${content}" />
+  </div>
+</div>
+
+<div th:fragment="text-row(label, text)" th:if="${!#strings.isEmpty(text)}" class="row py-2">
+  <div class="col-md-4 label pb-1 pb-md-0" th:text="${label}"></div>
+  <div class="col" th:text="${text}"></div>
+</div>
+
+</body>
+
+</html>
diff --git a/backend/src/main/resources/templates/layout/main.html b/backend/src/main/resources/templates/layout/main.html
index a8f2c38c..22bcdfd5 100644
--- a/backend/src/main/resources/templates/layout/main.html
+++ b/backend/src/main/resources/templates/layout/main.html
@@ -7,6 +7,7 @@
     <meta content="width=device-width, initial-scale=1" name="viewport" />
 
     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
+    <link th:href="@{/assets/style.css}" rel="stylesheet">
 
     <link rel="shortcut icon" th:href="@{/static/assets/images/favicon.ico}" type="image/x-icon" />
   </head>
diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html
new file mode 100644
index 00000000..abcd0dbd
--- /dev/null
+++ b/backend/src/main/resources/templates/site.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+
+<html
+  xmlns:th="http://www.thymeleaf.org"
+  xmlns:biom="http://www.thymeleaf.org"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
+>
+<head>
+  <title>Site <th:block th:text="${model.site.locationName}" /></title>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+</head>
+
+<body>
+<main>
+  <h1>Site <th:block th:text="${model.site.locationName}" /></h1>
+
+  <div th:if="${model.site.uri && !model.site.uri.startsWith('urn:')}"
+       th:replace="fragments/row::text-row(label='Permanent unique identifier', text=${model.site.uri})">
+  </div>
+
+  <div th:if="${model.source}"
+       th:replace="fragments/row::row(label='Source', content=~{::#source}, text='')">
+    <a id="source" target="_blank" th:href="${model.source.url}">
+      <img style="max-height: 60px;" th:src="${model.source.image}" th:alt="${model.source.name} + ' logo'" />
+    </a>
+  </div>
+
+  <div th:if="${model.site.url && model.source}"
+       th:replace="fragments/row::row(label='Data link', content=~{::#url}, text='')">
+    <a id="url" target="_blank" th:href="${model.site.url}">
+      Link to this site on <th:block th:text="${model.source.name}" />
+    </a>
+  </div>
+
+  <div th:replace="fragments/row::text-row(label='Abbreviation', text=${model.site.abbreviation})"></div>
+  <div th:replace="fragments/row::text-row(label='Type', text=${model.site.locationType})"></div>
+  <div th:replace="fragments/row::text-row(label='Status', text=${model.siteStatus})"></div>
+  <div th:replace="fragments/row::text-row(label='Institution/Landowner', text=${model.site.instituteName})"></div>
+  <div th:replace="fragments/row::text-row(label='Institution address', text=${model.site.instituteAddress})"></div>
+  <div th:replace="fragments/row::text-row(label='Coordinates precision', text=${model.coordinatesPrecision})"></div>
+  <div th:if="${model.site.latitude}"
+       th:replace="fragments/row::text-row(label='Latitude', text=${model.site.latitude + ' - TODO in degrees'})"></div>
+  <div th:if="${model.site.longitude}"
+       th:replace="fragments/row::text-row(label='Longitude', text=${model.site.longitude + ' - TODO in degrees'})"></div>
+  <div th:replace="fragments/row::text-row(label='Geographical location', text=${model.geographicalLocation})"></div>
+  <div th:if="${model.site.countryName && !model.geographicalLocation}"
+       th:replace="fragments/row::text-row(label='Country name', text=${model.site.countryName})"></div>
+  <div th:if="${model.site.countryCode && !model.geographicalLocation}"
+       th:replace="fragments/row::text-row(label='Country code', text=${model.site.countryName})"></div>
+  <div th:replace="fragments/row::text-row(label='Altitude', text=${model.site.altitude})"></div>
+  <div th:replace="fragments/row::text-row(label='Slope', text=${model.slope})"></div>
+  <div th:replace="fragments/row::text-row(label='Exposure', text=${model.exposure})"></div>
+  <div th:replace="fragments/row::text-row(label='Topography', text=${model.topography})"></div>
+  <div th:replace="fragments/row::text-row(label='Environment type', text=${model.environmentType})"></div>
+  <div th:replace="fragments/row::text-row(label='Distance to city', text=${model.distanceToCity})"></div>
+  <div th:replace="fragments/row::text-row(label='Direction from city', text=${model.directionFromCity})"></div>
+  <div th:replace="fragments/row::text-row(label='Comment', text=${model.comment})"></div>
+
+  <h2>Additional info</h2>
+  <th:block th:each="prop : ${model.additionalInfoProperties}">
+    <div th:replace="fragments/row::text-row(label=${prop.key}, text=${prop.value})"></div>
+  </th:block>
+
+</main>
+</body>
+</html>
-- 
GitLab


From 8ddd89536ed0eb698b832109d79e572ea13109e6 Mon Sep 17 00:00:00 2001
From: jnizet <jb@ninja-squad.com>
Date: Wed, 25 Aug 2021 10:00:40 +0200
Subject: [PATCH 09/16] feat: add xrefs and latitude/longitude to site card

---
 .../xref/XRefDocumentSearchCriteria.java      |  7 ++
 .../urgi/faidare/web/site/SiteController.java | 43 +++++++++--
 .../inra/urgi/faidare/web/site/SiteModel.java | 11 ++-
 .../faidare/web/thymeleaf/Coordinates.java    | 71 +++++++++++++++++++
 .../web/thymeleaf/CoordinatesDialect.java     | 28 ++++++++
 .../CoordinatesExpressionFactory.java         | 36 ++++++++++
 .../resources/templates/fragments/xrefs.html  | 37 ++++++++++
 .../src/main/resources/templates/site.html    | 13 ++--
 8 files changed, 236 insertions(+), 10 deletions(-)
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/Coordinates.java
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesDialect.java
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesExpressionFactory.java
 create mode 100644 backend/src/main/resources/templates/fragments/xrefs.html

diff --git a/backend/src/main/java/fr/inra/urgi/faidare/domain/xref/XRefDocumentSearchCriteria.java b/backend/src/main/java/fr/inra/urgi/faidare/domain/xref/XRefDocumentSearchCriteria.java
index 7ca7b26a..6dde515e 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/domain/xref/XRefDocumentSearchCriteria.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/domain/xref/XRefDocumentSearchCriteria.java
@@ -4,6 +4,7 @@ import fr.inra.urgi.faidare.domain.criteria.base.PaginationCriteriaImpl;
 import fr.inra.urgi.faidare.elasticsearch.criteria.annotation.CriteriaForDocument;
 import fr.inra.urgi.faidare.elasticsearch.criteria.annotation.DocumentPath;
 
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -18,6 +19,12 @@ public class XRefDocumentSearchCriteria extends PaginationCriteriaImpl {
     @DocumentPath("linkedRessourcesID")
 	private List<String> linkedRessourcesID;
 
+    public static XRefDocumentSearchCriteria forXRefId(String resourceId) {
+        XRefDocumentSearchCriteria criteria = new XRefDocumentSearchCriteria();
+        criteria.setLinkedRessourcesID(Collections.singletonList(resourceId));
+        return criteria;
+    }
+
     public String getEntryType() {
         return entryType;
     }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java
index d39535d8..b6fecd50 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java
@@ -1,10 +1,17 @@
 package fr.inra.urgi.faidare.web.site;
 
+import java.util.Arrays;
+import java.util.List;
+
 import fr.inra.urgi.faidare.api.NotFoundException;
 import fr.inra.urgi.faidare.config.FaidareProperties;
 import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiAdditionalInfo;
 import fr.inra.urgi.faidare.domain.data.LocationVO;
+import fr.inra.urgi.faidare.domain.response.PaginatedList;
+import fr.inra.urgi.faidare.domain.xref.XRefDocumentSearchCriteria;
+import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
 import fr.inra.urgi.faidare.repository.es.LocationRepository;
+import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
@@ -21,24 +28,42 @@ public class SiteController {
 
     private final LocationRepository locationRepository;
     private final FaidareProperties faidareProperties;
+    private final XRefDocumentRepository xRefDocumentRepository;
 
     public SiteController(LocationRepository locationRepository,
-                          FaidareProperties faidareProperties) {
+                          FaidareProperties faidareProperties,
+                          XRefDocumentRepository xRefDocumentRepository) {
         this.locationRepository = locationRepository;
         this.faidareProperties = faidareProperties;
+        this.xRefDocumentRepository = xRefDocumentRepository;
     }
 
     @GetMapping("/{siteId}")
     public ModelAndView site(@PathVariable("siteId") String siteId) {
-        // LocationVO site = locationRepository.getById(siteId);
-        LocationVO site = createSite();
+        LocationVO site = locationRepository.getById(siteId);
+
+        // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find(
+        //     XRefDocumentSearchCriteria.forXRefId(site.getLocationDbId()));
+        List<XRefDocumentVO> crossReferences = Arrays.asList(
+            createXref("foobar"),
+            createXref("bazbing")
+        );
+
+        // LocationVO site = createSite();
 
         if (site == null) {
             throw new NotFoundException("Site with ID " + siteId + " not found");
         }
+
+
         return new ModelAndView("site",
                                 "model",
-                                new SiteModel(site, faidareProperties.getByUri(site.getSourceUri())));
+                                new SiteModel(
+                                    site,
+                                    faidareProperties.getByUri(site.getSourceUri()),
+                                    crossReferences
+                                )
+        );
     }
 
     private LocationVO createSite() {
@@ -58,4 +83,14 @@ public class SiteController {
         site.setAdditionalInfo(additionalInfo);
         return site;
     }
+
+    private XRefDocumentVO createXref(String name) {
+        XRefDocumentVO xref = new XRefDocumentVO();
+        xref.setName(name);
+        xref.setDescription("A very large description for the xref " + name + " which has way more than 120 characters bla bla bla bla bla bla bla bla bla bla bla bla");
+        xref.setDatabaseName("db_" + name);
+        xref.setUrl("https://google.com");
+        xref.setEntryType("type " + name);
+        return xref;
+    }
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
index 839f7446..900dca16 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
@@ -10,6 +10,7 @@ import java.util.stream.Collectors;
 
 import fr.inra.urgi.faidare.domain.data.LocationVO;
 import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource;
+import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
 
 /**
  * TODO add javadoc
@@ -32,12 +33,16 @@ public final class SiteModel {
     private final LocationVO site;
     private final DataSource source;
     private final Map<String, Object> additionalInfo;
+    private final List<XRefDocumentVO> crossReferences;
     private final List<Map.Entry<String, Object>> additionalInfoProperties;
 
-    public SiteModel(LocationVO site, DataSource source) {
+    public SiteModel(LocationVO site,
+                     DataSource source,
+                     List<XRefDocumentVO> crossReferences) {
         this.site = site;
         this.source = source;
         this.additionalInfo = site.getAdditionalInfo() == null ? Collections.emptyMap() : site.getAdditionalInfo().getProperties();
+        this.crossReferences = crossReferences;
         this.additionalInfoProperties =
             this.additionalInfo
                 .entrySet()
@@ -103,4 +108,8 @@ public final class SiteModel {
     public List<Map.Entry<String, Object>> getAdditionalInfoProperties() {
         return additionalInfoProperties;
     }
+
+    public List<XRefDocumentVO> getCrossReferences() {
+        return crossReferences;
+    }
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/Coordinates.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/Coordinates.java
new file mode 100644
index 00000000..554929b1
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/Coordinates.java
@@ -0,0 +1,71 @@
+package fr.inra.urgi.faidare.web.thymeleaf;
+
+import java.text.DecimalFormat;
+import java.util.Locale;
+
+/**
+ * The actual object offering Coordinates helper methods to thymeleaf
+ * @author JB Nizet
+ */
+public class Coordinates {
+    private final Locale locale;
+
+    public Coordinates(Locale locale) {
+        this.locale = locale;
+    }
+
+    public String format(Double value) {
+        if (value == null) {
+            return "";
+        }
+        return DecimalFormat.getInstance(locale).format(value);
+    }
+
+    public String formatLatitude(Double value) {
+        if (value == null) {
+            return "";
+        }
+        return this.format(value) + " — " + this.toLatitudeDegrees(value);
+    }
+
+    public String formatLongitude(Double value) {
+        if (value == null) {
+            return "";
+        }
+        return this.format(value) + " — " + this.toLongitudeDegrees(value);
+    }
+
+    public String toLatitudeDegrees(Double latitude) {
+        if (latitude == null) {
+            return "";
+        }
+
+        return toDegrees(latitude) + " " + ((latitude < 0) ? "S" : "N");
+    }
+
+    public String toLongitudeDegrees(Double longitude) {
+        if (longitude == null) {
+            return "";
+        }
+
+        return toDegrees(longitude) + " " + ((longitude < 0) ? "W" : "E");
+    }
+
+    private String toDegrees(double value) {
+        double absoluteDegrees = Math.abs(value);
+        int fullDegrees = (int) absoluteDegrees;
+        double remainingMinutes = (absoluteDegrees - fullDegrees) * 60;
+        int minutes = (int) remainingMinutes;
+        double remainingSeconds = (remainingMinutes - minutes) * 60;
+        int seconds = (int) remainingSeconds;
+        if (seconds == 60) {
+            minutes += 1;
+            seconds = 0;
+        }
+        if (minutes == 60) {
+            fullDegrees += 1;
+            minutes = 0;
+        }
+        return fullDegrees + "°" + minutes + "'" + seconds;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesDialect.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesDialect.java
new file mode 100644
index 00000000..d443e589
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesDialect.java
@@ -0,0 +1,28 @@
+package fr.inra.urgi.faidare.web.thymeleaf;
+
+import org.springframework.stereotype.Component;
+import org.thymeleaf.dialect.AbstractDialect;
+import org.thymeleaf.dialect.IDialect;
+import org.thymeleaf.dialect.IExpressionObjectDialect;
+import org.thymeleaf.expression.IExpressionObjectFactory;
+import org.thymeleaf.extras.java8time.dialect.Java8TimeExpressionFactory;
+
+/**
+ * A thymeleaf dialect allowing to transform coordinates (latitude and longitude)
+ * to degrees.
+ * @author JB Nizet
+ */
+@Component
+public class CoordinatesDialect extends AbstractDialect implements IExpressionObjectDialect {
+
+    private final IExpressionObjectFactory COORDINATES_EXPRESSION_OBJECTS_FACTORY = new CoordinatesExpressionFactory();
+
+    protected CoordinatesDialect() {
+        super("coordinates");
+    }
+
+    @Override
+    public IExpressionObjectFactory getExpressionObjectFactory() {
+        return COORDINATES_EXPRESSION_OBJECTS_FACTORY;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesExpressionFactory.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesExpressionFactory.java
new file mode 100644
index 00000000..e3c25cc8
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/CoordinatesExpressionFactory.java
@@ -0,0 +1,36 @@
+package fr.inra.urgi.faidare.web.thymeleaf;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.thymeleaf.context.IExpressionContext;
+import org.thymeleaf.expression.IExpressionObjectFactory;
+import org.thymeleaf.extras.java8time.expression.Temporals;
+
+/**
+ * The object factory for the {@link CoordinatesDialect}
+ * @author JB Nizet
+ */
+public class CoordinatesExpressionFactory implements IExpressionObjectFactory {
+    private static final String COORDINATES_EVALUATION_VARIABLE_NAME = "coordinates";
+
+    private static final Set<String> ALL_EXPRESSION_OBJECT_NAMES =
+        Collections.singleton(COORDINATES_EVALUATION_VARIABLE_NAME);
+
+    @Override
+    public Set<String> getAllExpressionObjectNames() {
+        return ALL_EXPRESSION_OBJECT_NAMES;
+    }
+
+    @Override
+    public Object buildObject(IExpressionContext context, String expressionObjectName) {
+        return new Coordinates(context.getLocale());
+    }
+
+    @Override
+    public boolean isCacheable(String expressionObjectName) {
+        return true;
+    }
+}
diff --git a/backend/src/main/resources/templates/fragments/xrefs.html b/backend/src/main/resources/templates/fragments/xrefs.html
new file mode 100644
index 00000000..d51f1186
--- /dev/null
+++ b/backend/src/main/resources/templates/fragments/xrefs.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+
+<html xmlns:th="http://www.thymeleaf.org">
+
+<body>
+
+<div th:fragment="xrefs(crossReferences)" th:if="${!#lists.isEmpty(crossReferences)}">
+  <h2>Cross references</h2>
+
+  <div class="table-responsive scroll-big-table table-card-body">
+    <div class="card">
+      <table class="table table-sm table-striped">
+        <thead>
+          <tr>
+            <th scope="col">Name</th>
+            <th scope="col">Source</th>
+            <th scope="col">Type</th>
+            <th scope="col">Description</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr th:each="crossRef : ${crossReferences}">
+            <td><a th:href="${crossRef.url}" target="_blank" th:text="${crossRef.name}"></a></td>
+            <td th:text="${crossRef.databaseName}"></td>
+            <td th:text="${crossRef.entryType}"></td>
+            <td th:text="${#strings.abbreviate(crossRef.description, 120)}"></td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+
+</div>
+
+</body>
+
+</html>
diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html
index abcd0dbd..e9867c75 100644
--- a/backend/src/main/resources/templates/site.html
+++ b/backend/src/main/resources/templates/site.html
@@ -39,9 +39,9 @@
   <div th:replace="fragments/row::text-row(label='Institution address', text=${model.site.instituteAddress})"></div>
   <div th:replace="fragments/row::text-row(label='Coordinates precision', text=${model.coordinatesPrecision})"></div>
   <div th:if="${model.site.latitude}"
-       th:replace="fragments/row::text-row(label='Latitude', text=${model.site.latitude + ' - TODO in degrees'})"></div>
+       th:replace="fragments/row::text-row(label='Latitude', text=${#coordinates.formatLatitude(model.site.latitude)})"></div>
   <div th:if="${model.site.longitude}"
-       th:replace="fragments/row::text-row(label='Longitude', text=${model.site.longitude + ' - TODO in degrees'})"></div>
+       th:replace="fragments/row::text-row(label='Longitude', text=${#coordinates.formatLongitude(model.site.longitude)})"></div>
   <div th:replace="fragments/row::text-row(label='Geographical location', text=${model.geographicalLocation})"></div>
   <div th:if="${model.site.countryName && !model.geographicalLocation}"
        th:replace="fragments/row::text-row(label='Country name', text=${model.site.countryName})"></div>
@@ -56,11 +56,14 @@
   <div th:replace="fragments/row::text-row(label='Direction from city', text=${model.directionFromCity})"></div>
   <div th:replace="fragments/row::text-row(label='Comment', text=${model.comment})"></div>
 
-  <h2>Additional info</h2>
-  <th:block th:each="prop : ${model.additionalInfoProperties}">
-    <div th:replace="fragments/row::text-row(label=${prop.key}, text=${prop.value})"></div>
+  <th:block th:if="${!#lists.isEmpty(model.additionalInfoProperties)}">
+    <h2>Additional info</h2>
+    <th:block th:each="prop : ${model.additionalInfoProperties}">
+      <div th:replace="fragments/row::text-row(label=${prop.key}, text=${prop.value})"></div>
+    </th:block>
   </th:block>
 
+  <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
 </main>
 </body>
 </html>
-- 
GitLab


From 698ba5f35f2b80794cee14cd600fb286e4a5c0b5 Mon Sep 17 00:00:00 2001
From: jnizet <jb@ninja-squad.com>
Date: Wed, 25 Aug 2021 14:28:33 +0200
Subject: [PATCH 10/16] fix: fix tests in site card

---
 .../urgi/faidare/web/site/SiteController.java |  4 +-
 .../inra/urgi/faidare/web/site/SiteModel.java |  3 +-
 .../src/main/resources/templates/index.html   |  1 -
 .../src/main/resources/templates/site.html    | 58 +++++++++++--------
 4 files changed, 35 insertions(+), 31 deletions(-)

diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java
index b6fecd50..1ed29438 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java
@@ -7,8 +7,6 @@ import fr.inra.urgi.faidare.api.NotFoundException;
 import fr.inra.urgi.faidare.config.FaidareProperties;
 import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiAdditionalInfo;
 import fr.inra.urgi.faidare.domain.data.LocationVO;
-import fr.inra.urgi.faidare.domain.response.PaginatedList;
-import fr.inra.urgi.faidare.domain.xref.XRefDocumentSearchCriteria;
 import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
 import fr.inra.urgi.faidare.repository.es.LocationRepository;
 import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository;
@@ -22,7 +20,7 @@ import org.springframework.web.servlet.ModelAndView;
  * Controller used to display a site card based on its ID.
  * @author JB Nizet
  */
-@Controller
+@Controller("webSiteController")
 @RequestMapping("/sites")
 public class SiteController {
 
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
index 900dca16..84cd92e2 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
@@ -13,8 +13,7 @@ import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource;
 import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
 
 /**
- * TODO add javadoc
- *
+ * The model used py the site page
  * @author JB Nizet
  */
 public final class SiteModel {
diff --git a/backend/src/main/resources/templates/index.html b/backend/src/main/resources/templates/index.html
index ae925eac..49f401b0 100644
--- a/backend/src/main/resources/templates/index.html
+++ b/backend/src/main/resources/templates/index.html
@@ -2,7 +2,6 @@
 
 <html
   xmlns:th="http://www.thymeleaf.org"
-  xmlns:biom="http://www.thymeleaf.org"
   th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
 >
 <head>
diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html
index e9867c75..ad6affc4 100644
--- a/backend/src/main/resources/templates/site.html
+++ b/backend/src/main/resources/templates/site.html
@@ -2,7 +2,6 @@
 
 <html
   xmlns:th="http://www.thymeleaf.org"
-  xmlns:biom="http://www.thymeleaf.org"
   th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
 >
 <head>
@@ -14,23 +13,26 @@
 <main>
   <h1>Site <th:block th:text="${model.site.locationName}" /></h1>
 
-  <div th:if="${model.site.uri && !model.site.uri.startsWith('urn:')}"
-       th:replace="fragments/row::text-row(label='Permanent unique identifier', text=${model.site.uri})">
-  </div>
+  <th:block th:if="${model.site.uri != null && !model.site.uri.startsWith('urn:')}">
+    <div th:replace="fragments/row::text-row(label='Permanent unique identifier', text=${model.site.uri})"></div>
+  </th:block>
 
-  <div th:if="${model.source}"
-       th:replace="fragments/row::row(label='Source', content=~{::#source}, text='')">
-    <a id="source" target="_blank" th:href="${model.source.url}">
-      <img style="max-height: 60px;" th:src="${model.source.image}" th:alt="${model.source.name} + ' logo'" />
-    </a>
-  </div>
+  <th:block th:if="${model.source != null}">
+    <div th:replace="fragments/row::row(label='Source', content=~{::#source})">
+      <a id="source" target="_blank" th:href="${model.source.url}">
+        <img style="max-height: 60px;" th:src="${model.source.image}" th:alt="${model.source.name} + ' logo'" />
+      </a>
+    </div>
+  </th:block>
 
-  <div th:if="${model.site.url && model.source}"
-       th:replace="fragments/row::row(label='Data link', content=~{::#url}, text='')">
-    <a id="url" target="_blank" th:href="${model.site.url}">
-      Link to this site on <th:block th:text="${model.source.name}" />
-    </a>
-  </div>
+  <th:block th:if="${model.site.url != null && model.source != null}">
+    <div
+         th:replace="fragments/row::row(label='Data link', content=~{::#url})">
+      <a id="url" target="_blank" th:href="${model.site.url}">
+        Link to this site on <th:block th:text="${model.source.name}" />
+      </a>
+    </div>
+  </th:block>
 
   <div th:replace="fragments/row::text-row(label='Abbreviation', text=${model.site.abbreviation})"></div>
   <div th:replace="fragments/row::text-row(label='Type', text=${model.site.locationType})"></div>
@@ -38,15 +40,21 @@
   <div th:replace="fragments/row::text-row(label='Institution/Landowner', text=${model.site.instituteName})"></div>
   <div th:replace="fragments/row::text-row(label='Institution address', text=${model.site.instituteAddress})"></div>
   <div th:replace="fragments/row::text-row(label='Coordinates precision', text=${model.coordinatesPrecision})"></div>
-  <div th:if="${model.site.latitude}"
-       th:replace="fragments/row::text-row(label='Latitude', text=${#coordinates.formatLatitude(model.site.latitude)})"></div>
-  <div th:if="${model.site.longitude}"
-       th:replace="fragments/row::text-row(label='Longitude', text=${#coordinates.formatLongitude(model.site.longitude)})"></div>
+  <th:block th:if="${model.site.latitude}">
+    <div th:replace="fragments/row::text-row(label='Latitude', text=${#coordinates.formatLatitude(model.site.latitude)})"></div>
+  </th:block>
+  <th:block th:if="${model.site.longitude}">
+    <div th:replace="fragments/row::text-row(label='Longitude', text=${#coordinates.formatLongitude(model.site.longitude)})"></div>
+  </th:block>
   <div th:replace="fragments/row::text-row(label='Geographical location', text=${model.geographicalLocation})"></div>
-  <div th:if="${model.site.countryName && !model.geographicalLocation}"
-       th:replace="fragments/row::text-row(label='Country name', text=${model.site.countryName})"></div>
-  <div th:if="${model.site.countryCode && !model.geographicalLocation}"
-       th:replace="fragments/row::text-row(label='Country code', text=${model.site.countryName})"></div>
+  <th:block th:if="${model.site.countryName != null && model.geographicalLocation == null}">
+    <div th:replace="fragments/row::text-row(label='Country name', text=${model.site.countryName})"></div>
+  </th:block>
+
+  <th:block th:if="${model.site.countryCode != null && model.geographicalLocation == null}">
+    <div th:replace="fragments/row::text-row(label='Country code', text=${model.site.countryName})"></div>
+  </th:block>
+
   <div th:replace="fragments/row::text-row(label='Altitude', text=${model.site.altitude})"></div>
   <div th:replace="fragments/row::text-row(label='Slope', text=${model.slope})"></div>
   <div th:replace="fragments/row::text-row(label='Exposure', text=${model.exposure})"></div>
@@ -56,7 +64,7 @@
   <div th:replace="fragments/row::text-row(label='Direction from city', text=${model.directionFromCity})"></div>
   <div th:replace="fragments/row::text-row(label='Comment', text=${model.comment})"></div>
 
-  <th:block th:if="${!#lists.isEmpty(model.additionalInfoProperties)}">
+  <th:block th:unless="${#lists.isEmpty(model.additionalInfoProperties)}">
     <h2>Additional info</h2>
     <th:block th:each="prop : ${model.additionalInfoProperties}">
       <div th:replace="fragments/row::text-row(label=${prop.key}, text=${prop.value})"></div>
-- 
GitLab


From 5c77f169242500937da2137540c4dc85d6a20ad8 Mon Sep 17 00:00:00 2001
From: jnizet <jb@ninja-squad.com>
Date: Wed, 25 Aug 2021 14:29:06 +0200
Subject: [PATCH 11/16] feat: study card

---
 .../faidare/web/study/StudyController.java    | 137 ++++++++++++
 .../urgi/faidare/web/study/StudyModel.java    |  82 +++++++
 .../src/main/resources/templates/study.html   | 206 ++++++++++++++++++
 3 files changed, 425 insertions(+)
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
 create mode 100644 backend/src/main/resources/templates/study.html

diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
new file mode 100644
index 00000000..35767b09
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
@@ -0,0 +1,137 @@
+package fr.inra.urgi.faidare.web.study;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import com.google.common.collect.Lists;
+import fr.inra.urgi.faidare.api.NotFoundException;
+import fr.inra.urgi.faidare.config.FaidareProperties;
+import fr.inra.urgi.faidare.domain.criteria.GermplasmPOSTSearchCriteria;
+import fr.inra.urgi.faidare.domain.data.TrialVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
+import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO;
+import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO;
+import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+import fr.inra.urgi.faidare.repository.es.GermplasmRepository;
+import fr.inra.urgi.faidare.repository.es.StudyRepository;
+import fr.inra.urgi.faidare.repository.es.TrialRepository;
+import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository;
+import fr.inra.urgi.faidare.repository.file.CropOntologyRepository;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * Controller used to display a study card based on its ID.
+ * @author JB Nizet
+ */
+@Controller("webStudyController")
+@RequestMapping("/studies")
+public class StudyController {
+
+    private final StudyRepository studyRepository;
+    private final FaidareProperties faidareProperties;
+    private final XRefDocumentRepository xRefDocumentRepository;
+    private final GermplasmRepository germplasmRepository;
+    private final CropOntologyRepository cropOntologyRepository;
+    private final TrialRepository trialRepository;
+
+    public StudyController(StudyRepository studyRepository,
+                           FaidareProperties faidareProperties,
+                           XRefDocumentRepository xRefDocumentRepository,
+                           GermplasmRepository germplasmRepository,
+                           CropOntologyRepository cropOntologyRepository,
+                           TrialRepository trialRepository) {
+        this.studyRepository = studyRepository;
+        this.faidareProperties = faidareProperties;
+        this.xRefDocumentRepository = xRefDocumentRepository;
+        this.germplasmRepository = germplasmRepository;
+        this.cropOntologyRepository = cropOntologyRepository;
+        this.trialRepository = trialRepository;
+    }
+
+    @GetMapping("/{studyId}")
+    public ModelAndView site(@PathVariable("studyId") String studyId) {
+        StudyDetailVO study = studyRepository.getById(studyId);
+
+        // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find(
+        //     XRefDocumentSearchCriteria.forXRefId(site.getLocationDbId()));
+        List<XRefDocumentVO> crossReferences = Arrays.asList(
+            createXref("foobar"),
+            createXref("bazbing")
+        );
+
+        // LocationVO site = createSite();
+
+        if (study == null) {
+            throw new NotFoundException("Study with ID " + studyId + " not found");
+        }
+
+        List<GermplasmVO> germplasms = getGermplasms(study);
+        List<ObservationVariableVO>variables = getVariables(study);
+        List<TrialVO> trials = getTrials(study);
+
+        return new ModelAndView("study",
+                                "model",
+                                new StudyModel(
+                                    study,
+                                    faidareProperties.getByUri(study.getSourceUri()),
+                                    germplasms,
+                                    variables,
+                                    trials,
+                                    crossReferences
+                                )
+        );
+    }
+
+    private List<GermplasmVO> getGermplasms(StudyDetailVO study) {
+        if (study.getGermplasmDbIds() == null || study.getGermplasmDbIds().isEmpty()) {
+            return Collections.emptyList();
+        } else {
+            GermplasmPOSTSearchCriteria germplasmCriteria = new GermplasmPOSTSearchCriteria();
+            germplasmCriteria.setGermplasmDbIds(Lists.newArrayList(study.getGermplasmDbIds()));
+            return germplasmRepository.find(germplasmCriteria)
+                .stream()
+                .sorted(Comparator.comparing(GermplasmVO::getGermplasmName))
+                .collect(Collectors.toList());
+        }
+    }
+
+    private List<ObservationVariableVO> getVariables(StudyDetailVO study) {
+        Set<String> variableIds = studyRepository.getVariableIds(study.getStudyDbId());
+        return cropOntologyRepository.getVariableByIds(variableIds)
+            .stream()
+            .sorted(Comparator.comparing(ObservationVariableVO::getObservationVariableDbId))
+            .collect(Collectors.toList());
+    }
+
+    private List<TrialVO> getTrials(StudyDetailVO study) {
+        if (study.getTrialDbIds() == null || study.getTrialDbIds().isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        return study.getTrialDbIds()
+                    .stream()
+                    .sorted(Comparator.naturalOrder())
+                    .map(trialRepository::getById)
+                    .filter(Objects::nonNull)
+                    .collect(Collectors.toList());
+    }
+
+    private XRefDocumentVO createXref(String name) {
+        XRefDocumentVO xref = new XRefDocumentVO();
+        xref.setName(name);
+        xref.setDescription("A very large description for the xref " + name + " which has way more than 120 characters bla bla bla bla bla bla bla bla bla bla bla bla");
+        xref.setDatabaseName("db_" + name);
+        xref.setUrl("https://google.com");
+        xref.setEntryType("type " + name);
+        return xref;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
new file mode 100644
index 00000000..980c78d4
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
@@ -0,0 +1,82 @@
+package fr.inra.urgi.faidare.web.study;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import fr.inra.urgi.faidare.domain.data.LocationVO;
+import fr.inra.urgi.faidare.domain.data.TrialVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
+import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO;
+import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO;
+import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource;
+import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+
+/**
+ * The model used by the study page
+ * @author JB Nizet
+ */
+public final class StudyModel {
+    private final StudyDetailVO study;
+    private final DataSource source;
+    private final List<GermplasmVO> germplasms;
+    private final List<ObservationVariableVO> variables;
+    private final List<TrialVO> trials;
+    private final List<XRefDocumentVO> crossReferences;
+    private final List<Map.Entry<String, Object>> additionalInfoProperties;
+
+    public StudyModel(StudyDetailVO study,
+                      DataSource source,
+                      List<GermplasmVO> germplasms,
+                      List<ObservationVariableVO> variables,
+                      List<TrialVO> trials,
+                      List<XRefDocumentVO> crossReferences) {
+        this.study = study;
+        this.source = source;
+        this.germplasms = germplasms;
+        this.variables = variables;
+        this.trials = trials;
+        this.crossReferences = crossReferences;
+
+        Map<String, Object> additionalInfo =
+            study.getAdditionalInfo() == null ? Collections.emptyMap() : study.getAdditionalInfo().getProperties();
+        this.additionalInfoProperties =
+            additionalInfo.entrySet()
+                          .stream()
+                          .filter(entry -> entry.getValue() != null && !entry.getValue().toString().isEmpty())
+                          .sorted(Map.Entry.comparingByKey())
+                          .collect(Collectors.toList());
+    }
+
+    public StudyDetailVO getStudy() {
+        return study;
+    }
+
+    public DataSource getSource() {
+        return source;
+    }
+
+    public List<XRefDocumentVO> getCrossReferences() {
+        return crossReferences;
+    }
+
+    public List<GermplasmVO> getGermplasms() {
+        return germplasms;
+    }
+
+    public List<ObservationVariableVO> getVariables() {
+        return variables;
+    }
+
+    public List<TrialVO> getTrials() {
+        return trials;
+    }
+
+    public List<Map.Entry<String, Object>> getAdditionalInfoProperties() {
+        return additionalInfoProperties;
+    }
+}
diff --git a/backend/src/main/resources/templates/study.html b/backend/src/main/resources/templates/study.html
new file mode 100644
index 00000000..a1cafc65
--- /dev/null
+++ b/backend/src/main/resources/templates/study.html
@@ -0,0 +1,206 @@
+<!DOCTYPE html>
+
+<html
+  xmlns:th="http://www.thymeleaf.org"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
+>
+<head>
+  <title>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></title>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+</head>
+
+<body>
+<main>
+  <h1>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></h1>
+
+  <h2>Identification</h2>
+
+  <div th:replace="fragments/row::text-row(label='Name', text=${model.study.studyName})"></div>
+  <div th:replace="fragments/row::text-row(label='Identifier', text=${model.study.studyDbId})"></div>
+
+  <th:block th:if="${model.source != null}">
+    <div th:replace="fragments/row::row(label='Source', content=~{::#source}, text='')">
+      <a id="source" target="_blank" th:href="${model.source.url}">
+        <img style="max-height: 60px;" th:src="${model.source.image}" th:alt="${model.source.name} + ' logo'" />
+      </a>
+    </div>
+  </th:block>
+
+  <th:block th:if="${model.study.url != null && model.source != null}">
+    <div th:replace="fragments/row::row(label='Data link', content=~{::#url}, text='')">
+      <a id="url" target="_blank" th:href="${model.study.url}">
+        Link to this study on <th:block th:text="${model.source.name}" />
+      </a>
+    </div>
+  </th:block>
+
+  <div th:replace="fragments/row::text-row(label='Project name', text=${model.study.programName})"></div>
+  <div th:replace="fragments/row::text-row(label='Description', text=${model.study.studyDescription})"></div>
+  <th:block th:if="${model.study.active != null}">
+    <div th:replace="fragments/row::text-row(label='Active', text=${model.study.active ? 'Yes' : 'No'})"></div>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.study.seasons)}">
+    <div th:replace="fragments/row::text-row(label='Seasons', text=${#strings.listJoin(model.study.seasons, ',')})"></div>
+  </th:block>
+  <th:block th:if="${model.study.startDate != null && model.study.endDate != null}">
+    <div th:replace="fragments/row::text-row(label='Date', text=${'From ' + #dates.format(model.study.startDate, 'yyyy-MM-dd') + ' to ' + #dates.format(model.study.endDate, 'yyyy-MM-dd') })"></div>
+  </th:block>
+  <th:block th:if="${model.study.startDate != null && model.study.endDate == null}">
+    <div th:replace="fragments/row::text-row(label='Date', text=${'Started on ' + #dates.format(model.study.startDate, 'yyyy-MM-dd')})"></div>
+  </th:block>
+
+  <th:block th:if="${model.study.locationDbId}">
+    <div th:replace="fragments/row::row(label='Location name', content=~{::#location})">
+      <a id="location" th:href="@{/sites/{siteId}(siteId=${model.study.locationDbId})}" th:text="${model.study.locationName}"></a>
+    </div>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.study.dataLinks)}">
+    <div th:replace="fragments/row::row(label='Data files', content=~{::#data-files})">
+      <ul id="data-files" class="list-unstyled">
+        <li th:each="dataLink : ${model.study.dataLinks}">
+          <a target="_blank" th:href="${dataLink.url}" th:text="${dataLink.name}"></a>
+        </li>
+      </ul>
+    </div>
+  </th:block>
+
+  <th:block th:unles="${#lists.isEmpty(model.germplasms)}">
+    <h2>Genotype</h2>
+
+    <div class="table-responsive scroll-table table-card-body">
+      <div class="card">
+        <table class="table table-sm table-striped">
+          <thead>
+            <tr>
+              <th scope="col">Accession number</th>
+              <th scope="col">Name</th>
+              <th scope="col">Taxon</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row : ${model.germplasms}">
+              <td>
+                <a th:href="@{/germplasms/{germplasmId}(germplasmId=${row.germplasmDbId})}" th:text="${row.accessionNumber}"></a>
+              </td>
+              <td th:text="${row.germplasmName}"></td>
+              <td th:text="${(row.genus == null ? '' : row.genus) + ' ' + (row.species == null ? '' : row.species)+ ' ' + (row.subtaxa == null ? '' : row.subtaxa) }"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.variables)}">
+    <h2>Variables</h2>
+    <div class="table-responsive scroll-table table-card-body">
+      <div class="card">
+        <table class="table table-sm table-striped">
+          <thead>
+            <tr>
+              <th scope="col">Variable ID</th>
+              <th scope="col">Variable short name</th>
+              <th scope="col">Variable long name</th>
+              <th scope="col">Ontology name</th>
+              <th scope="col">Trait description</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row : ${model.variables}">
+              <td>
+                <a th:unless="${#strings.isEmpty(row.documentationURL)}" th:href="${row.documentationURL}" th:text="${row.observationVariableDbId}" target="_blank" ></a>
+                <span th:if="${#strings.isEmpty(row.documentationURL)}" th:text="${row.observationVariableDbId}"></span>
+              </td>
+              <td th:text="${row.name}"></td>
+              <td th:text="${#lists.isEmpty(row.synonyms) ? '' : row.synonyms[0]}"></td>
+              <td th:text="${row.ontologyName}"></td>
+              <td th:text="${row.trait.description}"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.trials)}">
+    <h2>Data Set</h2>
+    <div class="table-responsive scroll-big-table table-card-body">
+      <div class="card">
+        <table class="table table-sm table-striped">
+          <thead>
+            <tr>
+              <th scope="col">Name</th>
+              <th scope="col">Type</th>
+              <th scope="col">Linked studies identifier</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row : ${model.trials}">
+              <td>
+                <a th:unless="${#strings.isEmpty(row.documentationURL)}" th:href="${row.documentationURL}" th:text="${row.trialName}" target="_blank" ></a>
+                <span th:if="${#strings.isEmpty(row.documentationURL)}" th:text="${row.trialName}"></span>
+              </td>
+              <td th:text="${row.trialType}"></td>
+              <td style="width: 60%">
+                <th:block th:each="trialStudy, iterStat : ${row.studies}"
+                          th:if="${trialStudy.studyDbId != model.study.studyDbId}">
+                  <a th:href="@{/studies/{studyId}(studyId=${trialStudy.studyDbId})}"
+                     th:text="${trialStudy.studyName.trim()}">
+                  </a><th:block th:if="${iterStat.last}">; </th:block>
+                </th:block>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.study.contacts)}">
+    <h2>Contact</h2>
+    <div class="table-responsive scroll-table table-card-body">
+      <div class="card">
+        <table class="table table-sm table-striped">
+          <thead>
+            <tr>
+              <th scope="col">Role</th>
+              <th scope="col">Name</th>
+              <th scope="col">Email</th>
+              <th scope="col">Institution</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row : ${model.study.contacts}">
+              <td th:text="${row.type}"></td>
+              <td th:text="${row.name}"></td>
+              <td th:text="${row.email}"></td>
+              <td th:text="${row.institutionName}"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.additionalInfoProperties)}">
+    <h2>Additional information</h2>
+    <div class="table-responsive scroll-table table-card-body">
+      <div class="card">
+        <table class="table table-sm table-striped">
+          <tbody>
+            <tr th:each="row : ${model.additionalInfoProperties}">
+              <td style="width: 50%;" th:text="${row.key}"></td>
+              <td th:text="${row.value}"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </th:block>
+
+  <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
+</main>
+</body>
+</html>
-- 
GitLab


From 21b56eaf47bf274c5509bd0a45f6ac49ab64ee44 Mon Sep 17 00:00:00 2001
From: jnizet <jb@ninja-squad.com>
Date: Thu, 26 Aug 2021 15:34:25 +0200
Subject: [PATCH 12/16] feat: germplasm card

---
 .../web/germplasm/GermplasmController.java    | 392 ++++++++++++++++
 .../faidare/web/germplasm/GermplasmModel.java | 139 ++++++
 .../urgi/faidare/web/site/SiteController.java |   2 +-
 .../faidare/web/study/StudyController.java    |   2 +-
 .../faidare/web/thymeleaf/FaidareDialect.java |  25 ++
 .../thymeleaf/FaidareExpressionFactory.java   |  33 ++
 .../web/thymeleaf/FaidareExpressions.java     |  65 +++
 .../main/resources/static/assets/style.css    |   4 +
 .../templates/fragments/institute.html        |  34 ++
 .../resources/templates/fragments/link.html   |  20 +
 .../resources/templates/fragments/row.html    |  31 +-
 .../resources/templates/fragments/source.html |  35 ++
 .../resources/templates/fragments/xrefs.html  |   5 +
 .../main/resources/templates/germplasm.html   | 418 ++++++++++++++++++
 .../main/resources/templates/layout/main.html |  22 +
 .../src/main/resources/templates/site.html    |  17 +-
 .../src/main/resources/templates/study.html   |  16 +-
 17 files changed, 1226 insertions(+), 34 deletions(-)
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareDialect.java
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressionFactory.java
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java
 create mode 100644 backend/src/main/resources/templates/fragments/institute.html
 create mode 100644 backend/src/main/resources/templates/fragments/link.html
 create mode 100644 backend/src/main/resources/templates/fragments/source.html
 create mode 100644 backend/src/main/resources/templates/germplasm.html

diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java
new file mode 100644
index 00000000..cf343049
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java
@@ -0,0 +1,392 @@
+package fr.inra.urgi.faidare.web.germplasm;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import fr.inra.urgi.faidare.api.NotFoundException;
+import fr.inra.urgi.faidare.config.FaidareProperties;
+import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiGermplasmAttributeValue;
+import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiSibling;
+import fr.inra.urgi.faidare.domain.criteria.GermplasmAttributeCriteria;
+import fr.inra.urgi.faidare.domain.criteria.GermplasmGETSearchCriteria;
+import fr.inra.urgi.faidare.domain.data.germplasm.CollPopVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.DonorVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GenealogyVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmAttributeValueVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmInstituteVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.InstituteVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.PedigreeVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.PhotoVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.PuiNameValueVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.SiblingVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.SimpleVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.SiteVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.TaxonSourceVO;
+import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+import fr.inra.urgi.faidare.repository.es.GermplasmAttributeRepository;
+import fr.inra.urgi.faidare.repository.es.GermplasmRepository;
+import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.servlet.ModelAndView;
+
+/**
+ * Controller used to display a germplasm card based on its ID.
+ * @author JB Nizet
+ */
+@Controller("webGermplasmController")
+@RequestMapping("/germplasms")
+public class GermplasmController {
+
+    private final GermplasmRepository germplasmRepository;
+    private final FaidareProperties faidareProperties;
+    private final XRefDocumentRepository xRefDocumentRepository;
+    private GermplasmAttributeRepository germplasmAttributeRepository;
+
+    public GermplasmController(GermplasmRepository germplasmRepository,
+                               FaidareProperties faidareProperties,
+                               XRefDocumentRepository xRefDocumentRepository,
+                               GermplasmAttributeRepository germplasmAttributeRepository) {
+        this.germplasmRepository = germplasmRepository;
+        this.faidareProperties = faidareProperties;
+        this.xRefDocumentRepository = xRefDocumentRepository;
+        this.germplasmAttributeRepository = germplasmAttributeRepository;
+    }
+
+    @GetMapping("/{germplasmId}")
+    public ModelAndView get(@PathVariable("germplasmId") String germplasmId) {
+        // GermplasmVO germplasm = germplasmRepository.getById(germplasmId);
+
+        // TODO replace this block by the above commented one
+        GermplasmVO germplasm = createGermplasm();
+
+        if (germplasm == null) {
+            throw new NotFoundException("Germplasm with ID " + germplasmId + " not found");
+        }
+
+        return toModelAndView(germplasm);
+    }
+
+    @GetMapping(params = "pui")
+    public ModelAndView getByPui(@RequestParam("pui") String pui) {
+        GermplasmGETSearchCriteria criteria = new GermplasmGETSearchCriteria();
+        criteria.setGermplasmPUI(Collections.singletonList(pui));
+        List<GermplasmVO> germplasms = germplasmRepository.find(criteria);
+        if (germplasms.size() != 1) {
+            throw new NotFoundException("Germplasm with PUI " + pui + " not found");
+        }
+
+        return toModelAndView(germplasms.get(0));
+    }
+
+    private ModelAndView toModelAndView(GermplasmVO germplasm) {
+        // List<BrapiGermplasmAttributeValue> attributes = getAttributes(germplasm);
+        // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find(
+        //     XRefDocumentSearchCriteria.forXRefId(site.getLocationDbId()));
+        // PedigreeVO pedigree = getPedigree(germplasm);
+        // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find(
+        //     XRefDocumentSearchCriteria.forXRefId(germplasm.getGermplasmDbId())
+        // );
+
+        // TODO replace this block by the above commented one
+        List<BrapiGermplasmAttributeValue> attributes = Arrays.asList(
+            createAttribute()
+        );
+        PedigreeVO pedigree = createPedigree();
+        List<XRefDocumentVO> crossReferences = Arrays.asList(
+            createXref("foobar"),
+            createXref("bazbing")
+        );
+
+
+        sortDonors(germplasm);
+        sortPopulations(germplasm);
+        sortCollections(germplasm);
+        sortPanels(germplasm);
+        return new ModelAndView("germplasm",
+                                "model",
+                                new GermplasmModel(
+                                    germplasm,
+                                    faidareProperties.getByUri(germplasm.getSourceUri()),
+                                    attributes,
+                                    pedigree,
+                                    crossReferences
+                                )
+        );
+    }
+
+    private void sortPopulations(GermplasmVO germplasm) {
+        if (germplasm.getPopulation() != null) {
+            germplasm.setPopulation(germplasm.getPopulation()
+                                             .stream()
+                                             .sorted(Comparator.comparing(
+                                                 CollPopVO::getName))
+                                             .collect(Collectors.toList()));
+        }
+    }
+
+    private void sortCollections(GermplasmVO germplasm) {
+        if (germplasm.getCollection() != null) {
+            germplasm.setCollection(germplasm.getCollection()
+                                             .stream()
+                                             .sorted(Comparator.comparing(CollPopVO::getName))
+                                             .collect(Collectors.toList()));
+        }
+    }
+
+    private void sortPanels(GermplasmVO germplasm) {
+        if (germplasm.getPanel() != null) {
+            germplasm.setPanel(germplasm.getPanel()
+                                        .stream()
+                                        .sorted(Comparator.comparing(CollPopVO::getName))
+                                        .collect(Collectors.toList()));
+        }
+    }
+
+    private void sortDonors(GermplasmVO germplasm) {
+        if (germplasm.getDonors() != null) {
+            germplasm.setDonors(germplasm.getDonors()
+                                         .stream()
+                                         .sorted(Comparator.comparing(donor -> donor.getDonorInstitute()
+                                                                                    .getInstituteName()))
+                                         .collect(Collectors.toList()));
+        }
+    }
+
+    private List<BrapiGermplasmAttributeValue> getAttributes(GermplasmVO germplasm) {
+        GermplasmAttributeCriteria criteria = new GermplasmAttributeCriteria();
+        criteria.setGermplasmDbId(germplasm.getGermplasmDbId());
+        return germplasmAttributeRepository.find(criteria)
+            .stream()
+            .flatMap(vo -> vo.getData().stream())
+            .sorted(Comparator.comparing(BrapiGermplasmAttributeValue::getAttributeName))
+            .collect(Collectors.toList());
+    }
+
+    private PedigreeVO getPedigree(GermplasmVO germplasm) {
+        return germplasmRepository.findPedigree(germplasm.getGermplasmDbId());
+    }
+
+    private BrapiGermplasmAttributeValue createAttribute() {
+        GermplasmAttributeValueVO result = new GermplasmAttributeValueVO();
+        result.setAttributeName("A1");
+        result.setValue("V1");
+        return result;
+    }
+
+    private GermplasmVO createGermplasm() {
+        GermplasmVO result = new GermplasmVO();
+
+        result.setGermplasmName("BLE BARBU DU ROUSSILLON");
+        result.setAccessionNumber("1408");
+        result.setSynonyms(Arrays.asList("BLE DU ROUSSILLON", "FRA051:1699", "ROUSSILLON"));
+        PhotoVO photo = new PhotoVO();
+        photo.setPhotoName("Blé du roussillon");
+        photo.setCopyright("INRA, Emmanuelle BOULAT/Lionel BARDY 2012");
+        photo.setThumbnailFile("https://urgi.versailles.inrae.fr/files/siregal/images/accession/CEREALS/thumbnails/thumb_1408_R09_S.jpg");
+        photo.setFile("https://urgi.versailles.inrae.fr/files/siregal/images/accession/CEREALS/1408_R09_S.jpg");
+        result.setPhoto(photo);
+
+        InstituteVO holdingGenBank = new InstituteVO();
+        holdingGenBank.setLogo("https://urgi.versailles.inra.fr/files/siregal/images/grc/inra_brc_en.png");
+        holdingGenBank.setInstituteName("INRA BRC");
+        holdingGenBank.setWebSite("http://google.fr");
+        result.setHoldingGenbank(holdingGenBank);
+
+        result.setBiologicalStatusOfAccessionCode("Traditional cultivar/landrace ");
+        result.setPedigree("LV");
+        SiteVO originSite = new SiteVO();
+        originSite.setSiteId("1234");
+        originSite.setSiteName("Le Moulon");
+        result.setOriginSite(originSite);
+
+        result.setGenus("Genus 1");
+        result.setSpecies("Species 1");
+        result.setSpeciesAuthority("Species Auth");
+        result.setSourceUri("https://urgi.versailles.inrae.fr/gnpis");
+        result.setSubtaxa("Subtaxa 1");
+        result.setGenusSpeciesSubtaxa("Triticum aestivum subsp. aestivum");
+        result.setSubtaxaAuthority("INRAE");
+        result.setTaxonIds(Arrays.asList(createTaxonId(), createTaxonId()));
+        result.setTaxonComment("C'est bon le blé");
+        result.setTaxonCommonNames(Arrays.asList("Blé tendre", "Bread wheat", "Soft wheat"));
+        result.setTaxonSynonyms(Arrays.asList("Blé tendre1", "Bread wheat1", "Soft wheat1"));
+
+        InstituteVO holdingInstitute = new InstituteVO();
+        holdingInstitute.setInstituteName("GDEC - UMR Génétique, Diversité et Ecophysiologie des Céréales");
+        holdingInstitute.setLogo("https://urgi.versailles.inra.fr/files/siregal/images/grc/inra_brc_en.png");
+        holdingInstitute.setWebSite("https://google.fr/q=qsdqsdqsdslqlsdnqlsdqlsdlqskdlqdqlsdqsdqsdqd");
+        holdingInstitute.setInstituteCode("GDEC");
+        holdingInstitute.setInstituteType("Type1");
+        holdingInstitute.setAcronym("G.D.E.C");
+        holdingInstitute.setAddress("Lyon");
+        holdingInstitute.setOrganisation("SAS");
+        result.setHoldingInstitute(holdingInstitute);
+
+        result.setPresenceStatus("Maintained");
+
+        GermplasmInstituteVO collector = new GermplasmInstituteVO();
+        collector.setMaterialType("Fork");
+        collector.setCollectors("Joe, Jack, William, Averell");
+        InstituteVO collectingInstitute = new InstituteVO();
+        collectingInstitute.setInstituteName("Ninja Squad");
+        collector.setInstitute(collectingInstitute);
+        collector.setAccessionNumber("567");
+        result.setCollector(collector);
+
+        result.setCollectingSite(originSite);
+        result.setAcquisitionDate("In the summer");
+
+        GermplasmInstituteVO breeder = new GermplasmInstituteVO();
+        InstituteVO breedingInstitute = new InstituteVO();
+        breedingInstitute.setInstituteName("Microsoft");
+        breeder.setInstitute(breedingInstitute);
+        breeder.setAccessionCreationDate(2015);
+        breeder.setAccessionNumber("678");
+        breeder.setRegistrationYear(2016);
+        breeder.setDeregistrationYear(2019);
+        result.setBreeder(breeder);
+
+        result.setDonors(Arrays.asList(
+            createDonor()
+        ));
+
+        result.setDistributors(Arrays.asList(
+            createDistributor()
+        ));
+
+        result.setChildren(Arrays.asList(createChild(), createChild()));
+
+        result.setGermplasmPUI("germplasmPUI");
+        result.setPopulation(Arrays.asList(createPopulation1(), createPopulation2(), createPopulation3()));
+
+        result.setCollection(Arrays.asList(createCollection()));
+
+        result.setPanel(Arrays.asList(createPanel()));
+
+        return result;
+    }
+
+    private DonorVO createDonor() {
+        DonorVO result = new DonorVO();
+        result.setDonorGermplasmPUI("PUI1");
+        result.setDonationDate(2017);
+        result.setDonorAccessionNumber("3456");
+        result.setDonorInstituteCode("GD46U");
+        InstituteVO institute = new InstituteVO();
+        institute.setInstituteName("Hello");
+        result.setDonorInstitute(institute);
+        return result;
+    }
+
+    private GermplasmInstituteVO createDistributor() {
+        GermplasmInstituteVO result = new GermplasmInstituteVO();
+        InstituteVO institute = new InstituteVO();
+        institute.setInstituteName("Microsoft");
+        result.setInstitute(institute);
+        result.setAccessionNumber("678");
+        result.setDistributionStatus("OK");
+        return result;
+    }
+
+    private PedigreeVO createPedigree() {
+        PedigreeVO result = new PedigreeVO();
+        result.setPedigree("Pedigree 1");
+        result.setParent1DbId("12345");
+        result.setParent1Name("Parent 1");
+        result.setParent1Type("P1");
+        result.setParent2DbId("12346");
+        result.setParent2Name("Parent 2");
+        result.setParent2Type("P2");
+        result.setCrossingPlan("crossing plan 1");
+        result.setCrossingYear("2012");
+        result.setSiblings(Arrays.asList(createBrapiSibling()));
+        return result;
+    }
+
+    private BrapiSibling createBrapiSibling() {
+        SiblingVO sibling = new SiblingVO();
+        sibling.setGermplasmDbId("5678");
+        sibling.setDefaultDisplayName("Sibling 5678");
+        return sibling;
+    }
+
+    private GenealogyVO createChild() {
+        GenealogyVO result = new GenealogyVO();
+        result.setFirstParentName("CP1");
+        result.setSecondParentName("CP2");
+        result.setSibblings(Arrays.asList(createPuiNameValueVO(), createPuiNameValueVO()));
+        return result;
+    }
+
+    private PuiNameValueVO createPuiNameValueVO() {
+        PuiNameValueVO result = new PuiNameValueVO();
+        result.setName("Child 1");
+        result.setPui("pui1");
+        return result;
+    }
+
+    private CollPopVO createPopulation1() {
+        CollPopVO result = new CollPopVO();
+        result.setName("Population 1");
+        result.setType("Pop Type 1");
+        result.setGermplasmCount(3);
+        result.setGermplasmRef(createPuiNameValueVO());
+        return result;
+    }
+
+    private CollPopVO createPopulation2() {
+        CollPopVO result = new CollPopVO();
+        result.setName("Population 2");
+        result.setGermplasmCount(3);
+        PuiNameValueVO puiNameValueVO = createPuiNameValueVO();
+        puiNameValueVO.setPui("germplasmPUI");
+        result.setGermplasmRef(puiNameValueVO);
+        return result;
+    }
+
+    private CollPopVO createPopulation3() {
+        CollPopVO result = new CollPopVO();
+        result.setName("Population 3");
+        result.setGermplasmCount(5);
+        return result;
+    }
+
+    private CollPopVO createCollection() {
+        CollPopVO result = new CollPopVO();
+        result.setName("Collection 1");
+        result.setGermplasmCount(7);
+        return result;
+    }
+
+    private CollPopVO createPanel() {
+        CollPopVO result = new CollPopVO();
+        result.setName("The_panel_1");
+        result.setGermplasmCount(2);
+        return result;
+    }
+
+    private TaxonSourceVO createTaxonId() {
+        TaxonSourceVO result = new TaxonSourceVO();
+        result.setTaxonId("taxon1");
+        result.setSourceName("ThePlantList");
+        return result;
+    }
+
+    private XRefDocumentVO createXref(String name) {
+        XRefDocumentVO xref = new XRefDocumentVO();
+        xref.setName(name);
+        xref.setDescription("A very large description for the xref " + name + " which has way more than 120 characters bla bla bla bla bla bla bla bla bla bla bla bla");
+        xref.setDatabaseName("db_" + name);
+        xref.setUrl("https://google.com");
+        xref.setEntryType("type " + name);
+        return xref;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java
new file mode 100644
index 00000000..8acdf78f
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java
@@ -0,0 +1,139 @@
+package fr.inra.urgi.faidare.web.germplasm;
+
+import java.util.List;
+
+import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiGermplasmAttributeValue;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmInstituteVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.PedigreeVO;
+import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource;
+import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+import org.apache.logging.log4j.util.Strings;
+
+/**
+ * The model used by the germplasm page
+ * @author JB Nizet
+ */
+public final class GermplasmModel {
+    private final GermplasmVO germplasm;
+    private final DataSource source;
+    private final List<BrapiGermplasmAttributeValue> attributes;
+    private final PedigreeVO pedigree;
+    private final List<XRefDocumentVO> crossReferences;
+
+    public GermplasmModel(GermplasmVO germplasm,
+                          DataSource source,
+                          List<BrapiGermplasmAttributeValue> attributes,
+                          PedigreeVO pedigree,
+                          List<XRefDocumentVO> crossReferences) {
+        this.germplasm = germplasm;
+        this.source = source;
+        this.attributes = attributes;
+        this.pedigree = pedigree;
+        this.crossReferences = crossReferences;
+    }
+
+    public GermplasmVO getGermplasm() {
+        return germplasm;
+    }
+
+    public DataSource getSource() {
+        return source;
+    }
+
+    public List<BrapiGermplasmAttributeValue> getAttributes() {
+        return attributes;
+    }
+
+    public PedigreeVO getPedigree() {
+        return pedigree;
+    }
+
+    public List<XRefDocumentVO> getCrossReferences() {
+        return crossReferences;
+    }
+
+    public String getTaxon() {
+        if (Strings.isNotBlank(this.germplasm.getGenusSpeciesSubtaxa())) {
+            return this.germplasm.getGenusSpeciesSubtaxa();
+        } else if (Strings.isNotBlank(this.germplasm.getGenusSpecies())) {
+            return this.germplasm.getGenusSpecies();
+        } else if (Strings.isNotBlank(this.germplasm.getSubtaxa())) {
+            return this.germplasm.getGenus() + " " + this.germplasm.getSpecies() + " " + this.germplasm.getSubtaxa();
+        } else if (Strings.isNotBlank(this.germplasm.getSpecies())) {
+            return this.germplasm.getGenus() + " " + this.germplasm.getSpecies();
+        } else {
+            return this.germplasm.getGenus();
+        }
+    }
+
+    public String getTaxonAuthor() {
+        if (Strings.isNotBlank(this.germplasm.getGenusSpeciesSubtaxa())) {
+            return this.germplasm.getSubtaxaAuthority();
+        } else if (Strings.isNotBlank(this.germplasm.getGenusSpecies())) {
+            return this.germplasm.getSpeciesAuthority();
+        } else if (Strings.isNotBlank(this.germplasm.getSubtaxa())) {
+            return this.germplasm.getSubtaxaAuthority();
+        } else if (Strings.isNotBlank(this.germplasm.getSpecies())) {
+            return this.germplasm.getSpeciesAuthority();
+        } else {
+            return null;
+        }
+    }
+
+    public boolean isCollecting() {
+        return this.isCollectingSitePresent()
+            || this.isCollectorInstitutePresent()
+            || this.isCollectorIntituteFieldPresent();
+    }
+
+    private boolean isCollectingSitePresent() {
+        return this.germplasm.getCollectingSite() != null && Strings.isNotBlank(this.germplasm.getCollectingSite().getSiteName());
+    }
+
+    private boolean isCollectorInstitutePresent() {
+        return this.germplasm.getCollector() != null &&
+            this.germplasm.getCollector().getInstitute() != null &&
+            Strings.isNotBlank(this.germplasm.getCollector().getInstitute().getInstituteName());
+    }
+
+    private boolean isCollectorIntituteFieldPresent() {
+        GermplasmInstituteVO collector = this.germplasm.getCollector();
+        return (collector != null) &&
+            (Strings.isNotBlank(collector.getAccessionNumber())
+                || collector.getAccessionCreationDate() != null
+                || Strings.isNotBlank(collector.getMaterialType())
+                || Strings.isNotBlank(collector.getCollectors())
+                || collector.getRegistrationYear() != null
+                || collector.getDeregistrationYear() != null
+                || Strings.isNotBlank(collector.getDistributionStatus())
+            );
+    }
+
+    public boolean isBreeding() {
+        GermplasmInstituteVO breeder = this.germplasm.getBreeder();
+        return breeder != null &&
+            ((breeder.getInstitute() != null && Strings.isNotBlank(breeder.getInstitute().getInstituteName())) ||
+                breeder.getAccessionCreationDate() != null ||
+                Strings.isNotBlank(breeder.getAccessionNumber()) ||
+                breeder.getRegistrationYear() != null ||
+                breeder.getDeregistrationYear() != null);
+    }
+
+    public boolean isGenealogyPresent() {
+        return isPedigreePresent() || isProgenyPresent();
+    }
+
+    private boolean isProgenyPresent() {
+        return germplasm.getChildren() != null && !germplasm.getChildren().isEmpty();
+    }
+
+    private boolean isPedigreePresent() {
+        return this.pedigree != null &&
+            (Strings.isNotBlank(this.pedigree.getParent1Name())
+            || Strings.isNotBlank(this.pedigree.getParent2Name())
+            || Strings.isNotBlank(this.pedigree.getCrossingPlan())
+            || Strings.isNotBlank(this.pedigree.getCrossingYear())
+            || Strings.isNotBlank(this.pedigree.getFamilyCode()));
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java
index 1ed29438..151da527 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteController.java
@@ -37,7 +37,7 @@ public class SiteController {
     }
 
     @GetMapping("/{siteId}")
-    public ModelAndView site(@PathVariable("siteId") String siteId) {
+    public ModelAndView get(@PathVariable("siteId") String siteId) {
         LocationVO site = locationRepository.getById(siteId);
 
         // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find(
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
index 35767b09..ff6aee3b 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
@@ -58,7 +58,7 @@ public class StudyController {
     }
 
     @GetMapping("/{studyId}")
-    public ModelAndView site(@PathVariable("studyId") String studyId) {
+    public ModelAndView get(@PathVariable("studyId") String studyId) {
         StudyDetailVO study = studyRepository.getById(studyId);
 
         // List<XRefDocumentVO> crossReferences = xRefDocumentRepository.find(
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareDialect.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareDialect.java
new file mode 100644
index 00000000..2a02a842
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareDialect.java
@@ -0,0 +1,25 @@
+package fr.inra.urgi.faidare.web.thymeleaf;
+
+import org.springframework.stereotype.Component;
+import org.thymeleaf.dialect.AbstractDialect;
+import org.thymeleaf.dialect.IExpressionObjectDialect;
+import org.thymeleaf.expression.IExpressionObjectFactory;
+
+/**
+ * A thymeleaf dialect allowing to perform various tasks in the template related to Faidare
+ * @author JB Nizet
+ */
+@Component
+public class FaidareDialect extends AbstractDialect implements IExpressionObjectDialect {
+
+    private final IExpressionObjectFactory FAIDARE_EXPRESSION_OBJECTS_FACTORY = new FaidareExpressionFactory();
+
+    protected FaidareDialect() {
+        super("faidare");
+    }
+
+    @Override
+    public IExpressionObjectFactory getExpressionObjectFactory() {
+        return FAIDARE_EXPRESSION_OBJECTS_FACTORY;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressionFactory.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressionFactory.java
new file mode 100644
index 00000000..e873375c
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressionFactory.java
@@ -0,0 +1,33 @@
+package fr.inra.urgi.faidare.web.thymeleaf;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.thymeleaf.context.IExpressionContext;
+import org.thymeleaf.expression.IExpressionObjectFactory;
+
+/**
+ * The object factory for the {@link FaidareDialect}
+ * @author JB Nizet
+ */
+public class FaidareExpressionFactory implements IExpressionObjectFactory {
+    private static final String FAIDARE_EVALUATION_VARIABLE_NAME = "faidare";
+
+    private static final Set<String> ALL_EXPRESSION_OBJECT_NAMES =
+        Collections.singleton(FAIDARE_EVALUATION_VARIABLE_NAME);
+
+    @Override
+    public Set<String> getAllExpressionObjectNames() {
+        return ALL_EXPRESSION_OBJECT_NAMES;
+    }
+
+    @Override
+    public Object buildObject(IExpressionContext context, String expressionObjectName) {
+        return new FaidareExpressions(context.getLocale());
+    }
+
+    @Override
+    public boolean isCacheable(String expressionObjectName) {
+        return true;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java
new file mode 100644
index 00000000..a9f699de
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java
@@ -0,0 +1,65 @@
+package fr.inra.urgi.faidare.web.thymeleaf;
+
+import java.nio.charset.StandardCharsets;
+import java.text.DecimalFormat;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Function;
+
+import fr.inra.urgi.faidare.domain.data.germplasm.CollPopVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.TaxonSourceVO;
+import org.apache.logging.log4j.util.Strings;
+
+/**
+ * The actual object offering Faidare helper methods to thymeleaf
+ * @author JB Nizet
+ */
+public class FaidareExpressions {
+
+    private static final Map<String, Function<String, String>> TAXON_ID_URL_FACTORIES_BY_SOURCE_NAME =
+        createTaxonIdUrlFactories();
+
+    private static Map<String, Function<String, String>> createTaxonIdUrlFactories() {
+        Map<String, Function<String, String>> result = new HashMap<>();
+        result.put("NCBI", s -> "https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?mode=Info&id=" + s);
+        result.put("ThePlantList", s -> "http://www.theplantlist.org/tpl1.1/record/" + s);
+        result.put("TAXREF", s -> "https://inpn.mnhn.fr/espece/cd_nom/" + s);
+        result.put("CatalogueOfLife", s -> "http://www.catalogueoflife.org/col/details/species/id/" + s);
+        return Collections.unmodifiableMap(result);
+    }
+
+    private final Locale locale;
+
+    public FaidareExpressions(Locale locale) {
+        this.locale = locale;
+    }
+
+    public String toSiteParam(String siteId) {
+        return Base64.getUrlEncoder().encodeToString(("urn:URGI/location/" + siteId).getBytes(StandardCharsets.US_ASCII));
+    }
+
+    public String collPopTitle(CollPopVO collPopVO) {
+        return collPopTitle(collPopVO, Function.identity());
+    }
+
+    public String collPopTitleWithoutUnderscores(CollPopVO collPopVO) {
+        return collPopTitle(collPopVO, s -> s.replace('_', ' '));
+    }
+
+    public String taxonIdUrl(TaxonSourceVO taxonSource) {
+        Function<String, String> urlFactory =
+            TAXON_ID_URL_FACTORIES_BY_SOURCE_NAME.get(taxonSource.getSourceName());
+        return urlFactory != null ? urlFactory.apply(taxonSource.getTaxonId()) : null;
+    }
+
+    private String collPopTitle(CollPopVO collPopVO, Function<String, String> nameTransformer) {
+        if (Strings.isBlank(collPopVO.getType())) {
+            return nameTransformer.apply(collPopVO.getName());
+        } else {
+            return nameTransformer.apply(collPopVO.getName()) + " (" + collPopVO.getType() + ")";
+        }
+    }
+}
diff --git a/backend/src/main/resources/static/assets/style.css b/backend/src/main/resources/static/assets/style.css
index 59bcd117..340b22ea 100644
--- a/backend/src/main/resources/static/assets/style.css
+++ b/backend/src/main/resources/static/assets/style.css
@@ -1,3 +1,7 @@
 .label {
     font-weight: 500;
 }
+
+.popover {
+    max-width: min(80vw, 600px);
+}
diff --git a/backend/src/main/resources/templates/fragments/institute.html b/backend/src/main/resources/templates/fragments/institute.html
new file mode 100644
index 00000000..0efa76dd
--- /dev/null
+++ b/backend/src/main/resources/templates/fragments/institute.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+
+<html xmlns:th="http://www.thymeleaf.org">
+
+<body>
+
+<!--
+Reusable fragment displaying the content of an institute popover.
+Its unique argument (institute) is an InstituteVO
+-->
+
+<th:block th:fragment="institute(institute)">
+  <div class="text-center py-2" th:if="${institute.logo}">
+    <img th:src="${institute.logo}" />
+  </div>
+  <div th:replace="fragments/row::text-row(label='Code', text=${institute.instituteCode})"></div>
+  <div th:replace="fragments/row::text-row(label='Acronym', text=${institute.acronym})"></div>
+  <div th:replace="fragments/row::text-row(label='Organization', text=${institute.organisation})"></div>
+  <div th:replace="fragments/row::text-row(label='Type', text=${institute.instituteType})"></div>
+  <div th:replace="fragments/row::text-row(label='Address', text=${institute.address})"></div>
+
+  <th:block th:if="${institute.webSite}">
+    <div th:replace="fragments/row::row(label='Website', content=~{::.institute-website})">
+      <a class="institute-website"
+         target="_blank"
+         th:href="${institute.webSite}"
+         th:text="${#strings.abbreviate(institute.webSite, 25)}"></a>
+    </div>
+  </th:block>
+</th:block>
+
+</body>
+
+</html>
diff --git a/backend/src/main/resources/templates/fragments/link.html b/backend/src/main/resources/templates/fragments/link.html
new file mode 100644
index 00000000..d7c43bbd
--- /dev/null
+++ b/backend/src/main/resources/templates/fragments/link.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+
+<html xmlns:th="http://www.thymeleaf.org">
+
+<body>
+<!--
+Reusable fragment displaying a link with a label if the provided url is not
+empty, or a span with the label if the provided url is empty.
+Both arguments are strings.
+-->
+<th:block th:fragment="link(label, url)">
+  <a th:unless="${#strings.isEmpty(url)}"
+     th:href="${url}"
+     th:text="${label}"></a>
+  <span th:if="${#strings.isEmpty(url)}" th:text="${label}"></span>
+</th:block>
+
+</body>
+
+</html>
diff --git a/backend/src/main/resources/templates/fragments/row.html b/backend/src/main/resources/templates/fragments/row.html
index 5b523cef..e5eb9e7f 100644
--- a/backend/src/main/resources/templates/fragments/row.html
+++ b/backend/src/main/resources/templates/fragments/row.html
@@ -4,6 +4,21 @@
 
 <body>
 
+<!--
+Reusable fragment displaying a responsive row containing a label and a content.
+The label argument is a string.
+The content argument is a fragment which is displayed at the right of the label.
+
+Note that `th:if` is not evaluated when th:replace is used. So if this row must
+be displayed only if some condition is true, the fragment should be enclosed
+into a block with the condition:
+  <th:block th:if="${someCondition}">
+    <div th:replace="fragments/row::row(label='Some label', content=~{::#some-content-id})">
+      <span id="some-content-id">the content here</span>
+    </div>
+  </th:block>
+-->
+
 <div th:fragment="row(label, content)" class="row py-2">
   <div class="col-md-4 label pb-1 pb-md-0" th:text="${label}"></div>
   <div class="col">
@@ -11,7 +26,21 @@
   </div>
 </div>
 
-<div th:fragment="text-row(label, text)" th:if="${!#strings.isEmpty(text)}" class="row py-2">
+<!--
+Reusable fragment displaying a responsive row containing a label and a textual content.
+The label argument is a string.
+The text argument is a string which is displayed at the right of the label.
+The whole row is omitted if the textual content is empty, so the caller does not
+need to test that condition.
+
+Note that `th:if` is not evaluated when th:replace is used. So if this row must
+be displayed only if some other condition is true, the fragment should be enclosed
+into a block with the condition:
+  <th:block th:if="${someCondition}">
+    <div th:replace="fragments/row::text-row(label='Some label', text=${someTextExpression})"></div>
+  </th:block>
+-->
+<div th:fragment="text-row(label, text)" th:unless="${#strings.isEmpty(text)}" class="row py-2">
   <div class="col-md-4 label pb-1 pb-md-0" th:text="${label}"></div>
   <div class="col" th:text="${text}"></div>
 </div>
diff --git a/backend/src/main/resources/templates/fragments/source.html b/backend/src/main/resources/templates/fragments/source.html
new file mode 100644
index 00000000..795717ca
--- /dev/null
+++ b/backend/src/main/resources/templates/fragments/source.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+
+<html xmlns:th="http://www.thymeleaf.org">
+
+<body>
+
+<!--
+Reusable fragment displaying the source and the data links of an entity (site, study or germplasm).
+The source argument is a DataSource.
+The url argument is a string, which is the URL of the entity.
+The entityType argument is a string, which is used in the message
+"Link to this <entityType>".
+-->
+
+<th:block th:fragment="source(source, url, entityType)">
+  <th:block th:if="${source != null}">
+    <div th:replace="fragments/row::row(label='Source', content=~{::.source})">
+      <a class="source" target="_blank" th:href="${source.url}">
+        <img style="max-height: 60px;" th:src="${source.image}" th:alt="${source.name} + ' logo'" />
+      </a>
+    </div>
+  </th:block>
+
+  <th:block th:if="${url != null && source != null}">
+    <div th:replace="fragments/row::row(label='Data link', content=~{::.source-url})">
+      <a class="source-url" target="_blank" th:href="${url}">
+        Link to this <span th:text="${entityType}"></span> on <th:block th:text="${source.name}" />
+      </a>
+    </div>
+  </th:block>
+</th:block>
+
+</body>
+
+</html>
diff --git a/backend/src/main/resources/templates/fragments/xrefs.html b/backend/src/main/resources/templates/fragments/xrefs.html
index d51f1186..508b8923 100644
--- a/backend/src/main/resources/templates/fragments/xrefs.html
+++ b/backend/src/main/resources/templates/fragments/xrefs.html
@@ -4,6 +4,11 @@
 
 <body>
 
+<!--
+Reusable fragment displaying a cross references section, with its title.
+The unique argument (crossReferences) is a List<XRefDocumentVO>
+-->
+
 <div th:fragment="xrefs(crossReferences)" th:if="${!#lists.isEmpty(crossReferences)}">
   <h2>Cross references</h2>
 
diff --git a/backend/src/main/resources/templates/germplasm.html b/backend/src/main/resources/templates/germplasm.html
new file mode 100644
index 00000000..840b0d44
--- /dev/null
+++ b/backend/src/main/resources/templates/germplasm.html
@@ -0,0 +1,418 @@
+<!DOCTYPE html>
+
+<html
+  xmlns:th="http://www.thymeleaf.org"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
+>
+<head>
+  <title>Germplasm: <th:block th:text="${model.germplasm.germplasmName}" /></title>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+</head>
+
+<body>
+<main>
+  <div class="d-flex">
+    <h1 class="flex-grow-1">Germplasm: <th:block th:text="${model.germplasm.germplasmName}" /></h1>
+    <div th:if="${model.germplasm.holdingGenbank != null && model.germplasm.holdingGenbank.logo != null}">
+      <img th:src="${model.germplasm.holdingGenbank.logo}" th:alt="${model.germplasm.holdingGenbank.instituteName}" />
+    </div>
+  </div>
+
+  <div class="row align-items-center justify-content-center">
+    <div class="col-auto field" th:if="${model.germplasm.photo != null && model.germplasm.photo.thumbnailFile != null}">
+      <template id="photo-popover">
+        <div class="card">
+          <img th:src="${model.germplasm.photo.file}" class="card-img-top" alt="">
+          <div class="card-body">
+            <div th:replace="fragments/row::text-row(label='Accession name', text=${model.germplasm.germplasmName})"></div>
+            <div th:replace="fragments/row::text-row(label='Photo name', text=${model.germplasm.photo.photoName})"></div>
+            <div th:replace="fragments/row::text-row(label='Description', text=${model.germplasm.photo.description})"></div>
+            <div th:replace="fragments/row::text-row(label='Copyright', text=${model.germplasm.photo.copyright})"></div>
+          </div>
+        </div>
+      </template>
+
+      <button class="btn btn-link p-0"
+              data-bs-toggle="popover"
+              th:data-bs-title="${model.germplasm.photo.photoName}"
+              data-bs-element="#photo-popover"
+              data-bs-container="body"
+              data-bs-trigger="focus">
+        <img th:src="${model.germplasm.photo.thumbnailFile}" class="img-fluid" />
+
+        <figcaption class="figure-caption">
+          © <span th:text="${model.germplasm.photo.copyright}"></span>
+        </figcaption>
+      </button>
+    </div>
+
+
+    <div class="col-12 col-lg">
+      <h2>Identification</h2>
+
+      <div th:replace="fragments/row::text-row(label='Germplasm name', text=${model.germplasm.germplasmName})"></div>
+      <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.accessionNumber})"></div>
+
+      <div th:replace="fragments/source::source(source=${model.source}, url=${model.germplasm.url}, entityType='germplasm')"></div>
+
+      <th:block th:unless="${#lists.isEmpty(model.germplasm.synonyms)}">
+        <div th:replace="fragments/row::row(label='Accession synonyms', content=~{::#accession-synonyms})">
+          <div id="accession-synonyms" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.synonyms, ', ')}"></div>
+        </div>
+      </th:block>
+
+      <th:block th:unless="${#strings.isEmpty(model.taxon)}">
+        <div th:replace="fragments/row::row(label='Taxon', content=~{::#taxon})">
+          <div id="taxon">
+            <template id="taxon-popover">
+              <th:block th:unless="${#strings.isEmpty(model.germplasm.genus)}">
+                <div th:replace="fragments/row::row(label='Genus', content=~{::#taxon-genus})">
+                  <em id="taxon-genus" th:text="${model.germplasm.genus}"></em>
+                </div>
+              </th:block>
+              <th:block th:unless="${#strings.isEmpty(model.germplasm.species)}">
+                <div th:replace="fragments/row::row(label='Species', content=~{::#taxon-species})">
+                  <span id="taxon-species">
+                    <em th:text="${model.germplasm.species}"></em>
+                    <span th:unless="${#strings.isEmpty(model.germplasm.speciesAuthority)}"
+                          th:text="${'(' + model.germplasm.speciesAuthority + ')'}"></span>
+                  </span>
+                </div>
+              </th:block>
+              <th:block th:unless="${#strings.isEmpty(model.germplasm.subtaxa)}">
+                <div th:replace="fragments/row::row(label='Subtaxa', content=~{::#taxon-subtaxa})">
+                  <span id="taxon-subtaxa">
+                    <em th:text="${model.germplasm.subtaxa}"></em>
+                    <span th:unless="${#strings.isEmpty(model.germplasm.subtaxaAuthority)}"
+                          th:text="${'(' + model.germplasm.subtaxaAuthority + ')'}"></span>
+                  </span>
+                </div>
+              </th:block>
+
+              <div th:replace="fragments/row::text-row(label='Authority', text=${model.taxonAuthor})"></div>
+
+              <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonIds)}">
+                <div th:replace="fragments/row::row(label='Taxon IDs', content=~{::#taxon-ids})">
+                  <div id="taxon-ids">
+                    <div th:each="taxonId : ${model.germplasm.taxonIds}" class="row">
+                      <div class="col-6 text-nowrap" th:text="${taxonId.sourceName}"></div>
+                      <div class="col-6">
+                        <span class="taxon-id"
+                              th:replace="fragments/link::link(label=${taxonId.taxonId}, url=${#faidare.taxonIdUrl(taxonId)})"></span>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+              </th:block>
+
+              <div th:replace="fragments/row::text-row(label='Comment', text=${model.germplasm.taxonComment})"></div>
+              <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonCommonNames)}">
+                <div th:replace="fragments/row::row(label='Taxon common names', content=~{::#taxon-common-names})">
+                  <div id="taxon-common-names" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.taxonCommonNames, ', ')}"></div>
+                </div>
+              </th:block>
+              <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonSynonyms)}">
+                <div th:replace="fragments/row::row(label='Taxon common names', content=~{::#taxon-synonyms})">
+                  <div id="taxon-synonyms" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.taxonSynonyms, ', ')}"></div>
+                </div>
+              </th:block>
+            </template>
+            <button class="btn btn-link p-0"
+                    data-bs-toggle="popover"
+                    th:data-bs-title="${model.taxon}"
+                    data-bs-element="#taxon-popover"
+                    data-bs-container="body"
+                    data-bs-trigger="focus">
+              <em th:text="${model.taxon}"></em>
+              <th:block th:unless="${#strings.isEmpty(model.taxonAuthor)}">(<span th:text="${model.taxonAuthor}"></span>)</th:block>
+            </button>
+          </div>
+        </div>
+      </th:block>
+
+      <div th:replace="fragments/row::text-row(label='Biological status', text=${model.germplasm.biologicalStatusOfAccessionCode})"></div>
+      <div th:replace="fragments/row::text-row(label='Genetic nature', text=${model.germplasm.geneticNature})"></div>
+      <div th:replace="fragments/row::text-row(label='Seed source', text=${model.germplasm.seedSource})"></div>
+      <div th:replace="fragments/row::text-row(label='Pedigree', text=${model.germplasm.pedigree})"></div>
+      <div th:replace="fragments/row::text-row(label='Comments', text=${model.germplasm.comment})"></div>
+
+      <th:block th:if="${model.germplasm.originSite != null && !#strings.isEmpty(model.germplasm.originSite.siteName)}">
+        <div th:replace="fragments/row::row(label='Origin site', content=~{::#origin-site})">
+          <a id="origin-site" th:href="@{/sites/{siteId}(siteId=${#faidare.toSiteParam(model.germplasm.originSite.siteId)})}" th:text="${model.germplasm.originSite.siteName}"></a>
+        </div>
+      </th:block>
+    </div>
+  </div>
+
+  <th:block th:if="${model.germplasm.holdingInstitute}">
+    <h2>Depositary</h2>
+    <template id="holding-institute-popover">
+      <div th:replace="fragments/institute::institute(institute=${model.germplasm.holdingInstitute})"></div>
+    </template>
+    <div th:replace="fragments/row::row(label='Institution', content=~{::#institution})">
+      <button id="institution"
+              data-bs-toggle="popover"
+              th:data-bs-title="${model.germplasm.holdingInstitute.instituteName}"
+              data-bs-element="#holding-institute-popover"
+              data-bs-container="body"
+              data-bs-trigger="focus"
+              class="btn btn-link p-0"
+              th:text="${model.germplasm.holdingInstitute.instituteName}"></button>
+    </div>
+
+    <th:block th:if="${model.germplasm.holdingGenbank != null && !#strings.isEmpty(model.germplasm.holdingGenbank.instituteName) && !#strings.isEmpty(model.germplasm.holdingGenbank.webSite)}">
+      <div th:replace="fragments/row::row(label='Stock center name', content=~{::#stock-center-name})">
+        <a id="stock-center-name"
+           target="_blank"
+           th:href="${model.germplasm.holdingGenbank.webSite}"
+           th:text="${model.germplasm.holdingGenbank.instituteName}"></a>
+      </div>
+    </th:block>
+
+    <div th:replace="fragments/row::text-row(label='Presence status', text=${model.germplasm.presenceStatus})"></div>
+  </th:block>
+
+  <th:block th:if="${model.collecting}">
+    <h2>Collector</h2>
+    <th:block th:if="${model.germplasm.collectingSite != null && !#strings.isEmpty(model.germplasm.collectingSite.siteName)}">
+      <div th:replace="fragments/row::row(label='Collecting site', content=~{::#collecting-site})">
+        <a id="collecting-site"
+           th:href="@{/sites/{siteId}(siteId=${#faidare.toSiteParam(model.germplasm.collectingSite.siteId)})}"
+           th:text="${model.germplasm.collectingSite.siteName}"
+        ></a>
+      </div>
+    </th:block>
+
+    <div th:replace="fragments/row::text-row(label='Material type', text=${model.germplasm.collector.materialType})"></div>
+    <div th:replace="fragments/row::text-row(label='Collectors', text=${model.germplasm.collector.collectors})"></div>
+
+    <th:block th:if="${!#strings.isEmpty(model.germplasm.acquisitionDate) && model.germplasm.collector.accessionCreationDate == null}">
+      <div th:replace="fragments/row::text-row(label='Acquisition / Creation date', text=${model.germplasm.acquisitionDate})"></div>
+    </th:block>
+
+    <th:block th:if="${model.germplasm.collector.institute != null && !#strings.isEmpty(model.germplasm.collector.institute.instituteName)}">
+      <template id="collector-institute-popover">
+        <div th:replace="fragments/institute::institute(institute=${model.germplasm.collector.institute})"></div>
+      </template>
+      <div th:replace="fragments/row::row(label='Institution', content=~{::#collecting-institution})">
+        <button id="collecting-institution"
+                data-bs-toggle="popover"
+                th:data-bs-title="${model.germplasm.collector.institute.instituteName}"
+                data-bs-element="#collector-institute-popover"
+                data-bs-container="body"
+                data-bs-trigger="focus"
+                class="btn btn-link p-0"
+                th:text="${model.germplasm.collector.institute.instituteName}"></button>
+      </div>
+    </th:block>
+
+    <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.collector.accessionNumber})"></div>
+  </th:block>
+
+  <th:block th:if="${model.breeding}">
+    <h2>Breeder</h2>
+    <th:block th:if="${model.germplasm.breeder.institute != null && !#strings.isEmpty(model.germplasm.breeder.institute.instituteName)}">
+      <template id="breeder-institute-popover">
+        <div th:replace="fragments/institute::institute(institute=${model.germplasm.breeder.institute})"></div>
+      </template>
+      <div th:replace="fragments/row::row(label='Institute', content=~{::#breeding-institution})">
+        <button id="breeding-institution"
+                data-bs-toggle="popover"
+                th:data-bs-title="${model.germplasm.breeder.institute.instituteName}"
+                data-bs-element="#breeder-institute-popover"
+                data-bs-container="body"
+                data-bs-trigger="focus"
+                class="btn btn-link p-0"
+                th:text="${model.germplasm.breeder.institute.instituteName}"></button>
+      </div>
+    </th:block>
+
+    <div th:replace="fragments/row::text-row(label='Accession creation year', text=${model.germplasm.breeder.accessionCreationDate})"></div>
+    <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.breeder.accessionNumber})"></div>
+    <div th:replace="fragments/row::text-row(label='Catalog registration year', text=${model.germplasm.breeder.registrationYear})"></div>
+    <div th:replace="fragments/row::text-row(label='Catalog deregistration year', text=${model.germplasm.breeder.deregistrationYear})"></div>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.germplasm.donors)}">
+    <h2>Donors</h2>
+    <div class="table-responsive scroll-table table-card-body">
+      <div class="card">
+        <table class="table table-sm table-striped">
+          <thead>
+            <tr>
+              <th scope="col">Institute name</th>
+              <th scope="col">Institute code</th>
+              <th scope="col">Donation date</th>
+              <th scope="col">Accession number</th>
+              <th scope="col">Accession PUI</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row, donorIterStat : ${model.germplasm.donors}">
+              <td>
+                <template th:id="${'donor-institute-popover-' + donorIterStat.index}">
+                  <div th:replace="fragments/institute::institute(institute=${row.donorInstitute})"></div>
+                </template>
+                <button data-bs-toggle="popover"
+                        th:data-bs-title="${row.donorInstitute.instituteName}"
+                        th:data-bs-element="${'#donor-institute-popover-' + donorIterStat.index}"
+                        data-bs-container="body"
+                        data-bs-trigger="focus"
+                        class="btn btn-link p-0"
+                        th:text="${row.donorInstitute.instituteName}"></button>
+              </td>
+              <td th:text="${row.donorInstituteCode}"></td>
+              <td th:text="${row.donationDate}"></td>
+              <td th:text="${row.donorAccessionNumber}"></td>
+              <td th:text="${row.donorGermplasmPUI}"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.germplasm.distributors)}">
+    <h2>Donors</h2>
+    <div class="table-responsive scroll-table table-card-body">
+      <div class="card">
+        <table class="table table-sm table-striped">
+          <thead>
+            <tr>
+              <th scope="col">Institute</th>
+              <th scope="col">Accession number</th>
+              <th scope="col">Distribution status</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr th:each="row, distributorIterStat : ${model.germplasm.distributors}">
+              <td>
+                <template th:id="${'distributor-institute-popover-' + distributorIterStat.index}">
+                  <div th:replace="fragments/institute::institute(institute=${row.institute})"></div>
+                </template>
+                <button data-bs-toggle="popover"
+                        th:data-bs-title="${row.institute.instituteName}"
+                        th:data-bs-element="${'#distributor-institute-popover-' + distributorIterStat.index}"
+                        data-bs-container="body"
+                        data-bs-trigger="focus"
+                        class="btn btn-link p-0"
+                        th:text="${row.institute.instituteName}"></button>
+              </td>
+              <td th:text="${row.accessionNumber}"></td>
+              <td th:text="${row.distributionStatus}"></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.attributes)}">
+    <h2>Evaluation Data</h2>
+    <th:block th:each="descriptor : ${model.attributes}">
+      <div th:replace="fragments/row::text-row(label=${descriptor.attributeName}, text=${descriptor.value})"></div>
+    </th:block>
+  </th:block>
+
+  <th:block th:if="${model.genealogyPresent}">
+    <h2>Genealogy</h2>
+
+    <th:block th:if="${model.pedigree != null}">
+      <div th:replace="fragments/row::text-row(label='Crossing plan', text=${model.pedigree.crossingPlan})"></div>
+      <div th:replace="fragments/row::text-row(label='Crossing year', text=${model.pedigree.crossingYear})"></div>
+      <div th:replace="fragments/row::text-row(label='Family code', text=${model.pedigree.familyCode})"></div>
+      <th:block th:unless="${#strings.isEmpty(model.pedigree.parent1Name)}">
+        <div th:replace="fragments/row::row(label='Parent accessions', content=~{::#parent-accessions})">
+          <div id="parent-accessions">
+            <th:block th:if="${model.pedigree.parent1DbId}">
+              <div th:replace="fragments/row::row(label=${model.pedigree.parent1Type}, content=~{::#parent1-link})">
+                <a id="parent1-link" th:href="@{/germplasms/{germplasmId}(germplasmId=${model.pedigree.parent1DbId})}" th:text="${model.pedigree.parent1Name}"></a>
+              </div>
+            </th:block>
+
+            <th:block th:if="${model.pedigree.parent2DbId}">
+              <div th:replace="fragments/row::row(label=${model.pedigree.parent2Type}, content=~{::#parent2-link})">
+                <a id="parent2-link" th:href="@{/germplasms/{germplasmId}(germplasmId=${model.pedigree.parent2DbId})}" th:text="${model.pedigree.parent2Name}"></a>
+              </div>
+            </th:block>
+          </div>
+        </div>
+      </th:block>
+
+      <th:block th:unless="${#lists.isEmpty(model.pedigree.siblings)}">
+        <div th:replace="fragments/row::row(label='Sibling accessions', content=~{::#sibling-accessions})">
+          <div id="sibling-accessions" class="content-overflow">
+            <a th:each="sibling : ${model.pedigree.siblings}"
+               th:href="@{/germplasms/{germplasmId}(germplasmId=${sibling.germplasmDbId})}"
+               th:text="${sibling.defaultDisplayName}"></a>
+          </div>
+        </div>
+      </th:block>
+    </th:block>
+
+    <th:block th:unless="${#lists.isEmpty(model.germplasm.children)}">
+      <div th:replace="fragments/row::row(label='Descendants', content=~{::#descendants})">
+        <div id="descendants" class="content-overflow-big">
+          <th:block th:each="child : ${model.germplasm.children}">
+            <div th:replace="fragments/row::row(label=${#strings.isEmpty(child.secondParentName) ? ('children of ' + child.firstParentName) : ('children of ' + child.firstParentName + ' x ' + child.secondParentName) }, content=~{::.descendant-child})">
+              <div class="descendant-child">
+                <th:block th:each="sibling, siblingIterStat : ${child.sibblings}">
+                  <a th:href="@{/germplasms(pui=${sibling.pui})}"
+                     th:text="${sibling.name}"></a><th:block th:unless="${siblingIterStat.last}">, </th:block>
+                </th:block>
+              </div>
+            </div>
+          </th:block>
+        </div>
+      </div>
+    </th:block>
+
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.germplasm.population)}">
+    <h2>Population</h2>
+    <th:block th:each="population : ${model.germplasm.population}">
+
+      <th:block th:if="${population.germplasmRef != null}">
+        <th:block th:unless="${#strings.isEmpty(population.germplasmRef.pui)}">
+          <div th:replace="fragments/row::row(label=${#faidare.collPopTitle(population)}, content=~{::.population-1})">
+            <div class="population-1">
+              <a th:if="${population.germplasmRef.pui != model.germplasm.germplasmPUI}"
+                 th:href="@{/germplasms(pui=${population.germplasmRef.pui})}"
+                 th:text="${population.germplasmRef.name}"></a>
+              <span th:if="${population.germplasmRef.pui == model.germplasm.germplasmPUI}"
+                    th:text="${population.germplasmRef.name}"></span>
+              is composed by <span th:text="${population.germplasmCount}"></span> accession(s)
+              <!-- TODO there was a link pointing at a search here -->
+            </div>
+          </div>
+        </th:block>
+      </th:block>
+
+      <th:block th:if="${population.germplasmRef == null}">
+        <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitle(population)}, text=${population.germplasmCount + ' accession(s)'})"></div>
+        <!-- TODO there was a link pointing at a search here -->
+      </th:block>
+    </th:block>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.germplasm.collection)}">
+    <h2>Collection</h2>
+    <th:block th:each="collection : ${model.germplasm.collection}">
+      <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitle(collection)}, text=${collection.germplasmCount + ' accession(s)'})"></div>
+      <!-- TODO there was a link pointing at a search here -->
+    </th:block>
+  </th:block>
+
+  <th:block th:unless="${#lists.isEmpty(model.germplasm.panel)}">
+    <h2>Panel</h2>
+    <th:block th:each="panel : ${model.germplasm.panel}">
+      <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitleWithoutUnderscores(panel)}, text=${panel.germplasmCount + ' accession(s)'})"></div>
+      <!-- TODO there was a link pointing at a search here -->
+    </th:block>
+  </th:block>
+
+  <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
+</main>
+</body>
+</html>
diff --git a/backend/src/main/resources/templates/layout/main.html b/backend/src/main/resources/templates/layout/main.html
index 22bcdfd5..4cd33f70 100644
--- a/backend/src/main/resources/templates/layout/main.html
+++ b/backend/src/main/resources/templates/layout/main.html
@@ -26,5 +26,27 @@
         common footer
       </footer>
     </div>
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
+    <script type="text/javascript">
+      const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
+      popoverTriggerList.forEach(popoverTriggerEl => {
+        const options = {};
+        const contentSelector = popoverTriggerEl.dataset.bsElement;
+        if (contentSelector) {
+          const content = document.querySelector(contentSelector);
+          if (content) {
+            options.content = () => {
+              const element = document.createElement('div');
+              element.innerHTML = content.innerHTML;
+              return element;
+            };
+            options.html = true;
+          } else {
+            throw new Error('element with selector ' + contentSelector + ' not found');
+          }
+        }
+        return new bootstrap.Popover(popoverTriggerEl, options);
+      });
+    </script>
   </body>
 </html>
diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html
index ad6affc4..d5d65e5c 100644
--- a/backend/src/main/resources/templates/site.html
+++ b/backend/src/main/resources/templates/site.html
@@ -17,22 +17,7 @@
     <div th:replace="fragments/row::text-row(label='Permanent unique identifier', text=${model.site.uri})"></div>
   </th:block>
 
-  <th:block th:if="${model.source != null}">
-    <div th:replace="fragments/row::row(label='Source', content=~{::#source})">
-      <a id="source" target="_blank" th:href="${model.source.url}">
-        <img style="max-height: 60px;" th:src="${model.source.image}" th:alt="${model.source.name} + ' logo'" />
-      </a>
-    </div>
-  </th:block>
-
-  <th:block th:if="${model.site.url != null && model.source != null}">
-    <div
-         th:replace="fragments/row::row(label='Data link', content=~{::#url})">
-      <a id="url" target="_blank" th:href="${model.site.url}">
-        Link to this site on <th:block th:text="${model.source.name}" />
-      </a>
-    </div>
-  </th:block>
+  <div th:replace="fragments/source::source(source=${model.source}, url=${model.site.url}, entityType='site')"></div>
 
   <div th:replace="fragments/row::text-row(label='Abbreviation', text=${model.site.abbreviation})"></div>
   <div th:replace="fragments/row::text-row(label='Type', text=${model.site.locationType})"></div>
diff --git a/backend/src/main/resources/templates/study.html b/backend/src/main/resources/templates/study.html
index a1cafc65..7bd5bbbd 100644
--- a/backend/src/main/resources/templates/study.html
+++ b/backend/src/main/resources/templates/study.html
@@ -18,21 +18,7 @@
   <div th:replace="fragments/row::text-row(label='Name', text=${model.study.studyName})"></div>
   <div th:replace="fragments/row::text-row(label='Identifier', text=${model.study.studyDbId})"></div>
 
-  <th:block th:if="${model.source != null}">
-    <div th:replace="fragments/row::row(label='Source', content=~{::#source}, text='')">
-      <a id="source" target="_blank" th:href="${model.source.url}">
-        <img style="max-height: 60px;" th:src="${model.source.image}" th:alt="${model.source.name} + ' logo'" />
-      </a>
-    </div>
-  </th:block>
-
-  <th:block th:if="${model.study.url != null && model.source != null}">
-    <div th:replace="fragments/row::row(label='Data link', content=~{::#url}, text='')">
-      <a id="url" target="_blank" th:href="${model.study.url}">
-        Link to this study on <th:block th:text="${model.source.name}" />
-      </a>
-    </div>
-  </th:block>
+  <div th:replace="fragments/source::source(source=${model.source}, url=${model.study.url}, entityType='study')"></div>
 
   <div th:replace="fragments/row::text-row(label='Project name', text=${model.study.programName})"></div>
   <div th:replace="fragments/row::text-row(label='Description', text=${model.study.studyDescription})"></div>
-- 
GitLab


From 1fd79b03b91ad222342431fb92a8d2db29aa0781 Mon Sep 17 00:00:00 2001
From: jnizet <jb@ninja-squad.com>
Date: Thu, 26 Aug 2021 21:32:24 +0200
Subject: [PATCH 13/16] feat: maps

---
 .../fr/inra/urgi/faidare/utils/Sites.java     |  14 +++
 .../web/germplasm/GermplasmController.java    |  28 ++++-
 .../faidare/web/germplasm/GermplasmModel.java |  18 ++++
 .../urgi/faidare/web/site/MapLocation.java    |  82 ++++++++++++++
 .../inra/urgi/faidare/web/site/SiteModel.java |   4 +
 .../faidare/web/study/StudyController.java    |  26 ++++-
 .../urgi/faidare/web/study/StudyModel.java    |  16 ++-
 .../web/thymeleaf/FaidareExpressions.java     |  15 ++-
 .../static/assets/images/marker-icon-blue.png | Bin 0 -> 1747 bytes
 .../assets/images/marker-icon-green.png       | Bin 0 -> 1875 bytes
 .../assets/images/marker-icon-purple.png      | Bin 0 -> 4446 bytes
 .../static/assets/images/marker-icon-red.png  | Bin 0 -> 1869 bytes
 .../main/resources/static/assets/script.js    | 100 ++++++++++++++++++
 .../main/resources/static/assets/style.css    |   8 ++
 .../resources/templates/fragments/map.html    |  23 ++++
 .../main/resources/templates/germplasm.html   |  12 ++-
 .../main/resources/templates/layout/main.html |  34 +++---
 .../src/main/resources/templates/site.html    |  11 +-
 .../src/main/resources/templates/study.html   |  11 +-
 19 files changed, 366 insertions(+), 36 deletions(-)
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java
 create mode 100644 backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java
 create mode 100644 backend/src/main/resources/static/assets/images/marker-icon-blue.png
 create mode 100644 backend/src/main/resources/static/assets/images/marker-icon-green.png
 create mode 100644 backend/src/main/resources/static/assets/images/marker-icon-purple.png
 create mode 100644 backend/src/main/resources/static/assets/images/marker-icon-red.png
 create mode 100644 backend/src/main/resources/static/assets/script.js
 create mode 100644 backend/src/main/resources/templates/fragments/map.html

diff --git a/backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java b/backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java
new file mode 100644
index 00000000..67fde44b
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java
@@ -0,0 +1,14 @@
+package fr.inra.urgi.faidare.utils;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * Utilities for sites
+ * @author JB Nizet
+ */
+public class Sites {
+    public static String siteIdToLocationId(String siteId) {
+        return Base64.getUrlEncoder().encodeToString(("urn:URGI/location/" + siteId).getBytes(StandardCharsets.US_ASCII));
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java
index cf343049..cd24d06d 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java
@@ -1,5 +1,6 @@
 package fr.inra.urgi.faidare.web.germplasm;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
@@ -105,7 +106,6 @@ public class GermplasmController {
             createXref("bazbing")
         );
 
-
         sortDonors(germplasm);
         sortPopulations(germplasm);
         sortCollections(germplasm);
@@ -117,8 +117,7 @@ public class GermplasmController {
                                     faidareProperties.getByUri(germplasm.getSourceUri()),
                                     attributes,
                                     pedigree,
-                                    crossReferences
-                                )
+                                    crossReferences)
         );
     }
 
@@ -205,8 +204,23 @@ public class GermplasmController {
         SiteVO originSite = new SiteVO();
         originSite.setSiteId("1234");
         originSite.setSiteName("Le Moulon");
+        originSite.setSiteType("Origin site");
+        originSite.setLatitude(47.0F);
+        originSite.setLongitude(12.0F);
         result.setOriginSite(originSite);
 
+        List<SiteVO> evaluationSites = new ArrayList<>();
+        for (int i = 0; i < 5; i++) {
+            SiteVO evaluationSite = new SiteVO();
+            evaluationSite.setSiteId(Integer.toString(12347 + i));
+            evaluationSite.setSiteType("Evaluation site");
+            evaluationSite.setSiteName("Site " + i);
+            evaluationSite.setLatitude(46.0F + i);
+            evaluationSite.setLongitude(13.0F + i);
+            evaluationSites.add(evaluationSite);
+        }
+        result.setEvaluationSites(evaluationSites);
+
         result.setGenus("Genus 1");
         result.setSpecies("Species 1");
         result.setSpeciesAuthority("Species Auth");
@@ -241,7 +255,13 @@ public class GermplasmController {
         collector.setAccessionNumber("567");
         result.setCollector(collector);
 
-        result.setCollectingSite(originSite);
+        SiteVO collectingSite = new SiteVO();
+        collectingSite.setSiteId("1235");
+        collectingSite.setSiteName("St Just");
+        collectingSite.setSiteType("Collecting site");
+        collectingSite.setLatitude(48.0F);
+        collectingSite.setLongitude(13.0F);
+        result.setCollectingSite(collectingSite);
         result.setAcquisitionDate("In the summer");
 
         GermplasmInstituteVO breeder = new GermplasmInstituteVO();
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java
index 8acdf78f..ffa24336 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java
@@ -1,13 +1,16 @@
 package fr.inra.urgi.faidare.web.germplasm;
 
+import java.util.ArrayList;
 import java.util.List;
 
 import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiGermplasmAttributeValue;
 import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmInstituteVO;
 import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
 import fr.inra.urgi.faidare.domain.data.germplasm.PedigreeVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.SiteVO;
 import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource;
 import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+import fr.inra.urgi.faidare.web.site.MapLocation;
 import org.apache.logging.log4j.util.Strings;
 
 /**
@@ -136,4 +139,19 @@ public final class GermplasmModel {
             || Strings.isNotBlank(this.pedigree.getCrossingYear())
             || Strings.isNotBlank(this.pedigree.getFamilyCode()));
     }
+
+    public List<MapLocation> getMapLocations() {
+        List<SiteVO> sites = new ArrayList<>();
+        if (germplasm.getCollectingSite() != null) {
+            sites.add(germplasm.getCollectingSite());
+        }
+        if (germplasm.getOriginSite() != null) {
+            sites.add(germplasm.getOriginSite());
+        }
+        if (germplasm.getEvaluationSites() != null) {
+            sites.addAll(germplasm.getEvaluationSites());
+        }
+
+        return MapLocation.sitesToDisplayableMapLocations(sites);
+    }
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java
new file mode 100644
index 00000000..3b096853
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java
@@ -0,0 +1,82 @@
+package fr.inra.urgi.faidare.web.site;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import fr.inra.urgi.faidare.domain.data.LocationVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.SiteVO;
+import fr.inra.urgi.faidare.utils.Sites;
+
+/**
+ * An object that can be serialized to JSON to serve as a map marker.
+ * @author JB Nizet
+ */
+public final class MapLocation {
+    private final String locationDbId;
+    private final String locationType;
+    private final String locationName;
+    private final double latitude;
+    private final double longitude;
+
+    public MapLocation(String locationDbId,
+                       String locationType,
+                       String locationName,
+                       double latitude,
+                       double longitude) {
+        this.locationDbId = locationDbId;
+        this.locationType = locationType;
+        this.locationName = locationName;
+        this.latitude = latitude;
+        this.longitude = longitude;
+    }
+
+    public MapLocation(LocationVO site) {
+        this(site.getLocationDbId(),
+             site.getLocationType(),
+             site.getLocationName(),
+             site.getLatitude(),
+             site.getLongitude());
+    }
+
+    public MapLocation(SiteVO site) {
+        this(Sites.siteIdToLocationId(site.getSiteId()),
+             site.getSiteType(),
+             site.getSiteName(),
+             site.getLatitude(),
+             site.getLongitude());
+    }
+
+    public static List<MapLocation> locationsToDisplayableMapLocations(List<LocationVO> locations) {
+        return locations.stream()
+                        .filter(location -> location.getLatitude() != null && location.getLongitude() != null)
+                        .map(MapLocation::new)
+                        .collect(Collectors.toList());
+    }
+
+    public static List<MapLocation> sitesToDisplayableMapLocations(List<SiteVO> sites) {
+        return sites.stream()
+                    .filter(site -> site.getLatitude() != null && site.getLongitude() != null)
+                    .map(MapLocation::new)
+                    .collect(Collectors.toList());
+    }
+
+    public String getLocationDbId() {
+        return locationDbId;
+    }
+
+    public String getLocationType() {
+        return locationType;
+    }
+
+    public String getLocationName() {
+        return locationName;
+    }
+
+    public double getLatitude() {
+        return latitude;
+    }
+
+    public double getLongitude() {
+        return longitude;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
index 84cd92e2..0d7e1c0d 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
@@ -111,4 +111,8 @@ public final class SiteModel {
     public List<XRefDocumentVO> getCrossReferences() {
         return crossReferences;
     }
+
+    public List<MapLocation> getMapLocations() {
+        return MapLocation.locationsToDisplayableMapLocations(Collections.singletonList(this.site));
+    }
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
index ff6aee3b..833c4547 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
@@ -12,16 +12,20 @@ import com.google.common.collect.Lists;
 import fr.inra.urgi.faidare.api.NotFoundException;
 import fr.inra.urgi.faidare.config.FaidareProperties;
 import fr.inra.urgi.faidare.domain.criteria.GermplasmPOSTSearchCriteria;
+import fr.inra.urgi.faidare.domain.data.LocationVO;
 import fr.inra.urgi.faidare.domain.data.TrialVO;
 import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
 import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO;
 import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO;
 import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
 import fr.inra.urgi.faidare.repository.es.GermplasmRepository;
+import fr.inra.urgi.faidare.repository.es.LocationRepository;
 import fr.inra.urgi.faidare.repository.es.StudyRepository;
 import fr.inra.urgi.faidare.repository.es.TrialRepository;
 import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository;
 import fr.inra.urgi.faidare.repository.file.CropOntologyRepository;
+import fr.inra.urgi.faidare.web.site.MapLocation;
+import org.apache.logging.log4j.util.Strings;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
@@ -42,19 +46,22 @@ public class StudyController {
     private final GermplasmRepository germplasmRepository;
     private final CropOntologyRepository cropOntologyRepository;
     private final TrialRepository trialRepository;
+    private final LocationRepository locationRepository;
 
     public StudyController(StudyRepository studyRepository,
                            FaidareProperties faidareProperties,
                            XRefDocumentRepository xRefDocumentRepository,
                            GermplasmRepository germplasmRepository,
                            CropOntologyRepository cropOntologyRepository,
-                           TrialRepository trialRepository) {
+                           TrialRepository trialRepository,
+                           LocationRepository locationRepository) {
         this.studyRepository = studyRepository;
         this.faidareProperties = faidareProperties;
         this.xRefDocumentRepository = xRefDocumentRepository;
         this.germplasmRepository = germplasmRepository;
         this.cropOntologyRepository = cropOntologyRepository;
         this.trialRepository = trialRepository;
+        this.locationRepository = locationRepository;
     }
 
     @GetMapping("/{studyId}")
@@ -77,6 +84,11 @@ public class StudyController {
         List<GermplasmVO> germplasms = getGermplasms(study);
         List<ObservationVariableVO>variables = getVariables(study);
         List<TrialVO> trials = getTrials(study);
+        LocationVO location = getLocation(study);
+
+        // TODO remove this
+        location.setLatitude(34.0);
+        location.setLongitude(14.0);
 
         return new ModelAndView("study",
                                 "model",
@@ -86,11 +98,19 @@ public class StudyController {
                                     germplasms,
                                     variables,
                                     trials,
-                                    crossReferences
+                                    crossReferences,
+                                    location
                                 )
         );
     }
 
+    private LocationVO getLocation(StudyDetailVO study) {
+        if (Strings.isBlank(study.getLocationDbId())) {
+            return null;
+        }
+        return locationRepository.getById(study.getLocationDbId());
+    }
+
     private List<GermplasmVO> getGermplasms(StudyDetailVO study) {
         if (study.getGermplasmDbIds() == null || study.getGermplasmDbIds().isEmpty()) {
             return Collections.emptyList();
@@ -125,6 +145,8 @@ public class StudyController {
                     .collect(Collectors.toList());
     }
 
+
+
     private XRefDocumentVO createXref(String name) {
         XRefDocumentVO xref = new XRefDocumentVO();
         xref.setName(name);
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
index 980c78d4..bc77dfc3 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
@@ -1,11 +1,8 @@
 package fr.inra.urgi.faidare.web.study;
 
-import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 import fr.inra.urgi.faidare.domain.data.LocationVO;
@@ -15,6 +12,7 @@ import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO;
 import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO;
 import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource;
 import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+import fr.inra.urgi.faidare.web.site.MapLocation;
 
 /**
  * The model used by the study page
@@ -27,6 +25,7 @@ public final class StudyModel {
     private final List<ObservationVariableVO> variables;
     private final List<TrialVO> trials;
     private final List<XRefDocumentVO> crossReferences;
+    private final LocationVO location;
     private final List<Map.Entry<String, Object>> additionalInfoProperties;
 
     public StudyModel(StudyDetailVO study,
@@ -34,13 +33,15 @@ public final class StudyModel {
                       List<GermplasmVO> germplasms,
                       List<ObservationVariableVO> variables,
                       List<TrialVO> trials,
-                      List<XRefDocumentVO> crossReferences) {
+                      List<XRefDocumentVO> crossReferences,
+                      LocationVO location) {
         this.study = study;
         this.source = source;
         this.germplasms = germplasms;
         this.variables = variables;
         this.trials = trials;
         this.crossReferences = crossReferences;
+        this.location = location;
 
         Map<String, Object> additionalInfo =
             study.getAdditionalInfo() == null ? Collections.emptyMap() : study.getAdditionalInfo().getProperties();
@@ -79,4 +80,11 @@ public final class StudyModel {
     public List<Map.Entry<String, Object>> getAdditionalInfoProperties() {
         return additionalInfoProperties;
     }
+
+    public List<MapLocation> getMapLocations() {
+        if (this.location == null) {
+            return Collections.emptyList();
+        }
+        return MapLocation.locationsToDisplayableMapLocations(Collections.singletonList(this.location));
+    }
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java
index a9f699de..9ba16d34 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java
@@ -9,8 +9,11 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.function.Function;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import fr.inra.urgi.faidare.domain.data.germplasm.CollPopVO;
 import fr.inra.urgi.faidare.domain.data.germplasm.TaxonSourceVO;
+import fr.inra.urgi.faidare.utils.Sites;
 import org.apache.logging.log4j.util.Strings;
 
 /**
@@ -21,6 +24,7 @@ public class FaidareExpressions {
 
     private static final Map<String, Function<String, String>> TAXON_ID_URL_FACTORIES_BY_SOURCE_NAME =
         createTaxonIdUrlFactories();
+    private static final ObjectMapper objectMapper = new ObjectMapper();
 
     private static Map<String, Function<String, String>> createTaxonIdUrlFactories() {
         Map<String, Function<String, String>> result = new HashMap<>();
@@ -38,7 +42,7 @@ public class FaidareExpressions {
     }
 
     public String toSiteParam(String siteId) {
-        return Base64.getUrlEncoder().encodeToString(("urn:URGI/location/" + siteId).getBytes(StandardCharsets.US_ASCII));
+        return Sites.siteIdToLocationId(siteId);
     }
 
     public String collPopTitle(CollPopVO collPopVO) {
@@ -55,6 +59,15 @@ public class FaidareExpressions {
         return urlFactory != null ? urlFactory.apply(taxonSource.getTaxonId()) : null;
     }
 
+    public String toJson(Object value) {
+        try {
+            return objectMapper.writeValueAsString(value);
+        }
+        catch (JsonProcessingException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
     private String collPopTitle(CollPopVO collPopVO, Function<String, String> nameTransformer) {
         if (Strings.isBlank(collPopVO.getType())) {
             return nameTransformer.apply(collPopVO.getName());
diff --git a/backend/src/main/resources/static/assets/images/marker-icon-blue.png b/backend/src/main/resources/static/assets/images/marker-icon-blue.png
new file mode 100644
index 0000000000000000000000000000000000000000..e2e9f757f515ded172e6f72c3ce55bbe15579649
GIT binary patch
literal 1747
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

literal 0
HcmV?d00001

diff --git a/backend/src/main/resources/static/assets/images/marker-icon-green.png b/backend/src/main/resources/static/assets/images/marker-icon-green.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b4fb278f611f802d0c4e9cf88edad80e1e1c975
GIT binary patch
literal 1875
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
N002ovPDHLkV1jAYejES*

literal 0
HcmV?d00001

diff --git a/backend/src/main/resources/static/assets/images/marker-icon-purple.png b/backend/src/main/resources/static/assets/images/marker-icon-purple.png
new file mode 100644
index 0000000000000000000000000000000000000000..63e423d250842ad5c9666507a4dd843a8f6a2b93
GIT binary patch
literal 4446
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

literal 0
HcmV?d00001

diff --git a/backend/src/main/resources/static/assets/images/marker-icon-red.png b/backend/src/main/resources/static/assets/images/marker-icon-red.png
new file mode 100644
index 0000000000000000000000000000000000000000..e3c0026ef246271f89aad81b6cf47b2ff63596d9
GIT binary patch
literal 1869
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
Hu0mjf0`GLc

literal 0
HcmV?d00001

diff --git a/backend/src/main/resources/static/assets/script.js b/backend/src/main/resources/static/assets/script.js
new file mode 100644
index 00000000..a01603cc
--- /dev/null
+++ b/backend/src/main/resources/static/assets/script.js
@@ -0,0 +1,100 @@
+const faidare = (() => {
+    function initializePopovers() {
+        const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
+        popoverTriggerList.forEach(popoverTriggerEl => {
+            const options = {};
+            const contentSelector = popoverTriggerEl.dataset.bsElement;
+            if (contentSelector) {
+                const content = document.querySelector(contentSelector);
+                if (content) {
+                    options.content = () => {
+                        const element = document.createElement('div');
+                        element.innerHTML = content.innerHTML;
+                        return element;
+                    };
+                    options.html = true;
+                } else {
+                    throw new Error('element with selector ' + contentSelector + ' not found');
+                }
+            }
+            return new bootstrap.Popover(popoverTriggerEl, options);
+        });
+    }
+
+    function markerColor(location) {
+        switch (location.locationType) {
+            case 'Origin site':
+                return 'red';
+            case 'Collecting site':
+                return 'blue';
+            case 'Evaluation site':
+                return 'green';
+        }
+        return 'purple';
+    }
+
+    function markerIconUrl(contextPath, location) {
+        return `${contextPath}/assets/images/marker-icon-${markerColor(location)}.png`;
+    }
+
+    function initializeMap(options) {
+        if (!options.locations.length) {
+            return;
+        }
+
+        const mapContainerElement = document.querySelector('#map-container');
+        mapContainerElement.classList.remove("d-none");
+        const mapElement = document.querySelector('#map');
+        const map = L.map(mapElement);
+        L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', {
+            attribution: 'Tiles &copy; Esri &mdash; Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, ' +
+                'Esri China (Hong Kong), Esri (Thailand), TomTom, 2012'
+        }).addTo(map);
+
+        const firstLocation = options.locations[0];
+        map.setView([firstLocation.latitude, firstLocation.longitude], 5);
+
+        const markers = L.markerClusterGroup();
+        const mapMarkers = [];
+        for (const location of options.locations) {
+            const icon = L.icon({
+                iconUrl: markerIconUrl(options.contextPath, location),
+                iconAnchor: [12, 41], // point of the icon which will correspond to marker's location
+            });
+            const popupElement = document.createElement('div');
+            const titleElement = document.createElement('strong');
+            titleElement.innerText = location.locationName;
+            const typeElement = document.createElement('div');
+            typeElement.innerText = location.locationType;
+            const linkElement = document.createElement('a');
+            linkElement.innerText = 'Details';
+            linkElement.href = `${options.contextPath}/sites/${location.locationDbId}`;
+            popupElement.appendChild(titleElement);
+            popupElement.appendChild(typeElement);
+            popupElement.appendChild(linkElement);
+
+            const marker = L.marker(
+                [location.latitude, location.longitude],
+                { icon: icon }
+            );
+            markers.addLayer(marker.bindPopup(popupElement));
+            mapMarkers.push(marker);
+        }
+        const initialZoom = map.getZoom();
+
+        map.fitBounds(L.featureGroup(mapMarkers).getBounds());
+        const markerZoom = map.getZoom();
+
+        setTimeout(() => {
+            map.setZoom(Math.min(initialZoom, markerZoom));
+            map.addLayer(markers);
+        }, 100);
+    }
+
+    return {
+        initializePopovers,
+        initializeMap
+    };
+})();
+
+
diff --git a/backend/src/main/resources/static/assets/style.css b/backend/src/main/resources/static/assets/style.css
index 340b22ea..87c396ca 100644
--- a/backend/src/main/resources/static/assets/style.css
+++ b/backend/src/main/resources/static/assets/style.css
@@ -5,3 +5,11 @@
 .popover {
     max-width: min(80vw, 600px);
 }
+
+#map {
+    height: min(400px, 60vh);
+}
+
+.map-legend img {
+    height: 1.5rem;
+}
diff --git a/backend/src/main/resources/templates/fragments/map.html b/backend/src/main/resources/templates/fragments/map.html
new file mode 100644
index 00000000..7fce9d1e
--- /dev/null
+++ b/backend/src/main/resources/templates/fragments/map.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+
+<html xmlns:th="http://www.thymeleaf.org">
+
+<body>
+<!--
+Reusable fragment displaying a map and its legend.
+The map is initially hidden. The JavaScript displays it if there are locations
+to display
+-->
+<div th:fragment="map" id="map-container" class="d-none">
+  <div id="map" class="border rounded"></div>
+  <div class="map-legend mt-1">
+    <img th:src="@{/assets/images/marker-icon-red.png}" id="red"/>
+    <label for="red" class="me-2">Origin site</label>
+    <img th:src="@{/assets/images/marker-icon-blue.png}" id="blue"/>
+    <label for="blue" class="me-2">Collecting site</label>
+    <img th:src="@{/assets/images/marker-icon-green.png}" id="green"/>
+    <label for="green" class="me-2">Evaluation site</label>
+    <img th:src="@{/assets/images/marker-icon-purple.png}" id="purple"/>
+    <label for="purple">Multi-purpose site</label>
+  </div>
+</div>
diff --git a/backend/src/main/resources/templates/germplasm.html b/backend/src/main/resources/templates/germplasm.html
index 840b0d44..864b1813 100644
--- a/backend/src/main/resources/templates/germplasm.html
+++ b/backend/src/main/resources/templates/germplasm.html
@@ -2,7 +2,7 @@
 
 <html
   xmlns:th="http://www.thymeleaf.org"
-  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}"
 >
 <head>
   <title>Germplasm: <th:block th:text="${model.germplasm.germplasmName}" /></title>
@@ -18,6 +18,8 @@
     </div>
   </div>
 
+  <div th:replace="fragments/map::map"></div>
+
   <div class="row align-items-center justify-content-center">
     <div class="col-auto field" th:if="${model.germplasm.photo != null && model.germplasm.photo.thumbnailFile != null}">
       <template id="photo-popover">
@@ -414,5 +416,13 @@
 
   <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
 </main>
+
+<script th:inline="javascript">
+  faidare.initializePopovers();
+  faidare.initializeMap({
+    contextPath: [[${#request.getContextPath()}]],
+    locations: /*[[${model.mapLocations}]]*/ []
+  });
+</script>
 </body>
 </html>
diff --git a/backend/src/main/resources/templates/layout/main.html b/backend/src/main/resources/templates/layout/main.html
index 4cd33f70..b428faaf 100644
--- a/backend/src/main/resources/templates/layout/main.html
+++ b/backend/src/main/resources/templates/layout/main.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="fr" th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
+<html lang="fr" th:fragment="layout (title, content, script)" xmlns:th="http://www.thymeleaf.org">
   <head>
     <title th:replace="${title}">Layout Title</title>
 
@@ -8,6 +8,11 @@
 
     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
     <link th:href="@{/assets/style.css}" rel="stylesheet">
+    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
+          integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
+          crossorigin=""/>
+    <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.1.0/dist/MarkerCluster.css" />
+    <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.1.0/dist/MarkerCluster.Default.css" />
 
     <link rel="shortcut icon" th:href="@{/static/assets/images/favicon.ico}" type="image/x-icon" />
   </head>
@@ -27,26 +32,11 @@
       </footer>
     </div>
     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
-    <script type="text/javascript">
-      const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
-      popoverTriggerList.forEach(popoverTriggerEl => {
-        const options = {};
-        const contentSelector = popoverTriggerEl.dataset.bsElement;
-        if (contentSelector) {
-          const content = document.querySelector(contentSelector);
-          if (content) {
-            options.content = () => {
-              const element = document.createElement('div');
-              element.innerHTML = content.innerHTML;
-              return element;
-            };
-            options.html = true;
-          } else {
-            throw new Error('element with selector ' + contentSelector + ' not found');
-          }
-        }
-        return new bootstrap.Popover(popoverTriggerEl, options);
-      });
-    </script>
+    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
+            integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
+            crossorigin=""></script>
+    <script src="https://unpkg.com/leaflet.markercluster@1.1.0/dist/leaflet.markercluster.js"></script>
+    <script type="text/javascript" th:src="@{/assets/script.js}"></script>
+    <script type="text/javascript" th:replace="${script}"></script>
   </body>
 </html>
diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html
index d5d65e5c..d0fa7dd7 100644
--- a/backend/src/main/resources/templates/site.html
+++ b/backend/src/main/resources/templates/site.html
@@ -2,7 +2,7 @@
 
 <html
   xmlns:th="http://www.thymeleaf.org"
-  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}"
 >
 <head>
   <title>Site <th:block th:text="${model.site.locationName}" /></title>
@@ -13,6 +13,8 @@
 <main>
   <h1>Site <th:block th:text="${model.site.locationName}" /></h1>
 
+  <div th:replace="fragments/map::map"></div>
+
   <th:block th:if="${model.site.uri != null && !model.site.uri.startsWith('urn:')}">
     <div th:replace="fragments/row::text-row(label='Permanent unique identifier', text=${model.site.uri})"></div>
   </th:block>
@@ -58,5 +60,12 @@
 
   <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
 </main>
+
+<script th:inline="javascript">
+  faidare.initializeMap({
+    contextPath: [[${#request.getContextPath()}]],
+    locations: /*[[${model.mapLocations}]]*/ []
+  });
+</script>
 </body>
 </html>
diff --git a/backend/src/main/resources/templates/study.html b/backend/src/main/resources/templates/study.html
index 7bd5bbbd..c5111944 100644
--- a/backend/src/main/resources/templates/study.html
+++ b/backend/src/main/resources/templates/study.html
@@ -2,7 +2,7 @@
 
 <html
   xmlns:th="http://www.thymeleaf.org"
-  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}"
 >
 <head>
   <title>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></title>
@@ -13,6 +13,8 @@
 <main>
   <h1>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></h1>
 
+  <div th:replace="fragments/map::map"></div>
+
   <h2>Identification</h2>
 
   <div th:replace="fragments/row::text-row(label='Name', text=${model.study.studyName})"></div>
@@ -188,5 +190,12 @@
 
   <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
 </main>
+
+<script th:inline="javascript">
+  faidare.initializeMap({
+    contextPath: [[${#request.getContextPath()}]],
+    locations: /*[[${model.mapLocations}]]*/ []
+  });
+</script>
 </body>
 </html>
-- 
GitLab


From 3294e52de14a2858ef243cd8677fd1f144ab3ac4 Mon Sep 17 00:00:00 2001
From: jnizet <jb@ninja-squad.com>
Date: Fri, 27 Aug 2021 10:29:15 +0200
Subject: [PATCH 14/16] feat: move css and js to web project built with webpack

---
 .gitignore                                    |    1 +
 backend/build.gradle.kts                      |   39 +-
 backend/src/main/resources/application.yml    |    8 +
 .../main/resources/static/assets/script.js    |  100 --
 .../main/resources/static/assets/style.css    |   15 -
 .../main/resources/templates/layout/main.html |   11 -
 gradle/wrapper/gradle-wrapper.jar             |  Bin 55741 -> 59536 bytes
 gradle/wrapper/gradle-wrapper.properties      |    2 +-
 gradlew                                       |  282 ++--
 gradlew.bat                                   |   43 +-
 settings.gradle.kts                           |    2 +-
 web/build.gradle.kts                          |   42 +
 web/package.json                              |   35 +
 web/src/bootstrap/popovers.ts                 |   19 +
 web/src/index.ts                              |    7 +
 web/src/map/map.ts                            |   93 ++
 web/src/style/_custom-bootstrap.scss          |   53 +
 web/src/style/style.scss                      |   21 +
 web/tsconfig.json                             |   72 +
 web/webpack.common.js                         |   46 +
 web/webpack.dev.js                            |    7 +
 web/webpack.prod.js                           |   23 +
 web/yarn.lock                                 | 1359 +++++++++++++++++
 23 files changed, 2013 insertions(+), 267 deletions(-)
 delete mode 100644 backend/src/main/resources/static/assets/script.js
 delete mode 100644 backend/src/main/resources/static/assets/style.css
 create mode 100644 web/build.gradle.kts
 create mode 100644 web/package.json
 create mode 100644 web/src/bootstrap/popovers.ts
 create mode 100644 web/src/index.ts
 create mode 100644 web/src/map/map.ts
 create mode 100644 web/src/style/_custom-bootstrap.scss
 create mode 100644 web/src/style/style.scss
 create mode 100644 web/tsconfig.json
 create mode 100644 web/webpack.common.js
 create mode 100644 web/webpack.dev.js
 create mode 100644 web/webpack.prod.js
 create mode 100644 web/yarn.lock

diff --git a/.gitignore b/.gitignore
index 897c8ff3..ef6f3225 100644
--- a/.gitignore
+++ b/.gitignore
@@ -226,3 +226,4 @@ gradle-app.setting
 
 # End of https://www.gitignore.io/api/gradle,eclipse,intellij,visualstudiocode,kotlin,git,macos,linux
 frontend/package-lock.json
+/web/node_modules/
diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts
index 586fc953..64acb331 100644
--- a/backend/build.gradle.kts
+++ b/backend/build.gradle.kts
@@ -1,11 +1,8 @@
 import org.gradle.api.tasks.testing.logging.TestExceptionFormat
 import org.springframework.boot.gradle.tasks.buildinfo.BuildInfo
-import org.springframework.boot.gradle.tasks.bundling.BootJar
-import org.springframework.boot.gradle.tasks.run.BootRun
 
 buildscript {
     repositories {
-        mavenLocal()
         mavenCentral()
     }
 }
@@ -20,7 +17,6 @@ plugins {
     id("org.owasp.dependencycheck") version "6.0.3"
 }
 
-
 java {
     sourceCompatibility = JavaVersion.VERSION_1_8
 }
@@ -40,7 +36,7 @@ tasks {
         options.compilerArgs.add("-parameters")
     }
 
-    getByName<Copy>("processResources") {
+    processResources {
         inputs.property("app", "gnpis")
 
         filesMatching("bootstrap.yml") {}
@@ -51,21 +47,44 @@ tasks {
     // but it's better to do that than using the bootInfo() method of the springBoot closure, because that
     // makes the test task out of date, which makes the build much longer.
     // See https://github.com/spring-projects/spring-boot/issues/13152
-    val buildInfo by creating(BuildInfo::class) {
+    val buildInfo by registering(BuildInfo::class) {
         destinationDir = file("$buildDir/buildInfo")
     }
 
-    val bootJar by getting(BootJar::class) {
+    bootJar {
         archiveName = "${rootProject.name}.jar"
         dependsOn(buildInfo)
+        dependsOn(":web:assemble")
+
+        // replace the script.js and style.css file names referenced in main.html
+        // by their actual name, containing the content hash
+        filesMatching("**/layout/main.html") {
+            val webAssetsDir = project(":web").file("build/dist/assets/");
+            val scriptFileName = webAssetsDir.list().first { it.startsWith("script") && it.endsWith(".js") }
+            val styleFileName = webAssetsDir.list().first { it.startsWith("style") && it.endsWith(".css") }
+
+            filter { line ->
+                if (line.contains("script.js")) {
+                    line.replace("script.js", scriptFileName)
+                }
+                else if (line.contains("style.css")) {
+                    line.replace("style.css", styleFileName)
+                } else {
+                    line
+                }
+            }
+        }
 
         into("BOOT-INF/classes/META-INF") {
-            from(buildInfo.destinationDir)
+            from(buildInfo.map { it.destinationDir })
+        }
+        into("BOOT-INF/classes/static") {
+            from(project(":web").file("build/dist"))
         }
         launchScript()
     }
 
-    val test by getting(Test::class) {
+    test {
         useJUnitPlatform()
         testLogging {
             exceptionFormat = TestExceptionFormat.FULL
@@ -73,7 +92,7 @@ tasks {
         outputs.dir(snippetsDir)
     }
 
-    val jacocoTestReport by getting(JacocoReport::class) {
+    jacocoTestReport {
         reports {
             xml.setEnabled(true)
             html.setEnabled(true)
diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml
index 26c55fc3..2b684f60 100644
--- a/backend/src/main/resources/application.yml
+++ b/backend/src/main/resources/application.yml
@@ -90,3 +90,11 @@ server:
   servlet:
     context-path: /faidare-dev
 
+---
+spring:
+  profiles:
+    dev
+  resources:
+    static-locations:
+      - classpath:/static/
+      - file:./web/build/dist/
diff --git a/backend/src/main/resources/static/assets/script.js b/backend/src/main/resources/static/assets/script.js
deleted file mode 100644
index a01603cc..00000000
--- a/backend/src/main/resources/static/assets/script.js
+++ /dev/null
@@ -1,100 +0,0 @@
-const faidare = (() => {
-    function initializePopovers() {
-        const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
-        popoverTriggerList.forEach(popoverTriggerEl => {
-            const options = {};
-            const contentSelector = popoverTriggerEl.dataset.bsElement;
-            if (contentSelector) {
-                const content = document.querySelector(contentSelector);
-                if (content) {
-                    options.content = () => {
-                        const element = document.createElement('div');
-                        element.innerHTML = content.innerHTML;
-                        return element;
-                    };
-                    options.html = true;
-                } else {
-                    throw new Error('element with selector ' + contentSelector + ' not found');
-                }
-            }
-            return new bootstrap.Popover(popoverTriggerEl, options);
-        });
-    }
-
-    function markerColor(location) {
-        switch (location.locationType) {
-            case 'Origin site':
-                return 'red';
-            case 'Collecting site':
-                return 'blue';
-            case 'Evaluation site':
-                return 'green';
-        }
-        return 'purple';
-    }
-
-    function markerIconUrl(contextPath, location) {
-        return `${contextPath}/assets/images/marker-icon-${markerColor(location)}.png`;
-    }
-
-    function initializeMap(options) {
-        if (!options.locations.length) {
-            return;
-        }
-
-        const mapContainerElement = document.querySelector('#map-container');
-        mapContainerElement.classList.remove("d-none");
-        const mapElement = document.querySelector('#map');
-        const map = L.map(mapElement);
-        L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', {
-            attribution: 'Tiles &copy; Esri &mdash; Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, ' +
-                'Esri China (Hong Kong), Esri (Thailand), TomTom, 2012'
-        }).addTo(map);
-
-        const firstLocation = options.locations[0];
-        map.setView([firstLocation.latitude, firstLocation.longitude], 5);
-
-        const markers = L.markerClusterGroup();
-        const mapMarkers = [];
-        for (const location of options.locations) {
-            const icon = L.icon({
-                iconUrl: markerIconUrl(options.contextPath, location),
-                iconAnchor: [12, 41], // point of the icon which will correspond to marker's location
-            });
-            const popupElement = document.createElement('div');
-            const titleElement = document.createElement('strong');
-            titleElement.innerText = location.locationName;
-            const typeElement = document.createElement('div');
-            typeElement.innerText = location.locationType;
-            const linkElement = document.createElement('a');
-            linkElement.innerText = 'Details';
-            linkElement.href = `${options.contextPath}/sites/${location.locationDbId}`;
-            popupElement.appendChild(titleElement);
-            popupElement.appendChild(typeElement);
-            popupElement.appendChild(linkElement);
-
-            const marker = L.marker(
-                [location.latitude, location.longitude],
-                { icon: icon }
-            );
-            markers.addLayer(marker.bindPopup(popupElement));
-            mapMarkers.push(marker);
-        }
-        const initialZoom = map.getZoom();
-
-        map.fitBounds(L.featureGroup(mapMarkers).getBounds());
-        const markerZoom = map.getZoom();
-
-        setTimeout(() => {
-            map.setZoom(Math.min(initialZoom, markerZoom));
-            map.addLayer(markers);
-        }, 100);
-    }
-
-    return {
-        initializePopovers,
-        initializeMap
-    };
-})();
-
-
diff --git a/backend/src/main/resources/static/assets/style.css b/backend/src/main/resources/static/assets/style.css
deleted file mode 100644
index 87c396ca..00000000
--- a/backend/src/main/resources/static/assets/style.css
+++ /dev/null
@@ -1,15 +0,0 @@
-.label {
-    font-weight: 500;
-}
-
-.popover {
-    max-width: min(80vw, 600px);
-}
-
-#map {
-    height: min(400px, 60vh);
-}
-
-.map-legend img {
-    height: 1.5rem;
-}
diff --git a/backend/src/main/resources/templates/layout/main.html b/backend/src/main/resources/templates/layout/main.html
index b428faaf..47bfcf0d 100644
--- a/backend/src/main/resources/templates/layout/main.html
+++ b/backend/src/main/resources/templates/layout/main.html
@@ -6,13 +6,7 @@
     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
     <meta content="width=device-width, initial-scale=1" name="viewport" />
 
-    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
     <link th:href="@{/assets/style.css}" rel="stylesheet">
-    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
-          integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
-          crossorigin=""/>
-    <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.1.0/dist/MarkerCluster.css" />
-    <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.1.0/dist/MarkerCluster.Default.css" />
 
     <link rel="shortcut icon" th:href="@{/static/assets/images/favicon.ico}" type="image/x-icon" />
   </head>
@@ -31,11 +25,6 @@
         common footer
       </footer>
     </div>
-    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
-    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
-            integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
-            crossorigin=""></script>
-    <script src="https://unpkg.com/leaflet.markercluster@1.1.0/dist/leaflet.markercluster.js"></script>
     <script type="text/javascript" th:src="@{/assets/script.js}"></script>
     <script type="text/javascript" th:replace="${script}"></script>
   </body>
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 457aad0d98108420a977756b7145c93c8910b076..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644
GIT binary patch
delta 33114
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
TC&=K0so)<)a0}UIUq1aG&G$N0

delta 29441
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
b{r{#xMHy(Yqj2!40srZNdsTAa`P=^kHCx82

diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 75b8c7c8..ffed3a25 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index af6708ff..1b6c7873 100755
--- a/gradlew
+++ b/gradlew
@@ -1,78 +1,129 @@
-#!/usr/bin/env sh
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
 
 ##############################################################################
-##
-##  Gradle start up script for UN*X
-##
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
 ##############################################################################
 
 # Attempt to set APP_HOME
+
 # Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
-    ls=`ls -ld "$PRG"`
-    link=`expr "$ls" : '.*-> \(.*\)$'`
-    if expr "$link" : '/.*' > /dev/null; then
-        PRG="$link"
-    else
-        PRG=`dirname "$PRG"`"/$link"
-    fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
 done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
 
 APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
+APP_BASE_NAME=${0##*/}
 
 # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m"'
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
 
 # Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
 
 warn () {
     echo "$*"
-}
+} >&2
 
 die () {
     echo
     echo "$*"
     echo
     exit 1
-}
+} >&2
 
 # OS specific support (must be 'true' or 'false').
 cygwin=false
 msys=false
 darwin=false
 nonstop=false
-case "`uname`" in
-  CYGWIN* )
-    cygwin=true
-    ;;
-  Darwin* )
-    darwin=true
-    ;;
-  MINGW* )
-    msys=true
-    ;;
-  NONSTOP* )
-    nonstop=true
-    ;;
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
 esac
 
 CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
 
+
 # Determine the Java command to use to start the JVM.
 if [ -n "$JAVA_HOME" ] ; then
     if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
         # IBM's JDK on AIX uses strange locations for the executables
-        JAVACMD="$JAVA_HOME/jre/sh/java"
+        JAVACMD=$JAVA_HOME/jre/sh/java
     else
-        JAVACMD="$JAVA_HOME/bin/java"
+        JAVACMD=$JAVA_HOME/bin/java
     fi
     if [ ! -x "$JAVACMD" ] ; then
         die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -81,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
 location of your Java installation."
     fi
 else
-    JAVACMD="java"
+    JAVACMD=java
     which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
 
 Please set the JAVA_HOME variable in your environment to match the
@@ -89,84 +140,95 @@ location of your Java installation."
 fi
 
 # Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
-    MAX_FD_LIMIT=`ulimit -H -n`
-    if [ $? -eq 0 ] ; then
-        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
-            MAX_FD="$MAX_FD_LIMIT"
-        fi
-        ulimit -n $MAX_FD
-        if [ $? -ne 0 ] ; then
-            warn "Could not set maximum file descriptor limit: $MAX_FD"
-        fi
-    else
-        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
-    fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
 fi
 
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
-    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
 
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
-    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
-    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-    JAVACMD=`cygpath --unix "$JAVACMD"`
-
-    # We build the pattern for arguments to be converted via cygpath
-    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
-    SEP=""
-    for dir in $ROOTDIRSRAW ; do
-        ROOTDIRS="$ROOTDIRS$SEP$dir"
-        SEP="|"
-    done
-    OURCYGPATTERN="(^($ROOTDIRS))"
-    # Add a user-defined pattern to the cygpath arguments
-    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
-        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
-    fi
     # Now convert the arguments - kludge to limit ourselves to /bin/sh
-    i=0
-    for arg in "$@" ; do
-        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
-        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
-
-        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
-            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
-        else
-            eval `echo args$i`="\"$arg\""
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
         fi
-        i=$((i+1))
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
     done
-    case $i in
-        (0) set -- ;;
-        (1) set -- "$args0" ;;
-        (2) set -- "$args0" "$args1" ;;
-        (3) set -- "$args0" "$args1" "$args2" ;;
-        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
-        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
-        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
-        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
-        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
-        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
-    esac
 fi
 
-# Escape application args
-save () {
-    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
-    echo " "
-}
-APP_ARGS=$(save "$@")
-
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
-  cd "$(dirname "$0")"
-fi
+# Collect all arguments for the java command;
+#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+#     shell script including quotes and variable substitutions, so put them in
+#     double quotes to make sure that they get re-expanded; and
+#   * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
 
 exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index 6d57edc7..107acd32 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,3 +1,19 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
 @if "%DEBUG%" == "" @echo off
 @rem ##########################################################################
 @rem
@@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=.
 set APP_BASE_NAME=%~n0
 set APP_HOME=%DIRNAME%
 
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
 @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m"
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
 
 @rem Find java.exe
 if defined JAVA_HOME goto findJavaFromJavaHome
 
 set JAVA_EXE=java.exe
 %JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
+if "%ERRORLEVEL%" == "0" goto execute
 
 echo.
 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -35,7 +54,7 @@ goto fail
 set JAVA_HOME=%JAVA_HOME:"=%
 set JAVA_EXE=%JAVA_HOME%/bin/java.exe
 
-if exist "%JAVA_EXE%" goto init
+if exist "%JAVA_EXE%" goto execute
 
 echo.
 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -45,28 +64,14 @@ echo location of your Java installation.
 
 goto fail
 
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
 :execute
 @rem Setup the command line
 
 set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
 
+
 @rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
 
 :end
 @rem End local scope for the variables with windows NT shell
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 59e6a72b..b8faef34 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,2 +1,2 @@
 rootProject.name = "faidare"
-include("backend")
+include("backend", "web")
diff --git a/web/build.gradle.kts b/web/build.gradle.kts
new file mode 100644
index 00000000..b49ff9de
--- /dev/null
+++ b/web/build.gradle.kts
@@ -0,0 +1,42 @@
+import com.github.gradle.node.yarn.task.YarnInstallTask
+import com.github.gradle.node.yarn.task.YarnTask
+
+plugins {
+    base
+    id("com.github.node-gradle.node") version "3.0.1"
+}
+
+node {
+    version.set("14.17.0")
+    npmVersion.set("6.14.10")
+    yarnVersion.set("1.22.10")
+    download.set(true)
+}
+
+tasks {
+    npmInstall {
+        enabled = false
+    }
+
+    val prepare by registering {
+        dependsOn(YarnInstallTask.NAME)
+    }
+
+    // This is not a yarn_build task because the task to run is `yarn build:prod`
+    // and tasks with colons are not supported
+    val yarnBuildProd by registering(YarnTask::class) {
+        args.set(listOf("run", "build:prod"))
+        dependsOn(prepare)
+        inputs.file("webpack.common.js")
+        inputs.file("webpack.prod.js")
+        inputs.file("tsconfig.json")
+        inputs.file("package.json")
+        inputs.file("yarn.lock")
+        inputs.dir("src")
+        outputs.dir("$buildDir/dist")
+    }
+
+    assemble {
+        dependsOn(yarnBuildProd)
+    }
+}
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 00000000..e1348699
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,35 @@
+{
+  "name": "faidaire-web",
+  "version": "1.0.0",
+  "description": "",
+  "private": true,
+  "scripts": {
+    "build": "webpack --config webpack.dev.js",
+    "build:prod": "webpack --config webpack.prod.js",
+    "watch": "webpack --config webpack.dev.js --watch",
+    "watch:prod": "webpack --config webpack.prod.js --watch",
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "MIT",
+  "dependencies": {
+    "@types/leaflet": "1.7.4",
+    "bootstrap": "5.1.0",
+    "leaflet": "1.7.1"
+  },
+  "devDependencies": {
+    "@types/bootstrap": "5.1.2",
+    "@types/leaflet.markercluster": "1.4.5",
+    "clean-webpack-plugin": "4.0.0-alpha.0",
+    "css-loader": "6.2.0",
+    "leaflet.markercluster": "1.5.0",
+    "mini-css-extract-plugin": "2.2.0",
+    "sass": "1.38.1",
+    "sass-loader": "12.1.0",
+    "ts-loader": "9.2.5",
+    "typescript": "4.3.5",
+    "webpack": "5.51.1",
+    "webpack-cli": "4.8.0",
+    "webpack-merge": "5.8.0"
+  }
+}
diff --git a/web/src/bootstrap/popovers.ts b/web/src/bootstrap/popovers.ts
new file mode 100644
index 00000000..83b703de
--- /dev/null
+++ b/web/src/bootstrap/popovers.ts
@@ -0,0 +1,19 @@
+import { Popover } from 'bootstrap';
+
+export function initializePopovers() {
+    const popoverTriggerList: Array<HTMLElement> = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
+    popoverTriggerList.forEach(popoverTriggerEl => {
+        const options: Partial<Popover.Options> = {};
+        const contentSelector = popoverTriggerEl.dataset.bsElement;
+        if (contentSelector) {
+            const content = document.querySelector(contentSelector);
+            if (content) {
+                options.content = content.innerHTML;
+                options.html = true;
+            } else {
+                throw new Error('element with selector ' + contentSelector + ' not found');
+            }
+        }
+        return new Popover(popoverTriggerEl, options);
+    });
+}
diff --git a/web/src/index.ts b/web/src/index.ts
new file mode 100644
index 00000000..c906b28e
--- /dev/null
+++ b/web/src/index.ts
@@ -0,0 +1,7 @@
+import { initializePopovers } from './bootstrap/popovers';
+import { initializeMap } from './map/map';
+
+(window as any).faidare = {
+    initializePopovers,
+    initializeMap
+}
diff --git a/web/src/map/map.ts b/web/src/map/map.ts
new file mode 100644
index 00000000..9fc2a960
--- /dev/null
+++ b/web/src/map/map.ts
@@ -0,0 +1,93 @@
+import * as L from 'leaflet';
+import 'leaflet.markercluster';
+
+interface MapLocation {
+    locationDbId: string;
+    locationType: 'Origin site' | 'Collecting site' | 'Evaluation site' | null;
+    locationName: string;
+    latitude: number;
+    longitude: number;
+}
+
+interface MapOptions {
+    contextPath: string;
+    locations: Array<MapLocation>;
+}
+
+function markerColor(location: MapLocation) {
+    switch (location.locationType) {
+        case 'Origin site':
+            return 'red';
+        case 'Collecting site':
+            return 'blue';
+        case 'Evaluation site':
+            return 'green';
+    }
+    return 'purple';
+}
+
+function markerIconUrl(contextPath: string, location: MapLocation) {
+    return `${contextPath}/assets/images/marker-icon-${markerColor(location)}.png`;
+}
+
+export function initializeMap(options: MapOptions) {
+    if (!options.locations.length) {
+        return;
+    }
+
+    const mapContainerElement = document.querySelector('#map-container');
+    mapContainerElement!.classList.remove("d-none");
+    const mapElement = document.querySelector('#map') as HTMLElement;
+    const map = L.map(mapElement);
+    L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', {
+        attribution: 'Tiles &copy; Esri &mdash; Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, ' +
+            'Esri China (Hong Kong), Esri (Thailand), TomTom, 2012'
+    }).addTo(map);
+
+    const firstLocation = options.locations[0];
+    map.setView([firstLocation.latitude, firstLocation.longitude], 5);
+
+    const markers = L.markerClusterGroup();
+    const mapMarkers: Array<L.Marker> = [];
+    for (const location of options.locations) {
+        const icon = L.icon({
+            iconUrl: markerIconUrl(options.contextPath, location),
+            iconAnchor: [12, 41], // point of the icon which will correspond to marker's location
+        });
+
+        const popupElement = document.createElement('div');
+
+        const titleElement = document.createElement('strong');
+        titleElement.innerText = location.locationName;
+        popupElement.appendChild(titleElement);
+        popupElement.appendChild(document.createElement('br'));
+
+        if (location.locationType) {
+            const typeElement = document.createElement('span');
+            typeElement.innerText = location.locationType;
+            popupElement.appendChild(typeElement);
+            popupElement.appendChild(document.createElement('br'));
+        }
+
+        const linkElement = document.createElement('a');
+        linkElement.innerText = 'Details';
+        linkElement.href = `${options.contextPath}/sites/${location.locationDbId}`;
+        popupElement.appendChild(linkElement);
+
+        const marker = L.marker(
+            [location.latitude, location.longitude],
+            { icon: icon }
+        );
+        markers.addLayer(marker.bindPopup(popupElement));
+        mapMarkers.push(marker);
+    }
+    const initialZoom = map.getZoom();
+
+    map.fitBounds(L.featureGroup(mapMarkers).getBounds());
+    const markerZoom = map.getZoom();
+
+    setTimeout(() => {
+        map.setZoom(Math.min(initialZoom, markerZoom));
+        map.addLayer(markers);
+    }, 100);
+}
diff --git a/web/src/style/_custom-bootstrap.scss b/web/src/style/_custom-bootstrap.scss
new file mode 100644
index 00000000..9de7c3dc
--- /dev/null
+++ b/web/src/style/_custom-bootstrap.scss
@@ -0,0 +1,53 @@
+/*!
+ * Bootstrap v5.1.0 (https://getbootstrap.com/)
+ * Copyright 2011-2021 The Bootstrap Authors
+ * Copyright 2011-2021 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ */
+
+// scss-docs-start import-stack
+// Configuration
+@import '~bootstrap/scss/functions';
+@import '~bootstrap/scss/variables';
+@import '~bootstrap/scss/mixins';
+@import '~bootstrap/scss/utilities';
+
+// Layout & components
+@import '~bootstrap/scss/root';
+@import '~bootstrap/scss/reboot';
+@import '~bootstrap/scss/type';
+@import '~bootstrap/scss/images';
+@import '~bootstrap/scss/containers';
+@import '~bootstrap/scss/grid';
+@import '~bootstrap/scss/tables';
+// @import '~bootstrap/scss/forms';
+@import '~bootstrap/scss/buttons';
+@import '~bootstrap/scss/transitions';
+@import '~bootstrap/scss/dropdown';
+// @import '~bootstrap/scss/button-group';
+@import '~bootstrap/scss/nav';
+@import '~bootstrap/scss/navbar';
+@import '~bootstrap/scss/card';
+// @import '~bootstrap/scss/accordion';
+// @import '~bootstrap/scss/breadcrumb';
+// @import '~bootstrap/scss/pagination';
+// @import '~bootstrap/scss/badge';
+// @import '~bootstrap/scss/alert';
+// @import '~bootstrap/scss/progress';
+// @import '~bootstrap/scss/list-group';
+// @import '~bootstrap/scss/close';
+// @import '~bootstrap/scss/toasts';
+// @import '~bootstrap/scss/modal';
+// @import '~bootstrap/scss/tooltip';
+@import '~bootstrap/scss/popover';
+// @import '~bootstrap/scss/carousel';
+// @import '~bootstrap/scss/spinners';
+// @import '~bootstrap/scss/offcanvas';
+// @import '~bootstrap/scss/placeholders';
+
+// Helpers
+// @import '~bootstrap/scss/helpers';
+
+// Utilities
+@import '~bootstrap/scss/utilities/api';
+// scss-docs-end import-stack
diff --git a/web/src/style/style.scss b/web/src/style/style.scss
new file mode 100644
index 00000000..4dd56d61
--- /dev/null
+++ b/web/src/style/style.scss
@@ -0,0 +1,21 @@
+@import 'custom-bootstrap';
+@import '~leaflet/dist/leaflet.css';
+@import '~leaflet.markercluster/dist/MarkerCluster.css';
+@import '~leaflet.markercluster/dist/MarkerCluster.Default.css';
+
+.label {
+  font-weight: 500;
+}
+
+.popover {
+  max-width: min(80vw, 600px);
+}
+
+#map {
+  height: min(400px, 60vh);
+}
+
+.map-legend img {
+  height: 1.5rem;
+}
+
diff --git a/web/tsconfig.json b/web/tsconfig.json
new file mode 100644
index 00000000..714af2e5
--- /dev/null
+++ b/web/tsconfig.json
@@ -0,0 +1,72 @@
+{
+  "compilerOptions": {
+    /* Visit https://aka.ms/tsconfig.json to read more about this file */
+
+    /* Basic Options */
+    // "incremental": true,                         /* Enable incremental compilation */
+    "target": "es6",                                /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
+    "module": "es2015",                             /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
+    // "lib": [],                                   /* Specify library files to be included in the compilation. */
+    // "allowJs": true,                             /* Allow javascript files to be compiled. */
+    // "checkJs": true,                             /* Report errors in .js files. */
+    // "jsx": "preserve",                           /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
+    // "declaration": true,                         /* Generates corresponding '.d.ts' file. */
+    // "declarationMap": true,                      /* Generates a sourcemap for each corresponding '.d.ts' file. */
+    // "sourceMap": true,                           /* Generates corresponding '.map' file. */
+    // "outFile": "./",                             /* Concatenate and emit output to single file. */
+    // "outDir": "./",                              /* Redirect output structure to the directory. */
+    // "rootDir": "./",                             /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+    // "composite": true,                           /* Enable project compilation */
+    // "tsBuildInfoFile": "./",                     /* Specify file to store incremental compilation information */
+    // "removeComments": true,                      /* Do not emit comments to output. */
+    // "noEmit": true,                              /* Do not emit outputs. */
+    // "importHelpers": true,                       /* Import emit helpers from 'tslib'. */
+    // "downlevelIteration": true,                  /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+    // "isolatedModules": true,                     /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+
+    /* Strict Type-Checking Options */
+    "strict": true,                                 /* Enable all strict type-checking options. */
+    // "noImplicitAny": true,                       /* Raise error on expressions and declarations with an implied 'any' type. */
+    // "strictNullChecks": true,                    /* Enable strict null checks. */
+    // "strictFunctionTypes": true,                 /* Enable strict checking of function types. */
+    // "strictBindCallApply": true,                 /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+    // "strictPropertyInitialization": true,        /* Enable strict checking of property initialization in classes. */
+    // "noImplicitThis": true,                      /* Raise error on 'this' expressions with an implied 'any' type. */
+    // "alwaysStrict": true,                        /* Parse in strict mode and emit "use strict" for each source file. */
+
+    /* Additional Checks */
+    // "noUnusedLocals": true,                      /* Report errors on unused locals. */
+    // "noUnusedParameters": true,                  /* Report errors on unused parameters. */
+    // "noImplicitReturns": true,                   /* Report error when not all code paths in function return a value. */
+    // "noFallthroughCasesInSwitch": true,          /* Report errors for fallthrough cases in switch statement. */
+    // "noUncheckedIndexedAccess": true,            /* Include 'undefined' in index signature results */
+    // "noImplicitOverride": true,                  /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
+    // "noPropertyAccessFromIndexSignature": true,  /* Require undeclared properties from index signatures to use element accesses. */
+
+    /* Module Resolution Options */
+    // "moduleResolution": "node",                  /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+    // "baseUrl": "./",                             /* Base directory to resolve non-absolute module names. */
+    // "paths": {},                                 /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+    // "rootDirs": [],                              /* List of root folders whose combined content represents the structure of the project at runtime. */
+    // "typeRoots": [],                             /* List of folders to include type definitions from. */
+    // "types": [],                                 /* Type declaration files to be included in compilation. */
+    // "allowSyntheticDefaultImports": true,        /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+    "esModuleInterop": true,                        /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+    // "preserveSymlinks": true,                    /* Do not resolve the real path of symlinks. */
+    // "allowUmdGlobalAccess": true,                /* Allow accessing UMD globals from modules. */
+
+    /* Source Map Options */
+    // "sourceRoot": "",                            /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+    // "mapRoot": "",                               /* Specify the location where debugger should locate map files instead of generated locations. */
+    // "inlineSourceMap": true,                     /* Emit a single file with source maps instead of having a separate file. */
+    // "inlineSources": true,                       /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+
+    /* Experimental Options */
+    // "experimentalDecorators": true,              /* Enables experimental support for ES7 decorators. */
+    // "emitDecoratorMetadata": true,               /* Enables experimental support for emitting type metadata for decorators. */
+
+    /* Advanced Options */
+    "skipLibCheck": true,                           /* Skip type checking of declaration files. */
+    "forceConsistentCasingInFileNames": true        /* Disallow inconsistently-cased references to the same file. */
+  }
+}
diff --git a/web/webpack.common.js b/web/webpack.common.js
new file mode 100644
index 00000000..83633dec
--- /dev/null
+++ b/web/webpack.common.js
@@ -0,0 +1,46 @@
+const path = require('path');
+const { CleanWebpackPlugin } = require('clean-webpack-plugin');
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+
+module.exports = {
+    plugins: [
+        // cleans the output directory before each build
+        new CleanWebpackPlugin(),
+        // allows extracting the CSS into a CSS file instead of bundling it in a JS file
+        new MiniCssExtractPlugin()
+    ],
+    entry: {
+        // a JS bundle is generated for the index.ts entry point. Since the application is really small
+        // and does not have much JavaScript logic, a single bundle is sufficient
+        script: './src/index.ts',
+        // A CSS bundle is generated for the style.scss entry point.
+        style: './src/style/style.scss'
+    },
+    module: {
+        rules: [
+            {
+                test: /\.tsx?$/,
+                use: 'ts-loader',
+                exclude: /node_modules/,
+            },
+            {
+                // .scss files are loaded by the sass-loader, which transforms them into css loaded by the css-loader
+                // which are then bundled into a css file by the MiniCssExtractPlugin loader
+                test: /\.scss$/i,
+                use: [
+                    MiniCssExtractPlugin.loader,
+                    'css-loader',
+                    'sass-loader'
+                ],
+            },
+        ],
+    },
+    resolve: {
+        // our files are .ts files, but libraries are bundled in .js files
+        extensions: ['.ts', '.js'],
+    },
+    output: {
+        path: path.resolve(__dirname, 'build/dist/assets'),
+        filename: '[name].js'
+    }
+};
diff --git a/web/webpack.dev.js b/web/webpack.dev.js
new file mode 100644
index 00000000..ca85da79
--- /dev/null
+++ b/web/webpack.dev.js
@@ -0,0 +1,7 @@
+const { merge } = require('webpack-merge');
+const common = require('./webpack.common.js');
+
+module.exports = merge(common, {
+    mode: 'development',
+    devtool: 'inline-source-map'
+});
diff --git a/web/webpack.prod.js b/web/webpack.prod.js
new file mode 100644
index 00000000..cf236a59
--- /dev/null
+++ b/web/webpack.prod.js
@@ -0,0 +1,23 @@
+const { merge } = require('webpack-merge');
+const common = require('./webpack.common.js');
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+
+const mergedConfig = merge(common, {
+    mode: 'production',
+    output: {
+        filename: '[name].[contenthash].js'
+    }
+});
+
+mergedConfig.plugins = mergedConfig.plugins.map(plugin => {
+    if (plugin instanceof MiniCssExtractPlugin) {
+        return new MiniCssExtractPlugin({
+            filename: '[name].[contenthash].css'
+        });
+    } else {
+        return plugin
+    }
+});
+
+module.exports = mergedConfig;
+
diff --git a/web/yarn.lock b/web/yarn.lock
new file mode 100644
index 00000000..885dcc69
--- /dev/null
+++ b/web/yarn.lock
@@ -0,0 +1,1359 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@discoveryjs/json-ext@^0.5.0":
+  version "0.5.3"
+  resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz#90420f9f9c6d3987f176a19a7d8e764271a2f55d"
+  integrity sha512-Fxt+AfXgjMoin2maPIYzFZnQjAXjAL0PHscM5pRTtatFqB+vZxAM9tLp2Optnuw3QOQC40jTNeGYFOMvyf7v9g==
+
+"@popperjs/core@^2.9.2":
+  version "2.9.3"
+  resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.3.tgz#8b68da1ebd7fc603999cf6ebee34a4899a14b88e"
+  integrity sha512-xDu17cEfh7Kid/d95kB6tZsLOmSWKCZKtprnhVepjsSaCij+lM3mItSJDuuHDMbCWTh8Ejmebwb+KONcCJ0eXQ==
+
+"@types/bootstrap@5.1.2":
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.1.2.tgz#24f08f1957ff5859633f4bf620e921d296e6c3a2"
+  integrity sha512-dSQvMi2dMyNwJU6LZjP0pimuBowsMUvGScYdfqqeiDUoj9TxXZCpfu0cTl94U0Zvw/tdH9j/9ToOhi4LKNLZhg==
+  dependencies:
+    "@popperjs/core" "^2.9.2"
+    "@types/jquery" "*"
+
+"@types/eslint-scope@^3.7.0":
+  version "3.7.1"
+  resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.1.tgz#8dc390a7b4f9dd9f1284629efce982e41612116e"
+  integrity sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==
+  dependencies:
+    "@types/eslint" "*"
+    "@types/estree" "*"
+
+"@types/eslint@*":
+  version "7.28.0"
+  resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-7.28.0.tgz#7e41f2481d301c68e14f483fe10b017753ce8d5a"
+  integrity sha512-07XlgzX0YJUn4iG1ocY4IX9DzKSmMGUs6ESKlxWhZRaa0fatIWaHWUVapcuGa8r5HFnTqzj+4OCjd5f7EZ/i/A==
+  dependencies:
+    "@types/estree" "*"
+    "@types/json-schema" "*"
+
+"@types/estree@*", "@types/estree@^0.0.50":
+  version "0.0.50"
+  resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83"
+  integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==
+
+"@types/geojson@*":
+  version "7946.0.8"
+  resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.8.tgz#30744afdb385e2945e22f3b033f897f76b1f12ca"
+  integrity sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA==
+
+"@types/glob@^7.1.1":
+  version "7.1.4"
+  resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.4.tgz#ea59e21d2ee5c517914cb4bc8e4153b99e566672"
+  integrity sha512-w+LsMxKyYQm347Otw+IfBXOv9UWVjpHpCDdbBMt8Kz/xbvCYNjP+0qPh91Km3iKfSRLBB0P7fAMf0KHrPu+MyA==
+  dependencies:
+    "@types/minimatch" "*"
+    "@types/node" "*"
+
+"@types/jquery@*":
+  version "3.5.6"
+  resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.5.6.tgz#97ac8e36dccd8ad8ed3f3f3b48933614d9fd8cf0"
+  integrity sha512-SmgCQRzGPId4MZQKDj9Hqc6kSXFNWZFHpELkyK8AQhf8Zr6HKfCzFv9ZC1Fv3FyQttJZOlap3qYb12h61iZAIg==
+  dependencies:
+    "@types/sizzle" "*"
+
+"@types/json-schema@*", "@types/json-schema@^7.0.8":
+  version "7.0.9"
+  resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
+  integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
+
+"@types/leaflet.markercluster@1.4.5":
+  version "1.4.5"
+  resolved "https://registry.yarnpkg.com/@types/leaflet.markercluster/-/leaflet.markercluster-1.4.5.tgz#dc4457e2dff9baaacc17c9c04c65ab69b5f361f7"
+  integrity sha512-R9Ql//z6muSGI5mPfr+FaKQQB7EIdQQyivYweVSdOrWr8WyNNFcSwfl+mqGYJFhRRCO/6lbZiM3scEyp9LdaFg==
+  dependencies:
+    "@types/leaflet" "*"
+
+"@types/leaflet@*", "@types/leaflet@1.7.4":
+  version "1.7.4"
+  resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.7.4.tgz#bb9430f69d588ca5829c1ba82657e179454f93a1"
+  integrity sha512-a3qYlMwJ62+WRoiDmYODUD4KywA14jP2XohAkAWtELGuMAD3MohZa/MmIvQDqF52xNI9OYaY8BMsL+9z7yf2HQ==
+  dependencies:
+    "@types/geojson" "*"
+
+"@types/minimatch@*":
+  version "3.0.5"
+  resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
+  integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
+
+"@types/node@*":
+  version "16.7.2"
+  resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.2.tgz#0465a39b5456b61a04d98bd5545f8b34be340cb7"
+  integrity sha512-TbG4TOx9hng8FKxaVrCisdaxKxqEwJ3zwHoCWXZ0Jw6mnvTInpaB99/2Cy4+XxpXtjNv9/TgfGSvZFyfV/t8Fw==
+
+"@types/sizzle@*":
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef"
+  integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==
+
+"@webassemblyjs/ast@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7"
+  integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==
+  dependencies:
+    "@webassemblyjs/helper-numbers" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+
+"@webassemblyjs/floating-point-hex-parser@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f"
+  integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==
+
+"@webassemblyjs/helper-api-error@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16"
+  integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==
+
+"@webassemblyjs/helper-buffer@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5"
+  integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==
+
+"@webassemblyjs/helper-numbers@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae"
+  integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==
+  dependencies:
+    "@webassemblyjs/floating-point-hex-parser" "1.11.1"
+    "@webassemblyjs/helper-api-error" "1.11.1"
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/helper-wasm-bytecode@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1"
+  integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==
+
+"@webassemblyjs/helper-wasm-section@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a"
+  integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-buffer" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/wasm-gen" "1.11.1"
+
+"@webassemblyjs/ieee754@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614"
+  integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==
+  dependencies:
+    "@xtuc/ieee754" "^1.2.0"
+
+"@webassemblyjs/leb128@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5"
+  integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==
+  dependencies:
+    "@xtuc/long" "4.2.2"
+
+"@webassemblyjs/utf8@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff"
+  integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==
+
+"@webassemblyjs/wasm-edit@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6"
+  integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-buffer" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/helper-wasm-section" "1.11.1"
+    "@webassemblyjs/wasm-gen" "1.11.1"
+    "@webassemblyjs/wasm-opt" "1.11.1"
+    "@webassemblyjs/wasm-parser" "1.11.1"
+    "@webassemblyjs/wast-printer" "1.11.1"
+
+"@webassemblyjs/wasm-gen@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76"
+  integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/ieee754" "1.11.1"
+    "@webassemblyjs/leb128" "1.11.1"
+    "@webassemblyjs/utf8" "1.11.1"
+
+"@webassemblyjs/wasm-opt@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2"
+  integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-buffer" "1.11.1"
+    "@webassemblyjs/wasm-gen" "1.11.1"
+    "@webassemblyjs/wasm-parser" "1.11.1"
+
+"@webassemblyjs/wasm-parser@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199"
+  integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/helper-api-error" "1.11.1"
+    "@webassemblyjs/helper-wasm-bytecode" "1.11.1"
+    "@webassemblyjs/ieee754" "1.11.1"
+    "@webassemblyjs/leb128" "1.11.1"
+    "@webassemblyjs/utf8" "1.11.1"
+
+"@webassemblyjs/wast-printer@1.11.1":
+  version "1.11.1"
+  resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0"
+  integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==
+  dependencies:
+    "@webassemblyjs/ast" "1.11.1"
+    "@xtuc/long" "4.2.2"
+
+"@webpack-cli/configtest@^1.0.4":
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.0.4.tgz#f03ce6311c0883a83d04569e2c03c6238316d2aa"
+  integrity sha512-cs3XLy+UcxiP6bj0A6u7MLLuwdXJ1c3Dtc0RkKg+wiI1g/Ti1om8+/2hc2A2B60NbBNAbMgyBMHvyymWm/j4wQ==
+
+"@webpack-cli/info@^1.3.0":
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.3.0.tgz#9d78a31101a960997a4acd41ffd9b9300627fe2b"
+  integrity sha512-ASiVB3t9LOKHs5DyVUcxpraBXDOKubYu/ihHhU+t1UPpxsivg6Od2E2qU4gJCekfEddzRBzHhzA/Acyw/mlK/w==
+  dependencies:
+    envinfo "^7.7.3"
+
+"@webpack-cli/serve@^1.5.2":
+  version "1.5.2"
+  resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.5.2.tgz#ea584b637ff63c5a477f6f21604b5a205b72c9ec"
+  integrity sha512-vgJ5OLWadI8aKjDlOH3rb+dYyPd2GTZuQC/Tihjct6F9GpXGZINo3Y/IVuZVTM1eDQB+/AOsjPUWH/WySDaXvw==
+
+"@xtuc/ieee754@^1.2.0":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
+  integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==
+
+"@xtuc/long@4.2.2":
+  version "4.2.2"
+  resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
+  integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
+
+acorn-import-assertions@^1.7.6:
+  version "1.7.6"
+  resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.7.6.tgz#580e3ffcae6770eebeec76c3b9723201e9d01f78"
+  integrity sha512-FlVvVFA1TX6l3lp8VjDnYYq7R1nyW6x3svAt4nDgrWQ9SBaSh9CnbwgSUTasgfNfOG5HlM1ehugCvM+hjo56LA==
+
+acorn@^8.4.1:
+  version "8.4.1"
+  resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.4.1.tgz#56c36251fc7cabc7096adc18f05afe814321a28c"
+  integrity sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==
+
+ajv-keywords@^3.5.2:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
+  integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
+
+ajv@^6.12.5:
+  version "6.12.6"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
+  integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
+ansi-styles@^4.1.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
+  integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+  dependencies:
+    color-convert "^2.0.1"
+
+anymatch@~3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+  integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
+array-union@^1.0.1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+  integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=
+  dependencies:
+    array-uniq "^1.0.1"
+
+array-uniq@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+  integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
+
+balanced-match@^1.0.0:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
+  integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+binary-extensions@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
+  integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
+
+bootstrap@5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.1.0.tgz#543ef8f44f4b9af67b0230f19508542fec38ef55"
+  integrity sha512-bs74WNI9BgBo3cEovmdMHikSKoXnDgA6VQjJ7TyTotU6L7d41ZyCEEelPwkYEzsG/Zjv3ie9IE3EMAje0W9Xew==
+
+brace-expansion@^1.1.7:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
+  integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
+  dependencies:
+    balanced-match "^1.0.0"
+    concat-map "0.0.1"
+
+braces@^3.0.1, braces@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
+  integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
+  dependencies:
+    fill-range "^7.0.1"
+
+browserslist@^4.14.5:
+  version "4.16.8"
+  resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.8.tgz#cb868b0b554f137ba6e33de0ecff2eda403c4fb0"
+  integrity sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==
+  dependencies:
+    caniuse-lite "^1.0.30001251"
+    colorette "^1.3.0"
+    electron-to-chromium "^1.3.811"
+    escalade "^3.1.1"
+    node-releases "^1.1.75"
+
+buffer-from@^1.0.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
+  integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
+
+caniuse-lite@^1.0.30001251:
+  version "1.0.30001252"
+  resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001252.tgz#cb16e4e3dafe948fc4a9bb3307aea054b912019a"
+  integrity sha512-I56jhWDGMtdILQORdusxBOH+Nl/KgQSdDmpJezYddnAkVOmnoU8zwjTV9xAjMIYxr0iPreEAVylCGcmHCjfaOw==
+
+chalk@^4.1.0:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
+  integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+  dependencies:
+    ansi-styles "^4.1.0"
+    supports-color "^7.1.0"
+
+"chokidar@>=3.0.0 <4.0.0":
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
+  integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
+  dependencies:
+    anymatch "~3.1.2"
+    braces "~3.0.2"
+    glob-parent "~5.1.2"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.6.0"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
+chrome-trace-event@^1.0.2:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac"
+  integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==
+
+clean-webpack-plugin@4.0.0-alpha.0:
+  version "4.0.0-alpha.0"
+  resolved "https://registry.yarnpkg.com/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0-alpha.0.tgz#2aef48dfe7565360d128f5caa0904097d969d053"
+  integrity sha512-+X6mASBbGSVyw8L9/1rhQ+vS4uaQMopf194kX7Aes8qfezgCFL+qv5W0nwP3a0Tud5kUckARk8tFcoyOSKEjhg==
+  dependencies:
+    del "^4.1.1"
+
+clone-deep@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387"
+  integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==
+  dependencies:
+    is-plain-object "^2.0.4"
+    kind-of "^6.0.2"
+    shallow-clone "^3.0.0"
+
+color-convert@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
+  integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+  dependencies:
+    color-name "~1.1.4"
+
+color-name@~1.1.4:
+  version "1.1.4"
+  resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
+  integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+colorette@^1.2.1, colorette@^1.2.2, colorette@^1.3.0:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af"
+  integrity sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==
+
+commander@^2.20.0:
+  version "2.20.3"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
+  integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
+
+commander@^7.0.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
+  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
+concat-map@0.0.1:
+  version "0.0.1"
+  resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+  integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
+
+cross-spawn@^7.0.3:
+  version "7.0.3"
+  resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
+  integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+  dependencies:
+    path-key "^3.1.0"
+    shebang-command "^2.0.0"
+    which "^2.0.1"
+
+css-loader@6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.2.0.tgz#9663d9443841de957a3cb9bcea2eda65b3377071"
+  integrity sha512-/rvHfYRjIpymZblf49w8jYcRo2y9gj6rV8UroHGmBxKrIyGLokpycyKzp9OkitvqT29ZSpzJ0Ic7SpnJX3sC8g==
+  dependencies:
+    icss-utils "^5.1.0"
+    postcss "^8.2.15"
+    postcss-modules-extract-imports "^3.0.0"
+    postcss-modules-local-by-default "^4.0.0"
+    postcss-modules-scope "^3.0.0"
+    postcss-modules-values "^4.0.0"
+    postcss-value-parser "^4.1.0"
+    semver "^7.3.5"
+
+cssesc@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
+  integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+del@^4.1.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4"
+  integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==
+  dependencies:
+    "@types/glob" "^7.1.1"
+    globby "^6.1.0"
+    is-path-cwd "^2.0.0"
+    is-path-in-cwd "^2.0.0"
+    p-map "^2.0.0"
+    pify "^4.0.1"
+    rimraf "^2.6.3"
+
+electron-to-chromium@^1.3.811:
+  version "1.3.818"
+  resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.818.tgz#32ed024fa8316e5d469c96eecbea7d2463d80085"
+  integrity sha512-c/Z9gIr+jDZAR9q+mn40hEc1NharBT+8ejkarjbCDnBNFviI6hvcC5j2ezkAXru//bTnQp5n6iPi0JA83Tla1Q==
+
+enhanced-resolve@^5.0.0, enhanced-resolve@^5.8.0:
+  version "5.8.2"
+  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.8.2.tgz#15ddc779345cbb73e97c611cd00c01c1e7bf4d8b"
+  integrity sha512-F27oB3WuHDzvR2DOGNTaYy0D5o0cnrv8TeI482VM4kYgQd/FT9lUQwuNsJ0oOHtBUq7eiW5ytqzp7nBFknL+GA==
+  dependencies:
+    graceful-fs "^4.2.4"
+    tapable "^2.2.0"
+
+envinfo@^7.7.3:
+  version "7.8.1"
+  resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475"
+  integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==
+
+es-module-lexer@^0.7.1:
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.7.1.tgz#c2c8e0f46f2df06274cdaf0dd3f3b33e0a0b267d"
+  integrity sha512-MgtWFl5No+4S3TmhDmCz2ObFGm6lEpTnzbQi+Dd+pw4mlTIZTmM2iAs5gRlmx5zS9luzobCSBSI90JM/1/JgOw==
+
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
+eslint-scope@5.1.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
+  integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
+  dependencies:
+    esrecurse "^4.3.0"
+    estraverse "^4.1.1"
+
+esrecurse@^4.3.0:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
+  integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+  dependencies:
+    estraverse "^5.2.0"
+
+estraverse@^4.1.1:
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
+  integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
+
+estraverse@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
+  integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
+
+events@^3.2.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
+  integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
+
+execa@^5.0.0:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
+  integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
+  dependencies:
+    cross-spawn "^7.0.3"
+    get-stream "^6.0.0"
+    human-signals "^2.1.0"
+    is-stream "^2.0.0"
+    merge-stream "^2.0.0"
+    npm-run-path "^4.0.1"
+    onetime "^5.1.2"
+    signal-exit "^3.0.3"
+    strip-final-newline "^2.0.0"
+
+fast-deep-equal@^3.1.1:
+  version "3.1.3"
+  resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
+  integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-json-stable-stringify@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
+  integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fastest-levenshtein@^1.0.12:
+  version "1.0.12"
+  resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2"
+  integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==
+
+fill-range@^7.0.1:
+  version "7.0.1"
+  resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
+  integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
+  dependencies:
+    to-regex-range "^5.0.1"
+
+find-up@^4.0.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+  dependencies:
+    locate-path "^5.0.0"
+    path-exists "^4.0.0"
+
+fs.realpath@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+  integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
+
+fsevents@~2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
+  integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+function-bind@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+  integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
+
+get-stream@^6.0.0:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
+  integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
+
+glob-parent@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
+glob-to-regexp@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
+  integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
+
+glob@^7.0.3, glob@^7.1.3:
+  version "7.1.7"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
+  integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.4"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
+globby@^6.1.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
+  integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=
+  dependencies:
+    array-union "^1.0.1"
+    glob "^7.0.3"
+    object-assign "^4.0.1"
+    pify "^2.0.0"
+    pinkie-promise "^2.0.0"
+
+graceful-fs@^4.1.2, graceful-fs@^4.2.4:
+  version "4.2.8"
+  resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
+  integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
+
+has-flag@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
+  integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
+  integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
+  dependencies:
+    function-bind "^1.1.1"
+
+human-signals@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
+  integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
+
+icss-utils@^5.0.0, icss-utils@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
+  integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==
+
+import-local@^3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6"
+  integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==
+  dependencies:
+    pkg-dir "^4.2.0"
+    resolve-cwd "^3.0.0"
+
+inflight@^1.0.4:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+  integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
+  dependencies:
+    once "^1.3.0"
+    wrappy "1"
+
+inherits@2:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+interpret@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
+  integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==
+
+is-binary-path@~2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
+  integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+  dependencies:
+    binary-extensions "^2.0.0"
+
+is-core-module@^2.2.0:
+  version "2.6.0"
+  resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.6.0.tgz#d7553b2526fe59b92ba3e40c8df757ec8a709e19"
+  integrity sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==
+  dependencies:
+    has "^1.0.3"
+
+is-extglob@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+  integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
+
+is-glob@^4.0.1, is-glob@~4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
+  integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
+  dependencies:
+    is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
+  integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-path-cwd@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb"
+  integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==
+
+is-path-in-cwd@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb"
+  integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==
+  dependencies:
+    is-path-inside "^2.1.0"
+
+is-path-inside@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2"
+  integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==
+  dependencies:
+    path-is-inside "^1.0.2"
+
+is-plain-object@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+  integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
+  dependencies:
+    isobject "^3.0.1"
+
+is-stream@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
+  integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
+
+isexe@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+  integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
+
+isobject@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+  integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
+
+jest-worker@^27.0.2:
+  version "27.0.6"
+  resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.6.tgz#a5fdb1e14ad34eb228cfe162d9f729cdbfa28aed"
+  integrity sha512-qupxcj/dRuA3xHPMUd40gr2EaAurFbkwzOh7wfPaeE9id7hyjURRQoqNfHifHK3XjJU6YJJUQKILGUnwGPEOCA==
+  dependencies:
+    "@types/node" "*"
+    merge-stream "^2.0.0"
+    supports-color "^8.0.0"
+
+json-parse-better-errors@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
+  integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
+
+json-schema-traverse@^0.4.1:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
+  integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+kind-of@^6.0.2:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
+  integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+klona@^2.0.4:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0"
+  integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==
+
+leaflet.markercluster@1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.5.0.tgz#54db42485da32fc3d92c7ae22d0d7982879e0b67"
+  integrity sha512-Fvf/cq4o806mJL50n+fZW9+QALDDLPvt7vuAjlD2vfnxx3srMDs2vWINJze4nKYJYRY45OC6tM/669C3pLwMCA==
+
+leaflet@1.7.1:
+  version "1.7.1"
+  resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.7.1.tgz#10d684916edfe1bf41d688a3b97127c0322a2a19"
+  integrity sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw==
+
+loader-runner@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
+  integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
+
+locate-path@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+  integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+  dependencies:
+    p-locate "^4.1.0"
+
+lru-cache@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
+  integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+  dependencies:
+    yallist "^4.0.0"
+
+merge-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
+micromatch@^4.0.0:
+  version "4.0.4"
+  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
+  integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
+  dependencies:
+    braces "^3.0.1"
+    picomatch "^2.2.3"
+
+mime-db@1.49.0:
+  version "1.49.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed"
+  integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==
+
+mime-types@^2.1.27:
+  version "2.1.32"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5"
+  integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==
+  dependencies:
+    mime-db "1.49.0"
+
+mimic-fn@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
+  integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
+
+mini-css-extract-plugin@2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.2.0.tgz#48cb6d2bea8fa9eb36709856e003662eebb3eb92"
+  integrity sha512-91HeVHbq7PUJ4TwOuMTlFWfVWrLqf3SF0PlEDPV+wtgsfxrMebN9LLzflyQqdKLp4/H3PexRB1WLKsCqpWKkxQ==
+  dependencies:
+    schema-utils "^3.1.0"
+
+minimatch@^3.0.4:
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+  integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
+  dependencies:
+    brace-expansion "^1.1.7"
+
+nanoid@^3.1.23:
+  version "3.1.25"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152"
+  integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==
+
+neo-async@^2.6.2:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
+  integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+
+node-releases@^1.1.75:
+  version "1.1.75"
+  resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.75.tgz#6dd8c876b9897a1b8e5a02de26afa79bb54ebbfe"
+  integrity sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
+  integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+npm-run-path@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
+  integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
+  dependencies:
+    path-key "^3.0.0"
+
+object-assign@^4.0.1:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+  integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
+
+once@^1.3.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+  integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
+  dependencies:
+    wrappy "1"
+
+onetime@^5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
+  integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
+  dependencies:
+    mimic-fn "^2.1.0"
+
+p-limit@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+  dependencies:
+    p-try "^2.0.0"
+
+p-limit@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+  integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+  dependencies:
+    yocto-queue "^0.1.0"
+
+p-locate@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+  integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+  dependencies:
+    p-limit "^2.2.0"
+
+p-map@^2.0.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
+  integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
+
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+path-exists@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+  integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-is-absolute@^1.0.0:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+  integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
+
+path-is-inside@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+  integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
+
+path-key@^3.0.0, path-key@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
+  integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.6:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
+  integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
+  integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
+
+pify@^2.0.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+  integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
+
+pify@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
+  integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
+
+pinkie-promise@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+  integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o=
+  dependencies:
+    pinkie "^2.0.0"
+
+pinkie@^2.0.0:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+  integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
+
+pkg-dir@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
+  integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+  dependencies:
+    find-up "^4.0.0"
+
+postcss-modules-extract-imports@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
+  integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==
+
+postcss-modules-local-by-default@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c"
+  integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==
+  dependencies:
+    icss-utils "^5.0.0"
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.1.0"
+
+postcss-modules-scope@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06"
+  integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==
+  dependencies:
+    postcss-selector-parser "^6.0.4"
+
+postcss-modules-values@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c"
+  integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==
+  dependencies:
+    icss-utils "^5.0.0"
+
+postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
+  version "6.0.6"
+  resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea"
+  integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==
+  dependencies:
+    cssesc "^3.0.0"
+    util-deprecate "^1.0.2"
+
+postcss-value-parser@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
+  integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
+
+postcss@^8.2.15:
+  version "8.3.6"
+  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea"
+  integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==
+  dependencies:
+    colorette "^1.2.2"
+    nanoid "^3.1.23"
+    source-map-js "^0.6.2"
+
+punycode@^2.1.0:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
+  integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+
+randombytes@^2.1.0:
+  version "2.1.0"
+  resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
+  integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
+  dependencies:
+    safe-buffer "^5.1.0"
+
+readdirp@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+  dependencies:
+    picomatch "^2.2.1"
+
+rechoir@^0.7.0:
+  version "0.7.1"
+  resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686"
+  integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==
+  dependencies:
+    resolve "^1.9.0"
+
+resolve-cwd@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
+  integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
+  dependencies:
+    resolve-from "^5.0.0"
+
+resolve-from@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
+  integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
+
+resolve@^1.9.0:
+  version "1.20.0"
+  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
+  integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
+  dependencies:
+    is-core-module "^2.2.0"
+    path-parse "^1.0.6"
+
+rimraf@^2.6.3:
+  version "2.7.1"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
+  integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
+  dependencies:
+    glob "^7.1.3"
+
+safe-buffer@^5.1.0:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
+  integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+sass-loader@12.1.0:
+  version "12.1.0"
+  resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.1.0.tgz#b73324622231009da6fba61ab76013256380d201"
+  integrity sha512-FVJZ9kxVRYNZTIe2xhw93n3xJNYZADr+q69/s98l9nTCrWASo+DR2Ot0s5xTKQDDEosUkatsGeHxcH4QBp5bSg==
+  dependencies:
+    klona "^2.0.4"
+    neo-async "^2.6.2"
+
+sass@1.38.1:
+  version "1.38.1"
+  resolved "https://registry.yarnpkg.com/sass/-/sass-1.38.1.tgz#54dfb17fb168846b5850324b82fc62dc68f51bad"
+  integrity sha512-Lj8nPaSYOuRhgqdyShV50fY5jKnvaRmikUNalMPmbH+tKMGgEKVkltI/lP30PEfO2T1t6R9yc2QIBLgOc3uaFw==
+  dependencies:
+    chokidar ">=3.0.0 <4.0.0"
+
+schema-utils@^3.0.0, schema-utils@^3.1.0:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281"
+  integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==
+  dependencies:
+    "@types/json-schema" "^7.0.8"
+    ajv "^6.12.5"
+    ajv-keywords "^3.5.2"
+
+semver@^7.3.4, semver@^7.3.5:
+  version "7.3.5"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
+  integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
+  dependencies:
+    lru-cache "^6.0.0"
+
+serialize-javascript@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
+  integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
+  dependencies:
+    randombytes "^2.1.0"
+
+shallow-clone@^3.0.0:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
+  integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==
+  dependencies:
+    kind-of "^6.0.2"
+
+shebang-command@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
+  integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+  dependencies:
+    shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
+  integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+signal-exit@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
+  integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
+
+source-map-js@^0.6.2:
+  version "0.6.2"
+  resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
+  integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
+
+source-map-support@~0.5.19:
+  version "0.5.19"
+  resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
+  integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
+  dependencies:
+    buffer-from "^1.0.0"
+    source-map "^0.6.0"
+
+source-map@^0.6.0, source-map@^0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+  integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+source-map@~0.7.2:
+  version "0.7.3"
+  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
+  integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
+
+strip-final-newline@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
+  integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
+
+supports-color@^7.1.0:
+  version "7.2.0"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
+  integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+  dependencies:
+    has-flag "^4.0.0"
+
+supports-color@^8.0.0:
+  version "8.1.1"
+  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
+  integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
+  dependencies:
+    has-flag "^4.0.0"
+
+tapable@^2.1.1, tapable@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b"
+  integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==
+
+terser-webpack-plugin@^5.1.3:
+  version "5.1.4"
+  resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.1.4.tgz#c369cf8a47aa9922bd0d8a94fe3d3da11a7678a1"
+  integrity sha512-C2WkFwstHDhVEmsmlCxrXUtVklS+Ir1A7twrYzrDrQQOIMOaVAYykaoo/Aq1K0QRkMoY2hhvDQY1cm4jnIMFwA==
+  dependencies:
+    jest-worker "^27.0.2"
+    p-limit "^3.1.0"
+    schema-utils "^3.0.0"
+    serialize-javascript "^6.0.0"
+    source-map "^0.6.1"
+    terser "^5.7.0"
+
+terser@^5.7.0:
+  version "5.7.2"
+  resolved "https://registry.yarnpkg.com/terser/-/terser-5.7.2.tgz#d4d95ed4f8bf735cb933e802f2a1829abf545e3f"
+  integrity sha512-0Omye+RD4X7X69O0eql3lC4Heh/5iLj3ggxR/B5ketZLOtLiOqukUgjw3q4PDnNQbsrkKr3UMypqStQG3XKRvw==
+  dependencies:
+    commander "^2.20.0"
+    source-map "~0.7.2"
+    source-map-support "~0.5.19"
+
+to-regex-range@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
+  integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+  dependencies:
+    is-number "^7.0.0"
+
+ts-loader@9.2.5:
+  version "9.2.5"
+  resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.2.5.tgz#127733a5e9243bf6dafcb8aa3b8a266d8041dca9"
+  integrity sha512-al/ATFEffybdRMUIr5zMEWQdVnCGMUA9d3fXJ8dBVvBlzytPvIszoG9kZoR+94k6/i293RnVOXwMaWbXhNy9pQ==
+  dependencies:
+    chalk "^4.1.0"
+    enhanced-resolve "^5.0.0"
+    micromatch "^4.0.0"
+    semver "^7.3.4"
+
+typescript@4.3.5:
+  version "4.3.5"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
+  integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
+
+uri-js@^4.2.2:
+  version "4.4.1"
+  resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
+  integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+  dependencies:
+    punycode "^2.1.0"
+
+util-deprecate@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+
+v8-compile-cache@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
+  integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
+
+watchpack@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.2.0.tgz#47d78f5415fe550ecd740f99fe2882323a58b1ce"
+  integrity sha512-up4YAn/XHgZHIxFBVCdlMiWDj6WaLKpwVeGQk2I5thdYxF/KmF0aaz6TfJZ/hfl1h/XlcDr7k1KH7ThDagpFaA==
+  dependencies:
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.1.2"
+
+webpack-cli@4.8.0:
+  version "4.8.0"
+  resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.8.0.tgz#5fc3c8b9401d3c8a43e2afceacfa8261962338d1"
+  integrity sha512-+iBSWsX16uVna5aAYN6/wjhJy1q/GKk4KjKvfg90/6hykCTSgozbfz5iRgDTSJt/LgSbYxdBX3KBHeobIs+ZEw==
+  dependencies:
+    "@discoveryjs/json-ext" "^0.5.0"
+    "@webpack-cli/configtest" "^1.0.4"
+    "@webpack-cli/info" "^1.3.0"
+    "@webpack-cli/serve" "^1.5.2"
+    colorette "^1.2.1"
+    commander "^7.0.0"
+    execa "^5.0.0"
+    fastest-levenshtein "^1.0.12"
+    import-local "^3.0.2"
+    interpret "^2.2.0"
+    rechoir "^0.7.0"
+    v8-compile-cache "^2.2.0"
+    webpack-merge "^5.7.3"
+
+webpack-merge@5.8.0, webpack-merge@^5.7.3:
+  version "5.8.0"
+  resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61"
+  integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==
+  dependencies:
+    clone-deep "^4.0.1"
+    wildcard "^2.0.0"
+
+webpack-sources@^3.2.0:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.0.tgz#b16973bcf844ebcdb3afde32eda1c04d0b90f89d"
+  integrity sha512-fahN08Et7P9trej8xz/Z7eRu8ltyiygEo/hnRi9KqBUs80KeDcnf96ZJo++ewWd84fEf3xSX9bp4ZS9hbw0OBw==
+
+webpack@5.51.1:
+  version "5.51.1"
+  resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.51.1.tgz#41bebf38dccab9a89487b16dbe95c22e147aac57"
+  integrity sha512-xsn3lwqEKoFvqn4JQggPSRxE4dhsRcysWTqYABAZlmavcoTmwlOb9b1N36Inbt/eIispSkuHa80/FJkDTPos1A==
+  dependencies:
+    "@types/eslint-scope" "^3.7.0"
+    "@types/estree" "^0.0.50"
+    "@webassemblyjs/ast" "1.11.1"
+    "@webassemblyjs/wasm-edit" "1.11.1"
+    "@webassemblyjs/wasm-parser" "1.11.1"
+    acorn "^8.4.1"
+    acorn-import-assertions "^1.7.6"
+    browserslist "^4.14.5"
+    chrome-trace-event "^1.0.2"
+    enhanced-resolve "^5.8.0"
+    es-module-lexer "^0.7.1"
+    eslint-scope "5.1.1"
+    events "^3.2.0"
+    glob-to-regexp "^0.4.1"
+    graceful-fs "^4.2.4"
+    json-parse-better-errors "^1.0.2"
+    loader-runner "^4.2.0"
+    mime-types "^2.1.27"
+    neo-async "^2.6.2"
+    schema-utils "^3.1.0"
+    tapable "^2.1.1"
+    terser-webpack-plugin "^5.1.3"
+    watchpack "^2.2.0"
+    webpack-sources "^3.2.0"
+
+which@^2.0.1:
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
+  integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+  dependencies:
+    isexe "^2.0.0"
+
+wildcard@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec"
+  integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==
+
+wrappy@1:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
+
+yallist@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
+  integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
+
+yocto-queue@^0.1.0:
+  version "0.1.0"
+  resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+  integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
-- 
GitLab


From 42da092ffd086ba20d76fb306d8db363b0c44a52 Mon Sep 17 00:00:00 2001
From: jnizet <jb@ninja-squad.com>
Date: Fri, 27 Aug 2021 15:01:54 +0200
Subject: [PATCH 15/16] feat: implement styling

---
 .../resources/static/assets/images/logo.png   | Bin 0 -> 36294 bytes
 .../templates/fragments/institute.html        |   2 +-
 .../resources/templates/fragments/map.html    |   2 +-
 .../resources/templates/fragments/row.html    |   4 +-
 .../resources/templates/fragments/source.html |   2 +-
 .../resources/templates/fragments/xrefs.html  |  12 +-
 .../main/resources/templates/germplasm.html   | 574 +++++++++---------
 .../main/resources/templates/layout/main.html |  17 +-
 .../src/main/resources/templates/site.html    |  79 +--
 .../src/main/resources/templates/study.html   | 130 ++--
 web/src/style/style.scss                      |  76 ++-
 11 files changed, 499 insertions(+), 399 deletions(-)
 create mode 100644 backend/src/main/resources/static/assets/images/logo.png

diff --git a/backend/src/main/resources/static/assets/images/logo.png b/backend/src/main/resources/static/assets/images/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..86b3ab108693a19cca5a05c95faebd8573e23389
GIT binary patch
literal 36294
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
No{ph*g_a}g{{ePjMBxAc

literal 0
HcmV?d00001

diff --git a/backend/src/main/resources/templates/fragments/institute.html b/backend/src/main/resources/templates/fragments/institute.html
index 0efa76dd..b2e8da3a 100644
--- a/backend/src/main/resources/templates/fragments/institute.html
+++ b/backend/src/main/resources/templates/fragments/institute.html
@@ -11,7 +11,7 @@ Its unique argument (institute) is an InstituteVO
 
 <th:block th:fragment="institute(institute)">
   <div class="text-center py-2" th:if="${institute.logo}">
-    <img th:src="${institute.logo}" />
+    <img class="img-fluid" th:src="${institute.logo}" />
   </div>
   <div th:replace="fragments/row::text-row(label='Code', text=${institute.instituteCode})"></div>
   <div th:replace="fragments/row::text-row(label='Acronym', text=${institute.acronym})"></div>
diff --git a/backend/src/main/resources/templates/fragments/map.html b/backend/src/main/resources/templates/fragments/map.html
index 7fce9d1e..35ebed33 100644
--- a/backend/src/main/resources/templates/fragments/map.html
+++ b/backend/src/main/resources/templates/fragments/map.html
@@ -10,7 +10,7 @@ to display
 -->
 <div th:fragment="map" id="map-container" class="d-none">
   <div id="map" class="border rounded"></div>
-  <div class="map-legend mt-1">
+  <div class="map-legend mt-1 small">
     <img th:src="@{/assets/images/marker-icon-red.png}" id="red"/>
     <label for="red" class="me-2">Origin site</label>
     <img th:src="@{/assets/images/marker-icon-blue.png}" id="blue"/>
diff --git a/backend/src/main/resources/templates/fragments/row.html b/backend/src/main/resources/templates/fragments/row.html
index e5eb9e7f..8676b590 100644
--- a/backend/src/main/resources/templates/fragments/row.html
+++ b/backend/src/main/resources/templates/fragments/row.html
@@ -19,7 +19,7 @@ into a block with the condition:
   </th:block>
 -->
 
-<div th:fragment="row(label, content)" class="row py-2">
+<div th:fragment="row(label, content)" class="row f-row">
   <div class="col-md-4 label pb-1 pb-md-0" th:text="${label}"></div>
   <div class="col">
     <th:block th:replace="${content}" />
@@ -40,7 +40,7 @@ into a block with the condition:
     <div th:replace="fragments/row::text-row(label='Some label', text=${someTextExpression})"></div>
   </th:block>
 -->
-<div th:fragment="text-row(label, text)" th:unless="${#strings.isEmpty(text)}" class="row py-2">
+<div th:fragment="text-row(label, text)" th:unless="${#strings.isEmpty(text)}" class="row f-row">
   <div class="col-md-4 label pb-1 pb-md-0" th:text="${label}"></div>
   <div class="col" th:text="${text}"></div>
 </div>
diff --git a/backend/src/main/resources/templates/fragments/source.html b/backend/src/main/resources/templates/fragments/source.html
index 795717ca..b0963881 100644
--- a/backend/src/main/resources/templates/fragments/source.html
+++ b/backend/src/main/resources/templates/fragments/source.html
@@ -16,7 +16,7 @@ The entityType argument is a string, which is used in the message
   <th:block th:if="${source != null}">
     <div th:replace="fragments/row::row(label='Source', content=~{::.source})">
       <a class="source" target="_blank" th:href="${source.url}">
-        <img style="max-height: 60px;" th:src="${source.image}" th:alt="${source.name} + ' logo'" />
+        <img class="img-fluid" style="max-height: 60px;" th:src="${source.image}" th:alt="${source.name} + ' logo'" />
       </a>
     </div>
   </th:block>
diff --git a/backend/src/main/resources/templates/fragments/xrefs.html b/backend/src/main/resources/templates/fragments/xrefs.html
index 508b8923..c60310b6 100644
--- a/backend/src/main/resources/templates/fragments/xrefs.html
+++ b/backend/src/main/resources/templates/fragments/xrefs.html
@@ -9,12 +9,11 @@ Reusable fragment displaying a cross references section, with its title.
 The unique argument (crossReferences) is a List<XRefDocumentVO>
 -->
 
-<div th:fragment="xrefs(crossReferences)" th:if="${!#lists.isEmpty(crossReferences)}">
+<div class="f-card" th:fragment="xrefs(crossReferences)" th:if="${!#lists.isEmpty(crossReferences)}">
   <h2>Cross references</h2>
-
-  <div class="table-responsive scroll-big-table table-card-body">
-    <div class="card">
-      <table class="table table-sm table-striped">
+  <div class="f-card-body">
+    <div class="scroll-table-container scroll-table-container-big">
+      <table class="table table-sm table-striped table-sticky table-responsive-sm">
         <thead>
           <tr>
             <th scope="col">Name</th>
@@ -28,13 +27,12 @@ The unique argument (crossReferences) is a List<XRefDocumentVO>
             <td><a th:href="${crossRef.url}" target="_blank" th:text="${crossRef.name}"></a></td>
             <td th:text="${crossRef.databaseName}"></td>
             <td th:text="${crossRef.entryType}"></td>
-            <td th:text="${#strings.abbreviate(crossRef.description, 120)}"></td>
+            <td style="min-width: 30rem;" th:text="${#strings.abbreviate(crossRef.description, 120)}"></td>
           </tr>
         </tbody>
       </table>
     </div>
   </div>
-
 </div>
 
 </body>
diff --git a/backend/src/main/resources/templates/germplasm.html b/backend/src/main/resources/templates/germplasm.html
index 864b1813..f5c6a4ce 100644
--- a/backend/src/main/resources/templates/germplasm.html
+++ b/backend/src/main/resources/templates/germplasm.html
@@ -20,7 +20,7 @@
 
   <div th:replace="fragments/map::map"></div>
 
-  <div class="row align-items-center justify-content-center">
+  <div class="row align-items-center justify-content-center mt-4">
     <div class="col-auto field" th:if="${model.germplasm.photo != null && model.germplasm.photo.thumbnailFile != null}">
       <template id="photo-popover">
         <div class="card">
@@ -34,212 +34,227 @@
         </div>
       </template>
 
-      <button class="btn btn-link p-0"
-              data-bs-toggle="popover"
-              th:data-bs-title="${model.germplasm.photo.photoName}"
-              data-bs-element="#photo-popover"
-              data-bs-container="body"
-              data-bs-trigger="focus">
+      <a role="button"
+         class="d-flex flex-column align-items-center"
+         data-bs-toggle="popover"
+         tabindex="0"
+         th:data-bs-title="${model.germplasm.photo.photoName}"
+         data-bs-element="#photo-popover"
+         data-bs-container="body"
+         data-bs-trigger="focus">
         <img th:src="${model.germplasm.photo.thumbnailFile}" class="img-fluid" />
 
         <figcaption class="figure-caption">
           © <span th:text="${model.germplasm.photo.copyright}"></span>
         </figcaption>
-      </button>
+      </a>
     </div>
 
-
     <div class="col-12 col-lg">
-      <h2>Identification</h2>
-
-      <div th:replace="fragments/row::text-row(label='Germplasm name', text=${model.germplasm.germplasmName})"></div>
-      <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.accessionNumber})"></div>
+      <div class="f-card">
+        <h2>Identification</h2>
 
-      <div th:replace="fragments/source::source(source=${model.source}, url=${model.germplasm.url}, entityType='germplasm')"></div>
+        <div class="f-card-body">
+          <div th:replace="fragments/row::text-row(label='Germplasm name', text=${model.germplasm.germplasmName})"></div>
+          <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.accessionNumber})"></div>
 
-      <th:block th:unless="${#lists.isEmpty(model.germplasm.synonyms)}">
-        <div th:replace="fragments/row::row(label='Accession synonyms', content=~{::#accession-synonyms})">
-          <div id="accession-synonyms" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.synonyms, ', ')}"></div>
-        </div>
-      </th:block>
+          <div th:replace="fragments/source::source(source=${model.source}, url=${model.germplasm.url}, entityType='germplasm')"></div>
 
-      <th:block th:unless="${#strings.isEmpty(model.taxon)}">
-        <div th:replace="fragments/row::row(label='Taxon', content=~{::#taxon})">
-          <div id="taxon">
-            <template id="taxon-popover">
-              <th:block th:unless="${#strings.isEmpty(model.germplasm.genus)}">
-                <div th:replace="fragments/row::row(label='Genus', content=~{::#taxon-genus})">
-                  <em id="taxon-genus" th:text="${model.germplasm.genus}"></em>
-                </div>
-              </th:block>
-              <th:block th:unless="${#strings.isEmpty(model.germplasm.species)}">
-                <div th:replace="fragments/row::row(label='Species', content=~{::#taxon-species})">
-                  <span id="taxon-species">
-                    <em th:text="${model.germplasm.species}"></em>
-                    <span th:unless="${#strings.isEmpty(model.germplasm.speciesAuthority)}"
-                          th:text="${'(' + model.germplasm.speciesAuthority + ')'}"></span>
-                  </span>
-                </div>
-              </th:block>
-              <th:block th:unless="${#strings.isEmpty(model.germplasm.subtaxa)}">
-                <div th:replace="fragments/row::row(label='Subtaxa', content=~{::#taxon-subtaxa})">
-                  <span id="taxon-subtaxa">
-                    <em th:text="${model.germplasm.subtaxa}"></em>
-                    <span th:unless="${#strings.isEmpty(model.germplasm.subtaxaAuthority)}"
-                          th:text="${'(' + model.germplasm.subtaxaAuthority + ')'}"></span>
-                  </span>
-                </div>
-              </th:block>
-
-              <div th:replace="fragments/row::text-row(label='Authority', text=${model.taxonAuthor})"></div>
+          <th:block th:unless="${#lists.isEmpty(model.germplasm.synonyms)}">
+            <div th:replace="fragments/row::row(label='Accession synonyms', content=~{::#accession-synonyms})">
+              <div id="accession-synonyms" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.synonyms, ', ')}"></div>
+            </div>
+          </th:block>
 
-              <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonIds)}">
-                <div th:replace="fragments/row::row(label='Taxon IDs', content=~{::#taxon-ids})">
-                  <div id="taxon-ids">
-                    <div th:each="taxonId : ${model.germplasm.taxonIds}" class="row">
-                      <div class="col-6 text-nowrap" th:text="${taxonId.sourceName}"></div>
-                      <div class="col-6">
-                        <span class="taxon-id"
-                              th:replace="fragments/link::link(label=${taxonId.taxonId}, url=${#faidare.taxonIdUrl(taxonId)})"></span>
+          <th:block th:unless="${#strings.isEmpty(model.taxon)}">
+            <div th:replace="fragments/row::row(label='Taxon', content=~{::#taxon})">
+              <div id="taxon">
+                <template id="taxon-popover">
+                  <th:block th:unless="${#strings.isEmpty(model.germplasm.genus)}">
+                    <div th:replace="fragments/row::row(label='Genus', content=~{::#taxon-genus})">
+                      <em id="taxon-genus" th:text="${model.germplasm.genus}"></em>
+                    </div>
+                  </th:block>
+                  <th:block th:unless="${#strings.isEmpty(model.germplasm.species)}">
+                    <div th:replace="fragments/row::row(label='Species', content=~{::#taxon-species})">
+                      <span id="taxon-species">
+                        <em th:text="${model.germplasm.species}"></em>
+                        <span th:unless="${#strings.isEmpty(model.germplasm.speciesAuthority)}"
+                              th:text="${'(' + model.germplasm.speciesAuthority + ')'}"></span>
+                      </span>
+                    </div>
+                  </th:block>
+                  <th:block th:unless="${#strings.isEmpty(model.germplasm.subtaxa)}">
+                    <div th:replace="fragments/row::row(label='Subtaxa', content=~{::#taxon-subtaxa})">
+                      <span id="taxon-subtaxa">
+                        <em th:text="${model.germplasm.subtaxa}"></em>
+                        <span th:unless="${#strings.isEmpty(model.germplasm.subtaxaAuthority)}"
+                              th:text="${'(' + model.germplasm.subtaxaAuthority + ')'}"></span>
+                      </span>
+                    </div>
+                  </th:block>
+
+                  <div th:replace="fragments/row::text-row(label='Authority', text=${model.taxonAuthor})"></div>
+
+                  <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonIds)}">
+                    <div th:replace="fragments/row::row(label='Taxon IDs', content=~{::#taxon-ids})">
+                      <div id="taxon-ids">
+                        <div th:each="taxonId : ${model.germplasm.taxonIds}" class="row">
+                          <div class="col-6 text-nowrap" th:text="${taxonId.sourceName}"></div>
+                          <div class="col-6">
+                            <span class="taxon-id"
+                                  th:replace="fragments/link::link(label=${taxonId.taxonId}, url=${#faidare.taxonIdUrl(taxonId)})"></span>
+                          </div>
+                        </div>
                       </div>
                     </div>
-                  </div>
-                </div>
-              </th:block>
+                  </th:block>
 
-              <div th:replace="fragments/row::text-row(label='Comment', text=${model.germplasm.taxonComment})"></div>
-              <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonCommonNames)}">
-                <div th:replace="fragments/row::row(label='Taxon common names', content=~{::#taxon-common-names})">
-                  <div id="taxon-common-names" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.taxonCommonNames, ', ')}"></div>
-                </div>
-              </th:block>
-              <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonSynonyms)}">
-                <div th:replace="fragments/row::row(label='Taxon common names', content=~{::#taxon-synonyms})">
-                  <div id="taxon-synonyms" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.taxonSynonyms, ', ')}"></div>
-                </div>
-              </th:block>
-            </template>
-            <button class="btn btn-link p-0"
-                    data-bs-toggle="popover"
-                    th:data-bs-title="${model.taxon}"
-                    data-bs-element="#taxon-popover"
-                    data-bs-container="body"
-                    data-bs-trigger="focus">
-              <em th:text="${model.taxon}"></em>
-              <th:block th:unless="${#strings.isEmpty(model.taxonAuthor)}">(<span th:text="${model.taxonAuthor}"></span>)</th:block>
-            </button>
-          </div>
-        </div>
-      </th:block>
+                  <div th:replace="fragments/row::text-row(label='Comment', text=${model.germplasm.taxonComment})"></div>
+                  <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonCommonNames)}">
+                    <div th:replace="fragments/row::row(label='Taxon common names', content=~{::#taxon-common-names})">
+                      <div id="taxon-common-names" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.taxonCommonNames, ', ')}"></div>
+                    </div>
+                  </th:block>
+                  <th:block th:unless="${#lists.isEmpty(model.germplasm.taxonSynonyms)}">
+                    <div th:replace="fragments/row::row(label='Taxon common names', content=~{::#taxon-synonyms})">
+                      <div id="taxon-synonyms" class="content-overflow" th:text="${#strings.listJoin(model.germplasm.taxonSynonyms, ', ')}"></div>
+                    </div>
+                  </th:block>
+                </template>
+                <a role="button"
+                   tabindex="0"
+                   data-bs-toggle="popover"
+                   th:data-bs-title="${model.taxon}"
+                   data-bs-element="#taxon-popover"
+                   data-bs-container="body"
+                   data-bs-trigger="focus">
+                  <em th:text="${model.taxon}"></em>
+                  <th:block th:unless="${#strings.isEmpty(model.taxonAuthor)}">(<span th:text="${model.taxonAuthor}"></span>)</th:block>
+                </a>
+              </div>
+            </div>
+          </th:block>
 
-      <div th:replace="fragments/row::text-row(label='Biological status', text=${model.germplasm.biologicalStatusOfAccessionCode})"></div>
-      <div th:replace="fragments/row::text-row(label='Genetic nature', text=${model.germplasm.geneticNature})"></div>
-      <div th:replace="fragments/row::text-row(label='Seed source', text=${model.germplasm.seedSource})"></div>
-      <div th:replace="fragments/row::text-row(label='Pedigree', text=${model.germplasm.pedigree})"></div>
-      <div th:replace="fragments/row::text-row(label='Comments', text=${model.germplasm.comment})"></div>
+          <div th:replace="fragments/row::text-row(label='Biological status', text=${model.germplasm.biologicalStatusOfAccessionCode})"></div>
+          <div th:replace="fragments/row::text-row(label='Genetic nature', text=${model.germplasm.geneticNature})"></div>
+          <div th:replace="fragments/row::text-row(label='Seed source', text=${model.germplasm.seedSource})"></div>
+          <div th:replace="fragments/row::text-row(label='Pedigree', text=${model.germplasm.pedigree})"></div>
+          <div th:replace="fragments/row::text-row(label='Comments', text=${model.germplasm.comment})"></div>
 
-      <th:block th:if="${model.germplasm.originSite != null && !#strings.isEmpty(model.germplasm.originSite.siteName)}">
-        <div th:replace="fragments/row::row(label='Origin site', content=~{::#origin-site})">
-          <a id="origin-site" th:href="@{/sites/{siteId}(siteId=${#faidare.toSiteParam(model.germplasm.originSite.siteId)})}" th:text="${model.germplasm.originSite.siteName}"></a>
+          <th:block th:if="${model.germplasm.originSite != null && !#strings.isEmpty(model.germplasm.originSite.siteName)}">
+            <div th:replace="fragments/row::row(label='Origin site', content=~{::#origin-site})">
+              <a id="origin-site" th:href="@{/sites/{siteId}(siteId=${#faidare.toSiteParam(model.germplasm.originSite.siteId)})}" th:text="${model.germplasm.originSite.siteName}"></a>
+            </div>
+          </th:block>
         </div>
-      </th:block>
+      </div>
     </div>
   </div>
 
-  <th:block th:if="${model.germplasm.holdingInstitute}">
+  <div class="f-card" th:if="${model.germplasm.holdingInstitute}">
     <h2>Depositary</h2>
-    <template id="holding-institute-popover">
-      <div th:replace="fragments/institute::institute(institute=${model.germplasm.holdingInstitute})"></div>
-    </template>
-    <div th:replace="fragments/row::row(label='Institution', content=~{::#institution})">
-      <button id="institution"
-              data-bs-toggle="popover"
-              th:data-bs-title="${model.germplasm.holdingInstitute.instituteName}"
-              data-bs-element="#holding-institute-popover"
-              data-bs-container="body"
-              data-bs-trigger="focus"
-              class="btn btn-link p-0"
-              th:text="${model.germplasm.holdingInstitute.instituteName}"></button>
-    </div>
-
-    <th:block th:if="${model.germplasm.holdingGenbank != null && !#strings.isEmpty(model.germplasm.holdingGenbank.instituteName) && !#strings.isEmpty(model.germplasm.holdingGenbank.webSite)}">
-      <div th:replace="fragments/row::row(label='Stock center name', content=~{::#stock-center-name})">
-        <a id="stock-center-name"
-           target="_blank"
-           th:href="${model.germplasm.holdingGenbank.webSite}"
-           th:text="${model.germplasm.holdingGenbank.instituteName}"></a>
+    <div class="f-card-body">
+      <template id="holding-institute-popover">
+        <div th:replace="fragments/institute::institute(institute=${model.germplasm.holdingInstitute})"></div>
+      </template>
+      <div th:replace="fragments/row::row(label='Institution', content=~{::#institution})">
+        <a id="institution"
+           role="button"
+           tabindex="0"
+           data-bs-toggle="popover"
+           th:data-bs-title="${model.germplasm.holdingInstitute.instituteName}"
+           data-bs-element="#holding-institute-popover"
+           data-bs-container="body"
+           data-bs-trigger="focus"
+           th:text="${model.germplasm.holdingInstitute.instituteName}"></a>
       </div>
-    </th:block>
 
-    <div th:replace="fragments/row::text-row(label='Presence status', text=${model.germplasm.presenceStatus})"></div>
-  </th:block>
+      <th:block th:if="${model.germplasm.holdingGenbank != null && !#strings.isEmpty(model.germplasm.holdingGenbank.instituteName) && !#strings.isEmpty(model.germplasm.holdingGenbank.webSite)}">
+        <div th:replace="fragments/row::row(label='Stock center name', content=~{::#stock-center-name})">
+          <a id="stock-center-name"
+             target="_blank"
+             th:href="${model.germplasm.holdingGenbank.webSite}"
+             th:text="${model.germplasm.holdingGenbank.instituteName}"></a>
+        </div>
+      </th:block>
+
+      <div th:replace="fragments/row::text-row(label='Presence status', text=${model.germplasm.presenceStatus})"></div>
+    </div>
+  </div>
 
-  <th:block th:if="${model.collecting}">
+  <div class="f-card" th:if="${model.collecting}">
     <h2>Collector</h2>
-    <th:block th:if="${model.germplasm.collectingSite != null && !#strings.isEmpty(model.germplasm.collectingSite.siteName)}">
-      <div th:replace="fragments/row::row(label='Collecting site', content=~{::#collecting-site})">
-        <a id="collecting-site"
-           th:href="@{/sites/{siteId}(siteId=${#faidare.toSiteParam(model.germplasm.collectingSite.siteId)})}"
-           th:text="${model.germplasm.collectingSite.siteName}"
-        ></a>
-      </div>
-    </th:block>
+    <div class="f-card-body">
+      <th:block th:if="${model.germplasm.collectingSite != null && !#strings.isEmpty(model.germplasm.collectingSite.siteName)}">
+        <div th:replace="fragments/row::row(label='Collecting site', content=~{::#collecting-site})">
+          <a id="collecting-site"
+             th:href="@{/sites/{siteId}(siteId=${#faidare.toSiteParam(model.germplasm.collectingSite.siteId)})}"
+             th:text="${model.germplasm.collectingSite.siteName}"
+          ></a>
+        </div>
+      </th:block>
 
-    <div th:replace="fragments/row::text-row(label='Material type', text=${model.germplasm.collector.materialType})"></div>
-    <div th:replace="fragments/row::text-row(label='Collectors', text=${model.germplasm.collector.collectors})"></div>
+      <div th:replace="fragments/row::text-row(label='Material type', text=${model.germplasm.collector.materialType})"></div>
+      <div th:replace="fragments/row::text-row(label='Collectors', text=${model.germplasm.collector.collectors})"></div>
 
-    <th:block th:if="${!#strings.isEmpty(model.germplasm.acquisitionDate) && model.germplasm.collector.accessionCreationDate == null}">
-      <div th:replace="fragments/row::text-row(label='Acquisition / Creation date', text=${model.germplasm.acquisitionDate})"></div>
-    </th:block>
+      <th:block th:if="${!#strings.isEmpty(model.germplasm.acquisitionDate) && model.germplasm.collector.accessionCreationDate == null}">
+        <div th:replace="fragments/row::text-row(label='Acquisition / Creation date', text=${model.germplasm.acquisitionDate})"></div>
+      </th:block>
 
-    <th:block th:if="${model.germplasm.collector.institute != null && !#strings.isEmpty(model.germplasm.collector.institute.instituteName)}">
-      <template id="collector-institute-popover">
-        <div th:replace="fragments/institute::institute(institute=${model.germplasm.collector.institute})"></div>
-      </template>
-      <div th:replace="fragments/row::row(label='Institution', content=~{::#collecting-institution})">
-        <button id="collecting-institution"
-                data-bs-toggle="popover"
-                th:data-bs-title="${model.germplasm.collector.institute.instituteName}"
-                data-bs-element="#collector-institute-popover"
-                data-bs-container="body"
-                data-bs-trigger="focus"
-                class="btn btn-link p-0"
-                th:text="${model.germplasm.collector.institute.instituteName}"></button>
-      </div>
-    </th:block>
+      <th:block th:if="${model.germplasm.collector.institute != null && !#strings.isEmpty(model.germplasm.collector.institute.instituteName)}">
+        <template id="collector-institute-popover">
+          <div th:replace="fragments/institute::institute(institute=${model.germplasm.collector.institute})"></div>
+        </template>
+        <div th:replace="fragments/row::row(label='Institution', content=~{::#collecting-institution})">
+          <a id="collecting-institution"
+             role="button"
+             tabindex="0"
+             data-bs-toggle="popover"
+             th:data-bs-title="${model.germplasm.collector.institute.instituteName}"
+             data-bs-element="#collector-institute-popover"
+             data-bs-container="body"
+             data-bs-trigger="focus"
+             th:text="${model.germplasm.collector.institute.instituteName}"></a>
+        </div>
+      </th:block>
 
-    <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.collector.accessionNumber})"></div>
-  </th:block>
+      <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.collector.accessionNumber})"></div>
+    </div>
+  </div>
 
-  <th:block th:if="${model.breeding}">
+  <div class="f-card" th:if="${model.breeding}">
     <h2>Breeder</h2>
-    <th:block th:if="${model.germplasm.breeder.institute != null && !#strings.isEmpty(model.germplasm.breeder.institute.instituteName)}">
-      <template id="breeder-institute-popover">
-        <div th:replace="fragments/institute::institute(institute=${model.germplasm.breeder.institute})"></div>
-      </template>
-      <div th:replace="fragments/row::row(label='Institute', content=~{::#breeding-institution})">
-        <button id="breeding-institution"
-                data-bs-toggle="popover"
-                th:data-bs-title="${model.germplasm.breeder.institute.instituteName}"
-                data-bs-element="#breeder-institute-popover"
-                data-bs-container="body"
-                data-bs-trigger="focus"
-                class="btn btn-link p-0"
-                th:text="${model.germplasm.breeder.institute.instituteName}"></button>
-      </div>
-    </th:block>
+    <div class="f-card-body">
+      <th:block th:if="${model.germplasm.breeder.institute != null && !#strings.isEmpty(model.germplasm.breeder.institute.instituteName)}">
+        <template id="breeder-institute-popover">
+          <div th:replace="fragments/institute::institute(institute=${model.germplasm.breeder.institute})"></div>
+        </template>
+        <div th:replace="fragments/row::row(label='Institute', content=~{::#breeding-institution})">
+          <a id="breeding-institution"
+             role="button"
+             tabindex="0"
+             data-bs-toggle="popover"
+             th:data-bs-title="${model.germplasm.breeder.institute.instituteName}"
+             data-bs-element="#breeder-institute-popover"
+             data-bs-container="body"
+             data-bs-trigger="focus"
+             th:text="${model.germplasm.breeder.institute.instituteName}"></a>
+        </div>
+      </th:block>
 
-    <div th:replace="fragments/row::text-row(label='Accession creation year', text=${model.germplasm.breeder.accessionCreationDate})"></div>
-    <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.breeder.accessionNumber})"></div>
-    <div th:replace="fragments/row::text-row(label='Catalog registration year', text=${model.germplasm.breeder.registrationYear})"></div>
-    <div th:replace="fragments/row::text-row(label='Catalog deregistration year', text=${model.germplasm.breeder.deregistrationYear})"></div>
-  </th:block>
+      <div th:replace="fragments/row::text-row(label='Accession creation year', text=${model.germplasm.breeder.accessionCreationDate})"></div>
+      <div th:replace="fragments/row::text-row(label='Accession number', text=${model.germplasm.breeder.accessionNumber})"></div>
+      <div th:replace="fragments/row::text-row(label='Catalog registration year', text=${model.germplasm.breeder.registrationYear})"></div>
+      <div th:replace="fragments/row::text-row(label='Catalog deregistration year', text=${model.germplasm.breeder.deregistrationYear})"></div>
+    </div>
+  </div>
 
-  <th:block th:unless="${#lists.isEmpty(model.germplasm.donors)}">
+  <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.donors)}">
     <h2>Donors</h2>
-    <div class="table-responsive scroll-table table-card-body">
-      <div class="card">
-        <table class="table table-sm table-striped">
+    <div class="f-card-body">
+      <div class="scroll-table-container">
+        <table class="table table-sm table-striped table-sticky table-responsive-sm">
           <thead>
             <tr>
               <th scope="col">Institute name</th>
@@ -255,13 +270,14 @@
                 <template th:id="${'donor-institute-popover-' + donorIterStat.index}">
                   <div th:replace="fragments/institute::institute(institute=${row.donorInstitute})"></div>
                 </template>
-                <button data-bs-toggle="popover"
-                        th:data-bs-title="${row.donorInstitute.instituteName}"
-                        th:data-bs-element="${'#donor-institute-popover-' + donorIterStat.index}"
-                        data-bs-container="body"
-                        data-bs-trigger="focus"
-                        class="btn btn-link p-0"
-                        th:text="${row.donorInstitute.instituteName}"></button>
+                <a role="button"
+                   tabindex="0"
+                   data-bs-toggle="popover"
+                   th:data-bs-title="${row.donorInstitute.instituteName}"
+                   th:data-bs-element="${'#donor-institute-popover-' + donorIterStat.index}"
+                   data-bs-container="body"
+                   data-bs-trigger="focus"
+                   th:text="${row.donorInstitute.instituteName}"></a>
               </td>
               <td th:text="${row.donorInstituteCode}"></td>
               <td th:text="${row.donationDate}"></td>
@@ -272,13 +288,13 @@
         </table>
       </div>
     </div>
-  </th:block>
+  </div>
 
-  <th:block th:unless="${#lists.isEmpty(model.germplasm.distributors)}">
-    <h2>Donors</h2>
-    <div class="table-responsive scroll-table table-card-body">
-      <div class="card">
-        <table class="table table-sm table-striped">
+  <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.distributors)}">
+    <h2>Distributors</h2>
+    <div class="f-card-body">
+      <div class="scroll-table-container">
+        <table class="table table-sm table-striped table-sticky table-responsive-sm">
           <thead>
             <tr>
               <th scope="col">Institute</th>
@@ -292,13 +308,13 @@
                 <template th:id="${'distributor-institute-popover-' + distributorIterStat.index}">
                   <div th:replace="fragments/institute::institute(institute=${row.institute})"></div>
                 </template>
-                <button data-bs-toggle="popover"
-                        th:data-bs-title="${row.institute.instituteName}"
-                        th:data-bs-element="${'#distributor-institute-popover-' + distributorIterStat.index}"
-                        data-bs-container="body"
-                        data-bs-trigger="focus"
-                        class="btn btn-link p-0"
-                        th:text="${row.institute.instituteName}"></button>
+                <a role="button"
+                   tabindex="0"
+                   th:data-bs-title="${row.institute.instituteName}"
+                   th:data-bs-element="${'#distributor-institute-popover-' + distributorIterStat.index}"
+                   data-bs-container="body"
+                   data-bs-trigger="focus"
+                   th:text="${row.institute.instituteName}"></a>
               </td>
               <td th:text="${row.accessionNumber}"></td>
               <td th:text="${row.distributionStatus}"></td>
@@ -307,112 +323,120 @@
         </table>
       </div>
     </div>
-  </th:block>
+  </div>
 
-  <th:block th:unless="${#lists.isEmpty(model.attributes)}">
+  <div class="f-card" th:unless="${#lists.isEmpty(model.attributes)}">
     <h2>Evaluation Data</h2>
-    <th:block th:each="descriptor : ${model.attributes}">
-      <div th:replace="fragments/row::text-row(label=${descriptor.attributeName}, text=${descriptor.value})"></div>
-    </th:block>
-  </th:block>
+    <div class="f-card-body">
+      <th:block th:each="descriptor : ${model.attributes}">
+        <div th:replace="fragments/row::text-row(label=${descriptor.attributeName}, text=${descriptor.value})"></div>
+      </th:block>
+    </div>
+  </div>
 
-  <th:block th:if="${model.genealogyPresent}">
+  <div class="f-card" th:if="${model.genealogyPresent}">
     <h2>Genealogy</h2>
+    <div class="f-card-body">
+      <th:block th:if="${model.pedigree != null}">
+        <div th:replace="fragments/row::text-row(label='Crossing plan', text=${model.pedigree.crossingPlan})"></div>
+        <div th:replace="fragments/row::text-row(label='Crossing year', text=${model.pedigree.crossingYear})"></div>
+        <div th:replace="fragments/row::text-row(label='Family code', text=${model.pedigree.familyCode})"></div>
+        <th:block th:unless="${#strings.isEmpty(model.pedigree.parent1Name)}">
+          <div th:replace="fragments/row::row(label='Parent accessions', content=~{::#parent-accessions})">
+            <div id="parent-accessions">
+              <th:block th:if="${model.pedigree.parent1DbId}">
+                <div th:replace="fragments/row::row(label=${model.pedigree.parent1Type}, content=~{::#parent1-link})">
+                  <a id="parent1-link" th:href="@{/germplasms/{germplasmId}(germplasmId=${model.pedigree.parent1DbId})}" th:text="${model.pedigree.parent1Name}"></a>
+                </div>
+              </th:block>
 
-    <th:block th:if="${model.pedigree != null}">
-      <div th:replace="fragments/row::text-row(label='Crossing plan', text=${model.pedigree.crossingPlan})"></div>
-      <div th:replace="fragments/row::text-row(label='Crossing year', text=${model.pedigree.crossingYear})"></div>
-      <div th:replace="fragments/row::text-row(label='Family code', text=${model.pedigree.familyCode})"></div>
-      <th:block th:unless="${#strings.isEmpty(model.pedigree.parent1Name)}">
-        <div th:replace="fragments/row::row(label='Parent accessions', content=~{::#parent-accessions})">
-          <div id="parent-accessions">
-            <th:block th:if="${model.pedigree.parent1DbId}">
-              <div th:replace="fragments/row::row(label=${model.pedigree.parent1Type}, content=~{::#parent1-link})">
-                <a id="parent1-link" th:href="@{/germplasms/{germplasmId}(germplasmId=${model.pedigree.parent1DbId})}" th:text="${model.pedigree.parent1Name}"></a>
-              </div>
-            </th:block>
+              <th:block th:if="${model.pedigree.parent2DbId}">
+                <div th:replace="fragments/row::row(label=${model.pedigree.parent2Type}, content=~{::#parent2-link})">
+                  <a id="parent2-link" th:href="@{/germplasms/{germplasmId}(germplasmId=${model.pedigree.parent2DbId})}" th:text="${model.pedigree.parent2Name}"></a>
+                </div>
+              </th:block>
+            </div>
+          </div>
+        </th:block>
 
-            <th:block th:if="${model.pedigree.parent2DbId}">
-              <div th:replace="fragments/row::row(label=${model.pedigree.parent2Type}, content=~{::#parent2-link})">
-                <a id="parent2-link" th:href="@{/germplasms/{germplasmId}(germplasmId=${model.pedigree.parent2DbId})}" th:text="${model.pedigree.parent2Name}"></a>
-              </div>
-            </th:block>
+        <th:block th:unless="${#lists.isEmpty(model.pedigree.siblings)}">
+          <div th:replace="fragments/row::row(label='Sibling accessions', content=~{::#sibling-accessions})">
+            <div id="sibling-accessions" class="content-overflow">
+              <a th:each="sibling : ${model.pedigree.siblings}"
+                 th:href="@{/germplasms/{germplasmId}(germplasmId=${sibling.germplasmDbId})}"
+                 th:text="${sibling.defaultDisplayName}"></a>
+            </div>
           </div>
-        </div>
+        </th:block>
       </th:block>
 
-      <th:block th:unless="${#lists.isEmpty(model.pedigree.siblings)}">
-        <div th:replace="fragments/row::row(label='Sibling accessions', content=~{::#sibling-accessions})">
-          <div id="sibling-accessions" class="content-overflow">
-            <a th:each="sibling : ${model.pedigree.siblings}"
-               th:href="@{/germplasms/{germplasmId}(germplasmId=${sibling.germplasmDbId})}"
-               th:text="${sibling.defaultDisplayName}"></a>
+      <th:block th:unless="${#lists.isEmpty(model.germplasm.children)}">
+        <div th:replace="fragments/row::row(label='Descendants', content=~{::#descendants})">
+          <div id="descendants" class="content-overflow content-overflow-big">
+            <th:block th:each="child : ${model.germplasm.children}">
+              <div th:replace="fragments/row::row(label=${#strings.isEmpty(child.secondParentName) ? ('children of ' + child.firstParentName) : ('children of ' + child.firstParentName + ' x ' + child.secondParentName) }, content=~{::.descendant-child})">
+                <div class="descendant-child">
+                  <th:block th:each="sibling, siblingIterStat : ${child.sibblings}">
+                    <a th:href="@{/germplasms(pui=${sibling.pui})}"
+                       th:text="${sibling.name}"></a><th:block th:unless="${siblingIterStat.last}">, </th:block>
+                  </th:block>
+                </div>
+              </div>
+            </th:block>
           </div>
         </div>
       </th:block>
-    </th:block>
-
-    <th:block th:unless="${#lists.isEmpty(model.germplasm.children)}">
-      <div th:replace="fragments/row::row(label='Descendants', content=~{::#descendants})">
-        <div id="descendants" class="content-overflow-big">
-          <th:block th:each="child : ${model.germplasm.children}">
-            <div th:replace="fragments/row::row(label=${#strings.isEmpty(child.secondParentName) ? ('children of ' + child.firstParentName) : ('children of ' + child.firstParentName + ' x ' + child.secondParentName) }, content=~{::.descendant-child})">
-              <div class="descendant-child">
-                <th:block th:each="sibling, siblingIterStat : ${child.sibblings}">
-                  <a th:href="@{/germplasms(pui=${sibling.pui})}"
-                     th:text="${sibling.name}"></a><th:block th:unless="${siblingIterStat.last}">, </th:block>
-                </th:block>
+    </div>
+  </div>
+
+  <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.population)}">
+    <h2>Population</h2>
+    <div class="f-card-body">
+      <th:block th:each="population : ${model.germplasm.population}">
+
+        <th:block th:if="${population.germplasmRef != null}">
+          <th:block th:unless="${#strings.isEmpty(population.germplasmRef.pui)}">
+            <div th:replace="fragments/row::row(label=${#faidare.collPopTitle(population)}, content=~{::.population-1})">
+              <div class="population-1">
+                <a th:if="${population.germplasmRef.pui != model.germplasm.germplasmPUI}"
+                   th:href="@{/germplasms(pui=${population.germplasmRef.pui})}"
+                   th:text="${population.germplasmRef.name}"></a>
+                <span th:if="${population.germplasmRef.pui == model.germplasm.germplasmPUI}"
+                      th:text="${population.germplasmRef.name}"></span>
+                is composed by <span th:text="${population.germplasmCount}"></span> accession(s)
+                <!-- TODO there was a link pointing at a search here -->
               </div>
             </div>
           </th:block>
-        </div>
-      </div>
-    </th:block>
-
-  </th:block>
+        </th:block>
 
-  <th:block th:unless="${#lists.isEmpty(model.germplasm.population)}">
-    <h2>Population</h2>
-    <th:block th:each="population : ${model.germplasm.population}">
-
-      <th:block th:if="${population.germplasmRef != null}">
-        <th:block th:unless="${#strings.isEmpty(population.germplasmRef.pui)}">
-          <div th:replace="fragments/row::row(label=${#faidare.collPopTitle(population)}, content=~{::.population-1})">
-            <div class="population-1">
-              <a th:if="${population.germplasmRef.pui != model.germplasm.germplasmPUI}"
-                 th:href="@{/germplasms(pui=${population.germplasmRef.pui})}"
-                 th:text="${population.germplasmRef.name}"></a>
-              <span th:if="${population.germplasmRef.pui == model.germplasm.germplasmPUI}"
-                    th:text="${population.germplasmRef.name}"></span>
-              is composed by <span th:text="${population.germplasmCount}"></span> accession(s)
-              <!-- TODO there was a link pointing at a search here -->
-            </div>
-          </div>
+        <th:block th:if="${population.germplasmRef == null}">
+          <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitle(population)}, text=${population.germplasmCount + ' accession(s)'})"></div>
+          <!-- TODO there was a link pointing at a search here -->
         </th:block>
       </th:block>
+    </div>
+  </div>
 
-      <th:block th:if="${population.germplasmRef == null}">
-        <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitle(population)}, text=${population.germplasmCount + ' accession(s)'})"></div>
+  <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.collection)}">
+    <h2>Collection</h2>
+    <div class="f-card-body">
+      <th:block th:each="collection : ${model.germplasm.collection}">
+        <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitle(collection)}, text=${collection.germplasmCount + ' accession(s)'})"></div>
         <!-- TODO there was a link pointing at a search here -->
       </th:block>
-    </th:block>
-  </th:block>
-
-  <th:block th:unless="${#lists.isEmpty(model.germplasm.collection)}">
-    <h2>Collection</h2>
-    <th:block th:each="collection : ${model.germplasm.collection}">
-      <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitle(collection)}, text=${collection.germplasmCount + ' accession(s)'})"></div>
-      <!-- TODO there was a link pointing at a search here -->
-    </th:block>
-  </th:block>
+    </div>
+  </div>
 
-  <th:block th:unless="${#lists.isEmpty(model.germplasm.panel)}">
+  <div class="f-card" th:unless="${#lists.isEmpty(model.germplasm.panel)}">
     <h2>Panel</h2>
-    <th:block th:each="panel : ${model.germplasm.panel}">
-      <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitleWithoutUnderscores(panel)}, text=${panel.germplasmCount + ' accession(s)'})"></div>
-      <!-- TODO there was a link pointing at a search here -->
-    </th:block>
-  </th:block>
+    <div class="f-card-body">
+      <th:block th:each="panel : ${model.germplasm.panel}">
+        <div th:replace="fragments/row::text-row(label=${#faidare.collPopTitleWithoutUnderscores(panel)}, text=${panel.germplasmCount + ' accession(s)'})"></div>
+        <!-- TODO there was a link pointing at a search here -->
+      </th:block>
+    </div>
+  </div>
 
   <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
 </main>
diff --git a/backend/src/main/resources/templates/layout/main.html b/backend/src/main/resources/templates/layout/main.html
index 47bfcf0d..22c36a5e 100644
--- a/backend/src/main/resources/templates/layout/main.html
+++ b/backend/src/main/resources/templates/layout/main.html
@@ -12,18 +12,17 @@
   </head>
 
   <body>
-    <div class="container">
-      <header>
-        Common header
-      </header>
-
+    <nav class="navbar navbar-expand-lg navbar-light bg-light">
+      <div class="container">
+        <span class="navbar-brand py-0">
+          <img th:src="@{/assets/images/logo.png}" style="height: 40px"/>
+        </span>
+      </div>
+    </nav>
+    <div class="container mt-3">
       <div th:replace="${content}">
         <p>Layout content</p>
       </div>
-
-      <footer>
-        common footer
-      </footer>
     </div>
     <script type="text/javascript" th:src="@{/assets/script.js}"></script>
     <script type="text/javascript" th:replace="${script}"></script>
diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html
index d0fa7dd7..69deac41 100644
--- a/backend/src/main/resources/templates/site.html
+++ b/backend/src/main/resources/templates/site.html
@@ -15,48 +15,55 @@
 
   <div th:replace="fragments/map::map"></div>
 
-  <th:block th:if="${model.site.uri != null && !model.site.uri.startsWith('urn:')}">
-    <div th:replace="fragments/row::text-row(label='Permanent unique identifier', text=${model.site.uri})"></div>
-  </th:block>
+  <div class="f-card mt-4">
+    <h2>Details</h2>
+    <div class="f-card-body">
+      <th:block th:if="${model.site.uri != null && !model.site.uri.startsWith('urn:')}">
+        <div th:replace="fragments/row::text-row(label='Permanent unique identifier', text=${model.site.uri})"></div>
+      </th:block>
 
-  <div th:replace="fragments/source::source(source=${model.source}, url=${model.site.url}, entityType='site')"></div>
+      <div th:replace="fragments/source::source(source=${model.source}, url=${model.site.url}, entityType='site')"></div>
 
-  <div th:replace="fragments/row::text-row(label='Abbreviation', text=${model.site.abbreviation})"></div>
-  <div th:replace="fragments/row::text-row(label='Type', text=${model.site.locationType})"></div>
-  <div th:replace="fragments/row::text-row(label='Status', text=${model.siteStatus})"></div>
-  <div th:replace="fragments/row::text-row(label='Institution/Landowner', text=${model.site.instituteName})"></div>
-  <div th:replace="fragments/row::text-row(label='Institution address', text=${model.site.instituteAddress})"></div>
-  <div th:replace="fragments/row::text-row(label='Coordinates precision', text=${model.coordinatesPrecision})"></div>
-  <th:block th:if="${model.site.latitude}">
-    <div th:replace="fragments/row::text-row(label='Latitude', text=${#coordinates.formatLatitude(model.site.latitude)})"></div>
-  </th:block>
-  <th:block th:if="${model.site.longitude}">
-    <div th:replace="fragments/row::text-row(label='Longitude', text=${#coordinates.formatLongitude(model.site.longitude)})"></div>
-  </th:block>
-  <div th:replace="fragments/row::text-row(label='Geographical location', text=${model.geographicalLocation})"></div>
-  <th:block th:if="${model.site.countryName != null && model.geographicalLocation == null}">
-    <div th:replace="fragments/row::text-row(label='Country name', text=${model.site.countryName})"></div>
-  </th:block>
+      <div th:replace="fragments/row::text-row(label='Abbreviation', text=${model.site.abbreviation})"></div>
+      <div th:replace="fragments/row::text-row(label='Type', text=${model.site.locationType})"></div>
+      <div th:replace="fragments/row::text-row(label='Status', text=${model.siteStatus})"></div>
+      <div th:replace="fragments/row::text-row(label='Institution/Landowner', text=${model.site.instituteName})"></div>
+      <div th:replace="fragments/row::text-row(label='Institution address', text=${model.site.instituteAddress})"></div>
+      <div th:replace="fragments/row::text-row(label='Coordinates precision', text=${model.coordinatesPrecision})"></div>
+      <th:block th:if="${model.site.latitude}">
+        <div th:replace="fragments/row::text-row(label='Latitude', text=${#coordinates.formatLatitude(model.site.latitude)})"></div>
+      </th:block>
+      <th:block th:if="${model.site.longitude}">
+        <div th:replace="fragments/row::text-row(label='Longitude', text=${#coordinates.formatLongitude(model.site.longitude)})"></div>
+      </th:block>
+      <div th:replace="fragments/row::text-row(label='Geographical location', text=${model.geographicalLocation})"></div>
+      <th:block th:if="${model.site.countryName != null && model.geographicalLocation == null}">
+        <div th:replace="fragments/row::text-row(label='Country name', text=${model.site.countryName})"></div>
+      </th:block>
 
-  <th:block th:if="${model.site.countryCode != null && model.geographicalLocation == null}">
-    <div th:replace="fragments/row::text-row(label='Country code', text=${model.site.countryName})"></div>
-  </th:block>
+      <th:block th:if="${model.site.countryCode != null && model.geographicalLocation == null}">
+        <div th:replace="fragments/row::text-row(label='Country code', text=${model.site.countryName})"></div>
+      </th:block>
 
-  <div th:replace="fragments/row::text-row(label='Altitude', text=${model.site.altitude})"></div>
-  <div th:replace="fragments/row::text-row(label='Slope', text=${model.slope})"></div>
-  <div th:replace="fragments/row::text-row(label='Exposure', text=${model.exposure})"></div>
-  <div th:replace="fragments/row::text-row(label='Topography', text=${model.topography})"></div>
-  <div th:replace="fragments/row::text-row(label='Environment type', text=${model.environmentType})"></div>
-  <div th:replace="fragments/row::text-row(label='Distance to city', text=${model.distanceToCity})"></div>
-  <div th:replace="fragments/row::text-row(label='Direction from city', text=${model.directionFromCity})"></div>
-  <div th:replace="fragments/row::text-row(label='Comment', text=${model.comment})"></div>
+      <div th:replace="fragments/row::text-row(label='Altitude', text=${model.site.altitude})"></div>
+      <div th:replace="fragments/row::text-row(label='Slope', text=${model.slope})"></div>
+      <div th:replace="fragments/row::text-row(label='Exposure', text=${model.exposure})"></div>
+      <div th:replace="fragments/row::text-row(label='Topography', text=${model.topography})"></div>
+      <div th:replace="fragments/row::text-row(label='Environment type', text=${model.environmentType})"></div>
+      <div th:replace="fragments/row::text-row(label='Distance to city', text=${model.distanceToCity})"></div>
+      <div th:replace="fragments/row::text-row(label='Direction from city', text=${model.directionFromCity})"></div>
+      <div th:replace="fragments/row::text-row(label='Comment', text=${model.comment})"></div>
+    </div>
+  </div>
 
-  <th:block th:unless="${#lists.isEmpty(model.additionalInfoProperties)}">
+  <div class="f-card" th:unless="${#lists.isEmpty(model.additionalInfoProperties)}">
     <h2>Additional info</h2>
-    <th:block th:each="prop : ${model.additionalInfoProperties}">
-      <div th:replace="fragments/row::text-row(label=${prop.key}, text=${prop.value})"></div>
-    </th:block>
-  </th:block>
+    <div class="f-card-body">
+      <th:block th:each="prop : ${model.additionalInfoProperties}">
+        <div th:replace="fragments/row::text-row(label=${prop.key}, text=${prop.value})"></div>
+      </th:block>
+    </div>
+  </div>
 
   <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
 </main>
diff --git a/backend/src/main/resources/templates/study.html b/backend/src/main/resources/templates/study.html
index c5111944..5d26f834 100644
--- a/backend/src/main/resources/templates/study.html
+++ b/backend/src/main/resources/templates/study.html
@@ -15,51 +15,53 @@
 
   <div th:replace="fragments/map::map"></div>
 
-  <h2>Identification</h2>
-
-  <div th:replace="fragments/row::text-row(label='Name', text=${model.study.studyName})"></div>
-  <div th:replace="fragments/row::text-row(label='Identifier', text=${model.study.studyDbId})"></div>
-
-  <div th:replace="fragments/source::source(source=${model.source}, url=${model.study.url}, entityType='study')"></div>
-
-  <div th:replace="fragments/row::text-row(label='Project name', text=${model.study.programName})"></div>
-  <div th:replace="fragments/row::text-row(label='Description', text=${model.study.studyDescription})"></div>
-  <th:block th:if="${model.study.active != null}">
-    <div th:replace="fragments/row::text-row(label='Active', text=${model.study.active ? 'Yes' : 'No'})"></div>
-  </th:block>
-
-  <th:block th:unless="${#lists.isEmpty(model.study.seasons)}">
-    <div th:replace="fragments/row::text-row(label='Seasons', text=${#strings.listJoin(model.study.seasons, ',')})"></div>
-  </th:block>
-  <th:block th:if="${model.study.startDate != null && model.study.endDate != null}">
-    <div th:replace="fragments/row::text-row(label='Date', text=${'From ' + #dates.format(model.study.startDate, 'yyyy-MM-dd') + ' to ' + #dates.format(model.study.endDate, 'yyyy-MM-dd') })"></div>
-  </th:block>
-  <th:block th:if="${model.study.startDate != null && model.study.endDate == null}">
-    <div th:replace="fragments/row::text-row(label='Date', text=${'Started on ' + #dates.format(model.study.startDate, 'yyyy-MM-dd')})"></div>
-  </th:block>
-
-  <th:block th:if="${model.study.locationDbId}">
-    <div th:replace="fragments/row::row(label='Location name', content=~{::#location})">
-      <a id="location" th:href="@{/sites/{siteId}(siteId=${model.study.locationDbId})}" th:text="${model.study.locationName}"></a>
+  <div class="f-card mt-4">
+    <h2>Identification</h2>
+    <div class="f-card-body">
+      <div th:replace="fragments/row::text-row(label='Name', text=${model.study.studyName})"></div>
+      <div th:replace="fragments/row::text-row(label='Identifier', text=${model.study.studyDbId})"></div>
+
+      <div th:replace="fragments/source::source(source=${model.source}, url=${model.study.url}, entityType='study')"></div>
+
+      <div th:replace="fragments/row::text-row(label='Project name', text=${model.study.programName})"></div>
+      <div th:replace="fragments/row::text-row(label='Description', text=${model.study.studyDescription})"></div>
+      <th:block th:if="${model.study.active != null}">
+        <div th:replace="fragments/row::text-row(label='Active', text=${model.study.active ? 'Yes' : 'No'})"></div>
+      </th:block>
+
+      <th:block th:unless="${#lists.isEmpty(model.study.seasons)}">
+        <div th:replace="fragments/row::text-row(label='Seasons', text=${#strings.listJoin(model.study.seasons, ',')})"></div>
+      </th:block>
+      <th:block th:if="${model.study.startDate != null && model.study.endDate != null}">
+        <div th:replace="fragments/row::text-row(label='Date', text=${'From ' + #dates.format(model.study.startDate, 'yyyy-MM-dd') + ' to ' + #dates.format(model.study.endDate, 'yyyy-MM-dd') })"></div>
+      </th:block>
+      <th:block th:if="${model.study.startDate != null && model.study.endDate == null}">
+        <div th:replace="fragments/row::text-row(label='Date', text=${'Started on ' + #dates.format(model.study.startDate, 'yyyy-MM-dd')})"></div>
+      </th:block>
+
+      <th:block th:if="${model.study.locationDbId}">
+        <div th:replace="fragments/row::row(label='Location name', content=~{::#location})">
+          <a id="location" th:href="@{/sites/{siteId}(siteId=${model.study.locationDbId})}" th:text="${model.study.locationName}"></a>
+        </div>
+      </th:block>
+
+      <th:block th:unless="${#lists.isEmpty(model.study.dataLinks)}">
+        <div th:replace="fragments/row::row(label='Data files', content=~{::#data-files})">
+          <ul id="data-files" class="list-unstyled">
+            <li th:each="dataLink : ${model.study.dataLinks}">
+              <a target="_blank" th:href="${dataLink.url}" th:text="${dataLink.name}"></a>
+            </li>
+          </ul>
+        </div>
+      </th:block>
     </div>
-  </th:block>
-
-  <th:block th:unless="${#lists.isEmpty(model.study.dataLinks)}">
-    <div th:replace="fragments/row::row(label='Data files', content=~{::#data-files})">
-      <ul id="data-files" class="list-unstyled">
-        <li th:each="dataLink : ${model.study.dataLinks}">
-          <a target="_blank" th:href="${dataLink.url}" th:text="${dataLink.name}"></a>
-        </li>
-      </ul>
-    </div>
-  </th:block>
+  </div>
 
-  <th:block th:unles="${#lists.isEmpty(model.germplasms)}">
+  <div class="f-card" th:unles="${#lists.isEmpty(model.germplasms)}">
     <h2>Genotype</h2>
-
-    <div class="table-responsive scroll-table table-card-body">
-      <div class="card">
-        <table class="table table-sm table-striped">
+    <div class="f-card-body">
+      <div class="scroll-table-container scroll-table-container-big">
+        <table class="table table-sm table-striped table-sticky table-responsive-sm">
           <thead>
             <tr>
               <th scope="col">Accession number</th>
@@ -79,13 +81,13 @@
         </table>
       </div>
     </div>
-  </th:block>
+  </div>
 
-  <th:block th:unless="${#lists.isEmpty(model.variables)}">
+  <div class="f-card" th:unless="${#lists.isEmpty(model.variables)}">
     <h2>Variables</h2>
-    <div class="table-responsive scroll-table table-card-body">
-      <div class="card">
-        <table class="table table-sm table-striped">
+    <div class="f-card-body">
+      <div class="scroll-table-container">
+        <table class="table table-sm table-striped table-sticky table-responsive-sm">
           <thead>
             <tr>
               <th scope="col">Variable ID</th>
@@ -110,13 +112,13 @@
         </table>
       </div>
     </div>
-  </th:block>
+  </div>
 
-  <th:block th:unless="${#lists.isEmpty(model.trials)}">
+  <div class="f-card" th:unless="${#lists.isEmpty(model.trials)}">
     <h2>Data Set</h2>
-    <div class="table-responsive scroll-big-table table-card-body">
-      <div class="card">
-        <table class="table table-sm table-striped">
+    <div class="f-card-body">
+      <div class="scroll-table-container scroll-table-container-big">
+        <table class="table table-sm table-striped table-sticky table-responsive-sm">
           <thead>
             <tr>
               <th scope="col">Name</th>
@@ -144,13 +146,13 @@
         </table>
       </div>
     </div>
-  </th:block>
+  </div>
 
-  <th:block th:unless="${#lists.isEmpty(model.study.contacts)}">
+  <div class="f-card" th:unless="${#lists.isEmpty(model.study.contacts)}">
     <h2>Contact</h2>
-    <div class="table-responsive scroll-table table-card-body">
-      <div class="card">
-        <table class="table table-sm table-striped">
+    <div class="f-card-body">
+      <div class="scroll-table-container">
+        <table class="table table-sm table-striped table-sticky table-responsive-sm">
           <thead>
             <tr>
               <th scope="col">Role</th>
@@ -170,23 +172,23 @@
         </table>
       </div>
     </div>
-  </th:block>
+  </div>
 
-  <th:block th:unless="${#lists.isEmpty(model.additionalInfoProperties)}">
+  <div class="f-card" th:unless="${#lists.isEmpty(model.additionalInfoProperties)}">
     <h2>Additional information</h2>
-    <div class="table-responsive scroll-table table-card-body">
-      <div class="card">
-        <table class="table table-sm table-striped">
+    <div class="f-card-body">
+      <div class="scroll-table-container">
+        <table class="table table-sm">
           <tbody>
             <tr th:each="row : ${model.additionalInfoProperties}">
-              <td style="width: 50%;" th:text="${row.key}"></td>
+              <th class="label" style="width: 33.33%" th:text="${row.key}" scope="row"></th>
               <td th:text="${row.value}"></td>
             </tr>
           </tbody>
         </table>
       </div>
     </div>
-  </th:block>
+  </div>
 
   <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
 </main>
diff --git a/web/src/style/style.scss b/web/src/style/style.scss
index 4dd56d61..42aa2bb3 100644
--- a/web/src/style/style.scss
+++ b/web/src/style/style.scss
@@ -1,14 +1,53 @@
+$headings-color: #0f6191;
+$border-color: #0f6e9f;
+$link-color: #0f6fa1;
+$link-decoration: none;
+$link-hover-decoration: underline;
+$enable-shadows: true;
+$table-border-color: #dee2e6;
+$table-group-separator-color: $table-border-color;
+
 @import 'custom-bootstrap';
 @import '~leaflet/dist/leaflet.css';
 @import '~leaflet.markercluster/dist/MarkerCluster.css';
 @import '~leaflet.markercluster/dist/MarkerCluster.Default.css';
 
-.label {
-  font-weight: 500;
+a[role=button] {
+  color: $link-color !important;
+}
+
+.f-row {
+  border-top: 1px solid $gray-300;
+  padding: 0.5rem 0;
+  .label {
+    font-weight: 700;
+  }
+}
+
+.f-card {
+  border: 1px solid $border-color;
+  border-radius: 0.25rem;
+  margin: 0.5rem 0;
+  h2 {
+    font-size: $h4-font-size;
+    padding: 0.5rem 1rem;
+    background-image: repeating-linear-gradient(#0f96cd, #0f6191, #0f76a5);
+    color: $white;
+  }
+  .f-card-body {
+    padding: 0.25rem 1rem;
+    .f-row:first-of-type {
+      border-top: 0;
+    }
+  }
 }
 
 .popover {
-  max-width: min(80vw, 600px);
+  max-width: min(80vw, 500px);
+}
+
+.popover-header {
+  font-weight: 700;
 }
 
 #map {
@@ -19,3 +58,34 @@
   height: 1.5rem;
 }
 
+.content-overflow {
+  max-height: 200px;
+  overflow-y: auto;
+  overflow-x: hidden;
+  &.content-overflow-big {
+    max-height: 275px;
+  }
+}
+
+.scroll-table-container {
+  max-height: 200px;
+  overflow-y: auto;
+  padding-top: 0;
+  &.scroll-table-container-big {
+    max-height: 500px;
+  }
+}
+
+.table-sticky {
+  width: 100%;
+  thead th {
+    position: sticky;
+    position: -webkit-sticky;
+    top: 0;
+    background-color: white;
+    border-top-width: 0;
+    th {
+      padding-top: 0;
+    }
+  }
+}
-- 
GitLab


From a313aa36070760af7e25eb98a19fd2a58078ec99 Mon Sep 17 00:00:00 2001
From: jnizet <jb@ninja-squad.com>
Date: Sat, 28 Aug 2021 18:38:34 +0200
Subject: [PATCH 16/16] refactor: simplify webpack build and optimize css

---
 web/build.gradle.kts                         |   3 +-
 web/package.json                             |  12 +-
 web/{webpack.common.js => webpack.config.js} |  33 +-
 web/webpack.dev.js                           |   7 -
 web/webpack.prod.js                          |  23 -
 web/yarn.lock                                | 483 ++++++++++++++++++-
 6 files changed, 509 insertions(+), 52 deletions(-)
 rename web/{webpack.common.js => webpack.config.js} (60%)
 delete mode 100644 web/webpack.dev.js
 delete mode 100644 web/webpack.prod.js

diff --git a/web/build.gradle.kts b/web/build.gradle.kts
index b49ff9de..44076adb 100644
--- a/web/build.gradle.kts
+++ b/web/build.gradle.kts
@@ -27,8 +27,7 @@ tasks {
     val yarnBuildProd by registering(YarnTask::class) {
         args.set(listOf("run", "build:prod"))
         dependsOn(prepare)
-        inputs.file("webpack.common.js")
-        inputs.file("webpack.prod.js")
+        inputs.file("webpack.config.js")
         inputs.file("tsconfig.json")
         inputs.file("package.json")
         inputs.file("yarn.lock")
diff --git a/web/package.json b/web/package.json
index e1348699..4ce0986c 100644
--- a/web/package.json
+++ b/web/package.json
@@ -4,10 +4,10 @@
   "description": "",
   "private": true,
   "scripts": {
-    "build": "webpack --config webpack.dev.js",
-    "build:prod": "webpack --config webpack.prod.js",
-    "watch": "webpack --config webpack.dev.js --watch",
-    "watch:prod": "webpack --config webpack.prod.js --watch",
+    "build": "webpack --mode development",
+    "build:prod": "webpack --mode production",
+    "watch": "webpack --mode development --watch",
+    "watch:prod": "webpack --mode production --watch",
     "test": "echo \"Error: no test specified\" && exit 1"
   },
   "author": "",
@@ -22,6 +22,7 @@
     "@types/leaflet.markercluster": "1.4.5",
     "clean-webpack-plugin": "4.0.0-alpha.0",
     "css-loader": "6.2.0",
+    "css-minimizer-webpack-plugin": "3.0.2",
     "leaflet.markercluster": "1.5.0",
     "mini-css-extract-plugin": "2.2.0",
     "sass": "1.38.1",
@@ -29,7 +30,6 @@
     "ts-loader": "9.2.5",
     "typescript": "4.3.5",
     "webpack": "5.51.1",
-    "webpack-cli": "4.8.0",
-    "webpack-merge": "5.8.0"
+    "webpack-cli": "4.8.0"
   }
 }
diff --git a/web/webpack.common.js b/web/webpack.config.js
similarity index 60%
rename from web/webpack.common.js
rename to web/webpack.config.js
index 83633dec..1c7b8080 100644
--- a/web/webpack.common.js
+++ b/web/webpack.config.js
@@ -1,13 +1,23 @@
 const path = require('path');
 const { CleanWebpackPlugin } = require('clean-webpack-plugin');
 const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
 
-module.exports = {
+module.exports = (env, argv) => ({
+    context: path.resolve(__dirname, '.'),
+    // inline source maps only in development mode
+    devtool: argv.mode === 'production' ? undefined : 'inline-source-map',
     plugins: [
-        // cleans the output directory before each build
-        new CleanWebpackPlugin(),
         // allows extracting the CSS into a CSS file instead of bundling it in a JS file
-        new MiniCssExtractPlugin()
+        new MiniCssExtractPlugin({
+            filename: argv.mode === 'production' ? '[name].[contenthash].css' : '[name].css'
+        }),
+        // cleans the output directory before each build
+        new CleanWebpackPlugin({
+            // and the empty, useless style.js after each build
+            protectWebpackAssets: false,
+            cleanAfterEveryBuildPatterns: ['style*.js']
+        })
     ],
     entry: {
         // a JS bundle is generated for the index.ts entry point. Since the application is really small
@@ -40,7 +50,18 @@ module.exports = {
         extensions: ['.ts', '.js'],
     },
     output: {
+        // the output is stored in build/dist/assets
         path: path.resolve(__dirname, 'build/dist/assets'),
-        filename: '[name].js'
+        filename: argv.mode === 'production' ? '[name].[contenthash].js' : '[name].js'
+    },
+    optimization: {
+        minimizer: [
+            '...',
+            new CssMinimizerPlugin()
+        ]
+    },
+    performance: {
+        maxAssetSize: 300000,
+        maxEntrypointSize: 300000
     }
-};
+});
diff --git a/web/webpack.dev.js b/web/webpack.dev.js
deleted file mode 100644
index ca85da79..00000000
--- a/web/webpack.dev.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const { merge } = require('webpack-merge');
-const common = require('./webpack.common.js');
-
-module.exports = merge(common, {
-    mode: 'development',
-    devtool: 'inline-source-map'
-});
diff --git a/web/webpack.prod.js b/web/webpack.prod.js
deleted file mode 100644
index cf236a59..00000000
--- a/web/webpack.prod.js
+++ /dev/null
@@ -1,23 +0,0 @@
-const { merge } = require('webpack-merge');
-const common = require('./webpack.common.js');
-const MiniCssExtractPlugin = require("mini-css-extract-plugin");
-
-const mergedConfig = merge(common, {
-    mode: 'production',
-    output: {
-        filename: '[name].[contenthash].js'
-    }
-});
-
-mergedConfig.plugins = mergedConfig.plugins.map(plugin => {
-    if (plugin instanceof MiniCssExtractPlugin) {
-        return new MiniCssExtractPlugin({
-            filename: '[name].[contenthash].css'
-        });
-    } else {
-        return plugin
-    }
-});
-
-module.exports = mergedConfig;
-
diff --git a/web/yarn.lock b/web/yarn.lock
index 885dcc69..ba30e63f 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -12,6 +12,11 @@
   resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.3.tgz#8b68da1ebd7fc603999cf6ebee34a4899a14b88e"
   integrity sha512-xDu17cEfh7Kid/d95kB6tZsLOmSWKCZKtprnhVepjsSaCij+lM3mItSJDuuHDMbCWTh8Ejmebwb+KONcCJ0eXQ==
 
+"@trysound/sax@0.1.1":
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.1.1.tgz#3348564048e7a2d7398c935d466c0414ebb6a669"
+  integrity sha512-Z6DoceYb/1xSg5+e+ZlPZ9v0N16ZvZ+wYMraFue4HYrE4ttONKtsvruIRf6t9TBR0YvSOfi1hUU0fJfBLCDYow==
+
 "@types/bootstrap@5.1.2":
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.1.2.tgz#24f08f1957ff5859633f4bf620e921d296e6c3a2"
@@ -268,6 +273,11 @@ ajv@^6.12.5:
     json-schema-traverse "^0.4.1"
     uri-js "^4.2.2"
 
+alphanum-sort@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
+  integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=
+
 ansi-styles@^4.1.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
@@ -305,6 +315,11 @@ binary-extensions@^2.0.0:
   resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
   integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
 
+boolbase@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+  integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
+
 bootstrap@5.1.0:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.1.0.tgz#543ef8f44f4b9af67b0230f19508542fec38ef55"
@@ -325,7 +340,7 @@ braces@^3.0.1, braces@~3.0.2:
   dependencies:
     fill-range "^7.0.1"
 
-browserslist@^4.14.5:
+browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.0, browserslist@^4.16.6:
   version "4.16.8"
   resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.8.tgz#cb868b0b554f137ba6e33de0ecff2eda403c4fb0"
   integrity sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==
@@ -341,7 +356,17 @@ buffer-from@^1.0.0:
   resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
   integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
 
-caniuse-lite@^1.0.30001251:
+caniuse-api@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
+  integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==
+  dependencies:
+    browserslist "^4.0.0"
+    caniuse-lite "^1.0.0"
+    lodash.memoize "^4.1.2"
+    lodash.uniq "^4.5.0"
+
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001251:
   version "1.0.30001252"
   resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001252.tgz#cb16e4e3dafe948fc4a9bb3307aea054b912019a"
   integrity sha512-I56jhWDGMtdILQORdusxBOH+Nl/KgQSdDmpJezYddnAkVOmnoU8zwjTV9xAjMIYxr0iPreEAVylCGcmHCjfaOw==
@@ -402,6 +427,11 @@ color-name@~1.1.4:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
+colord@^2.0.1, colord@^2.6:
+  version "2.7.0"
+  resolved "https://registry.yarnpkg.com/colord/-/colord-2.7.0.tgz#706ea36fe0cd651b585eb142fe64b6480185270e"
+  integrity sha512-pZJBqsHz+pYyw3zpX6ZRXWoCHM1/cvFikY9TV8G3zcejCaKE0lhankoj8iScyrrePA8C7yJ5FStfA9zbcOnw7Q==
+
 colorette@^1.2.1, colorette@^1.2.2, colorette@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af"
@@ -412,7 +442,7 @@ commander@^2.20.0:
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
-commander@^7.0.0:
+commander@^7.0.0, commander@^7.2.0:
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
   integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
@@ -431,6 +461,18 @@ cross-spawn@^7.0.3:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
+css-color-names@^1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/css-color-names/-/css-color-names-1.0.1.tgz#6ff7ee81a823ad46e020fa2fd6ab40a887e2ba67"
+  integrity sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA==
+
+css-declaration-sorter@^6.0.3:
+  version "6.1.1"
+  resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.1.1.tgz#77b32b644ba374bc562c0fc6f4fdaba4dfb0b749"
+  integrity sha512-BZ1aOuif2Sb7tQYY1GeCjG7F++8ggnwUkH5Ictw0mrdpqpEd+zWmcPdstnH2TItlb74FqR0DrVEieon221T/1Q==
+  dependencies:
+    timsort "^0.3.0"
+
 css-loader@6.2.0:
   version "6.2.0"
   resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.2.0.tgz#9663d9443841de957a3cb9bcea2eda65b3377071"
@@ -445,11 +487,105 @@ css-loader@6.2.0:
     postcss-value-parser "^4.1.0"
     semver "^7.3.5"
 
+css-minimizer-webpack-plugin@3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.0.2.tgz#8fadbdf10128cb40227bff275a4bb47412534245"
+  integrity sha512-B3I5e17RwvKPJwsxjjWcdgpU/zqylzK1bPVghcmpFHRL48DXiBgrtqz1BJsn68+t/zzaLp9kYAaEDvQ7GyanFQ==
+  dependencies:
+    cssnano "^5.0.6"
+    jest-worker "^27.0.2"
+    p-limit "^3.0.2"
+    postcss "^8.3.5"
+    schema-utils "^3.0.0"
+    serialize-javascript "^6.0.0"
+    source-map "^0.6.1"
+
+css-select@^4.1.3:
+  version "4.1.3"
+  resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.1.3.tgz#a70440f70317f2669118ad74ff105e65849c7067"
+  integrity sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==
+  dependencies:
+    boolbase "^1.0.0"
+    css-what "^5.0.0"
+    domhandler "^4.2.0"
+    domutils "^2.6.0"
+    nth-check "^2.0.0"
+
+css-tree@^1.1.2, css-tree@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
+  integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
+  dependencies:
+    mdn-data "2.0.14"
+    source-map "^0.6.1"
+
+css-what@^5.0.0:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad"
+  integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==
+
 cssesc@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
   integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
 
+cssnano-preset-default@^5.1.4:
+  version "5.1.4"
+  resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.1.4.tgz#359943bf00c5c8e05489f12dd25f3006f2c1cbd2"
+  integrity sha512-sPpQNDQBI3R/QsYxQvfB4mXeEcWuw0wGtKtmS5eg8wudyStYMgKOQT39G07EbW1LB56AOYrinRS9f0ig4Y3MhQ==
+  dependencies:
+    css-declaration-sorter "^6.0.3"
+    cssnano-utils "^2.0.1"
+    postcss-calc "^8.0.0"
+    postcss-colormin "^5.2.0"
+    postcss-convert-values "^5.0.1"
+    postcss-discard-comments "^5.0.1"
+    postcss-discard-duplicates "^5.0.1"
+    postcss-discard-empty "^5.0.1"
+    postcss-discard-overridden "^5.0.1"
+    postcss-merge-longhand "^5.0.2"
+    postcss-merge-rules "^5.0.2"
+    postcss-minify-font-values "^5.0.1"
+    postcss-minify-gradients "^5.0.2"
+    postcss-minify-params "^5.0.1"
+    postcss-minify-selectors "^5.1.0"
+    postcss-normalize-charset "^5.0.1"
+    postcss-normalize-display-values "^5.0.1"
+    postcss-normalize-positions "^5.0.1"
+    postcss-normalize-repeat-style "^5.0.1"
+    postcss-normalize-string "^5.0.1"
+    postcss-normalize-timing-functions "^5.0.1"
+    postcss-normalize-unicode "^5.0.1"
+    postcss-normalize-url "^5.0.2"
+    postcss-normalize-whitespace "^5.0.1"
+    postcss-ordered-values "^5.0.2"
+    postcss-reduce-initial "^5.0.1"
+    postcss-reduce-transforms "^5.0.1"
+    postcss-svgo "^5.0.2"
+    postcss-unique-selectors "^5.0.1"
+
+cssnano-utils@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-2.0.1.tgz#8660aa2b37ed869d2e2f22918196a9a8b6498ce2"
+  integrity sha512-i8vLRZTnEH9ubIyfdZCAdIdgnHAUeQeByEeQ2I7oTilvP9oHO6RScpeq3GsFUVqeB8uZgOQ9pw8utofNn32hhQ==
+
+cssnano@^5.0.6:
+  version "5.0.8"
+  resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.0.8.tgz#39ad166256980fcc64faa08c9bb18bb5789ecfa9"
+  integrity sha512-Lda7geZU0Yu+RZi2SGpjYuQz4HI4/1Y+BhdD0jL7NXAQ5larCzVn+PUGuZbDMYz904AXXCOgO5L1teSvgu7aFg==
+  dependencies:
+    cssnano-preset-default "^5.1.4"
+    is-resolvable "^1.1.0"
+    lilconfig "^2.0.3"
+    yaml "^1.10.2"
+
+csso@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
+  integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
+  dependencies:
+    css-tree "^1.1.2"
+
 del@^4.1.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4"
@@ -463,6 +599,36 @@ del@^4.1.1:
     pify "^4.0.1"
     rimraf "^2.6.3"
 
+dom-serializer@^1.0.1:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91"
+  integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==
+  dependencies:
+    domelementtype "^2.0.1"
+    domhandler "^4.2.0"
+    entities "^2.0.0"
+
+domelementtype@^2.0.1, domelementtype@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
+  integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
+
+domhandler@^4.2.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.0.tgz#f9768a5f034be60a89a27c2e4d0f74eba0d8b059"
+  integrity sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==
+  dependencies:
+    domelementtype "^2.2.0"
+
+domutils@^2.6.0:
+  version "2.8.0"
+  resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
+  integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
+  dependencies:
+    dom-serializer "^1.0.1"
+    domelementtype "^2.2.0"
+    domhandler "^4.2.0"
+
 electron-to-chromium@^1.3.811:
   version "1.3.818"
   resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.818.tgz#32ed024fa8316e5d469c96eecbea7d2463d80085"
@@ -476,6 +642,11 @@ enhanced-resolve@^5.0.0, enhanced-resolve@^5.8.0:
     graceful-fs "^4.2.4"
     tapable "^2.2.0"
 
+entities@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
+  integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
+
 envinfo@^7.7.3:
   version "7.8.1"
   resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475"
@@ -674,6 +845,11 @@ interpret@^2.2.0:
   resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9"
   integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==
 
+is-absolute-url@^3.0.3:
+  version "3.0.3"
+  resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698"
+  integrity sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==
+
 is-binary-path@~2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
@@ -731,6 +907,11 @@ is-plain-object@^2.0.4:
   dependencies:
     isobject "^3.0.1"
 
+is-resolvable@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
+  integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
+
 is-stream@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
@@ -785,6 +966,11 @@ leaflet@1.7.1:
   resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.7.1.tgz#10d684916edfe1bf41d688a3b97127c0322a2a19"
   integrity sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw==
 
+lilconfig@^2.0.3:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.3.tgz#68f3005e921dafbd2a2afb48379986aa6d2579fd"
+  integrity sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==
+
 loader-runner@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
@@ -797,6 +983,16 @@ locate-path@^5.0.0:
   dependencies:
     p-locate "^4.1.0"
 
+lodash.memoize@^4.1.2:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
+  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
+
+lodash.uniq@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+  integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
+
 lru-cache@^6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
@@ -804,6 +1000,11 @@ lru-cache@^6.0.0:
   dependencies:
     yallist "^4.0.0"
 
+mdn-data@2.0.14:
+  version "2.0.14"
+  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
+  integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
+
 merge-stream@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
@@ -868,6 +1069,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
   resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
   integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
 
+normalize-url@^6.0.1:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
+  integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
+
 npm-run-path@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
@@ -875,6 +1081,13 @@ npm-run-path@^4.0.1:
   dependencies:
     path-key "^3.0.0"
 
+nth-check@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.0.tgz#1bb4f6dac70072fc313e8c9cd1417b5074c0a125"
+  integrity sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==
+  dependencies:
+    boolbase "^1.0.0"
+
 object-assign@^4.0.1:
   version "4.1.1"
   resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -901,7 +1114,7 @@ p-limit@^2.2.0:
   dependencies:
     p-try "^2.0.0"
 
-p-limit@^3.1.0:
+p-limit@^3.0.2, p-limit@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
   integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
@@ -984,6 +1197,106 @@ pkg-dir@^4.2.0:
   dependencies:
     find-up "^4.0.0"
 
+postcss-calc@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.0.0.tgz#a05b87aacd132740a5db09462a3612453e5df90a"
+  integrity sha512-5NglwDrcbiy8XXfPM11F3HeC6hoT9W7GUH/Zi5U/p7u3Irv4rHhdDcIZwG0llHXV4ftsBjpfWMXAnXNl4lnt8g==
+  dependencies:
+    postcss-selector-parser "^6.0.2"
+    postcss-value-parser "^4.0.2"
+
+postcss-colormin@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.2.0.tgz#2b620b88c0ff19683f3349f4cf9e24ebdafb2c88"
+  integrity sha512-+HC6GfWU3upe5/mqmxuqYZ9B2Wl4lcoUUNkoaX59nEWV4EtADCMiBqui111Bu8R8IvaZTmqmxrqOAqjbHIwXPw==
+  dependencies:
+    browserslist "^4.16.6"
+    caniuse-api "^3.0.0"
+    colord "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-convert-values@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.0.1.tgz#4ec19d6016534e30e3102fdf414e753398645232"
+  integrity sha512-C3zR1Do2BkKkCgC0g3sF8TS0koF2G+mN8xxayZx3f10cIRmTaAnpgpRQZjNekTZxM2ciSPoh2IWJm0VZx8NoQg==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-discard-comments@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz#9eae4b747cf760d31f2447c27f0619d5718901fe"
+  integrity sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg==
+
+postcss-discard-duplicates@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz#68f7cc6458fe6bab2e46c9f55ae52869f680e66d"
+  integrity sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA==
+
+postcss-discard-empty@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz#ee136c39e27d5d2ed4da0ee5ed02bc8a9f8bf6d8"
+  integrity sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw==
+
+postcss-discard-overridden@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.0.1.tgz#454b41f707300b98109a75005ca4ab0ff2743ac6"
+  integrity sha512-Y28H7y93L2BpJhrdUR2SR2fnSsT+3TVx1NmVQLbcnZWwIUpJ7mfcTC6Za9M2PG6w8j7UQRfzxqn8jU2VwFxo3Q==
+
+postcss-merge-longhand@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.0.2.tgz#277ada51d9a7958e8ef8cf263103c9384b322a41"
+  integrity sha512-BMlg9AXSI5G9TBT0Lo/H3PfUy63P84rVz3BjCFE9e9Y9RXQZD3+h3YO1kgTNsNJy7bBc1YQp8DmSnwLIW5VPcw==
+  dependencies:
+    css-color-names "^1.0.1"
+    postcss-value-parser "^4.1.0"
+    stylehacks "^5.0.1"
+
+postcss-merge-rules@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.0.2.tgz#d6e4d65018badbdb7dcc789c4f39b941305d410a"
+  integrity sha512-5K+Md7S3GwBewfB4rjDeol6V/RZ8S+v4B66Zk2gChRqLTCC8yjnHQ601omj9TKftS19OPGqZ/XzoqpzNQQLwbg==
+  dependencies:
+    browserslist "^4.16.6"
+    caniuse-api "^3.0.0"
+    cssnano-utils "^2.0.1"
+    postcss-selector-parser "^6.0.5"
+    vendors "^1.0.3"
+
+postcss-minify-font-values@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.0.1.tgz#a90cefbfdaa075bd3dbaa1b33588bb4dc268addf"
+  integrity sha512-7JS4qIsnqaxk+FXY1E8dHBDmraYFWmuL6cgt0T1SWGRO5bzJf8sUoelwa4P88LEWJZweHevAiDKxHlofuvtIoA==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-minify-gradients@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.0.2.tgz#7c175c108f06a5629925d698b3c4cf7bd3864ee5"
+  integrity sha512-7Do9JP+wqSD6Prittitt2zDLrfzP9pqKs2EcLX7HJYxsxCOwrrcLt4x/ctQTsiOw+/8HYotAoqNkrzItL19SdQ==
+  dependencies:
+    colord "^2.6"
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-minify-params@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.0.1.tgz#371153ba164b9d8562842fdcd929c98abd9e5b6c"
+  integrity sha512-4RUC4k2A/Q9mGco1Z8ODc7h+A0z7L7X2ypO1B6V8057eVK6mZ6xwz6QN64nHuHLbqbclkX1wyzRnIrdZehTEHw==
+  dependencies:
+    alphanum-sort "^1.0.2"
+    browserslist "^4.16.0"
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+    uniqs "^2.0.0"
+
+postcss-minify-selectors@^5.1.0:
+  version "5.1.0"
+  resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.1.0.tgz#4385c845d3979ff160291774523ffa54eafd5a54"
+  integrity sha512-NzGBXDa7aPsAcijXZeagnJBKBPMYLaJJzB8CQh6ncvyl2sIndLVWfbcDi0SBjRWk5VqEjXvf8tYwzoKf4Z07og==
+  dependencies:
+    alphanum-sort "^1.0.2"
+    postcss-selector-parser "^6.0.5"
+
 postcss-modules-extract-imports@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d"
@@ -1012,7 +1325,98 @@ postcss-modules-values@^4.0.0:
   dependencies:
     icss-utils "^5.0.0"
 
-postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
+postcss-normalize-charset@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz#121559d1bebc55ac8d24af37f67bd4da9efd91d0"
+  integrity sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg==
+
+postcss-normalize-display-values@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.1.tgz#62650b965981a955dffee83363453db82f6ad1fd"
+  integrity sha512-uupdvWk88kLDXi5HEyI9IaAJTE3/Djbcrqq8YgjvAVuzgVuqIk3SuJWUisT2gaJbZm1H9g5k2w1xXilM3x8DjQ==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-positions@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.0.1.tgz#868f6af1795fdfa86fbbe960dceb47e5f9492fe5"
+  integrity sha512-rvzWAJai5xej9yWqlCb1OWLd9JjW2Ex2BCPzUJrbaXmtKtgfL8dBMOOMTX6TnvQMtjk3ei1Lswcs78qKO1Skrg==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-repeat-style@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.1.tgz#cbc0de1383b57f5bb61ddd6a84653b5e8665b2b5"
+  integrity sha512-syZ2itq0HTQjj4QtXZOeefomckiV5TaUO6ReIEabCh3wgDs4Mr01pkif0MeVwKyU/LHEkPJnpwFKRxqWA/7O3w==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-string@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.0.1.tgz#d9eafaa4df78c7a3b973ae346ef0e47c554985b0"
+  integrity sha512-Ic8GaQ3jPMVl1OEn2U//2pm93AXUcF3wz+OriskdZ1AOuYV25OdgS7w9Xu2LO5cGyhHCgn8dMXh9bO7vi3i9pA==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-timing-functions@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.1.tgz#8ee41103b9130429c6cbba736932b75c5e2cb08c"
+  integrity sha512-cPcBdVN5OsWCNEo5hiXfLUnXfTGtSFiBU9SK8k7ii8UD7OLuznzgNRYkLZow11BkQiiqMcgPyh4ZqXEEUrtQ1Q==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-unicode@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.1.tgz#82d672d648a411814aa5bf3ae565379ccd9f5e37"
+  integrity sha512-kAtYD6V3pK0beqrU90gpCQB7g6AOfP/2KIPCVBKJM2EheVsBQmx/Iof+9zR9NFKLAx4Pr9mDhogB27pmn354nA==
+  dependencies:
+    browserslist "^4.16.0"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-url@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.0.2.tgz#ddcdfb7cede1270740cf3e4dfc6008bd96abc763"
+  integrity sha512-k4jLTPUxREQ5bpajFQZpx8bCF2UrlqOTzP9kEqcEnOfwsRshWs2+oAFIHfDQB8GO2PaUaSE0NlTAYtbluZTlHQ==
+  dependencies:
+    is-absolute-url "^3.0.3"
+    normalize-url "^6.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-normalize-whitespace@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.1.tgz#b0b40b5bcac83585ff07ead2daf2dcfbeeef8e9a"
+  integrity sha512-iPklmI5SBnRvwceb/XH568yyzK0qRVuAG+a1HFUsFRf11lEJTiQQa03a4RSCQvLKdcpX7XsI1Gen9LuLoqwiqA==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+
+postcss-ordered-values@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.0.2.tgz#1f351426977be00e0f765b3164ad753dac8ed044"
+  integrity sha512-8AFYDSOYWebJYLyJi3fyjl6CqMEG/UVworjiyK1r573I56kb3e879sCJLGvR3merj+fAdPpVplXKQZv+ey6CgQ==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-reduce-initial@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.0.1.tgz#9d6369865b0f6f6f6b165a0ef5dc1a4856c7e946"
+  integrity sha512-zlCZPKLLTMAqA3ZWH57HlbCjkD55LX9dsRyxlls+wfuRfqCi5mSlZVan0heX5cHr154Dq9AfbH70LyhrSAezJw==
+  dependencies:
+    browserslist "^4.16.0"
+    caniuse-api "^3.0.0"
+
+postcss-reduce-transforms@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.1.tgz#93c12f6a159474aa711d5269923e2383cedcf640"
+  integrity sha512-a//FjoPeFkRuAguPscTVmRQUODP+f3ke2HqFNgGPwdYnpeC29RZdCBvGRGTsKpMURb/I3p6jdKoBQ2zI+9Q7kA==
+  dependencies:
+    cssnano-utils "^2.0.1"
+    postcss-value-parser "^4.1.0"
+
+postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5:
   version "6.0.6"
   resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz#2c5bba8174ac2f6981ab631a42ab0ee54af332ea"
   integrity sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==
@@ -1020,12 +1424,29 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4:
     cssesc "^3.0.0"
     util-deprecate "^1.0.2"
 
-postcss-value-parser@^4.1.0:
+postcss-svgo@^5.0.2:
+  version "5.0.2"
+  resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.0.2.tgz#bc73c4ea4c5a80fbd4b45e29042c34ceffb9257f"
+  integrity sha512-YzQuFLZu3U3aheizD+B1joQ94vzPfE6BNUcSYuceNxlVnKKsOtdo6hL9/zyC168Q8EwfLSgaDSalsUGa9f2C0A==
+  dependencies:
+    postcss-value-parser "^4.1.0"
+    svgo "^2.3.0"
+
+postcss-unique-selectors@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.0.1.tgz#3be5c1d7363352eff838bd62b0b07a0abad43bfc"
+  integrity sha512-gwi1NhHV4FMmPn+qwBNuot1sG1t2OmacLQ/AX29lzyggnjd+MnVD5uqQmpXO3J17KGL2WAxQruj1qTd3H0gG/w==
+  dependencies:
+    alphanum-sort "^1.0.2"
+    postcss-selector-parser "^6.0.5"
+    uniqs "^2.0.0"
+
+postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb"
   integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==
 
-postcss@^8.2.15:
+postcss@^8.2.15, postcss@^8.3.5:
   version "8.3.6"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.6.tgz#2730dd76a97969f37f53b9a6096197be311cc4ea"
   integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==
@@ -1177,11 +1598,24 @@ source-map@~0.7.2:
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
   integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
 
+stable@^0.1.8:
+  version "0.1.8"
+  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
+  integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+
 strip-final-newline@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
   integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
 
+stylehacks@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.0.1.tgz#323ec554198520986806388c7fdaebc38d2c06fb"
+  integrity sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA==
+  dependencies:
+    browserslist "^4.16.0"
+    postcss-selector-parser "^6.0.4"
+
 supports-color@^7.1.0:
   version "7.2.0"
   resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
@@ -1196,6 +1630,19 @@ supports-color@^8.0.0:
   dependencies:
     has-flag "^4.0.0"
 
+svgo@^2.3.0:
+  version "2.5.0"
+  resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.5.0.tgz#3c9051b606d85a02fcb59f459b19970d2cc2c9bf"
+  integrity sha512-FSdBOOo271VyF/qZnOn1PgwCdt1v4Dx0Sey+U1jgqm1vqRYjPGdip0RGrFW6ItwtkBB8rHgHk26dlVr0uCs82Q==
+  dependencies:
+    "@trysound/sax" "0.1.1"
+    colorette "^1.3.0"
+    commander "^7.2.0"
+    css-select "^4.1.3"
+    css-tree "^1.1.3"
+    csso "^4.2.0"
+    stable "^0.1.8"
+
 tapable@^2.1.1, tapable@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b"
@@ -1222,6 +1669,11 @@ terser@^5.7.0:
     source-map "~0.7.2"
     source-map-support "~0.5.19"
 
+timsort@^0.3.0:
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
+  integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
+
 to-regex-range@^5.0.1:
   version "5.0.1"
   resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -1244,6 +1696,11 @@ typescript@4.3.5:
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
   integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
 
+uniqs@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
+  integrity sha1-/+3ks2slKQaW5uFl1KWe25mOawI=
+
 uri-js@^4.2.2:
   version "4.4.1"
   resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
@@ -1261,6 +1718,11 @@ v8-compile-cache@^2.2.0:
   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
   integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
 
+vendors@^1.0.3:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
+  integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
+
 watchpack@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.2.0.tgz#47d78f5415fe550ecd740f99fe2882323a58b1ce"
@@ -1288,7 +1750,7 @@ webpack-cli@4.8.0:
     v8-compile-cache "^2.2.0"
     webpack-merge "^5.7.3"
 
-webpack-merge@5.8.0, webpack-merge@^5.7.3:
+webpack-merge@^5.7.3:
   version "5.8.0"
   resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61"
   integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==
@@ -1353,6 +1815,11 @@ yallist@^4.0.0:
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
   integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
 
+yaml@^1.10.2:
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
+  integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
+
 yocto-queue@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
-- 
GitLab