Refactor PWA settings copy and sections

This commit is contained in:
2026-01-18 00:36:42 +00:00
parent 1d6c968386
commit 09a99fef6a
6 changed files with 398 additions and 207 deletions

View File

@@ -1,39 +1,31 @@
import { Card } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Separator } from '@/components/ui/separator'
import { Badge } from '@/components/ui/badge'
import { Card } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import pwaSettingsCopy from '@/data/pwa-settings.json'
import { usePWA } from '@/hooks/use-pwa'
import { useState, useEffect } from 'react'
import {
Download,
CloudArrowDown,
Trash,
Bell,
WifiSlash,
WifiHigh,
CheckCircle,
XCircle,
Question
} from '@phosphor-icons/react'
import { useEffect, useState } from 'react'
import { CheckCircle, WifiHigh, WifiSlash, XCircle } from '@phosphor-icons/react'
import { toast } from 'sonner'
import { CacheSection } from './pwa-settings/CacheSection'
import { InstallSection } from './pwa-settings/InstallSection'
import { NotificationsSection } from './pwa-settings/NotificationsSection'
import { UpdateSection } from './pwa-settings/UpdateSection'
export function PWASettings() {
const {
isInstalled,
isInstallable,
isOnline,
const {
isInstalled,
isInstallable,
isOnline,
isUpdateAvailable,
installApp,
updateApp,
installApp,
updateApp,
clearCache,
requestNotificationPermission,
registration
} = usePWA()
const [notificationPermission, setNotificationPermission] = useState<NotificationPermission>('default')
const [cacheSize, setCacheSize] = useState<string>('Calculating...')
const [cacheSize, setCacheSize] = useState<string>(pwaSettingsCopy.defaults.cacheCalculating)
useEffect(() => {
if ('Notification' in window) {
@@ -41,9 +33,9 @@ export function PWASettings() {
}
if ('storage' in navigator && 'estimate' in navigator.storage) {
navigator.storage.estimate().then(estimate => {
navigator.storage.estimate().then((estimate) => {
const usageInMB = ((estimate.usage || 0) / (1024 * 1024)).toFixed(2)
setCacheSize(`${usageInMB} MB`)
setCacheSize(`${usageInMB} ${pwaSettingsCopy.cache.storageUnit}`)
})
}
}, [])
@@ -51,99 +43,55 @@ export function PWASettings() {
const handleInstall = async () => {
const success = await installApp()
if (success) {
toast.success('App installed successfully!')
toast.success(pwaSettingsCopy.toasts.installSuccess)
} else {
toast.error('Installation cancelled')
toast.error(pwaSettingsCopy.toasts.installCancelled)
}
}
const handleUpdate = () => {
updateApp()
toast.info('Updating app...')
toast.info(pwaSettingsCopy.toasts.update)
}
const handleClearCache = () => {
clearCache()
toast.success('Cache cleared! Reloading...')
toast.success(pwaSettingsCopy.toasts.cacheCleared)
}
const handleNotificationToggle = async (enabled: boolean) => {
if (enabled) {
const permission = await requestNotificationPermission()
setNotificationPermission(permission as NotificationPermission)
if (permission === 'granted') {
toast.success('Notifications enabled')
} else {
toast.error('Notification permission denied')
}
}
}
const getPermissionIcon = () => {
switch (notificationPermission) {
case 'granted':
return <CheckCircle size={16} className="text-accent" weight="fill" />
case 'denied':
return <XCircle size={16} className="text-destructive" weight="fill" />
default:
return <Question size={16} className="text-muted-foreground" weight="fill" />
if (permission === 'granted') {
toast.success(pwaSettingsCopy.toasts.notificationsEnabled)
} else {
toast.error(pwaSettingsCopy.toasts.notificationsDenied)
}
}
}
return (
<div className="h-full overflow-auto p-6 space-y-6">
<div>
<h2 className="text-2xl font-bold mb-2">PWA Settings</h2>
<p className="text-sm text-muted-foreground">
Configure Progressive Web App features and behavior
</p>
<h2 className="text-2xl font-bold mb-2">{pwaSettingsCopy.header.title}</h2>
<p className="text-sm text-muted-foreground">{pwaSettingsCopy.header.description}</p>
</div>
<div className="grid gap-6">
<Card className="p-6">
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold mb-1">Installation Status</h3>
<p className="text-sm text-muted-foreground">
Install the app for offline access and better performance
</p>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Download size={20} className="text-muted-foreground" />
<div>
<Label className="text-base">App Installation</Label>
<p className="text-xs text-muted-foreground">
{isInstalled ? 'Installed' : isInstallable ? 'Available' : 'Not available'}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{isInstalled && (
<Badge variant="default">Installed</Badge>
)}
{isInstallable && !isInstalled && (
<Button size="sm" onClick={handleInstall}>
Install Now
</Button>
)}
{!isInstallable && !isInstalled && (
<Badge variant="secondary">Not Available</Badge>
)}
</div>
</div>
</div>
</Card>
<InstallSection
isInstalled={isInstalled}
isInstallable={isInstallable}
onInstall={handleInstall}
copy={pwaSettingsCopy.install}
/>
<Card className="p-6">
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold mb-1">Connection Status</h3>
<p className="text-sm text-muted-foreground">
Current network connectivity status
</p>
<h3 className="text-lg font-semibold mb-1">{pwaSettingsCopy.connection.title}</h3>
<p className="text-sm text-muted-foreground">{pwaSettingsCopy.connection.description}</p>
</div>
<div className="flex items-center justify-between">
@@ -154,142 +102,52 @@ export function PWASettings() {
<WifiSlash size={20} className="text-destructive" />
)}
<div>
<Label className="text-base">Network Status</Label>
<Label className="text-base">{pwaSettingsCopy.connection.label}</Label>
<p className="text-xs text-muted-foreground">
{isOnline ? 'Connected to internet' : 'Working offline'}
{isOnline ? pwaSettingsCopy.connection.status.online : pwaSettingsCopy.connection.status.offline}
</p>
</div>
</div>
<Badge variant={isOnline ? 'default' : 'destructive'}>
{isOnline ? 'Online' : 'Offline'}
{isOnline ? pwaSettingsCopy.connection.badge.online : pwaSettingsCopy.connection.badge.offline}
</Badge>
</div>
</div>
</Card>
{isUpdateAvailable && (
<Card className="p-6 border-accent">
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold mb-1">Update Available</h3>
<p className="text-sm text-muted-foreground">
A new version of the app is ready to install
</p>
</div>
<UpdateSection
isUpdateAvailable={isUpdateAvailable}
onUpdate={handleUpdate}
copy={pwaSettingsCopy.update}
/>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<CloudArrowDown size={20} className="text-accent" />
<div>
<Label className="text-base">App Update</Label>
<p className="text-xs text-muted-foreground">
Update now for latest features
</p>
</div>
</div>
<Button onClick={handleUpdate}>
Update Now
</Button>
</div>
</div>
</Card>
)}
<NotificationsSection
permission={notificationPermission}
onToggle={handleNotificationToggle}
copy={pwaSettingsCopy.notifications}
/>
<CacheSection
cacheSize={cacheSize}
hasRegistration={Boolean(registration)}
onClearCache={handleClearCache}
copy={pwaSettingsCopy.cache}
/>
<Card className="p-6">
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold mb-1">Notifications</h3>
<p className="text-sm text-muted-foreground">
Receive updates about your projects and builds
</p>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Bell size={20} className="text-muted-foreground" />
<div>
<Label className="text-base">Push Notifications</Label>
<div className="flex items-center gap-2">
<p className="text-xs text-muted-foreground">
Permission: {notificationPermission}
</p>
{getPermissionIcon()}
</div>
</div>
</div>
<Switch
checked={notificationPermission === 'granted'}
onCheckedChange={handleNotificationToggle}
disabled={notificationPermission === 'denied'}
/>
</div>
{notificationPermission === 'denied' && (
<div className="text-xs text-destructive bg-destructive/10 p-3 rounded-md">
Notifications are blocked. Please enable them in your browser settings.
</div>
)}
</div>
</Card>
<Card className="p-6">
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold mb-1">Cache Management</h3>
<p className="text-sm text-muted-foreground">
Manage offline storage and cached resources
</p>
</div>
<Separator />
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-sm">Cache Size</Label>
<span className="text-sm font-mono text-muted-foreground">{cacheSize}</span>
</div>
<div className="flex items-center justify-between">
<Label className="text-sm">Service Worker</Label>
<Badge variant={registration ? 'default' : 'secondary'}>
{registration ? 'Active' : 'Inactive'}
</Badge>
</div>
</div>
<Separator />
<Button
variant="destructive"
className="w-full"
onClick={handleClearCache}
>
<Trash size={16} className="mr-2" />
Clear Cache & Reload
</Button>
<p className="text-xs text-muted-foreground text-center">
This will remove all cached files and reload the app
</p>
</div>
</Card>
<Card className="p-6">
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold mb-1">PWA Features</h3>
<p className="text-sm text-muted-foreground">
Progressive Web App capabilities
</p>
<h3 className="text-lg font-semibold mb-1">{pwaSettingsCopy.features.title}</h3>
<p className="text-sm text-muted-foreground">{pwaSettingsCopy.features.description}</p>
</div>
<div className="grid gap-3 text-sm">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Offline Support</span>
<span className="text-muted-foreground">{pwaSettingsCopy.features.items.offline}</span>
<CheckCircle size={16} className="text-accent" weight="fill" />
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Installable</span>
<span className="text-muted-foreground">{pwaSettingsCopy.features.items.installable}</span>
{isInstallable || isInstalled ? (
<CheckCircle size={16} className="text-accent" weight="fill" />
) : (
@@ -297,11 +155,11 @@ export function PWASettings() {
)}
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Background Sync</span>
<span className="text-muted-foreground">{pwaSettingsCopy.features.items.backgroundSync}</span>
<CheckCircle size={16} className="text-accent" weight="fill" />
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Push Notifications</span>
<span className="text-muted-foreground">{pwaSettingsCopy.features.items.pushNotifications}</span>
{'Notification' in window ? (
<CheckCircle size={16} className="text-accent" weight="fill" />
) : (
@@ -309,7 +167,7 @@ export function PWASettings() {
)}
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">App Shortcuts</span>
<span className="text-muted-foreground">{pwaSettingsCopy.features.items.shortcuts}</span>
<CheckCircle size={16} className="text-accent" weight="fill" />
</div>
</div>

View File

@@ -0,0 +1,66 @@
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator'
import { Trash } from '@phosphor-icons/react'
interface CacheSectionProps {
cacheSize: string
hasRegistration: boolean
onClearCache: () => void
copy: {
title: string
description: string
labels: {
size: string
serviceWorker: string
}
status: {
active: string
inactive: string
}
action: {
clear: string
}
helper: string
}
}
export function CacheSection({ cacheSize, hasRegistration, onClearCache, copy }: CacheSectionProps) {
return (
<Card className="p-6">
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold mb-1">{copy.title}</h3>
<p className="text-sm text-muted-foreground">{copy.description}</p>
</div>
<Separator />
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label className="text-sm">{copy.labels.size}</Label>
<span className="text-sm font-mono text-muted-foreground">{cacheSize}</span>
</div>
<div className="flex items-center justify-between">
<Label className="text-sm">{copy.labels.serviceWorker}</Label>
<Badge variant={hasRegistration ? 'default' : 'secondary'}>
{hasRegistration ? copy.status.active : copy.status.inactive}
</Badge>
</div>
</div>
<Separator />
<Button variant="destructive" className="w-full" onClick={onClearCache}>
<Trash size={16} className="mr-2" />
{copy.action.clear}
</Button>
<p className="text-xs text-muted-foreground text-center">{copy.helper}</p>
</div>
</Card>
)
}

View File

@@ -0,0 +1,68 @@
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Download } from '@phosphor-icons/react'
interface InstallSectionProps {
isInstalled: boolean
isInstallable: boolean
onInstall: () => void
copy: {
title: string
description: string
label: string
status: {
installed: string
available: string
notAvailable: string
}
badge: {
installed: string
notAvailable: string
}
action: {
install: string
}
}
}
export function InstallSection({ isInstalled, isInstallable, onInstall, copy }: InstallSectionProps) {
const statusText = isInstalled
? copy.status.installed
: isInstallable
? copy.status.available
: copy.status.notAvailable
return (
<Card className="p-6">
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold mb-1">{copy.title}</h3>
<p className="text-sm text-muted-foreground">{copy.description}</p>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Download size={20} className="text-muted-foreground" />
<div>
<Label className="text-base">{copy.label}</Label>
<p className="text-xs text-muted-foreground">{statusText}</p>
</div>
</div>
<div className="flex items-center gap-2">
{isInstalled && <Badge variant="default">{copy.badge.installed}</Badge>}
{isInstallable && !isInstalled && (
<Button size="sm" onClick={onInstall}>
{copy.action.install}
</Button>
)}
{!isInstallable && !isInstalled && (
<Badge variant="secondary">{copy.badge.notAvailable}</Badge>
)}
</div>
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,66 @@
import { Card } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Bell, CheckCircle, Question, XCircle } from '@phosphor-icons/react'
interface NotificationsSectionProps {
permission: NotificationPermission
onToggle: (enabled: boolean) => void
copy: {
title: string
description: string
label: string
permissionLabel: string
blocked: string
}
}
export function NotificationsSection({ permission, onToggle, copy }: NotificationsSectionProps) {
const getPermissionIcon = () => {
switch (permission) {
case 'granted':
return <CheckCircle size={16} className="text-accent" weight="fill" />
case 'denied':
return <XCircle size={16} className="text-destructive" weight="fill" />
default:
return <Question size={16} className="text-muted-foreground" weight="fill" />
}
}
return (
<Card className="p-6">
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold mb-1">{copy.title}</h3>
<p className="text-sm text-muted-foreground">{copy.description}</p>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Bell size={20} className="text-muted-foreground" />
<div>
<Label className="text-base">{copy.label}</Label>
<div className="flex items-center gap-2">
<p className="text-xs text-muted-foreground">
{copy.permissionLabel} {permission}
</p>
{getPermissionIcon()}
</div>
</div>
</div>
<Switch
checked={permission === 'granted'}
onCheckedChange={onToggle}
disabled={permission === 'denied'}
/>
</div>
{permission === 'denied' && (
<div className="text-xs text-destructive bg-destructive/10 p-3 rounded-md">
{copy.blocked}
</div>
)}
</div>
</Card>
)
}

View File

@@ -0,0 +1,44 @@
import { Button } from '@/components/ui/button'
import { Card } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { CloudArrowDown } from '@phosphor-icons/react'
interface UpdateSectionProps {
isUpdateAvailable: boolean
onUpdate: () => void
copy: {
title: string
description: string
label: string
status: string
action: string
}
}
export function UpdateSection({ isUpdateAvailable, onUpdate, copy }: UpdateSectionProps) {
if (!isUpdateAvailable) {
return null
}
return (
<Card className="p-6 border-accent">
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold mb-1">{copy.title}</h3>
<p className="text-sm text-muted-foreground">{copy.description}</p>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<CloudArrowDown size={20} className="text-accent" />
<div>
<Label className="text-base">{copy.label}</Label>
<p className="text-xs text-muted-foreground">{copy.status}</p>
</div>
</div>
<Button onClick={onUpdate}>{copy.action}</Button>
</div>
</div>
</Card>
)
}

View File

@@ -0,0 +1,89 @@
{
"header": {
"title": "PWA Settings",
"description": "Configure Progressive Web App features and behavior"
},
"install": {
"title": "Installation Status",
"description": "Install the app for offline access and better performance",
"label": "App Installation",
"status": {
"installed": "Installed",
"available": "Available",
"notAvailable": "Not available"
},
"badge": {
"installed": "Installed",
"notAvailable": "Not Available"
},
"action": {
"install": "Install Now"
}
},
"connection": {
"title": "Connection Status",
"description": "Current network connectivity status",
"label": "Network Status",
"status": {
"online": "Connected to internet",
"offline": "Working offline"
},
"badge": {
"online": "Online",
"offline": "Offline"
}
},
"update": {
"title": "Update Available",
"description": "A new version of the app is ready to install",
"label": "App Update",
"status": "Update now for latest features",
"action": "Update Now"
},
"notifications": {
"title": "Notifications",
"description": "Receive updates about your projects and builds",
"label": "Push Notifications",
"permissionLabel": "Permission:",
"blocked": "Notifications are blocked. Please enable them in your browser settings."
},
"cache": {
"title": "Cache Management",
"description": "Manage offline storage and cached resources",
"labels": {
"size": "Cache Size",
"serviceWorker": "Service Worker"
},
"status": {
"active": "Active",
"inactive": "Inactive"
},
"action": {
"clear": "Clear Cache & Reload"
},
"helper": "This will remove all cached files and reload the app",
"storageUnit": "MB"
},
"features": {
"title": "PWA Features",
"description": "Progressive Web App capabilities",
"items": {
"offline": "Offline Support",
"installable": "Installable",
"backgroundSync": "Background Sync",
"pushNotifications": "Push Notifications",
"shortcuts": "App Shortcuts"
}
},
"toasts": {
"installSuccess": "App installed successfully!",
"installCancelled": "Installation cancelled",
"update": "Updating app...",
"cacheCleared": "Cache cleared! Reloading...",
"notificationsEnabled": "Notifications enabled",
"notificationsDenied": "Notification permission denied"
},
"defaults": {
"cacheCalculating": "Calculating..."
}
}