summaryrefslogtreecommitdiff
path: root/addons/website_event_track/static
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/website_event_track/static
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/website_event_track/static')
-rw-r--r--addons/website_event_track/static/description/blog.pngbin0 -> 71285 bytes
-rw-r--r--addons/website_event_track/static/description/event.pngbin0 -> 44250 bytes
-rw-r--r--addons/website_event_track/static/description/icon.pngbin0 -> 11221 bytes
-rw-r--r--addons/website_event_track/static/description/icon.svg1
-rw-r--r--addons/website_event_track/static/description/index.html97
-rw-r--r--addons/website_event_track/static/description/sponsor.pngbin0 -> 100472 bytes
-rw-r--r--addons/website_event_track/static/description/tracks.pngbin0 -> 59623 bytes
-rw-r--r--addons/website_event_track/static/lib/idb-keyval/idb-keyval.js81
-rw-r--r--addons/website_event_track/static/src/css/website_event_track.css82
-rw-r--r--addons/website_event_track/static/src/img/event_sponsor_default_0.jpegbin0 -> 11061 bytes
-rw-r--r--addons/website_event_track/static/src/img/event_track_default_0.jpegbin0 -> 8236 bytes
-rw-r--r--addons/website_event_track/static/src/img/event_track_default_1.jpegbin0 -> 13487 bytes
-rw-r--r--addons/website_event_track/static/src/js/event_track_reminder.js99
-rw-r--r--addons/website_event_track/static/src/js/service_worker.js363
-rw-r--r--addons/website_event_track/static/src/js/website_event_pwa_widget.js215
-rw-r--r--addons/website_event_track/static/src/js/website_event_track.js32
-rw-r--r--addons/website_event_track/static/src/scss/event_track_templates.scss244
-rw-r--r--addons/website_event_track/static/src/scss/event_track_templates_online.scss89
-rw-r--r--addons/website_event_track/static/src/scss/pwa_frontend.scss35
-rw-r--r--addons/website_event_track/static/src/xml/website_event_pwa.xml11
20 files changed, 1349 insertions, 0 deletions
diff --git a/addons/website_event_track/static/description/blog.png b/addons/website_event_track/static/description/blog.png
new file mode 100644
index 00000000..45da5099
--- /dev/null
+++ b/addons/website_event_track/static/description/blog.png
Binary files differ
diff --git a/addons/website_event_track/static/description/event.png b/addons/website_event_track/static/description/event.png
new file mode 100644
index 00000000..1bc349f3
--- /dev/null
+++ b/addons/website_event_track/static/description/event.png
Binary files differ
diff --git a/addons/website_event_track/static/description/icon.png b/addons/website_event_track/static/description/icon.png
new file mode 100644
index 00000000..c9e5ecb0
--- /dev/null
+++ b/addons/website_event_track/static/description/icon.png
Binary files differ
diff --git a/addons/website_event_track/static/description/icon.svg b/addons/website_event_track/static/description/icon.svg
new file mode 100644
index 00000000..87c7872c
--- /dev/null
+++ b/addons/website_event_track/static/description/icon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="100%"><stop offset="0%" stop-color="#CD7690"/><stop offset="100%" stop-color="#CA5377"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M34.79 69H4c-2 0-4-.146-4-4.075v-20.97l25-23.86 9-2.038 10 2.037L45 15h12v8.15l-6 5.095 2 8.151-1 7.132-3 5.095L34.79 69z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><path fill="#000" d="M24.762 52.559V50c-3.65-2.952-4.016-8.81-1.095-17.571l9.767 1.485A79.883 79.883 0 0 1 33 32l10-3c3.333 10 3.667 15.667 1 17l1.4 4.898c3.439-3.111 5.6-7.61 5.6-12.612 0-9.39-7.611-17-17-17s-17 7.61-17 17c0 5.982 3.09 11.243 7.762 14.273zm2.566 1.367A16.945 16.945 0 0 0 34 55.286c3.28 0 6.343-.93 8.94-2.538L41 47c-2.868-.574-5.242-4.355-7.123-11.344-.816 9.87-2.759 15-5.83 15.392l-.72 2.878zm15.289-32.579a8.215 8.215 0 1 1 8.68 9.066A18.93 18.93 0 0 1 53 38.286c0 10.493-8.507 19-19 19s-19-8.507-19-19c0-10.494 8.507-19 19-19 3.102 0 6.03.743 8.617 2.061zM49.786 18v3H47v2h2.786v3H52v-3h3v-2h-3v-3h-2.214z" opacity=".3"/><path fill="#FFF" d="M24.762 50.559V48c-3.65-2.952-4.016-8.81-1.095-17.571l9.767 1.485A79.883 79.883 0 0 1 33 30l10-3c3.333 10 3.667 15.667 1 17l1.4 4.898c3.439-3.111 5.6-7.61 5.6-12.612 0-9.39-7.611-17-17-17s-17 7.61-17 17c0 5.982 3.09 11.243 7.762 14.273zm2.566 1.367A16.945 16.945 0 0 0 34 53.286c3.28 0 6.343-.93 8.94-2.538L41 45c-2.868-.574-5.242-4.355-7.123-11.344-.816 9.87-2.759 15-5.83 15.392l-.72 2.878zm15.289-32.579a8.215 8.215 0 1 1 8.68 9.066A18.93 18.93 0 0 1 53 36.286c0 10.493-8.507 19-19 19s-19-8.507-19-19c0-10.494 8.507-19 19-19 3.102 0 6.03.743 8.617 2.061zM49.786 16v3H47v2h2.786v3H52v-3h3v-2h-3v-3h-2.214z"/></g></g></svg> \ No newline at end of file
diff --git a/addons/website_event_track/static/description/index.html b/addons/website_event_track/static/description/index.html
new file mode 100644
index 00000000..52dde14f
--- /dev/null
+++ b/addons/website_event_track/static/description/index.html
@@ -0,0 +1,97 @@
+<section class="oe_container">
+ <div class="oe_row oe_spaced">
+ <div class="oe_span12">
+ <h2 class="oe_slogan">Organize Events, Trainings &amp; Webinars</h2>
+ <h3 class="oe_slogan">Schedule, Promote, Sell, Organize</h3>
+ </div>
+ <div class="oe_span6">
+ <div class="oe_demo oe_picture oe_screenshot">
+ <img src="event.png">
+ </div>
+ </div>
+ <div class="oe_span6">
+ <p class='oe_mt32'>
+ Get extra features per event; multiple pages, sponsors,
+ multiple talks, talk proposal form, agenda, event-related news,
+ documents (slides of presentations), event-specific menus.
+ </p>
+ </div>
+ </div>
+</section>
+
+<section class="oe_container oe_dark">
+ <div class="oe_row">
+ <h2 class="oe_slogan">Organize Your Tracks</h2>
+ <h3 class="oe_slogan">From the talk proposal to the publication</h3>
+ <div class="oe_span6">
+ <p class='oe_mt32'>
+ Add a talk proposal form on your events to allow visitors to
+ submit talks and speakers. Organize the validation process
+ of every talk, and schedule easily.
+ </p><p>
+ Odoo's unique frontend and backend integration makes
+ organization and publication so easy. Easily design beautiful
+ speaker biographies and talks description.
+ </p>
+ </div>
+ <div class="oe_span6">
+ <div class="oe_bg_img oe_centered">
+ <img class="oe_picture oe_screenshot" src="tracks.png">
+ </div>
+ </div>
+ </div>
+</section>
+
+<section class="oe_container">
+ <div class="oe_row">
+ <h2 class="oe_slogan">Agenda and List of Talks</h2>
+ <h3 class="oe_slogan">A strong user interface</h3>
+ <div class="oe_span6">
+ <img class="oe_picture oe_screenshot" src="tracks.png">
+ </div>
+ <div class="oe_span6">
+ <p class='oe_mt32'>
+ Get a beautiful agenda for each event published automatically on
+ your website. Allow your visitors to easily search and browse
+ talks, filter by tags, locations or speakers.
+ </p>
+ </div>
+ </div>
+</section>
+
+<section class="oe_container oe_dark">
+ <div class="oe_row">
+ <h2 class="oe_slogan">Manage Sponsors</h2>
+ <h3 class="oe_slogan">Sell sponsorship, promote your sponsors</h3>
+ <div class="oe_span6">
+ <p class='oe_mt32'>
+ Add sponsors to your events and publish sponsors per level (e.g.
+ bronze, silver, gold) on the bottom of every page of the event.
+ </p><p>
+ Sell sponsorship packages online through the Odoo eCommerce for
+ a full sales cycle integration.
+ </p>
+ </div>
+ <div class="oe_span6">
+ <img class="oe_picture oe_screenshot" src="sponsor.png">
+ </div>
+ </div>
+</section>
+
+<section class="oe_container">
+ <div class="oe_row">
+ <h2 class="oe_slogan">Communicate Efficiently</h2>
+ <h3 class="oe_slogan">Activate a blog for some events</h3>
+ <div class="oe_span6">
+ <img class="oe_picture oe_screenshot" src="blog.png">
+ </div>
+ <div class="oe_span6">
+ <p class='oe_mt32'>
+ You can activate a blog for each event allowing you to communicate
+ on specific events. Visitors can subscribe to news to get informed.
+ </p>
+ </div>
+ </div>
+</section>
+
+
diff --git a/addons/website_event_track/static/description/sponsor.png b/addons/website_event_track/static/description/sponsor.png
new file mode 100644
index 00000000..63fb9331
--- /dev/null
+++ b/addons/website_event_track/static/description/sponsor.png
Binary files differ
diff --git a/addons/website_event_track/static/description/tracks.png b/addons/website_event_track/static/description/tracks.png
new file mode 100644
index 00000000..b74a144b
--- /dev/null
+++ b/addons/website_event_track/static/description/tracks.png
Binary files differ
diff --git a/addons/website_event_track/static/lib/idb-keyval/idb-keyval.js b/addons/website_event_track/static/lib/idb-keyval/idb-keyval.js
new file mode 100644
index 00000000..c08909dc
--- /dev/null
+++ b/addons/website_event_track/static/lib/idb-keyval/idb-keyval.js
@@ -0,0 +1,81 @@
+// idb-keyval.js 3.2.0
+// https://github.com/jakearchibald/idb-keyval
+// Copyright 2016, Jake Archibald
+// Licensed under the Apache License, Version 2.0
+
+var idbKeyval = (function (exports) {
+ 'use strict';
+
+ class Store {
+ constructor(dbName = 'keyval-store', storeName = 'keyval') {
+ this.storeName = storeName;
+ this._dbp = new Promise((resolve, reject) => {
+ const openreq = indexedDB.open(dbName, 1);
+ openreq.onerror = () => reject(openreq.error);
+ openreq.onsuccess = () => resolve(openreq.result);
+ // First time setup: create an empty object store
+ openreq.onupgradeneeded = () => {
+ openreq.result.createObjectStore(storeName);
+ };
+ });
+ }
+ _withIDBStore(type, callback) {
+ return this._dbp.then(db => new Promise((resolve, reject) => {
+ const transaction = db.transaction(this.storeName, type);
+ transaction.oncomplete = () => resolve();
+ transaction.onabort = transaction.onerror = () => reject(transaction.error);
+ callback(transaction.objectStore(this.storeName));
+ }));
+ }
+ }
+ let store;
+ function getDefaultStore() {
+ if (!store)
+ store = new Store();
+ return store;
+ }
+ function get(key, store = getDefaultStore()) {
+ let req;
+ return store._withIDBStore('readonly', store => {
+ req = store.get(key);
+ }).then(() => req.result);
+ }
+ function set(key, value, store = getDefaultStore()) {
+ return store._withIDBStore('readwrite', store => {
+ store.put(value, key);
+ });
+ }
+ function del(key, store = getDefaultStore()) {
+ return store._withIDBStore('readwrite', store => {
+ store.delete(key);
+ });
+ }
+ function clear(store = getDefaultStore()) {
+ return store._withIDBStore('readwrite', store => {
+ store.clear();
+ });
+ }
+ function keys(store = getDefaultStore()) {
+ const keys = [];
+ return store._withIDBStore('readonly', store => {
+ // This would be store.getAllKeys(), but it isn't supported by Edge or Safari.
+ // And openKeyCursor isn't supported by Safari.
+ (store.openKeyCursor || store.openCursor).call(store).onsuccess = function () {
+ if (!this.result)
+ return;
+ keys.push(this.result.key);
+ this.result.continue();
+ };
+ }).then(() => keys);
+ }
+
+ exports.Store = Store;
+ exports.get = get;
+ exports.set = set;
+ exports.del = del;
+ exports.clear = clear;
+ exports.keys = keys;
+
+ return exports;
+
+ }({}));
diff --git a/addons/website_event_track/static/src/css/website_event_track.css b/addons/website_event_track/static/src/css/website_event_track.css
new file mode 100644
index 00000000..0940ceea
--- /dev/null
+++ b/addons/website_event_track/static/src/css/website_event_track.css
@@ -0,0 +1,82 @@
+.o_wevent_event .o_wevent_sponsor {
+ position: relative;
+ display: inline-block;
+ overflow: hidden;
+}
+
+.o_wevent_event .o_ribbon {
+ font: 12px Sans-Serif;
+ color: #404040;
+ padding-top: 2px;
+ padding-bottom: 2px;
+ -webkit-box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3);
+ -moz-box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3);
+ box-shadow: 0px 0px 3px rgba(0, 0, 0, 0.3);
+}
+
+.o_wevent_event .ribbon_Gold {
+ background-color: #FDE21B;
+ background-image: -webkit-gradient(linear, left top, left bottom, from(#E9CE0C), to(#FDE21B));
+ background-image: -webkit-linear-gradient(top, #E9CE0C, #FDE21B);
+ background-image: -moz-linear-gradient(top, #E9CE0C, #FDE21B);
+ background-image: -ms-linear-gradient(top, #E9CE0C, #FDE21B);
+ background-image: -o-linear-gradient(top, #E9CE0C, #FDE21B);
+}
+
+.o_wevent_event .ribbon_Silver {
+ background-color: #CCCCCC;
+ background-image: -webkit-gradient(linear, left top, left bottom, from(#BBBBBB), to(#CCCCCC));
+ background-image: -webkit-linear-gradient(top, #BBBBBB, #CCCCCC);
+ background-image: -moz-linear-gradient(top, #BBBBBB, #CCCCCC);
+ background-image: -ms-linear-gradient(top, #BBBBBB, #CCCCCC);
+ background-image: -o-linear-gradient(top, #BBBBBB, #CCCCCC);
+}
+
+.o_wevent_event .ribbon_Bronze {
+ background-color: #DB9141;
+ background-image: -webkit-gradient(linear, left top, left bottom, from(#C2792A), to(#DB9141));
+ background-image: -webkit-linear-gradient(top, #C2792A, #DB9141);
+ background-image: -moz-linear-gradient(top, #C2792A, #DB9141);
+ background-image: -ms-linear-gradient(top, #C2792A, #DB9141);
+ background-image: -o-linear-gradient(top, #C2792A, #DB9141);
+}
+.o_wevent_event .event_color_0 {
+ background-color: white;
+ color: #5a5a5a;
+}
+.o_wevent_event .event_color_1 {
+ background-color: #cccccc;
+ color: #424242;
+}
+.o_wevent_event .event_color_2 {
+ background-color: #ffc7c7;
+ color: #7a3737;
+}
+.o_wevent_event .event_color_3 {
+ background-color: #fff1c7;
+ color: #756832;
+}
+.o_wevent_event .event_color_4 {
+ background-color: #e3ffc7;
+ color: #5d6937;
+}
+.o_wevent_event .event_color_5 {
+ background-color: #c7ffd5;
+ color: #1a7759;
+}
+.o_wevent_event .event_color_6 {
+ background-color: #c7ffff;
+ color: #1a5d83;
+}
+.o_wevent_event .event_color_7 {
+ background-color: #c7d5ff;
+ color: #3b3e75;
+}
+.o_wevent_event .event_color_8 {
+ background-color: #e3c7ff;
+ color: #4c3668;
+}
+.o_wevent_event .event_color_9 {
+ background-color: #ffc7f1;
+ color: #6d2c70;
+}
diff --git a/addons/website_event_track/static/src/img/event_sponsor_default_0.jpeg b/addons/website_event_track/static/src/img/event_sponsor_default_0.jpeg
new file mode 100644
index 00000000..32aad675
--- /dev/null
+++ b/addons/website_event_track/static/src/img/event_sponsor_default_0.jpeg
Binary files differ
diff --git a/addons/website_event_track/static/src/img/event_track_default_0.jpeg b/addons/website_event_track/static/src/img/event_track_default_0.jpeg
new file mode 100644
index 00000000..d7c19533
--- /dev/null
+++ b/addons/website_event_track/static/src/img/event_track_default_0.jpeg
Binary files differ
diff --git a/addons/website_event_track/static/src/img/event_track_default_1.jpeg b/addons/website_event_track/static/src/img/event_track_default_1.jpeg
new file mode 100644
index 00000000..5269b0b2
--- /dev/null
+++ b/addons/website_event_track/static/src/img/event_track_default_1.jpeg
Binary files differ
diff --git a/addons/website_event_track/static/src/js/event_track_reminder.js b/addons/website_event_track/static/src/js/event_track_reminder.js
new file mode 100644
index 00000000..cae10488
--- /dev/null
+++ b/addons/website_event_track/static/src/js/event_track_reminder.js
@@ -0,0 +1,99 @@
+odoo.define('website_event_track.website_event_track_reminder', function (require) {
+'use strict';
+
+var core = require('web.core');
+var _t = core._t;
+var utils = require('web.utils');
+var publicWidget = require('web.public.widget');
+
+publicWidget.registry.websiteEventTrackReminder = publicWidget.Widget.extend({
+ selector: '.o_wetrack_js_reminder',
+ events: {
+ 'click': '_onReminderToggleClick',
+ },
+
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this._onReminderToggleClick = _.debounce(this._onReminderToggleClick, 500, true);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //-------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onReminderToggleClick: function (ev) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ var self = this;
+ var $trackLink = $(ev.currentTarget).find('i');
+
+ if (this.reminderOn === undefined) {
+ this.reminderOn = $trackLink.data('reminderOn');
+ }
+
+ var reminderOnValue = !this.reminderOn;
+
+ this._rpc({
+ route: '/event/track/toggle_reminder',
+ params: {
+ track_id: $trackLink.data('trackId'),
+ set_reminder_on: reminderOnValue,
+ },
+ }).then(function (result) {
+ if (result.error && result.error === 'ignored') {
+ self.displayNotification({
+ type: 'info',
+ title: _t('Error'),
+ message: _.str.sprintf(_t('Talk already in your Favorites')),
+ });
+ } else {
+ self.reminderOn = reminderOnValue;
+ var reminderText = self.reminderOn ? _t('Favorite On') : _t('Set Favorite');
+ self.$('.o_wetrack_js_reminder_text').text(reminderText);
+ self._updateDisplay();
+ var message = self.reminderOn ? _t('Talk added to your Favorites') : _t('Talk removed from your Favorites');
+ self.displayNotification({
+ type: 'info',
+ title: message
+ });
+ }
+ if (result.visitor_uuid) {
+ utils.set_cookie('visitor_uuid', result.visitor_uuid);
+ }
+ });
+ },
+
+ _updateDisplay: function () {
+ var $trackLink = this.$el.find('i');
+ var isReminderLight = $trackLink.data('isReminderLight');
+ if (this.reminderOn) {
+ $trackLink.addClass('fa-bell').removeClass('fa-bell-o');
+ $trackLink.attr('title', _t('Favorite On'));
+
+ if (!isReminderLight) {
+ this.$el.addClass('btn-primary');
+ this.$el.removeClass('btn-outline-primary');
+ }
+ } else {
+ $trackLink.addClass('fa-bell-o').removeClass('fa-bell');
+ $trackLink.attr('title', _t('Set Favorite'));
+
+ if (!isReminderLight) {
+ this.$el.removeClass('btn-primary');
+ this.$el.addClass('btn-outline-primary');
+ }
+ }
+ },
+
+});
+
+return publicWidget.registry.websiteEventTrackReminder;
+
+});
diff --git a/addons/website_event_track/static/src/js/service_worker.js b/addons/website_event_track/static/src/js/service_worker.js
new file mode 100644
index 00000000..41ce7e54
--- /dev/null
+++ b/addons/website_event_track/static/src/js/service_worker.js
@@ -0,0 +1,363 @@
+importScripts("/website_event_track/static/lib/idb-keyval/idb-keyval.js");
+
+const PREFIX = "odoo-event";
+const SYNCABLE_ROUTES = ["/event/track/toggle_reminder"];
+const CACHABLE_ROUTES = ["/web/webclient/version_info"];
+const MAX_CACHE_SIZE = 512 * 1024 * 1024; // 500 MB
+const MAX_CACHE_QUOTA = 0.5;
+const CDN_URL = __ODOO_CDN_URL__; // {string|undefined} the cdn_url configured for the website if activated
+
+const { Store, set, get, del } = idbKeyval;
+const pendingRequestsQueueName = `${PREFIX}-pending-requests`;
+const cacheName = `${PREFIX}-cache`;
+const syncStore = new Store(`${PREFIX}-sync-db`, `${PREFIX}-sync-store`);
+const cacheStore = new Store(`${PREFIX}-cache-db`, `${PREFIX}-cache-store`);
+const offlineRoute = `${self.registration.scope}/offline`;
+const scopeURL = new URL(self.registration.scope);
+const cdnURL = CDN_URL ? (CDN_URL.startsWith("http") ? new URL(CDN_URL) : new URL(`http:${CDN_URL}`)) : undefined;
+
+/**
+ *
+ * @param {string} url
+ * @returns {string}
+ */
+const urlPathname = (url) => new URL(url).pathname;
+
+/**
+ *
+ * @param {Array} whitelist
+ * @returns {Function}
+ */
+const canHandleRoutes = (whitelist) => (url) => whitelist.includes(urlPathname(url));
+
+/**
+ *
+ * @param {Request} request
+ * @returns {boolean}
+ */
+const isGET = (request) => request.method === "GET";
+
+/**
+ *
+ * @returns {Function}
+ */
+const isSyncableURL = canHandleRoutes(SYNCABLE_ROUTES);
+
+/**
+ *
+ * @returns {Function}
+ */
+const isCachableURL = canHandleRoutes(CACHABLE_ROUTES);
+
+/**
+ *
+ * @returns {boolean} true if navigator has a quota we can read and we reached it
+ */
+const isCacheFull = async () => {
+ if (!("storage" in navigator && "estimate" in navigator.storage)) {
+ return false;
+ }
+ const { usage, quota } = await navigator.storage.estimate();
+ return usage / quota > MAX_CACHE_QUOTA || usage > MAX_CACHE_SIZE;
+};
+
+/**
+ *
+ * @return {Promise}
+ */
+const fetchToCacheOfflinePage = () => caches.open(cacheName).then((cache) => cache.add(offlineRoute));
+
+/**
+ *
+ * @param {Request} req
+ * @returns {Promise<Object>}
+ */
+const serializeRequest = async (req) => ({
+ url: req.url,
+ method: req.method,
+ headers: Object.fromEntries(req.headers.entries()),
+ body: await req.text(),
+ mode: req.mode,
+ credentials: req.credentials,
+ cache: req.cache,
+ redirect: req.redirect,
+ referrer: req.referrer,
+ integrity: req.integrity,
+});
+
+/**
+ *
+ * @param {Object} requestData
+ * @returns {Request}
+ */
+const deserializeRequest = (requestData) => {
+ const { url } = requestData;
+ delete requestData.url;
+ return new Request(url, requestData);
+};
+
+/**
+ *
+ * @param {Response} res
+ * @returns {Promise<Object>}
+ */
+const serializeResponse = async (res) => ({
+ body: await res.text(),
+ status: res.status,
+ statusText: res.statusText,
+ headers: Object.fromEntries(res.headers.entries()),
+});
+
+/**
+ *
+ * @param {Object} responseData
+ * @returns {Response}
+ */
+const deserializeResponse = (responseData) => {
+ const { body } = responseData;
+ delete responseData.body;
+ return new Response(body, responseData);
+};
+
+/**
+ *
+ * @param {Object} serializedRequest
+ * @returns {string}
+ */
+const buildCacheKey = ({ url, body: { method, params } }) =>
+ JSON.stringify({
+ url,
+ method,
+ params,
+ });
+
+/**
+ *
+ * @returns {int}
+ */
+const uniqueRequestId = () => Math.floor(Math.random() * 1000 * 1000 * 1000);
+
+/**
+ *
+ * @returns {Response}
+ */
+const buildEmptyResponse = () => new Response(JSON.stringify({ jsonrpc: "2.0", id: uniqueRequestId(), result: {} }));
+
+/**
+ *
+ * @param {Request} request
+ * @param {Response} response
+ * @returns {Promise}
+ */
+const cacheRequest = async (request, response) => {
+ // only attempts to cache local or cdn delivered urls
+ const url = new URL(request.url);
+ if (url.hostname !== scopeURL.hostname && (!cdnURL || url.hostname !== cdnURL.hostname)) {
+ console.error(`ignoring cache for ${request.url} => ${url.hostname}, local: ${scopeURL.hostname}, cdn: ${cdnURL ? cdnURL.hostname : cdnURL}`);
+ return;
+ }
+
+ // don't even attempt to cache:
+ // - error pages (why cache that?)
+ // - non-"basic" response types, which include tracker 1-time opaque requests
+ // that are consuming cache space for no reason (namely due to padding MBs accounted for
+ // each opaque request)
+ if (!response || !response.ok || response.type !== "basic") {
+ console.error(`ignoring cache for ${request.url} => ${response.type}, mode: ${request.mode}, cache: ${request.cache}`);
+ return;
+ }
+
+ // never blow up cache quota, as it will break things, and the space
+ // is shared with cookies and localStorage
+ if (await isCacheFull()) {
+ // TODO: clear some part of the cache to free older/less-relevant content
+ console.log("Cache full, not caching!");
+ return;
+ }
+
+ console.log(`grant cache for ${request.url} => ${response.type}, mode: ${request.mode}, cache: ${request.cache},
+ isGet: ${isGET(request)}, isCachable: ${isCachableURL(request.url)}`);
+ if (isGET(request)) {
+ const cache = await caches.open(cacheName);
+ await cache.put(request, response.clone());
+ } else if (isCachableURL(request.url)) {
+ const serializedRequest = await serializeRequest(request);
+ const serializedResponse = await serializeResponse(response.clone());
+ await set(buildCacheKey(serializedRequest), serializedResponse, cacheStore);
+ }
+};
+
+/**
+ *
+ * @param {Request} request
+ * @returns {boolean}
+ */
+const isCachableRequest = (request) => isGET(request) || isCachableURL(request.url);
+
+/**
+ *
+ * @param request
+ * @param requestError
+ * @return {boolean}
+ */
+const isOfflineDocumentRequest = (request, requestError) =>
+ request && requestError && requestError.message === 'Failed to fetch' && (
+ (isGET(request) && request.mode === 'navigate' && request.destination === 'document') ||
+ // request.mode = navigate isn't supported in all browsers => check for http header accept:text/html
+ (request.method === 'GET' && request.headers.get('accept').includes('text/html'))
+ );
+
+/**
+ *
+ * @param {Request} request
+ * @returns {Promise<Response|null>}
+ */
+const matchCache = async (request) => {
+ if (isGET(request)) {
+ const cache = await caches.open(cacheName);
+ const response = await cache.match(request.url);
+ if (response) {
+ return deserializeResponse(await serializeResponse(response.clone()));
+ }
+ }
+ if (isCachableURL(request.url)) {
+ const serializedRequest = await serializeRequest(request);
+ const cachedResponse = await get(buildCacheKey(serializedRequest), cacheStore);
+ if (cachedResponse) {
+ return deserializeResponse(cachedResponse);
+ }
+ }
+ return null;
+};
+
+/**
+ *
+ * @param {FetchEvent} param0
+ * @returns {Promise<Response>}
+ */
+const processFetchRequest = async ({ request }) => {
+ const requestCopy = request.clone();
+ let response;
+ try {
+ response = await fetch(request);
+ await cacheRequest(request, response);
+ } catch (requestError) {
+ if (isCachableRequest(requestCopy)) {
+ try {
+ response = await matchCache(requestCopy);
+ } catch (err) {
+ console.warn("An error occurs when reading the cache request", err);
+ }
+ } else if (isSyncableURL(requestCopy.url)) {
+ const pendingRequests = (await get(pendingRequestsQueueName, syncStore)) || [];
+ const serializedRequest = await serializeRequest(requestCopy);
+ await set(pendingRequestsQueueName, [...pendingRequests, serializedRequest], syncStore);
+ if (self.registration.sync) {
+ await self.registration.sync.register(pendingRequestsQueueName).catch((err) => {
+ console.warn("Cannot use BackgroundSync", err);
+ throw requestError;
+ });
+ }
+ return buildEmptyResponse();
+ } else {
+ console.warn(`Offline ${requestCopy.method} request currently not supported`, requestCopy);
+ }
+
+ if (!response) {
+ if (isOfflineDocumentRequest(request, requestError)) {
+ const cache = await caches.open(cacheName);
+ return await cache.match(offlineRoute);
+ }
+ throw requestError;
+ }
+ }
+ return response;
+};
+
+/**
+ *
+ * @returns {Promise}
+ */
+const processPendingRequests = async () => {
+ const pendingRequests = (await get(pendingRequestsQueueName, syncStore)) || [];
+ if (!pendingRequests.length) {
+ console.info("Nothing to sync!");
+ return;
+ }
+ let pendingRequest;
+ while ((pendingRequest = pendingRequests.shift())) {
+ const request = deserializeRequest(pendingRequest);
+ await fetch(request);
+ await set(pendingRequestsQueueName, pendingRequests, syncStore);
+ }
+};
+
+/**
+ * Add given urls to the Cache, skipping the ones already present
+ * @param {Array<string>} urls
+ */
+const prefetchUrls = async (urls = []) => {
+ const cache = await caches.open(cacheName);
+ const uniqUrls = new Set(urls);
+ for (let url of uniqUrls) {
+ if (await cache.match(url)) {
+ continue;
+ }
+ try {
+ await processFetchRequest({ request: new Request(url) });
+ } catch (error) {
+ console.error(`fail to prefetch ${url} : ${error}`);
+ }
+ }
+};
+
+/**
+ * Handle the message sent to the Worker (using the postMessage() method).
+ * The message is defined by the name of the action to perform and its associated parameters (optional).
+ *
+ * Actions:
+ * - prefetch-pages: add {Array} urls with their "alternative url" to the Cache (if not already present).
+ * - prefetch-assets: add {Array} urls to the Cache (if not already present).
+ *
+ * @param {Object} data
+ * @param {string} data.action action's name
+ * @param {*} data.* action's parameter(s)
+ * @returns {Promise}
+ */
+const processMessage = (data) => {
+ const { action } = data;
+ switch (action) {
+ case "prefetch-pages":
+ const { urls: pagesUrls } = data;
+ // To prevent redirection cached by the browser (cf. 301 Permanently Moved) from breaking the offline cache
+ // we also add alternative urls with the following rule:
+ // * if original url has a trailing "/", adds url with striped trailing "/"
+ // * if original url doesn't end with "/", adds url without the trailing "/"
+ const maybeRedirectedUrl = pagesUrls.map((url) => (url.endsWith("/") ? url.slice(0, -1) : url));
+ return prefetchUrls([...pagesUrls, ...maybeRedirectedUrl]);
+ case "prefetch-assets":
+ const { urls: assetsUrls } = data;
+ return prefetchUrls(assetsUrls);
+ }
+ throw new Error(`Action '${action}' not found.`);
+};
+
+self.addEventListener("fetch", (event) => {
+ event.respondWith(processFetchRequest(event));
+});
+
+self.addEventListener("sync", (event) => {
+ console.info(`Syncing pending requests...`);
+ if (event.tag === pendingRequestsQueueName) {
+ event.waitUntil(processPendingRequests());
+ }
+});
+
+self.addEventListener("message", (event) => {
+ event.waitUntil(processMessage(event.data));
+});
+
+// Precache static resources here. Like offline page
+self.addEventListener('install', (event) => {
+ event.waitUntil(fetchToCacheOfflinePage());
+});
diff --git a/addons/website_event_track/static/src/js/website_event_pwa_widget.js b/addons/website_event_track/static/src/js/website_event_pwa_widget.js
new file mode 100644
index 00000000..2cc986a4
--- /dev/null
+++ b/addons/website_event_track/static/src/js/website_event_pwa_widget.js
@@ -0,0 +1,215 @@
+odoo.define("website_event_track.website_event_pwa_widget", function (require) {
+ "use strict";
+
+ /*
+ * The "deferredPrompt" Promise will resolve only if the "beforeinstallprompt" event
+ * has been triggered. It allows to register this listener as soon as possible
+ * to avoid missed-events (as the browser can trigger it very early in the page lifecycle).
+ */
+ var deferredPrompt = new Promise(function (resolve, reject) {
+ if (!("serviceWorker" in navigator)) {
+ return reject();
+ }
+ window.addEventListener("beforeinstallprompt", function (ev) {
+ ev.preventDefault();
+ resolve(ev);
+ });
+ });
+
+ var config = require("web.config");
+ var publicWidget = require("web.public.widget");
+ var utils = require("web.utils");
+
+ var PWAInstallBanner = publicWidget.Widget.extend({
+ xmlDependencies: ["/website_event_track/static/src/xml/website_event_pwa.xml"],
+ template: "pwa_install_banner",
+ events: {
+ "click .o_btn_install": "_onClickInstall",
+ "click .o_btn_close": "_onClickClose",
+ },
+
+ /**
+ * @private
+ */
+ _onClickClose: function () {
+ this.trigger_up("prompt_close_bar");
+ },
+
+ /**
+ * @private
+ */
+ _onClickInstall: function () {
+ this.trigger_up("prompt_install");
+ },
+ });
+
+ publicWidget.registry.WebsiteEventPWAWidget = publicWidget.Widget.extend({
+ selector: "#wrapwrap.event",
+ custom_events: {
+ prompt_install: "_onPromptInstall",
+ prompt_close_bar: "_onPromptCloseBar",
+ },
+
+ /**
+ *
+ * @override
+ */
+ start: function () {
+ var self = this;
+ return this._super.apply(this, arguments)
+ .then(this._registerServiceWorker.bind(this))
+ .then(function () {
+ // Don't wait for the prompt's Promise as it may never resolve.
+ deferredPrompt.then(self._showInstallBanner.bind(self)).catch(function () {
+ console.log("ServiceWorker not supported");
+ });
+ })
+ .then(this._prefetch.bind(this));
+ },
+
+ /**
+ *
+ * @override
+ */
+ destroy: function () {
+ this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns the PWA's scope
+ *
+ * Note: this method performs a matching to handle URLs with the language prefix.
+ * Typically this prefix is in the form of "en" or "en_US" but it can also be
+ * any string using the customization options in the Website's settings.
+ * @private
+ * @returns {String}
+ */
+ _getScope: function () {
+ var matches = window.location.pathname.match(/^(\/(?:event|[^/]+\/event))\/?/);
+ if (matches && matches[1]) {
+ return matches[1];
+ }
+ return "/event";
+ },
+
+ /**
+ * @private
+ */
+ _hideInstallBanner: function () {
+ this.installBanner ? this.installBanner.destroy() : undefined;
+ $(".o_livechat_button").css("bottom", "0");
+ },
+
+ /**
+ * Parse the current page for first-level children pages and ask the ServiceWorker
+ * to already fetch them to populate the cache.
+ * @private
+ */
+ _prefetch: function () {
+ if (!("serviceWorker" in navigator)) {
+ return;
+ }
+ var assetsUrls = Array.from(document.querySelectorAll('link[rel="stylesheet"], script[src]')).map(function (el) {
+ return el.href || el.src;
+ });
+ navigator.serviceWorker.ready.then(function (registration) {
+ registration.active.postMessage({
+ action: "prefetch-assets",
+ urls: assetsUrls,
+ });
+ }).catch(function (error) {
+ console.error("Service worker ready failed, error:", error);
+ });
+ var currentPageUrl = window.location.href;
+ var childrenPagesUrls = Array.from(document.querySelectorAll('a[href^="' + this._getScope() + '/"]')).map(function (el) {
+ return el.href;
+ });
+ navigator.serviceWorker.ready.then(function (registration) {
+ registration.active.postMessage({
+ action: "prefetch-pages",
+ urls: childrenPagesUrls.concat(currentPageUrl),
+ });
+ }).catch(function (error) {
+ console.error("Service worker ready failed, error:", error);
+ });
+ },
+
+ /**
+ *
+ * @private
+ */
+ _registerServiceWorker: function () {
+ if (!("serviceWorker" in navigator)) {
+ return;
+ }
+ var scope = this._getScope();
+ return navigator.serviceWorker
+ .register(scope + "/service-worker.js", { scope: scope })
+ .then(function (registration) {
+ console.info("Registration successful, scope is:", registration.scope);
+ })
+ .catch(function (error) {
+ console.error("Service worker registration failed, error:", error);
+ });
+ },
+
+ /**
+ * @private
+ */
+ _showInstallBanner: function () {
+ if (!config.device.isMobile) {
+ return;
+ }
+ var self = this;
+ this.installBanner = new PWAInstallBanner(this);
+ this.installBanner.appendTo(this.$el).then(function () {
+ // If Livechat available, It should be placed above the PWA banner.
+ var height = self.$(".o_pwa_install_banner").outerHeight(true);
+ $(".o_livechat_button").css("bottom", height + "px");
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param ev {Event}
+ */
+ _onPromptCloseBar: function (ev) {
+ ev.stopPropagation();
+ this._hideInstallBanner();
+ },
+ /**
+ * @private
+ * @param ev {Event}
+ */
+ _onPromptInstall: function (ev) {
+ ev.stopPropagation();
+ this._hideInstallBanner();
+ deferredPrompt.then(function (prompt) {
+ prompt.prompt();
+ prompt.userChoice.then(function (choiceResult) {
+ if (choiceResult.outcome === "accepted") {
+ console.log("User accepted the install prompt");
+ } else {
+ console.log("User dismissed the install prompt");
+ }
+ });
+ })
+ .catch(function () {
+ console.log("ServiceWorker not supported");
+ });
+ },
+ });
+
+ return {
+ PWAInstallBanner: PWAInstallBanner,
+ WebsiteEventPWAWidget: publicWidget.registry.WebsiteEventPWAWidget,
+ };
+});
diff --git a/addons/website_event_track/static/src/js/website_event_track.js b/addons/website_event_track/static/src/js/website_event_track.js
new file mode 100644
index 00000000..c5868312
--- /dev/null
+++ b/addons/website_event_track/static/src/js/website_event_track.js
@@ -0,0 +1,32 @@
+odoo.define('website_event_track.website_event_track', function (require) {
+'use strict';
+
+var publicWidget = require('web.public.widget');
+
+publicWidget.registry.websiteEventTrack = publicWidget.Widget.extend({
+ selector: '.o_wevent_event',
+ events: {
+ 'input #event_track_search': '_onEventTrackSearchInput',
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onEventTrackSearchInput: function (ev) {
+ ev.preventDefault();
+
+ var text = $(ev.currentTarget).val();
+ var filter = _.str.sprintf(':containsLike(%s)', text);
+
+ $('#search_summary').removeClass('invisible');
+ var $tracks = $('.event_track');
+ $('#search_number').text($tracks.filter(filter).length);
+ $tracks.removeClass('invisible').not(filter).addClass('invisible');
+ },
+});
+});
diff --git a/addons/website_event_track/static/src/scss/event_track_templates.scss b/addons/website_event_track/static/src/scss/event_track_templates.scss
new file mode 100644
index 00000000..6338bdec
--- /dev/null
+++ b/addons/website_event_track/static/src/scss/event_track_templates.scss
@@ -0,0 +1,244 @@
+// small hack to hide sponsors on specific views
+.o_wevent_hide_sponsors .container.mt32.mb16.d-none.d-md-block.d-print-none {
+ // Not a very accurate way to target the 'sponsors' block -> improve in master
+ display: none !important;
+}
+
+/*
+ * EVENT TOOL: REMINDER WIDGET
+ */
+.o_wevent_event .o_wetrack_js_reminder {
+ // Icon only
+ &.btn-link {
+ padding: 0;
+ }
+
+ // Ensure width for size coherency
+ &:not(.btn-link) {
+ min-width: 100px;
+ }
+
+ i {
+ &.fa-bell {
+ color: gold;
+ }
+ &.fa-bell-o {
+ color: black;
+ }
+ }
+}
+
+
+/*
+ * AGENDA
+ */
+.o_we_online_agenda {
+ overflow-x: auto;
+
+ table {
+ border-collapse: separate;
+ border-spacing: 0em 0em;
+ tr {
+ height: 15px;
+ line-height: 1em;
+ &.active {
+ td.active {
+ padding: 0em 0.5em;
+ font-size: smaller;
+ border-top: 1px solid lightgrey;
+ }
+ }
+ }
+ th.active, td:not(.active) {
+ background-color: rgba(211, 211, 211, 0.1);
+ border: 0px;
+ border-right: 1em solid white;
+ vertical-align: middle;
+ span {
+ word-break: break-word;
+ }
+ }
+ th:not(.active), td.active {
+ width: 100px;
+ }
+ th.position-sticky {
+ left: 0;
+ }
+ td {
+ border: 0px;
+
+ @for $size from 1 through 20 {
+ @if #{$size} != 1 {
+ &.o_location_size_#{$size} {
+ width: calc(100% / (#{$size} - 1));
+ min-width: 150px;
+ }
+ } @else {
+ width: calc(100%);
+ }
+ }
+
+ &.active {
+ z-index: 1;
+ position: sticky;
+ left: 0;
+ min-width: 100px;
+ background-color: white;
+ }
+ div.o_we_agenda_card_content {
+ height: 100%;
+ span {
+ cursor: pointer;
+ }
+ .o_we_agenda_card_title, small {
+ word-break: break-word;
+ }
+ }
+ .badge {
+ height: fit-content;
+ max-width: 100%;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ font-size: xx-small;
+ padding: 0.2em 0.5em;
+ border-radius: 1em;
+ line-height: 10px;
+ }
+ &.invisible {
+ visibility: visible !important;
+ opacity: 0.3;
+ }
+ &.o_we_agenda_time_slot_main, &.o_we_agenda_time_slot_half {
+ padding: 0;
+ position: relative;
+ > div {
+ position: absolute;
+ top: 0;
+ width: 100%;
+ }
+ }
+ &.o_we_agenda_time_slot_main > div {
+ padding: 0.3em;
+ border-top: 1px solid lightgrey;
+ }
+ &.o_we_agenda_time_slot_half > div {
+ padding: 0.3em;
+ border-top: 1px dashed lightgrey;
+ }
+ &.event_track {
+ position: relative;
+ padding: 0;
+ &::before {
+ content: "";
+ display: block;
+ width: 100%;
+ position: absolute;
+ top: 0;
+ border-top: 1px solid lightgrey;
+ }
+ > div {
+ padding: 0.3em;
+ }
+
+ }
+ // Remove me in master
+ &.event_color_0 {
+ background-color: rgba(211, 211, 211, 0.5);
+ }
+ &.event_color_1 {
+ background-color: rgba(240, 96, 80, 0.2);
+ }
+ &.event_color_2 {
+ background-color: rgba(244, 164, 96, 0.2);
+ }
+ &.event_color_3 {
+ background-color: rgba(247, 205, 31, 0.2);
+ }
+ &.event_color_4 {
+ background-color: rgba(108,193,237,0.2);
+ }
+ &.event_color_5 {
+ background-color: rgba(129,73,104,0.2);
+ }
+ &.event_color_6 {
+ background-color: rgba(235,126,127,0.2);
+ }
+ &.event_color_7 {
+ background-color: rgba(44,131,151,0.2);
+ }
+ &.event_color_8 {
+ background-color: rgba(71,85,119,0.2);
+ }
+ &.event_color_9 {
+ background-color: rgba(214,20,95,0.2);
+ }
+ &.event_color_10 {
+ background-color: rgba(48,195,129,0.2);
+ }
+ &.event_color_11 {
+ background-color: rgba(147,101,184,0.2);
+ }
+ }
+ }
+}
+
+/*
+ * EVENT TOOL: SPONSOR WIDGET
+ */
+.o_wevent_event .o_wevent_sponsor_card {
+ position: relative;
+ width: 100px;
+ height: 100px;
+ border: 1px solid $gray-200;
+ background-color: $white;
+ margin: 0 -1px -1px 0;
+
+ .ribbon-wrapper {
+ width: 50px;
+ height: 50px;
+ }
+
+ .ribbon {
+ font: bold 10px Sans-Serif;
+ padding: 2px 0;
+ top: 10px;
+ width: 70px;
+ background-image: none; // not needed if colors for ribbons (Gold,Silver,Bronze) is removed in website_event_track.css
+
+ &.ribbon_Gold {
+ background-color: #e3aa24;
+ color:$white;
+ }
+
+ &.ribbon_Silver {
+ background-color: #adb5bd;
+ color: $white;
+ }
+
+ &.ribbon_Bronze {
+ background-color: #c7632a;
+ color: $white;
+ }
+ }
+
+ &:before {
+ content: "";
+ display: block;
+ @include o-position-absolute(0,0,0,0);
+ background-color: $black;
+ opacity: 0;
+ transition: opacity .3s;
+ }
+
+ &:hover:before {
+ opacity:.1;
+ }
+}
+
+/*
+ * EVENT TOOL: DATE
+ */
+.o_wevent_event .o_we_track_day_header > div > span.h1 {
+ font-size: xx-large;
+}
diff --git a/addons/website_event_track/static/src/scss/event_track_templates_online.scss b/addons/website_event_track/static/src/scss/event_track_templates_online.scss
new file mode 100644
index 00000000..170bc4c8
--- /dev/null
+++ b/addons/website_event_track/static/src/scss/event_track_templates_online.scss
@@ -0,0 +1,89 @@
+.o_wesession_index {
+
+ /*
+ * COMMON
+ */
+
+ .o_wesession_gradient {
+ background-image: linear-gradient(120deg, #875A7B, darken(#875A7B, 10%));
+ opacity: 0.8;
+ }
+
+ /*
+ * MAIN PAGE: LIST
+ */
+
+ // Track card
+ .o_wesession_track_card {
+ .card-body {
+ padding: 1rem;
+ }
+
+ .card-footer {
+ padding: 0.75rem 1rem;
+ }
+
+ .o_wesession_track_card_header_badge {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ padding: $card-spacer-y $card-spacer-x;
+ text-align: right;
+ }
+ &.o_wesession_track_card_unpublished {
+ opacity: 0.8;
+ }
+ }
+
+ /*
+ * TRACK PAGE: VIEW
+ */
+
+ .o_wevent_online_page_container {
+ // Main panel: current track
+ .o_wesession_track_main {
+ // Force side panel min-width to account for potential Youtube chat
+ // And adapt main panel max-width accordingly.
+ @include media-breakpoint-up(md) {
+ max-width: calc(100% - 22rem);
+ }
+
+ @media screen and (min-width: 1900px) {
+ // return to bootstrap value for col-lg-9 / 10 breakpoint if screen is big enough
+ &.col-lg-9 {
+ max-width: 75%;
+ }
+
+ &.col-lg-10 {
+ max-width: 83.33333333%;
+ }
+ }
+ }
+
+ // Left panel: other tracks
+ .o_wesession_track_aside {
+ // Force side panel min-width to account for potential Youtube chat
+ @include media-breakpoint-up(md) {
+ min-width: 22rem;
+ }
+
+ @media screen and (min-width: 1900px) {
+ min-width: auto;
+ }
+
+ // Navigation: keel layout simple
+ .o_wesession_track_aside_nav {
+ .nav-link {
+ background-color: transparent;
+ border: 0;
+ color: $gray-600;
+
+ &.active {
+ color: $gray-800;
+ font-weight: 500;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/addons/website_event_track/static/src/scss/pwa_frontend.scss b/addons/website_event_track/static/src/scss/pwa_frontend.scss
new file mode 100644
index 00000000..f1328902
--- /dev/null
+++ b/addons/website_event_track/static/src/scss/pwa_frontend.scss
@@ -0,0 +1,35 @@
+@media all and (display-mode: standalone) {
+ #wrapwrap.event {
+ #top,
+ #bottom {
+ display: none;
+ }
+ }
+}
+
+#wrapwrap.event {
+ .o_pwa_install_banner {
+ position: fixed;
+ display: flex;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ margin: 0;
+ justify-content: space-between;
+ align-items: center;
+ background-color: rgba(255, 255, 255, 1);
+ border-top: 1px solid $gray-200;
+ box-shadow: 0 -0.125rem 0.25rem rgba(0, 0, 0, 0.075);
+ font-size: 1rem;
+ z-index: $zindex-tooltip;
+
+ .o_btn_close {
+ margin-left: 0;
+ margin-right: auto;
+ }
+
+ .o_btn_install {
+ margin-left: auto;
+ }
+ }
+}
diff --git a/addons/website_event_track/static/src/xml/website_event_pwa.xml b/addons/website_event_track/static/src/xml/website_event_pwa.xml
new file mode 100644
index 00000000..1105c4b3
--- /dev/null
+++ b/addons/website_event_track/static/src/xml/website_event_pwa.xml
@@ -0,0 +1,11 @@
+<templates>
+
+ <t t-name="pwa_install_banner">
+ <div class="alert alert-default o_pwa_install_banner">
+ <button class="btn o_btn_close">x</button>
+ <strong>Install Application</strong>
+ <button class="btn btn-primary o_btn_install">Install</button>
+ </div>
+ </t>
+
+</templates>