BunnyCDN, Search tab, journeys redesign, and dashboard polish #52
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { ResponsiveSankey } from '@nivo/sankey'
|
import { Sankey } from '@nivo/sankey'
|
||||||
import { TreeStructure, X } from '@phosphor-icons/react'
|
import { TreeStructure, X } from '@phosphor-icons/react'
|
||||||
import type { PathTransition } from '@/lib/api/journeys'
|
import type { PathTransition } from '@/lib/api/journeys'
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ const COLUMN_COLORS = [
|
|||||||
'#EC4899', '#06B6D4', '#EF4444', '#84CC16', '#F97316', '#6366F1',
|
'#EC4899', '#06B6D4', '#EF4444', '#84CC16', '#F97316', '#6366F1',
|
||||||
]
|
]
|
||||||
|
|
||||||
const MAX_NODES_PER_STEP = 15
|
const MAX_NODES_PER_STEP = 25
|
||||||
|
|
||||||
// ─── Helpers ────────────────────────────────────────────────────────
|
// ─── Helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -257,9 +257,11 @@ export default function SankeyJourney({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const labelColor = isDark ? '#a3a3a3' : '#525252'
|
const labelColor = isDark ? '#a3a3a3' : '#525252'
|
||||||
const steps = data.nodes.map((n) => n.stepIndex)
|
|
||||||
const minStep = Math.min(...steps)
|
// Calculate dimensions: give each step ~250px of horizontal space
|
||||||
const maxStep = Math.max(...steps)
|
const numSteps = new Set(data.nodes.map((n) => n.stepIndex)).size
|
||||||
|
const chartWidth = Math.max(800, numSteps * 250)
|
||||||
|
const chartHeight = 500
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -283,73 +285,71 @@ export default function SankeyJourney({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ height: 500 }}>
|
<div className="overflow-x-auto -mx-6 px-6" style={{ maxHeight: chartHeight + 16 }}>
|
||||||
<ResponsiveSankey<SankeyNode, SankeyLink>
|
<div style={{ width: chartWidth, height: chartHeight }}>
|
||||||
data={data}
|
<Sankey<SankeyNode, SankeyLink>
|
||||||
margin={{ top: 8, right: 140, bottom: 8, left: 140 }}
|
data={data}
|
||||||
align="justify"
|
width={chartWidth}
|
||||||
sort="descending"
|
height={chartHeight}
|
||||||
colors={(node) =>
|
margin={{ top: 8, right: 160, bottom: 8, left: 160 }}
|
||||||
COLUMN_COLORS[node.stepIndex % COLUMN_COLORS.length]
|
align="justify"
|
||||||
}
|
sort="descending"
|
||||||
nodeThickness={12}
|
colors={(node) =>
|
||||||
nodeSpacing={20}
|
COLUMN_COLORS[node.stepIndex % COLUMN_COLORS.length]
|
||||||
nodeInnerPadding={0}
|
|
||||||
nodeBorderWidth={0}
|
|
||||||
nodeBorderRadius={3}
|
|
||||||
nodeOpacity={1}
|
|
||||||
nodeHoverOpacity={1}
|
|
||||||
nodeHoverOthersOpacity={0.3}
|
|
||||||
linkOpacity={0.2}
|
|
||||||
linkHoverOpacity={0.5}
|
|
||||||
linkHoverOthersOpacity={0.05}
|
|
||||||
linkContract={2}
|
|
||||||
enableLinkGradient
|
|
||||||
enableLabels
|
|
||||||
label={(node) => {
|
|
||||||
// Only show labels for first and last step columns
|
|
||||||
if (node.stepIndex === minStep || node.stepIndex === maxStep) {
|
|
||||||
return smartLabel(pathFromId(node.id))
|
|
||||||
}
|
}
|
||||||
return ''
|
nodeThickness={8}
|
||||||
}}
|
nodeSpacing={8}
|
||||||
labelPosition="outside"
|
nodeInnerPadding={0}
|
||||||
labelPadding={8}
|
nodeBorderWidth={0}
|
||||||
labelTextColor={labelColor}
|
nodeBorderRadius={2}
|
||||||
isInteractive
|
nodeOpacity={1}
|
||||||
onClick={handleClick}
|
nodeHoverOpacity={1}
|
||||||
nodeTooltip={({ node }) => (
|
nodeHoverOthersOpacity={0.3}
|
||||||
<div className="rounded-lg px-3 py-2 text-sm shadow-lg bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700">
|
linkOpacity={0.15}
|
||||||
<div className="font-medium text-neutral-900 dark:text-white">
|
linkHoverOpacity={0.5}
|
||||||
{pathFromId(node.id)}
|
linkHoverOthersOpacity={0.03}
|
||||||
|
linkContract={1}
|
||||||
|
enableLinkGradient
|
||||||
|
enableLabels
|
||||||
|
label={(node) => smartLabel(pathFromId(node.id))}
|
||||||
|
labelPosition="outside"
|
||||||
|
labelPadding={6}
|
||||||
|
labelTextColor={labelColor}
|
||||||
|
isInteractive
|
||||||
|
onClick={handleClick}
|
||||||
|
nodeTooltip={({ node }) => (
|
||||||
|
<div className="rounded-lg px-3 py-2 text-sm shadow-lg bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700">
|
||||||
|
<div className="font-medium text-neutral-900 dark:text-white">
|
||||||
|
{pathFromId(node.id)}
|
||||||
|
</div>
|
||||||
|
<div className="text-neutral-500 dark:text-neutral-400 text-xs mt-0.5">
|
||||||
|
Step {node.stepIndex + 1} ·{' '}
|
||||||
|
{node.value.toLocaleString()} sessions
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-neutral-500 dark:text-neutral-400 text-xs mt-0.5">
|
)}
|
||||||
Step {node.stepIndex + 1} ·{' '}
|
linkTooltip={({ link }) => (
|
||||||
{node.value.toLocaleString()} sessions
|
<div className="rounded-lg px-3 py-2 text-sm shadow-lg bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700">
|
||||||
|
<div className="font-medium text-neutral-900 dark:text-white">
|
||||||
|
{pathFromId(link.source.id)} →{' '}
|
||||||
|
{pathFromId(link.target.id)}
|
||||||
|
</div>
|
||||||
|
<div className="text-neutral-500 dark:text-neutral-400 text-xs mt-0.5">
|
||||||
|
{link.value.toLocaleString()} sessions
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
theme={{
|
||||||
linkTooltip={({ link }) => (
|
tooltip: {
|
||||||
<div className="rounded-lg px-3 py-2 text-sm shadow-lg bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700">
|
container: {
|
||||||
<div className="font-medium text-neutral-900 dark:text-white">
|
background: 'transparent',
|
||||||
{pathFromId(link.source.id)} →{' '}
|
boxShadow: 'none',
|
||||||
{pathFromId(link.target.id)}
|
padding: 0,
|
||||||
</div>
|
},
|
||||||
<div className="text-neutral-500 dark:text-neutral-400 text-xs mt-0.5">
|
|
||||||
{link.value.toLocaleString()} sessions
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
theme={{
|
|
||||||
tooltip: {
|
|
||||||
container: {
|
|
||||||
background: 'transparent',
|
|
||||||
boxShadow: 'none',
|
|
||||||
padding: 0,
|
|
||||||
},
|
},
|
||||||
},
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user