feat: add email notification preferences and update settings page structure
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useAuth } from '@/lib/auth/context'
|
import { useAuth } from '@/lib/auth/context'
|
||||||
import ProfileSettings from '@/components/settings/ProfileSettings'
|
import ProfileSettings from '@/components/settings/ProfileSettings'
|
||||||
import TrustedDevicesCard from '@/components/settings/TrustedDevicesCard'
|
import TrustedDevicesCard from '@/components/settings/TrustedDevicesCard'
|
||||||
import SecurityActivityCard from '@/components/settings/SecurityActivityCard'
|
import SecurityActivityCard from '@/components/settings/SecurityActivityCard'
|
||||||
|
import { updateUserPreferences } from '@/lib/api/user'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import {
|
import {
|
||||||
UserIcon,
|
UserIcon,
|
||||||
@@ -29,15 +30,16 @@ function BellIcon({ className }: { className?: string }) {
|
|||||||
// --- Types ---
|
// --- Types ---
|
||||||
|
|
||||||
type ProfileSubTab = 'profile' | 'security' | 'preferences'
|
type ProfileSubTab = 'profile' | 'security' | 'preferences'
|
||||||
|
type NotificationSubTab = 'email' | 'center'
|
||||||
|
|
||||||
type ActiveSelection =
|
type ActiveSelection =
|
||||||
| { section: 'profile'; subTab: ProfileSubTab }
|
| { section: 'profile'; subTab: ProfileSubTab }
|
||||||
| { section: 'notifications' }
|
| { section: 'notifications'; subTab: NotificationSubTab }
|
||||||
| { section: 'account' }
|
| { section: 'account' }
|
||||||
| { section: 'devices' }
|
| { section: 'devices' }
|
||||||
| { section: 'activity' }
|
| { section: 'activity' }
|
||||||
|
|
||||||
type ExpandableSection = 'profile' | 'account'
|
type ExpandableSection = 'profile' | 'notifications' | 'account'
|
||||||
|
|
||||||
// --- Sidebar Components ---
|
// --- Sidebar Components ---
|
||||||
|
|
||||||
@@ -137,6 +139,98 @@ function ExpandableSubItems({ expanded, children }: { expanded: boolean; childre
|
|||||||
|
|
||||||
// --- Content Components ---
|
// --- Content Components ---
|
||||||
|
|
||||||
|
// Email Notification Preferences Card
|
||||||
|
const PULSE_NOTIFICATION_OPTIONS = [
|
||||||
|
{ key: 'security_alerts', label: 'Security Alerts', description: 'Important security events like new logins, password changes, and 2FA updates.' },
|
||||||
|
]
|
||||||
|
|
||||||
|
function EmailNotificationPreferencesCard() {
|
||||||
|
const { user } = useAuth()
|
||||||
|
const [emailNotifications, setEmailNotifications] = useState<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user?.preferences?.email_notifications) {
|
||||||
|
setEmailNotifications(user.preferences.email_notifications)
|
||||||
|
} else {
|
||||||
|
const defaults = PULSE_NOTIFICATION_OPTIONS.reduce((acc, option) => ({
|
||||||
|
...acc,
|
||||||
|
[option.key]: true
|
||||||
|
}), {} as Record<string, boolean>)
|
||||||
|
setEmailNotifications(defaults)
|
||||||
|
}
|
||||||
|
}, [user])
|
||||||
|
|
||||||
|
const handleToggle = async (key: string) => {
|
||||||
|
const newState = {
|
||||||
|
...emailNotifications,
|
||||||
|
[key]: !emailNotifications[key]
|
||||||
|
}
|
||||||
|
setEmailNotifications(newState)
|
||||||
|
try {
|
||||||
|
await updateUserPreferences({
|
||||||
|
email_notifications: newState as { new_file_received: boolean; file_downloaded: boolean; security_alerts: boolean }
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
setEmailNotifications(prev => ({
|
||||||
|
...prev,
|
||||||
|
[key]: !prev[key]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-neutral-900 rounded-2xl border border-neutral-200 dark:border-neutral-800 p-6 shadow-sm">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<div className="p-2 rounded-lg bg-brand-orange/10">
|
||||||
|
<BellIcon className="w-5 h-5 text-brand-orange" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-neutral-900 dark:text-white">Email Notifications</h2>
|
||||||
|
<p className="text-sm text-neutral-500">Choose which email notifications you receive</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{PULSE_NOTIFICATION_OPTIONS.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.key}
|
||||||
|
className={`flex items-center justify-between p-4 border rounded-xl transition-all duration-200 ${
|
||||||
|
emailNotifications[item.key]
|
||||||
|
? 'bg-orange-50 dark:bg-brand-orange/10 border-brand-orange shadow-sm'
|
||||||
|
: 'bg-white dark:bg-neutral-900 border-neutral-200 dark:border-neutral-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<span className={`block text-sm font-medium transition-colors duration-200 ${
|
||||||
|
emailNotifications[item.key] ? 'text-brand-orange' : 'text-neutral-900 dark:text-white'
|
||||||
|
}`}>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
<span className={`block text-xs transition-colors duration-200 ${
|
||||||
|
emailNotifications[item.key] ? 'text-brand-orange/80' : 'text-neutral-500 dark:text-neutral-400'
|
||||||
|
}`}>
|
||||||
|
{item.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handleToggle(item.key)}
|
||||||
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
||||||
|
emailNotifications[item.key] ? 'bg-brand-orange' : 'bg-neutral-200 dark:bg-neutral-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
|
emailNotifications[item.key] ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function AccountManagementCard() {
|
function AccountManagementCard() {
|
||||||
const accountLinks = [
|
const accountLinks = [
|
||||||
{
|
{
|
||||||
@@ -234,13 +328,14 @@ function AppSettingsSection() {
|
|||||||
case 'profile':
|
case 'profile':
|
||||||
return <ProfileSettings activeTab={active.subTab} />
|
return <ProfileSettings activeTab={active.subTab} />
|
||||||
case 'notifications':
|
case 'notifications':
|
||||||
return (
|
if (active.subTab === 'email') return <EmailNotificationPreferencesCard />
|
||||||
|
if (active.subTab === 'center') return (
|
||||||
<div className="bg-white dark:bg-neutral-900 rounded-2xl border border-neutral-200 dark:border-neutral-800 p-8 shadow-sm">
|
<div className="bg-white dark:bg-neutral-900 rounded-2xl border border-neutral-200 dark:border-neutral-800 p-8 shadow-sm">
|
||||||
<div className="text-center max-w-md mx-auto">
|
<div className="text-center max-w-md mx-auto">
|
||||||
<BellIcon className="w-12 h-12 text-neutral-300 mx-auto mb-4" />
|
<BellIcon className="w-12 h-12 text-neutral-300 mx-auto mb-4" />
|
||||||
<h3 className="text-lg font-medium text-neutral-900 dark:text-white mb-2">Notification Preferences</h3>
|
<h3 className="text-lg font-medium text-neutral-900 dark:text-white mb-2">Notification Center</h3>
|
||||||
<p className="text-sm text-neutral-500 mb-4">
|
<p className="text-sm text-neutral-500 mb-4">
|
||||||
Configure which notifications you receive and how you want to be notified.
|
View and manage all your notifications in one place.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href="/notifications"
|
href="/notifications"
|
||||||
@@ -252,6 +347,7 @@ function AppSettingsSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
return null
|
||||||
case 'account':
|
case 'account':
|
||||||
return <AccountManagementCard />
|
return <AccountManagementCard />
|
||||||
case 'devices':
|
case 'devices':
|
||||||
@@ -306,16 +402,34 @@ function AppSettingsSection() {
|
|||||||
</ExpandableSubItems>
|
</ExpandableSubItems>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Notifications (flat, no expansion) */}
|
{/* Notifications (expandable) */}
|
||||||
<SectionHeader
|
<div>
|
||||||
expanded={false}
|
<SectionHeader
|
||||||
active={active.section === 'notifications'}
|
expanded={expanded.has('notifications')}
|
||||||
onToggle={() => setActive({ section: 'notifications' })}
|
active={active.section === 'notifications'}
|
||||||
icon={BellIcon}
|
onToggle={() => {
|
||||||
label="Notifications"
|
toggleSection('notifications')
|
||||||
description="Email and in-app notifications"
|
if (!expanded.has('notifications')) {
|
||||||
hasChildren={false}
|
selectSubTab({ section: 'notifications', subTab: 'email' })
|
||||||
/>
|
}
|
||||||
|
}}
|
||||||
|
icon={BellIcon}
|
||||||
|
label="Notifications"
|
||||||
|
description="Email and in-app notifications"
|
||||||
|
/>
|
||||||
|
<ExpandableSubItems expanded={expanded.has('notifications')}>
|
||||||
|
<SubItem
|
||||||
|
active={active.section === 'notifications' && active.subTab === 'email'}
|
||||||
|
onClick={() => selectSubTab({ section: 'notifications', subTab: 'email' })}
|
||||||
|
label="Email Preferences"
|
||||||
|
/>
|
||||||
|
<SubItem
|
||||||
|
active={active.section === 'notifications' && active.subTab === 'center'}
|
||||||
|
onClick={() => selectSubTab({ section: 'notifications', subTab: 'center' })}
|
||||||
|
label="Notification Center"
|
||||||
|
/>
|
||||||
|
</ExpandableSubItems>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export default function ProfileSettings({ activeTab }: Props = {}) {
|
|||||||
logout={logout}
|
logout={logout}
|
||||||
activeTab={activeTab}
|
activeTab={activeTab}
|
||||||
hideNav={activeTab !== undefined}
|
hideNav={activeTab !== undefined}
|
||||||
|
hideNotifications
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"name": "pulse-frontend",
|
"name": "pulse-frontend",
|
||||||
"version": "0.11.1-alpha",
|
"version": "0.11.1-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ciphera-net/ui": "^0.0.75",
|
"@ciphera-net/ui": "^0.0.76",
|
||||||
"@ducanh2912/next-pwa": "^10.2.9",
|
"@ducanh2912/next-pwa": "^10.2.9",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@simplewebauthn/browser": "^13.2.2",
|
"@simplewebauthn/browser": "^13.2.2",
|
||||||
@@ -1543,9 +1543,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ciphera-net/ui": {
|
"node_modules/@ciphera-net/ui": {
|
||||||
"version": "0.0.75",
|
"version": "0.0.76",
|
||||||
"resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.75/0a5f40babb9c7a8ae9d067155bc7c05c322ca410",
|
"resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.76/f584def8ea9ac4bccc52abdd281212f2e28959c0",
|
||||||
"integrity": "sha512-t1dPS9sID1qCC8A3bz91dtJX9NwnfqaNhkWJpeLQQDPxg89+HodtoqQ2gZaLv2X0rK1gekh6MRBNwXil1ePxBA==",
|
"integrity": "sha512-Bw7KOSUXQajfMAmgU39XiJGFudxx7Gj7Tnjr2tHHL+2oLOoBuDCNRRIC+A6Uo5WmxDBitw6voipoOfRcFo5XAA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"type-check": "tsc --noEmit"
|
"type-check": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ciphera-net/ui": "^0.0.75",
|
"@ciphera-net/ui": "^0.0.76",
|
||||||
"@ducanh2912/next-pwa": "^10.2.9",
|
"@ducanh2912/next-pwa": "^10.2.9",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@simplewebauthn/browser": "^13.2.2",
|
"@simplewebauthn/browser": "^13.2.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user