feat: add PWA install button to header

This commit is contained in:
root 2026-05-22 14:10:08 +00:00
parent a04e8edb6e
commit 89d6a01b68
3 changed files with 88 additions and 0 deletions

View File

@ -0,0 +1,76 @@
import React from "react";
/**
* Compact PWA install button for the header.
* - Shows 📲 icon when install prompt is available (Chrome/Edge on Android & desktop).
* - Shows iOS Safari instructions tooltip on click when on iOS.
* - Hidden when app is already installed (standalone mode).
* - After install: auto-hidden via appinstalled event.
*/
export const PwaInstallButton = ({ onInstall, isInstalled, isInstallAvailable }) => {
const [showTip, setShowTip] = React.useState(false);
const tipRef = React.useRef(null);
// Detect iOS (no beforeinstallprompt, but can be added to home screen)
const isIOS = typeof navigator !== "undefined" && /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const handleInstallClick = async () => {
if (isInstallAvailable && onInstall) {
await onInstall();
} else {
// Show instruction tip for iOS or unsupported browsers
setShowTip((prev) => !prev);
}
};
// Close tip on outside click
React.useEffect(() => {
if (!showTip) return;
const handler = (e) => {
if (tipRef.current && !tipRef.current.contains(e.target)) {
setShowTip(false);
}
};
document.addEventListener("click", handler);
return () => document.removeEventListener("click", handler);
}, [showTip]);
// Don't render if already installed as PWA AFTER all hooks
if (isInstalled) return null;
return (
<div className="relative inline-flex" ref={tipRef}>
<button
type="button"
onClick={handleInstallClick}
className="inline-flex h-8 w-8 items-center justify-center rounded-full text-base transition hover:bg-[var(--color-accent-soft)]"
aria-label="Установить приложение"
title="Установить приложение"
>
📲
</button>
{showTip && (
<div className="absolute right-0 top-full z-50 mt-2 w-60 rounded-xl border border-[var(--color-border)] bg-[var(--color-surface)] p-3 text-sm shadow-lg">
{isIOS ? (
<>
<p className="font-medium">Установка на iOS</p>
<ol className="mt-1.5 list-decimal pl-4 leading-relaxed text-[var(--color-text-muted)]">
<li>Откройте в <strong>Safari</strong></li>
<li>Нажмите <strong>Поделиться</strong> </li>
<li>Выберите <strong>«На экран Домой»</strong></li>
</ol>
</>
) : (
<>
<p className="font-medium">Установка</p>
<p className="mt-1 leading-relaxed text-[var(--color-text-muted)]">
Нажмите значок 📲 в адресной строке браузера или используйте меню <strong>«Установить приложение»</strong>.
</p>
</>
)}
</div>
)}
</div>
);
};

View File

@ -4,11 +4,15 @@ import { Badge } from "../components/UI/Badge";
import { Button } from "../components/UI/Button"; import { Button } from "../components/UI/Button";
import { Panel } from "../components/UI/Panel"; import { Panel } from "../components/UI/Panel";
import { ThemeToggle } from "../components/UI/ThemeToggle"; import { ThemeToggle } from "../components/UI/ThemeToggle";
import { PwaInstallButton } from "../components/UI/PwaInstallButton";
import { NotificationBell } from "../components/notifications/NotificationBell"; import { NotificationBell } from "../components/notifications/NotificationBell";
import { NotificationSettings } from "../components/notifications/NotificationSettings"; import { NotificationSettings } from "../components/notifications/NotificationSettings";
export const AppShell = ({ export const AppShell = ({
user, user,
onInstallApp,
isInstalled,
isInstallAvailable,
onSignOut, onSignOut,
onOpenGuide, onOpenGuide,
isGuideOpen = false, isGuideOpen = false,
@ -108,6 +112,7 @@ export const AppShell = ({
{isGuideOpen ? "Назад" : "?"} {isGuideOpen ? "Назад" : "?"}
</Button> </Button>
) : null} ) : null}
<PwaInstallButton onInstall={onInstallApp} isInstalled={isInstalled} isInstallAvailable={isInstallAvailable} />
<ThemeToggle /> <ThemeToggle />
<Button size="sm" variant="ghost" onClick={onSignOut}> <Button size="sm" variant="ghost" onClick={onSignOut}>
Выйти Выйти
@ -146,6 +151,7 @@ export const AppShell = ({
{isGuideOpen ? "Назад" : "?"} {isGuideOpen ? "Назад" : "?"}
</Button> </Button>
) : null} ) : null}
<PwaInstallButton onInstall={onInstallApp} isInstalled={isInstalled} isInstallAvailable={isInstallAvailable} />
<ThemeToggle /> <ThemeToggle />
</div> </div>
</div> </div>

View File

@ -8,6 +8,7 @@ import { ProductGuidePanel } from "../components/dashboard/ProductGuidePanel";
import { useAuth } from "../context/AuthContext"; import { useAuth } from "../context/AuthContext";
import { useNotifications } from "../hooks/useNotifications"; import { useNotifications } from "../hooks/useNotifications";
import { usePushNotifications } from "../hooks/usePushNotifications"; import { usePushNotifications } from "../hooks/usePushNotifications";
import { usePwaStatus } from "../hooks/usePwaStatus";
import { useOrderGroups } from "../hooks/useOrderGroups"; import { useOrderGroups } from "../hooks/useOrderGroups";
import { AppShell } from "../layouts/AppShell"; import { AppShell } from "../layouts/AppShell";
@ -53,6 +54,8 @@ export const DashboardPage = () => {
} }
}, [isSupported, isSubscribed, user?.id, subscribe]); }, [isSupported, isSubscribed, user?.id, subscribe]);
const { isInstalled, isInstallAvailable, installApp: onInstallApp } = usePwaStatus();
const { const {
orderGroups, orderGroups,
allOrderGroups, allOrderGroups,
@ -143,6 +146,9 @@ export const DashboardPage = () => {
<AppShell <AppShell
user={user} user={user}
onSignOut={signOut} onSignOut={signOut}
onInstallApp={onInstallApp}
isInstalled={isInstalled}
isInstallAvailable={isInstallAvailable}
onOpenGuide={() => setActiveSection((current) => (current === "guide" ? section.key : "guide"))} onOpenGuide={() => setActiveSection((current) => (current === "guide" ? section.key : "guide"))}
isGuideOpen={isGuideOpen} isGuideOpen={isGuideOpen}
navItems={navItems} navItems={navItems}