Add Progressive Web App functionality

- Add web app manifest for home screen installation
- Implement service worker with offline caching strategy
- Create offline fallback page with auto-reconnect
- Generate PWA icons in multiple sizes (72px-512px)
- Add PWA meta tags and Apple Touch icons to all pages
- Register service worker with graceful degradation
- Update documentation with PWA installation instructions
- Add browserconfig.xml for Windows tile support

Features:
- Installable on mobile and desktop
- Offline functionality with cached resources
- App-like experience in standalone mode
- Automatic updates when online
- Works seamlessly with existing progressive enhancement

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Claude Code 2025-07-06 00:46:00 -04:00
parent 10c6e54062
commit c13b61cd03
21 changed files with 940 additions and 15 deletions

View file

@ -4,8 +4,24 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Great Lakes Ice Report Admin</title>
<link rel="icon" type="image/svg+xml" href="https://iceymi.b-cdn.net/favicon.svg">
<!-- PWA Meta Tags -->
<meta name="description" content="Admin panel for Great Lakes Ice Report - manage winter road condition reports">
<meta name="theme-color" content="#2196F3">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Ice Report Admin">
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json">
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icons/favicon.svg">
<link rel="icon" type="image/x-icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIj4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLnNub3dmbGFrZSB7IGZpbGw6ICMyMTk2RjM7IH0KICAgICAgLmNlbnRlciB7IGZpbGw6ICMxOTc2RDI7IH0KICAgIDwvc3R5bGU+CiAgPC9kZWZzPgogIAogIDxnIGNsYXNzPSJzbm93Zmxha2UiPgogICAgPHJlY3QgeD0iMTUiIHk9IjIiIHdpZHRoPSIyIiBoZWlnaHQ9IjI4IiAvPgogICAgPHJlY3QgeD0iMiIgeT0iMTUiIHdpZHRoPSIyOCIgaGVpZ2h0PSIyIiAvPgogICAgPHJlY3QgeD0iMTUiIHk9IjIiIHdpZHRoPSIyIiBoZWlnaHQ9IjI4IiB0cmFuc2Zvcm09InJvdGF0ZSg0NSAxNiAxNikiIC8+CiAgICA8cmVjdCB4PSIxNSIgeT0iMiIgd2lkdGg9IjIiIGhlaWdodD0iMjgiIHRyYW5zZm9ybT0icm90YXRlKC00NSAxNiAxNikiIC8+CiAgICA8cG9seWdvbiBwb2ludHM9IjE2LDIgMTQsNiAxOCw2IiAvPgogICAgPHBvbHlnb24gcG9pbnRzPSIxNiwzMCAxNCwyNiAxOCwyNiIgLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iMiwxNiA2LDE0IDYsMTgiIC8+CiAgICA8cG9seWdvbiBwb2ludHM9IjMwLDE2IDI2LDE0IDI2LDE4IiAvPgogICAgPHBvbHlnb24gcG9pbnRzPSI2LjMsNi4zIDguNiw0IDkuOSw3LjciIHRyYW5zZm9ybT0icm90YXRlKDQ1IDE2IDE2KSIgLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iMjUuNywyNS43IDIzLjQsMjggMjIuMSwyNC4zIiB0cmFuc2Zvcm09InJvdGF0ZSg0NSAxNiAxNikiIC8+CiAgICA8cG9seWdvbiBwb2ludHM9IjYuMywyNS43IDguNiwyOCA5LjksMjQuMyIgdHJhbnNmb3JtPSJyb3RhdGUoLTQ1IDE2IDE2KSIgLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iMjUuNyw2LjMgMjMuNCw0IDIyLjEsNy43IiB0cmFuc2Zvcm09InJvdGF0ZSgtNDUgMTYgMTYpIiAvPgogIDwvZz4KICA8Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIzIiBjbGFzcz0iY2VudGVyIiAvPgo8L3N2Zz4K">
<!-- Apple Touch Icons -->
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152.svg">
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192.svg">
<link rel="stylesheet" href="style.css">
<script>
// Apply theme immediately to prevent flash

10
public/browserconfig.xml Normal file
View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/icons/icon-152.svg"/>
<square310x310logo src="/icons/icon-384.svg"/>
<TileColor>#2196F3</TileColor>
</tile>
</msapplication>
</browserconfig>

32
public/icons/favicon.svg Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="16" cy="16" r="16" fill="url(#bg)"/>
<!-- Ice crystal/snowflake icon -->
<g transform="translate(16, 16)">
<!-- Main cross -->
<line x1="-9.6" y1="0" x2="9.6" y2="0" stroke="white" stroke-width="2.56" stroke-linecap="round"/>
<line x1="0" y1="-9.6" x2="0" y2="9.6" stroke="white" stroke-width="2.56" stroke-linecap="round"/>
<!-- Diagonal lines -->
<line x1="-6.72" y1="-6.72" x2="6.72" y2="6.72" stroke="white" stroke-width="1.92" stroke-linecap="round"/>
<line x1="6.72" y1="-6.72" x2="-6.72" y2="6.72" stroke="white" stroke-width="1.92" stroke-linecap="round"/>
<!-- Small decorative elements -->
<circle cx="0" cy="0" r="1.28" fill="white"/>
<circle cx="4.8" cy="0" r="0.64" fill="white"/>
<circle cx="-4.8" cy="0" r="0.64" fill="white"/>
<circle cx="0" cy="4.8" r="0.64" fill="white"/>
<circle cx="0" cy="-4.8" r="0.64" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

32
public/icons/icon-128.svg Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="64" cy="64" r="64" fill="url(#bg)"/>
<!-- Ice crystal/snowflake icon -->
<g transform="translate(64, 64)">
<!-- Main cross -->
<line x1="-38.4" y1="0" x2="38.4" y2="0" stroke="white" stroke-width="10.24" stroke-linecap="round"/>
<line x1="0" y1="-38.4" x2="0" y2="38.4" stroke="white" stroke-width="10.24" stroke-linecap="round"/>
<!-- Diagonal lines -->
<line x1="-26.88" y1="-26.88" x2="26.88" y2="26.88" stroke="white" stroke-width="7.68" stroke-linecap="round"/>
<line x1="26.88" y1="-26.88" x2="-26.88" y2="26.88" stroke="white" stroke-width="7.68" stroke-linecap="round"/>
<!-- Small decorative elements -->
<circle cx="0" cy="0" r="5.12" fill="white"/>
<circle cx="19.2" cy="0" r="2.56" fill="white"/>
<circle cx="-19.2" cy="0" r="2.56" fill="white"/>
<circle cx="0" cy="19.2" r="2.56" fill="white"/>
<circle cx="0" cy="-19.2" r="2.56" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

32
public/icons/icon-144.svg Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="72" cy="72" r="72" fill="url(#bg)"/>
<!-- Ice crystal/snowflake icon -->
<g transform="translate(72, 72)">
<!-- Main cross -->
<line x1="-43.199999999999996" y1="0" x2="43.199999999999996" y2="0" stroke="white" stroke-width="11.52" stroke-linecap="round"/>
<line x1="0" y1="-43.199999999999996" x2="0" y2="43.199999999999996" stroke="white" stroke-width="11.52" stroke-linecap="round"/>
<!-- Diagonal lines -->
<line x1="-30.24" y1="-30.24" x2="30.24" y2="30.24" stroke="white" stroke-width="8.64" stroke-linecap="round"/>
<line x1="30.24" y1="-30.24" x2="-30.24" y2="30.24" stroke="white" stroke-width="8.64" stroke-linecap="round"/>
<!-- Small decorative elements -->
<circle cx="0" cy="0" r="5.76" fill="white"/>
<circle cx="21.599999999999998" cy="0" r="2.88" fill="white"/>
<circle cx="-21.599999999999998" cy="0" r="2.88" fill="white"/>
<circle cx="0" cy="21.599999999999998" r="2.88" fill="white"/>
<circle cx="0" cy="-21.599999999999998" r="2.88" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

32
public/icons/icon-152.svg Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="152" height="152" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="76" cy="76" r="76" fill="url(#bg)"/>
<!-- Ice crystal/snowflake icon -->
<g transform="translate(76, 76)">
<!-- Main cross -->
<line x1="-45.6" y1="0" x2="45.6" y2="0" stroke="white" stroke-width="12.16" stroke-linecap="round"/>
<line x1="0" y1="-45.6" x2="0" y2="45.6" stroke="white" stroke-width="12.16" stroke-linecap="round"/>
<!-- Diagonal lines -->
<line x1="-31.919999999999998" y1="-31.919999999999998" x2="31.919999999999998" y2="31.919999999999998" stroke="white" stroke-width="9.12" stroke-linecap="round"/>
<line x1="31.919999999999998" y1="-31.919999999999998" x2="-31.919999999999998" y2="31.919999999999998" stroke="white" stroke-width="9.12" stroke-linecap="round"/>
<!-- Small decorative elements -->
<circle cx="0" cy="0" r="6.08" fill="white"/>
<circle cx="22.8" cy="0" r="3.04" fill="white"/>
<circle cx="-22.8" cy="0" r="3.04" fill="white"/>
<circle cx="0" cy="22.8" r="3.04" fill="white"/>
<circle cx="0" cy="-22.8" r="3.04" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="192" height="192" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="96" cy="96" r="96" fill="url(#bg)"/>
<!-- Ice crystal/snowflake icon -->
<g transform="translate(96, 96)">
<!-- Main cross -->
<line x1="-46.08" y1="0" x2="46.08" y2="0" stroke="white" stroke-width="12.288" stroke-linecap="round"/>
<line x1="0" y1="-46.08" x2="0" y2="46.08" stroke="white" stroke-width="12.288" stroke-linecap="round"/>
<!-- Diagonal lines -->
<line x1="-32.256" y1="-32.256" x2="32.256" y2="32.256" stroke="white" stroke-width="9.216" stroke-linecap="round"/>
<line x1="32.256" y1="-32.256" x2="-32.256" y2="32.256" stroke="white" stroke-width="9.216" stroke-linecap="round"/>
<!-- Small decorative elements -->
<circle cx="0" cy="0" r="6.144" fill="white"/>
<circle cx="23.04" cy="0" r="3.072" fill="white"/>
<circle cx="-23.04" cy="0" r="3.072" fill="white"/>
<circle cx="0" cy="23.04" r="3.072" fill="white"/>
<circle cx="0" cy="-23.04" r="3.072" fill="white"/>
</g>
<!-- Safe zone indicator (invisible) -->
<circle cx="96" cy="96" r="76.80000000000001" fill="none" stroke="none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

32
public/icons/icon-192.svg Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="192" height="192" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="96" cy="96" r="96" fill="url(#bg)"/>
<!-- Ice crystal/snowflake icon -->
<g transform="translate(96, 96)">
<!-- Main cross -->
<line x1="-57.599999999999994" y1="0" x2="57.599999999999994" y2="0" stroke="white" stroke-width="15.36" stroke-linecap="round"/>
<line x1="0" y1="-57.599999999999994" x2="0" y2="57.599999999999994" stroke="white" stroke-width="15.36" stroke-linecap="round"/>
<!-- Diagonal lines -->
<line x1="-40.32" y1="-40.32" x2="40.32" y2="40.32" stroke="white" stroke-width="11.52" stroke-linecap="round"/>
<line x1="40.32" y1="-40.32" x2="-40.32" y2="40.32" stroke="white" stroke-width="11.52" stroke-linecap="round"/>
<!-- Small decorative elements -->
<circle cx="0" cy="0" r="7.68" fill="white"/>
<circle cx="28.799999999999997" cy="0" r="3.84" fill="white"/>
<circle cx="-28.799999999999997" cy="0" r="3.84" fill="white"/>
<circle cx="0" cy="28.799999999999997" r="3.84" fill="white"/>
<circle cx="0" cy="-28.799999999999997" r="3.84" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

32
public/icons/icon-384.svg Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="384" height="384" viewBox="0 0 384 384" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="192" cy="192" r="192" fill="url(#bg)"/>
<!-- Ice crystal/snowflake icon -->
<g transform="translate(192, 192)">
<!-- Main cross -->
<line x1="-115.19999999999999" y1="0" x2="115.19999999999999" y2="0" stroke="white" stroke-width="30.72" stroke-linecap="round"/>
<line x1="0" y1="-115.19999999999999" x2="0" y2="115.19999999999999" stroke="white" stroke-width="30.72" stroke-linecap="round"/>
<!-- Diagonal lines -->
<line x1="-80.64" y1="-80.64" x2="80.64" y2="80.64" stroke="white" stroke-width="23.04" stroke-linecap="round"/>
<line x1="80.64" y1="-80.64" x2="-80.64" y2="80.64" stroke="white" stroke-width="23.04" stroke-linecap="round"/>
<!-- Small decorative elements -->
<circle cx="0" cy="0" r="15.36" fill="white"/>
<circle cx="57.599999999999994" cy="0" r="7.68" fill="white"/>
<circle cx="-57.599999999999994" cy="0" r="7.68" fill="white"/>
<circle cx="0" cy="57.599999999999994" r="7.68" fill="white"/>
<circle cx="0" cy="-57.599999999999994" r="7.68" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="256" cy="256" r="256" fill="url(#bg)"/>
<!-- Ice crystal/snowflake icon -->
<g transform="translate(256, 256)">
<!-- Main cross -->
<line x1="-122.88" y1="0" x2="122.88" y2="0" stroke="white" stroke-width="32.768" stroke-linecap="round"/>
<line x1="0" y1="-122.88" x2="0" y2="122.88" stroke="white" stroke-width="32.768" stroke-linecap="round"/>
<!-- Diagonal lines -->
<line x1="-86.016" y1="-86.016" x2="86.016" y2="86.016" stroke="white" stroke-width="24.576" stroke-linecap="round"/>
<line x1="86.016" y1="-86.016" x2="-86.016" y2="86.016" stroke="white" stroke-width="24.576" stroke-linecap="round"/>
<!-- Small decorative elements -->
<circle cx="0" cy="0" r="16.384" fill="white"/>
<circle cx="61.44" cy="0" r="8.192" fill="white"/>
<circle cx="-61.44" cy="0" r="8.192" fill="white"/>
<circle cx="0" cy="61.44" r="8.192" fill="white"/>
<circle cx="0" cy="-61.44" r="8.192" fill="white"/>
</g>
<!-- Safe zone indicator (invisible) -->
<circle cx="256" cy="256" r="204.8" fill="none" stroke="none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

32
public/icons/icon-512.svg Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="256" cy="256" r="256" fill="url(#bg)"/>
<!-- Ice crystal/snowflake icon -->
<g transform="translate(256, 256)">
<!-- Main cross -->
<line x1="-153.6" y1="0" x2="153.6" y2="0" stroke="white" stroke-width="40.96" stroke-linecap="round"/>
<line x1="0" y1="-153.6" x2="0" y2="153.6" stroke="white" stroke-width="40.96" stroke-linecap="round"/>
<!-- Diagonal lines -->
<line x1="-107.52" y1="-107.52" x2="107.52" y2="107.52" stroke="white" stroke-width="30.72" stroke-linecap="round"/>
<line x1="107.52" y1="-107.52" x2="-107.52" y2="107.52" stroke="white" stroke-width="30.72" stroke-linecap="round"/>
<!-- Small decorative elements -->
<circle cx="0" cy="0" r="20.48" fill="white"/>
<circle cx="76.8" cy="0" r="10.24" fill="white"/>
<circle cx="-76.8" cy="0" r="10.24" fill="white"/>
<circle cx="0" cy="76.8" r="10.24" fill="white"/>
<circle cx="0" cy="-76.8" r="10.24" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

32
public/icons/icon-72.svg Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72" height="72" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="36" cy="36" r="36" fill="url(#bg)"/>
<!-- Ice crystal/snowflake icon -->
<g transform="translate(36, 36)">
<!-- Main cross -->
<line x1="-21.599999999999998" y1="0" x2="21.599999999999998" y2="0" stroke="white" stroke-width="5.76" stroke-linecap="round"/>
<line x1="0" y1="-21.599999999999998" x2="0" y2="21.599999999999998" stroke="white" stroke-width="5.76" stroke-linecap="round"/>
<!-- Diagonal lines -->
<line x1="-15.12" y1="-15.12" x2="15.12" y2="15.12" stroke="white" stroke-width="4.32" stroke-linecap="round"/>
<line x1="15.12" y1="-15.12" x2="-15.12" y2="15.12" stroke="white" stroke-width="4.32" stroke-linecap="round"/>
<!-- Small decorative elements -->
<circle cx="0" cy="0" r="2.88" fill="white"/>
<circle cx="10.799999999999999" cy="0" r="1.44" fill="white"/>
<circle cx="-10.799999999999999" cy="0" r="1.44" fill="white"/>
<circle cx="0" cy="10.799999999999999" r="1.44" fill="white"/>
<circle cx="0" cy="-10.799999999999999" r="1.44" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

32
public/icons/icon-96.svg Normal file
View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="96" height="96" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2196F3;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1976D2;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="48" cy="48" r="48" fill="url(#bg)"/>
<!-- Ice crystal/snowflake icon -->
<g transform="translate(48, 48)">
<!-- Main cross -->
<line x1="-28.799999999999997" y1="0" x2="28.799999999999997" y2="0" stroke="white" stroke-width="7.68" stroke-linecap="round"/>
<line x1="0" y1="-28.799999999999997" x2="0" y2="28.799999999999997" stroke="white" stroke-width="7.68" stroke-linecap="round"/>
<!-- Diagonal lines -->
<line x1="-20.16" y1="-20.16" x2="20.16" y2="20.16" stroke="white" stroke-width="5.76" stroke-linecap="round"/>
<line x1="20.16" y1="-20.16" x2="-20.16" y2="20.16" stroke="white" stroke-width="5.76" stroke-linecap="round"/>
<!-- Small decorative elements -->
<circle cx="0" cy="0" r="3.84" fill="white"/>
<circle cx="14.399999999999999" cy="0" r="1.92" fill="white"/>
<circle cx="-14.399999999999999" cy="0" r="1.92" fill="white"/>
<circle cx="0" cy="14.399999999999999" r="1.92" fill="white"/>
<circle cx="0" cy="-14.399999999999999" r="1.92" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -4,8 +4,26 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Great Lakes Ice Report</title>
<link rel="icon" type="image/svg+xml" href="https://iceymi.b-cdn.net/favicon.svg">
<!-- PWA Meta Tags -->
<meta name="description" content="Community-driven winter road conditions and icy hazards tracker for the Great Lakes region">
<meta name="theme-color" content="#2196F3">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Ice Report">
<meta name="msapplication-TileColor" content="#2196F3">
<meta name="msapplication-config" content="/browserconfig.xml">
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json">
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icons/favicon.svg">
<link rel="icon" type="image/x-icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIj4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLnNub3dmbGFrZSB7IGZpbGw6ICMyMTk2RjM7IH0KICAgICAgLmNlbnRlciB7IGZpbGw6ICMxOTc2RDI7IH0KICAgIDwvc3R5bGU+CiAgPC9kZWZzPgogIAogIDxnIGNsYXNzPSJzbm93Zmxha2UiPgogICAgPHJlY3QgeD0iMTUiIHk9IjIiIHdpZHRoPSIyIiBoZWlnaHQ9IjI4IiAvPgogICAgPHJlY3QgeD0iMiIgeT0iMTUiIHdpZHRoPSIyOCIgaGVpZ2h0PSIyIiAvPgogICAgPHJlY3QgeD0iMTUiIHk9IjIiIHdpZHRoPSIyIiBoZWlnaHQ9IjI4IiB0cmFuc2Zvcm09InJvdGF0ZSg0NSAxNiAxNikiIC8+CiAgICA8cmVjdCB4PSIxNSIgeT0iMiIgd2lkdGg9IjIiIGhlaWdodD0iMjgiIHRyYW5zZm9ybT0icm90YXRlKC00NSAxNiAxNikiIC8+CiAgICA8cG9seWdvbiBwb2ludHM9IjE2LDIgMTQsNiAxOCw2IiAvPgogICAgPHBvbHlnb24gcG9pbnRzPSIxNiwzMCAxNCwyNiAxOCwyNiIgLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iMiwxNiA2LDE0IDYsMTgiIC8+CiAgICA8cG9seWdvbiBwb2ludHM9IjMwLDE2IDI2LDE0IDI2LDE4IiAvPgogICAgPHBvbHlnb24gcG9pbnRzPSI2LjMsNi4zIDguNiw0IDkuOSw3LjciIHRyYW5zZm9ybT0icm90YXRlKDQ1IDE2IDE2KSIgLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iMjUuNywyNS43IDIzLjQsMjggMjIuMSwyNC4zIiB0cmFuc2Zvcm09InJvdGF0ZSg0NSAxNiAxNikiIC8+CiAgICA8cG9seWdvbiBwb2ludHM9IjYuMywyNS43IDguNiwyOCA5LjksMjQuMyIgdHJhbnNmb3JtPSJyb3RhdGUoLTQ1IDE2IDE2KSIgLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iMjUuNyw2LjMgMjMuNCw0IDIyLjEsNy43IiB0cmFuc2Zvcm09InJvdGF0ZSgtNDUgMTYgMTYpIiAvPgogIDwvZz4KICA8Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIzIiBjbGFzcz0iY2VudGVyIiAvPgo8L3N2Zz4K">
<!-- Apple Touch Icons -->
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152.svg">
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192.svg">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="style.css">
<script>
@ -154,5 +172,58 @@ placeholder="Enter address, intersection (e.g., Main St & Second St, City), or l
<script src="theme-utils.js"></script>
<script src="utils.js"></script>
<script src="app-mapbox.js"></script>
<!-- PWA Service Worker Registration -->
<script>
// Register service worker for PWA functionality
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('✅ Service Worker registered successfully:', registration.scope);
// Check for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
if (newWorker) {
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// New content available, notify user
console.log('🔄 New content available! Please refresh.');
// Could show a notification here
}
}
});
}
});
})
.catch((error) => {
console.warn('❌ Service Worker registration failed:', error);
});
});
} else {
console.log('Service Worker not supported in this browser');
}
// PWA Install Prompt
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
console.log('💾 PWA install prompt available');
// Prevent Chrome 67 and earlier from automatically showing the prompt
e.preventDefault();
// Stash the event so it can be triggered later
deferredPrompt = e;
// Could show custom install button here
// For now, let the browser handle it naturally
});
window.addEventListener('appinstalled', (evt) => {
console.log('🎉 PWA was installed successfully!');
deferredPrompt = null;
});
</script>
</body>
</html>

75
public/manifest.json Normal file
View file

@ -0,0 +1,75 @@
{
"name": "Great Lakes Ice Report",
"short_name": "Ice Report",
"description": "Community-driven winter road conditions and icy hazards tracker for the Great Lakes region",
"start_url": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#ffffff",
"theme_color": "#2196F3",
"lang": "en-US",
"scope": "/",
"categories": ["weather", "transportation", "utilities"],
"icons": [
{
"src": "/icons/icon-72.svg",
"sizes": "72x72",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/icons/icon-96.svg",
"sizes": "96x96",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/icons/icon-128.svg",
"sizes": "128x128",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/icons/icon-144.svg",
"sizes": "144x144",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/icons/icon-152.svg",
"sizes": "152x152",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/icons/icon-192.svg",
"sizes": "192x192",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/icons/icon-384.svg",
"sizes": "384x384",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/icons/icon-512.svg",
"sizes": "512x512",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/icons/icon-192-maskable.svg",
"sizes": "192x192",
"type": "image/svg+xml",
"purpose": "maskable"
},
{
"src": "/icons/icon-512-maskable.svg",
"sizes": "512x512",
"type": "image/svg+xml",
"purpose": "maskable"
}
]
}

89
public/offline.html Normal file
View file

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Offline - Great Lakes Ice Report</title>
<link rel="stylesheet" href="style.css">
<style>
.offline-container {
max-width: 600px;
margin: 50px auto;
padding: 2rem;
text-align: center;
}
.offline-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.7;
}
.offline-message {
background: var(--card-bg);
color: var(--text-color);
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.retry-btn {
margin-top: 1rem;
padding: 0.75rem 1.5rem;
background-color: var(--primary-color, #2196F3);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.retry-btn:hover {
opacity: 0.9;
}
</style>
</head>
<body>
<div class="offline-container">
<div class="offline-message">
<div class="offline-icon">📡</div>
<h1>You're Offline</h1>
<p>It looks like you've lost your internet connection. The Great Lakes Ice Report needs an internet connection to show current road conditions and submit new reports.</p>
<p><strong>When you're back online, you'll be able to:</strong></p>
<ul style="text-align: left; max-width: 400px; margin: 1rem auto;">
<li>View current ice condition reports</li>
<li>Submit new hazard reports</li>
<li>Get real-time location updates</li>
<li>Access the interactive map</li>
</ul>
<button class="retry-btn" onclick="window.location.reload()">
🔄 Try Again
</button>
<p style="margin-top: 1rem; font-size: 0.9rem; opacity: 0.8;">
This app works best with an internet connection for up-to-date safety information.
</p>
</div>
</div>
<script>
// Auto-reload when connection is restored
window.addEventListener('online', () => {
window.location.reload();
});
// Apply theme from localStorage if available
(function() {
const savedTheme = localStorage.getItem('theme') || 'auto';
if (savedTheme === 'auto') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
} else {
document.documentElement.setAttribute('data-theme', savedTheme);
}
})();
</script>
</body>
</html>

View file

@ -4,8 +4,24 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Privacy Policy - Great Lakes Ice Report</title>
<link rel="icon" type="image/svg+xml" href="https://iceymi.b-cdn.net/favicon.svg">
<!-- PWA Meta Tags -->
<meta name="description" content="Privacy policy for Great Lakes Ice Report - winter road conditions tracker">
<meta name="theme-color" content="#2196F3">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Ice Report Privacy">
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json">
<!-- Icons -->
<link rel="icon" type="image/svg+xml" href="/icons/favicon.svg">
<link rel="icon" type="image/x-icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiIgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIj4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLnNub3dmbGFrZSB7IGZpbGw6ICMyMTk2RjM7IH0KICAgICAgLmNlbnRlciB7IGZpbGw6ICMxOTc2RDI7IH0KICAgIDwvc3R5bGU+CiAgPC9kZWZzPgogIAogIDxnIGNsYXNzPSJzbm93Zmxha2UiPgogICAgPHJlY3QgeD0iMTUiIHk9IjIiIHdpZHRoPSIyIiBoZWlnaHQ9IjI4IiAvPgogICAgPHJlY3QgeD0iMiIgeT0iMTUiIHdpZHRoPSIyOCIgaGVpZ2h0PSIyIiAvPgogICAgPHJlY3QgeD0iMTUiIHk9IjIiIHdpZHRoPSIyIiBoZWlnaHQ9IjI4IiB0cmFuc2Zvcm09InJvdGF0ZSg0NSAxNiAxNikiIC8+CiAgICA8cmVjdCB4PSIxNSIgeT0iMiIgd2lkdGg9IjIiIGhlaWdodD0iMjgiIHRyYW5zZm9ybT0icm90YXRlKC00NSAxNiAxNikiIC8+CiAgICA8cG9seWdvbiBwb2ludHM9IjE2LDIgMTQsNiAxOCw2IiAvPgogICAgPHBvbHlnb24gcG9pbnRzPSIxNiwzMCAxNCwyNiAxOCwyNiIgLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iMiwxNiA2LDE0IDYsMTgiIC8+CiAgICA8cG9seWdvbiBwb2ludHM9IjMwLDE2IDI2LDE0IDI2LDE4IiAvPgogICAgPHBvbHlnb24gcG9pbnRzPSI2LjMsNi4zIDguNiw0IDkuOSw3LjciIHRyYW5zZm9ybT0icm90YXRlKDQ1IDE2IDE2KSIgLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iMjUuNywyNS43IDIzLjQsMjggMjIuMSwyNC4zIiB0cmFuc2Zvcm09InJvdGF0ZSg0NSAxNiAxNikiIC8+CiAgICA8cG9seWdvbiBwb2ludHM9IjYuMywyNS43IDguNiwyOCA5LjksMjQuMyIgdHJhbnNmb3JtPSJyb3RhdGUoLTQ1IDE2IDE2KSIgLz4KICAgIDxwb2x5Z29uIHBvaW50cz0iMjUuNyw2LjMgMjMuNCw0IDIyLjEsNy43IiB0cmFuc2Zvcm09InJvdGF0ZSgtNDUgMTYgMTYpIiAvPgogIDwvZz4KICA8Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIzIiBjbGFzcz0iY2VudGVyIiAvPgo8L3N2Zz4K">
<!-- Apple Touch Icons -->
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152.svg">
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-192.svg">
<link rel="stylesheet" href="style.css">
<script>
// Apply theme immediately to prevent flash

143
public/sw.js Normal file
View file

@ -0,0 +1,143 @@
// Service Worker for Great Lakes Ice Report PWA
const CACHE_NAME = 'ice-report-v1';
const OFFLINE_URL = '/offline.html';
// Files to cache for offline functionality
const CACHE_FILES = [
'/',
'/index.html',
'/admin.html',
'/privacy.html',
'/style.css',
'/app.js',
'/admin.js',
'/utils.js',
'/manifest.json',
OFFLINE_URL
];
// Install event - cache essential files
self.addEventListener('install', (event) => {
console.log('Service Worker: Installing...');
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Service Worker: Caching files');
return cache.addAll(CACHE_FILES);
})
.then(() => {
console.log('Service Worker: Files cached successfully');
return self.skipWaiting();
})
.catch((error) => {
console.error('Service Worker: Cache failed:', error);
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('Service Worker: Activating...');
event.waitUntil(
caches.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames
.filter((cacheName) => cacheName !== CACHE_NAME)
.map((cacheName) => {
console.log('Service Worker: Deleting old cache:', cacheName);
return caches.delete(cacheName);
})
);
})
.then(() => {
console.log('Service Worker: Activated');
return self.clients.claim();
})
);
});
// Fetch event - serve cached content when offline
self.addEventListener('fetch', (event) => {
// Only handle GET requests
if (event.request.method !== 'GET') {
return;
}
// Skip cross-origin requests
if (!event.request.url.startsWith(self.location.origin)) {
return;
}
event.respondWith(
caches.match(event.request)
.then((response) => {
// Return cached version if available
if (response) {
return response;
}
// Try to fetch from network
return fetch(event.request)
.then((response) => {
// Don't cache non-successful responses
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// Clone the response for caching
const responseToCache = response.clone();
// Cache successful responses for static assets
if (event.request.url.match(/\.(js|css|html|png|jpg|jpeg|gif|svg|ico|woff|woff2)$/)) {
caches.open(CACHE_NAME)
.then((cache) => {
cache.put(event.request, responseToCache);
});
}
return response;
})
.catch(() => {
// If network fails and we're requesting an HTML page, show offline page
if (event.request.headers.get('accept').includes('text/html')) {
return caches.match(OFFLINE_URL);
}
// For other requests, just fail
throw new Error('Network failed and no cache available');
});
})
);
});
// Background sync for when connection is restored
self.addEventListener('sync', (event) => {
if (event.tag === 'background-sync') {
console.log('Service Worker: Background sync triggered');
// Could implement queued location submissions here
}
});
// Push notifications (for future use)
self.addEventListener('push', (event) => {
if (event.data) {
const data = event.data.json();
console.log('Service Worker: Push message received:', data);
// Could show notifications about severe weather warnings
const options = {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/icon-72.png',
tag: 'ice-report',
renotify: true
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
}
});