add to homescreen feature

This commit is contained in:
2026-03-31 11:05:26 +05:30
parent a1dd555c96
commit de7ce040c8
6 changed files with 254 additions and 1 deletions

View File

@@ -2,7 +2,13 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🌱</text></svg>" />
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/icon.svg" />
<link rel="manifest" href="/manifest.json" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Grateful" />
<meta name="theme-color" content="#16a34a" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"

7
public/icon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

24
public/manifest.json Normal file
View File

@@ -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"
}
]
}

View File

@@ -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;

View 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,
}
}

View File

@@ -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>
)