feat: add PWA install button to header
This commit is contained in:
parent
a04e8edb6e
commit
89d6a01b68
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue