refactor: convert notification preferences to modal dialog

- Convert NotificationPreferences from inline Card to Modal dialog
- Add button next to export button in repair admin header
- Load preferences on modal open instead of component mount
- Add close button in modal footer
- Improve mobile responsiveness with button grouping
This commit is contained in:
Claude 2025-11-23 08:14:24 +00:00
parent c6e5a9535a
commit eb9b68e753
No known key found for this signature in database
2 changed files with 91 additions and 68 deletions

View file

@ -1,5 +1,14 @@
import { useEffect, useState } from "react"
import { Card, CardBody, CardHeader, Switch, Spinner, Button } from "@heroui/react"
import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Switch,
Spinner,
Button,
} from "@heroui/react"
import { saturdayClient } from "../../utils/client"
import type { components } from "../../types/saturday"
@ -11,15 +20,23 @@ interface NotificationPreferencesProps {
}
export default function NotificationPreferences({ token }: NotificationPreferencesProps) {
const [isOpen, setIsOpen] = useState(false)
const [preferences, setPreferences] = useState<NotificationPreferenceItem[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isLoading, setIsLoading] = useState(false)
const [isSaving, setIsSaving] = useState(false)
const [errorMessage, setErrorMessage] = useState<string>("")
const [successMessage, setSuccessMessage] = useState<string>("")
useEffect(() => {
const openModal = () => {
setIsOpen(true)
loadPreferences()
}, [token])
}
const closeModal = () => {
setIsOpen(false)
setErrorMessage("")
setSuccessMessage("")
}
const loadPreferences = async () => {
if (!token) return
@ -99,60 +116,73 @@ export default function NotificationPreferences({ token }: NotificationPreferenc
}
}
if (isLoading) {
return (
<Card className="w-full">
<CardBody className="flex justify-center items-center py-8">
<Spinner label="加载中..." />
</CardBody>
</Card>
)
}
return (
<Card className="w-full">
<CardHeader className="flex flex-col items-start gap-2 pb-4">
<h3 className="text-lg sm:text-xl font-bold"></h3>
<p className="text-sm text-gray-500"></p>
</CardHeader>
<CardBody className="gap-4">
{preferences.map((pref) => (
<div
key={pref.notificationType}
className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 sm:gap-4 py-2 border-b border-gray-100 last:border-0"
>
<div className="flex-1">
<p className="text-sm sm:text-base font-medium">{pref.description}</p>
<p className="text-xs text-gray-400 mt-1">{pref.notificationType}</p>
</div>
<Switch
isSelected={pref.enabled}
onValueChange={(value) => handleToggle(pref.notificationType, value)}
isDisabled={isSaving}
size="sm"
/>
</div>
))}
<>
<Button onPress={openModal} color="primary" variant="flat">
</Button>
{/* Messages */}
{errorMessage && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3 mt-2">
<p className="text-sm text-red-800">{errorMessage}</p>
</div>
)}
<Modal isOpen={isOpen} onClose={closeModal} size="2xl">
<ModalContent>
<ModalHeader className="flex flex-col gap-1">
<h3 className="text-xl font-bold"></h3>
<p className="text-sm text-gray-500 font-normal"></p>
</ModalHeader>
<ModalBody>
{isLoading ? (
<div className="flex justify-center items-center py-8">
<Spinner label="加载中..." />
</div>
) : (
<>
<div className="flex flex-col gap-4">
{preferences.map((pref) => (
<div
key={pref.notificationType}
className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 sm:gap-4 py-3 border-b border-gray-100 last:border-0"
>
<div className="flex-1">
<p className="text-sm sm:text-base font-medium">{pref.description}</p>
<p className="text-xs text-gray-400 mt-1">{pref.notificationType}</p>
</div>
<Switch
isSelected={pref.enabled}
onValueChange={(value) => handleToggle(pref.notificationType, value)}
isDisabled={isSaving}
size="sm"
/>
</div>
))}
{successMessage && (
<div className="bg-green-50 border border-green-200 rounded-lg p-3 mt-2">
<p className="text-sm text-green-800">{successMessage}</p>
</div>
)}
{preferences.length === 0 && !errorMessage && (
<div className="text-center py-4 text-gray-500">
<p className="text-sm"></p>
</div>
)}
</div>
{preferences.length === 0 && !errorMessage && (
<div className="text-center py-4 text-gray-500">
<p className="text-sm"></p>
</div>
)}
</CardBody>
</Card>
{/* Messages */}
{errorMessage && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3 mt-2">
<p className="text-sm text-red-800">{errorMessage}</p>
</div>
)}
{successMessage && (
<div className="bg-green-50 border border-green-200 rounded-lg p-3 mt-2">
<p className="text-sm text-green-800">{successMessage}</p>
</div>
)}
</>
)}
</ModalBody>
<ModalFooter>
<Button variant="flat" onPress={closeModal}>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
)
}

View file

@ -526,20 +526,13 @@ export default function App() {
<section className="box-border max-w-full px-4 sm:px-6 lg:max-w-[1024px] lg:px-[22px] mx-auto mb-16 sm:mb-24">
<div className="mt-6 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="text-xl sm:text-2xl font-bold"></div>
{
userInfo?.roles?.find(v => v.toLowerCase() == "repair admin")
? <div className="w-full sm:w-auto"><ExportExcelModal></ExportExcelModal></div>
: <></>
}
</div>
{/* Notification Preferences Section */}
{token && (
<div className="mt-6">
<NotificationPreferences token={token} />
<div className="flex gap-2 w-full sm:w-auto">
{token && <NotificationPreferences token={token} />}
{userInfo?.roles?.find(v => v.toLowerCase() == "repair admin") && (
<ExportExcelModal />
)}
</div>
)}
</div>
<div className="my-8 flex flex-col gap-4">
{/* Mobile Cards Layout */}
<div className="block sm:hidden">