This commit is contained in:
2026-10-23 22:31:08 -04:00
commit 270b402ccc
31 changed files with 15846 additions and 0 deletions

36
public/icon.svg Normal file
View File

@@ -0,0 +1,36 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<defs>
<linearGradient id="bgGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f59e0b" />
<stop offset="100%" stop-color="#b45309" />
</linearGradient>
<linearGradient id="tileGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="100%" stop-color="#f3f4f6" />
</linearGradient>
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
<feDropShadow dx="0" dy="8" stdDeviation="10" flood-opacity="0.25" />
</filter>
</defs>
<!-- Background iOS squircle -->
<rect width="512" height="512" rx="112" fill="url(#bgGrad)" />
<!-- 4 Quartile Tiles forming a grid/Q -->
<g filter="url(#shadow)">
<!-- Top Left -->
<rect x="116" y="116" width="124" height="124" rx="28" fill="url(#tileGrad)" />
<text x="178" y="200" font-family="-apple-system, BlinkMacSystemFont, 'Inter', sans-serif" font-weight="800" font-size="68" fill="#1f2937" text-anchor="middle">Q</text>
<!-- Top Right -->
<rect x="272" y="116" width="124" height="124" rx="28" fill="url(#tileGrad)" />
<text x="334" y="200" font-family="-apple-system, BlinkMacSystemFont, 'Inter', sans-serif" font-weight="800" font-size="68" fill="#1f2937" text-anchor="middle">U</text>
<!-- Bottom Left -->
<rect x="116" y="272" width="124" height="124" rx="28" fill="url(#tileGrad)" />
<text x="178" y="356" font-family="-apple-system, BlinkMacSystemFont, 'Inter', sans-serif" font-weight="800" font-size="68" fill="#1f2937" text-anchor="middle">A</text>
<!-- Bottom Right with emerald accent -->
<rect x="272" y="272" width="124" height="124" rx="28" fill="#10b981" />
<text x="334" y="356" font-family="-apple-system, BlinkMacSystemFont, 'Inter', sans-serif" font-weight="800" font-size="68" fill="#ffffff" text-anchor="middle">R</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,26 @@
{
"name": "Quartiles — Daily Word Puzzle",
"short_name": "Quartiles",
"description": "Combine letter tiles to form words, find the 5 hidden Quartiles, and score 100 points for Expert rank!",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#000000",
"theme_color": "#1c1c1e",
"categories": ["games", "puzzle"],
"icons": [
{
"src": "/icon.svg",
"sizes": "512x512",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/icon.svg",
"sizes": "512x512",
"type": "image/svg+xml",
"purpose": "maskable"
}
]
}

76
public/sw.js Normal file
View File

@@ -0,0 +1,76 @@
const CACHE_NAME = 'quartiles-pwa-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/manifest.webmanifest',
'/icon.svg'
];
// Install: Cache initial shell assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(STATIC_ASSETS);
}).then(() => self.skipWaiting())
);
});
// Activate: Clean up old cache versions
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys.map(key => {
if (key !== CACHE_NAME) {
return caches.delete(key);
}
})
);
}).then(() => self.clients.claim())
);
});
// Fetch: Stale-While-Revalidate for local assets, Network-First for HTML navigation
self.addEventListener('fetch', event => {
const request = event.request;
const url = new URL(request.url);
// Only handle GET requests
if (request.method !== 'GET') return;
// Handle HTML navigation requests (Offline support)
if (request.mode === 'navigate') {
event.respondWith(
fetch(request)
.then(response => {
if (response && response.status === 200) {
const responseClone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(request, responseClone));
}
return response;
})
.catch(() => caches.match('/index.html') || caches.match('/'))
);
return;
}
// Same-origin static assets or Google Fonts: Stale-While-Revalidate
if (url.origin === location.origin || url.hostname.includes('googleapis.com') || url.hostname.includes('gstatic.com')) {
event.respondWith(
caches.match(request).then(cachedResponse => {
const fetchPromise = fetch(request)
.then(networkResponse => {
if (networkResponse && networkResponse.status === 200) {
const responseClone = networkResponse.clone();
caches.open(CACHE_NAME).then(cache => cache.put(request, responseClone));
}
return networkResponse;
})
.catch(() => cachedResponse);
return cachedResponse || fetchPromise;
})
);
}
});