teecup/frontend/components/install-prompt.tsx

151 lines
5.2 KiB
TypeScript
Raw Normal View History

Update Todos Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner) Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1) Design og bygg scramble/greensome-statistikk (valgt utslag per spiller) Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker) Scratch-verifiser backend for alle fire (27/27 sjekker gjort) Browserverifiser alle fire i nettleser mot scratch-backend Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md Alle fire er ferdigstilt, verifisert og live. Kort oppsummert: PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten. Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database. Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen). Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet. 27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
"use client"
import { useEffect, useState } from "react"
import { Download, Share, SquarePlus, X } from "lucide-react"
import {
clearDeferredInstallPrompt,
getDeferredInstallPrompt,
subscribeInstallPrompt,
type BeforeInstallPromptEvent,
} from "@/lib/pwa-install"
const DISMISS_KEY = "teecup-install-dismissed-until"
const DISMISS_COOLDOWN_DAYS = 14
function isStandalone() {
if (typeof window === "undefined") return false
const nav = window.navigator as Navigator & { standalone?: boolean }
return window.matchMedia("(display-mode: standalone)").matches || nav.standalone === true
}
function isIOS() {
if (typeof window === "undefined") return false
return /iphone|ipad|ipod/i.test(window.navigator.userAgent)
}
function isDismissedForNow() {
if (typeof window === "undefined") return true
const raw = window.localStorage.getItem(DISMISS_KEY)
if (!raw) return false
const until = Number(raw)
return Number.isFinite(until) && Date.now() < until
}
function dismissForNow() {
const until = Date.now() + DISMISS_COOLDOWN_DAYS * 24 * 60 * 60 * 1000
window.localStorage.setItem(DISMISS_KEY, String(until))
}
// PWA-installasjonsoppfordring (FEATURE_BACKLOG.md "PWA-installasjon").
// To distinkte varianter -- INGEN visning for andre nettlesere uten en
// reell installasjonsvei:
// - Android/Chrome-familien: ekte "Installer"-knapp via det fangede
// beforeinstallprompt-eventet (lib/pwa-install.ts).
// - iOS Safari: Apple har ALDRI implementert beforeinstallprompt -- kun et
// instruksjonsbanner (Del-ikon -> "Legg til på Hjemskjerm") er mulig.
export function InstallPrompt() {
const [dismissed, setDismissed] = useState(true)
const [platform, setPlatform] = useState<"android" | "ios" | "none">("none")
const [deferredEvent, setDeferredEvent] = useState<BeforeInstallPromptEvent | null>(null)
useEffect(() => {
if (isStandalone() || isDismissedForNow()) return
if (isIOS()) {
setPlatform("ios")
setDismissed(false)
return
}
// Android/Chrome-familien: vis kun når/hvis eventet faktisk fyres --
// andre nettlesere uten en reell installasjonsvei skal ikke vise noe.
const existing = getDeferredInstallPrompt()
if (existing) {
setDeferredEvent(existing)
setPlatform("android")
setDismissed(false)
}
const unsubscribe = subscribeInstallPrompt((event) => {
if (event) {
setDeferredEvent(event)
setPlatform("android")
setDismissed(false)
}
})
return unsubscribe
}, [])
if (dismissed || platform === "none") return null
async function handleInstall() {
if (!deferredEvent) return
await deferredEvent.prompt()
await deferredEvent.userChoice
clearDeferredInstallPrompt()
setDismissed(true)
}
function handleDismiss() {
dismissForNow()
setDismissed(true)
}
return (
<div
role="region"
aria-label="Installer TeeCup"
2026-07-29 12:44:51 +02:00
className="flex items-start gap-3 rounded-2xl border border-border bg-card p-4 shadow-md shadow-black/8"
Update Todos Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner) Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1) Design og bygg scramble/greensome-statistikk (valgt utslag per spiller) Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker) Scratch-verifiser backend for alle fire (27/27 sjekker gjort) Browserverifiser alle fire i nettleser mot scratch-backend Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md Alle fire er ferdigstilt, verifisert og live. Kort oppsummert: PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten. Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database. Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen). Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet. 27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
>
<div className="mt-0.5 flex size-10 shrink-0 items-center justify-center rounded-full bg-primary/15 text-primary">
{platform === "ios" ? (
<Share aria-hidden="true" className="size-5" />
) : (
<Download aria-hidden="true" className="size-5" />
)}
</div>
<div className="flex-1">
<p className="text-sm font-bold text-foreground">Legg TeeCup til hjemskjermen</p>
{platform === "ios" ? (
<p className="mt-1 text-sm leading-relaxed text-muted-foreground text-pretty">
Trykk <Share aria-hidden="true" className="inline size-4 -translate-y-0.5" /> (Del) nederst i Safari,
velg deretter <SquarePlus aria-hidden="true" className="inline size-4 -translate-y-0.5" />{" "}
«Legg til Hjem-skjerm».
</p>
) : (
<p className="mt-1 text-sm leading-relaxed text-muted-foreground text-pretty">
Rask tilgang, fungerer som en egen app, og virker delvis uten nett.
</p>
)}
<div className="mt-3 flex flex-wrap gap-2">
{platform === "android" && (
<button
type="button"
onClick={handleInstall}
2026-07-29 12:44:51 +02:00
className="inline-flex min-h-[44px] items-center gap-1.5 rounded-lg bg-foreground px-4 text-sm font-bold text-background transition-colors hover:opacity-90"
Update Todos Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner) Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1) Design og bygg scramble/greensome-statistikk (valgt utslag per spiller) Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker) Scratch-verifiser backend for alle fire (27/27 sjekker gjort) Browserverifiser alle fire i nettleser mot scratch-backend Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md Alle fire er ferdigstilt, verifisert og live. Kort oppsummert: PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten. Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database. Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen). Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet. 27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
>
<Download aria-hidden="true" className="size-4" />
Installer
</button>
)}
<button
type="button"
onClick={handleDismiss}
className="inline-flex min-h-[44px] items-center gap-1.5 rounded-lg px-3 text-sm font-semibold text-muted-foreground transition-colors hover:text-foreground"
>
Ikke
</button>
</div>
</div>
<button
type="button"
onClick={handleDismiss}
aria-label="Lukk installasjonsoppfordringen"
className="flex size-11 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:text-foreground"
>
<X aria-hidden="true" className="size-4" />
</button>
</div>
)
}