add to homescreen feature
This commit is contained in:
75
src/App.css
75
src/App.css
@@ -1443,6 +1443,10 @@
|
||||
background: rgba(59, 130, 246, 0.12);
|
||||
color: #3b82f6;
|
||||
}
|
||||
.settings-item-icon-purple {
|
||||
background: rgba(139, 92, 246, 0.12);
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.settings-item-content {
|
||||
flex: 1;
|
||||
@@ -2035,6 +2039,59 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* iOS Install Modal */
|
||||
.ios-install-modal {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ios-install-icon {
|
||||
font-size: 2.5rem;
|
||||
text-align: center;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.ios-install-modal .confirm-modal-title {
|
||||
color: #1a1a1a;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ios-install-subtitle {
|
||||
font-size: 0.85rem;
|
||||
color: #6b7280;
|
||||
margin: 0 0 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ios-install-steps {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.ios-install-steps li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
color: #374151;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ios-install-step-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex-shrink: 0;
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
color: #7c3aed;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* ============================
|
||||
RESPONSIVE – DESKTOP (≥ 860px)
|
||||
Sidebar navigation + single-column content
|
||||
@@ -2407,6 +2464,24 @@
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .settings-item-icon-purple {
|
||||
background: rgba(167, 139, 250, 0.12);
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .ios-install-modal .confirm-modal-title {
|
||||
color: #e8f5e8;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .ios-install-steps li {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .ios-install-step-icon {
|
||||
background: rgba(167, 139, 250, 0.12);
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
/* -- Settings buttons -- */
|
||||
[data-theme="dark"] .settings-tutorial-btn {
|
||||
background: #1a1a1a;
|
||||
|
||||
61
src/hooks/usePWAInstall.ts
Normal file
61
src/hooks/usePWAInstall.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
prompt: () => Promise<void>
|
||||
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
|
||||
}
|
||||
|
||||
interface PWAInstall {
|
||||
canInstall: boolean // Android/Chrome: native prompt available
|
||||
isIOS: boolean // iOS Safari: must show manual instructions
|
||||
isInstalled: boolean // Already running as installed PWA
|
||||
triggerInstall: () => Promise<void>
|
||||
}
|
||||
|
||||
export function usePWAInstall(): PWAInstall {
|
||||
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null)
|
||||
const [isInstalled, setIsInstalled] = useState(false)
|
||||
|
||||
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !(window as unknown as { MSStream?: unknown }).MSStream
|
||||
|
||||
useEffect(() => {
|
||||
// Detect if already installed (standalone mode)
|
||||
const mq = window.matchMedia('(display-mode: standalone)')
|
||||
const iosStandalone = (navigator as unknown as { standalone?: boolean }).standalone === true
|
||||
if (mq.matches || iosStandalone) {
|
||||
setIsInstalled(true)
|
||||
return
|
||||
}
|
||||
|
||||
const handler = (e: Event) => {
|
||||
e.preventDefault()
|
||||
setDeferredPrompt(e as BeforeInstallPromptEvent)
|
||||
}
|
||||
|
||||
window.addEventListener('beforeinstallprompt', handler)
|
||||
|
||||
window.addEventListener('appinstalled', () => {
|
||||
setIsInstalled(true)
|
||||
setDeferredPrompt(null)
|
||||
})
|
||||
|
||||
return () => window.removeEventListener('beforeinstallprompt', handler)
|
||||
}, [])
|
||||
|
||||
const triggerInstall = async () => {
|
||||
if (!deferredPrompt) return
|
||||
await deferredPrompt.prompt()
|
||||
const { outcome } = await deferredPrompt.userChoice
|
||||
if (outcome === 'accepted') {
|
||||
setIsInstalled(true)
|
||||
setDeferredPrompt(null)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
canInstall: !!deferredPrompt,
|
||||
isIOS,
|
||||
isInstalled,
|
||||
triggerInstall,
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
import BottomNav from '../components/BottomNav'
|
||||
import { useOnboardingTour, hasPendingTourStep, clearPendingTourStep } from '../hooks/useOnboardingTour'
|
||||
import { PageLoader } from '../components/PageLoader'
|
||||
import { usePWAInstall } from '../hooks/usePWAInstall'
|
||||
|
||||
const MAX_PHOTO_SIZE = 200 // px — resize to 200x200
|
||||
|
||||
@@ -51,6 +52,8 @@ export default function SettingsPage() {
|
||||
const { continueTourOnSettings } = useOnboardingTour()
|
||||
const navigate = useNavigate()
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const { canInstall, isIOS, isInstalled, triggerInstall } = usePWAInstall()
|
||||
const [showIOSModal, setShowIOSModal] = useState(false)
|
||||
|
||||
// Edit profile modal state
|
||||
const [showEditModal, setShowEditModal] = useState(false)
|
||||
@@ -282,6 +285,34 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* App */}
|
||||
{!isInstalled && (isIOS || canInstall) && (
|
||||
<section className="settings-section">
|
||||
<h3 className="settings-section-title">APP</h3>
|
||||
<div className="settings-card">
|
||||
<button
|
||||
type="button"
|
||||
className="settings-item settings-item-button"
|
||||
onClick={() => isIOS ? setShowIOSModal(true) : triggerInstall()}
|
||||
>
|
||||
<div className="settings-item-icon settings-item-icon-purple">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="5" y="2" width="14" height="20" rx="2" ry="2" />
|
||||
<line x1="12" y1="18" x2="12.01" y2="18" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="settings-item-content">
|
||||
<h4 className="settings-item-title">Add to Home Screen</h4>
|
||||
<p className="settings-item-subtitle">Open as an app, no browser bar</p>
|
||||
</div>
|
||||
<svg className="settings-item-arrow" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Data & Look */}
|
||||
<section className="settings-section">
|
||||
<h3 className="settings-section-title">DATA & LOOK</h3>
|
||||
@@ -495,6 +526,55 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* iOS "Add to Home Screen" instructions modal */}
|
||||
{showIOSModal && (
|
||||
<div className="confirm-modal-overlay" onClick={() => setShowIOSModal(false)}>
|
||||
<div className="confirm-modal ios-install-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="ios-install-icon">🌱</div>
|
||||
<h3 className="confirm-modal-title">Add to Home Screen</h3>
|
||||
<p className="ios-install-subtitle">Follow these steps in Safari:</p>
|
||||
<ol className="ios-install-steps">
|
||||
<li>
|
||||
<span className="ios-install-step-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
|
||||
<polyline points="16 6 12 2 8 6" />
|
||||
<line x1="12" y1="2" x2="12" y2="15" />
|
||||
</svg>
|
||||
</span>
|
||||
Tap the <strong>Share</strong> button at the bottom of Safari
|
||||
</li>
|
||||
<li>
|
||||
<span className="ios-install-step-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||
<line x1="12" y1="8" x2="12" y2="16" />
|
||||
<line x1="8" y1="12" x2="16" y2="12" />
|
||||
</svg>
|
||||
</span>
|
||||
Scroll down and tap <strong>Add to Home Screen</strong>
|
||||
</li>
|
||||
<li>
|
||||
<span className="ios-install-step-icon">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</span>
|
||||
Tap <strong>Add</strong> to confirm
|
||||
</li>
|
||||
</ol>
|
||||
<button
|
||||
type="button"
|
||||
className="edit-modal-save"
|
||||
style={{ width: '100%', marginTop: '1rem' }}
|
||||
onClick={() => setShowIOSModal(false)}
|
||||
>
|
||||
Got it
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user