fix(shared-email): Improve date handling and error resilience in shared email page

This commit is contained in:
beilunyang
2025-10-18 22:03:27 +08:00
parent b83b05b7b0
commit 503856a8a8
4 changed files with 110 additions and 46 deletions
+101 -29
View File
@@ -11,8 +11,8 @@ import { EMAIL_CONFIG } from "@/config"
interface Email { interface Email {
id: string id: string
address: string address: string
createdAt: string createdAt: Date
expiresAt: string expiresAt: Date
} }
interface Message { interface Message {
@@ -20,8 +20,8 @@ interface Message {
from_address?: string from_address?: string
to_address?: string to_address?: string
subject: string subject: string
received_at?: string received_at?: Date
sent_at?: string sent_at?: Date
} }
interface MessageDetail extends Message { interface MessageDetail extends Message {
@@ -37,16 +37,16 @@ interface SharedEmailPageClientProps {
token: string token: string
} }
export function SharedEmailPageClient({ export function SharedEmailPageClient({
email, email,
initialMessages, initialMessages,
initialNextCursor, initialNextCursor,
initialTotal, initialTotal,
token token
}: SharedEmailPageClientProps) { }: SharedEmailPageClientProps) {
const t = useTranslations("emails") const t = useTranslations("emails")
const tShared = useTranslations("emails.shared") const tShared = useTranslations("emails.shared")
const [messages, setMessages] = useState<Message[]>(initialMessages) const [messages, setMessages] = useState<Message[]>(initialMessages)
const [selectedMessage, setSelectedMessage] = useState<MessageDetail | null>(null) const [selectedMessage, setSelectedMessage] = useState<MessageDetail | null>(null)
const [messageLoading, setMessageLoading] = useState(false) const [messageLoading, setMessageLoading] = useState(false)
@@ -75,12 +75,12 @@ export function SharedEmailPageClient({
const messagesResponse = await fetch(url) const messagesResponse = await fetch(url)
if (messagesResponse.ok) { if (messagesResponse.ok) {
const messagesData = await messagesResponse.json() as { const messagesData = await messagesResponse.json() as {
messages: Message[] messages: Message[]
nextCursor: string | null nextCursor: string | null
total: number total: number
} }
if (!cursor) { if (!cursor) {
// 刷新时:合并新消息和旧消息,避免重复 // 刷新时:合并新消息和旧消息,避免重复
const newMessages = messagesData.messages const newMessages = messagesData.messages
@@ -144,7 +144,7 @@ export function SharedEmailPageClient({
return () => { return () => {
stopPolling() stopPolling()
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [token]) }, [token])
const handleLoadMore = () => { const handleLoadMore = () => {
@@ -156,13 +156,13 @@ export function SharedEmailPageClient({
const fetchMessageDetail = async (messageId: string) => { const fetchMessageDetail = async (messageId: string) => {
try { try {
setMessageLoading(true) setMessageLoading(true)
const response = await fetch(`/api/shared/${token}/messages/${messageId}`) const response = await fetch(`/api/shared/${token}/messages/${messageId}`)
if (!response.ok) { if (!response.ok) {
throw new Error("Failed to load message") throw new Error("Failed to load message")
} }
const data = await response.json() as { message: MessageDetail } const data = await response.json() as { message: MessageDetail }
setSelectedMessage(data.message) setSelectedMessage(data.message)
} catch (err) { } catch (err) {
@@ -175,23 +175,47 @@ export function SharedEmailPageClient({
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900"> <div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<div className="container mx-auto p-4 max-w-7xl"> <div className="container mx-auto p-4 max-w-7xl">
<BrandHeader <BrandHeader
title={email.address} title={email.address}
subtitle={new Date(email.expiresAt).getFullYear() === 9999 subtitle={(() => {
? tShared("permanent") try {
: `${tShared("expiresAt")}: ${new Date(email.expiresAt).toLocaleDateString()} ${new Date(email.expiresAt).toLocaleTimeString()}`} const expiresDate = new Date(email.expiresAt)
if (isNaN(expiresDate.getTime())) return tShared("sharedMailbox")
return expiresDate.getFullYear() === 9999
? tShared("permanent")
: `${tShared("expiresAt")}: ${expiresDate.toLocaleDateString()} ${expiresDate.toLocaleTimeString()}`
} catch {
return tShared("sharedMailbox")
}
})()}
showCta={true} showCta={true}
ctaText={tShared("createOwnEmail")} ctaText={tShared("createOwnEmail")}
/> />
{/* 桌面端双栏布局 */} {/* 桌面端双栏布局 */}
<div className="hidden lg:grid grid-cols-2 gap-4 h-[calc(100vh-280px)] mt-6"> <div className="hidden lg:grid grid-cols-2 gap-4 h-[calc(100vh-280px)] mt-6">
<div className="border-2 border-primary/20 bg-background rounded-lg overflow-hidden"> <div className="border-2 border-primary/20 bg-background rounded-lg overflow-hidden">
<SharedMessageList <SharedMessageList
messages={messages.map(msg => ({ messages={messages.map(msg => ({
...msg, ...msg,
received_at: msg.received_at ? new Date(msg.received_at as string).getTime() : undefined, received_at: (() => {
sent_at: msg.sent_at ? new Date(msg.sent_at as string).getTime() : undefined if (!msg.received_at) return undefined
try {
const date = new Date(msg.received_at)
return isNaN(date.getTime()) ? undefined : date.getTime()
} catch {
return undefined
}
})(),
sent_at: (() => {
if (!msg.sent_at) return undefined
try {
const date = new Date(msg.sent_at)
return isNaN(date.getTime()) ? undefined : date.getTime()
} catch {
return undefined
}
})()
}))} }))}
selectedMessageId={selectedMessage?.id} selectedMessageId={selectedMessage?.id}
onMessageSelect={fetchMessageDetail} onMessageSelect={fetchMessageDetail}
@@ -216,8 +240,24 @@ export function SharedEmailPageClient({
<SharedMessageDetail <SharedMessageDetail
message={selectedMessage ? { message={selectedMessage ? {
...selectedMessage, ...selectedMessage,
received_at: selectedMessage.received_at ? new Date(selectedMessage.received_at as string).getTime() : undefined, received_at: (() => {
sent_at: selectedMessage.sent_at ? new Date(selectedMessage.sent_at as string).getTime() : undefined if (!selectedMessage.received_at) return undefined
try {
const date = new Date(selectedMessage.received_at)
return isNaN(date.getTime()) ? undefined : date.getTime()
} catch {
return undefined
}
})(),
sent_at: (() => {
if (!selectedMessage.sent_at) return undefined
try {
const date = new Date(selectedMessage.sent_at)
return isNaN(date.getTime()) ? undefined : date.getTime()
} catch {
return undefined
}
})()
} : null} } : null}
loading={messageLoading} loading={messageLoading}
t={{ t={{
@@ -243,8 +283,24 @@ export function SharedEmailPageClient({
<SharedMessageList <SharedMessageList
messages={messages.map(msg => ({ messages={messages.map(msg => ({
...msg, ...msg,
received_at: msg.received_at ? new Date(msg.received_at as string).getTime() : undefined, received_at: (() => {
sent_at: msg.sent_at ? new Date(msg.sent_at as string).getTime() : undefined if (!msg.received_at) return undefined
try {
const date = new Date(msg.received_at)
return isNaN(date.getTime()) ? undefined : date.getTime()
} catch {
return undefined
}
})(),
sent_at: (() => {
if (!msg.sent_at) return undefined
try {
const date = new Date(msg.sent_at)
return isNaN(date.getTime()) ? undefined : date.getTime()
} catch {
return undefined
}
})()
}))} }))}
selectedMessageId={null} selectedMessageId={null}
onMessageSelect={fetchMessageDetail} onMessageSelect={fetchMessageDetail}
@@ -279,8 +335,24 @@ export function SharedEmailPageClient({
<SharedMessageDetail <SharedMessageDetail
message={{ message={{
...selectedMessage, ...selectedMessage,
received_at: selectedMessage.received_at ? new Date(selectedMessage.received_at as string).getTime() : undefined, received_at: (() => {
sent_at: selectedMessage.sent_at ? new Date(selectedMessage.sent_at as string).getTime() : undefined if (!selectedMessage.received_at) return undefined
try {
const date = new Date(selectedMessage.received_at)
return isNaN(date.getTime()) ? undefined : date.getTime()
} catch {
return undefined
}
})(),
sent_at: (() => {
if (!selectedMessage.sent_at) return undefined
try {
const date = new Date(selectedMessage.sent_at)
return isNaN(date.getTime()) ? undefined : date.getTime()
} catch {
return undefined
}
})()
}} }}
loading={messageLoading} loading={messageLoading}
t={{ t={{
@@ -301,7 +373,7 @@ export function SharedEmailPageClient({
</div> </div>
</div> </div>
</div> </div>
<FloatingLanguageSwitcher /> <FloatingLanguageSwitcher />
</div> </div>
) )
+5 -13
View File
@@ -13,10 +13,10 @@ interface PageProps {
export default async function SharedEmailPage({ params }: PageProps) { export default async function SharedEmailPage({ params }: PageProps) {
const { token } = await params const { token } = await params
const tShared = await getTranslations("emails.shared") const tShared = await getTranslations("emails.shared")
// 服务端获取数据 // 服务端获取数据
const email = await getSharedEmail(token) const email = await getSharedEmail(token)
if (!email) { if (!email) {
return ( return (
<SharedErrorPage <SharedErrorPage
@@ -33,17 +33,9 @@ export default async function SharedEmailPage({ params }: PageProps) {
const messagesResult = await getSharedEmailMessages(token) const messagesResult = await getSharedEmailMessages(token)
return ( return (
<SharedEmailPageClient <SharedEmailPageClient
email={{ email={email}
...email, initialMessages={messagesResult.messages}
createdAt: email.createdAt.toISOString(),
expiresAt: email.expiresAt.toISOString()
}}
initialMessages={messagesResult.messages.map(msg => ({
...msg,
received_at: msg.received_at?.toISOString(),
sent_at: msg.sent_at?.toISOString()
}))}
initialNextCursor={messagesResult.nextCursor} initialNextCursor={messagesResult.nextCursor}
initialTotal={messagesResult.total} initialTotal={messagesResult.total}
token={token} token={token}
+2 -2
View File
@@ -256,7 +256,7 @@ export function ShareDialog({ emailId }: ShareDialogProps) {
} }
}} }}
className={cn( className={cn(
"flex-1 text-xs p-1 rounded truncate font-mono transition-colors", "flex-1 text-xs p-1 rounded font-mono transition-colors break-all",
isExpired isExpired
? "bg-destructive/10 text-destructive/70 cursor-not-allowed pointer-events-none" ? "bg-destructive/10 text-destructive/70 cursor-not-allowed pointer-events-none"
: "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary cursor-pointer" : "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary cursor-pointer"
@@ -281,7 +281,7 @@ export function ShareDialog({ emailId }: ShareDialogProps) {
<Trash2 className="h-4 w-4 text-destructive" /> <Trash2 className="h-4 w-4 text-destructive" />
</Button> </Button>
</div> </div>
<div className="flex gap-4 text-xs"> <div className="flex gap-y-4 gap-x-2 sm:gap-x-4 text-xs">
<span className={cn( <span className={cn(
isExpired ? "text-destructive/70" : "text-gray-500" isExpired ? "text-destructive/70" : "text-gray-500"
)}> )}>
@@ -265,7 +265,7 @@ export function ShareMessageDialog({ emailId, messageId, messageSubject, trigger
} }
}} }}
className={cn( className={cn(
"flex-1 text-xs p-1 rounded truncate font-mono transition-colors", "flex-1 text-xs p-1 rounded font-mono transition-colors break-all",
isExpired isExpired
? "bg-destructive/10 text-destructive/70 cursor-not-allowed pointer-events-none" ? "bg-destructive/10 text-destructive/70 cursor-not-allowed pointer-events-none"
: "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary cursor-pointer" : "bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:text-primary dark:hover:text-primary cursor-pointer"
@@ -290,7 +290,7 @@ export function ShareMessageDialog({ emailId, messageId, messageSubject, trigger
<Trash2 className="h-4 w-4 text-destructive" /> <Trash2 className="h-4 w-4 text-destructive" />
</Button> </Button>
</div> </div>
<div className="flex gap-4 text-xs"> <div className="flex gap-y-4 gap-x-2 sm:gap-x-4 text-xs">
<span className={cn( <span className={cn(
isExpired ? "text-destructive/70" : "text-gray-500" isExpired ? "text-destructive/70" : "text-gray-500"
)}> )}>