diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..ee8a16d --- /dev/null +++ b/.htaccess @@ -0,0 +1,106 @@ +# ------------------------------------------------------------------------------------------ +# +# ### PWA Bunga! .htaccess +# +# This htaccess configuration file is used by the Apache web server, +# it improves performance, security and manages redirects +# +# Documentation of .htaccess +# -------------------------- +# https://pwabunga.com/documentation/starter.html#htaccess +# +# ------------------------------------------------------------------------------------------ + +# ------------------------------------------------------------------------------------------ +# +# ### PERFORMANCE +# +# ------------------------------------------------------------------------------------------ + +# ---------------------------------------------------------------------- +# ## Cache-Control Headers +# ---------------------------------------------------------------------- + + + + Header set Cache-Control "max-age=2592000, public" + + + Header set Cache-Control "max-age=2592000, private" + + + Header set Cache-Control "max-age=7200, public" + + + Header unset Cache-Control + + + +# ---------------------------------------------------------------------- +# ## Deflate Outpout filters +# ---------------------------------------------------------------------- + +SetOutputFilter DEFLATE +AddOutputFilterByType DEFLATE "application/atom+xml" "application/javascript" "application/json" "application/ld+json" "application/manifest+json" "application/rdf+xml" "application/rss+xml" "application/schema+json" "application/vnd.geo+json" "application/vnd.ms-fontobject" "application/x-font-ttf" "application/x-javascript" "application/x-web-app-manifest+json" "application/xhtml+xml" "application/xml" "font/eot" "font/opentype" "image/bmp" "image/svg+xml" "image/vnd.microsoft.icon" "image/x-icon" "text/cache-manifest" "text/css" "text/html" "text/javascript" "text/plain" "text/vcard" "text/vnd.rim.location.xloc" "text/vtt" "text/x-component" "text/x-cross-domain-policy" "text/xml" + +# ------------------------------------------------------------------------------------------ +# +# ### SECURITY +# +# ------------------------------------------------------------------------------------------ + +# ---------------------------------------------------------------------- +# ## http:// to https:// +# ---------------------------------------------------------------------- + + + RewriteCond %{HTTPS} off + RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + + + +# ---------------------------------------------------------------------- +# ## Disables directory indexing +# ---------------------------------------------------------------------- + +Options All -Indexes + +# ---------------------------------------------------------------------- +# ## Protect hidden files from being viewed +# ---------------------------------------------------------------------- + + + order allow,deny + deny from all + + +# ------------------------------------------------------------------------------------------ +# +# ### Redirect +# +# ------------------------------------------------------------------------------------------ + +# ---------------------------------------------------------------------- +# ## Errors redirect +# ---------------------------------------------------------------------- + +ErrorDocument 403 https://demo.pwabunga.com/403.html +ErrorDocument 404 https://demo.pwabunga.com/404.html + +# ---------------------------------------------------------------------- +# ## domain.com to www.domain.com +# ---------------------------------------------------------------------- + +# +# RewriteCond %{HTTP_HOST} !^www\..+$ [NC] +# RewriteRule ^ https://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L] +# + +# ---------------------------------------------------------------------- +# ## www.domain.com to domain.com +# ---------------------------------------------------------------------- + + + RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC] + RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L] + \ No newline at end of file diff --git a/403.html b/403.html new file mode 100644 index 0000000..94912ba --- /dev/null +++ b/403.html @@ -0,0 +1,54 @@ + + + + + + + + + + 403 Forbidden + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

403 Forbidden

+ +

The web page you are trying to open in your browser is a resource that you are not authorized to access.

+ + + + + + + + \ No newline at end of file diff --git a/404.html b/404.html new file mode 100644 index 0000000..ec7c599 --- /dev/null +++ b/404.html @@ -0,0 +1,54 @@ + + + + + + + + + + 404 Page Not Found + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

404 Page not found

+ +

The web page you are trying to access could not be found on the website server.

+ + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 176e237..d1dc3ef 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,71 @@ -# template -A modern, modular and intuitive front-end template for fast and efficient development of websites and Progressive Web Apps +
+ logo PWA Bunga! +
+ +# PWA Bunga! + +Welcome to the PWA Bunga! project, a modern, modular and intuitive front-end template for fast and efficient development of websites and Progressive Web Apps. + +Whether you're a beginner or an experienced developer, this front-end template will help you save time and focus on creating specific features for your site or application, rather than setting up the basic architecture. + +* The project's website: **[pwabunga.com](https://pwabunga.com/)** + + +  + +## Contents of PWA Bunga! + +This front-end template is designed in a modular way and consists of three main modules: Starter, CSS, and PWA. + +### PWA Bunga! Starter + +The Starter module provides the basic files and folders to quickly start a website or PWA project. This includes resource folders, an HTML template, 404 and 403 error pages, an htaccess file, favicons in .ico, .svg or .png formats, as well as empty CSS and JS files that can be filled according to your needs. + +### PWA Bunga! CSS + +The CSS module includes a base CSS file with best practices, HTML element normalization, ergonomic improvements, and reset to improve consistency and compatibility across browsers, as well as to make the developer's work easier. + +### PWA Bunga! PWA + +The PWA module provides files and instructions to transform your website into a Progressive Web App (PWA). This includes the webmanifest file that contains application metadata, icons for the home screen, the service worker file that allows the application to work offline, a JavaScript file to enhance the user experience, and an optional CSS file to customize the user interface. + +  + +## Getting started with PWA Bunga! + +### Old school way + +* Download the PWA Bunga! front-end template zip: [Download PWA Bunga! 1.0](https://pwabunga.com/assets/download/pwabunga-v1-0.zip) +* Open your favorite code editor +* Customize the template to fit your project! + +  + +## Documentation + +You can find the documentation on the PWA Bunga! website: + +* [Documentation of PWA Bunga! on the PWA Bunga! website[EN]](https://pwabunga.com/documentation/)[[FR]](https://pwabunga.com/fr/documentation/) + +Or on the PWA Bunga! documentation repository: + +* [Documentation of PWA Bunga![EN]](https://github.com/PwaBunga/documentation)[[FR]](https://github.com/PwaBunga/documentation/blob/main/fr/) + +  + +## Contribute + +To contribute, you can use the issue trackers of the PWA Bunga! module repositories. + +* [https://github.com/PwaBunga/starter/issues](https://github.com/PwaBunga/starter/issues) +* [https://github.com/PwaBunga/css/issues](https://github.com/PwaBunga/css/issues) +* [https://github.com/PwaBunga/pwa/issues](https://github.com/PwaBunga/pwa/issues) + +Changes made to these 3 repositories will then be applied to the main project repository. + +The issue tracker is the preferred channel for bug reports, features requests and submitting pull requests, but please respect the following restrictions: + +* Please **do not** use the issue tracker for personal support requests. +* Please **do not** derail or troll issues. Keep the discussion on topic and= respect the opinions of others. + +  diff --git a/assets/css/pwabunga.css b/assets/css/pwabunga.css new file mode 100644 index 0000000..9a9d544 --- /dev/null +++ b/assets/css/pwabunga.css @@ -0,0 +1,552 @@ +/*! pwabunga.css 1.0 | 2023 MIT Licence | github.com/Effeilo/pwabunga.css */ + +/* ---------------------------------------------------------------------------- + + ### PWA Bunga! CSS + + This file is a basic CSS file with best practices, normalization of + HTML elements, ergonomic improvements and reset to improve consistency + and compatibility between browsers, as well as to facilitate the work + of the developer. + + Documentation of pwabunga.css + -------------------------------- + https://pwabunga.com/documentation/css.html + +---------------------------------------------------------------------------- */ + +/* ---------------------------------------------------------------------------- + ## Scroll Behavior +---------------------------------------------------------------------------- */ + +/** + * 1. If the user has not requested the system to minimize the amount + of animation or movement. + @see https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion + * 2. Defines a smooth scroll effect to the scrolling behavior for a scrolling box, + when scrolling happens due to navigation or CSSOM scrolling APIs. + @see https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior + */ + +@media (prefers-reduced-motion: no-preference) { /* 1 */ + :root { + scroll-behavior: smooth; /* 2 */ + } +} + +/* ---------------------------------------------------------------------------- + ## Universal inheritance +---------------------------------------------------------------------------- */ + +/** + * Makes irritable the `box sizing` property for all elements. + @for All modern browsers. + @see www.paulirish.com/2012/box-sizing-border-box-ftw/ + */ + +*, +::before, +::after { + box-sizing: inherit; +} + +/* ---------------------------------------------------------------------------- + ## Selection +---------------------------------------------------------------------------- */ + +/** + * Remove text-shadow in selection highlight. + * 1. @for Firefox 2+, Firefox for Android 33+. + * 2. @for Chrome 4+, Safari 3.1+, and Android 4.4+. + @see https://twitter.com/miketaylr/status/12228805301 + */ + +::-moz-selection { + text-shadow: none; /* 1 */ +} + +::selection { + text-shadow: none; /* 2 */ +} + +/* ---------------------------------------------------------------------------- + ## The root element +---------------------------------------------------------------------------- */ + +/** + * 1. Changing the dimensioning of the box model for the `html` element. + * 2. Improve consistency of default fonts in all browsers. + @see https://github.com/sindresorhus/modern-normalize/issues/3 + * 3. Relative value unit refers to the size of the parent element. + Browsers font size default is 16px. For a 1em/10px report, + we divide the size by the default size: 10/16 x 100 = 62.5%. + * 4. Force scrollbars to always be visible to prevent awkward jumps when + navigating between pages that do/do not have enough content to produce + scrollbars naturally. + @see https://css-tricks.com/snippets/css/force-vertical-scrollbar/ + * 5. Displays auto-hiding scrollbars during mouse interactions + and panning indicators during touch and keyboard interactions. + @for Edge 12+ + * 6. Fonts on OSX will look more consistent with other systems that do not + render text using sub-pixel anti-aliasing. + @for OSX. + * 7. Remove the highlighting effect when "tapped" action on webkit. + @for Androids. + * 8. @for some Androids. + @see https://phonegap-tips.com/articles/essential-phonegap-css-webkit-tap-highlight-color.html + * 9. Prevent iOS text size adjust after orientation change, without disabling + user zoom. + @for iOS Safari 5.1+. + * 10. Use a more readable tab size. + */ + +html { + box-sizing: border-box; /* 1 */ + font-family: system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; /* 2 */ + font-size: 62.5%; /* 3 */ + overflow-y: scroll; /* 4 */ + -ms-overflow-style: -ms-autohiding-scrollbar; /* 5 */ + -moz-osx-font-smoothing: grayscale; /* 6 */ + -webkit-font-smoothing: antialiased; /* 6 */ + -webkit-tap-highlight-color: rgba(0,0,0,0); /* 7 */ + -webkit-tap-highlight-color: transparent; /* 8 */ + -webkit-text-size-adjust: 100%; /* 9 */ + tab-size: 4; /* 10 */ + -moz-tab-size: 4; /* 10 */ +} + +/* ---------------------------------------------------------------------------- + ## Sections +---------------------------------------------------------------------------- */ + +/** + * Reset margins and paddings. + */ + +body, section, nav, article, aside, +h1, h2, h3, h4, h5, h6, header, footer, address { + margin: 0; + padding: 0; +} + +/* ---------------------------------------------------------------------------- + ## Grouping content +---------------------------------------------------------------------------- */ + +/** + * Prevents certain content from spilling their container. + */ + +blockquote, +pre { + max-width: 100%; +} + +/** + * Removes list style. + */ + +ol, ul { + list-style: none; +} + +/** + * Reset margins and paddings. + */ + +p, hr, pre, blockquote, ol, ul, li, dl, dt, dd, figure, div { + margin: 0; + padding: 0; +} + +/** + * Improve consistency of default fonts in all browsers. + @see https://github.com/sindresorhus/modern-normalize/issues/3 + */ + +pre { + font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; +} + +/* ---------------------------------------------------------------------------- + ## Text-level semantics +---------------------------------------------------------------------------- */ + +/** + * 1. Removes text-decoration and outline to links + * 2. Removes delay from tapping on clickable elements. + @see https://www.sitepoint.com/5-ways-prevent-300ms-click-delay-mobile-devices/ + * 3. Removes gaps in links underline in iOS 8+ and Safari 8+. + */ + +a { + outline: 0; /* 1 */ + text-decoration: none; /* 1 */ + touch-action: manipulation; /* 2 */ + -webkit-text-decoration-skip: objects; /* 3 */ +} + +/** + * Add the correct text decoration in Chrome, Edge and Safari. + */ + +abbr[title] { + text-decoration: underline dotted; +} + +/** + * Give a help cursor to elements that give extra info on `:hover`. + */ + +abbr[title], +dfn[title] { + cursor: help; +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Display the text as it was written and line breaks so that + the text does not leave the block. + * 2. Prevents certain content from spilling their container. + */ + +code { + white-space: pre-wrap; /* 1 */ + max-width: 100%; /* 2 */ +} + +/** + * Improve consistency of default fonts in all browsers. + @see https://github.com/sindresorhus/modern-normalize/issues/3 + */ + +code, +kbd, +samp { + font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* ---------------------------------------------------------------------------- + ## Embedded content +---------------------------------------------------------------------------- */ + +/** + * Removes delay from tapping on clickable elements. + @see https://www.sitepoint.com/5-ways-prevent-300ms-click-delay-mobile-devices/ + */ + +area { + touch-action: manipulation; +} + +/** + * Remove the gap between audio, canvas, iframes, + images, videos and the bottom of their containers. + @see https:////github.com/h5bp/html5-boilerplate/issues/440 + */ + +audio, +canvas, +iframe, +img, +svg, +video { + vertical-align: middle; +} + +/** + * Reset margins and paddings. + */ + +iframe, embed, object, param, video { + margin: 0; + padding: 0; +} + +/** + * Preserves the aspect ratio. + */ + +img { + height: auto; +} + +/** + * Disable highlight on image when select and drag it with the mouse. + * 1. @for Firefox 2+, Firefox for Android 33+. + * 2. @for Chrome 4+, Safari 3.1+, Opera 9.6+, Android 4.4+. + @see https://stackoverflow.com/questions/6816080/how-to-disable-highlight-on-a-image + */ + +img::-moz-selection { /* 1 */ + background-color: transparent; +} + +img::selection { /* 2 */ + background-color: transparent; +} + +/** + * Prevents certain content from spilling their container. + */ + +img, +svg, +video { + max-width: 100%; +} + +/** + * Change the fill color to match the text color in all browsers. + */ + +svg { + fill: currentColor; +} + +/* ---------------------------------------------------------------------------- + ## Tabular data +---------------------------------------------------------------------------- */ + +/** + * 1. Remove text indentation from table contents in Chrome and Safari. + https://bugs.chromium.org/p/chromium/issues/detail?id=999088 + https://bugs.webkit.org/show_bug.cgi?id=201297 + * 2. Correct table border color inheritance in Chrome and Safari. + https://bugs.chromium.org/p/chromium/issues/detail?id=935729 + https://bugs.webkit.org/show_bug.cgi?id=195016 + */ + +table { + text-indent: 0; /* 1 */ + border-color: inherit; /* 2 */ +} + +/** + * Reset margins and paddings. + */ + +table, caption, tr, td, th { + margin: 0; + padding: 0; +} + +/** + * Prevents certain content from spilling their container. + */ + +table, +td { + max-width: 100%; +} + +/* ---------------------------------------------------------------------------- + ## Forms +---------------------------------------------------------------------------- */ + +/** + * Remove the tapping delay from clickable elements. + @see https://www.sitepoint.com/5-ways-prevent-300ms-click-delay-mobile-devices/ + */ + +button, +input, +label, +select, +textarea { + touch-action: manipulation; +} + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ +} + +/** + * Remove the inheritance of text transform in Edge and Firefox. + */ + +button, +select { + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Gives a pointer cursor to clickable forms elements. + */ + +[type=button]:not(:disabled), +[type=reset]:not(:disabled), +[type=submit]:not(:disabled), +button:not(:disabled), +label[for], +select { + cursor: pointer; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * Reset margins and paddings. + */ + +form, button, input, label, select, textarea { + margin: 0; + padding: 0; +} + +/** + * Prevents certain content from spilling their container. + */ + +input, +textarea { + max-width: 100%; +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Allow only vertical resizing of textareas. + @for Firefox 5+, Chrome 4+, Safari 4+, opera 12.1+. + */ + +textarea { + resize: vertical; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Remove the additional ':invalid' styles in Firefox. + @see https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737 + */ + +:-moz-ui-invalid { + box-shadow: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Safari. + */ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* ---------------------------------------------------------------------------- + ## Interactive +---------------------------------------------------------------------------- */ + +/** + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} \ No newline at end of file diff --git a/assets/css/styles.css b/assets/css/styles.css new file mode 100644 index 0000000..e69de29 diff --git a/assets/fonts/.gitignore b/assets/fonts/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/assets/fonts/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/assets/images/.gitignore b/assets/images/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/assets/images/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/assets/js/scripts.js b/assets/js/scripts.js new file mode 100644 index 0000000..e69de29 diff --git a/favicon-16.png b/favicon-16.png new file mode 100644 index 0000000..a3d4695 Binary files /dev/null and b/favicon-16.png differ diff --git a/favicon-32.png b/favicon-32.png new file mode 100644 index 0000000..32b405c Binary files /dev/null and b/favicon-32.png differ diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..ab40d76 Binary files /dev/null and b/favicon.ico differ diff --git a/favicon.svg b/favicon.svg new file mode 100644 index 0000000..a25e0cc --- /dev/null +++ b/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..3b432fd --- /dev/null +++ b/index.html @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + close + + +

+ Your PWA + Get our free app. It won't take up space on your phone. +

+ + +
+ + +
+

+ The app has been successfully installed! + The icon will appear shortly on your home screen +

+ + + + close + + +
+ + +
+ + + + close + + +

+ New version available! +

+ + +
+ + +
+ + + + +
+ + +
+
+ +
+

Version

+

+ + + + +

+
+ +
+

Permissions

+
    + +
  • + + + I authorize to receive notifications +
  • + +
  • + + + I authorize geolocation +
  • +
+ +
+

To remove permission, please go to your device or browser settings

+
+ +
+ + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + \ No newline at end of file diff --git a/pwa/app.webmanifest b/pwa/app.webmanifest new file mode 100644 index 0000000..aa78fae --- /dev/null +++ b/pwa/app.webmanifest @@ -0,0 +1,38 @@ +{ + "short_name": "your Short App Name", + "name": "your App Name", + "description": "your App description", + "id": "?homescreen=1", + "start_url": "../?utm_source=homescreen", + "scope": "../", + "display": "standalone", + "orientation": "portrait", + "theme_color": "#ffffff", + "background_color": "#ffffff", + "icons": [ + { + "src": "icons/icon-192.png", + "type": "image/png", + "sizes": "192x192", + "purpose": "any" + }, + { + "src": "icons/icon-maskable-192.png", + "type": "image/png", + "sizes": "192x192", + "purpose": "maskable" + }, + { + "src": "icons/icon-512.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "any" + }, + { + "src": "icons/icon-maskable-512.png", + "type": "image/png", + "sizes": "512x512", + "purpose": "maskable" + } + ] +} \ No newline at end of file diff --git a/pwa/css/pwabunga-ui.css b/pwa/css/pwabunga-ui.css new file mode 100644 index 0000000..3c132e6 --- /dev/null +++ b/pwa/css/pwabunga-ui.css @@ -0,0 +1,573 @@ +/* ---------------------------------------------------------------------------- + + ### PWA Bunga! UI + + This CSS file is an example file of styles to format the user + interface elements offered by the functions of PWA Bunga! + present on the pwabunga.js file. + + Documentation of pwabunga-ui.css + -------------------------------- + https://pwabunga.com/documentation/pwa/css-ui.html + +---------------------------------------------------------------------------- */ + +/* ---------------------------------------------------------------------------- + + ### PWA Bunga! UI variables + +---------------------------------------------------------------------------- */ + +:root { + --pwa-color-primary: #5d00d8; + --pwa-color-primary-opacity: rgba(93, 0, 216, 0.9); + --pwa-color-secondary: #dfcdf7; + --pwa-color-secondary-opacity: rgba(223, 205, 247, 0.9); + --pwa-color-light: #fff; + --pwa-color-dark: #000; + --pwa-color-gray: #ccc; + --pwa-box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px; + --z-index-1: 1; + --z-index-2: 2; +} + +/* ---------------------------------------------------------------------------- + + ### PWA Bunga! UI Components + +---------------------------------------------------------------------------- */ + +/* ---------------------------------------------------------------------------- + ## PWA UI Button +---------------------------------------------------------------------------- */ + +.pwa-btn { + background: var(--pwa-color-light); + border: none; + border-radius: 20px; + color: var(--pwa-color-primary); + display: inline-block; + padding: 8px 20px; + text-decoration: none; + font-weight: bold; +} + +.pwa-btn:active, +.pwa-btn:hover { + background: var(--pwa-color-secondary); + color: var(--pwa-color-primary); +} + +/* + # PWA UI Button primary +------------------------ */ + +.pwa-btn.pwa-btn-primary { + background: var(--pwa-color-primary); + color: var(--pwa-color-light); +} + +.pwa-btn.pwa-btn-primary:active, +.pwa-btn.pwa-btn-primary:hover { + background: var(--pwa-color-secondary); + color: var(--pwa-color-primary); +} + +/* + # PWA UI Button secondary +-------------------------- */ + +.pwa-btn.pwa-btn-secondary { + background: var(--pwa-color-secondary); + color: var(--pwa-color-primary); +} + +.pwa-btn.pwa-btn-secondary:active, +.pwa-btn.pwa-btn-secondary:hover { + background: var(--pwa-color-primary); + color: var(--pwa-color-light); +} + +/* + # PWA UI Button dark +--------------------- */ + +.pwa-btn.pwa-btn-dark { + background: var(--pwa-color-dark); + color: var(--pwa-color-light); +} + +.pwa-btn.pwa-btn-dark:active, +.pwa-btn.pwa-btn-dark:hover { + background: var(--pwa-color-primary); + color: var(--pwa-color-light); +} + +/* ---------------------------------------------------------------------------- + ## PWA UI Banner +---------------------------------------------------------------------------- */ + +.pwa-banner { + align-items: center; + background: var(--pwa-color-primary); + color: var(--pwa-color-light); + display: flex; + justify-content: space-between; + left: 0; + opacity: 0; + padding: 10px; + position: fixed; + right: 0; + top: 0; + transform: translate(0, -100%); + transition: all 0.2s ease-in-out; + width: 100%; + z-index: var(--z-index-2); +} + +/* + # PWA UI Banner secondary theme +-------------------------------- */ + +.pwa-banner.pwa-banner-secondary { + background: var(--pwa-color-secondary); + color: var(--pwa-color-dark); +} + +/* + # PWA UI Banner close button +----------------------------- */ + +.pwa-banner-close-btn { + cursor: pointer; + min-width: 32px; +} + +/* + # PWA UI Banner content +------------------------ */ + +.pwa-banner-content { + flex-grow: 1; + padding: 0 10px; +} + +/* + # PWA UI Banner title +---------------------- */ + +.pwa-banner-title { + display: block; + font-weight: bold; +} + +/* + # PWA UI Banner is-visible state +--------------------------------- */ + +.pwa-banner.is-visible { + opacity: 1; + transform: translate(0, 0); +} + +/* ---------------------------------------------------------------------------- + ## PWA UI Banner to Snackbar (desktop) +---------------------------------------------------------------------------- */ + +@media only screen and (min-width : 1024px) { + + .pwa-banner.to-pwa-snackbar { + border-radius: 10px; + bottom: 20px; + height: 100px; + left: 20px; + max-width: 400px; + padding: 0 20px; + right: auto; + top: auto; + transform: translate(calc(-100% - 20px), 0); + } + + /* + # PWA UI Snackbar top left + --------------------------- */ + + .pwa-banner.to-pwa-snackbar.top-left { + bottom: auto; + left: 20px; + right: auto; + top: 20px; + } + + /* + # PWA UI Snackbar top right + ---------------------------- */ + + .pwa-banner.to-pwa-snackbar.top-right { + bottom: auto; + left: auto; + right: 20px; + top: 20px; + transform: translate(0, calc(-100% - 20px)); + } + + /* + # PWA UI Snackbar bottom left + ------------------------------ */ + + .pwa-banner.to-pwa-snackbar.bottom-left { + bottom: 20px; + left: 20px; + right: auto; + top: auto; + } + + /* + # PWA UI Snackbar bottom right + ------------------------------- */ + + .pwa-banner.to-pwa-snackbar.bottom-right { + bottom: 20px; + left: auto; + right: 20px; + top: auto; + transform: translate(0, calc(100% + 20px)); + } + +} + +/* + # PWA UI Banner to Snackbar is-visible state +--------------------------------------------- */ + +.pwa-banner.to-pwa-snackbar.is-visible { + opacity: 1; + transform: translate(0, 0); +} + +/* ---------------------------------------------------------------------------- + ## PWA UI Modal +---------------------------------------------------------------------------- */ + +.pwa-modal { + align-items: center; + background: var(--pwa-color-light); + bottom: 0; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + left: 0; + overflow-y: auto; + position: fixed; + right: 0; + text-align: left; + top: 0; + transform: translateY(-100%); + transition: transform 0.25s ease-in-out; + width: 100%; + z-index: var(--z-index-2); +} + +@media only screen and (min-width : 1024px) { + .pwa-modal { + background: var(--pwa-color-primary-opacity); + } +} + +/* + # PWA UI Modal inner +--------------------- */ + +.pwa-modal-inner { + padding: 0 20px; +} + +@media only screen and (min-width : 1024px) { + .pwa-modal-inner { + padding: 20px; + background: #fff; + position: relative; + border-radius: 20px; + } +} + +/* + # PWA UI Modal title +--------------------- */ + +.pwa-modal-title { + font-size: 2rem; + font-weight: bold; + margin-bottom: 25px; + margin-top: 0; +} + +/* + # PWA UI Modal close button +---------------------------- */ + +.pwa-modal-close { + background: none; + border: none; + padding: 0; + position: absolute; + right: 10px; + top: 10px; +} + +/* + # PWA UI Modal close btn icon +------------------------------ */ + +.pwa-modal-close-icon { + fill: var(--pwa-color-primary); + width: 40px; +} + +/* + # PWA UI Modal is-visible state +-------------------------------- */ + +.pwa-modal.is-visible { + transform: translateY(0); +} + +/* ---------------------------------------------------------------------------- + ## PWA UI Switch +---------------------------------------------------------------------------- */ + +.pwa-switch { + background: none; + border: none; + display: inline-block; + height: 34px; + position: relative; + width: 60px; +} + +/* + # PWA UI Switch slider +----------------------- */ + +.pwa-switch-slider { + background-color: var(--pwa-color-gray); + border-radius: 34px; + bottom: 0; + cursor: pointer; + height: 34px; + left: 0; + position: absolute; + right: 0; + top: 0; + transition: .4s; + width: 60px; +} + +.pwa-switch-slider::before { + background-color: var(--pwa-color-light); + border-radius: 50%; + bottom: 4px; + content: ""; + height: 26px; + left: 4px; + position: absolute; + transition: .4s; + width: 26px; +} + +/* + # PWA UI Switch slider is-active state +------------------------------------ */ + +.is-active .pwa-switch-slider { + background-color: var(--pwa-color-primary); +} + +.is-active .pwa-switch-slider::before { + transform: translateX(26px); +} + +/* ---------------------------------------------------------------------------- + ## PWA UI Alert +---------------------------------------------------------------------------- */ + +.pwa-alert { + align-items: center; + background-color: var(--pwa-color-secondary-opacity); + bottom: 0; + display: flex; + flex-direction: column; + height: 100%; + justify-content: center; + left: 0; + overflow-y: auto; + position: fixed; + right: 0; + text-align: left; + top: 0; + transform: translateY(-100%); + transition: transform 0.25s ease-in-out; + width: 100%; + z-index: var(--z-index-2); +} + +/* + # PWA UI Alert inner +--------------------- */ + +.pwa-alert-inner { + background: #fff; + border-radius: 20px; + box-shadow: var(--pwa-box-shadow); + max-width: 320px; + padding: 20px; + text-align: center; +} + +/* + # PWA UI Alert is-visible state +-------------------------------- */ + +.pwa-alert.is-visible { + transform: translateY(0); +} + +/* ---------------------------------------------------------------------------- + ## PWA UI Loader +---------------------------------------------------------------------------- */ + +.pwa-loader { + align-items: center; + background-color: var(--pwa-color-primary-opacity); + display: none; + justify-content: center; + padding: 20px; + position: fixed; + bottom: 0; + left: 0; + right: 0; + top: 0; + z-index: var(--z-index-2); +} + +/* + # PWA UI Loader is-visible state +--------------------------------- */ + +.pwa-loader.is-visible { + display: flex; +} + +/* ---------------------------------------------------------------------------- + ## PWA UI Actions bar +---------------------------------------------------------------------------- */ + +.pwa-actions { + background: var(--pwa-color-light); + display: flex; + padding: 10px 0 0 20px; + position: fixed; + right: 0; + top: 0; + z-index: var(--z-index-1); +} + +/* + # PWA UI Actions bar button +---------------------------- */ + +.pwa-actions-btn { + background: none; + border: none; + font-size: 1.2rem; + margin-right: 20px; +} + +/* + # PWA UI Actions bar button icon +--------------------------------- */ + +.pwa-actions-icon { + display: flex; + fill: var(--pwa-color-primary); + flex-direction: column; + margin: 0 auto; + width: 32px; +} + +/* ---------------------------------------------------------------------------- + ## PWA UI Params Version (@for pwaParams() function) +---------------------------------------------------------------------------- */ + +.pwa-params-version { + margin-bottom: 50px; +} + +/* ---------------------------------------------------------------------------- + ## PWA UI Params Permissions (@for pwaParams() function) +---------------------------------------------------------------------------- */ + +.pwa-permission-item { + align-items: center; + display: flex; + margin-bottom: 20px; +} + +.pwa-permission-item-label { + display: block; + margin-left: 10px; +} + +/* ---------------------------------------------------------------------------- + + ### PWA Bunga! UI functions core + +---------------------------------------------------------------------------- */ + +/* ---------------------------------------------------------------------------- + ## PWA Install button (@for pwaInstall() function) +---------------------------------------------------------------------------- */ + +.pwa-install-btn { + display: none; +} + +.pwa-install-btn.is-visible { + display: block; +} + +/* ---------------------------------------------------------------------------- + ## PWA Share button (@for pwaShare() function) +---------------------------------------------------------------------------- */ + +.pwa-share-btn { + display: none; +} + +.pwa-share-btn.is-visible { + display: block; +} + +/* ---------------------------------------------------------------------------- + ## PWA UI Params (@for pwaParams() function) +---------------------------------------------------------------------------- */ + +.pwa-params-version, +.pwa-params-permission-notification, +.pwa-params-permission-geolocation, +.pwa-params-permission, +.pwa-params-btn { + display: none; +} + +.pwa-params-version.is-visible, +.pwa-params-permission.is-visible, +.pwa-params-btn.is-visible { + display: block; +} + +.pwa-params-permission-notification.is-visible, +.pwa-params-permission-geolocation.is-visible { + display: flex; +} \ No newline at end of file diff --git a/pwa/icons/apple-touch-icon.png b/pwa/icons/apple-touch-icon.png new file mode 100644 index 0000000..5ce7ec3 Binary files /dev/null and b/pwa/icons/apple-touch-icon.png differ diff --git a/pwa/icons/icon-192.png b/pwa/icons/icon-192.png new file mode 100644 index 0000000..680b6d8 Binary files /dev/null and b/pwa/icons/icon-192.png differ diff --git a/pwa/icons/icon-512.png b/pwa/icons/icon-512.png new file mode 100644 index 0000000..acf3dbf Binary files /dev/null and b/pwa/icons/icon-512.png differ diff --git a/pwa/icons/icon-maskable-192.png b/pwa/icons/icon-maskable-192.png new file mode 100644 index 0000000..c6f237e Binary files /dev/null and b/pwa/icons/icon-maskable-192.png differ diff --git a/pwa/icons/icon-maskable-512.png b/pwa/icons/icon-maskable-512.png new file mode 100644 index 0000000..dbe5c95 Binary files /dev/null and b/pwa/icons/icon-maskable-512.png differ diff --git a/pwa/js/pwabunga.js b/pwa/js/pwabunga.js new file mode 100644 index 0000000..81d7e18 --- /dev/null +++ b/pwa/js/pwabunga.js @@ -0,0 +1,584 @@ +//----------------------------------------------------------------------------- + +// ### PWA Bunga! UX + +// This js file is used to register the PWA service worker +// and provides functions to improve the user experience + +// Documentation of pwabunga.js +// ---------------------------- +// https://pwabunga.com/documentation/pwa/js.html + +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- + +// ### PWA Register + +//----------------------------------------------------------------------------- + +// @doc https://pwabunga.com/documentation/pwa/register.html + +const pwaRegister = async (file = 'serviceworker.js') => { + + // -- If the browser supports the serviceWorker + if ("serviceWorker" in navigator) { + try { + // -- We try to register the service worker + const registration = await navigator.serviceWorker.register(file) + if (registration.installing) { + console.log("Service worker installing") + } else if (registration.waiting) { + console.log("Service worker installed") + } else if (registration.active) { + console.log("Service worker active") + } + } + catch (error) { + console.error(`Registration failed with ${error}`) + } + } + +} + +//----------------------------------------------------------------------------- + +// ### PWA UX/UI + +//----------------------------------------------------------------------------- + +//-------------------------------------------------------------------- +// ## PWA Install +//-------------------------------------------------------------------- + +// @doc https://pwabunga.com/documentation/pwa/install.html + +const pwaInstall = async () => { + + // # Selection of dom elements + //---------------------------- + + const pwaInstallPromotion = document.querySelector('.pwa-install-promotion') + const pwaInstallPromotionClose = document.querySelector('.pwa-install-promotion-close') + const pwaInstallBtn = document.querySelector('.pwa-install-btn') + const pwaInstallConfirm = document.querySelector('.pwa-install-confirm') + const pwaInstallConfirmClose = document.querySelector('.pwa-install-confirm-close') + const pwaInstallLoader = document.querySelector('.pwa-loader') + + // # Show PWA Install Button and promotion bar + //-------------------------------------------- + + // -- Initialize deferredPrompt for use later to show browser install prompt + let deferredPrompt + + // -- If the installation has not already been done + window.addEventListener('beforeinstallprompt', (e) => { + // - Prevent the mini-infobar from appearing on mobile + e.preventDefault() + // - Stash the event so it can be triggered later. + deferredPrompt = e + // - Show PWA promotion bar + pwaInstallPromotion.classList.add('is-visible') + // - Show PWA install btn + pwaInstallBtn.classList.add('is-visible') + }) + + // # PWA Install Button + //--------------------- + + pwaInstallBtn.addEventListener('click', async () => { + // -- Show the install prompt + deferredPrompt.prompt() + // Find out whether the user confirmed the installation or not + const { outcome } = await deferredPrompt.userChoice + // -- We've used the prompt, and can't use it again, throw it away + deferredPrompt = null + // -- Act on the user's choice + if (outcome === 'accepted') { + // - Hide PWA install btn + pwaInstallBtn.classList.remove('is-visible') + } + }) + + // # PWA Install Confirm + //---------------------- + + // -- When the app is installed + window.addEventListener("appinstalled", () => { + // - Hide PWA Install Promotion bar + pwaInstallPromotion.classList.remove('is-visible') + // - Show PWA loader + pwaInstallLoader.classList.add('is-visible') + // - We simulate the loading time by putting a delay of 2.5s + setTimeout(() => { + // Show PWA Install Confirm bar + pwaInstallConfirm.classList.add('is-visible') + // Hide PWA loader + pwaInstallLoader.classList.remove('is-visible') + }, 2500) + }) + + // # PWA close Install Promotion + //------------------------------ + + pwaInstallPromotionClose.addEventListener('click', () => { + // -- Hide PWA Install Promotion bar + pwaInstallPromotion.classList.remove('is-visible') + }) + + // # PWA close Install confirm + //---------------------------- + + pwaInstallConfirmClose.addEventListener('click', () => { + // -- Hide PWA Install confirm bar + pwaInstallConfirm.classList.remove('is-visible') + }) + +} + +//-------------------------------------------------------------------- +// ## PWA Update +//-------------------------------------------------------------------- + +// @doc https://pwabunga.com/documentation/pwa/update.html + +const pwaUpdate = async () => { + + // # Selection of dom elements + //---------------------------- + + const pwaUpdateBar = document.querySelector('.pwa-update') + const pwaUpdateBtn = document.querySelector('.pwa-update-btn') + const pwaUpdateClose = document.querySelector('.pwa-update-close') + const pwaUpdateLoader = document.querySelector('.pwa-loader') + + // # Service Worker + //----------------- + + // -- Get the Service Worker instance + const registration = await navigator.serviceWorker.getRegistration() + + // # PWA Update found + //------------------- + + registration.addEventListener("updatefound", async () => { + // -- If a service worker is registered + if (registration.installing) { + // - Wait until the new Service worker is actually installed (ready to take over) + registration.installing.addEventListener('statechange', async () => { + if (registration.waiting) { + // if there's an existing controller (previous Service Worker) + if (navigator.serviceWorker.controller) { + // Show update bar + pwaUpdateBar.classList.add('is-visible') + } + } + }) + } + }) + + // # PWA Update button + //-------------------- + + pwaUpdateBtn.addEventListener('click', async () => { + // -- Hide Update bar + pwaUpdateBar.classList.remove('is-visible') + // -- Show loader + pwaUpdateLoader.classList.add('is-visible') + // -- We send a message to the serviceworker which will be used to update the latter when he receives it + registration.waiting.postMessage('SKIP_WAITING') + }) + + // # PWA Refresh + //-------------- + + // -- When the service worker updated after receiving the message + navigator.serviceWorker.addEventListener('controllerchange', () => { + // - We refresh all the tabs of the open app + window.location.reload() + }) + + // # Update bar + //------------- + + // -- Show the update bar if it was skipped + if (registration.waiting) { + // -- Show Update bar + pwaUpdateBar.classList.add('is-visible') + } + + // # PWA close Promotion confirm + //------------------------------ + + pwaUpdateClose.addEventListener('click', () => { + // -- Hide PWA promotion confirm bar + pwaUpdateBar.classList.remove('is-visible') + }) + +} + +//-------------------------------------------------------------------- +// ## PWA Share +//-------------------------------------------------------------------- + +// @doc https://pwabunga.com/documentation/pwa/share.html + +const pwaShare = async (options = {}) => { + + try { + + // # Selection of dom elements + //---------------------------- + + const pwaShareBtn = document.querySelector('.pwa-share-btn') + + // # PWA Webmanifest data + //----------------------- + + // -- Fetches the PWA web manifest and stores the response in a variable + const webmanifestResponse = await fetch(new Request(document.querySelector('link[rel="manifest"]').href)) + // -- Converts the response from JSON to a JavaScript object and stores it in a variable + const webmanifest = await webmanifestResponse.json() + + // # Attribute data for the share API + //----------------------------------- + + // -- Define the sharing title with parameter to the function, or with name property in the webmanifest, or with title meta tag of the document + const title = options.title || webmanifest.name || document.title + // -- Define the sharing text with parameter to the function, or with description property in the webmanifest, or empty + const text = options.text || webmanifest.description || '' + // -- Define the sharing URL with parameter to the function, or with the URL of the page + const url = options.url || location.origin + location.pathname + + // # PWA Show Share button + //------------------------ + + // -- Checks whether the PWA is running in standalone mode + const isStandalone = window.matchMedia('(display-mode: standalone)').matches + // -- Checks whether the PWA is running in fullscreen mode + const isFullscreen = window.matchMedia('(display-mode: fullscreen)').matches + // -- Checks whether the PWA is running in standalone mode on iOS + const isStandaloneIOS = window.navigator.standalone + + // -- If the application is in fullscreen or standalone + if (isStandalone || isFullscreen || isStandaloneIOS) { + // - Show PWA Share Button + pwaShareBtn.classList.add('is-visible') + } + + // # PWA Share button + //------------------- + + // -- Adds a click event listener to the PWA share button that triggers the share functionality + pwaShareBtn.addEventListener('click', async () => { + // - Checks if the Web Share API is supported by the browser + if ('share' in navigator) { + // - Defines an object called data that contains the title, text, and url of the page to be shared + let data = { title, text, url } + try { + // Opens the device's native sharing module with the data to be shared + await navigator.share(data) + } catch (error) { + // If there is an error, an error message is logged to the console + console.error('Error sharing:', error) + } + // - If the Web Share API is not supported by the browser + } else { + // An warn message is logged to the console + console.warn('Share API not supported') + // The user is redirected to a WhatsApp API with the shared content as a text message + location.href = 'https://api.whatsapp.com/send?text=' + encodeURIComponent(text + ' - ') + url + } + }) + + } catch (error) { + // -- An error message is logged to the console + console.error('Error initializing pwaShare:', error) + } + +} + +//-------------------------------------------------------------------- +// ## PWA Parameters +//-------------------------------------------------------------------- + +// @doc https://pwabunga.com/documentation/pwa/params.html + +const pwaParams = async ( + version = true, + notifications = true, + geolocation = true +) => { + + // # Selection of dom elements + //---------------------------- + + const pwaParams = document.querySelector('.pwa-params') + const pwaParamsButton = document.querySelector('.pwa-params-btn') + const pwaParamsButtonClose = document.querySelector('.pwa-params-close-btn') + const pwaParamsVersion = document.querySelector('.pwa-params-version') + const pwaParamsPermission = document.querySelector('.pwa-params-permission') + const pwaParamsPermissionNotification = document.querySelector('.pwa-params-permission-notification') + const pwaParamsPermissionGeolocation = document.querySelector('.pwa-params-permission-geolocation') + const pwaAddName = document.querySelector('.pwa-name') + const pwaAddVersion = document.querySelector('.pwa-version') + + // # Service Worker + //----------------- + + // -- Get the Service Worker instance + const registration = await navigator.serviceWorker.getRegistration() + + // # PWA Webmanifest data + //----------------------- + + // -- Fetches the PWA web manifest and stores the response in a variable + const webmanifestResponse = await fetch(new Request(document.querySelector('link[rel="manifest"]').href)) + // -- Converts the response from JSON to a JavaScript object and stores it in a variable + const webmanifest = await webmanifestResponse.json() + + // # PWA Show Params button + //------------------------- + + // -- Retrieves the value of the 'utm_source' parameter from the URL query string of the current page + const utm_source = new URLSearchParams(window.location.search).get('utm_source') + + // -- If the utm_source parameter in the URL equals to 'homescreen' + if(utm_source == 'homescreen') { + // - Show PWA Params button + pwaParamsButton.classList.add('is-visible') + } + + // # PWA Version + //-------------- + + const pwaVersion = async () => { + // Send a message to the active Service Worker requesting the PWA versio + registration.active.postMessage('GET_VERSION') + // Listen for incoming messages from the Service Worker + navigator.serviceWorker.addEventListener('message', function(event) { + // Check if the message contains any data + if (event.data) { + // Update a DOM element to display the PWA version contained in the message + pwaAddVersion.innerText = event.data + } + }) + } + + // # PWA Params Version + //--------------------- + + // -- If the 'version' parameter of the function has a value of true + if(version) { + // - Show PWA Version + pwaParamsVersion.classList.add('is-visible') + // - Set the name of the PWA based on the data in the Web manifest file + pwaAddName.innerText = webmanifest.name + + // - Set the version of the PWA based on the data in the Service worker file + pwaVersion() + } + + // # PWA Params Permissions + //------------------------- + + // -- If both the 'notifications' and 'geolocation' parameters of the function have their value set to false + if (!(notifications) && !(geolocation)) { + // - Hide Params permission block + pwaParamsPermission.classList.remove('is-visible') + } else { + // - Show Params permission block + pwaParamsPermission.classList.add('is-visible') + } + + // # PWA Params Permissions Notifications + //--------------------------------------- + + // -- If the 'notifications' parameter of the function has a value of true + if (notifications) { + // - Show Params permission Notifications block + pwaParamsPermissionNotification.classList.add('is-visible') + // - Invoke the function pwaNotifications() + pwaNotifications() + } + + // # PWA Params Permissions Geolocation + //------------------------------------- + + // -- If the 'geolocation' parameter of the function has a value of true + if (geolocation) { + // - Show Params permission geolocation block + pwaParamsPermissionGeolocation.classList.add('is-visible') + // - Invoke the function pwaGeolocation() + pwaGeolocation() + } + + // # PWA Params button + //-------------------- + + // -- Adds a click event listener to the PWA params button + pwaParamsButton.addEventListener('click', () => { + // - Show PWA Params Block + pwaParams.classList.add('is-visible') + }) + + // # PWA Params Close button + //-------------------------- + + // -- Adds a click event listener to the PWA params close button + pwaParamsButtonClose.addEventListener('click', () => { + // - Hide PWA Params Block + pwaParams.classList.remove('is-visible') + }) + +} + +//----------------------------------------------------------------------------- + +// ### PWA Permissions + +//----------------------------------------------------------------------------- + +//-------------------------------------------------------------------- +// ## PWA Notifications +//-------------------------------------------------------------------- + +// @doc https://pwabunga.com/documentation/pwa/notification.html + +const pwaNotifications = async () => { + + // # Selection of dom element + //--------------------------- + + const pwaPermissionNotificationsBtn = document.querySelector('.pwa-permission-notifications-btn') + const pwaPermissionRemove = document.querySelector('.pwa-permission-remove') + + // # PWA Actual state of the noiifications permission + //--------------------------------------------------- + + // -- If the user has already granted permission for notifications + if (Notification.permission === 'granted') { + // - Add the 'is-active' class to the button to show that notifications are enabled + pwaPermissionNotificationsBtn.classList.add('is-active') + } else { + // - Remove the 'is-active' class from the button to show that notifications are disabled + pwaPermissionNotificationsBtn.classList.remove('is-active') + } + + // # PWA Notifications authorisation Ask function + //----------------------------------------------- + + const pwaNotificationsPermission = async () => { + // -- If the user is not denying notifications + if (Notification.permission !== 'denied') { + // - The user is asked the right to send him notifications + const permission = await Notification.requestPermission() + // If he accepted + if(permission == "granted"){ + // Add the 'is-active' class to the button to show that notifications are enabled + pwaPermissionNotificationsBtn.classList.add('is-active') + } + } + } + + // # PWA Notifications permission button + //-------------------------------------- + + pwaPermissionNotificationsBtn.addEventListener('click', async () => { + // -- Checks if the button has the 'is-active' class + if(pwaPermissionNotificationsBtn.classList.contains('is-active')) { + // - Show the alert box indicating how to remove the notification permission + pwaPermissionRemove.classList.add('is-visible') + } else { + // - Requests notification permission + pwaNotificationsPermission() + } + }) + + // # PWA Remove permission alert close + //------------------------------------ + + // -- When the alert box is clicked + pwaPermissionRemove.addEventListener('click', () => { + // - Removes the alert box indicating how to remove the notification permission + pwaPermissionRemove.classList.remove('is-visible') + }) + +} + +//-------------------------------------------------------------------- +// ## PWA Geolocation +//-------------------------------------------------------------------- + +// @doc https://pwabunga.com/documentation/pwa/geolocation.html + +const pwaGeolocation = async () => { + + // # Selection of dom element + //--------------------------- + + const pwaPermissionGeolocationBtn = document.querySelector('.pwa-permission-geolocation-btn') + const pwaPermissionRemove = document.querySelector('.pwa-permission-remove') + + // # PWA Geolocation Get current position + //--------------------------------------- + + const pwaGeolocationCurrentPosition = () => { + // -- Use the geolocation API to get the user's position + navigator.geolocation.getCurrentPosition(function(position) { + // - Log the user's latitude to the console + console.log('Latitude:' + position.coords.latitude) + // - Log the user's longitude to the console + console.log('Longitude:' + position.coords.longitude) + // - Add the 'is-active' class to the button to show that geolocation are enabled + pwaPermissionGeolocationBtn.classList.add('is-active') + }) + } + + // # PWA Actual state of the geolocation permission + //------------------------------------------------- + + // Check if the user has already granted permission for geolocation + const permissionStatus = await navigator.permissions.query({ name: 'geolocation' }) + + if (permissionStatus.state === "granted") { + // - Add the 'is-active' class to the button to show that geolocation are enabled + pwaGeolocationCurrentPosition() + } + + // # PWA Notifications permission button + //------------------------------------------------ + + pwaPermissionGeolocationBtn.addEventListener('click', () => { + // -- Checks if the button has the 'is-active' class + if(pwaPermissionGeolocationBtn.classList.contains('is-active')) { + // - Show the alert box indicating how to remove the geolocation permission + pwaPermissionRemove.classList.add('is-visible') + } else { + // - Requests geolocation permission with pwaGeolocationCurrentPosition() function + pwaGeolocationCurrentPosition() + } + }) + + // # PWA Remove permission alert close + //------------------------------------ + + // -- When the alert box is clicked + pwaPermissionRemove.addEventListener('click', () => { + // - Removes the alert box indicating how to remove the geolocation permission + pwaPermissionRemove.classList.remove('is-visible') + }) + +} + +//----------------------------------------------------------------------------- + +// ### PWA Init + +//----------------------------------------------------------------------------- + +pwaRegister() +pwaInstall() +pwaUpdate() +pwaShare() +pwaParams() \ No newline at end of file diff --git a/serviceworker.js b/serviceworker.js new file mode 100644 index 0000000..110087c --- /dev/null +++ b/serviceworker.js @@ -0,0 +1,333 @@ +//----------------------------------------------------------------------------- +// +// ### PWA Bunga! Service Worker +// +// Documentation of serviceworker.js +// --------------------------------- +// https://pwabunga.com/documentation/pwa/serviceworker.html +// +//----------------------------------------------------------------------------- + +//-------------------------------------------------------------------- +// +// ### Cache & Request Processing Strategy +// +//-------------------------------------------------------------------- + +// # Cache Config +//--------------- + +// -- Cache name and version +const cacheTitle = 'yourAppName' +const cacheVersion = 'v1.0' +const cacheName = cacheTitle + '-' + cacheVersion + +// -- static assets +const contentToCache = [ + // - Pages + '/', + 'index.html', + // - Favicons + 'favicon.ico', + 'favicon-16.png', + 'favicon-32.png', + // - CSS + 'assets/css/pwabunga.css', + 'assets/css/styles.css', + // - JS + 'assets/js/scripts.js', + // - PWA + 'pwa/css/pwabunga-ui.css', + 'pwa/icons/apple-touch-icon.png', + 'pwa/icons/icon-192.png', + 'pwa/icons/icon-512.png', + 'pwa/icons/icon-maskable-192.png', + 'pwa/icons/icon-maskable-512.png', + 'pwa/js/pwabunga.js', + 'pwa/app.webmanifest' +] + +// # Request Processing Strategy Config +//------------------------------------- + +// -- Request Processing Strategy +const requestProcessingMethod = 'cacheFirst' + +// -- Dynamic folder (Network Only Request Processing) +const dynamicFolder = '/api/' + +//-------------------------------------------------------------------- +// ## Add to cache +//-------------------------------------------------------------------- + +// # Add Resources to cache +//------------------------- + +const addResourcesToCache = async (resources) => { + // -- Open the cache with the specified name (cacheName) + const cache = await caches.open(cacheName) + // -- Add all resources in the resources array to the cache + await cache.addAll(resources) +} + +// # Put request to cache +//----------------------- + +const putInCache = async (request, response) => { + // -- Open the cache with the specified name (cacheName) + const cache = await caches.open(cacheName) + // -- Add the response to the cache for the given request + await cache.put(request, response) +} + +//-------------------------------------------------------------------- +// ## Delete cache +//-------------------------------------------------------------------- + +// # Delete cache +//--------------- + +const deleteCache = async (key) => { + // -- Delete the cache with the specified key + await caches.delete(key) +} + +// # Delete old caches +//-------------------- + +const deleteOldCaches = async () => { + // -- Define the cache to keep + const cacheKeepList = [cacheName] + // -- Get a list of all cache keys + const keyList = await caches.keys() + // -- Filter the list of keys to caches that are not in the keep list + const cachesToDelete = keyList.filter((key) => !cacheKeepList.includes(key)) + // -- Delete all caches to be deleted + await Promise.all(cachesToDelete.map(deleteCache)) +} + +//-------------------------------------------------------------------- +// ## Request Processing method +//-------------------------------------------------------------------- + +// # Cache Only +//------------- + +const cacheOnly = async ({ request }) => { + // -- Try to match the request with a cached response + const responseFromCache = await caches.match(request) + // -- If a cached response is found + if (responseFromCache) { + // - Return the response from cache + return responseFromCache + } else { + // - If no cached response is found + return new Response("No cached response found", { + // Return a 404 response with a plain text error message + status: 404, + headers: { "Content-Type": "text/plain" } + }) + } +} + +// # Network Only +//--------------- + +const networkOnly = async ({ request }) => { + try { + // -- Try to fetch the request from the network + const responseFromNetwork = await fetch(request) + // -- If the network request succeeds, return the response + return responseFromNetwork + } catch (error) { + // -- If the network request fails + return new Response("Network error happened", { + // - Return a 408 response with a plain text error message + status: 408, + headers: { "Content-Type": "text/plain" } + }) + } +} + +// # Cache first method +//--------------------- + +const cacheFirst = async ({ request }) => { + // -- If the request URL includes the dynamicFolder + if (request.url.includes(dynamicFolder)) { + // - Always fetch it from the network + return fetch(request) + } + // -- Try to match the request with a cached response + const responseFromCache = await caches.match(request) + // -- If a cached response is found + if (responseFromCache) { + // Return it + return responseFromCache + } + // -- If the response is not in cache + try { + // - Try to fetch it from the network + const responseFromNetwork = await fetch(request) + // - Cache the response + putInCache(request, responseFromNetwork.clone()) + // - Return the response from network + return responseFromNetwork + } catch (error) { + // - If there is a network error, return an error message + return new Response("Network error happened", { + status: 408, + headers: { "Content-Type": "text/plain" } + }) + } +} + +// # Network first method +//----------------------- + +const networkFirst = async ({ request }) => { + try { + // -- Try to fetch the resource from the network + const responseFromNetwork = await fetch(request) + // -- If the network request is successful, store a copy of the response in cache + putInCache(request, responseFromNetwork.clone()) + // -- Return the response from networkr + return responseFromNetwork + } catch (error) { + // -- If there was an error fetching from the network, attempt to retrieve the response from cache + const responseFromCache = await caches.match(request) + // -- If the cached response exists + if (responseFromCache) { + // - Return the response from cache + return responseFromCache + } + // -- If there was no cached response and the network request failed + return new Response("Network error happened", { + // - return a 408 response with a plain text error message + status: 408, + headers: { "Content-Type": "text/plain" } + }) + } +} + +// # Stale while revalidate method +//-------------------------------- + +const staleWhileRevalidate = async ({ request }) => { + // -- open cache + const cache = await caches.open(cacheName) + // -- check if response is in cache + const responseFromCache = await caches.match(request) + // -- fetch response from network + const responseFromNetwork = fetch(request) + // -- If a cached response is found + if (responseFromCache) { + // - Clone the cached response to avoid modifying the original + const responseClone = responseFromCache.clone() + // - Asynchronously fetch a fresh response from the network + responseFromNetwork.then(response => { + // Put the fresh response into the cache + cache.put(request, response.clone()) + }) + // - Return the cached response to the user + return responseClone + } + try { + // - Attempt to fetch the request from the network + const response = await responseFromNetwork + // - If successful, put the response in the cache + cache.put(request, response.clone()) + // - Return it + return response + } catch (error) { + // - If an error occurs + return new Response('Network error occurred.', { + // return a 408 response with a plain text error message + status: 408, + statusText: 'Request Timeout', + headers: { 'Content-Type': 'text/plain' } + }) + } +} + +//-------------------------------------------------------------------- +// +// ### Service Worker Events +// +//-------------------------------------------------------------------- + +//-------------------------------------------------------------------- +// ## Service Worker Install +//-------------------------------------------------------------------- + +// -- The service worker's "install" event listener +self.addEventListener('install', (event) => { + // - Log the installation message + console.log(cacheName + ' Installation') + // - Wait until the resources are added to the cache + event.waitUntil( + addResourcesToCache(contentToCache) + ) +}) + +//-------------------------------------------------------------------- +// ## Service Worker Activate +//-------------------------------------------------------------------- + +// -- The service worker's "activate" event listener +self.addEventListener('activate', (event) => { + // - This event is triggered when the service worker is activated + event.waitUntil( + // Wait until the deleteOldCaches function has finished running + deleteOldCaches() + ) +}) + +//-------------------------------------------------------------------- +// ## Service Worker Message +//-------------------------------------------------------------------- + +// -- The service worker's "message" event listener +self.addEventListener('message', (event) => { + // - If the message is 'SKIP_WAITING' + if (event.data === 'SKIP_WAITING') { + // Call the skipWaiting method to activate the service worker immediately + self.skipWaiting() + } + // - If the message is 'GET_VERSION' + if (event.data === 'GET_VERSION') { + // Send the cache version to all clients associated with the Service Worker + self.clients.matchAll().then(function(clients) { + // For each client + clients.forEach(function(client) { + // Send a message containing the cache version + client.postMessage(cacheVersion) + }) + }) + } +}) + +//-------------------------------------------------------------------- +// ## Service Worker Responses to requests +//-------------------------------------------------------------------- + +// -- The service worker's "message" event listener +self.addEventListener('fetch', (event) => { + // - Get the request object from the event + const request = event.request + // - Define a mapping of request processing methods + const methods = { + 'cacheOnly': cacheOnly, + 'networkOnly': networkOnly, + 'cacheFirst': cacheFirst, + 'networkFirst': networkFirst, + 'staleWhileRevalidate': staleWhileRevalidate + } + // - Get the processing method from the mapping based on the current request processing method + const method = methods[requestProcessingMethod] + // - If a processing method exists for the current request processing method + if (method) { + // Use it to handle the request + event.respondWith(method({request})) + } +}) \ No newline at end of file