From 6a52ca0d91f86966541a990012cc21cc748d8a6d Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Fri, 11 Dec 2020 15:27:04 +0100
Subject: [PATCH 1/3] Produce and use webp pictures with different sizes

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 js/package.json                               |   5 +-
 ...lustration-E_realisation.jpg => error.jpg} | Bin
 ...ation-evenement.jpg => event_creation.jpg} | Bin
 ...on-illustration-C_groupe.jpg => group.jpg} | Bin
 ...lustration-A_homepage.jpg => homepage.jpg} | Bin
 ...tion-D_realisation.jpg => realisation.jpg} | Bin
 .../pics/{2020-10-06_Rose.jpg => rose.jpg}    | Bin
 js/scripts/build/pictures.sh                  |  90 ++++++++++++++++++
 js/src/components/Footer.vue                  |  18 +++-
 js/src/utils/support.ts                       |  10 ++
 js/src/views/Event/MyEvents.vue               |  21 +++-
 js/src/views/Group/MyGroups.vue               |  22 ++++-
 js/src/views/Home.vue                         |  72 ++++++++++++--
 js/src/views/PageNotFound.vue                 |  22 ++++-
 js/yarn.lock                                  |  24 ++++-
 15 files changed, 267 insertions(+), 17 deletions(-)
 rename js/public/img/pics/{2020-10-06-mobilizon-illustration-E_realisation.jpg => error.jpg} (100%)
 rename js/public/img/pics/{2020-10-06-mobilizon-illustration-B_creation-evenement.jpg => event_creation.jpg} (100%)
 rename js/public/img/pics/{2020-10-06-mobilizon-illustration-C_groupe.jpg => group.jpg} (100%)
 rename js/public/img/pics/{2020-10-06-mobilizon-illustration-A_homepage.jpg => homepage.jpg} (100%)
 rename js/public/img/pics/{2020-10-06-mobilizon-illustration-D_realisation.jpg => realisation.jpg} (100%)
 rename js/public/img/pics/{2020-10-06_Rose.jpg => rose.jpg} (100%)
 create mode 100755 js/scripts/build/pictures.sh
 create mode 100644 js/src/utils/support.ts

diff --git a/js/package.json b/js/package.json
index 7f2bb5663..8598a0d92 100644
--- a/js/package.json
+++ b/js/package.json
@@ -4,7 +4,9 @@
   "private": true,
   "scripts": {
     "serve": "vue-cli-service serve",
-    "build": "vue-cli-service build --modern",
+    "build:assets": "vue-cli-service build --modern",
+    "build:pictures": "scripty",
+    "build": "yarn run build:assets && yarn run build:pictures",
     "test:unit": "LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 vue-cli-service test:unit",
     "test:e2e": "vue-cli-service test:e2e",
     "lint": "vue-cli-service lint"
@@ -87,6 +89,7 @@
     "prettier-eslint": "^12.0.0",
     "sass": "^1.29.0",
     "sass-loader": "^10.0.1",
+    "scripty": "^2.0.0",
     "typescript": "~4.1.2",
     "vue-cli-plugin-svg": "~0.1.3",
     "vue-i18n-extract": "^1.0.2",
diff --git a/js/public/img/pics/2020-10-06-mobilizon-illustration-E_realisation.jpg b/js/public/img/pics/error.jpg
similarity index 100%
rename from js/public/img/pics/2020-10-06-mobilizon-illustration-E_realisation.jpg
rename to js/public/img/pics/error.jpg
diff --git a/js/public/img/pics/2020-10-06-mobilizon-illustration-B_creation-evenement.jpg b/js/public/img/pics/event_creation.jpg
similarity index 100%
rename from js/public/img/pics/2020-10-06-mobilizon-illustration-B_creation-evenement.jpg
rename to js/public/img/pics/event_creation.jpg
diff --git a/js/public/img/pics/2020-10-06-mobilizon-illustration-C_groupe.jpg b/js/public/img/pics/group.jpg
similarity index 100%
rename from js/public/img/pics/2020-10-06-mobilizon-illustration-C_groupe.jpg
rename to js/public/img/pics/group.jpg
diff --git a/js/public/img/pics/2020-10-06-mobilizon-illustration-A_homepage.jpg b/js/public/img/pics/homepage.jpg
similarity index 100%
rename from js/public/img/pics/2020-10-06-mobilizon-illustration-A_homepage.jpg
rename to js/public/img/pics/homepage.jpg
diff --git a/js/public/img/pics/2020-10-06-mobilizon-illustration-D_realisation.jpg b/js/public/img/pics/realisation.jpg
similarity index 100%
rename from js/public/img/pics/2020-10-06-mobilizon-illustration-D_realisation.jpg
rename to js/public/img/pics/realisation.jpg
diff --git a/js/public/img/pics/2020-10-06_Rose.jpg b/js/public/img/pics/rose.jpg
similarity index 100%
rename from js/public/img/pics/2020-10-06_Rose.jpg
rename to js/public/img/pics/rose.jpg
diff --git a/js/scripts/build/pictures.sh b/js/scripts/build/pictures.sh
new file mode 100755
index 000000000..d19fec20a
--- /dev/null
+++ b/js/scripts/build/pictures.sh
@@ -0,0 +1,90 @@
+#!/bin/bash
+
+set -eu
+
+output_dir="../priv/static/img/pics"
+resolutions=(
+    480
+    1024
+    1920
+)
+ignore=(
+    homepage_background.png
+)
+
+file_extension () {
+    filename=$(basename -- "$file")
+    echo "${filename##*.}"
+}
+
+file_name () {
+    filename=$(basename -- "$file")
+    echo "${filename%.*}"
+}
+
+convert_image () {
+    name=$(file_name)
+    extension=$(file_extension)
+    res="$1w"
+    output="$output_dir/$name-$res.$extension"
+    convert -geometry "$resolution"x $file $output
+}
+
+produce_webp () {
+    name=$(file_name)
+    output="$output_dir/$name.webp"
+    cwebp $file -quiet -o $output
+}
+
+progress() {
+    local w=80 p=$1;  shift
+    # create a string of spaces, then change them to dots
+    printf -v dots "%*s" "$(( $p*$w/100 ))" ""; dots=${dots// /.};
+    # print those dots on a fixed-width space plus the percentage etc. 
+    printf "\r\e[K|%-*s| %3d %% %s" "$w" "$dots" "$p" "$*"; 
+}
+
+
+echo "Generating responsive versions of the pictures…"
+
+if ! command -v convert &> /dev/null
+then
+    echo "$(tput setaf 1)ERROR: The convert command could not be found. You need to install ImageMagick.$(tput sgr 0)"
+    exit 1
+fi
+
+nb_files=$( shopt -s nullglob ; set -- $output_dir/* ; echo $#)
+
+tasks=$((${#resolutions[@]}*$nb_files))
+i=1
+for file in $output_dir/*
+do
+    if [[ -f $file ]]; then
+        for resolution in "${resolutions[@]}"; do
+            convert_image $resolution
+            progress $(($i*100/$tasks)) still working...
+            i=$((i+1))
+        done
+    fi
+done
+echo -e "\nDone!"
+
+echo "Generating optimized versions of the pictures…"
+
+if ! command -v cwebp &> /dev/null
+then
+    echo "$(tput setaf 1)ERROR: The cwebp command could not be found. You need to install webp.$(tput sgr 0)"
+    exit 1
+fi
+
+nb_files=$( shopt -s nullglob ; set -- $output_dir/* ; echo $#)
+i=1
+for file in $output_dir/*
+do
+    if [[ -f $file ]]; then
+        produce_webp
+        progress $(($i*100/$nb_files)) still working...
+        i=$((i+1))
+    fi
+done
+echo -e "\nDone!"
\ No newline at end of file
diff --git a/js/src/components/Footer.vue b/js/src/components/Footer.vue
index 50d4eaf7f..b82328f1f 100644
--- a/js/src/components/Footer.vue
+++ b/js/src/components/Footer.vue
@@ -1,6 +1,22 @@
 <template>
   <footer class="footer" ref="footer">
-    <img :src="`/img/pics/footer_${random}.jpg`" alt="" />
+    <picture>
+      <source
+        :srcset="`/img/pics/footer_${random}-1024w.webp 1x, /img/pics/footer_${random}-1920w.webp 2x`"
+        type="image/webp"
+      />
+      <source
+        :srcset="`/img/pics/footer_${random}-1024w.jpg 1x, /img/pics/footer_${random}-1920w.jpg 2x`"
+        type="image/jpeg"
+      />
+      <img
+        :src="`/img/pics/footer_${random}-1024w.jpg`"
+        alt=""
+        width="5234"
+        height="2189"
+        loading="lazy"
+      />
+    </picture>
     <ul>
       <li>
         <b-select
diff --git a/js/src/utils/support.ts b/js/src/utils/support.ts
new file mode 100644
index 000000000..b49ef7d6c
--- /dev/null
+++ b/js/src/utils/support.ts
@@ -0,0 +1,10 @@
+export function supportsWebPFormat(): boolean {
+  const elem = document.createElement("canvas");
+
+  if (elem.getContext && elem.getContext("2d")) {
+    // was able or not to get WebP representation
+    return elem.toDataURL("image/webp").indexOf("data:image/webp") === 0;
+  }
+  // very old browser like IE 8, canvas not supported
+  return false;
+}
diff --git a/js/src/views/Event/MyEvents.vue b/js/src/views/Event/MyEvents.vue
index 32083c01b..2d48a6a53 100644
--- a/js/src/views/Event/MyEvents.vue
+++ b/js/src/views/Event/MyEvents.vue
@@ -101,7 +101,7 @@
     >
       <div class="columns is-vertical is-centered">
         <div class="column is-three-quarters">
-          <div class="img-container" />
+          <div class="img-container" :class="{ webp: supportsWebPFormat }" />
           <div class="content has-text-centered">
             <p>
               {{ $t("You didn't create or join any event yet.") }}
@@ -129,6 +129,7 @@
 import { Component, Vue } from "vue-property-decorator";
 import { ParticipantRole } from "@/types/enums";
 import RouteName from "@/router/name";
+import { supportsWebPFormat } from "@/utils/support";
 import { IParticipant, Participant } from "../../types/participant.model";
 import {
   LOGGED_USER_PARTICIPATIONS,
@@ -211,6 +212,8 @@ export default class MyEvents extends Vue {
 
   RouteName = RouteName;
 
+  supportsWebPFormat = supportsWebPFormat;
+
   static monthlyParticipations(
     participations: IParticipant[],
     revertSort = false
@@ -355,7 +358,21 @@ section {
 
 .not-found {
   .img-container {
-    background-image: url("/img/pics/2020-10-06-mobilizon-illustration-B_creation-evenement.jpg");
+    background-image: url("/img/pics/event_creation-480w.jpg");
+    @media (min-resolution: 2dppx) {
+      & {
+        background-image: url("/img/pics/event_creation-1024w.jpg");
+      }
+    }
+
+    &.webp {
+      background-image: url("/img/pics/event_creation-480w.webp");
+      @media (min-resolution: 2dppx) {
+        & {
+          background-image: url("/img/pics/event_creation-1024w.webp");
+        }
+      }
+    }
     max-width: 450px;
     height: 300px;
     box-shadow: 0 0 8px 8px white inset;
diff --git a/js/src/views/Group/MyGroups.vue b/js/src/views/Group/MyGroups.vue
index 43473ddaa..9ee2cf5a0 100644
--- a/js/src/views/Group/MyGroups.vue
+++ b/js/src/views/Group/MyGroups.vue
@@ -46,7 +46,7 @@
     >
       <div class="columns is-vertical is-centered">
         <div class="column is-three-quarters">
-          <div class="img-container" />
+          <div class="img-container" :class="{ webp: supportsWebPFormat }" />
           <div class="content has-text-centered">
             <p>
               {{ $t("You are not part of any group.") }}
@@ -81,6 +81,7 @@ import { IGroup, usernameWithDomain } from "@/types/actor";
 import { Route } from "vue-router";
 import { IMember } from "@/types/actor/member.model";
 import { MemberRole } from "@/types/enums";
+import { supportsWebPFormat } from "@/utils/support";
 import RouteName from "../../router/name";
 
 @Component({
@@ -119,6 +120,8 @@ export default class MyGroups extends Vue {
 
   limit = 10;
 
+  supportsWebPFormat = supportsWebPFormat;
+
   acceptInvitation(member: IMember): Promise<Route> {
     return this.$router.push({
       name: RouteName.GROUP,
@@ -199,7 +202,22 @@ section {
 
 .not-found {
   .img-container {
-    background-image: url("/img/pics/2020-10-06-mobilizon-illustration-C_groupe.jpg");
+    background-image: url("/img/pics/group-480w.jpg");
+
+    @media (min-resolution: 2dppx) {
+      & {
+        background-image: url("/img/pics/group-1024w.jpg");
+      }
+    }
+    &.webp {
+      background-image: url("/img/pics/group-480w.webp");
+      @media (min-resolution: 2dppx) {
+        & {
+          background-image: url("/img/pics/group-1024w.webp");
+        }
+      }
+    }
+
     max-width: 450px;
     height: 300px;
     box-shadow: 0 0 8px 8px white inset;
diff --git a/js/src/views/Home.vue b/js/src/views/Home.vue
index 41c975c25..993501e50 100644
--- a/js/src/views/Home.vue
+++ b/js/src/views/Home.vue
@@ -2,6 +2,7 @@
   <div id="homepage">
     <section
       class="hero"
+      :class="{ webp: supportsWebPFormat }"
       v-if="config && (!currentUser.id || !currentActor.id)"
     >
       <div class="hero-body">
@@ -72,10 +73,59 @@
     </div>
     <div id="picture" v-if="config && (!currentUser.id || !currentActor.id)">
       <div class="picture-container">
-        <img
-          src="/img/pics/2020-10-06-mobilizon-illustration-A_homepage.jpg"
-          alt=""
-        />
+        <picture>
+          <source
+            media="(max-width: 799px)"
+            srcset="/img/pics/homepage-480w.webp"
+            type="image/webp"
+          />
+          <source
+            media="(max-width: 799px)"
+            srcset="/img/pics/homepage-480w.jpg"
+            type="image/jpeg"
+          />
+
+          <source
+            media="(max-width: 1024px)"
+            srcset="/img/pics/homepage-1024w.webp"
+            type="image/webp"
+          />
+          <source
+            media="(max-width: 1024px)"
+            srcset="/img/pics/homepage-1024w.jpg"
+            type="image/jpeg"
+          />
+
+          <source
+            media="(max-width: 1920px)"
+            srcset="/img/pics/homepage-1920w.webp"
+            type="image/webp"
+          />
+          <source
+            media="(max-width: 1920px)"
+            srcset="/img/pics/homepage-1920w.jpg"
+            type="image/jpeg"
+          />
+
+          <source
+            media="(min-width: 1921px)"
+            srcset="/img/pics/homepage.webp"
+            type="image/webp"
+          />
+          <source
+            media="(min-width: 1921px)"
+            srcset="/img/pics/homepage.jpg"
+            type="image/jpeg"
+          />
+
+          <img
+            src="/img/pics/homepage-1024w.jpg"
+            width="3840"
+            height="2719"
+            alt=""
+            loading="lazy"
+          />
+        </picture>
       </div>
       <div class="container section">
         <div class="columns">
@@ -221,6 +271,7 @@
 import { Component, Vue, Watch } from "vue-property-decorator";
 import { ParticipantRole } from "@/types/enums";
 import { Paginate } from "@/types/paginate";
+import { supportsWebPFormat } from "@/utils/support";
 import { IParticipant, Participant } from "../types/participant.model";
 import { FETCH_EVENTS } from "../graphql/event";
 import EventListCard from "../components/Event/EventListCard.vue";
@@ -296,7 +347,10 @@ import Subtitle from "../components/Utils/Subtitle.vue";
   },
 })
 export default class Home extends Vue {
-  events!: Paginate<IEvent>;
+  events: Paginate<IEvent> = {
+    elements: [],
+    total: 0,
+  };
 
   locations = [];
 
@@ -316,6 +370,8 @@ export default class Home extends Vue {
 
   currentUserParticipations: IParticipant[] = [];
 
+  supportsWebPFormat = supportsWebPFormat;
+
   // get displayed_name() {
   //   return this.loggedPerson && this.loggedPerson.name === null
   //     ? this.loggedPerson.preferredUsername
@@ -531,7 +587,11 @@ section.hero {
     height: 100%;
     opacity: 0.3;
     z-index: -1;
-    background: url("/img/pics/homepage_background.png");
+    background: url("/img/pics/homepage_background-1024w.png");
+    background-size: cover;
+  }
+  &.webp::before {
+    background-image: url("/img/pics/homepage_background-1024w.webp");
   }
 
   & > .hero-body {
diff --git a/js/src/views/PageNotFound.vue b/js/src/views/PageNotFound.vue
index c5d70c07e..afce22e21 100644
--- a/js/src/views/PageNotFound.vue
+++ b/js/src/views/PageNotFound.vue
@@ -2,10 +2,24 @@
   <section class="section container has-text-centered not-found">
     <div class="columns is-vertical is-centered">
       <div class="column is-half">
-        <img
-          src="/img/pics/2020-10-06-mobilizon-illustration-E_realisation.jpg"
-          alt=""
-        />
+        <picture>
+          <source
+            srcset="/img/pics/error-480w.webp 1x, /img/pics/error-1024w.webp 2x"
+            type="image/webp"
+          />
+          <source
+            srcset="/img/pics/error-480w.jpg 1x, /img/pics/error-1024w.jpg 2x"
+            type="image/jpeg"
+          />
+
+          <img
+            :src="`/img/pics/error-480w.jpg`"
+            alt=""
+            width="2616"
+            height="1698"
+            loading="lazy"
+          />
+        </picture>
         <h1 class="title">
           {{ $t("The page you're looking for doesn't exist.") }}
         </h1>
diff --git a/js/yarn.lock b/js/yarn.lock
index 2ad9ab1c6..ff83d75cf 100644
--- a/js/yarn.lock
+++ b/js/yarn.lock
@@ -2701,7 +2701,7 @@ async@2.6.1:
   dependencies:
     lodash "^4.17.10"
 
-async@^2.6.2:
+async@^2.6.1, async@^2.6.2:
   version "2.6.3"
   resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
   integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
@@ -10489,6 +10489,11 @@ resolve-dir@^1.0.0, resolve-dir@^1.0.1:
     expand-tilde "^2.0.0"
     global-modules "^1.0.0"
 
+resolve-from@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57"
+  integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=
+
 resolve-from@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
@@ -10499,6 +10504,13 @@ resolve-from@^4.0.0:
   resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
   integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
 
+resolve-pkg@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/resolve-pkg/-/resolve-pkg-1.0.0.tgz#e19a15e78aca2e124461dc92b2e3943ef93494d9"
+  integrity sha1-4ZoV54rKLhJEYdySsuOUPvk0lNk=
+  dependencies:
+    resolve-from "^2.0.0"
+
 resolve-url@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@@ -10739,6 +10751,16 @@ schema-utils@^3.0.0:
     ajv "^6.12.5"
     ajv-keywords "^3.5.2"
 
+scripty@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/scripty/-/scripty-2.0.0.tgz#25761bb2e237a7563f705d87357db07791d38459"
+  integrity sha512-vbd4FPeuNwYNGtRtYa1wDZLPCx5PpW6VrldCEiBGqPz7Je1xZOgNvVPD2axymvqNghBIRiXxAU+JwYrOzvuLJg==
+  dependencies:
+    async "^2.6.1"
+    glob "^7.0.3"
+    lodash "^4.17.11"
+    resolve-pkg "^1.0.0"
+
 select-hose@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"

From 71f1701ce83b00be56c2d9c508ef87e98bb2739d Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Fri, 11 Dec 2020 15:27:31 +0100
Subject: [PATCH 2/3] Add back pwa support

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 js/src/main.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/js/src/main.ts b/js/src/main.ts
index 5fcc4addb..125100ad7 100644
--- a/js/src/main.ts
+++ b/js/src/main.ts
@@ -12,6 +12,7 @@ import { NotifierPlugin } from "./plugins/notifier";
 import filters from "./filters";
 import { i18n } from "./utils/i18n";
 import apolloProvider from "./vue-apollo";
+import "./registerServiceWorker";
 
 Vue.config.productionTip = false;
 

From c43aeb8a3e686ffafa66a23f1f4c9e2eb7e561bc Mon Sep 17 00:00:00 2001
From: Thomas Citharel <tcit@tcit.fr>
Date: Fri, 11 Dec 2020 15:44:48 +0100
Subject: [PATCH 3/3] Update CI

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
---
 .gitlab-ci.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ba1304b14..5350a2cfa 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -41,7 +41,7 @@ lint:
     - yarn install
     #- yarn run lint || export EXITVALUE=1
     - yarn run prettier -c . || export EXITVALUE=1
-    - yarn run build
+    - yarn run build:assets
     - cd ../
     - exit $EXITVALUE
   artifacts:
@@ -69,7 +69,7 @@ exunit:
   before_script:
     - cd js
     - yarn install
-    - yarn run build
+    - yarn run build:assets
     - cd ../
     - mix deps.get
     - MIX_ENV=test mix ecto.create