Reorganize repository: Move Hugo site to docker/showerloop/public, clean up root directory, update Docker configuration to use Caddy, add Woodpecker CI config

This commit is contained in:
Your Name 2025-03-06 13:48:05 -05:00
parent d5cbd80092
commit 1bd18e39e1
594 changed files with 9952 additions and 785 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
.DS_Store
node_modules
public
public/public
temp_backup

113
.woodpecker.yml Normal file
View File

@ -0,0 +1,113 @@
# build:1
labels:
location: manager
clone:
git:
image: woodpeckerci/plugin-git
settings:
partial: false
depth: 1
when:
branch: [main, production]
steps:
# Build Step for staging Branch
build-staging:
name: build-staging
image: woodpeckerci/plugin-docker-buildx
environment:
REGISTRY_USER:
from_secret: REGISTRY_USER
REGISTRY_PASSWORD:
from_secret: REGISTRY_PASSWORD
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- echo "Building application for staging branch"
- echo "$${REGISTRY_PASSWORD}" | docker login -u "$${REGISTRY_USER}" --password-stdin git.nixc.us
- echo compose build
- docker compose -f docker-compose.staging.yml pull --ignore-buildable
- docker compose -f docker-compose.staging.yml build --pull
when:
event: push
deploy-new:
name: deploy-new
image: woodpeckerci/plugin-docker-buildx
environment:
REGISTRY_USER:
from_secret: REGISTRY_USER
REGISTRY_PASSWORD:
from_secret: REGISTRY_PASSWORD
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- echo "$${REGISTRY_PASSWORD}" | docker login -u "$${REGISTRY_USER}" --password-stdin git.nixc.us
- echo compose push
- docker compose -f docker-compose.staging.yml push
- docker stack deploy --with-registry-auth -c ./stack.staging.yml $${CI_REPO_NAME}-staging
when:
event: push
cleanup-staging:
name: cleanup-staging
image: woodpeckerci/plugin-docker-buildx
environment:
REGISTRY_USER:
from_secret: REGISTRY_USER
REGISTRY_PASSWORD:
from_secret: REGISTRY_PASSWORD
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- for i in {1..5}; do docker stack rm ${CI_REPO_NAME}-staging && break || sleep 10; done
- docker compose -f docker-compose.staging.yml down
- docker compose -f docker-compose.staging.yml rm -f
when:
event: push
build-push-production:
name: build-push-production
image: woodpeckerci/plugin-docker-buildx
environment:
REGISTRY_USER:
from_secret: REGISTRY_USER
REGISTRY_PASSWORD:
from_secret: REGISTRY_PASSWORD
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- echo "Building application for production branch"
- echo "$${REGISTRY_PASSWORD}" | docker login -u "$${REGISTRY_USER}" --password-stdin git.nixc.us
- echo compose build
- docker compose -f docker-compose.production.yml pull --ignore-buildable
- docker compose -f docker-compose.production.yml build --pull
- docker compose -f docker-compose.production.yml push
when:
branch: main
event: push
deploy-production:
name: deploy-production
image: woodpeckerci/plugin-docker-buildx
environment:
REGISTRY_USER:
from_secret: REGISTRY_USER
REGISTRY_PASSWORD:
from_secret: REGISTRY_PASSWORD
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- echo "$${REGISTRY_PASSWORD}" | docker login -u "$${REGISTRY_USER}" --password-stdin git.nixc.us
- docker stack deploy --with-registry-auth -c ./stack.production.yml $${CI_REPO_NAME}
when:
branch: main
event: push
post-deploy-smoke-tests-git-nixc-us:
name: run-post-deploy-smoke-tests-git-nixc-us
image: git.nixc.us/colin/playwright:latest
environment:
BASE_URL: "https://git.nixc.us"
when:
branch: production
event: push

View File

@ -1,68 +0,0 @@
# CSS and JavaScript Optimization Guide
This document explains how to eliminate unused CSS and JavaScript from the ShowerLoop website.
## Quick Start
1. Install dependencies:
```bash
npm install --legacy-peer-deps
```
2. Build the production version with optimizations:
```bash
./build-production.sh
```
## What This Does
### CSS Optimization
The build script optimizes CSS in two ways:
1. **CSSO Optimization**: Uses CSSO to remove unused selectors, merge similar rules, and minify the CSS.
- Typically reduces CSS by 5-30% depending on the file
- Maintains full functionality while eliminating bloat
- Works without requiring a running website
2. **File Path Updates**: Automatically updates all HTML files to reference the optimized CSS versions.
### JavaScript Optimization
JavaScript optimization happens through:
1. **Tree Shaking**: Rollup analyzes your code to detect which parts are actually used and removes dead code.
- Eliminates unused imports and functions
- Reduces JavaScript file sizes significantly
2. **Code Splitting**: JavaScript is split into separate bundles:
- app.modern.min.js - Main application code
- video-init.modern.min.js - Video player initialization
- skip-to-content.modern.min.js - Accessibility features
3. **Minification**: All JavaScript is minified to reduce file size.
## Optimization Results
In our tests, the optimization achieves:
| File Type | Size Reduction |
|-----------|---------------|
| CSS | 5-30% |
| JavaScript| 30-60% |
## How to Keep It Optimized
1. **Maintain Source Files**: Keep original JavaScript source files in `src/js/`
2. **Run Build Before Deployment**: Always run `./build-production.sh` before deploying to production
3. **Add New Components Wisely**: When adding new CSS/JS, consider if it's truly needed or if existing code can be reused
## Troubleshooting
- **Missing Styles**: If elements look unstyled after optimization, check the original CSS files and update your HTML accordingly.
- **JavaScript Errors**: If functionality breaks, check the browser console for errors and ensure all required code is in your source files.
- **Build Errors**: Make sure all dependencies are installed with `npm install --legacy-peer-deps` before running the build scripts.

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020 Shower-Loop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,27 +1,34 @@
# website
https://showerloop.cc new site for https://showerloop.org [planned]
# ShowerLoop Website
## Local Development
This repository contains the source code for the ShowerLoop website, built with Hugo and served via Docker.
To test this site locally:
## Repository Structure
1. Make sure you have [Hugo](https://gohugo.io/installation/) installed on your system
2. Clone this repository
3. Run the development server using the script:
- `/docker/showerloop/` - Container configuration for the website
- `/docker/showerloop/public/` - Hugo website source files
- `/docker/showerloop/Dockerfile` - Docker image definition
- `/docker/showerloop/Caddyfile` - Caddy server configuration
## Development
To run the Hugo development server:
```bash
cd docker/showerloop/public
./run-hugo-server.sh
```
Or run Hugo directly:
The development server will be available at http://localhost:1313/
## Building
To build the production site:
```bash
hugo server -D --disableFastRender
cd docker/showerloop/public
./build-production.sh
```
4. Open your browser and go to http://localhost:1313/ to view the site
5. The site will automatically reload when you make changes to the content or templates
## Deployment
### Notes
- The `-D` flag enables draft content to be visible in the development environment
- `--disableFastRender` ensures full rebuilds for more reliable preview
The site is automatically built and deployed using Woodpecker CI when changes are pushed to the master branch.

View File

@ -1 +0,0 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Categories on</title><link>http://localhost:54386/categories/</link><description>Recent content in Categories on</description><generator>Hugo</generator><language>en-us</language><atom:link href="http://localhost:54386/categories/index.xml" rel="self" type="application/rss+xml"/></channel></rss>

View File

@ -1,29 +0,0 @@
<!doctype html><html lang=en><head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=53498&amp;path=livereload" data-no-instant defer></script><title>Support the Project | ShowerLoop</title>
<meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=description content="ShowerLoop - Real-time filtration, purification, recycling & heat recovery system for showers. Open source and sustainable water conservation technology."><script>let liveReloadSocket=null;const OriginalWebSocket=window.WebSocket;window.WebSocket=function(e,t){if(e.includes("/__livereload")){if(document.readyState==="complete"){liveReloadSocket=new OriginalWebSocket(e,t);const n={onmessage:null,onclose:null};return Object.defineProperty(liveReloadSocket,"onmessage",{set:function(e){n.onmessage=e},get:function(){return n.onmessage}}),Object.defineProperty(liveReloadSocket,"onclose",{set:function(e){n.onclose=e},get:function(){return n.onclose}}),liveReloadSocket.addEventListener("message",function(e){n.onmessage&&n.onmessage(e)}),liveReloadSocket.addEventListener("close",function(e){liveReloadSocket=null,n.onclose&&n.onclose(e)}),liveReloadSocket}return{send:function(){},close:function(){},addEventListener:function(){},removeEventListener:function(){},set onmessage(e){},set onclose(e){}}}return new OriginalWebSocket(e,t)};for(const e in OriginalWebSocket)OriginalWebSocket.hasOwnProperty(e)&&(window.WebSocket[e]=OriginalWebSocket[e]);window.WebSocket.prototype=OriginalWebSocket.prototype,document.addEventListener("pageshow",function(e){if(e.persisted){console.log("Page restored from bfcache");const e=window.location.protocol==="https:"?"wss:":"ws:";liveReloadSocket=new OriginalWebSocket(`${e}//${window.location.host}/__livereload`),liveReloadSocket.onmessage=function(e){e.data==="reload"&&window.location.reload()}}}),window.addEventListener("pagehide",function(){liveReloadSocket&&(liveReloadSocket.onclose=null,liveReloadSocket.close(),liveReloadSocket=null)})</script><link rel=preload href=/css/vendor/material-icons.css as=style><link rel=preload href=/images/logo2.webp as=image><link rel=stylesheet href=/css/vendor/material-icons.css><link rel=stylesheet href=/css/vendor/material.indigo-pink.min.css media=print onload='this.media="all"'><noscript><link rel=stylesheet href=/css/vendor/material.indigo-pink.min.css></noscript><link rel=stylesheet href=/css/vendor/fontawesome.min.css media=print onload='this.media="all"'><noscript><link rel=stylesheet href=/css/vendor/fontawesome.min.css></noscript><script type=module>
import * as utils from '/js/utils.modern.min.js';
window.utilsModule = utils;
</script><script type=module src=/js/app.modern.min.js defer></script><script type=module src=/js/skip-to-content.modern.min.js defer></script><script type=module src=/js/material.modern.min.js defer></script><script nomodule src=/js/app.min.js defer></script><script nomodule src=/js/skip-to-content.min.js defer></script><script nomodule src=/js/material.min.js defer></script><link rel=stylesheet type=text/css href=/css/app.min.css><link rel=stylesheet type=text/css href=/css/custom.css media=print onload='this.media="all"'><noscript><link rel=stylesheet href=/css/custom.css></noscript></head><body class=page><div class=skip-to-content role=button tabindex=0>Skip to Content</div><style>.mdl-navigation .mdl-button.mdl-navigation__link{display:flex;align-items:center;justify-content:center;height:36px;line-height:36px;padding:0 16px;margin:8px 0}.logo{height:50px;width:auto;max-width:150px;transition:none!important}</style><div class="mdl-layout mdl-js-layout
mdl-layout--fixed-header"><header class="mdl-layout__header site-header"><div class=mdl-layout__header-row><a href=/ class=mdl-layout-title><img class=logo src=/images/logo2.webp height=50 width=auto alt="ShowerLoop Logo"></a><div class=mdl-layout-spacer></div><nav class="mdl-navigation mdl-layout--large-screen-only"><a class=mdl-navigation__link href=/ title=Home>Home</a>
<a class=mdl-navigation__link href=/how-it-works/ title="How It Works">How It Works</a>
<a class=mdl-navigation__link href=/research/ title=Research>Research</a>
<a class=mdl-navigation__link href=/posts/ title=Posts>Posts</a>
<a class=mdl-navigation__link href=/components/ title=Components>Components</a>
<a class="mdl-navigation__link mdl-button mdl-js-button mdl-button--raised mdl-button--colored" href=/make-it/ title="Make It">Make It</a></nav></div></header><div class=mdl-layout__drawer><span class=mdl-layout-title><strong>ShowerLoop</strong></span><nav class=mdl-navigation><a class=mdl-navigation__link href=/ title=Home tabindex=0>Home</a>
<a class=mdl-navigation__link href=/how-it-works/ title="How It Works" tabindex=0>How It Works</a>
<a class=mdl-navigation__link href=/research/ title=Research tabindex=0>Research</a>
<a class=mdl-navigation__link href=/posts/ title=Posts tabindex=0>Posts</a>
<a class=mdl-navigation__link href=/components/ title=Components tabindex=0>Components</a>
<a class="mdl-navigation__link mdl-button mdl-js-button mdl-button--raised mdl-button--colored" href=/make-it/ title="Make It" tabindex=0>Make It</a></nav></div><main aria-role=main><div class=subpage-content><div class=chocolate-container><h1>Support the Project</h1><div class=mdl-grid><section class="mdl-cell mdl-cell--12-col"><p>If you want to support the ShowerLoop project, check out our <a href=/make-it>Make-It Button</a> where you can contribute to our ongoing development.</p><pre><code> &lt;p&gt;We work with limited resources and recycle many materials. With access to workspace and tools (thank you, Fablab!), we're able to continue developing this sustainable technology.&lt;/p&gt;
&lt;p&gt;We're collaborating with people around the world to bring ShowerLoop to various environments - boats, RVs, houses in remote locations, Pacific Islands, eco-friendly buildings, and historic structures without modern plumbing. The possibilities are endless, and together we hope to realize them all.&lt;/p&gt;
&lt;p&gt;Our team is developing a business model that fosters community around social and environmental ethics within the open source circular economy.&lt;/p&gt;
&lt;div class=&quot;mdl-grid&quot; style=&quot;justify-content: center; margin-top: 30px;&quot;&gt;
&lt;a class=&quot;mdl-button mdl-js-button mdl-button--raised mdl-button--colored&quot; href=&quot;/make-it&quot;&gt;
Support the Project
&lt;/a&gt;
&lt;/div&gt;
&lt;/section&gt;
</code></pre></div></div></div></main><footer class="mdl-mini-footer site-footer"><div class=mdl-mini-footer__left-section>&copy 2020 Shower Loop | All Rights Reserved</div><div class=mdl-mini-footer__right-section></div></footer></div><script>document.addEventListener("DOMContentLoaded",function(){setTimeout(function(){if(!window.__bfcacheLiveReloadActive){const e=window.location.protocol==="https:"?"wss:":"ws:",t=`${e}//${window.location.host}/__livereload`;try{if(window.OriginalWebSocket){const e=new window.OriginalWebSocket(t);e.onmessage=function(e){e.data==="reload"&&window.location.reload()},window.__bfcacheLiveReloadActive=!0}}catch(e){console.warn("LiveReload connection error:",e)}}},500)})</script></body></html>

View File

@ -0,0 +1,7 @@
services:
showerloop:
build:
context: docker/showerloop
dockerfile: Dockerfile.production
pull: true
image: git.nixc.us/colin/showerloop-cc:production

View File

@ -0,0 +1,6 @@
services:
showerloop:
build:
context: docker/showerloop
pull: true
image: git.nixc.us/colin/showerloop-cc:staging

View File

@ -0,0 +1 @@
FROM git.nixc.us/colin/showerloop-cc:staging

View File

@ -0,0 +1,87 @@
# Template: Caddyfile.override
# Purpose: Default configuration for custom containers.
# Description:
# - Serves static files from /srv.
# - Provides a /health endpoint for health checks.
# - Designed to run behind a reverse proxy like Træfik, listening only on port 80.
# - comes with security headers
:80 {
# Health check endpoint
respond /health "OK" 200
# Enable compression for text-based resources
encode gzip zstd
# Security headers
header {
# Cross-Origin headers
Cross-Origin-Embedder-Policy "require-corp"
Cross-Origin-Opener-Policy "same-origin"
Cross-Origin-Resource-Policy "same-origin"
# Permissions Policy
Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=()"
# Referrer Policy
Referrer-Policy "strict-origin-when-cross-origin"
# HSTS
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
# Content Type Options
X-Content-Type-Options "nosniff"
# XSS Protection
X-XSS-Protection "1; mode=block"
# Frame Options (prevents clickjacking)
X-Frame-Options "SAMEORIGIN"
# Frame ancestors (prevents embedding in other sites)
Content-Security-Policy "frame-ancestors 'none'"
# Remove Server header
-Server
}
# Cache control for static assets - images, fonts, etc.
@staticAssets {
path *.jpg *.jpeg *.png *.webp *.avif *.gif *.ico *.svg *.woff *.woff2 *.ttf *.eot
method GET HEAD
}
header @staticAssets Cache-Control "public, max-age=31536000, immutable"
header @staticAssets ?Access-Control-Allow-Origin *
# Special handling for CSS and JS files
@cssAndJs {
path *.css *.js
method GET HEAD
}
header @cssAndJs Cache-Control "public, max-age=31536000, immutable"
# Cache HTML files but for a shorter period
@htmlFiles {
path *.html
method GET HEAD
}
header @htmlFiles Cache-Control "public, max-age=86400, must-revalidate"
# Static file server
file_server {
root /srv # Root directory for serving static files
}
# Restrict allowed methods to only GET and HEAD
@staticRequests {
method GET HEAD
}
handle @staticRequests {
root * /srv
file_server
}
# Handle all other methods
respond "Method Not Allowed" 405
}

View File

@ -0,0 +1,42 @@
# Stage 1: Build Hugo site
FROM alpine:latest AS hugo-builder
# Install necessary dependencies (Hugo, Git, Node.js, and npm)
RUN apk add --no-cache hugo git nodejs npm
# Copy our enhanced Caddyfile
COPY Caddyfile.default.template /etc/caddy/Caddyfile.override
# Set working directory
WORKDIR /site
# Copy the Hugo source files
COPY public/ /site
# Install PostCSS and its dependencies locally and update browserslist
RUN cd /site && npm init -y && \
npm install --save-dev postcss postcss-cli autoprefixer && \
npm install --save-dev caniuse-lite && \
npm update caniuse-lite browserslist
# Build the Hugo site for production with optimizations
# Disable GitInfo, enable minification, and set production environment
RUN mkdir /public && \
cd /site && \
HUGO_ENABLEGITINFO=false \
HUGO_ENV=production \
npm run build:prod && \
cp -r /site/public/* /public/
# Stage 2: Production image with prebuilt static files
FROM git.nixc.us/colin/container-base:production-nixiusstatic
# Copy the built site from the first stage
COPY --from=hugo-builder /public /srv
# Copy our enhanced Caddyfile for development
COPY Caddyfile.default.template /etc/caddy/Caddyfile.override
# Add health check endpoint for production monitoring
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q --spider http://localhost/healthcheck.txt || exit 1

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Categories on</title><link>http://localhost:1313/categories/</link><description>Recent content in Categories on</description><generator>Hugo</generator><language>en-us</language><atom:link href="http://localhost:1313/categories/index.xml" rel="self" type="application/rss+xml"/></channel></rss>

View File

@ -1,4 +1,4 @@
<!doctype html><html lang=en><head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=54386&amp;path=livereload" data-no-instant defer></script><title>Components | ShowerLoop</title>
<!doctype html><html lang=en><head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=1313&amp;path=livereload" data-no-instant defer></script><title>Components | ShowerLoop</title>
<meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=description content="ShowerLoop - Real-time filtration, purification, recycling & heat recovery system for showers. Open source and sustainable water conservation technology."><script>let liveReloadSocket=null;const OriginalWebSocket=window.WebSocket;window.WebSocket=function(e,t){if(e.includes("/__livereload")){if(document.readyState==="complete"){liveReloadSocket=new OriginalWebSocket(e,t);const n={onmessage:null,onclose:null};return Object.defineProperty(liveReloadSocket,"onmessage",{set:function(e){n.onmessage=e},get:function(){return n.onmessage}}),Object.defineProperty(liveReloadSocket,"onclose",{set:function(e){n.onclose=e},get:function(){return n.onclose}}),liveReloadSocket.addEventListener("message",function(e){n.onmessage&&n.onmessage(e)}),liveReloadSocket.addEventListener("close",function(e){liveReloadSocket=null,n.onclose&&n.onclose(e)}),liveReloadSocket}return{send:function(){},close:function(){},addEventListener:function(){},removeEventListener:function(){},set onmessage(e){},set onclose(e){}}}return new OriginalWebSocket(e,t)};for(const e in OriginalWebSocket)OriginalWebSocket.hasOwnProperty(e)&&(window.WebSocket[e]=OriginalWebSocket[e]);window.WebSocket.prototype=OriginalWebSocket.prototype,document.addEventListener("pageshow",function(e){if(e.persisted){console.log("Page restored from bfcache");const e=window.location.protocol==="https:"?"wss:":"ws:";liveReloadSocket=new OriginalWebSocket(`${e}//${window.location.host}/__livereload`),liveReloadSocket.onmessage=function(e){e.data==="reload"&&window.location.reload()}}}),window.addEventListener("pagehide",function(){liveReloadSocket&&(liveReloadSocket.onclose=null,liveReloadSocket.close(),liveReloadSocket=null)})</script><link rel=preload href=/css/vendor/material-icons.css as=style><link rel=preload href=/images/logo2.webp as=image><link rel=stylesheet href=/css/vendor/material-icons.css><link rel=stylesheet href=/css/vendor/material.indigo-pink.min.css media=print onload='this.media="all"'><noscript><link rel=stylesheet href=/css/vendor/material.indigo-pink.min.css></noscript><link rel=stylesheet href=/css/vendor/fontawesome.min.css media=print onload='this.media="all"'><noscript><link rel=stylesheet href=/css/vendor/fontawesome.min.css></noscript><script type=module>
import * as utils from '/js/utils.modern.min.js';

View File

Before

Width:  |  Height:  |  Size: 730 KiB

After

Width:  |  Height:  |  Size: 730 KiB

View File

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 141 KiB

View File

Before

Width:  |  Height:  |  Size: 898 KiB

After

Width:  |  Height:  |  Size: 898 KiB

View File

@ -1,4 +1,4 @@
<!doctype html><html lang=en><head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=54386&amp;path=livereload" data-no-instant defer></script><title>How It Works | ShowerLoop</title>
<!doctype html><html lang=en><head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=1313&amp;path=livereload" data-no-instant defer></script><title>How It Works | ShowerLoop</title>
<meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><meta name=description content="Learn how the ShowerLoop system filters, purifies and recycles shower water in real-time, saving up to 90% of water and energy."><script>let liveReloadSocket=null;const OriginalWebSocket=window.WebSocket;window.WebSocket=function(e,t){if(e.includes("/__livereload")){if(document.readyState==="complete"){liveReloadSocket=new OriginalWebSocket(e,t);const n={onmessage:null,onclose:null};return Object.defineProperty(liveReloadSocket,"onmessage",{set:function(e){n.onmessage=e},get:function(){return n.onmessage}}),Object.defineProperty(liveReloadSocket,"onclose",{set:function(e){n.onclose=e},get:function(){return n.onclose}}),liveReloadSocket.addEventListener("message",function(e){n.onmessage&&n.onmessage(e)}),liveReloadSocket.addEventListener("close",function(e){liveReloadSocket=null,n.onclose&&n.onclose(e)}),liveReloadSocket}return{send:function(){},close:function(){},addEventListener:function(){},removeEventListener:function(){},set onmessage(e){},set onclose(e){}}}return new OriginalWebSocket(e,t)};for(const e in OriginalWebSocket)OriginalWebSocket.hasOwnProperty(e)&&(window.WebSocket[e]=OriginalWebSocket[e]);window.WebSocket.prototype=OriginalWebSocket.prototype,document.addEventListener("pageshow",function(e){if(e.persisted){console.log("Page restored from bfcache");const e=window.location.protocol==="https:"?"wss:":"ws:";liveReloadSocket=new OriginalWebSocket(`${e}//${window.location.host}/__livereload`),liveReloadSocket.onmessage=function(e){e.data==="reload"&&window.location.reload()}}}),window.addEventListener("pagehide",function(){liveReloadSocket&&(liveReloadSocket.onclose=null,liveReloadSocket.close(),liveReloadSocket=null)})</script><link rel=preload href=/css/vendor/material-icons.css as=style><link rel=preload href=/images/logo2.webp as=image><link rel=stylesheet href=/css/vendor/material-icons.css><link rel=stylesheet href=/css/vendor/material.indigo-pink.min.css media=print onload='this.media="all"'><noscript><link rel=stylesheet href=/css/vendor/material.indigo-pink.min.css></noscript><link rel=stylesheet href=/css/vendor/fontawesome.min.css media=print onload='this.media="all"'><noscript><link rel=stylesheet href=/css/vendor/fontawesome.min.css></noscript><script type=module>
import * as utils from '/js/utils.modern.min.js';

View File

Before

Width:  |  Height:  |  Size: 270 KiB

After

Width:  |  Height:  |  Size: 270 KiB

View File

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 213 KiB

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 194 KiB

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

View File

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 194 KiB

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

Before

Width:  |  Height:  |  Size: 988 KiB

After

Width:  |  Height:  |  Size: 988 KiB

View File

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 121 KiB

View File

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

Before

Width:  |  Height:  |  Size: 488 KiB

After

Width:  |  Height:  |  Size: 488 KiB

View File

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 156 KiB

View File

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Some files were not shown because too many files have changed in this diff Show More