more cards

This commit is contained in:
nick-delirium 2024-04-30 14:38:53 +02:00
parent e05f143812
commit b01a38ecc9
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
17 changed files with 530 additions and 92 deletions

View file

@ -17,7 +17,6 @@ function DashboardList({ history, siteId }: { history: any; siteId: string }) {
const { dashboardStore } = useStore();
const list = dashboardStore.filteredList;
const dashboardsSearch = dashboardStore.filter.query;
const lenth = list.length;
const tableConfig: TableColumnsType<Dashboard> = [
@ -72,7 +71,7 @@ function DashboardList({ history, siteId }: { history: any; siteId: string }) {
];
return (
<NoContent
show={lenth === 0}
show={list.length === 0 && !dashboardStore.filter.showMine}
title={
<div className="flex flex-col items-center justify-center">
<AnimatedSVG name={ICONS.NO_DASHBOARDS} size={180} />

View file

@ -1,25 +1,41 @@
import { PlusOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { observer } from 'mobx-react-lite';
import React from 'react';
import { PageTitle } from 'UI';
import DashboardSearch from './DashboardSearch';
import { useStore } from 'App/mstore';
import { observer, useObserver } from 'mobx-react-lite';
import { withSiteId } from 'App/routes';
import { Button } from 'antd'
import { PlusOutlined } from "@ant-design/icons";
import { PageTitle } from 'UI';
import DashboardSearch from './DashboardSearch';
import NewDashboardModal from './NewDashModal';
function Header({ history, siteId }: { history: any; siteId: string }) {
const [showModal, setShowModal] = React.useState(false);
const { dashboardStore } = useStore();
const sort = useObserver(() => dashboardStore.sort);
const onSaveDashboard = () => {
dashboardStore.initDashboard();
dashboardStore
.save(dashboardStore.dashboardInstance)
.then(async (syncedDashboard) => {
dashboardStore.selectDashboardById(syncedDashboard.dashboardId);
history.push(
withSiteId(`/dashboard/${syncedDashboard.dashboardId}`, siteId)
);
});
};
const onAddDashboardClick = () => {
dashboardStore.initDashboard();
dashboardStore.save(dashboardStore.dashboardInstance).then(async (syncedDashboard) => {
dashboardStore.selectDashboardById(syncedDashboard.dashboardId);
history.push(withSiteId(`/dashboard/${syncedDashboard.dashboardId}`, siteId));
});
setShowModal(true);
};
const onClose = () => {
setShowModal(false);
};
return (
<>
<div className="flex items-center justify-between px-4 pb-2">
<div className="flex items-baseline mr-3">
<PageTitle title="Dashboards" />
@ -27,7 +43,9 @@ function Header({ history, siteId }: { history: any; siteId: string }) {
<div className="ml-auto flex items-center">
<Button
icon={<PlusOutlined />}
type="primary" onClick={onAddDashboardClick}>
type="primary"
onClick={onAddDashboardClick}
>
Create Dashboard
</Button>
<div className="mx-2"></div>
@ -35,17 +53,9 @@ function Header({ history, siteId }: { history: any; siteId: string }) {
<DashboardSearch />
</div>
</div>
{/*<Select*/}
{/* options={[*/}
{/* { label: 'Newest', value: 'desc' },*/}
{/* { label: 'Oldest', value: 'asc' },*/}
{/* ]}*/}
{/* defaultValue={sort.by}*/}
{/* plain*/}
{/* onChange={({ value }) => dashboardStore.updateKey('sort', { by: value.value })}*/}
{/*/>*/}
</div>
<NewDashboardModal onClose={onClose} open={showModal} onSave={onSaveDashboard} />
</>
);
}

View file

@ -0,0 +1,143 @@
import { Angry, ArrowDownUp, Mouse, MousePointerClick, Unlink } from 'lucide-react';
import React from 'react';
import ExCard from './ExCard';
function ExampleCount() {
return <ExCard title={'Sessions by'}>
<Frustrations />
</ExCard>;
}
function Frustrations() {
const rows = [
{
label: 'Rage Clicks',
progress: 25,
value: 100,
icon: <Angry size={12} strokeWidth={1} />,
},
{
label: 'Dead Clicks',
progress: 75,
value: 75,
icon: <MousePointerClick size={12} strokeWidth={1} />,
},
{
label: '4XX Pages',
progress: 50,
value: 50,
icon: <Unlink size={12} strokeWidth={1} />,
},
{
label: 'Mouse Trashing',
progress: 10,
value: 25,
icon: <Mouse size={12} strokeWidth={1} />,
},
{
label: 'Excessive Scrolling',
progress: 10,
value: 10,
icon: <ArrowDownUp size={12} strokeWidth={1} />,
},
];
return (
<div className={'flex gap-2 flex-col'}>
{rows.map((r) => (
<div
className={
'flex items-center gap-2 border-b border-dotted last:border-0 py-2 first:pt-0 last:pb-0'
}
>
<Circle badgeType={0}>{r.icon}</Circle>
<div>{r.label}</div>
<div style={{ marginLeft: 'auto', marginRight: 20, display: 'flex' }}>
<div
style={{
height: 2,
width: 140 * (0.01 * r.progress),
background: '#394EFF',
}}
className={'rounded-l'}
/>
<div
style={{
height: 2,
width: 140-(140 * (0.01 * r.progress)),
background: '#E2E4F6',
}}
className={'rounded-r'}
/>
</div>
<div className={'min-w-8'}>{r.value}</div>
</div>
))}
</div>
);
}
function Errors() {
const rows = [
{
label: 'HTTP response status code (404 Not Found)',
value: 500,
progress: 90,
icon: <div className={'text-red text-xs'}>4XX</div>,
},
{
label: 'Cross-origin request blocked',
value: 300,
progress: 60,
icon: <div className={'text-red text-xs'}>CROS</div>,
},
{
label: 'Reference error',
value: 200,
progress: 40,
icon: <div className={'text-red text-xs'}>RE</div>,
},
{
label: 'Unhandled Promise Rejection',
value: 50,
progress: 20,
icon: <div className={'text-red text-xs'}>NULL</div>,
},
{
label: 'Failed Network Request',
value: 10,
progress: 5,
icon: <div className={'text-red text-xs'}>XHR</div>,
},
];
}
function Circle({
children,
badgeType,
}: {
children: React.ReactNode;
badgeType: 0 | 1 | 2;
}) {
const colors = {
0: '#FFFBE6',
1: '#FFF1F0',
2: '#EBF4F5',
};
return (
<div
className={'p-2 rounded-full'}
style={{ background: colors[badgeType] }}
>
{children}
</div>
);
}
export default ExampleCount

View file

@ -0,0 +1,21 @@
import React from 'react'
function ExCard({
title,
children,
}: {
title: React.ReactNode;
children: React.ReactNode;
}) {
return (
<div
className={'rounded overflow-hidden border p-4 bg-white'}
style={{ width: 400, height: 286 }}
>
<div className={'font-semibold text-lg'}>{title}</div>
<div className={'flex flex-col gap-2 mt-2'}>{children}</div>
</div>
);
}
export default ExCard

View file

@ -0,0 +1,51 @@
import { ArrowRight } from 'lucide-react';
import React from 'react';
import ExCard from './ExCard';
function ExampleFunnel() {
const steps = [
{
progress: 500,
},
{
progress: 250,
},
{
progress: 100,
},
];
return (
<ExCard title={'Funnel'}>
<>
{steps.map((step, index) => (
<div key={index}>
<div>Step {index + 1}</div>
<div className={'rounded flex items-center w-full overflow-hidden'}>
<div
style={{
backgroundColor: step.progress <= 100 ? '#394EFF' : '#E2E4F6',
width: `${(step.progress / 500) * 100}%`,
height: 30,
}}
/>
<div
style={{
width: `${((500 - step.progress) / 500) * 100}%`,
height: 30,
background: '#FFF1F0',
}}
/>
</div>
<div className={'flex items-center gap-2'}>
<ArrowRight size={14} color={'#8C8C8C'} strokeWidth={1} />
<div className={'text-disabled-text'}>{step.progress}</div>
</div>
</div>
))}
</>
</ExCard>
);
}
export default ExampleFunnel;

View file

@ -0,0 +1,55 @@
import React from 'react';
import { ResponsiveContainer, Sankey } from 'recharts';
import CustomLink from 'App/components/shared/Insights/SankeyChart/CustomLink';
import CustomNode from 'App/components/shared/Insights/SankeyChart/CustomNode';
import ExCard from './ExCard';
function ExamplePath() {
const data = {
nodes: [
{ idd: 0, name: 'Home' },
{ idd: 1, name: 'Google' },
{ idd: 2, name: 'Facebook' },
{ idd: 3, name: 'Search' },
{ idd: 4, name: 'Product' },
{ idd: 5, name: 'Chart' },
],
links: [
{ source: 0, target: 3, value: 40 },
{ source: 0, target: 4, value: 60 },
{ source: 1, target: 3, value: 100 },
{ source: 2, target: 3, value: 100 },
{ source: 3, target: 4, value: 50 },
{ source: 3, target: 5, value: 50 },
{ source: 4, target: 5, value: 15 },
],
};
return (
<ExCard title={'Path Finder'}>
<ResponsiveContainer width={'100%'} height={230}>
<Sankey
nodeWidth={6}
sort={false}
iterations={128}
node={<CustomNode isDemo />}
link={(linkProps) => <CustomLink {...linkProps} />}
data={data}
>
<defs>
<linearGradient id={'linkGradient'}>
<stop offset="0%" stopColor="rgba(57, 78, 255, 0.2)" />
<stop offset="100%" stopColor="rgba(57, 78, 255, 0.2)" />
</linearGradient>
</defs>
</Sankey>
</ResponsiveContainer>
</ExCard>
);
}
export default ExamplePath

View file

@ -0,0 +1,90 @@
import { Segmented } from 'antd';
import React from 'react';
import ExCard from './ExCard';
function ExampleTrend() {
const rows = [50, 40, 30, 20, 10];
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May'];
const [isMulti, setIsMulti] = React.useState(false);
return (
<ExCard
title={
<div className={'flex items-center gap-2'}>
<div>Trend</div>
<Segmented
options={[
{ label: 'Single-Series', value: 'single' },
{ label: 'Multi-Series', value: 'multi' },
]}
onChange={(v) => setIsMulti(v === 'multi')}
/>
</div>
}
>
<div className={'relative'}>
<div className={'flex flex-col gap-4'}>
{rows.map((r) => (
<div className={'flex items-center gap-2'}>
<div className={'text-gray-dark'}>{r}K</div>
<div className="border-t border-dotted border-gray-lighter w-full"></div>
</div>
))}
</div>
<div className={'ml-4 flex items-center justify-around w-full'}>
{months.map((m) => (
<div className={'text-gray-dark'}>{m}</div>
))}
</div>
<div style={{ position: 'absolute', top: 50, left: 50 }}>
<svg
width="310"
height="65"
viewBox="0 0 310 65"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1 55.3477L12.3778 48.184C23.7556 41.0204 46.5111 26.6931 69.2667 16.6138C92.0222 6.53442 114.778 0.703032 137.533 1.58821C160.289 2.47339 183.044 10.0751 205.8 18.5821C228.556 27.0891 251.311 36.5013 274.067 34.6878C296.822 32.8743 319.578 19.8351 342.333 12.8615C365.089 5.88789 387.844 4.97992 410.6 13.8627C433.356 22.7454 456.111 41.4189 478.867 51.0086C501.622 60.5983 524.378 61.1042 547.133 60.1189C569.889 59.1335 592.644 56.6569 615.4 51.4908C638.156 46.3247 660.911 38.4691 683.667 33.0755C706.422 27.6819 729.178 24.7502 751.933 18.4788C774.689 12.2073 797.444 2.59602 820.2 4.96937C842.956 7.34271 865.711 21.7007 888.467 24.9543C911.222 28.2078 933.978 20.357 956.733 24.9861C979.489 29.6152 1002.24 46.7243 1013.62 55.2788L1025 63.8333"
stroke="#394EFF"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</div>
{isMulti ? (
<div style={{ position: 'absolute', top: 50, left: 50 }}>
<svg
width="310"
height="66"
viewBox="0 0 310 66"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1028 60.5095L1016.64 55.3116C1005.28 50.1137 982.569 19.7179 959.854 12.4043C937.138 5.09082 914.423 0.85959 891.708 1.50187C868.992 2.14416 846.277 7.65995 823.561 13.8326C800.846 20.0052 778.131 46.8346 755.415 45.5188C732.7 44.2029 709.984 34.7417 687.269 29.6817C664.554 24.6217 641.838 23.9629 619.123 30.4082C596.407 36.8535 573.692 50.4029 550.977 57.3611C528.261 64.3193 505.546 64.6864 482.83 63.9714C460.115 63.2565 437.4 61.4595 414.684 57.711C391.969 53.9625 369.253 48.2625 346.538 44.3489C323.823 40.4353 301.107 38.3081 278.392 33.7576C255.676 29.207 232.961 22.2331 210.246 23.9552C187.53 25.6773 164.815 36.0954 142.099 38.4562C119.384 40.8169 96.6686 35.1204 73.9532 38.4793C51.2378 41.8381 16.7156 44.2524 5.35794 50.4595L-5.99987 56.6666"
stroke="#24959A"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
</div>
) : null}
</div>
<div className={'flex gap-4 justify-center'}>
<div className={'flex gap-2 items-center'}>
<div className={'w-4 h-4 rounded-full bg-main'} />
<div>CTA 1</div>
</div>
<div className={'flex gap-2 items-center'}>
<div className={'w-4 h-4 rounded-full bg-tealx'} />
<div>CTA 2</div>
</div>
</div>
</ExCard>
);
}
export default ExampleTrend;

View file

@ -0,0 +1,80 @@
import { Segmented } from 'antd';
import { Activity, BarChart, TableCellsMerge, TrendingUp } from 'lucide-react';
import React from 'react';
import { Modal } from 'UI';
import ExampleCount from "./Examples/Count";
import ExampleFunnel from './Examples/Funnel';
import ExamplePath from './Examples/Path';
import ExampleTrend from './Examples/Trend';
function NewDashboardModal(props: { onClose: () => void; open: boolean }) {
return (
<Modal onClose={props.onClose} open={props.open} size={'xlarge'}>
<Modal.Content className={'bg-[#FAFAFA]'}>
<div className={'flex flex-col gap-4'}>
<div className={'flex items-center justify-between'}>
<div className={'text-2xl leading-4 font-semibold'}>
Select your first card type to add to the dashboard
</div>
<div className={'link'} onClick={props.onClose}>
Close
</div>
</div>
<div>
<Segmented
options={[
{
label: (
<div className={'flex items-center gap-2'}>
<TrendingUp size={16} strokeWidth={1} />
<div>Product Analytics</div>
</div>
),
value: 'product-analytics',
},
{
label: (
<div className={'flex items-center gap-2'}>
<Activity size={16} strokeWidth={1} />
<div>Performance Monitoring</div>
</div>
),
value: 'performance-monitoring',
},
{
label: (
<div className={'flex items-center gap-2'}>
<BarChart size={16} strokeWidth={1} />
<div>Web Analytics</div>
</div>
),
value: 'web-analytics',
},
{
label: (
<div className={'flex items-center gap-2'}>
<TableCellsMerge size={16} strokeWidth={1} />
<div>Core Web Vitals</div>
</div>
),
value: 'core-web-vitals',
},
]}
/>
<div className={'mt-2 w-full flex flex-wrap gap-2 overflow-scroll'}>
<ExampleFunnel />
<ExamplePath />
<ExampleTrend />
<ExampleCount />
</div>
</div>
</div>
</Modal.Content>
</Modal>
);
}
export default NewDashboardModal;

View file

@ -1,13 +0,0 @@
import React from 'react';
import SessionListItem from '../SessionListItem';
function SessionList(props) {
return (
<div>
Session list
<SessionListItem session={{}} />
</div>
);
}
export default SessionList;

View file

@ -1 +0,0 @@
export { default } from './SessionList';

View file

@ -1,14 +0,0 @@
import React from 'react';
interface Props {
session: any;
}
function SessionListItem(props: Props) {
return (
<div>
Session list item
</div>
);
}
export default SessionListItem;

View file

@ -1 +0,0 @@
export { default } from './SessionListItem';

View file

@ -1,12 +0,0 @@
import React from 'react';
import SessionList from '../SessionList';
function SessionWidget(props) {
return (
<div>
<SessionList />
</div>
);
}
export default SessionWidget;

View file

@ -1 +0,0 @@
export { default } from './SessionWidget';

View file

@ -34,9 +34,11 @@ const CustomLink: React.FC<CustomLinkProps> = (props) => {
targetControlX,
linkWidth,
index,
} = props;
const isActive = activeLinks.length > 0 && activeLinks.includes(payload.id);
const isHover = hoveredLinks.length > 0 && hoveredLinks.includes(payload.id);
activeLink
} =
props;
const isActive = activeLinks?.length > 0 && activeLinks.includes(payload.id);
const isHover = hoveredLinks?.length > 0 && hoveredLinks.includes(payload.id);
const onClick = () => {
if (props.onClick) {

View file

@ -17,19 +17,40 @@ const CustomNode: React.FC<CustomNodeProps> = (props) => {
const { x, y, width, height, index, payload, containerWidth } = props;
const isOut = x + width + 6 > containerWidth;
return (
<Layer key={`CustomNode${index}`} style={{ cursor: 'pointer' }}>
<Rectangle x={x} y={y} width={width} height={height} fill='#394EFF' fillOpacity='1' />
<foreignObject
x={isOut ? x - 6 : x + width + 5}
y={y + 5}
height='25'
style={{ width: '150px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
>
<NodeButton payload={payload} />
</foreignObject>
</Layer>
);
};
return (
<Layer key={`CustomNode${index}`} style={{ cursor: 'pointer' }}>
<Rectangle x={x} y={y} width={width} height={height} fill='#394EFF' fillOpacity='1' />
{/*<foreignObject*/}
{/* x={isOut ? x - 6 : x + width + 5}*/}
{/* y={0}*/}
{/* height={48}*/}
{/* style={{ width: '150px', padding: '2px' }}*/}
{/*>*/}
{/* <NodeDropdown payload={payload} />*/}
{/*</foreignObject>*/}
{!isDemo ? (
<foreignObject
x={isOut ? x - 6 : x + width + 5}
y={y + 5}
height="25"
style={{ width: "150px", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}
>
<NodeButton payload={payload} />
</foreignObject>
) : (
<foreignObject
x={isOut ? x - 6 : x + width + 5}
y={y + 5}
height="28"
style={{ width: "70px", whiteSpace: "nowrap" }}
>
<div className={'p-1 bg-white rounded border'}>{payload.name}</div>
</foreignObject>
)}
</Layer>
);
}
export default CustomNode;

View file

@ -4,7 +4,7 @@ import cn from 'classnames';
interface Props {
children: React.ReactNode;
open?: boolean;
size ?: 'tiny' | 'small' | 'large' | 'fullscreen';
size ?: 'tiny' | 'small' | 'large' | 'fullscreen' | 'xlarge';
onClose?: () => void;
}
function Modal(props: Props) {
@ -20,14 +20,22 @@ function Modal(props: Props) {
}, [open]);
const style: any = {};
if (size === 'tiny') {
style.width = '300px';
} else if (size === 'small') {
style.width = '400px';
} else if (size === 'large') {
style.width = '700px';
} else if (size === 'fullscreen') {
style.width = '100%';
switch (size) {
case 'tiny':
style.width = '300px';
break;
case 'small':
style.width = '400px';
break;
case 'large':
style.width = '700px';
break;
case 'xlarge':
style.width = '846px';
break;
case 'fullscreen':
style.width = '100%';
break;
}
const handleClose = (e: React.MouseEvent<HTMLDivElement>) => {