Fusion UI LogoFusion UI Logo
Fusion UI LogoFusion UI Logo
/
Getting Started
Concepts
Components
Example Apps
Getting Started
Concepts
Components
Example Apps
Fusion UI LogoFusion UI Logo
Getting Started
Concepts
Components
Example Apps
Overview
Example Apps
Analytics Dashboard
Customer Management
Account Settings
Support Inbox
Subscription Checkout

Support Inbox

A list of tickets next to the conversation of the selected ticket.

Preview

/preview/support-inbox

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 (
<PageHeaderView
pageHeaderTitle={
<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 => (
<Chip
key={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)}>
<div
className={styles.item}
data-selected={ticket.id === selectedId || undefined}>
<div className={styles.itemHeader}>
<span className={ticket.unread ? "strong-font" : undefined}>
{ticket.subject}
</span>
<Status
status={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}>
<Status
status={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"}>
<Button
buttonStyle={"ghost"}
color={"neutral"}
aria-label={"Mark as solved"}
primaryIcon={<IconCheck />}
onClick={() => {
update(selected.id, {status: "Solved"});
showSnackbar({message: `#${selected.id} marked as solved`});
}}
/>
</Tooltip>
<ContextMenu
trigger={"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}),
},
],
},
]}>
<Button
buttonStyle={"ghost"}
color={"neutral"}
aria-label={"More actions"}
primaryIcon={<IconMore2Alt />}
/>
</ContextMenu>
</div>
</div>
</TileHeader>
<TileContent>
<Timeline>
{[...selected.events].reverse().map((event, index) => (
<TimelineStep
key={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}>
<RichTextInput
key={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.

ListTileTagStatusBadgeTimelineRich Text InputContext MenuChipTooltipButton
On this page
  • Preview
  • What it shows
  • Components used