diff --git a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx index 2406cde2..b949e60f 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteChart.tsx @@ -1,5 +1,5 @@ import { LoadingPanel } from '@/components/common/LoadingPanel'; -import { useDateRange } from '@/components/hooks'; +import { useDateRange, useTimezone } from '@/components/hooks'; import { useWebsitePageviewsQuery } from '@/components/hooks/queries/useWebsitePageviewsQuery'; import { PageviewsChart } from '@/components/metrics/PageviewsChart'; import { useMemo } from 'react'; @@ -11,7 +11,8 @@ export function WebsiteChart({ websiteId: string; compareMode?: boolean; }) { - const { dateRange, dateCompare } = useDateRange(); + const { timezone } = useTimezone(); + const { dateRange, dateCompare } = useDateRange({ timezone: timezone }); const { startDate, endDate, unit, value } = dateRange; const { data, isLoading, isFetching, error } = useWebsitePageviewsQuery({ websiteId, diff --git a/src/components/hooks/useDateParameters.ts b/src/components/hooks/useDateParameters.ts index 16e12314..d84b4236 100644 --- a/src/components/hooks/useDateParameters.ts +++ b/src/components/hooks/useDateParameters.ts @@ -5,13 +5,13 @@ export function useDateParameters() { const { dateRange: { startDate, endDate, unit }, } = useDateRange(); - const { timezone, toUtc, canonicalizeTimezone } = useTimezone(); + const { timezone, localToUtc, canonicalizeTimezone } = useTimezone(); return { - startAt: +toUtc(startDate), - endAt: +toUtc(endDate), - startDate: toUtc(startDate).toISOString(), - endDate: toUtc(endDate).toISOString(), + startAt: +localToUtc(startDate), + endAt: +localToUtc(endDate), + startDate: localToUtc(startDate).toISOString(), + endDate: localToUtc(endDate).toISOString(), unit, timezone: canonicalizeTimezone(timezone), }; diff --git a/src/components/hooks/useDateRange.ts b/src/components/hooks/useDateRange.ts index de53343a..7d388433 100644 --- a/src/components/hooks/useDateRange.ts +++ b/src/components/hooks/useDateRange.ts @@ -5,7 +5,7 @@ import { useLocale } from '@/components/hooks/useLocale'; import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants'; import { getItem } from '@/lib/storage'; -export function useDateRange(options: { ignoreOffset?: boolean } = {}) { +export function useDateRange(options: { ignoreOffset?: boolean; timezone?: string } = {}) { const { query: { date = '', offset = 0, compare = 'prev' }, } = useNavigation(); @@ -15,6 +15,7 @@ export function useDateRange(options: { ignoreOffset?: boolean } = {}) { const dateRangeObject = parseDateRange( date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE, locale, + options.timezone, ); return !options.ignoreOffset && offset diff --git a/src/components/hooks/useTimezone.ts b/src/components/hooks/useTimezone.ts index 3770c26b..22a34194 100644 --- a/src/components/hooks/useTimezone.ts +++ b/src/components/hooks/useTimezone.ts @@ -3,11 +3,13 @@ import { TIMEZONE_CONFIG, TIMEZONE_LEGACY } from '@/lib/constants'; import { formatInTimeZone, zonedTimeToUtc, utcToZonedTime } from 'date-fns-tz'; import { useApp, setTimezone } from '@/store/app'; import { useLocale } from './useLocale'; +import { getTimezone } from '@/lib/date'; const selector = (state: { timezone: string }) => state.timezone; export function useTimezone() { const timezone = useApp(selector); + const localTimeZone = getTimezone(); const { dateLocale } = useLocale(); const saveTimezone = (value: string) => { @@ -26,6 +28,38 @@ export function useTimezone() { ); }; + const formatSeriesTimezone = (data: any, column: string, timezone: string) => { + return data.map(item => { + const date = new Date(item[column]); + + const format = new Intl.DateTimeFormat('en-US', { + timeZone: timezone, + hour12: false, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); + + const parts = format.formatToParts(date); + const get = type => parts.find(p => p.type === type)?.value; + + const year = get('year'); + const month = get('month'); + const day = get('day'); + const hour = get('hour'); + const minute = get('minute'); + const second = get('second'); + + return { + ...item, + [column]: `${year}-${month}-${day} ${hour}:${minute}:${second}`, + }; + }); + }; + const toUtc = (date: Date | string | number) => { return zonedTimeToUtc(date, timezone); }; @@ -34,16 +68,28 @@ export function useTimezone() { return utcToZonedTime(date, timezone); }; + const localToUtc = (date: Date | string | number) => { + return zonedTimeToUtc(date, localTimeZone); + }; + + const localFromUtc = (date: Date | string | number) => { + return utcToZonedTime(date, localTimeZone); + }; + const canonicalizeTimezone = (timezone: string): string => { return TIMEZONE_LEGACY[timezone] ?? timezone; }; return { timezone, - saveTimezone, - formatTimezoneDate, + localTimeZone, toUtc, fromUtc, + localToUtc, + localFromUtc, + saveTimezone, + formatTimezoneDate, + formatSeriesTimezone, canonicalizeTimezone, }; } diff --git a/src/components/metrics/EventsChart.tsx b/src/components/metrics/EventsChart.tsx index 246772b3..03ed6e29 100644 --- a/src/components/metrics/EventsChart.tsx +++ b/src/components/metrics/EventsChart.tsx @@ -1,6 +1,11 @@ import { BarChart, BarChartProps } from '@/components/charts/BarChart'; import { LoadingPanel } from '@/components/common/LoadingPanel'; -import { useDateRange, useLocale, useWebsiteEventsSeriesQuery } from '@/components/hooks'; +import { + useDateRange, + useLocale, + useTimezone, + useWebsiteEventsSeriesQuery, +} from '@/components/hooks'; import { renderDateLabels } from '@/lib/charts'; import { CHART_COLORS } from '@/lib/constants'; import { generateTimeSeries } from '@/lib/date'; @@ -13,9 +18,10 @@ export interface EventsChartProps extends BarChartProps { } export function EventsChart({ websiteId, focusLabel }: EventsChartProps) { + const { timezone } = useTimezone(); const { dateRange: { startDate, endDate, unit }, - } = useDateRange(); + } = useDateRange({ timezone: timezone }); const { locale, dateLocale } = useLocale(); const { data, isLoading, error } = useWebsiteEventsSeriesQuery(websiteId); const [label, setLabel] = useState(focusLabel); @@ -33,20 +39,32 @@ export function EventsChart({ websiteId, focusLabel }: EventsChartProps) { return obj; }, {}); - return { - datasets: Object.keys(map).map((key, index) => { - const color = colord(CHART_COLORS[index % CHART_COLORS.length]); - return { - label: key, - data: generateTimeSeries(map[key], startDate, endDate, unit, dateLocale), - lineTension: 0, - backgroundColor: color.alpha(0.6).toRgbString(), - borderColor: color.alpha(0.7).toRgbString(), - borderWidth: 1, - }; - }), - focusLabel, - }; + if (!map || Object.keys(map).length === 0) { + return { + datasets: [ + { + data: generateTimeSeries([], startDate, endDate, unit, dateLocale), + lineTension: 0, + borderWidth: 1, + }, + ], + }; + } else { + return { + datasets: Object.keys(map).map((key, index) => { + const color = colord(CHART_COLORS[index % CHART_COLORS.length]); + return { + label: key, + data: generateTimeSeries(map[key], startDate, endDate, unit, dateLocale), + lineTension: 0, + backgroundColor: color.alpha(0.6).toRgbString(), + borderColor: color.alpha(0.7).toRgbString(), + borderWidth: 1, + }; + }), + focusLabel, + }; + } }, [data, startDate, endDate, unit, focusLabel]); useEffect(() => { diff --git a/src/components/metrics/MetricLabel.tsx b/src/components/metrics/MetricLabel.tsx index bc46a033..c2f1ea06 100644 --- a/src/components/metrics/MetricLabel.tsx +++ b/src/components/metrics/MetricLabel.tsx @@ -25,16 +25,16 @@ export function MetricLabel({ type, data }: MetricLabelProps) { const { getRegionName } = useRegionNames(locale); const { label, country, domain } = data; - const isType = ['browser', 'country', 'device', 'os'].includes(type); switch (type) { case 'browser': + case 'os': return ( } + label={formatValue(label, type)} + icon={} /> ); @@ -100,7 +100,7 @@ export function MetricLabel({ type, data }: MetricLabelProps) { type="device" value={labels[label] && label} label={formatValue(label, 'device')} - icon={} + icon={} /> ); @@ -141,14 +141,6 @@ export function MetricLabel({ type, data }: MetricLabelProps) { - ) - } /> ); } diff --git a/src/components/metrics/RealtimeChart.tsx b/src/components/metrics/RealtimeChart.tsx index a70361d7..a71c03c3 100644 --- a/src/components/metrics/RealtimeChart.tsx +++ b/src/components/metrics/RealtimeChart.tsx @@ -3,6 +3,7 @@ import { startOfMinute, subMinutes, isBefore } from 'date-fns'; import { PageviewsChart } from './PageviewsChart'; import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from '@/lib/constants'; import { RealtimeData } from '@/lib/types'; +import { useTimezone } from '@/components/hooks'; export interface RealtimeChartProps { data: RealtimeData; @@ -11,6 +12,7 @@ export interface RealtimeChartProps { } export function RealtimeChart({ data, unit, ...props }: RealtimeChartProps) { + const { formatSeriesTimezone, fromUtc, timezone } = useTimezone(); const endDate = startOfMinute(new Date()); const startDate = subMinutes(endDate, REALTIME_RANGE); const prevEndDate = useRef(endDate); @@ -21,8 +23,8 @@ export function RealtimeChart({ data, unit, ...props }: RealtimeChartProps) { } return { - pageviews: data.series.views, - sessions: data.series.visitors, + pageviews: formatSeriesTimezone(data.series.views, 'x', timezone), + sessions: formatSeriesTimezone(data.series.visitors, 'x', timezone), }; }, [data, startDate, endDate, unit]); @@ -38,8 +40,8 @@ export function RealtimeChart({ data, unit, ...props }: RealtimeChartProps) { return (