feat: better dashboard chart (#378)
* dashbaord ui stuff * graph stuff * stuff
This commit is contained in:
@@ -30,9 +30,14 @@ export default function DashboardFilters({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||||
<Select value={domain ?? "All Domains"} onValueChange={(val) => handleDomain(val)}>
|
<Select
|
||||||
|
value={domain ?? "All Domains"}
|
||||||
|
onValueChange={(val) => handleDomain(val)}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-full sm:w-[180px]">
|
<SelectTrigger className="w-full sm:w-[180px]">
|
||||||
{domain ? domainsQuery?.find((d) => d.id === Number(domain))?.name : "All Domains"}
|
{domain
|
||||||
|
? domainsQuery?.find((d) => d.id === Number(domain))?.name
|
||||||
|
: "All Domains"}
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="All Domains" className="capitalize">
|
<SelectItem value="All Domains" className="capitalize">
|
||||||
@@ -46,7 +51,7 @@ export default function DashboardFilters({
|
|||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Tabs value={days || "7"} onValueChange={(value) => setDays(value)}>
|
<Tabs value={days || "30"} onValueChange={(value) => setDays(value)}>
|
||||||
<TabsList className="w-full sm:w-auto">
|
<TabsList className="w-full sm:w-auto">
|
||||||
<TabsTrigger value="7" className="flex-1 sm:flex-none">
|
<TabsTrigger value="7" className="flex-1 sm:flex-none">
|
||||||
7 Days
|
7 Days
|
||||||
|
|||||||
@@ -32,16 +32,18 @@ const STACK_ORDER: string[] = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
type StackKey = (typeof STACK_ORDER)[number];
|
type StackKey = (typeof STACK_ORDER)[number];
|
||||||
|
function createRoundedTopShape(
|
||||||
function createRoundedTopShape(currentKey: StackKey) {
|
currentKey: StackKey,
|
||||||
const currentIndex = STACK_ORDER.indexOf(currentKey);
|
visibleStackOrder: StackKey[],
|
||||||
|
) {
|
||||||
|
const currentIndex = visibleStackOrder.indexOf(currentKey);
|
||||||
return (props: any) => {
|
return (props: any) => {
|
||||||
const payload = props.payload as
|
const payload = props.payload as
|
||||||
| Partial<Record<StackKey, number>>
|
| Partial<Record<StackKey, number>>
|
||||||
| undefined;
|
| undefined;
|
||||||
let hasAbove = false;
|
let hasAbove = false;
|
||||||
for (let i = currentIndex + 1; i < STACK_ORDER.length; i++) {
|
for (let i = currentIndex + 1; i < visibleStackOrder.length; i++) {
|
||||||
const key = STACK_ORDER[i];
|
const key = visibleStackOrder[i];
|
||||||
const val = key ? (payload?.[key] ?? 0) : 0;
|
const val = key ? (payload?.[key] ?? 0) : 0;
|
||||||
if (val > 0) {
|
if (val > 0) {
|
||||||
hasAbove = true;
|
hasAbove = true;
|
||||||
@@ -55,6 +57,7 @@ function createRoundedTopShape(currentKey: StackKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function EmailChart({ days, domain }: EmailChartProps) {
|
export default function EmailChart({ days, domain }: EmailChartProps) {
|
||||||
|
const [selectedMetrics, setSelectedMetrics] = React.useState<StackKey[]>([]);
|
||||||
const domainId = domain ? Number(domain) : undefined;
|
const domainId = domain ? Number(domain) : undefined;
|
||||||
const statusQuery = api.dashboard.emailTimeSeries.useQuery({
|
const statusQuery = api.dashboard.emailTimeSeries.useQuery({
|
||||||
days: days,
|
days: days,
|
||||||
@@ -63,6 +66,32 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
|
|||||||
|
|
||||||
const currentColors = useColors();
|
const currentColors = useColors();
|
||||||
|
|
||||||
|
const metricMeta: Record<StackKey, { label: string; color: string }> = {
|
||||||
|
delivered: { label: "Delivered", color: currentColors.delivered },
|
||||||
|
bounced: { label: "Bounced", color: currentColors.bounced },
|
||||||
|
complained: { label: "Complained", color: currentColors.complained },
|
||||||
|
opened: { label: "Opened", color: currentColors.opened },
|
||||||
|
clicked: { label: "Clicked", color: currentColors.clicked },
|
||||||
|
};
|
||||||
|
|
||||||
|
const visibleMetrics =
|
||||||
|
selectedMetrics.length === 0
|
||||||
|
? STACK_ORDER
|
||||||
|
: STACK_ORDER.filter((key) => selectedMetrics.includes(key));
|
||||||
|
|
||||||
|
const toggleMetric = (metric: StackKey) => {
|
||||||
|
setSelectedMetrics((prev) => {
|
||||||
|
const exists = prev.includes(metric);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
return prev.filter((key) => key !== metric);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSet = new Set([...prev, metric]);
|
||||||
|
return STACK_ORDER.filter((key) => nextSet.has(key));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-16">
|
<div className="flex flex-col gap-16">
|
||||||
{!statusQuery.isLoading && statusQuery.data ? (
|
{!statusQuery.isLoading && statusQuery.data ? (
|
||||||
@@ -75,6 +104,8 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
|
|||||||
status={"total"}
|
status={"total"}
|
||||||
count={statusQuery.data.totalCounts.sent}
|
count={statusQuery.data.totalCounts.sent}
|
||||||
percentage={100}
|
percentage={100}
|
||||||
|
isActive={selectedMetrics.length === 0}
|
||||||
|
isClickable={false}
|
||||||
/>
|
/>
|
||||||
<EmailChartItem
|
<EmailChartItem
|
||||||
status={EmailStatus.DELIVERED}
|
status={EmailStatus.DELIVERED}
|
||||||
@@ -83,6 +114,11 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
|
|||||||
statusQuery.data.totalCounts.delivered /
|
statusQuery.data.totalCounts.delivered /
|
||||||
statusQuery.data.totalCounts.sent
|
statusQuery.data.totalCounts.sent
|
||||||
}
|
}
|
||||||
|
isActive={
|
||||||
|
selectedMetrics.length === 0 ||
|
||||||
|
selectedMetrics.includes("delivered")
|
||||||
|
}
|
||||||
|
onClick={() => toggleMetric("delivered")}
|
||||||
/>
|
/>
|
||||||
<EmailChartItem
|
<EmailChartItem
|
||||||
status={EmailStatus.BOUNCED}
|
status={EmailStatus.BOUNCED}
|
||||||
@@ -91,6 +127,11 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
|
|||||||
statusQuery.data.totalCounts.bounced /
|
statusQuery.data.totalCounts.bounced /
|
||||||
statusQuery.data.totalCounts.sent
|
statusQuery.data.totalCounts.sent
|
||||||
}
|
}
|
||||||
|
isActive={
|
||||||
|
selectedMetrics.length === 0 ||
|
||||||
|
selectedMetrics.includes("bounced")
|
||||||
|
}
|
||||||
|
onClick={() => toggleMetric("bounced")}
|
||||||
/>
|
/>
|
||||||
<EmailChartItem
|
<EmailChartItem
|
||||||
status={EmailStatus.COMPLAINED}
|
status={EmailStatus.COMPLAINED}
|
||||||
@@ -99,6 +140,11 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
|
|||||||
statusQuery.data.totalCounts.complained /
|
statusQuery.data.totalCounts.complained /
|
||||||
statusQuery.data.totalCounts.sent
|
statusQuery.data.totalCounts.sent
|
||||||
}
|
}
|
||||||
|
isActive={
|
||||||
|
selectedMetrics.length === 0 ||
|
||||||
|
selectedMetrics.includes("complained")
|
||||||
|
}
|
||||||
|
onClick={() => toggleMetric("complained")}
|
||||||
/>
|
/>
|
||||||
<EmailChartItem
|
<EmailChartItem
|
||||||
status={EmailStatus.CLICKED}
|
status={EmailStatus.CLICKED}
|
||||||
@@ -107,6 +153,11 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
|
|||||||
statusQuery.data.totalCounts.clicked /
|
statusQuery.data.totalCounts.clicked /
|
||||||
statusQuery.data.totalCounts.sent
|
statusQuery.data.totalCounts.sent
|
||||||
}
|
}
|
||||||
|
isActive={
|
||||||
|
selectedMetrics.length === 0 ||
|
||||||
|
selectedMetrics.includes("clicked")
|
||||||
|
}
|
||||||
|
onClick={() => toggleMetric("clicked")}
|
||||||
/>
|
/>
|
||||||
<EmailChartItem
|
<EmailChartItem
|
||||||
status={EmailStatus.OPENED}
|
status={EmailStatus.OPENED}
|
||||||
@@ -115,6 +166,11 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
|
|||||||
statusQuery.data.totalCounts.opened /
|
statusQuery.data.totalCounts.opened /
|
||||||
statusQuery.data.totalCounts.sent
|
statusQuery.data.totalCounts.sent
|
||||||
}
|
}
|
||||||
|
isActive={
|
||||||
|
selectedMetrics.length === 0 ||
|
||||||
|
selectedMetrics.includes("opened")
|
||||||
|
}
|
||||||
|
onClick={() => toggleMetric("opened")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,6 +191,9 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
|
|||||||
fontSize={12}
|
fontSize={12}
|
||||||
className="font-mono"
|
className="font-mono"
|
||||||
stroke={currentColors.xaxis}
|
stroke={currentColors.xaxis}
|
||||||
|
tick={{ fill: currentColors.xaxis, fillOpacity: 0.65 }}
|
||||||
|
axisLine={false}
|
||||||
|
tickLine={false}
|
||||||
/>
|
/>
|
||||||
{/* <YAxis fontSize={12} className="font-mono" /> */}
|
{/* <YAxis fontSize={12} className="font-mono" /> */}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@@ -154,11 +213,10 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
|
|||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
const hasAnyData =
|
const hasAnyData =
|
||||||
(data.delivered || 0) > 0 ||
|
visibleMetrics.reduce(
|
||||||
(data.bounced || 0) > 0 ||
|
(sum, key) => sum + (data[key] || 0),
|
||||||
(data.complained || 0) > 0 ||
|
0,
|
||||||
(data.opened || 0) > 0 ||
|
) > 0;
|
||||||
(data.clicked || 0) > 0;
|
|
||||||
|
|
||||||
if (!hasAnyData) return null;
|
if (!hasAnyData) return null;
|
||||||
|
|
||||||
@@ -167,105 +225,43 @@ export default function EmailChart({ days, domain }: EmailChartProps) {
|
|||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{data.date}
|
{data.date}
|
||||||
</p>
|
</p>
|
||||||
{data.delivered ? (
|
{visibleMetrics.map((metricKey) => {
|
||||||
<div className="flex gap-2 items-center">
|
const metricValue = data[metricKey] || 0;
|
||||||
|
if (!metricValue) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-2.5 h-2.5 rounded-[2px]"
|
key={metricKey}
|
||||||
style={{ backgroundColor: currentColors.delivered }}
|
className="flex gap-2 items-center"
|
||||||
></div>
|
>
|
||||||
<p className="text-xs text-muted-foreground w-[70px]">
|
|
||||||
Delivered
|
|
||||||
</p>
|
|
||||||
<p className="text-xs font-mono">{data.delivered}</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{data.bounced ? (
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<div
|
|
||||||
className="w-2.5 h-2.5 rounded-[2px]"
|
|
||||||
style={{ backgroundColor: currentColors.bounced }}
|
|
||||||
></div>
|
|
||||||
<p className="text-xs text-muted-foreground w-[70px]">
|
|
||||||
Bounced
|
|
||||||
</p>
|
|
||||||
<p className="text-xs font-mono">{data.bounced}</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{data.complained ? (
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<div
|
<div
|
||||||
className="w-2.5 h-2.5 rounded-[2px]"
|
className="w-2.5 h-2.5 rounded-[2px]"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: currentColors.complained,
|
backgroundColor: metricMeta[metricKey].color,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
<p className="text-xs text-muted-foreground w-[70px]">
|
<p className="text-xs text-muted-foreground w-[70px]">
|
||||||
Complained
|
{metricMeta[metricKey].label}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs font-mono">{data.complained}</p>
|
<p className="text-xs font-mono">{metricValue}</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
);
|
||||||
{data.opened ? (
|
})}
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<div
|
|
||||||
className="w-2.5 h-2.5 rounded-[2px]"
|
|
||||||
style={{ backgroundColor: currentColors.opened }}
|
|
||||||
></div>
|
|
||||||
<p className="text-xs text-muted-foreground w-[70px]">
|
|
||||||
Opened
|
|
||||||
</p>
|
|
||||||
<p className="text-xs font-mono">{data.opened}</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{data.clicked ? (
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<div
|
|
||||||
className="w-2.5 h-2.5 rounded-[2px]"
|
|
||||||
style={{ backgroundColor: currentColors.clicked }}
|
|
||||||
></div>
|
|
||||||
<p className="text-xs text-muted-foreground w-[70px]">
|
|
||||||
Clicked
|
|
||||||
</p>
|
|
||||||
<p className="text-xs font-mono">{data.clicked}</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
cursor={false}
|
cursor={false}
|
||||||
/>
|
/>
|
||||||
{/* <Legend /> */}
|
{visibleMetrics.map((metricKey) => (
|
||||||
<Bar
|
<Bar
|
||||||
barSize={8}
|
key={metricKey}
|
||||||
dataKey="delivered"
|
barSize={20}
|
||||||
|
dataKey={metricKey}
|
||||||
stackId="a"
|
stackId="a"
|
||||||
fill={currentColors.delivered}
|
fill={metricMeta[metricKey].color}
|
||||||
shape={createRoundedTopShape("delivered")}
|
shape={createRoundedTopShape(metricKey, visibleMetrics)}
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="bounced"
|
|
||||||
stackId="a"
|
|
||||||
fill={currentColors.bounced}
|
|
||||||
shape={createRoundedTopShape("bounced")}
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="complained"
|
|
||||||
stackId="a"
|
|
||||||
fill={currentColors.complained}
|
|
||||||
shape={createRoundedTopShape("complained")}
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="opened"
|
|
||||||
stackId="a"
|
|
||||||
fill={currentColors.opened}
|
|
||||||
shape={createRoundedTopShape("opened")}
|
|
||||||
/>
|
|
||||||
<Bar
|
|
||||||
dataKey="clicked"
|
|
||||||
stackId="a"
|
|
||||||
fill={currentColors.clicked}
|
|
||||||
shape={createRoundedTopShape("clicked")}
|
|
||||||
/>
|
/>
|
||||||
|
))}
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
@@ -280,6 +276,9 @@ type DashboardItemCardProps = {
|
|||||||
status: EmailStatus | "total";
|
status: EmailStatus | "total";
|
||||||
count: number;
|
count: number;
|
||||||
percentage: number;
|
percentage: number;
|
||||||
|
onClick?: () => void;
|
||||||
|
isActive?: boolean;
|
||||||
|
isClickable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DashboardItemCard: React.FC<DashboardItemCardProps> = ({
|
const DashboardItemCard: React.FC<DashboardItemCardProps> = ({
|
||||||
@@ -311,6 +310,9 @@ const EmailChartItem: React.FC<DashboardItemCardProps> = ({
|
|||||||
status,
|
status,
|
||||||
count,
|
count,
|
||||||
percentage,
|
percentage,
|
||||||
|
onClick,
|
||||||
|
isActive = false,
|
||||||
|
isClickable = true,
|
||||||
}) => {
|
}) => {
|
||||||
const currentColors = useColors();
|
const currentColors = useColors();
|
||||||
|
|
||||||
@@ -333,7 +335,17 @@ const EmailChartItem: React.FC<DashboardItemCardProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3 items-stretch font-mono">
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={!isClickable}
|
||||||
|
aria-pressed={isClickable ? isActive : undefined}
|
||||||
|
className={`flex gap-3 items-stretch font-mono transition-opacity ${
|
||||||
|
isClickable ? "cursor-pointer" : "cursor-default"
|
||||||
|
} ${isActive ? "opacity-100" : "opacity-45 hover:opacity-100"} ${
|
||||||
|
isClickable ? "" : "pointer-events-none"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className=" flex items-center gap-2">
|
<div className=" flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
@@ -354,6 +366,6 @@ const EmailChartItem: React.FC<DashboardItemCardProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useUrlState } from "~/hooks/useUrlState";
|
|||||||
import { ReputationMetrics } from "./reputation-metrics";
|
import { ReputationMetrics } from "./reputation-metrics";
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const [days, setDays] = useUrlState("days", "7");
|
const [days, setDays] = useUrlState("days", "30");
|
||||||
const [domain, setDomain] = useUrlState("domain");
|
const [domain, setDomain] = useUrlState("domain");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -16,16 +16,16 @@ export default function Dashboard() {
|
|||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<H1>Analytics</H1>
|
<H1>Analytics</H1>
|
||||||
<DashboardFilters
|
<DashboardFilters
|
||||||
days={days ?? "7"}
|
days={days ?? "30"}
|
||||||
setDays={setDays}
|
setDays={setDays}
|
||||||
domain={domain}
|
domain={domain}
|
||||||
setDomain={setDomain}
|
setDomain={setDomain}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className=" space-y-12">
|
<div className=" space-y-12">
|
||||||
<EmailChart days={Number(days ?? "7")} domain={domain} />
|
<EmailChart days={Number(days ?? "30")} domain={domain} />
|
||||||
|
|
||||||
<ReputationMetrics days={Number(days ?? "7")} domain={domain} />
|
<ReputationMetrics days={Number(days ?? "30")} domain={domain} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { Prisma, Team } from "@prisma/client";
|
|||||||
|
|
||||||
type EmailTimeSeries = {
|
type EmailTimeSeries = {
|
||||||
days?: number;
|
days?: number;
|
||||||
domain?: number
|
domain?: number;
|
||||||
team: Team
|
team: Team;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function emailTimeSeries(input: EmailTimeSeries) {
|
export async function emailTimeSeries(input: EmailTimeSeries) {
|
||||||
const days = input.days !== 7 ? 30 : 7;
|
const days = input.days !== 7 ? 30 : 7;
|
||||||
const { domain, team } = input
|
const { domain, team } = input;
|
||||||
const startDate = new Date();
|
const startDate = new Date();
|
||||||
startDate.setDate(startDate.getDate() - days);
|
startDate.setDate(startDate.getDate() - days);
|
||||||
const isoStartDate = startDate.toISOString().split("T")[0];
|
const isoStartDate = startDate.toISOString().split("T")[0];
|
||||||
@@ -87,20 +87,19 @@ export async function emailTimeSeries(input: EmailTimeSeries) {
|
|||||||
clicked: 0,
|
clicked: 0,
|
||||||
bounced: 0,
|
bounced: 0,
|
||||||
complained: 0,
|
complained: 0,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return { result: filledResult, totalCounts };
|
return { result: filledResult, totalCounts };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type ReputationMetricsData = {
|
type ReputationMetricsData = {
|
||||||
domain?: number
|
domain?: number;
|
||||||
team: Team
|
team: Team;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function reputationMetricsData(input: ReputationMetricsData) {
|
export async function reputationMetricsData(input: ReputationMetricsData) {
|
||||||
const { domain, team } = input
|
const { domain, team } = input;
|
||||||
|
|
||||||
const reputations = await db.cumulatedMetrics.findMany({
|
const reputations = await db.cumulatedMetrics.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -116,7 +115,7 @@ export async function reputationMetricsData(input: ReputationMetricsData) {
|
|||||||
acc.complained += Number(curr.complained);
|
acc.complained += Number(curr.complained);
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{ delivered: 0, hardBounced: 0, complained: 0 }
|
{ delivered: 0, hardBounced: 0, complained: 0 },
|
||||||
);
|
);
|
||||||
|
|
||||||
const resultWithRates = {
|
const resultWithRates = {
|
||||||
|
|||||||
@@ -122,10 +122,10 @@
|
|||||||
--chart-4: 43 74% 66%;
|
--chart-4: 43 74% 66%;
|
||||||
--chart-5: 27 87% 67%;
|
--chart-5: 27 87% 67%;
|
||||||
|
|
||||||
--sidebar-background: 225 3% 94%;
|
--sidebar-background: 220 2% 96%;
|
||||||
--sidebar-foreground: 240 5.3% 26.1%;
|
--sidebar-foreground: 234 16% 35%;
|
||||||
--sidebar-primary: 240 5.9% 10%;
|
--sidebar-primary: 167 34% 20%;
|
||||||
--sidebar-primary-foreground: 0 0% 98%;
|
--sidebar-primary-foreground: 210 40% 98%;
|
||||||
--sidebar-accent: 240 11% 88%;
|
--sidebar-accent: 240 11% 88%;
|
||||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||||
--sidebar-border: 240 11% 88%;
|
--sidebar-border: 240 11% 88%;
|
||||||
@@ -182,10 +182,10 @@
|
|||||||
--chart-4: 280 65% 60%;
|
--chart-4: 280 65% 60%;
|
||||||
--chart-5: 340 75% 55%;
|
--chart-5: 340 75% 55%;
|
||||||
|
|
||||||
--sidebar-background: 240 23% 9%;
|
--sidebar-background: 240 21% 12%;
|
||||||
--sidebar-foreground: 226 64% 88%;
|
--sidebar-foreground: 226 64% 88%;
|
||||||
--sidebar-primary: 224.3 76.3% 48%;
|
--sidebar-primary: 167 64% 94%;
|
||||||
--sidebar-primary-foreground: 0 0% 100%;
|
--sidebar-primary-foreground: 240 23% 9%;
|
||||||
--sidebar-accent: 237 17% 20%;
|
--sidebar-accent: 237 17% 20%;
|
||||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||||
--sidebar-border: 240 21% 15%;
|
--sidebar-border: 240 21% 15%;
|
||||||
|
|||||||
Reference in New Issue
Block a user