@djangocfg/layouts 2.1.36 â 2.1.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +204 -18
- package/package.json +5 -5
- package/src/components/errors/index.ts +9 -0
- package/src/components/errors/types.ts +38 -0
- package/src/layouts/AppLayout/AppLayout.tsx +33 -45
- package/src/layouts/AppLayout/BaseApp.tsx +105 -28
- package/src/layouts/AuthLayout/AuthContext.tsx +7 -1
- package/src/layouts/AuthLayout/OAuthProviders.tsx +1 -10
- package/src/layouts/AuthLayout/OTPForm.tsx +1 -0
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +1 -1
- package/src/layouts/PublicLayout/PublicLayout.tsx +1 -1
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +1 -1
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +1 -1
- package/src/layouts/_components/UserMenu.tsx +1 -1
- package/src/layouts/index.ts +1 -1
- package/src/layouts/types/index.ts +47 -0
- package/src/layouts/types/layout.types.ts +61 -0
- package/src/layouts/types/providers.types.ts +65 -0
- package/src/layouts/types/ui.types.ts +103 -0
- package/src/snippets/Analytics/index.ts +1 -0
- package/src/snippets/Analytics/types.ts +10 -0
- package/src/snippets/McpChat/context/ChatContext.tsx +9 -0
- package/src/snippets/PWAInstall/@docs/README.md +92 -0
- package/src/snippets/PWAInstall/@docs/research/ios-android-install-flows.md +576 -0
- package/src/snippets/PWAInstall/README.md +185 -0
- package/src/snippets/PWAInstall/components/A2HSHint.tsx +227 -0
- package/src/snippets/PWAInstall/components/DesktopGuide.tsx +229 -0
- package/src/snippets/PWAInstall/components/IOSGuide.tsx +29 -0
- package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +101 -0
- package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +101 -0
- package/src/snippets/PWAInstall/context/InstallContext.tsx +102 -0
- package/src/snippets/PWAInstall/hooks/useInstallPrompt.ts +167 -0
- package/src/snippets/PWAInstall/hooks/useIsPWA.ts +115 -0
- package/src/snippets/PWAInstall/index.ts +76 -0
- package/src/snippets/PWAInstall/types/components.ts +95 -0
- package/src/snippets/PWAInstall/types/config.ts +22 -0
- package/src/snippets/PWAInstall/types/index.ts +26 -0
- package/src/snippets/PWAInstall/types/install.ts +38 -0
- package/src/snippets/PWAInstall/types/platform.ts +29 -0
- package/src/snippets/PWAInstall/utils/localStorage.ts +181 -0
- package/src/snippets/PWAInstall/utils/logger.ts +149 -0
- package/src/snippets/PWAInstall/utils/platform.ts +151 -0
- package/src/snippets/PushNotifications/@docs/README.md +191 -0
- package/src/snippets/PushNotifications/@docs/guides/django-integration.md +648 -0
- package/src/snippets/PushNotifications/@docs/guides/service-worker.md +467 -0
- package/src/snippets/PushNotifications/@docs/guides/vapid-setup.md +352 -0
- package/src/snippets/PushNotifications/README.md +328 -0
- package/src/snippets/PushNotifications/components/PushPrompt.tsx +165 -0
- package/src/snippets/PushNotifications/config.ts +20 -0
- package/src/snippets/PushNotifications/context/DjangoPushContext.tsx +190 -0
- package/src/snippets/PushNotifications/hooks/useDjangoPush.ts +259 -0
- package/src/snippets/PushNotifications/hooks/usePushNotifications.ts +209 -0
- package/src/snippets/PushNotifications/index.ts +87 -0
- package/src/snippets/PushNotifications/types/config.ts +28 -0
- package/src/snippets/PushNotifications/types/index.ts +9 -0
- package/src/snippets/PushNotifications/types/push.ts +21 -0
- package/src/snippets/PushNotifications/utils/localStorage.ts +60 -0
- package/src/snippets/PushNotifications/utils/logger.ts +149 -0
- package/src/snippets/PushNotifications/utils/platform.ts +151 -0
- package/src/snippets/PushNotifications/utils/vapid.ts +226 -0
- package/src/snippets/index.ts +55 -0
- package/src/layouts/shared/index.ts +0 -21
- package/src/layouts/shared/types.ts +0 -211
- /package/src/layouts/{shared â types}/README.md +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# PWA Install
|
|
2
|
+
|
|
3
|
+
Progressive Web App installation snippet for web applications.
|
|
4
|
+
|
|
5
|
+
## Responsibility
|
|
6
|
+
|
|
7
|
+
**Installation of PWA on user's device** (Add to Home Screen)
|
|
8
|
+
|
|
9
|
+
- iOS Safari: Visual guide for manual installation
|
|
10
|
+
- Android Chrome: Native install prompt
|
|
11
|
+
- Unified UX across platforms
|
|
12
|
+
- Smart dismissal with auto-reset
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
// app/layout.tsx
|
|
18
|
+
import { PwaProvider, A2HSHint } from '@/snippets/PWAInstall';
|
|
19
|
+
|
|
20
|
+
export default function RootLayout({ children }) {
|
|
21
|
+
return (
|
|
22
|
+
<html>
|
|
23
|
+
<body>
|
|
24
|
+
<PwaProvider>
|
|
25
|
+
{children}
|
|
26
|
+
<A2HSHint resetAfterDays={3} />
|
|
27
|
+
</PwaProvider>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What You Get
|
|
35
|
+
|
|
36
|
+
- **Unified UX** â Same hint position (bottom) for both iOS & Android
|
|
37
|
+
- **iOS Safari** â Click hint â Opens visual step-by-step guide
|
|
38
|
+
- **Android Chrome** â Click hint â Native install prompt
|
|
39
|
+
- **Visual guide** â Adaptive (drawer on mobile, modal on desktop)
|
|
40
|
+
- **Zero config** â Works out of the box
|
|
41
|
+
- **Smart reset** â Re-appears after 3 days (user gets second chance)
|
|
42
|
+
- **No spam** â Dismissible, respects user choice
|
|
43
|
+
|
|
44
|
+
## API
|
|
45
|
+
|
|
46
|
+
### `<PwaProvider>`
|
|
47
|
+
|
|
48
|
+
Wrap your app once:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
<PwaProvider>
|
|
52
|
+
{children}
|
|
53
|
+
</PwaProvider>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### `<A2HSHint />`
|
|
57
|
+
|
|
58
|
+
Unified install hint for iOS & Android (auto-shows, dismissible):
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
<A2HSHint
|
|
62
|
+
resetAfterDays={3} // Default: 3 days (set to null for never)
|
|
63
|
+
delayMs={3000} // Default: 3 seconds
|
|
64
|
+
forceShow={isDevelopment} // Show on ANY browser (for dev testing)
|
|
65
|
+
logo="/logo192.png" // App logo URL (fallback: Share icon)
|
|
66
|
+
/>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Behavior (iOS Safari):**
|
|
70
|
+
- Shows after 3 seconds
|
|
71
|
+
- Text: "Keep terminal with you â Tap to learn how"
|
|
72
|
+
- Click â Opens visual guide (adaptive: drawer on mobile, modal on desktop)
|
|
73
|
+
- Dismissible (saved to localStorage)
|
|
74
|
+
- Auto-resets after 3 days
|
|
75
|
+
|
|
76
|
+
**Behavior (Android Chrome):**
|
|
77
|
+
- Shows after 3 seconds
|
|
78
|
+
- Text: "Install Cmdop â Tap to install"
|
|
79
|
+
- Click â Triggers native install prompt
|
|
80
|
+
- Dismissible (saved to localStorage)
|
|
81
|
+
- Auto-resets after 3 days
|
|
82
|
+
|
|
83
|
+
### `useInstall()` hook
|
|
84
|
+
|
|
85
|
+
Access install state anywhere:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { useInstall } from '@/snippets/PWAInstall';
|
|
89
|
+
|
|
90
|
+
function Header() {
|
|
91
|
+
const { isIOS, isInstalled, canPrompt, install } = useInstall();
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<header>
|
|
95
|
+
{canPrompt && <button onClick={install}>Install</button>}
|
|
96
|
+
{isInstalled && <span>â Installed</span>}
|
|
97
|
+
</header>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Returns:**
|
|
103
|
+
```ts
|
|
104
|
+
{
|
|
105
|
+
isIOS: boolean;
|
|
106
|
+
isAndroid: boolean;
|
|
107
|
+
isSafari: boolean;
|
|
108
|
+
isChrome: boolean;
|
|
109
|
+
isInstalled: boolean;
|
|
110
|
+
canPrompt: boolean;
|
|
111
|
+
install: () => Promise<'accepted' | 'dismissed' | null>;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Usage with Push Notifications
|
|
116
|
+
|
|
117
|
+
Use together with the PushNotifications snippet:
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { PwaProvider, A2HSHint } from '@/snippets/PWAInstall';
|
|
121
|
+
import { DjangoPushProvider, PushPrompt } from '@/snippets/PushNotifications';
|
|
122
|
+
|
|
123
|
+
export default function Layout({ children }) {
|
|
124
|
+
return (
|
|
125
|
+
<PwaProvider>
|
|
126
|
+
<DjangoPushProvider vapidPublicKey={VAPID_KEY}>
|
|
127
|
+
{children}
|
|
128
|
+
|
|
129
|
+
{/* PWA Install hint */}
|
|
130
|
+
<A2HSHint resetAfterDays={3} />
|
|
131
|
+
|
|
132
|
+
{/* Push notification prompt (after PWA install) */}
|
|
133
|
+
<PushPrompt
|
|
134
|
+
requirePWA={true}
|
|
135
|
+
delayMs={5000}
|
|
136
|
+
resetAfterDays={7}
|
|
137
|
+
/>
|
|
138
|
+
</DjangoPushProvider>
|
|
139
|
+
</PwaProvider>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Browser Support
|
|
145
|
+
|
|
146
|
+
| Platform | Browser | Support |
|
|
147
|
+
|----------|---------|---------|
|
|
148
|
+
| iOS | Safari | â
Visual guide |
|
|
149
|
+
| iOS | Chrome/Firefox | â No PWA support |
|
|
150
|
+
| Android | Chrome | â
Native prompt |
|
|
151
|
+
| Android | Firefox | â ī¸ Manual only |
|
|
152
|
+
| Desktop | Chrome/Edge | â
Native prompt |
|
|
153
|
+
|
|
154
|
+
## Architecture
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
PWAInstall/
|
|
158
|
+
âââ context/
|
|
159
|
+
â âââ InstallContext.tsx # Install state management
|
|
160
|
+
âââ components/
|
|
161
|
+
â âââ A2HSHint.tsx # Unified install hint
|
|
162
|
+
â âââ IOSGuide.tsx # Visual guide wrapper
|
|
163
|
+
â âââ IOSGuideDrawer.tsx # Mobile guide
|
|
164
|
+
â âââ IOSGuideModal.tsx # Desktop guide
|
|
165
|
+
âââ hooks/
|
|
166
|
+
â âââ useInstallPrompt.ts # Install prompt logic
|
|
167
|
+
â âââ useIsPWA.ts # PWA detection
|
|
168
|
+
âââ utils/
|
|
169
|
+
â âââ platform.ts # Platform detection
|
|
170
|
+
â âââ localStorage.ts # Persistence
|
|
171
|
+
â âââ logger.ts # Logging
|
|
172
|
+
âââ types/
|
|
173
|
+
âââ platform.ts
|
|
174
|
+
âââ install.ts
|
|
175
|
+
âââ components.ts
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Separation from Push Notifications
|
|
179
|
+
|
|
180
|
+
This snippet is **completely independent** from push notifications:
|
|
181
|
+
|
|
182
|
+
- **PWAInstall** â Handles device installation
|
|
183
|
+
- **PushNotifications** â Handles web push subscriptions
|
|
184
|
+
|
|
185
|
+
Both can be used together or separately.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PWA Install Hint (Unified for iOS & Android)
|
|
5
|
+
*
|
|
6
|
+
* Inline, non-blocking hint that shows at the bottom of the screen
|
|
7
|
+
* - iOS Safari: Opens visual guide on click
|
|
8
|
+
* - Android Chrome: Triggers native install prompt on click
|
|
9
|
+
* - Unified UX: Same position, same style, same behavior
|
|
10
|
+
*
|
|
11
|
+
* Auto-resets after 3 days (configurable)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { useState, useEffect } from 'react';
|
|
15
|
+
import { Share, X, ChevronRight, Download } from 'lucide-react';
|
|
16
|
+
import { Button } from '@djangocfg/ui-nextjs';
|
|
17
|
+
import { cn } from '@djangocfg/ui-nextjs/lib';
|
|
18
|
+
|
|
19
|
+
import { useInstall } from '../context/InstallContext';
|
|
20
|
+
import { IOSGuide } from './IOSGuide';
|
|
21
|
+
import { DesktopGuide } from './DesktopGuide';
|
|
22
|
+
import { pwaLogger } from '../utils/logger';
|
|
23
|
+
import { markA2HSDismissed, isA2HSDismissedRecently } from '../utils/localStorage';
|
|
24
|
+
|
|
25
|
+
const DEFAULT_RESET_DAYS = 3;
|
|
26
|
+
|
|
27
|
+
interface A2HSHintProps {
|
|
28
|
+
/**
|
|
29
|
+
* Additional class names for the container
|
|
30
|
+
*/
|
|
31
|
+
className?: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Number of days before re-showing dismissed hint
|
|
35
|
+
* @default 3
|
|
36
|
+
* Set to null to never reset (show once forever)
|
|
37
|
+
*/
|
|
38
|
+
resetAfterDays?: number | null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Delay before showing hint (ms)
|
|
42
|
+
* @default 3000
|
|
43
|
+
*/
|
|
44
|
+
delayMs?: number;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Demo mode - shows hint on all platforms with appropriate guides
|
|
48
|
+
* Production: only iOS Safari & Android Chrome
|
|
49
|
+
* Demo: shows on desktop too with desktop install guide
|
|
50
|
+
* @default false
|
|
51
|
+
*/
|
|
52
|
+
demo?: boolean;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* App logo URL to display in hint
|
|
56
|
+
* If not provided, uses Share icon
|
|
57
|
+
*/
|
|
58
|
+
logo?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function A2HSHint({
|
|
62
|
+
className,
|
|
63
|
+
resetAfterDays = DEFAULT_RESET_DAYS,
|
|
64
|
+
delayMs = 3000,
|
|
65
|
+
demo = false,
|
|
66
|
+
logo,
|
|
67
|
+
}: A2HSHintProps = {}) {
|
|
68
|
+
const { isIOS, isSafari, isAndroid, isDesktop, isInstalled, canPrompt, install } = useInstall();
|
|
69
|
+
const [show, setShow] = useState(false);
|
|
70
|
+
const [showGuide, setShowGuide] = useState(false);
|
|
71
|
+
const [installing, setInstalling] = useState(false);
|
|
72
|
+
|
|
73
|
+
// Determine if should show hint
|
|
74
|
+
// Production: only iOS Safari & Android Chrome with native prompt
|
|
75
|
+
// Demo: show on all platforms (desktop, iOS, Android)
|
|
76
|
+
const shouldShow = demo
|
|
77
|
+
? !isInstalled // Demo: show on all platforms if not installed
|
|
78
|
+
: !isInstalled && ((isIOS && isSafari) || canPrompt); // Production: only supported platforms
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!shouldShow) return;
|
|
82
|
+
|
|
83
|
+
// Check if previously dismissed (skip localStorage check in demo mode)
|
|
84
|
+
if (!demo && typeof window !== 'undefined') {
|
|
85
|
+
// If resetAfterDays is null, never reset (check with very large number)
|
|
86
|
+
if (resetAfterDays === null) {
|
|
87
|
+
if (isA2HSDismissedRecently(Number.MAX_SAFE_INTEGER)) {
|
|
88
|
+
return; // Dismissed forever
|
|
89
|
+
}
|
|
90
|
+
} else if (isA2HSDismissedRecently(resetAfterDays)) {
|
|
91
|
+
return; // Still within reset period
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Show after delay (user is already engaged)
|
|
96
|
+
const timer = setTimeout(() => setShow(true), delayMs);
|
|
97
|
+
return () => clearTimeout(timer);
|
|
98
|
+
}, [shouldShow, resetAfterDays, delayMs, demo]);
|
|
99
|
+
|
|
100
|
+
const handleDismiss = () => {
|
|
101
|
+
setShow(false);
|
|
102
|
+
// Don't save to localStorage if demo mode (dev testing)
|
|
103
|
+
if (!demo) {
|
|
104
|
+
markA2HSDismissed();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const handleGuideDismiss = () => {
|
|
109
|
+
setShowGuide(false);
|
|
110
|
+
// In demo mode, keep hint visible after guide is dismissed
|
|
111
|
+
// In production mode, dismiss both guide and hint
|
|
112
|
+
if (!demo) {
|
|
113
|
+
handleDismiss();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const handleClick = async () => {
|
|
118
|
+
const isIOSPlatform = isIOS && isSafari;
|
|
119
|
+
|
|
120
|
+
// iOS or Desktop: Open visual guide
|
|
121
|
+
if (isIOSPlatform || isDesktop) {
|
|
122
|
+
setShowGuide(true);
|
|
123
|
+
} else if (canPrompt) {
|
|
124
|
+
// Android: Trigger native install prompt
|
|
125
|
+
setInstalling(true);
|
|
126
|
+
try {
|
|
127
|
+
await install();
|
|
128
|
+
// If install succeeds, dismiss hint
|
|
129
|
+
handleDismiss();
|
|
130
|
+
} catch (error) {
|
|
131
|
+
pwaLogger.error('[A2HSHint] Install error:', error);
|
|
132
|
+
} finally {
|
|
133
|
+
setInstalling(false);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (!show) return null;
|
|
139
|
+
|
|
140
|
+
// Platform-specific content
|
|
141
|
+
const isIOSPlatform = isIOS && isSafari;
|
|
142
|
+
|
|
143
|
+
// Determine which guide/action to show
|
|
144
|
+
let title: string;
|
|
145
|
+
let subtitle: React.ReactNode;
|
|
146
|
+
|
|
147
|
+
if (isIOSPlatform) {
|
|
148
|
+
title = 'Add to Home Screen';
|
|
149
|
+
subtitle = <>Tap to learn how <ChevronRight className="w-3 h-3" /></>;
|
|
150
|
+
} else if (isDesktop) {
|
|
151
|
+
title = 'Install App';
|
|
152
|
+
subtitle = <>Click to see desktop guide <ChevronRight className="w-3 h-3" /></>;
|
|
153
|
+
} else {
|
|
154
|
+
// Android or other mobile with native prompt
|
|
155
|
+
title = 'Install App';
|
|
156
|
+
subtitle = <>Tap to install <Download className="w-3 h-3" /></>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<>
|
|
161
|
+
<div className={cn(
|
|
162
|
+
"fixed bottom-4 left-4 right-4 z-50 animate-in slide-in-from-bottom-4 duration-300",
|
|
163
|
+
demo && "relative inset-auto z-auto", // Demo mode: remove fixed positioning
|
|
164
|
+
className
|
|
165
|
+
)}>
|
|
166
|
+
{/* Make entire card clickable */}
|
|
167
|
+
<div
|
|
168
|
+
role="button"
|
|
169
|
+
tabIndex={0}
|
|
170
|
+
onClick={handleClick}
|
|
171
|
+
onKeyDown={(e) => {
|
|
172
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
173
|
+
e.preventDefault();
|
|
174
|
+
handleClick();
|
|
175
|
+
}
|
|
176
|
+
}}
|
|
177
|
+
className={cn(
|
|
178
|
+
"w-full bg-zinc-900 border border-zinc-700 rounded-lg p-4 shadow-lg cursor-pointer hover:bg-zinc-800 transition-colors",
|
|
179
|
+
installing && "opacity-70 cursor-not-allowed"
|
|
180
|
+
)}
|
|
181
|
+
aria-disabled={installing}
|
|
182
|
+
>
|
|
183
|
+
<div className="flex items-center gap-3">
|
|
184
|
+
{/* App logo or icon */}
|
|
185
|
+
<div className="flex-shrink-0">
|
|
186
|
+
{logo ? (
|
|
187
|
+
<img src={logo} alt="App logo" className="w-10 h-10 rounded-lg" />
|
|
188
|
+
) : (
|
|
189
|
+
<Share className="w-5 h-5 text-blue-400" />
|
|
190
|
+
)}
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* Content - now just displays, clicking anywhere triggers action */}
|
|
194
|
+
<div className="flex-1 min-w-0">
|
|
195
|
+
<p className="text-sm font-medium text-white mb-1">{title}</p>
|
|
196
|
+
<p className="text-xs text-zinc-400 flex items-center gap-1">
|
|
197
|
+
{installing ? 'Installing...' : subtitle}
|
|
198
|
+
</p>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{/* Close button - prevent event bubbling */}
|
|
202
|
+
<button
|
|
203
|
+
onClick={(e) => {
|
|
204
|
+
e.stopPropagation();
|
|
205
|
+
handleDismiss();
|
|
206
|
+
}}
|
|
207
|
+
className="flex-shrink-0 p-1 hover:bg-zinc-700 rounded transition-colors"
|
|
208
|
+
aria-label="Dismiss"
|
|
209
|
+
>
|
|
210
|
+
<X className="w-4 h-4 text-zinc-400" />
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
{/* Show appropriate guide based on platform */}
|
|
217
|
+
{(isIOSPlatform || (demo && isIOS)) && (
|
|
218
|
+
<IOSGuide open={showGuide} onDismiss={handleGuideDismiss} />
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
{/* Desktop guide */}
|
|
222
|
+
{isDesktop && (
|
|
223
|
+
<DesktopGuide open={showGuide} onDismiss={handleGuideDismiss} />
|
|
224
|
+
)}
|
|
225
|
+
</>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Desktop Installation Guide Modal
|
|
5
|
+
*
|
|
6
|
+
* Visual step-by-step guide for installing PWA on desktop browsers
|
|
7
|
+
* Uses platform detection from InstallContext
|
|
8
|
+
* Supports: Chrome, Edge, Brave, Arc, Vivaldi, Opera, Yandex, Firefox, Safari
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { useMemo } from 'react';
|
|
12
|
+
import { Monitor, Check, ArrowDownToLine, Menu, Search, Plus } from 'lucide-react';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
Dialog,
|
|
16
|
+
DialogContent,
|
|
17
|
+
DialogDescription,
|
|
18
|
+
DialogFooter,
|
|
19
|
+
DialogHeader,
|
|
20
|
+
DialogTitle,
|
|
21
|
+
Button,
|
|
22
|
+
Card,
|
|
23
|
+
CardContent,
|
|
24
|
+
} from '@djangocfg/ui-nextjs';
|
|
25
|
+
|
|
26
|
+
import type { IOSGuideModalProps, InstallStep } from '../types';
|
|
27
|
+
import { useInstall } from '../context/InstallContext';
|
|
28
|
+
|
|
29
|
+
type BrowserCategory = 'chromium' | 'firefox' | 'safari' | 'unknown';
|
|
30
|
+
|
|
31
|
+
function getBrowserCategory(browser: {
|
|
32
|
+
isChromium: boolean;
|
|
33
|
+
isFirefox: boolean;
|
|
34
|
+
isSafari: boolean;
|
|
35
|
+
}): BrowserCategory {
|
|
36
|
+
if (browser.isChromium) return 'chromium';
|
|
37
|
+
if (browser.isFirefox) return 'firefox';
|
|
38
|
+
if (browser.isSafari) return 'safari';
|
|
39
|
+
return 'unknown';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getBrowserSteps(category: BrowserCategory, browserName: string): InstallStep[] {
|
|
43
|
+
switch (category) {
|
|
44
|
+
case 'chromium':
|
|
45
|
+
// Chrome, Edge, Brave, Arc, Vivaldi, Opera, Yandex, etc.
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
number: 1,
|
|
49
|
+
title: 'Find Install Icon',
|
|
50
|
+
icon: ArrowDownToLine,
|
|
51
|
+
description: 'Look for install icon in address bar (right side)',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
number: 2,
|
|
55
|
+
title: 'Click Install',
|
|
56
|
+
icon: Plus,
|
|
57
|
+
description: 'Click the icon and select "Install"',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
number: 3,
|
|
61
|
+
title: 'Confirm',
|
|
62
|
+
icon: Check,
|
|
63
|
+
description: 'Click "Install" in the popup dialog',
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
case 'firefox':
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
number: 1,
|
|
71
|
+
title: 'Open Menu',
|
|
72
|
+
icon: Menu,
|
|
73
|
+
description: 'Click the menu button (three lines)',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
number: 2,
|
|
77
|
+
title: 'Find Install Option',
|
|
78
|
+
icon: Search,
|
|
79
|
+
description: 'Look for "Install" or "Add to Home Screen"',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
number: 3,
|
|
83
|
+
title: 'Confirm',
|
|
84
|
+
icon: Check,
|
|
85
|
+
description: 'Follow the installation prompts',
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
case 'safari':
|
|
90
|
+
return [
|
|
91
|
+
{
|
|
92
|
+
number: 1,
|
|
93
|
+
title: 'Limited Support',
|
|
94
|
+
icon: Monitor,
|
|
95
|
+
description: 'Safari on macOS has limited PWA support',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
number: 2,
|
|
99
|
+
title: 'Use Chromium Browser',
|
|
100
|
+
icon: ArrowDownToLine,
|
|
101
|
+
description: 'Consider using Chrome, Edge, or Brave for full PWA experience',
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
default:
|
|
106
|
+
return [
|
|
107
|
+
{
|
|
108
|
+
number: 1,
|
|
109
|
+
title: 'Check Address Bar',
|
|
110
|
+
icon: ArrowDownToLine,
|
|
111
|
+
description: 'Look for an install or download icon',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
number: 2,
|
|
115
|
+
title: 'Or Use Menu',
|
|
116
|
+
icon: Menu,
|
|
117
|
+
description: 'Check browser menu for install option',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
number: 3,
|
|
121
|
+
title: 'Confirm',
|
|
122
|
+
icon: Check,
|
|
123
|
+
description: 'Follow the installation prompts',
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function StepCard({ step }: { step: InstallStep }) {
|
|
130
|
+
return (
|
|
131
|
+
<Card className="border border-border">
|
|
132
|
+
<CardContent className="p-4">
|
|
133
|
+
<div className="flex items-start gap-3">
|
|
134
|
+
<div
|
|
135
|
+
className="flex items-center justify-center rounded-full bg-primary text-primary-foreground flex-shrink-0"
|
|
136
|
+
style={{ width: '32px', height: '32px' }}
|
|
137
|
+
>
|
|
138
|
+
<span className="text-sm font-semibold">{step.number}</span>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="flex-1 min-w-0">
|
|
142
|
+
<div className="flex items-center gap-2 mb-1">
|
|
143
|
+
<step.icon className="w-5 h-5 text-primary" />
|
|
144
|
+
<h3 className="font-semibold text-foreground">{step.title}</h3>
|
|
145
|
+
</div>
|
|
146
|
+
<p className="text-sm text-muted-foreground">{step.description}</p>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</CardContent>
|
|
150
|
+
</Card>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function DesktopGuide({ onDismiss, open = true }: IOSGuideModalProps) {
|
|
155
|
+
const {
|
|
156
|
+
browserName,
|
|
157
|
+
isChromium,
|
|
158
|
+
isFirefox,
|
|
159
|
+
isSafari,
|
|
160
|
+
isEdge,
|
|
161
|
+
isBrave,
|
|
162
|
+
isArc,
|
|
163
|
+
isVivaldi,
|
|
164
|
+
isOpera,
|
|
165
|
+
isYandex,
|
|
166
|
+
} = useInstall();
|
|
167
|
+
|
|
168
|
+
const category = useMemo(
|
|
169
|
+
() => getBrowserCategory({ isChromium, isFirefox, isSafari }),
|
|
170
|
+
[isChromium, isFirefox, isSafari]
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const steps = useMemo(() => getBrowserSteps(category, browserName), [category, browserName]);
|
|
174
|
+
|
|
175
|
+
// Get specific browser display name with emoji
|
|
176
|
+
const displayName = useMemo(() => {
|
|
177
|
+
if (isEdge) return 'Edge';
|
|
178
|
+
if (isBrave) return 'Brave';
|
|
179
|
+
if (isArc) return 'Arc';
|
|
180
|
+
if (isVivaldi) return 'Vivaldi';
|
|
181
|
+
if (isOpera) return 'Opera';
|
|
182
|
+
if (isYandex) return 'Yandex Browser';
|
|
183
|
+
return browserName;
|
|
184
|
+
}, [browserName, isEdge, isBrave, isArc, isVivaldi, isOpera, isYandex]);
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onDismiss()}>
|
|
188
|
+
<DialogContent className="sm:max-w-md">
|
|
189
|
+
<DialogHeader className="text-left">
|
|
190
|
+
<DialogTitle className="flex items-center gap-2">
|
|
191
|
+
<Monitor className="w-5 h-5 text-primary" />
|
|
192
|
+
Install App on Desktop
|
|
193
|
+
</DialogTitle>
|
|
194
|
+
<DialogDescription className="text-left">
|
|
195
|
+
{isSafari
|
|
196
|
+
? 'Safari on macOS has limited PWA support. For the best experience, use Chrome, Edge, or Brave.'
|
|
197
|
+
: `Install this app on ${displayName} for quick access from your desktop`
|
|
198
|
+
}
|
|
199
|
+
</DialogDescription>
|
|
200
|
+
</DialogHeader>
|
|
201
|
+
|
|
202
|
+
<div className="space-y-3 py-4">
|
|
203
|
+
{steps.map((step) => (
|
|
204
|
+
<StepCard key={step.number} step={step} />
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
{category === 'chromium' && (
|
|
209
|
+
<div className="p-3 bg-muted/30 rounded-lg text-xs text-muted-foreground">
|
|
210
|
+
đĄ Tip: You can also right-click the page and look for "Install" option in {displayName}
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{category === 'firefox' && (
|
|
215
|
+
<div className="p-3 bg-amber-500/10 rounded-lg text-xs text-amber-700 dark:text-amber-400">
|
|
216
|
+
âšī¸ Note: Firefox has limited PWA support. Some features may not work as expected.
|
|
217
|
+
</div>
|
|
218
|
+
)}
|
|
219
|
+
|
|
220
|
+
<DialogFooter>
|
|
221
|
+
<Button onClick={onDismiss} variant="default" className="w-full">
|
|
222
|
+
<Check className="w-4 h-4 mr-2" />
|
|
223
|
+
Got It
|
|
224
|
+
</Button>
|
|
225
|
+
</DialogFooter>
|
|
226
|
+
</DialogContent>
|
|
227
|
+
</Dialog>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* iOS Installation Guide (Adaptive)
|
|
5
|
+
*
|
|
6
|
+
* Automatically uses:
|
|
7
|
+
* - Drawer on mobile (better swipe UX)
|
|
8
|
+
* - Dialog on desktop/tablet
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React from 'react';
|
|
12
|
+
|
|
13
|
+
import { useIsMobile } from '@djangocfg/ui-nextjs/hooks';
|
|
14
|
+
|
|
15
|
+
import { IOSGuideModal } from './IOSGuideModal';
|
|
16
|
+
import { IOSGuideDrawer } from './IOSGuideDrawer';
|
|
17
|
+
|
|
18
|
+
import type { IOSGuideModalProps } from '../types';
|
|
19
|
+
|
|
20
|
+
export function IOSGuide(props: IOSGuideModalProps) {
|
|
21
|
+
const isMobile = useIsMobile(); // Viewport < 768px
|
|
22
|
+
|
|
23
|
+
// Use drawer on mobile, dialog on desktop
|
|
24
|
+
if (isMobile) {
|
|
25
|
+
return <IOSGuideDrawer {...props} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return <IOSGuideModal {...props} />;
|
|
29
|
+
}
|