Refactored frontend directory
Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Failing after 2m46s
BIN
frontend/public/child-mode.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
frontend/public/edit.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
frontend/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
frontend/public/icons/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
frontend/public/icons/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
BIN
frontend/public/images/bc_logo.png
Normal file
|
After Width: | Height: | Size: 255 KiB |
BIN
frontend/public/images/c_logo.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
frontend/public/images/chorly_logo.png
Normal file
|
After Width: | Height: | Size: 401 KiB |
BIN
frontend/public/images/feature00.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
frontend/public/images/feature01.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
frontend/public/images/feature02.png
Normal file
|
After Width: | Height: | Size: 2.0 MiB |
BIN
frontend/public/images/feature03.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
frontend/public/images/full_logo.png
Normal file
|
After Width: | Height: | Size: 392 KiB |
BIN
frontend/public/images/hero.png
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
29
frontend/public/manifest.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"id": "/",
|
||||
"name": "Chorly",
|
||||
"short_name": "Chorly",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#4a90e2",
|
||||
"background_color": "#ffffff",
|
||||
"description": "Track chores and rewards for your family",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.ico",
|
||||
"sizes": "32x32",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
frontend/public/nav/children.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
frontend/public/nav/chores.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
frontend/public/nav/notifications.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
frontend/public/nav/rewards.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
frontend/public/profile.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
frontend/public/sign-out.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
122
frontend/public/sw.js
Normal file
@@ -0,0 +1,122 @@
|
||||
/* Service Worker for Reward App push notifications */
|
||||
|
||||
/* Activate immediately — no waiting for old tabs to close */
|
||||
self.addEventListener('install', function (event) {
|
||||
event.waitUntil(self.skipWaiting())
|
||||
})
|
||||
|
||||
self.addEventListener('activate', function (event) {
|
||||
event.waitUntil(self.clients.claim())
|
||||
})
|
||||
|
||||
self.addEventListener('push', function (event) {
|
||||
if (!event.data) return
|
||||
|
||||
let payload
|
||||
try {
|
||||
payload = event.data.json()
|
||||
} catch (e) {
|
||||
payload = { title: 'Reward App', body: event.data.text() }
|
||||
}
|
||||
|
||||
const title = payload.title || 'Reward App'
|
||||
// Expiry-warning notifications are informational only — no action buttons
|
||||
const actions =
|
||||
payload.type === 'chore_expiring_soon'
|
||||
? []
|
||||
: [
|
||||
{ action: 'approve', title: 'Approve' },
|
||||
{ action: 'deny', title: 'Deny' },
|
||||
]
|
||||
const options = {
|
||||
body: payload.body || '',
|
||||
icon: '/icons/icon-192x192.png',
|
||||
data: payload,
|
||||
actions,
|
||||
}
|
||||
|
||||
event.waitUntil(self.registration.showNotification(title, options))
|
||||
})
|
||||
|
||||
self.addEventListener('notificationclick', function (event) {
|
||||
event.notification.close()
|
||||
|
||||
const payload = event.notification.data || {}
|
||||
const approveToken = payload.approve_token
|
||||
const denyToken = payload.deny_token
|
||||
const childId = payload.child_id
|
||||
const entityId = payload.entity_id
|
||||
const entityType = payload.entity_type
|
||||
|
||||
function navigateOrOpen(url) {
|
||||
return clients
|
||||
.matchAll({ type: 'window', includeUncontrolled: true })
|
||||
.then(function (clientList) {
|
||||
const sameOriginClients = clientList.filter(function (c) {
|
||||
return new URL(c.url).origin === self.location.origin
|
||||
})
|
||||
if (sameOriginClients.length > 0) {
|
||||
const client = sameOriginClients[0]
|
||||
return client.focus().then(function () {
|
||||
return client.navigate(url)
|
||||
})
|
||||
}
|
||||
return clients.openWindow(url)
|
||||
})
|
||||
}
|
||||
|
||||
function executeDigestAction(token) {
|
||||
return fetch('/api/digest-action/' + token, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
})
|
||||
.then(function (res) {
|
||||
if (res.ok) {
|
||||
// Action executed — navigate to parent view to show the result
|
||||
var successUrl =
|
||||
childId && entityId && entityType
|
||||
? '/parent/' + childId + '?scrollTo=' + entityId + '&entityType=' + entityType
|
||||
: '/parent'
|
||||
return navigateOrOpen(successUrl)
|
||||
}
|
||||
if (res.status === 401) {
|
||||
// Not authenticated — navigate with digestToken so the PIN flow executes the action
|
||||
var authUrl =
|
||||
childId && entityId && entityType
|
||||
? '/parent/' +
|
||||
childId +
|
||||
'?digestToken=' +
|
||||
token +
|
||||
'&scrollTo=' +
|
||||
entityId +
|
||||
'&entityType=' +
|
||||
entityType
|
||||
: '/parent'
|
||||
return navigateOrOpen(authUrl)
|
||||
}
|
||||
// Token invalid/used/expired — navigate to parent
|
||||
return navigateOrOpen(childId ? '/parent/' + childId : '/parent')
|
||||
})
|
||||
.catch(function () {
|
||||
return navigateOrOpen('/parent')
|
||||
})
|
||||
}
|
||||
|
||||
if (event.action === 'approve' && approveToken) {
|
||||
event.waitUntil(executeDigestAction(approveToken))
|
||||
return
|
||||
}
|
||||
|
||||
if (event.action === 'deny' && denyToken) {
|
||||
event.waitUntil(executeDigestAction(denyToken))
|
||||
return
|
||||
}
|
||||
|
||||
// Body tap — open or focus the app then navigate to the deep link
|
||||
const deepLinkUrl =
|
||||
childId && entityId && entityType
|
||||
? '/parent/' + childId + '?scrollTo=' + entityId + '&entityType=' + entityType
|
||||
: '/parent'
|
||||
|
||||
event.waitUntil(navigateOrOpen(deepLinkUrl))
|
||||
})
|
||||