diff --git a/js/src/assets/mobilizon_logo.svg b/js/src/assets/mobilizon_logo.svg
deleted file mode 100644
index 97ef70b74..000000000
--- a/js/src/assets/mobilizon_logo.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 248.16 46.78">
-    <title>Mobilizon Logo</title>
-    <g data-name="header">
-        <path d="M0 45.82l3.18-40.8a29.88 29.88 0 015.07-.36 27.74 27.74 0 014.95.36l4.86 17.16a92.19 92.19 0 012.34 10.08h.36a92.19 92.19 0 012.34-10.08L28 5.02a29.23 29.23 0 015-.36 29.23 29.23 0 015 .36l3.18 40.8a13.61 13.61 0 01-3.63.42 23.41 23.41 0 01-3.63-.24l-1.2-19.92q-.36-5.52-.48-12.84h-.44l-7.32 26.51a25.62 25.62 0 01-4 .3 23.36 23.36 0 01-3.84-.3L9.36 13.24H9q-.3 8.94-.48 12.84L7.26 46a22.47 22.47 0 01-3.6.24A13.75 13.75 0 010 45.82zM74 31.06q0 8-4.26 12.3a12.21 12.21 0 01-9 3.42 12.21 12.21 0 01-9-3.42q-4.26-4.26-4.26-12.3t4.24-12.31a12.21 12.21 0 019-3.42 12.21 12.21 0 019 3.42Q74 23.02 74 31.06zM60.75 20.98q-5.67 0-5.67 10.08t5.67 10.08q5.67 0 5.67-10.08t-5.67-10.08zM103.2 19.75q2.7 4.11 2.7 11.28T102 42.31a13.18 13.18 0 01-10 4.11 31.41 31.41 0 01-11.34-2V2.2l.4-.45h2.76A4 4 0 0187 2.83a5.38 5.38 0 01.93 3.57v11.94a12.08 12.08 0 017.56-2.7 8.71 8.71 0 017.71 4.11zm-9.72 2a7.28 7.28 0 00-5.58 2.82v16a15 15 0 004.08.54 5.25 5.25 0 004.68-2.67q1.68-2.67 1.68-7.59 0-9.03-4.86-9.1zM121 22v23.94a20.85 20.85 0 01-3.66.3 23 23 0 01-3.78-.3V24.75q0-3.24-2.7-3.24h-.72a9.32 9.32 0 01-.3-2.58 10.7 10.7 0 01.3-2.7 39.63 39.63 0 014.38-.24h1a5.19 5.19 0 014 1.62A6.27 6.27 0 01121 22z" />
-        <path d="M119.82.84a7.37 7.37 0 01.6 3 7.37 7.37 0 01-.6 3 7.46 7.46 0 01-3.87.84 6.49 6.49 0 01-3.69-.93 7.37 7.37 0 01-.6-3 7.37 7.37 0 01.6-3 8.09 8.09 0 013.87-.84 7.05 7.05 0 013.69.93z" fill="#fff" />
-        <path d="M139.08 40.42h2a10.23 10.23 0 01.6 3.18 9.24 9.24 0 01-.18 2.1 38.47 38.47 0 01-5.64.54q-6.48 0-6.48-7v-37l.36-.42h2.88a3.94 3.94 0 013.12 1.05 5.52 5.52 0 01.9 3.57v31.31q-.02 2.67 2.44 2.67zM155.94 22v23.94a20.85 20.85 0 01-3.66.3 23 23 0 01-3.78-.3V24.75q0-3.24-2.7-3.24h-.72a9.32 9.32 0 01-.3-2.58 10.7 10.7 0 01.3-2.7 39.63 39.63 0 014.38-.24h1a5.19 5.19 0 014.05 1.62 6.27 6.27 0 011.43 4.39z" />
-        <path d="M154.8 2.84a7.37 7.37 0 01.6 3 7.37 7.37 0 01-.6 3 7.46 7.46 0 01-3.87.84 6.49 6.49 0 01-3.69-.93 7.37 7.37 0 01-.6-3 7.37 7.37 0 01.6-3 8.09 8.09 0 013.87-.84 7.05 7.05 0 013.69.93z" fill="#fff" />
-        <path d="M163.08 39.22l8.76-11.82q1.32-1.8 4.8-5.7l-.18-.3a63.09 63.09 0 01-7.74.42H163a9.79 9.79 0 01-.24-2.34 15.8 15.8 0 01.42-3.3h20.4a16.31 16.31 0 011 4.26 4.1 4.1 0 01-.78 2.34L175 34.66a64.65 64.65 0 01-4.56 5.7l.18.24q3.12-.3 5.22-.3h2.58a15.35 15.35 0 006.12-.9 9.4 9.4 0 01.72 3.12q0 3.42-4.32 3.42h-18a14.27 14.27 0 01-.9-3.93 5.08 5.08 0 011.04-2.79zM215.88 31.06q0 8-4.26 12.3a13.63 13.63 0 01-18.06 0q-4.26-4.26-4.26-12.3t4.26-12.31a13.63 13.63 0 0118.06 0q4.26 4.27 4.26 12.31zm-13.29-10.08q-5.67 0-5.67 10.08t5.67 10.08q5.67 0 5.67-10.08t-5.67-10.08zM247 25.84v13.32a11 11 0 001.2 5.64 7 7 0 01-4.41 1.56q-2.43 0-3.33-1.14a5.69 5.69 0 01-.9-3.54V27.4a7.74 7.74 0 00-.72-3.87 2.78 2.78 0 00-2.58-1.17 8.62 8.62 0 00-6.3 3v20.58a20.85 20.85 0 01-3.66.3 23 23 0 01-3.78-.3v-29.7l.42-.36h2.76q3.42 0 4.08 3.6 4.38-3.84 8.73-3.84t6.42 2.82a12.17 12.17 0 012.07 7.38z" />
-        <path d="M57.26 10.75a7.37 7.37 0 01-.6-3 7.37 7.37 0 01.6-3 8.09 8.09 0 013.87-.84 7.05 7.05 0 013.69.84 7.37 7.37 0 01.6 3 7.37 7.37 0 01-.6 3 7.46 7.46 0 01-3.87.84 6.49 6.49 0 01-3.69-.84zM198.26 10.75a7.37 7.37 0 01-.6-3 7.37 7.37 0 01.6-3 8.09 8.09 0 013.87-.84 7.05 7.05 0 013.69.84 7.37 7.37 0 01.6 3 7.37 7.37 0 01-.6 3 7.46 7.46 0 01-3.87.84 6.49 6.49 0 01-3.69-.84z" fill="#fff" />
-    </g>
-</svg>
\ No newline at end of file
diff --git a/js/src/components/Comment/CommentTree.vue b/js/src/components/Comment/CommentTree.vue
index 592e23cd9..10c5bbc44 100644
--- a/js/src/components/Comment/CommentTree.vue
+++ b/js/src/components/Comment/CommentTree.vue
@@ -39,10 +39,13 @@
     <b-notification v-else :closable="false">{{
       $t("The organiser has chosen to close comments.")
     }}</b-notification>
-    <transition name="comment-empty-list" mode="out-in">
-      <p v-if="$apollo.queries.comments.loading" class="loading">
-        {{ $t("Loading…") }}
-      </p>
+    <p
+      v-if="$apollo.queries.comments.loading"
+      class="loading has-text-centered"
+    >
+      {{ $t("Loading comments…") }}
+    </p>
+    <transition name="comment-empty-list" mode="out-in" v-else>
       <transition-group
         name="comment-list"
         v-if="comments.length"
diff --git a/js/src/components/Logo.vue b/js/src/components/Logo.vue
index 97d010f41..92c1d47ea 100644
--- a/js/src/components/Logo.vue
+++ b/js/src/components/Logo.vue
@@ -1,19 +1,36 @@
 <template>
-  <mobilizon-logo />
+  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 248.16 46.78">
+    <title>Mobilizon Logo</title>
+    <g data-name="header">
+      <path
+        d="M0 45.82l3.18-40.8a29.88 29.88 0 015.07-.36 27.74 27.74 0 014.95.36l4.86 17.16a92.19 92.19 0 012.34 10.08h.36a92.19 92.19 0 012.34-10.08L28 5.02a29.23 29.23 0 015-.36 29.23 29.23 0 015 .36l3.18 40.8a13.61 13.61 0 01-3.63.42 23.41 23.41 0 01-3.63-.24l-1.2-19.92q-.36-5.52-.48-12.84h-.44l-7.32 26.51a25.62 25.62 0 01-4 .3 23.36 23.36 0 01-3.84-.3L9.36 13.24H9q-.3 8.94-.48 12.84L7.26 46a22.47 22.47 0 01-3.6.24A13.75 13.75 0 010 45.82zM74 31.06q0 8-4.26 12.3a12.21 12.21 0 01-9 3.42 12.21 12.21 0 01-9-3.42q-4.26-4.26-4.26-12.3t4.24-12.31a12.21 12.21 0 019-3.42 12.21 12.21 0 019 3.42Q74 23.02 74 31.06zM60.75 20.98q-5.67 0-5.67 10.08t5.67 10.08q5.67 0 5.67-10.08t-5.67-10.08zM103.2 19.75q2.7 4.11 2.7 11.28T102 42.31a13.18 13.18 0 01-10 4.11 31.41 31.41 0 01-11.34-2V2.2l.4-.45h2.76A4 4 0 0187 2.83a5.38 5.38 0 01.93 3.57v11.94a12.08 12.08 0 017.56-2.7 8.71 8.71 0 017.71 4.11zm-9.72 2a7.28 7.28 0 00-5.58 2.82v16a15 15 0 004.08.54 5.25 5.25 0 004.68-2.67q1.68-2.67 1.68-7.59 0-9.03-4.86-9.1zM121 22v23.94a20.85 20.85 0 01-3.66.3 23 23 0 01-3.78-.3V24.75q0-3.24-2.7-3.24h-.72a9.32 9.32 0 01-.3-2.58 10.7 10.7 0 01.3-2.7 39.63 39.63 0 014.38-.24h1a5.19 5.19 0 014 1.62A6.27 6.27 0 01121 22z"
+      />
+      <path
+        d="M119.82.84a7.37 7.37 0 01.6 3 7.37 7.37 0 01-.6 3 7.46 7.46 0 01-3.87.84 6.49 6.49 0 01-3.69-.93 7.37 7.37 0 01-.6-3 7.37 7.37 0 01.6-3 8.09 8.09 0 013.87-.84 7.05 7.05 0 013.69.93z"
+        fill="#fff"
+      />
+      <path
+        d="M139.08 40.42h2a10.23 10.23 0 01.6 3.18 9.24 9.24 0 01-.18 2.1 38.47 38.47 0 01-5.64.54q-6.48 0-6.48-7v-37l.36-.42h2.88a3.94 3.94 0 013.12 1.05 5.52 5.52 0 01.9 3.57v31.31q-.02 2.67 2.44 2.67zM155.94 22v23.94a20.85 20.85 0 01-3.66.3 23 23 0 01-3.78-.3V24.75q0-3.24-2.7-3.24h-.72a9.32 9.32 0 01-.3-2.58 10.7 10.7 0 01.3-2.7 39.63 39.63 0 014.38-.24h1a5.19 5.19 0 014.05 1.62 6.27 6.27 0 011.43 4.39z"
+      />
+      <path
+        d="M154.8 2.84a7.37 7.37 0 01.6 3 7.37 7.37 0 01-.6 3 7.46 7.46 0 01-3.87.84 6.49 6.49 0 01-3.69-.93 7.37 7.37 0 01-.6-3 7.37 7.37 0 01.6-3 8.09 8.09 0 013.87-.84 7.05 7.05 0 013.69.93z"
+        fill="#fff"
+      />
+      <path
+        d="M163.08 39.22l8.76-11.82q1.32-1.8 4.8-5.7l-.18-.3a63.09 63.09 0 01-7.74.42H163a9.79 9.79 0 01-.24-2.34 15.8 15.8 0 01.42-3.3h20.4a16.31 16.31 0 011 4.26 4.1 4.1 0 01-.78 2.34L175 34.66a64.65 64.65 0 01-4.56 5.7l.18.24q3.12-.3 5.22-.3h2.58a15.35 15.35 0 006.12-.9 9.4 9.4 0 01.72 3.12q0 3.42-4.32 3.42h-18a14.27 14.27 0 01-.9-3.93 5.08 5.08 0 011.04-2.79zM215.88 31.06q0 8-4.26 12.3a13.63 13.63 0 01-18.06 0q-4.26-4.26-4.26-12.3t4.26-12.31a13.63 13.63 0 0118.06 0q4.26 4.27 4.26 12.31zm-13.29-10.08q-5.67 0-5.67 10.08t5.67 10.08q5.67 0 5.67-10.08t-5.67-10.08zM247 25.84v13.32a11 11 0 001.2 5.64 7 7 0 01-4.41 1.56q-2.43 0-3.33-1.14a5.69 5.69 0 01-.9-3.54V27.4a7.74 7.74 0 00-.72-3.87 2.78 2.78 0 00-2.58-1.17 8.62 8.62 0 00-6.3 3v20.58a20.85 20.85 0 01-3.66.3 23 23 0 01-3.78-.3v-29.7l.42-.36h2.76q3.42 0 4.08 3.6 4.38-3.84 8.73-3.84t6.42 2.82a12.17 12.17 0 012.07 7.38z"
+      />
+      <path
+        d="M57.26 10.75a7.37 7.37 0 01-.6-3 7.37 7.37 0 01.6-3 8.09 8.09 0 013.87-.84 7.05 7.05 0 013.69.84 7.37 7.37 0 01.6 3 7.37 7.37 0 01-.6 3 7.46 7.46 0 01-3.87.84 6.49 6.49 0 01-3.69-.84zM198.26 10.75a7.37 7.37 0 01-.6-3 7.37 7.37 0 01.6-3 8.09 8.09 0 013.87-.84 7.05 7.05 0 013.69.84 7.37 7.37 0 01.6 3 7.37 7.37 0 01-.6 3 7.46 7.46 0 01-3.87.84 6.49 6.49 0 01-3.69-.84z"
+        fill="#fff"
+      />
+    </g>
+  </svg>
 </template>
 
 <script lang="ts">
 import { Component, Prop, Vue } from "vue-property-decorator";
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-ignore
-import MobilizonLogo from "../assets/mobilizon_logo.svg";
-// TODO: Jest does not like the ?inline after the import path
 
-@Component({
-  components: {
-    MobilizonLogo,
-  },
-})
+@Component
 export default class Logo extends Vue {
   @Prop({ type: Boolean, required: false, default: false }) invert!: boolean;
 }
diff --git a/js/src/components/Participation/ParticipationSection.vue b/js/src/components/Participation/ParticipationSection.vue
new file mode 100644
index 000000000..5be6e1bfd
--- /dev/null
+++ b/js/src/components/Participation/ParticipationSection.vue
@@ -0,0 +1,240 @@
+<template>
+  <div>
+    <div
+      class="event-participation has-text-right"
+      v-if="isEventNotAlreadyPassed"
+    >
+      <participation-button
+        v-if="shouldShowParticipationButton"
+        :participation="participation"
+        :event="event"
+        :current-actor="currentActor"
+        @join-event="(actor) => $emit('join-event', actor)"
+        @join-modal="$emit('join-modal')"
+        @join-event-with-confirmation="
+          (actor) => $emit('join-event-with-confirmation', actor)
+        "
+        @confirm-leave="$emit('confirm-leave')"
+      />
+      <b-button
+        type="is-text"
+        v-if="!actorIsParticipant && anonymousParticipation !== null"
+        @click="$emit('cancel-anonymous-participation')"
+        >{{ $t("Cancel anonymous participation") }}</b-button
+      >
+      <small v-if="!actorIsParticipant && anonymousParticipation">
+        {{ $t("You are participating in this event anonymously") }}
+        <b-tooltip :label="$t('Click for more information')">
+          <span
+            class="is-clickable"
+            @click="isAnonymousParticipationModalOpen = true"
+          >
+            <b-icon size="is-small" icon="information-outline" />
+          </span>
+        </b-tooltip>
+      </small>
+      <small
+        v-else-if="!actorIsParticipant && anonymousParticipation === false"
+      >
+        {{
+          $t(
+            "You are participating in this event anonymously but didn't confirm participation"
+          )
+        }}
+        <b-tooltip
+          :label="
+            $t(
+              'This information is saved only on your computer. Click for details'
+            )
+          "
+        >
+          <router-link :to="{ name: RouteName.TERMS }">
+            <b-icon size="is-small" icon="help-circle-outline" />
+          </router-link>
+        </b-tooltip>
+      </small>
+    </div>
+    <div v-else>
+      <button class="button is-primary" type="button" slot="trigger" disabled>
+        <template>
+          <span>{{ $t("Event already passed") }}</span>
+        </template>
+        <b-icon icon="menu-down" />
+      </button>
+    </div>
+    <b-modal
+      :active.sync="isAnonymousParticipationModalOpen"
+      has-modal-card
+      ref="anonymous-participation-modal"
+    >
+      <div class="modal-card">
+        <header class="modal-card-head">
+          <p class="modal-card-title">
+            {{ $t("About anonymous participation") }}
+          </p>
+        </header>
+
+        <section class="modal-card-body">
+          <b-notification
+            type="is-primary"
+            :closable="false"
+            v-if="event.joinOptions === EventJoinOptions.RESTRICTED"
+          >
+            {{
+              $t(
+                "As the event organizer has chosen to manually validate participation requests, your participation will be really confirmed only once you receive an email stating it's being accepted."
+              )
+            }}
+          </b-notification>
+          <p>
+            {{
+              $t(
+                "Your participation status is saved only on this device and will be deleted one month after the event's passed."
+              )
+            }}
+          </p>
+          <p v-if="isSecureContext">
+            {{
+              $t(
+                "You may clear all participation information for this device with the buttons below."
+              )
+            }}
+          </p>
+          <div class="buttons" v-if="isSecureContext">
+            <b-button
+              type="is-danger is-outlined"
+              @click="clearEventParticipationData"
+            >
+              {{ $t("Clear participation data for this event") }}
+            </b-button>
+            <b-button type="is-danger" @click="clearAllParticipationData">
+              {{ $t("Clear participation data for all events") }}
+            </b-button>
+          </div>
+        </section>
+      </div>
+    </b-modal>
+  </div>
+</template>
+<script lang="ts">
+import { EventJoinOptions, EventStatus, ParticipantRole } from "@/types/enums";
+import { IParticipant } from "@/types/participant.model";
+import { Component, Prop, Vue } from "vue-property-decorator";
+import RouteName from "@/router/name";
+import { IEvent } from "@/types/event.model";
+import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
+import { IPerson } from "@/types/actor";
+import { IConfig } from "@/types/config.model";
+import { CONFIG } from "@/graphql/config";
+import {
+  removeAllAnonymousParticipations,
+  removeAnonymousParticipation,
+} from "@/services/AnonymousParticipationStorage";
+import ParticipationButton from "../Event/ParticipationButton.vue";
+
+@Component({
+  apollo: {
+    currentActor: CURRENT_ACTOR_CLIENT,
+    config: CONFIG,
+  },
+  components: {
+    ParticipationButton,
+  },
+})
+export default class ParticipationSection extends Vue {
+  @Prop({ required: true }) participation!: IParticipant;
+
+  @Prop({ required: true }) event!: IEvent;
+
+  @Prop({ required: true, default: null }) anonymousParticipation!:
+    | boolean
+    | null;
+
+  currentActor!: IPerson;
+
+  config!: IConfig;
+
+  RouteName = RouteName;
+
+  EventJoinOptions = EventJoinOptions;
+
+  isAnonymousParticipationModalOpen = false;
+
+  get actorIsParticipant(): boolean {
+    if (this.actorIsOrganizer) return true;
+
+    return (
+      this.participation &&
+      this.participation.role === ParticipantRole.PARTICIPANT
+    );
+  }
+
+  get actorIsOrganizer(): boolean {
+    return (
+      this.participation && this.participation.role === ParticipantRole.CREATOR
+    );
+  }
+
+  get shouldShowParticipationButton(): boolean {
+    // If we have an anonymous participation, don't show the participation button
+    if (
+      this.config &&
+      this.config.anonymous.participation.allowed &&
+      this.anonymousParticipation
+    ) {
+      return false;
+    }
+
+    // So that people can cancel their participation
+    if (this.actorIsParticipant) return true;
+
+    // You can participate to draft or cancelled events
+    if (this.event.draft || this.event.status === EventStatus.CANCELLED)
+      return false;
+
+    // Organizer can't participate
+    if (this.actorIsOrganizer) return false;
+
+    // If capacity is OK
+    if (this.eventCapacityOK) return true;
+
+    // Else
+    return false;
+  }
+
+  get eventCapacityOK(): boolean {
+    if (this.event.draft) return true;
+    if (!this.event.options.maximumAttendeeCapacity) return true;
+    return (
+      this.event.options.maximumAttendeeCapacity >
+      this.event.participantStats.participant
+    );
+  }
+
+  get isEventNotAlreadyPassed(): boolean {
+    return new Date(this.endDate) > new Date();
+  }
+
+  get endDate(): Date {
+    return this.event.endsOn !== null && this.event.endsOn > this.event.beginsOn
+      ? this.event.endsOn
+      : this.event.beginsOn;
+  }
+
+  // eslint-disable-next-line class-methods-use-this
+  get isSecureContext(): boolean {
+    return window.isSecureContext;
+  }
+
+  async clearEventParticipationData(): Promise<void> {
+    await removeAnonymousParticipation(this.event.uuid);
+    window.location.reload();
+  }
+
+  // eslint-disable-next-line class-methods-use-this
+  clearAllParticipationData(): void {
+    removeAllAnonymousParticipations();
+    window.location.reload();
+  }
+}
+</script>
diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json
index d03b83648..a676c79b0 100644
--- a/js/src/i18n/en_US.json
+++ b/js/src/i18n/en_US.json
@@ -800,5 +800,15 @@
   "Mobilizon is a federated software, meaning you can interact - depending on your admin's federation settings - with content from other instances, such as joining groups or events that were created elsewhere.": "Mobilizon is a federated software, meaning you can interact - depending on your admin federation settings - with content from other instances, such as joining groups or events that were created elsewhere.",
   "This instance, <b>{instanceName} ({domain})</b>, hosts your profile, so remember its name.": "This instance, <b>{instanceName} ({domain})</b>, hosts your profile, so remember its name.",
   "If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:": "If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:",
-  "Uploaded media size": "Uploaded media size"
+  "Uploaded media size": "Uploaded media size",
+  "Loading comments…": "Loading comments…",
+  "Tentative": "Tentative",
+  "Cancelled": "Cancelled",
+  "Click for more information": "Click for more information",
+  "About anonymous participation": "About anonymous participation",
+  "As the event organizer has chosen to manually validate participation requests, your participation will be really confirmed only once you receive an email stating it's being accepted.": "As the event organizer has chosen to manually validate participation requests, your participation will be really confirmed only once you receive an email stating it's being accepted.",
+  "Your participation status is saved only on this device and will be deleted one month after the event's passed.": "Your participation status is saved only on this device and will be deleted one month after the event's passed.",
+  "You may clear all participation information for this device with the buttons below.": "You may clear all participation information for this device with the buttons below.",
+  "Clear participation data for this event": "Clear participation data for this event",
+  "Clear participation data for all events": "Clear participation data for all events"
 }
diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json
index 1e52c9b99..257a116e2 100644
--- a/js/src/i18n/fr_FR.json
+++ b/js/src/i18n/fr_FR.json
@@ -888,5 +888,15 @@
   "Mobilizon is a federated software, meaning you can interact - depending on your admin's federation settings - with content from other instances, such as joining groups or events that were created elsewhere.": "Mobilizon est un logiciel fédéré, ce qui signifie que vous pouvez interagir - en fonction des paramètres de fédération de votre administrateur·ice - avec du contenu d'autres instances, comme par exemple rejoindre des groupes ou des événements ayant été créés ailleurs.",
   "This instance, <b>{instanceName} ({domain})</b>, hosts your profile, so remember its name.": "Cette instance, <b>{instanceName} ({domain})</b>, héberge votre profil, donc notez bien son nom.",
   "If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:": "Si l'on vous demande votre identité fédérée, elle est composée de votre nom d'utilisateur·ice et de votre instance. Par exemple, l'identité fédérée de votre premier profil est :",
-  "Uploaded media size": "Taille des médias téléversés"
+  "Uploaded media size": "Taille des médias téléversés",
+  "Loading comments…": "Chargement des commentaires…",
+  "Tentative": "Provisoire",
+  "Cancelled": "Annulé",
+  "Click for more information": "Cliquez pour plus d'informations",
+  "About anonymous participation": "À propos de la participation anonyme",
+  "As the event organizer has chosen to manually validate participation requests, your participation will be really confirmed only once you receive an email stating it's being accepted.": "L'organisateur de l'événement ayant choisi de valider manuellement les demandes de participation, votre participation ne sera réellement confirmée que lorsque vous recevrez un courriel indiquant qu'elle est acceptée.",
+  "Your participation status is saved only on this device and will be deleted one month after the event's passed.": "Le statut de votre participation est enregistré uniquement sur cet appareil et sera supprimé un mois après la fin de l'événement.",
+  "You may clear all participation information for this device with the buttons below.": "Vous pouvez effacer toutes les informations de participation pour cet appareil avec les boutons ci-dessous.",
+  "Clear participation data for this event": "Effacer mes données de participation pour cet événement",
+  "Clear participation data for all events": "Effacer mes données de participation pour tous les événements"
 }
diff --git a/js/src/services/AnonymousParticipationStorage.ts b/js/src/services/AnonymousParticipationStorage.ts
index 25f3e441f..ab03f3a68 100644
--- a/js/src/services/AnonymousParticipationStorage.ts
+++ b/js/src/services/AnonymousParticipationStorage.ts
@@ -160,11 +160,16 @@ async function removeAnonymousParticipation(eventUUID: string): Promise<void> {
   );
 }
 
+function removeAllAnonymousParticipations(): void {
+  localStorage.removeItem(ANONYMOUS_PARTICIPATIONS_LOCALSTORAGE_KEY);
+}
+
 export {
   addLocalUnconfirmedAnonymousParticipation,
   confirmLocalAnonymousParticipation,
   getLeaveTokenForParticipation,
   isParticipatingInThisEvent,
   removeAnonymousParticipation,
+  removeAllAnonymousParticipations,
   AnonymousParticipationNotFoundError,
 };
diff --git a/js/src/views/About/AboutInstance.vue b/js/src/views/About/AboutInstance.vue
index a1b7db04d..86b75262f 100644
--- a/js/src/views/About/AboutInstance.vue
+++ b/js/src/views/About/AboutInstance.vue
@@ -47,7 +47,7 @@
       <table class="table is-fullwidth">
         <tr>
           <td>{{ $t("Instance languages") }}</td>
-          <td :title="this.config.languages.join(', ')">
+          <td :title="this.config ? this.config.languages.join(', ') : ''">
             {{ formattedLanguageList }}
           </td>
         </tr>
@@ -105,7 +105,7 @@ import langs from "../../i18n/langs.json";
         };
       },
       skip() {
-        return !this.config.languages;
+        return !this.config || !this.config.languages;
       },
     },
   },
@@ -126,8 +126,11 @@ export default class AboutInstance extends Vue {
   }
 
   get formattedLanguageList(): string {
-    const list = this.languages.map(({ name }) => name);
-    return formatList(list);
+    if (this.languages) {
+      const list = this.languages.map(({ name }) => name);
+      return formatList(list);
+    }
+    return "";
   }
 
   // eslint-disable-next-line class-methods-use-this
diff --git a/js/src/views/Event/Event.vue b/js/src/views/Event/Event.vue
index 3974c7519..5c665e347 100644
--- a/js/src/views/Event/Event.vue
+++ b/js/src/views/Event/Event.vue
@@ -104,80 +104,16 @@
               </span>
             </div>
             <div class="column is-3-tablet">
-              <div>
-                <div
-                  class="event-participation has-text-right"
-                  v-if="new Date(endDate) > new Date()"
-                >
-                  <participation-button
-                    v-if="shouldShowParticipationButton"
-                    :participation="participations[0]"
-                    :event="event"
-                    :current-actor="currentActor"
-                    @join-event="joinEvent"
-                    @join-modal="isJoinModalActive = true"
-                    @join-event-with-confirmation="joinEventWithConfirmation"
-                    @confirm-leave="confirmLeave"
-                  />
-                  <b-button
-                    type="is-text"
-                    v-if="
-                      !actorIsParticipant && anonymousParticipation !== null
-                    "
-                    @click="cancelAnonymousParticipation"
-                    >{{ $t("Cancel anonymous participation") }}</b-button
-                  >
-                  <small v-if="!actorIsParticipant && anonymousParticipation">
-                    {{ $t("You are participating in this event anonymously") }}
-                    <b-tooltip
-                      :label="
-                        $t(
-                          'This information is saved only on your computer. Click for details'
-                        )
-                      "
-                    >
-                      <router-link :to="{ name: RouteName.TERMS }">
-                        <b-icon size="is-small" icon="help-circle-outline" />
-                      </router-link>
-                    </b-tooltip>
-                  </small>
-                  <small
-                    v-else-if="
-                      !actorIsParticipant && anonymousParticipation === false
-                    "
-                  >
-                    {{
-                      $t(
-                        "You are participating in this event anonymously but didn't confirm participation"
-                      )
-                    }}
-                    <b-tooltip
-                      :label="
-                        $t(
-                          'This information is saved only on your computer. Click for details'
-                        )
-                      "
-                    >
-                      <router-link :to="{ name: RouteName.TERMS }">
-                        <b-icon size="is-small" icon="help-circle-outline" />
-                      </router-link>
-                    </b-tooltip>
-                  </small>
-                </div>
-                <div v-else>
-                  <button
-                    class="button is-primary"
-                    type="button"
-                    slot="trigger"
-                    disabled
-                  >
-                    <template>
-                      <span>{{ $t("Event already passed") }}</span>
-                    </template>
-                    <b-icon icon="menu-down" />
-                  </button>
-                </div>
-              </div>
+              <participation-section
+                :participation="participations[0]"
+                :event="event"
+                :anonymousParticipation="anonymousParticipation"
+                @join-event="joinEvent"
+                @join-modal="isJoinModalActive = true"
+                @join-event-with-confirmation="joinEventWithConfirmation"
+                @confirm-leave="confirmLeave"
+                @cancel-anonymous-participation="cancelAnonymousParticipation"
+              />
               <div class="has-text-right">
                 <template class="visibility" v-if="!event.draft">
                   <p v-if="event.visibility === EventVisibility.PUBLIC">
@@ -646,7 +582,7 @@ import { IReport } from "../../types/report.model";
 import { CREATE_REPORT } from "../../graphql/report";
 import EventMixin from "../../mixins/event";
 import IdentityPicker from "../Account/IdentityPicker.vue";
-import ParticipationButton from "../../components/Event/ParticipationButton.vue";
+import ParticipationSection from "../../components/Participation/ParticipationSection.vue";
 import RouteName from "../../router/name";
 import { Address } from "../../types/address.model";
 import CommentTree from "../../components/Comment/CommentTree.vue";
@@ -676,7 +612,7 @@ import { IParticipant } from "../../types/participant.model";
     DateCalendarIcon,
     ReportModal,
     IdentityPicker,
-    ParticipationButton,
+    ParticipationSection,
     CommentTree,
     Tag,
     ActorCard,
diff --git a/js/tests/unit/specs/components/Comment/CommentTree.spec.ts b/js/tests/unit/specs/components/Comment/CommentTree.spec.ts
index 950ea51b6..886c6a443 100644
--- a/js/tests/unit/specs/components/Comment/CommentTree.spec.ts
+++ b/js/tests/unit/specs/components/Comment/CommentTree.spec.ts
@@ -91,7 +91,7 @@ describe("CommentTree", () => {
     expect(wrapper.findComponent({ name: "b-notification" }).text()).toBe(
       "The organiser has chosen to close comments."
     );
-    expect(wrapper.find(".loading").text()).toBe("Loading…");
+    expect(wrapper.find(".loading").text()).toBe("Loading comments…");
     expect(wrapper.html()).toMatchSnapshot();
   });
 
diff --git a/js/tests/unit/specs/components/Comment/__snapshots__/CommentTree.spec.ts.snap b/js/tests/unit/specs/components/Comment/__snapshots__/CommentTree.spec.ts.snap
index 4ea79739c..48a66f29d 100644
--- a/js/tests/unit/specs/components/Comment/__snapshots__/CommentTree.spec.ts.snap
+++ b/js/tests/unit/specs/components/Comment/__snapshots__/CommentTree.spec.ts.snap
@@ -3,12 +3,9 @@
 exports[`CommentTree renders a comment tree 1`] = `
 <div>
   <b-notification-stub active="true" duration="2000" animation="fade">The organiser has chosen to close comments.</b-notification-stub>
-  <transition-stub name="comment-empty-list" mode="out-in">
-    <p class="loading">
-      Loading…
-    </p>
-    <!---->
-  </transition-stub>
+  <p class="loading has-text-centered">
+    Loading comments…
+  </p>
 </div>
 `;
 
@@ -16,7 +13,6 @@ exports[`CommentTree renders a comment tree 2`] = `
 <div>
   <b-notification-stub active="true" duration="2000" animation="fade">The organiser has chosen to close comments.</b-notification-stub>
   <transition-stub name="comment-empty-list" mode="out-in">
-    <!---->
     <transition-group-stub tag="ul" name="comment-list" class="comment-list">
       <comment-stub comment="[object Object]" event="[object Object]" class="root-comment"></comment-stub>
       <comment-stub comment="[object Object]" event="[object Object]" class="root-comment"></comment-stub>
diff --git a/js/tests/unit/specs/components/Participation/ParticipationSection.spec.ts b/js/tests/unit/specs/components/Participation/ParticipationSection.spec.ts
new file mode 100644
index 000000000..2d6eb7657
--- /dev/null
+++ b/js/tests/unit/specs/components/Participation/ParticipationSection.spec.ts
@@ -0,0 +1,189 @@
+import { config, createLocalVue, mount, Wrapper } from "@vue/test-utils";
+import ParticipationSection from "@/components/Participation/ParticipationSection.vue";
+import Buefy from "buefy";
+import VueRouter from "vue-router";
+import { routes } from "@/router";
+import { CommentModeration, EventJoinOptions } from "@/types/enums";
+import {
+  createMockClient,
+  MockApolloClient,
+  RequestHandler,
+} from "mock-apollo-client";
+import buildCurrentUserResolver from "@/apollo/user";
+import { InMemoryCache } from "apollo-cache-inmemory";
+import { CONFIG } from "@/graphql/config";
+import VueApollo from "vue-apollo";
+import { configMock } from "../../mocks/config";
+
+const localVue = createLocalVue();
+localVue.use(Buefy);
+localVue.use(VueRouter);
+const router = new VueRouter({ routes, mode: "history" });
+config.mocks.$t = (key: string): string => key;
+
+const eventData = {
+  id: "1",
+  uuid: "e37910ea-fd5a-4756-7634-00971f3f4107",
+  options: {
+    commentModeration: CommentModeration.ALLOW_ALL,
+  },
+  beginsOn: new Date("2089-12-04T09:21:25Z"),
+  endsOn: new Date("2089-12-04T11:21:25Z"),
+};
+
+describe("ParticipationSection", () => {
+  let wrapper: Wrapper<Vue>;
+  let mockClient: MockApolloClient;
+  let apolloProvider;
+  let requestHandlers: Record<string, RequestHandler>;
+
+  const generateWrapper = (
+    handlers: Record<string, unknown> = {},
+    customProps: Record<string, unknown> = {},
+    baseData: Record<string, unknown> = {}
+  ) => {
+    const cache = new InMemoryCache({ addTypename: false });
+
+    mockClient = createMockClient({
+      cache,
+      resolvers: buildCurrentUserResolver(cache),
+    });
+    requestHandlers = {
+      configQueryHandler: jest.fn().mockResolvedValue(configMock),
+      ...handlers,
+    };
+    mockClient.setRequestHandler(CONFIG, requestHandlers.configQueryHandler);
+
+    apolloProvider = new VueApollo({
+      defaultClient: mockClient,
+    });
+
+    wrapper = mount(ParticipationSection, {
+      localVue,
+      router,
+      apolloProvider,
+      propsData: {
+        participation: null,
+        event: eventData,
+        anonymousParticipation: null,
+        ...customProps,
+      },
+      data() {
+        return {
+          currentActor: {
+            id: "76",
+          },
+          ...baseData,
+        };
+      },
+    });
+  };
+
+  it("renders the participation section with minimal data", async () => {
+    generateWrapper();
+    await wrapper.vm.$nextTick();
+
+    expect(wrapper.exists()).toBe(true);
+    expect(requestHandlers.configQueryHandler).toHaveBeenCalled();
+    expect(wrapper.vm.$apollo.queries.config).toBeTruthy();
+
+    expect(wrapper.find(".event-participation").exists()).toBeTruthy();
+
+    const participationButton = wrapper.find(
+      ".event-participation .participation-button a.button.is-large.is-primary"
+    );
+    expect(participationButton.attributes("href")).toBe(
+      `/events/${eventData.uuid}/participate/with-account`
+    );
+
+    expect(participationButton.text()).toBe("Participate");
+  });
+
+  it("renders the participation section with existing confimed anonymous participation", async () => {
+    generateWrapper({}, { anonymousParticipation: true });
+
+    expect(wrapper.find(".event-participation > small").text()).toContain(
+      "You are participating in this event anonymously"
+    );
+
+    const cancelAnonymousParticipationButton = wrapper.find(
+      ".event-participation > button.button.is-text"
+    );
+    expect(cancelAnonymousParticipationButton.text()).toBe(
+      "Cancel anonymous participation"
+    );
+
+    wrapper.find(".event-participation small .is-clickable").trigger("click");
+    expect(
+      wrapper
+        .findComponent({ ref: "anonymous-participation-modal" })
+        .isVisible()
+    ).toBeTruthy();
+
+    cancelAnonymousParticipationButton.trigger("click");
+    await wrapper.vm.$nextTick();
+    expect(wrapper.emitted("cancel-anonymous-participation")).toBeTruthy();
+  });
+
+  it("renders the participation section with existing confimed anonymous participation but event moderation", async () => {
+    generateWrapper(
+      {},
+      {
+        anonymousParticipation: true,
+        event: { ...eventData, joinOptions: EventJoinOptions.RESTRICTED },
+      }
+    );
+
+    expect(wrapper.find(".event-participation > small").text()).toContain(
+      "You are participating in this event anonymously"
+    );
+
+    const cancelAnonymousParticipationButton = wrapper.find(
+      ".event-participation > button.button.is-text"
+    );
+    expect(cancelAnonymousParticipationButton.text()).toBe(
+      "Cancel anonymous participation"
+    );
+
+    wrapper.find(".event-participation small .is-clickable").trigger("click");
+
+    await wrapper.vm.$nextTick();
+    const modal = wrapper.findComponent({
+      ref: "anonymous-participation-modal",
+    });
+    expect(modal.isVisible()).toBeTruthy();
+    expect(modal.find("article.notification.is-primary").text()).toBe(
+      "As the event organizer has chosen to manually validate participation requests, your participation will be really confirmed only once you receive an email stating it's being accepted."
+    );
+
+    cancelAnonymousParticipationButton.trigger("click");
+    await wrapper.vm.$nextTick();
+    expect(wrapper.emitted("cancel-anonymous-participation")).toBeTruthy();
+  });
+
+  it("renders the participation section with existing unconfirmed anonymous participation", async () => {
+    generateWrapper({}, { anonymousParticipation: false });
+
+    expect(wrapper.find(".event-participation > small").text()).toContain(
+      "You are participating in this event anonymously but didn't confirm participation"
+    );
+  });
+
+  it("renders the participation section but the event is already passed", async () => {
+    generateWrapper(
+      {},
+      {
+        event: {
+          ...eventData,
+          beginsOn: "2020-12-02T10:52:56Z",
+          endsOn: "2020-12-03T10:52:56Z",
+        },
+      }
+    );
+
+    expect(wrapper.find(".event-participation").exists()).toBeFalsy();
+    expect(wrapper.find("button.button.is-primary").text()).toBe(
+      "Event already passed"
+    );
+  });
+});