| // src/components/threads/ThreadParentBanner.tsx | |
| import { useState, useEffect } from "react"; | |
| import api from "@/lib/api"; | |
| import { CornerDownRight, ChevronDown, ChevronUp, Loader2, AlertCircle } from "lucide-react"; | |
| import { toast } from "react-toastify"; | |
| interface ThreadParentBannerProps { | |
| messageId: number; | |
| threadTitle: string; | |
| } | |
| interface ParentMessage { | |
| body: string; | |
| from: string; | |
| sentAt: string; | |
| } | |
| export function ThreadParentBanner({ messageId, threadTitle }: ThreadParentBannerProps) { | |
| const [parent, setParent] = useState<ParentMessage | null>(null); | |
| const [expanded, setExpanded] = useState(false); | |
| const [loading, setLoading] = useState(true); | |
| const [error, setError] = useState(false); | |
| useEffect(() => { | |
| setLoading(true); | |
| setError(false); | |
| api | |
| .get(`/messages/${messageId}`) | |
| .then((res) => { | |
| setParent(res.data); | |
| setLoading(false); | |
| }) | |
| .catch(() => { | |
| setError(true); | |
| setLoading(false); | |
| toast.error("Could not load parent message"); | |
| }); | |
| }, [messageId]); | |
| if (loading) { | |
| return ( | |
| <div className="flex items-center gap-2 border-b border-gray-100 bg-gray-50 px-4 py-2 dark:border-gray-700 dark:bg-gray-800/50"> | |
| <Loader2 className="h-4 w-4 animate-spin text-primary" /> | |
| <span className="text-xs text-gray-500">Loading parent message…</span> | |
| </div> | |
| ); | |
| } | |
| if (error || !parent) { | |
| return ( | |
| <div className="flex items-center gap-2 border-b border-gray-100 bg-red-50 px-4 py-2 dark:border-gray-700 dark:bg-red-900/20"> | |
| <AlertCircle className="h-4 w-4 text-red-500" /> | |
| <span className="text-xs text-red-600 dark:text-red-400"> | |
| Could not load parent message | |
| </span> | |
| </div> | |
| ); | |
| } | |
| const formattedDate = new Date(parent.sentAt).toLocaleDateString(undefined, { | |
| month: "short", | |
| day: "numeric", | |
| year: "numeric", | |
| }); | |
| return ( | |
| <div className="border-b border-gray-100 bg-yellow-50/50 dark:border-gray-700 dark:bg-yellow-900/10"> | |
| <button | |
| onClick={() => setExpanded(!expanded)} | |
| className="flex w-full items-center gap-2 px-4 py-2 text-left transition hover:bg-yellow-100/50 dark:hover:bg-yellow-900/20" | |
| aria-expanded={expanded} | |
| aria-label={expanded ? "Collapse parent message" : "Expand parent message"} | |
| > | |
| <CornerDownRight className="h-4 w-4 shrink-0 text-yellow-600" /> | |
| <div className="min-w-0 flex-1"> | |
| <span className="text-xs font-semibold text-yellow-700 dark:text-yellow-400"> | |
| {parent.from} | |
| </span> | |
| <span className="ml-2 text-xs text-yellow-600/70 dark:text-yellow-400/70"> | |
| {formattedDate} | |
| </span> | |
| <span className="ml-2 text-xs text-gray-400">in thread: {threadTitle}</span> | |
| {!expanded && ( | |
| <p className="mt-0.5 truncate text-xs text-gray-500"> | |
| {parent.body.substring(0, 80)}… | |
| </p> | |
| )} | |
| </div> | |
| {expanded ? ( | |
| <ChevronUp className="h-4 w-4 text-yellow-600" /> | |
| ) : ( | |
| <ChevronDown className="h-4 w-4 text-yellow-600" /> | |
| )} | |
| </button> | |
| {expanded && ( | |
| <div className="px-4 pb-3"> | |
| <p className="whitespace-pre-wrap border-l-2 border-yellow-300 pl-3 text-sm text-gray-700 dark:text-gray-300"> | |
| {parent.body} | |
| </p> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } |
Xet Storage Details
- Size:
- 3.47 kB
- Xet hash:
- 9da8b20e930efff9a187230999e4cbcb21724c546006b5cd8b6e8d8e8052b92e
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.