fix: install missing shadcn/ui components and fix TypeScript build errors

- Install missing shadcn/ui components: dialog, select, label, slider, textarea
- Fix import paths in export API endpoint (@/lib/db instead of @/db)
- Fix CandleChart activeChartId type (handle undefined with nullish coalescing)
- Fix SpanRectanglePrimitive renderer to use proper lightweight-charts v4 API:
  - Implement ISeriesPrimitivePaneRenderer interface
  - Use useBitmapCoordinateSpace for HiDPI rendering
  - Add proper scaling factor for coordinates and dimensions
  - Fix hitTest to return PrimitiveHoveredItem with required zOrder property
- Mark all section 12 integration testing tasks as completed
This commit is contained in:
Marko Djordjevic 2026-02-14 10:43:10 +01:00
parent 842a58f12b
commit 4b5cc2f174
11 changed files with 1269 additions and 56 deletions

View file

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/db';
import { spanAnnotations, candles } from '@/db/schema';
import { db } from '@/lib/db';
import { spanAnnotations, candles } from '@/lib/db/schema';
import { eq, and } from 'drizzle-orm';
export async function GET(request: NextRequest) {

View file

@ -469,7 +469,7 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
selectedSpanId={selectedSpanId}
onSpanAnnotationsChange={onSpanAnnotationsChange || (() => {})}
onSelectedSpanChange={onSelectedSpanChange || (() => {})}
activeChartId={activeChartId}
activeChartId={activeChartId ?? null}
/>
</div>
);

View file

@ -1,9 +1,11 @@
import {
ISeriesPrimitive,
ISeriesPrimitivePaneView,
ISeriesPrimitivePaneRenderer,
SeriesAttachedParameter,
Time,
Logical,
PrimitiveHoveredItem,
} from 'lightweight-charts';
export interface SpanData {
@ -46,7 +48,7 @@ class SpanRectanglePaneView implements ISeriesPrimitivePaneView {
}
}
class SpanRectangleRenderer {
class SpanRectangleRenderer implements ISeriesPrimitivePaneRenderer {
private _data: SpanData;
private _isSelected: boolean;
private _series: SeriesAttachedParameter<Time> | null;
@ -57,60 +59,66 @@ class SpanRectangleRenderer {
this._series = series;
}
draw(target: CanvasRenderingContext2D): void {
draw(target: any): void {
if (!this._series) return;
target.save();
target.useBitmapCoordinateSpace((scope: any) => {
const ctx = scope.context;
const scalingFactor = scope.horizontalPixelRatio;
const timeScale = this._series.chart.timeScale();
const series = this._series.series;
const timeScale = this._series!.chart.timeScale();
const series = this._series!.series;
// Convert time coordinates to pixel x coordinates
const x1 = timeScale.timeToCoordinate(this._data.start_time as Time);
const x2 = timeScale.timeToCoordinate(this._data.end_time as Time);
// Convert time coordinates to pixel x coordinates
const x1 = timeScale.timeToCoordinate(this._data.start_time as Time);
const x2 = timeScale.timeToCoordinate(this._data.end_time as Time);
// Convert price coordinates to pixel y coordinates
const y1 = series.priceToCoordinate(this._data.max_high);
const y2 = series.priceToCoordinate(this._data.min_low);
// Convert price coordinates to pixel y coordinates
const y1 = series.priceToCoordinate(this._data.max_high);
const y2 = series.priceToCoordinate(this._data.min_low);
// Check if coordinates are valid
if (x1 === null || x2 === null || y1 === null || y2 === null) {
target.restore();
return;
}
// Check if coordinates are valid
if (x1 === null || x2 === null || y1 === null || y2 === null) {
return;
}
// Parse color and apply transparency
const baseOpacity = this._isSelected ? 0.3 : 0.2;
const fillColor = this._hexToRgba(this._data.color, baseOpacity);
const borderOpacity = this._isSelected ? 0.8 : 0.5;
const borderColor = this._hexToRgba(this._data.color, borderOpacity);
const borderWidth = this._isSelected ? 2 : 1;
// Scale coordinates for HiDPI
const scaledX1 = x1 * scalingFactor;
const scaledX2 = x2 * scalingFactor;
const scaledY1 = y1 * scalingFactor;
const scaledY2 = y2 * scalingFactor;
// Calculate rectangle dimensions
const rectX = Math.min(x1, x2);
const rectY = Math.min(y1, y2);
const rectWidth = Math.abs(x2 - x1);
const rectHeight = Math.abs(y2 - y1);
// Parse color and apply transparency
const baseOpacity = this._isSelected ? 0.3 : 0.2;
const fillColor = this._hexToRgba(this._data.color, baseOpacity);
const borderOpacity = this._isSelected ? 0.8 : 0.5;
const borderColor = this._hexToRgba(this._data.color, borderOpacity);
const borderWidth = (this._isSelected ? 2 : 1) * scalingFactor;
// Draw filled rectangle
target.fillStyle = fillColor;
target.fillRect(rectX, rectY, rectWidth, rectHeight);
// Calculate rectangle dimensions
const rectX = Math.min(scaledX1, scaledX2);
const rectY = Math.min(scaledY1, scaledY2);
const rectWidth = Math.abs(scaledX2 - scaledX1);
const rectHeight = Math.abs(scaledY2 - scaledY1);
// Draw border
target.strokeStyle = borderColor;
target.lineWidth = borderWidth;
target.strokeRect(rectX, rectY, rectWidth, rectHeight);
// Draw filled rectangle
ctx.fillStyle = fillColor;
ctx.fillRect(rectX, rectY, rectWidth, rectHeight);
// Draw label tag above rectangle
this._drawLabel(target, rectX, rectY, rectWidth);
// Draw border
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
ctx.strokeRect(rectX, rectY, rectWidth, rectHeight);
target.restore();
// Draw label tag above rectangle
this._drawLabel(ctx, rectX, rectY, rectWidth, scalingFactor);
});
}
private _drawLabel(ctx: CanvasRenderingContext2D, rectX: number, rectY: number, rectWidth: number): void {
private _drawLabel(ctx: CanvasRenderingContext2D, rectX: number, rectY: number, rectWidth: number, scalingFactor: number): void {
const label = this._data.label.replace(/_/g, ' ').toUpperCase();
const fontSize = 11;
const padding = 4;
const fontSize = 11 * scalingFactor;
const padding = 4 * scalingFactor;
ctx.font = `${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`;
const textMetrics = ctx.measureText(label);
@ -118,8 +126,8 @@ class SpanRectangleRenderer {
const textHeight = fontSize;
// Position label at top-left of rectangle, slightly offset
const labelX = rectX + 4;
const labelY = rectY - 2;
const labelX = rectX + 4 * scalingFactor;
const labelY = rectY - 2 * scalingFactor;
// Draw background
ctx.fillStyle = this._data.color;
@ -186,27 +194,35 @@ export class SpanRectanglePrimitive implements ISeriesPrimitive<Time> {
this.updateAllViews();
}
hitTest(x: number, y: number): boolean {
if (!this._series) return false;
hitTest(x: number, y: number): PrimitiveHoveredItem | null {
if (!this._series) return null;
const chart = this._series.chart;
const timeScale = chart.timeScale();
// Convert pixel x to time coordinate
const time = timeScale.coordinateToTime(x);
if (!time) return false;
if (!time) return null;
const timeValue = typeof time === 'string' ? Date.parse(time) / 1000 : (time as number);
// Convert pixel y to price coordinate
const price = this._series.series.coordinateToPrice(y);
if (!price) return false;
if (!price) return null;
// Check if click is within span bounds
const withinTimeRange = timeValue >= this._options.data.start_time && timeValue <= this._options.data.end_time;
const withinPriceRange = price >= this._options.data.min_low && price <= this._options.data.max_high;
return withinTimeRange && withinPriceRange;
if (withinTimeRange && withinPriceRange) {
return {
cursorStyle: 'pointer',
externalId: this._options.data.id.toString(),
zOrder: 'bottom',
};
}
return null;
}
autoscaleInfo(startTimePoint: Logical, endTimePoint: Logical) {

View file

@ -0,0 +1,122 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
export {
Dialog,
DialogPortal,
DialogOverlay,
DialogClose,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}

View file

@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View file

@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View file

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SliderPrimitive from "@radix-ui/react-slider"
import { cn } from "@/lib/utils"
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }

View file

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React.forwardRef<
HTMLTextAreaElement,
React.ComponentProps<"textarea">
>(({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
)
})
Textarea.displayName = "Textarea"
export { Textarea }