teecup/frontend/components/install-prompt.tsx
Erol Haagenrud 2562e20ec3 13-årsgrense — håndhevet server-side (eneste vei inn i appen, ADR-051) og med umiddelbar rød feilmelding klientside i begge profil-skjemaer. Testet på nøyaktig dagsgrensen.
PWA-installasjonstekst — skiller nå Safari/Chrome på iOS, ikke lenger feilaktig Safari-only.
Beskrivelsestekst — "golfklubber" fjernet, nytt punkt om at turneringsadmin/-presentasjon fungerer godt på PC.
Dette krever både teecup_api og teecup_frontend (ingen migrasjon).
2026-08-10 07:00:11 +02:00

191 lines
7.5 KiB
TypeScript

"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)
}
// Siden iOS 16.4 kan en PWA installeres fra Chrome på iPhone også, ikke
// bare Safari (brukerønske 2026-08-10, tidligere tekst her hevdet feilaktig
// at man MÅTTE til Safari -- se PDF-vedlegg "Installere PWA fra Chrome på
// iPhone"). Del-ikonets PLASSERING er derimot ulik mellom nettleserne
// (Safari: nederst på skjermen. Chrome: oppe til høyre i adressefeltet),
// så instruksjonsteksten må skille dem for å faktisk stemme -- ikke bare
// fjerne Safari-nevnelsen og la den bli vag. Chrome-på-iOS har "CriOS" i
// user agent-strengen (deler ellers WebKit-motoren med Safari, så vanlig
// "Chrome"-sniffing ville feilaktig truffet Safari også).
function iosBrowser(): "safari" | "chrome" | "other" {
if (typeof window === "undefined") return "other"
const ua = window.navigator.userAgent
if (/crios/i.test(ua)) return "chrome"
if (/safari/i.test(ua) && !/crios|fxios|edgios/i.test(ua)) return "safari"
return "other"
}
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 ELLER Chrome, siden iOS 16.4 -- se iosBrowser() over):
// Apple har ALDRI implementert beforeinstallprompt der -- 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 [iosBrowserKind, setIosBrowserKind] = useState<"safari" | "chrome" | "other">("other")
const [deferredEvent, setDeferredEvent] = useState<BeforeInstallPromptEvent | null>(null)
useEffect(() => {
if (isStandalone() || isDismissedForNow()) return
if (isIOS()) {
setPlatform("ios")
setIosBrowserKind(iosBrowser())
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)
}
// "Clubhouse"-visuell redesign (2026-08-07) -- samme `--tee`/`--tee-strong`-
// familie som resten av dashbordets nye reskinn, inline-anvendt siden dette
// er den eneste komponenten som trenger den. All logikk over uendret.
return (
<div
role="region"
aria-label="Installer TeeCup"
className="relative overflow-hidden rounded-2xl p-4 sm:p-5"
style={{
background: "linear-gradient(180deg, #2f6b1e 0%, #1c4212 100%)",
boxShadow: "0 18px 40px -22px rgba(23, 32, 15, 0.5)",
}}
>
<div className="flex items-start gap-3.5">
<span className="flex size-11 shrink-0 items-center justify-center rounded-xl" style={{ backgroundColor: "rgba(255,255,255,0.12)" }}>
{platform === "ios" ? (
<Share aria-hidden="true" className="size-5" color="#d8f0c4" />
) : (
<Download aria-hidden="true" className="size-5" color="#d8f0c4" />
)}
</span>
<div className="flex min-w-0 flex-1 flex-col gap-3">
<div className="flex flex-col gap-1 pr-6">
<h2 className="text-base font-bold text-balance" style={{ color: "#ffffff" }}>
Legg TeeCup til hjemskjermen
</h2>
{platform === "ios" ? (
<p className="text-sm leading-relaxed text-pretty" style={{ color: "#d8f0c4" }}>
Trykk <Share aria-hidden="true" className="inline size-4 -translate-y-0.5" /> (Del){" "}
{iosBrowserKind === "chrome"
? "oppe til høyre i adressefeltet"
: iosBrowserKind === "safari"
? "nederst på skjermen"
: "i nettleseren din"}
, velg deretter <SquarePlus aria-hidden="true" className="inline size-4 -translate-y-0.5" />{" "}
«Legg til Hjem-skjerm».
</p>
) : (
<p className="text-sm leading-relaxed text-pretty" style={{ color: "#d8f0c4" }}>
Rask tilgang, fungerer som en egen app, og virker delvis uten nett.
</p>
)}
</div>
<div className="flex items-center gap-2">
{platform === "android" && (
<button
type="button"
onClick={handleInstall}
className="inline-flex min-h-[44px] items-center gap-1.5 rounded-xl px-4 text-sm font-bold transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white"
style={{ backgroundColor: "#ffffff", color: "#17200f" }}
>
<Download aria-hidden="true" className="size-4" />
Installer
</button>
)}
<button
type="button"
onClick={handleDismiss}
className="inline-flex min-h-[44px] items-center rounded-xl px-3 text-sm font-semibold transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white"
style={{ color: "#d8f0c4" }}
>
Ikke
</button>
</div>
</div>
</div>
<button
type="button"
onClick={handleDismiss}
aria-label="Lukk installasjonsoppfordringen"
className="absolute right-2 top-2 flex size-9 items-center justify-center rounded-lg transition-colors hover:bg-white/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white"
style={{ color: "#d8f0c4" }}
>
<X aria-hidden="true" className="size-4" />
</button>
</div>
)
}