Support Inbox
A list of tickets next to the conversation of the selected ticket.
Preview
File: app.tsx
"use client";import React, {useMemo, useState} from "react";import {Badge,Button,Chip,Container,ContextMenu,IconCheck,IconMail,IconMore2Alt,IconReplyAll,IconSendPlane,IconUserShared,List,ListHeader,ListItem,PageHeaderView,RichText,RichTextInput,Status,StatusStyle,Tag,TagStyle,Tile,TileContent,TileFooter,TileHeader,Timeline,TimelineStep,Tooltip,useSimpleList,useSnackbar,} from "@brynlabs/fusion-ui";import {Priority, Ticket, tickets as initialTickets, TicketStatus} from "./data";import styles from "./support-inbox.module.scss";const statusStyles: Record<TicketStatus, StatusStyle> = {Open: "info",Waiting: "warn",Solved: "success",};const priorityStyles: Record<Priority, TagStyle> = {Low: "secondary",Normal: "primary",Urgent: "critical",};const filters = ["All", "Mine", "Open"] as const;type Filter = (typeof filters)[number];export const SupportInboxApp = () => {const {showSnackbar} = useSnackbar();const [tickets, setTickets] = useState(initialTickets);const [filter, setFilter] = useState<Filter>("All");const [selectedId, setSelectedId] = useState(initialTickets[0]!.id);const [reply, setReply] = useState<RichText | null>(null);const [replyKey, setReplyKey] = useState(0);const visible = useMemo(() =>tickets.filter(ticket =>filter === "All" ||(filter === "Mine" && ticket.assignedToMe) ||(filter === "Open" && ticket.status === "Open"),),[tickets, filter],);const list = useSimpleList(visible, {searchIndexRow: ticket => [ticket.subject, ticket.customer, ...ticket.tags],});const selected = tickets.find(ticket => ticket.id === selectedId);const unread = tickets.filter(ticket => ticket.unread).length;const update = (id: number, changes: Partial<Ticket>) =>setTickets(current => current.map(t => (t.id === id ? {...t, ...changes} : t)));const open = (ticket: Ticket) => {setSelectedId(ticket.id);update(ticket.id, {unread: false});};const sendReply = () => {if (!selected) return;update(selected.id, {status: "Waiting",events: [...selected.events,{title: "Reply sent",date: "Just now",text: "Your reply was sent to the customer.",highlighted: true,},],});setReply(null);setReplyKey(key => key + 1);showSnackbar({message: "Reply sent"});};return (<PageHeaderViewpageHeaderTitle={<span className={styles.title}>Inbox <Badge style={"primary"}>{String(unread)}</Badge></span>}subtitle={"Support tickets of your team"}><Container direction={"both"}><div className={styles.layout}><div className={styles.listColumn}><div className={styles.filters}>{filters.map(name => (<Chipkey={name}label={name}state={filter === name ? "selected" : "unselected"}onClick={() => setFilter(name)}/>))}</div><List header={<ListHeader onSearch={list.search} sort={list.sort} />}>{list.data.map(({data: ticket}) => (<ListItem key={ticket.id} onClick={() => open(ticket)}><divclassName={styles.item}data-selected={ticket.id === selectedId || undefined}><div className={styles.itemHeader}><span className={ticket.unread ? "strong-font" : undefined}>{ticket.subject}</span><Statusstatus={ticket.status}statusStyle={statusStyles[ticket.status]}emphasis={"low"}/></div><span className={"small-font"}>{ticket.customer} · #{ticket.id}</span><span className={`small-font ${styles.preview}`}>{ticket.preview}</span></div></ListItem>))}</List></div>{selected && (<Tile className={styles.detail}><TileHeader showBorder><div className={styles.detailHeader}><div><div>{selected.subject}</div><div className={styles.tags}><Statusstatus={selected.status}statusStyle={statusStyles[selected.status]}/><Tag tagStyle={priorityStyles[selected.priority]}>{selected.priority}</Tag>{selected.tags.map(tag => (<Tag key={tag} tagStyle={"secondary"} emphasis={"low"}>{tag}</Tag>))}</div></div><div className={styles.actions}><Tooltip tooltip={"Mark as solved"}><ButtonbuttonStyle={"ghost"}color={"neutral"}aria-label={"Mark as solved"}primaryIcon={<IconCheck />}onClick={() => {update(selected.id, {status: "Solved"});showSnackbar({message: `#${selected.id} marked as solved`});}}/></Tooltip><ContextMenutrigger={"leftClick"}origin={"element"}menu={[{items: [{title: "Assign to me",icon: <IconUserShared />,callback: () => update(selected.id, {assignedToMe: true}),},{title: "Mark as unread",icon: <IconMail />,callback: () => update(selected.id, {unread: true}),},],},]}><ButtonbuttonStyle={"ghost"}color={"neutral"}aria-label={"More actions"}primaryIcon={<IconMore2Alt />}/></ContextMenu></div></div></TileHeader><TileContent><Timeline>{[...selected.events].reverse().map((event, index) => (<TimelineStepkey={index}stepTitle={event.title}stepSubtitle={event.date}highlighted={event.highlighted}icon={event.title.startsWith("Reply") ? <IconReplyAll /> : undefined}>{event.text}</TimelineStep>))}</Timeline></TileContent><TileFooter showBorder><div className={styles.reply}><RichTextInputkey={replyKey}label={`Reply to ${selected.customer}`}placeholder={"Write a reply …"}value={reply}onChange={setReply}/><div><Button primaryIcon={<IconSendPlane />} onClick={sendReply}>Send reply</Button></div></div></TileFooter></Tile>)}</div></Container></PageHeaderView>);};
What it shows
- A searchable list with selection in a master detail layout
- Tags, statuses and badges to scan tickets quickly
- A timeline with the history of the ticket
- A rich text input to reply, and a context menu with ticket actions
Components used
Read more about each component and when to use it.

