diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/website_event_track/static | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/website_event_track/static')
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 Binary files differnew file mode 100644 index 00000000..45da5099 --- /dev/null +++ b/addons/website_event_track/static/description/blog.png diff --git a/addons/website_event_track/static/description/event.png b/addons/website_event_track/static/description/event.png Binary files differnew file mode 100644 index 00000000..1bc349f3 --- /dev/null +++ b/addons/website_event_track/static/description/event.png diff --git a/addons/website_event_track/static/description/icon.png b/addons/website_event_track/static/description/icon.png Binary files differnew file mode 100644 index 00000000..c9e5ecb0 --- /dev/null +++ b/addons/website_event_track/static/description/icon.png 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 & 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 Binary files differnew file mode 100644 index 00000000..63fb9331 --- /dev/null +++ b/addons/website_event_track/static/description/sponsor.png diff --git a/addons/website_event_track/static/description/tracks.png b/addons/website_event_track/static/description/tracks.png Binary files differnew file mode 100644 index 00000000..b74a144b --- /dev/null +++ b/addons/website_event_track/static/description/tracks.png 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 Binary files differnew file mode 100644 index 00000000..32aad675 --- /dev/null +++ b/addons/website_event_track/static/src/img/event_sponsor_default_0.jpeg 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 Binary files differnew file mode 100644 index 00000000..d7c19533 --- /dev/null +++ b/addons/website_event_track/static/src/img/event_track_default_0.jpeg 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 Binary files differnew file mode 100644 index 00000000..5269b0b2 --- /dev/null +++ b/addons/website_event_track/static/src/img/event_track_default_1.jpeg 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> |
