diff --git a/index.html b/index.html index d5d86d8..4817c9d 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,13 @@ - + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..e8b6b66 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,24 @@ +{ + "name": "Grateful Journal", + "short_name": "Grateful", + "description": "Your private, encrypted gratitude journal", + "start_url": "/", + "display": "standalone", + "background_color": "#f0fdf4", + "theme_color": "#16a34a", + "orientation": "portrait", + "icons": [ + { + "src": "/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + }, + { + "src": "/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "maskable" + } + ] +} diff --git a/src/App.css b/src/App.css index a2cbf43..f8c5d99 100644 --- a/src/App.css +++ b/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; diff --git a/src/hooks/usePWAInstall.ts b/src/hooks/usePWAInstall.ts new file mode 100644 index 0000000..9a5f117 --- /dev/null +++ b/src/hooks/usePWAInstall.ts @@ -0,0 +1,61 @@ +import { useState, useEffect } from 'react' + +interface BeforeInstallPromptEvent extends Event { + prompt: () => Promise + 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 +} + +export function usePWAInstall(): PWAInstall { + const [deferredPrompt, setDeferredPrompt] = useState(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, + } +} diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index d698ce4..e07093a 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -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(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() { + {/* App */} + {!isInstalled && (isIOS || canInstall) && ( +
+

APP

+
+ +
+
+ )} + {/* Data & Look */}

DATA & LOOK

@@ -495,6 +526,55 @@ export default function SettingsPage() { )} + {/* iOS "Add to Home Screen" instructions modal */} + {showIOSModal && ( +
setShowIOSModal(false)}> +
e.stopPropagation()}> +
🌱
+

Add to Home Screen

+

Follow these steps in Safari:

+
    +
  1. + + + + + + + + Tap the Share button at the bottom of Safari +
  2. +
  3. + + + + + + + + Scroll down and tap Add to Home Screen +
  4. +
  5. + + + + + + Tap Add to confirm +
  6. +
+ +
+
+ )} + )