fix timepicker

This commit is contained in:
Carl-Gerhard Lindesvärd
2026-03-09 13:48:02 +01:00
parent c9cf7901ad
commit 0f9e5f6e93
9 changed files with 174 additions and 189 deletions

View File

@@ -1,24 +1,27 @@
import { pushModal } from '@/modals';
import type { import type {
IReport,
IChartRange, IChartRange,
IChartType, IChartType,
IInterval, IInterval,
IReport,
} from '@openpanel/validation'; } from '@openpanel/validation';
import { SaveIcon } from 'lucide-react'; import { SaveIcon } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
import { ReportChart } from '../report-chart';
import { ReportChartType } from '../report/ReportChartType'; import { ReportChartType } from '../report/ReportChartType';
import { ReportInterval } from '../report/ReportInterval'; import { ReportInterval } from '../report/ReportInterval';
import { ReportChart } from '../report-chart';
import { TimeWindowPicker } from '../time-window-picker'; import { TimeWindowPicker } from '../time-window-picker';
import { Button } from '../ui/button'; import { Button } from '../ui/button';
import { pushModal } from '@/modals';
export function ChatReport({ export function ChatReport({
lazy, lazy,
...props ...props
}: { report: IReport & { startDate: string; endDate: string }; lazy: boolean }) { }: {
report: IReport & { startDate: string; endDate: string };
lazy: boolean;
}) {
const [chartType, setChartType] = useState<IChartType>( const [chartType, setChartType] = useState<IChartType>(
props.report.chartType, props.report.chartType
); );
const [startDate, setStartDate] = useState<string>(props.report.startDate); const [startDate, setStartDate] = useState<string>(props.report.startDate);
const [endDate, setEndDate] = useState<string>(props.report.endDate); const [endDate, setEndDate] = useState<string>(props.report.endDate);
@@ -35,47 +38,48 @@ export function ChatReport({
}; };
return ( return (
<div className="card"> <div className="card">
<div className="text-center text-sm font-mono font-medium pt-4"> <div className="pt-4 text-center font-medium font-mono text-sm">
{props.report.name} {props.report.name}
</div> </div>
<div className="p-4"> <div className="p-4">
<ReportChart lazy={lazy} report={report} /> <ReportChart lazy={lazy} report={report} />
</div> </div>
<div className="row justify-between gap-1 border-t border-border p-2"> <div className="row justify-between gap-1 border-border border-t p-2">
<div className="col md:row gap-1"> <div className="col md:row gap-1">
<TimeWindowPicker <TimeWindowPicker
className="min-w-0" className="min-w-0"
onChange={setRange}
value={report.range}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
endDate={report.endDate} endDate={report.endDate}
onChange={setRange}
onEndDateChange={setEndDate}
onIntervalChange={setInterval}
onStartDateChange={setStartDate}
startDate={report.startDate} startDate={report.startDate}
value={report.range}
/> />
<ReportInterval <ReportInterval
chartType={chartType}
className="min-w-0" className="min-w-0"
interval={interval} interval={interval}
range={range}
chartType={chartType}
onChange={setInterval} onChange={setInterval}
range={range}
/> />
<ReportChartType <ReportChartType
value={chartType}
onChange={(type) => { onChange={(type) => {
setChartType(type); setChartType(type);
}} }}
value={chartType}
/> />
</div> </div>
<Button <Button
icon={SaveIcon} icon={SaveIcon}
variant="outline"
size="sm"
onClick={() => { onClick={() => {
pushModal('SaveReport', { pushModal('SaveReport', {
report, report,
disableRedirect: true, disableRedirect: true,
}); });
}} }}
size="sm"
variant="outline"
> >
Save report Save report
</Button> </Button>

View File

@@ -2,17 +2,25 @@ import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
import { TimeWindowPicker } from '@/components/time-window-picker'; import { TimeWindowPicker } from '@/components/time-window-picker';
export function OverviewRange() { export function OverviewRange() {
const { range, setRange, setStartDate, setEndDate, endDate, startDate } = const {
useOverviewOptions(); range,
setRange,
setStartDate,
setEndDate,
endDate,
startDate,
setInterval,
} = useOverviewOptions();
return ( return (
<TimeWindowPicker <TimeWindowPicker
onChange={setRange}
value={range}
onStartDateChange={setStartDate}
onEndDateChange={setEndDate}
endDate={endDate} endDate={endDate}
onChange={setRange}
onEndDateChange={setEndDate}
onIntervalChange={setInterval}
onStartDateChange={setStartDate}
startDate={startDate} startDate={startDate}
value={range}
/> />
); );
} }

View File

@@ -1,4 +1,7 @@
import { ReportChart } from '@/components/report-chart'; import type { IServiceReport } from '@openpanel/db';
import { GanttChartSquareIcon, ShareIcon } from 'lucide-react';
import { useEffect } from 'react';
import EditReportName from '../report/edit-report-name';
import { ReportChartType } from '@/components/report/ReportChartType'; import { ReportChartType } from '@/components/report/ReportChartType';
import { ReportInterval } from '@/components/report/ReportInterval'; import { ReportInterval } from '@/components/report/ReportInterval';
import { ReportLineType } from '@/components/report/ReportLineType'; import { ReportLineType } from '@/components/report/ReportLineType';
@@ -14,18 +17,13 @@ import {
setReport, setReport,
} from '@/components/report/reportSlice'; } from '@/components/report/reportSlice';
import { ReportSidebar } from '@/components/report/sidebar/ReportSidebar'; import { ReportSidebar } from '@/components/report/sidebar/ReportSidebar';
import { ReportChart } from '@/components/report-chart';
import { TimeWindowPicker } from '@/components/time-window-picker'; import { TimeWindowPicker } from '@/components/time-window-picker';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import { useAppParams } from '@/hooks/use-app-params'; import { useAppParams } from '@/hooks/use-app-params';
import { pushModal } from '@/modals'; import { pushModal } from '@/modals';
import { useDispatch, useSelector } from '@/redux'; import { useDispatch, useSelector } from '@/redux';
import { bind } from 'bind-event-listener';
import { GanttChartSquareIcon, ShareIcon } from 'lucide-react';
import { useEffect } from 'react';
import type { IServiceReport } from '@openpanel/db';
import EditReportName from '../report/edit-report-name';
interface ReportEditorProps { interface ReportEditorProps {
report: IServiceReport | null; report: IServiceReport | null;
@@ -54,15 +52,15 @@ export default function ReportEditor({
return ( return (
<Sheet> <Sheet>
<div> <div>
<div className="p-4 flex items-center justify-between"> <div className="flex items-center justify-between p-4">
<EditReportName /> <EditReportName />
{initialReport?.id && ( {initialReport?.id && (
<Button <Button
variant="outline"
icon={ShareIcon} icon={ShareIcon}
onClick={() => onClick={() =>
pushModal('ShareReportModal', { reportId: initialReport.id }) pushModal('ShareReportModal', { reportId: initialReport.id })
} }
variant="outline"
> >
Share Share
</Button> </Button>
@@ -71,9 +69,9 @@ export default function ReportEditor({
<div className="grid grid-cols-2 gap-2 p-4 pt-0 md:grid-cols-6"> <div className="grid grid-cols-2 gap-2 p-4 pt-0 md:grid-cols-6">
<SheetTrigger asChild> <SheetTrigger asChild>
<Button <Button
className="self-start"
icon={GanttChartSquareIcon} icon={GanttChartSquareIcon}
variant="cta" variant="cta"
className="self-start"
> >
Pick events Pick events
</Button> </Button>
@@ -88,23 +86,26 @@ export default function ReportEditor({
/> />
<TimeWindowPicker <TimeWindowPicker
className="min-w-0 flex-1" className="min-w-0 flex-1"
endDate={report.endDate}
onChange={(value) => { onChange={(value) => {
dispatch(changeDateRanges(value)); dispatch(changeDateRanges(value));
}} }}
value={report.range}
onStartDateChange={(date) => dispatch(changeStartDate(date))}
onEndDateChange={(date) => dispatch(changeEndDate(date))} onEndDateChange={(date) => dispatch(changeEndDate(date))}
endDate={report.endDate} onIntervalChange={(interval) =>
dispatch(changeInterval(interval))
}
onStartDateChange={(date) => dispatch(changeStartDate(date))}
startDate={report.startDate} startDate={report.startDate}
value={report.range}
/> />
<ReportInterval <ReportInterval
chartType={report.chartType}
className="min-w-0 flex-1" className="min-w-0 flex-1"
endDate={report.endDate}
interval={report.interval} interval={report.interval}
onChange={(newInterval) => dispatch(changeInterval(newInterval))} onChange={(newInterval) => dispatch(changeInterval(newInterval))}
range={report.range} range={report.range}
chartType={report.chartType}
startDate={report.startDate} startDate={report.startDate}
endDate={report.endDate}
/> />
<ReportLineType className="min-w-0 flex-1" /> <ReportLineType className="min-w-0 flex-1" />
</div> </div>
@@ -114,7 +115,7 @@ export default function ReportEditor({
</div> </div>
<div className="flex flex-col gap-4 p-4" id="report-editor"> <div className="flex flex-col gap-4 p-4" id="report-editor">
{report.ready && ( {report.ready && (
<ReportChart report={{ ...report, projectId }} isEditMode /> <ReportChart isEditMode report={{ ...report, projectId }} />
)} )}
</div> </div>
</div> </div>

View File

@@ -1,3 +1,9 @@
import { timeWindows } from '@openpanel/constants';
import type { IChartRange, IInterval } from '@openpanel/validation';
import { bind } from 'bind-event-listener';
import { endOfDay, format, startOfDay } from 'date-fns';
import { CalendarIcon } from 'lucide-react';
import { useCallback, useEffect, useRef } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
DropdownMenu, DropdownMenu,
@@ -11,24 +17,18 @@ import {
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { pushModal, useOnPushModal } from '@/modals'; import { pushModal, useOnPushModal } from '@/modals';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { bind } from 'bind-event-listener';
import { CalendarIcon } from 'lucide-react';
import { useCallback, useEffect, useRef } from 'react';
import { shouldIgnoreKeypress } from '@/utils/should-ignore-keypress'; import { shouldIgnoreKeypress } from '@/utils/should-ignore-keypress';
import { timeWindows } from '@openpanel/constants';
import type { IChartRange } from '@openpanel/validation';
import { endOfDay, format, startOfDay } from 'date-fns';
type Props = { interface Props {
value: IChartRange; value: IChartRange;
onChange: (value: IChartRange) => void; onChange: (value: IChartRange) => void;
onStartDateChange: (date: string) => void; onStartDateChange: (date: string) => void;
onEndDateChange: (date: string) => void; onEndDateChange: (date: string) => void;
onIntervalChange: (interval: IInterval) => void;
endDate: string | null; endDate: string | null;
startDate: string | null; startDate: string | null;
className?: string; className?: string;
}; }
export function TimeWindowPicker({ export function TimeWindowPicker({
value, value,
onChange, onChange,
@@ -36,6 +36,7 @@ export function TimeWindowPicker({
onStartDateChange, onStartDateChange,
endDate, endDate,
onEndDateChange, onEndDateChange,
onIntervalChange,
className, className,
}: Props) { }: Props) {
const isDateRangerPickerOpen = useRef(false); const isDateRangerPickerOpen = useRef(false);
@@ -46,10 +47,11 @@ export function TimeWindowPicker({
const handleCustom = useCallback(() => { const handleCustom = useCallback(() => {
pushModal('DateRangerPicker', { pushModal('DateRangerPicker', {
onChange: ({ startDate, endDate }) => { onChange: ({ startDate, endDate, interval }) => {
onStartDateChange(format(startOfDay(startDate), 'yyyy-MM-dd HH:mm:ss')); onStartDateChange(format(startOfDay(startDate), 'yyyy-MM-dd HH:mm:ss'));
onEndDateChange(format(endOfDay(endDate), 'yyyy-MM-dd HH:mm:ss')); onEndDateChange(format(endOfDay(endDate), 'yyyy-MM-dd HH:mm:ss'));
onChange('custom'); onChange('custom');
onIntervalChange(interval);
}, },
startDate: startDate ? new Date(startDate) : undefined, startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined, endDate: endDate ? new Date(endDate) : undefined,
@@ -69,7 +71,7 @@ export function TimeWindowPicker({
} }
const match = Object.values(timeWindows).find( const match = Object.values(timeWindows).find(
(tw) => event.key === tw.shortcut.toLowerCase(), (tw) => event.key === tw.shortcut.toLowerCase()
); );
if (match?.key === 'custom') { if (match?.key === 'custom') {
handleCustom(); handleCustom();
@@ -84,9 +86,9 @@ export function TimeWindowPicker({
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="outline"
icon={CalendarIcon}
className={cn('justify-start', className)} className={cn('justify-start', className)}
icon={CalendarIcon}
variant="outline"
> >
{timeWindow?.label} {timeWindow?.label}
</Button> </Button>

View File

@@ -9,7 +9,6 @@ import {
DayPicker, DayPicker,
getDefaultClassNames, getDefaultClassNames,
} from 'react-day-picker'; } from 'react-day-picker';
import { Button, buttonVariants } from '@/components/ui/button'; import { Button, buttonVariants } from '@/components/ui/button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -29,99 +28,93 @@ function Calendar({
return ( return (
<DayPicker <DayPicker
showOutsideDays={showOutsideDays} captionLayout={captionLayout}
className={cn( className={cn(
'bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent', 'group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent',
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`, String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className, className
)} )}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString('default', { month: 'short' }),
...formatters,
}}
classNames={{ classNames={{
root: cn('w-fit', defaultClassNames.root), root: cn('w-fit', defaultClassNames.root),
months: cn( months: cn(
'flex gap-4 flex-col sm:flex-row relative', 'relative flex flex-col gap-4 sm:flex-row',
defaultClassNames.months, defaultClassNames.months
), ),
month: cn('flex flex-col w-full gap-4', defaultClassNames.month), month: cn('flex w-full flex-col gap-4', defaultClassNames.month),
nav: cn( nav: cn(
'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between', 'absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1',
defaultClassNames.nav, defaultClassNames.nav
), ),
button_previous: cn( button_previous: cn(
buttonVariants({ variant: buttonVariant }), buttonVariants({ variant: buttonVariant }),
'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none', 'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',
defaultClassNames.button_previous, defaultClassNames.button_previous
), ),
button_next: cn( button_next: cn(
buttonVariants({ variant: buttonVariant }), buttonVariants({ variant: buttonVariant }),
'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none', 'size-(--cell-size) select-none p-0 aria-disabled:opacity-50',
defaultClassNames.button_next, defaultClassNames.button_next
), ),
month_caption: cn( month_caption: cn(
'flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)', 'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)',
defaultClassNames.month_caption, defaultClassNames.month_caption
), ),
dropdowns: cn( dropdowns: cn(
'w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5', 'flex h-(--cell-size) w-full items-center justify-center gap-1.5 font-medium text-sm',
defaultClassNames.dropdowns, defaultClassNames.dropdowns
), ),
dropdown_root: cn( dropdown_root: cn(
'relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md', 'relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50',
defaultClassNames.dropdown_root, defaultClassNames.dropdown_root
), ),
dropdown: cn( dropdown: cn(
'absolute bg-popover inset-0 opacity-0', 'absolute inset-0 bg-popover opacity-0',
defaultClassNames.dropdown, defaultClassNames.dropdown
), ),
caption_label: cn( caption_label: cn(
'select-none font-medium', 'select-none font-medium',
captionLayout === 'label' captionLayout === 'label'
? 'text-sm' ? 'text-sm'
: 'rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5', : 'flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
defaultClassNames.caption_label, defaultClassNames.caption_label
), ),
table: 'w-full border-collapse', table: 'w-full border-collapse',
weekdays: cn('flex', defaultClassNames.weekdays), weekdays: cn('flex', defaultClassNames.weekdays),
weekday: cn( weekday: cn(
'text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none', 'flex-1 select-none rounded-md font-normal text-[0.8rem] text-muted-foreground',
defaultClassNames.weekday, defaultClassNames.weekday
), ),
week: cn('flex w-full mt-2', defaultClassNames.week), week: cn('mt-2 flex w-full', defaultClassNames.week),
week_number_header: cn( week_number_header: cn(
'select-none w-(--cell-size)', 'w-(--cell-size) select-none',
defaultClassNames.week_number_header, defaultClassNames.week_number_header
), ),
week_number: cn( week_number: cn(
'text-[0.8rem] select-none text-muted-foreground', 'select-none text-[0.8rem] text-muted-foreground',
defaultClassNames.week_number, defaultClassNames.week_number
), ),
day: cn( day: cn(
'relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none', 'group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md',
defaultClassNames.day, defaultClassNames.day
), ),
range_start: cn( range_start: cn(
'rounded-l-md bg-accent', 'rounded-l-md bg-accent',
defaultClassNames.range_start, defaultClassNames.range_start
), ),
range_middle: cn('rounded-none', defaultClassNames.range_middle), range_middle: cn('rounded-none', defaultClassNames.range_middle),
range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end), range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end),
today: cn( today: cn(
'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none', 'rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none',
defaultClassNames.today, defaultClassNames.today
), ),
outside: cn( outside: cn(
'text-muted-foreground aria-selected:text-muted-foreground', 'text-muted-foreground aria-selected:text-muted-foreground',
defaultClassNames.outside, defaultClassNames.outside
), ),
disabled: cn( disabled: cn(
'text-muted-foreground opacity-50', 'text-muted-foreground opacity-50',
defaultClassNames.disabled, defaultClassNames.disabled
), ),
hidden: cn('invisible', defaultClassNames.hidden), hidden: cn('invisible', defaultClassNames.hidden),
...classNames, ...classNames,
@@ -130,9 +123,9 @@ function Calendar({
Root: ({ className, rootRef, ...props }) => { Root: ({ className, rootRef, ...props }) => {
return ( return (
<div <div
className={cn(className)}
data-slot="calendar" data-slot="calendar"
ref={rootRef} ref={rootRef}
className={cn(className)}
{...props} {...props}
/> />
); );
@@ -169,6 +162,12 @@ function Calendar({
}, },
...components, ...components,
}} }}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString('default', { month: 'short' }),
...formatters,
}}
showOutsideDays={showOutsideDays}
{...props} {...props}
/> />
); );
@@ -184,29 +183,31 @@ function CalendarDayButton({
const ref = React.useRef<HTMLButtonElement>(null); const ref = React.useRef<HTMLButtonElement>(null);
React.useEffect(() => { React.useEffect(() => {
if (modifiers.focused) ref.current?.focus(); if (modifiers.focused) {
ref.current?.focus();
}
}, [modifiers.focused]); }, [modifiers.focused]);
return ( return (
<Button <Button
ref={ref} className={cn(
variant="ghost" 'flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-start=true]:rounded-l-md data-[range-end=true]:bg-primary data-[range-middle=true]:bg-accent data-[range-start=true]:bg-primary data-[selected-single=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:text-accent-foreground data-[range-start=true]:text-primary-foreground data-[selected-single=true]:text-primary-foreground group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70',
size="icon" defaultClassNames.day,
className
)}
data-day={day.date.toLocaleDateString()} data-day={day.date.toLocaleDateString()}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
data-range-start={modifiers.range_start}
data-selected-single={ data-selected-single={
modifiers.selected && modifiers.selected &&
!modifiers.range_start && !modifiers.range_start &&
!modifiers.range_end && !modifiers.range_end &&
!modifiers.range_middle !modifiers.range_middle
} }
data-range-start={modifiers.range_start} ref={ref}
data-range-end={modifiers.range_end} size="icon"
data-range-middle={modifiers.range_middle} variant="ghost"
className={cn(
'data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70',
defaultClassNames.day,
className,
)}
{...props} {...props}
/> />
); );

View File

@@ -1,20 +1,24 @@
import { getDefaultIntervalByDates } from '@openpanel/constants';
import type { IInterval } from '@openpanel/validation';
import { endOfDay, subMonths } from 'date-fns';
import { CheckIcon, XIcon } from 'lucide-react';
import { useState } from 'react';
import { popModal } from '.';
import { ModalContent } from './Modal/Container';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar'; import { Calendar } from '@/components/ui/calendar';
import { useBreakpoint } from '@/hooks/use-breakpoint'; import { useBreakpoint } from '@/hooks/use-breakpoint';
import { subMonths } from 'date-fns';
import { useState } from 'react';
import { Input } from '@/components/ui/input';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { CheckIcon, XIcon } from 'lucide-react';
import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container';
type Props = { interface Props {
onChange: (payload: { startDate: Date; endDate: Date }) => void; onChange: (payload: {
startDate: Date;
endDate: Date;
interval: IInterval;
}) => void;
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
}; }
export default function DateRangerPicker({ export default function DateRangerPicker({
onChange, onChange,
startDate: initialStartDate, startDate: initialStartDate,
@@ -25,20 +29,20 @@ export default function DateRangerPicker({
const [endDate, setEndDate] = useState(initialEndDate); const [endDate, setEndDate] = useState(initialEndDate);
return ( return (
<ModalContent className="p-4 md:p-8 min-w-fit"> <ModalContent className="min-w-fit p-4 md:p-8">
<Calendar <Calendar
captionLayout="dropdown" captionLayout="dropdown"
initialFocus className="mx-auto min-h-[310px] p-0 [&_table]:mx-auto [&_table]:w-auto"
mode="range"
defaultMonth={subMonths( defaultMonth={subMonths(
startDate ? new Date(startDate) : new Date(), startDate ? new Date(startDate) : new Date(),
isBelowSm ? 0 : 1, isBelowSm ? 0 : 1
)} )}
selected={{ hidden={{
from: startDate, after: endOfDay(new Date()),
to: endDate,
}} }}
toDate={new Date()} initialFocus
mode="range"
numberOfMonths={isBelowSm ? 1 : 2}
onSelect={(range) => { onSelect={(range) => {
if (range?.from) { if (range?.from) {
setStartDate(range.from); setStartDate(range.from);
@@ -47,33 +51,39 @@ export default function DateRangerPicker({
setEndDate(range.to); setEndDate(range.to);
} }
}} }}
numberOfMonths={isBelowSm ? 1 : 2} selected={{
className="mx-auto min-h-[310px] [&_table]:mx-auto [&_table]:w-auto p-0" from: startDate,
to: endDate,
}}
/> />
<div className="col flex-col-reverse md:row gap-2"> <div className="col md:row flex-col-reverse gap-2">
<Button <Button
icon={XIcon}
onClick={() => popModal()}
type="button" type="button"
variant="outline" variant="outline"
onClick={() => popModal()}
icon={XIcon}
> >
Cancel Cancel
</Button> </Button>
{startDate && endDate && ( {startDate && endDate && (
<Button <Button
type="button"
className="md:ml-auto" className="md:ml-auto"
icon={startDate && endDate ? CheckIcon : XIcon}
onClick={() => { onClick={() => {
popModal(); popModal();
if (startDate && endDate) { if (startDate && endDate) {
onChange({ onChange({
startDate: startDate, startDate,
endDate: endDate, endDate,
interval: getDefaultIntervalByDates(
startDate.toISOString(),
endDate.toISOString()
)!,
}); });
} }
}} }}
icon={startDate && endDate ? CheckIcon : XIcon} type="button"
> >
{startDate && endDate {startDate && endDate
? `Select ${formatDate(startDate)} - ${formatDate(endDate)}` ? `Select ${formatDate(startDate)} - ${formatDate(endDate)}`

View File

@@ -27,7 +27,7 @@ type GscChartData = { date: string; clicks: number; impressions: number };
const { TooltipProvider, Tooltip: GscTooltip } = createChartTooltip< const { TooltipProvider, Tooltip: GscTooltip } = createChartTooltip<
GscChartData, GscChartData,
Record<string, never> Record<string, unknown>
>(({ data }) => { >(({ data }) => {
const item = data[0]; const item = data[0];
if (!item) { if (!item) {
@@ -267,7 +267,7 @@ function GscViewsChart({
const yAxisProps = useYAxisProps(); const yAxisProps = useYAxisProps();
return ( return (
<TooltipProvider data={[]}> <TooltipProvider>
<ResponsiveContainer height={160} width="100%"> <ResponsiveContainer height={160} width="100%">
<ComposedChart data={data}> <ComposedChart data={data}>
<defs> <defs>
@@ -328,7 +328,7 @@ function GscTimeseriesChart({
const yAxisProps = useYAxisProps(); const yAxisProps = useYAxisProps();
return ( return (
<TooltipProvider data={data}> <TooltipProvider>
<ResponsiveContainer height={160} width="100%"> <ResponsiveContainer height={160} width="100%">
<ComposedChart data={data}> <ComposedChart data={data}>
<defs> <defs>

View File

@@ -1,13 +1,6 @@
import {
getDefaultIntervalByRange,
intervals,
timeWindows,
} from '@openpanel/constants';
import type { IChartRange, IInterval } from '@openpanel/validation';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { createFileRoute, useNavigate } from '@tanstack/react-router';
import { SearchIcon } from 'lucide-react'; import { SearchIcon } from 'lucide-react';
import { parseAsString, parseAsStringEnum, useQueryState } from 'nuqs';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { import {
CartesianGrid, CartesianGrid,
@@ -23,8 +16,11 @@ import {
createChartTooltip, createChartTooltip,
} from '@/components/charts/chart-tooltip'; } from '@/components/charts/chart-tooltip';
import { FullPageEmptyState } from '@/components/full-page-empty-state'; import { FullPageEmptyState } from '@/components/full-page-empty-state';
import { OverviewInterval } from '@/components/overview/overview-interval';
import { OverviewMetricCard } from '@/components/overview/overview-metric-card'; import { OverviewMetricCard } from '@/components/overview/overview-metric-card';
import { OverviewRange } from '@/components/overview/overview-range';
import { OverviewWidgetTable } from '@/components/overview/overview-widget-table'; import { OverviewWidgetTable } from '@/components/overview/overview-widget-table';
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
import { GscCannibalization } from '@/components/page/gsc-cannibalization'; import { GscCannibalization } from '@/components/page/gsc-cannibalization';
import { GscCtrBenchmark } from '@/components/page/gsc-ctr-benchmark'; import { GscCtrBenchmark } from '@/components/page/gsc-ctr-benchmark';
import { GscPositionChart } from '@/components/page/gsc-position-chart'; import { GscPositionChart } from '@/components/page/gsc-position-chart';
@@ -32,14 +28,12 @@ import { PagesInsights } from '@/components/page/pages-insights';
import { PageContainer } from '@/components/page-container'; import { PageContainer } from '@/components/page-container';
import { PageHeader } from '@/components/page-header'; import { PageHeader } from '@/components/page-header';
import { Pagination } from '@/components/pagination'; import { Pagination } from '@/components/pagination';
import { ReportInterval } from '@/components/report/ReportInterval';
import { import {
useYAxisProps, useYAxisProps,
X_AXIS_STYLE_PROPS, X_AXIS_STYLE_PROPS,
} from '@/components/report-chart/common/axis'; } from '@/components/report-chart/common/axis';
import { SerieIcon } from '@/components/report-chart/common/serie-icon'; import { SerieIcon } from '@/components/report-chart/common/serie-icon';
import { Skeleton } from '@/components/skeleton'; import { Skeleton } from '@/components/skeleton';
import { TimeWindowPicker } from '@/components/time-window-picker';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { useAppParams } from '@/hooks/use-app-params'; import { useAppParams } from '@/hooks/use-app-params';
@@ -94,27 +88,13 @@ function SeoPage() {
const { projectId, organizationId } = useAppParams(); const { projectId, organizationId } = useAppParams();
const trpc = useTRPC(); const trpc = useTRPC();
const navigate = useNavigate(); const navigate = useNavigate();
const { range, startDate, endDate, interval } = useOverviewOptions();
const [range, setRange] = useQueryState(
'range',
parseAsStringEnum(Object.keys(timeWindows) as IChartRange[]).withDefault(
'30d' as IChartRange
)
);
const [startDate, setStartDate] = useQueryState('start', parseAsString);
const [endDate, setEndDate] = useQueryState('end', parseAsString);
const [interval, setInterval] = useQueryState(
'interval',
parseAsStringEnum(Object.keys(intervals) as IInterval[]).withDefault(
(getDefaultIntervalByRange(range) ?? 'day') as IInterval
)
);
const dateInput = { const dateInput = {
range, range,
interval, interval,
startDate: startDate ?? undefined, startDate,
endDate: endDate ?? undefined, endDate,
}; };
const connectionQuery = useQuery( const connectionQuery = useQuery(
@@ -265,31 +245,8 @@ function SeoPage() {
<PageHeader <PageHeader
actions={ actions={
<> <>
<ReportInterval <OverviewRange />
chartType="linear" <OverviewInterval />
endDate={endDate}
interval={interval ?? 'day'}
onChange={(v) => setInterval(v)}
range={range}
startDate={startDate}
/>
<TimeWindowPicker
endDate={endDate}
onChange={(v) => {
if (v !== 'custom') {
setStartDate(null);
setEndDate(null);
}
setInterval(
(getDefaultIntervalByRange(v) ?? 'day') as IInterval
);
setRange(v);
}}
onEndDateChange={setEndDate}
onStartDateChange={setStartDate}
startDate={startDate}
value={range}
/>
</> </>
} }
description={`Search performance for ${connection.siteUrl}`} description={`Search performance for ${connection.siteUrl}`}

View File

@@ -24,11 +24,13 @@ const zGscDateInput = z.object({
projectId: z.string(), projectId: z.string(),
range: zRange, range: zRange,
interval: zTimeInterval.optional().default('day'), interval: zTimeInterval.optional().default('day'),
startDate: z.string().nullish(),
endDate: z.string().nullish(),
}); });
async function resolveDates( async function resolveDates(
projectId: string, projectId: string,
input: { range: string; startDate?: string; endDate?: string } input: { range: string; startDate?: string | null; endDate?: string | null }
) { ) {
const { timezone } = await getSettingsForProject(projectId); const { timezone } = await getSettingsForProject(projectId);
const { startDate, endDate } = getChartStartEndDate( const { startDate, endDate } = getChartStartEndDate(