Skip to content

Commit 5b1f7f5

Browse files
committed
fix(admin): track ui/data and (assets) sources dropped by .gitignore
Repo-root .gitignore had bare `data` and `assets` rules (for the server runtime dirs) that also matched admin source dirs of the same name, so 6 files (apps/admin/src/ui/data/*, the assets route template) were never committed and the admin build failed in CI. Anchor those rules to the repo root (/data, /assets).
1 parent a8be48e commit 5b1f7f5

7 files changed

Lines changed: 887 additions & 2 deletions

File tree

.gitignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ release.zip
4141

4242
run
4343

44-
data
45-
assets
44+
# anchored to repo root: these are the server's runtime dirs, not nested
45+
# source dirs of the same name (e.g. apps/admin/src/ui/data, .../assets)
46+
/data
47+
/assets
4648
.env
4749

4850
scripts/workflow/docker-compose.yml

apps/admin/src/ui/data/chart.tsx

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
import * as React from 'react'
2+
import * as RechartsPrimitive from 'recharts'
3+
import type { TooltipValueType } from 'recharts'
4+
5+
import { cn } from '~/utils/cn'
6+
7+
const THEMES = { light: '', dark: '.dark' } as const
8+
9+
const INITIAL_DIMENSION = { width: 320, height: 200 } as const
10+
type TooltipNameType = number | string
11+
12+
export type ChartConfig = Record<
13+
string,
14+
{
15+
label?: React.ReactNode
16+
icon?: React.ComponentType
17+
} & (
18+
| { color?: string; theme?: never }
19+
| { color?: never; theme: Record<keyof typeof THEMES, string> }
20+
)
21+
>
22+
23+
type ChartContextProps = {
24+
config: ChartConfig
25+
}
26+
27+
const ChartContext = React.createContext<ChartContextProps | null>(null)
28+
29+
function useChart() {
30+
const context = React.useContext(ChartContext)
31+
32+
if (!context) {
33+
throw new Error('useChart must be used within a <ChartContainer />')
34+
}
35+
36+
return context
37+
}
38+
39+
function ChartContainer({
40+
id,
41+
className,
42+
children,
43+
config,
44+
initialDimension = INITIAL_DIMENSION,
45+
...props
46+
}: React.ComponentProps<'div'> & {
47+
config: ChartConfig
48+
children: React.ComponentProps<
49+
typeof RechartsPrimitive.ResponsiveContainer
50+
>['children']
51+
initialDimension?: {
52+
width: number
53+
height: number
54+
}
55+
}) {
56+
const uniqueId = React.useId()
57+
const chartId = `chart-${id ?? uniqueId.replace(/:/g, '')}`
58+
59+
return (
60+
<ChartContext.Provider value={{ config }}>
61+
<div
62+
data-slot="chart"
63+
data-chart={chartId}
64+
className={cn(
65+
"[&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-neutral-500 dark:[&_.recharts-cartesian-axis-tick_text]:fill-neutral-400 [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-neutral-200/60 dark:[&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-neutral-800/60 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-neutral-200 dark:[&_.recharts-curve.recharts-tooltip-cursor]:stroke-neutral-800 [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-neutral-200 dark:[&_.recharts-polar-grid_[stroke='#ccc']]:stroke-neutral-800 [&_.recharts-radial-bar-background-sector]:fill-neutral-100 dark:[&_.recharts-radial-bar-background-sector]:fill-neutral-800 [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-neutral-100 dark:[&_.recharts-rectangle.recharts-tooltip-cursor]:fill-neutral-800 [&_.recharts-reference-line_[stroke='#ccc']]:stroke-neutral-200 dark:[&_.recharts-reference-line_[stroke='#ccc']]:stroke-neutral-800 [&_.recharts-sector[stroke='#fff']]:stroke-transparent",
66+
className,
67+
)}
68+
{...props}
69+
>
70+
<ChartStyle id={chartId} config={config} />
71+
<RechartsPrimitive.ResponsiveContainer
72+
initialDimension={initialDimension}
73+
>
74+
{children}
75+
</RechartsPrimitive.ResponsiveContainer>
76+
</div>
77+
</ChartContext.Provider>
78+
)
79+
}
80+
81+
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
82+
const colorConfig = Object.entries(config).filter(
83+
([, config]) => config.theme ?? config.color,
84+
)
85+
86+
if (!colorConfig.length) {
87+
return null
88+
}
89+
90+
return (
91+
<style
92+
dangerouslySetInnerHTML={{
93+
__html: Object.entries(THEMES)
94+
.map(
95+
([theme, prefix]) => `
96+
${prefix} [data-chart=${id}] {
97+
${colorConfig
98+
.map(([key, itemConfig]) => {
99+
const color =
100+
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ??
101+
itemConfig.color
102+
return color ? ` --color-${key}: ${color};` : null
103+
})
104+
.join('\n')}
105+
}
106+
`,
107+
)
108+
.join('\n'),
109+
}}
110+
/>
111+
)
112+
}
113+
114+
const ChartTooltip = RechartsPrimitive.Tooltip
115+
116+
function ChartTooltipContent({
117+
active,
118+
payload,
119+
className,
120+
indicator = 'dot',
121+
hideLabel = false,
122+
hideIndicator = false,
123+
label,
124+
labelFormatter,
125+
labelClassName,
126+
formatter,
127+
color,
128+
nameKey,
129+
labelKey,
130+
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
131+
React.ComponentProps<'div'> & {
132+
hideLabel?: boolean
133+
hideIndicator?: boolean
134+
indicator?: 'line' | 'dot' | 'dashed'
135+
nameKey?: string
136+
labelKey?: string
137+
} & Omit<
138+
RechartsPrimitive.DefaultTooltipContentProps<
139+
TooltipValueType,
140+
TooltipNameType
141+
>,
142+
'accessibilityLayer'
143+
>) {
144+
const { config } = useChart()
145+
146+
const tooltipLabel = React.useMemo(() => {
147+
if (hideLabel || !payload?.length) {
148+
return null
149+
}
150+
151+
const [item] = payload
152+
const key = `${labelKey ?? item?.dataKey ?? item?.name ?? 'value'}`
153+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
154+
const value =
155+
!labelKey && typeof label === 'string'
156+
? (config[label]?.label ?? label)
157+
: itemConfig?.label
158+
159+
if (labelFormatter) {
160+
return (
161+
<div className={cn('font-medium', labelClassName)}>
162+
{labelFormatter(value, payload)}
163+
</div>
164+
)
165+
}
166+
167+
if (!value) {
168+
return null
169+
}
170+
171+
return <div className={cn('font-medium', labelClassName)}>{value}</div>
172+
}, [
173+
label,
174+
labelFormatter,
175+
payload,
176+
hideLabel,
177+
labelClassName,
178+
config,
179+
labelKey,
180+
])
181+
182+
if (!active || !payload?.length) {
183+
return null
184+
}
185+
186+
const nestLabel = payload.length === 1 && indicator !== 'dot'
187+
188+
return (
189+
<div
190+
className={cn(
191+
'grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-neutral-200/60 bg-white px-2.5 py-1.5 text-xs shadow-xl dark:border-neutral-800/60 dark:bg-neutral-950',
192+
className,
193+
)}
194+
>
195+
{!nestLabel ? tooltipLabel : null}
196+
<div className="grid gap-1.5">
197+
{payload
198+
.filter((item) => item.type !== 'none')
199+
.map((item, index) => {
200+
const key = `${nameKey ?? item.name ?? item.dataKey ?? 'value'}`
201+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
202+
const indicatorColor = color ?? item.payload?.fill ?? item.color
203+
204+
return (
205+
<div
206+
key={index}
207+
className={cn(
208+
'flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-neutral-500 dark:[&>svg]:text-neutral-400',
209+
indicator === 'dot' && 'items-center',
210+
)}
211+
>
212+
{formatter && item?.value !== undefined && item.name ? (
213+
formatter(item.value, item.name, item, index, item.payload)
214+
) : (
215+
<>
216+
{itemConfig?.icon ? (
217+
<itemConfig.icon />
218+
) : (
219+
!hideIndicator && (
220+
<div
221+
className={cn(
222+
'border-(--color-border) bg-(--color-bg) shrink-0 rounded-[2px]',
223+
indicator === 'dot' && 'h-2.5 w-2.5',
224+
indicator === 'line' && 'w-1',
225+
indicator === 'dashed' &&
226+
'w-0 border-[1.5px] border-dashed bg-transparent',
227+
nestLabel && indicator === 'dashed' && 'my-0.5',
228+
)}
229+
style={
230+
{
231+
'--color-bg': indicatorColor,
232+
'--color-border': indicatorColor,
233+
} as React.CSSProperties
234+
}
235+
/>
236+
)
237+
)}
238+
<div
239+
className={cn(
240+
'flex flex-1 justify-between leading-none',
241+
nestLabel ? 'items-end' : 'items-center',
242+
)}
243+
>
244+
<div className="grid gap-1.5">
245+
{nestLabel ? tooltipLabel : null}
246+
<span className="text-neutral-500 dark:text-neutral-400">
247+
{itemConfig?.label ?? item.name}
248+
</span>
249+
</div>
250+
{item.value != null && (
251+
<span className="font-mono font-medium tabular-nums text-neutral-900 dark:text-neutral-100">
252+
{typeof item.value === 'number'
253+
? item.value.toLocaleString()
254+
: String(item.value)}
255+
</span>
256+
)}
257+
</div>
258+
</>
259+
)}
260+
</div>
261+
)
262+
})}
263+
</div>
264+
</div>
265+
)
266+
}
267+
268+
const ChartLegend = RechartsPrimitive.Legend
269+
270+
function ChartLegendContent({
271+
className,
272+
hideIcon = false,
273+
payload,
274+
verticalAlign = 'bottom',
275+
nameKey,
276+
}: React.ComponentProps<'div'> & {
277+
hideIcon?: boolean
278+
nameKey?: string
279+
} & RechartsPrimitive.DefaultLegendContentProps) {
280+
const { config } = useChart()
281+
282+
if (!payload?.length) {
283+
return null
284+
}
285+
286+
return (
287+
<div
288+
className={cn(
289+
'flex items-center justify-center gap-4 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-neutral-500 dark:[&>svg]:text-neutral-400',
290+
verticalAlign === 'top' ? 'pb-3' : 'pt-3',
291+
className,
292+
)}
293+
>
294+
{payload
295+
.filter((item) => item.type !== 'none')
296+
.map((item, index) => {
297+
const key = `${nameKey ?? item.dataKey ?? 'value'}`
298+
const itemConfig = getPayloadConfigFromPayload(config, item, key)
299+
300+
return (
301+
<div key={index} className="flex items-center gap-1.5">
302+
{itemConfig?.icon && !hideIcon ? (
303+
<itemConfig.icon />
304+
) : (
305+
<div
306+
className="h-2 w-2 shrink-0 rounded-[2px]"
307+
style={{
308+
backgroundColor: item.color,
309+
}}
310+
/>
311+
)}
312+
{itemConfig?.label}
313+
</div>
314+
)
315+
})}
316+
</div>
317+
)
318+
}
319+
320+
function getPayloadConfigFromPayload(
321+
config: ChartConfig,
322+
payload: unknown,
323+
key: string,
324+
) {
325+
if (typeof payload !== 'object' || payload === null) {
326+
return undefined
327+
}
328+
329+
const payloadPayload =
330+
'payload' in payload &&
331+
typeof payload.payload === 'object' &&
332+
payload.payload !== null
333+
? payload.payload
334+
: undefined
335+
336+
let configLabelKey: string = key
337+
338+
if (
339+
key in payload &&
340+
typeof payload[key as keyof typeof payload] === 'string'
341+
) {
342+
configLabelKey = payload[key as keyof typeof payload] as string
343+
} else if (
344+
payloadPayload &&
345+
key in payloadPayload &&
346+
typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'
347+
) {
348+
configLabelKey = payloadPayload[
349+
key as keyof typeof payloadPayload
350+
] as string
351+
}
352+
353+
return configLabelKey in config ? config[configLabelKey] : config[key]
354+
}
355+
356+
export {
357+
ChartContainer,
358+
ChartLegend,
359+
ChartLegendContent,
360+
ChartStyle,
361+
ChartTooltip,
362+
ChartTooltipContent,
363+
}

0 commit comments

Comments
 (0)