use range instead of dates across the web

This commit is contained in:
Carl-Gerhard Lindesvärd
2023-10-28 21:58:17 +02:00
parent c8c86d8c23
commit c5823dc4cb
11 changed files with 156 additions and 96 deletions

View File

@@ -4,27 +4,29 @@ import { changeDateRanges, changeInterval } from "./reportSlice";
import { Combobox } from "../ui/combobox";
import { type IInterval } from "@/types";
import { timeRanges } from "@/utils/constants";
import { entries } from "@/utils/object";
export function ReportDateRange() {
const dispatch = useDispatch();
const range = useSelector((state) => state.report.range);
const interval = useSelector((state) => state.report.interval);
const chartType = useSelector((state) => state.report.chartType);
return (
<>
<RadioGroup>
{entries(timeRanges).map(([range, title]) => (
<RadioGroupItem
key={range}
// active={range === interval}
onClick={() => {
dispatch(changeDateRanges(range));
}}
>
{title}
</RadioGroupItem>
))}
{timeRanges.map(item => {
return (
<RadioGroupItem
key={item.range}
active={item.range === range}
onClick={() => {
dispatch(changeDateRanges(item.range));
}}
>
{item.title}
</RadioGroupItem>
)
})}
</RadioGroup>
{chartType === "linear" && (
<div className="w-full max-w-[200px]">

View File

@@ -8,23 +8,23 @@ type ReportLineChartProps = IChartInput
export const Chart = withChartProivder(({
interval,
startDate,
endDate,
events,
breakdowns,
chartType,
name,
range,
}: ReportLineChartProps) => {
const hasEmptyFilters = events.some((event) => event.filters.some((filter) => filter.value.length === 0));
const chart = api.chart.chart.useQuery(
{
interval,
chartType,
startDate,
endDate,
events,
breakdowns,
name,
range,
startDate: null,
endDate: null,
},
{
keepPreviousData: true,

View File

@@ -4,22 +4,26 @@ import {
type IChartEvent,
type IInterval,
type IChartType,
type IChartRange,
} from "@/types";
import { alphabetIds } from "@/utils/constants";
import { getDaysOldDate } from "@/utils/date";
import { type PayloadAction, createSlice } from "@reduxjs/toolkit";
type InitialState = IChartInput;
type InitialState = IChartInput & {
startDate: string | null;
endDate: string | null;
}
// First approach: define the initial state using that type
const initialState: InitialState = {
name: "screen_view",
chartType: "linear",
startDate: getDaysOldDate(7).toISOString(),
endDate: new Date().toISOString(),
interval: "day",
breakdowns: [],
events: [],
range: 30,
startDate: null,
endDate: null,
};
export const reportSlice = createSlice({
@@ -30,7 +34,11 @@ export const reportSlice = createSlice({
return initialState
},
setReport(state, action: PayloadAction<IChartInput>) {
return action.payload
return {
...action.payload,
startDate: null,
endDate: null,
}
},
// Events
addEvent: (state, action: PayloadAction<Omit<IChartEvent, "id">>) => {
@@ -107,21 +115,9 @@ export const reportSlice = createSlice({
state.endDate = action.payload;
},
changeDateRanges: (state, action: PayloadAction<number | 'today'>) => {
if(action.payload === 'today') {
const startDate = new Date()
startDate.setHours(0,0,0,0)
state.startDate = startDate.toISOString();
state.endDate = new Date().toISOString();
state.interval = 'hour'
return state
}
state.startDate = getDaysOldDate(action.payload).toISOString();
state.endDate = new Date().toISOString()
if (action.payload === 1) {
changeDateRanges: (state, action: PayloadAction<IChartRange>) => {
state.range = action.payload
if (action.payload === 0 || action.payload === 1) {
state.interval = "hour";
} else if (action.payload <= 30) {
state.interval = "day";

View File

@@ -9,6 +9,7 @@ import { popModal } from ".";
import { toast } from "@/components/ui/use-toast";
import { InputWithLabel } from "@/components/forms/InputWithLabel";
import { useRefetchActive } from "@/hooks/useRefetchActive";
import { useOrganizationParams } from "@/hooks/useOrganizationParams";
const validator = z.object({
name: z.string().min(1),
@@ -17,6 +18,7 @@ const validator = z.object({
type IForm = z.infer<typeof validator>;
export default function AddProject() {
const params = useOrganizationParams()
const refetch = useRefetchActive()
const mutation = api.project.create.useMutation({
onError: handleError,
@@ -41,7 +43,10 @@ export default function AddProject() {
<ModalHeader title="Create project" />
<form
onSubmit={handleSubmit((values) => {
mutation.mutate(values);
mutation.mutate({
...values,
organizationSlug: params.organization,
});
})}
>
<InputWithLabel label="Name" placeholder="Name" {...register('name')} />

View File

@@ -12,7 +12,6 @@ export const getServerSideProps = createServerSideProps()
export default function Home() {
const params = useOrganizationParams();
const query = api.dashboard.list.useQuery({
organizationSlug: params.organization,
projectSlug: params.project,
}, {
enabled: Boolean(params.organization && params.project),

View File

@@ -3,8 +3,13 @@ import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import { last, pipe, sort, uniq } from "ramda";
import { toDots } from "@/utils/object";
import { zChartInput } from "@/utils/validation";
import { type IChartInput, type IChartEvent } from "@/types";
import { zChartInputWithDates } from "@/utils/validation";
import {
type IChartInputWithDates,
type IChartEvent,
type IChartRange,
} from "@/types";
import { getDaysOldDate } from "@/utils/date";
export const config = {
api: {
@@ -89,18 +94,14 @@ export const chartRouter = createTRPCRouter({
}),
chart: protectedProcedure
.input(zChartInput)
.input(zChartInputWithDates)
.query(async ({ input: { events, ...input } }) => {
const startDate = input.startDate ?? new Date();
const endDate = input.endDate ?? new Date();
const series: Awaited<ReturnType<typeof getChartData>> = [];
for (const event of events) {
series.push(
...(await getChartData({
...input,
event,
startDate,
endDate,
})),
);
}
@@ -121,16 +122,21 @@ export const chartRouter = createTRPCRouter({
};
return {
events: Object.entries(series.reduce((acc, item) => {
if(acc[item.event.id]) {
acc[item.event.id] += item.totalCount;
} else {
acc[item.event.id] = item.totalCount;
}
return acc
}, {} as Record<typeof series[number]['event']['id'], number>)).map(([id, count]) => ({
count,
...events.find((event) => event.id === id)!,
events: Object.entries(
series.reduce(
(acc, item) => {
if (acc[item.event.id]) {
acc[item.event.id] += item.totalCount;
} else {
acc[item.event.id] = item.totalCount;
}
return acc;
},
{} as Record<(typeof series)[number]["event"]["id"], number>,
),
).map(([id, count]) => ({
count,
...events.find((event) => event.id === id)!,
})),
series: sorted.map((item) => ({
...item,
@@ -184,16 +190,45 @@ function getTotalCount(arr: ResultItem[]) {
return arr.reduce((acc, item) => acc + item.count, 0);
}
function getDatesFromRange(range: IChartRange) {
if (range === 0) {
const startDate = new Date();
const endDate = new Date().toISOString();
startDate.setHours(0, 0, 0, 0);
return {
startDate: startDate.toISOString(),
endDate: endDate,
};
}
const startDate = getDaysOldDate(range).toISOString();
const endDate = new Date().toISOString();
return {
startDate,
endDate,
};
}
async function getChartData({
chartType,
event,
breakdowns,
interval,
startDate,
endDate,
range,
startDate: _startDate,
endDate: _endDate,
}: {
event: IChartEvent;
} & Omit<IChartInput, "events">) {
} & Omit<IChartInputWithDates, "events">) {
const { startDate, endDate } =
_startDate && _endDate
? {
startDate: _startDate,
endDate: _endDate,
}
: getDatesFromRange(range);
const select = [];
const where = [];
const groupBy = [];
@@ -362,6 +397,8 @@ function fillEmptySpotsInTimeline(
const result = [];
const clonedStartDate = new Date(startDate);
const clonedEndDate = new Date(endDate);
const today = new Date();
if (interval === "hour") {
clonedStartDate.setMinutes(0, 0, 0);
clonedEndDate.setMinutes(0, 0, 0);
@@ -370,7 +407,16 @@ function fillEmptySpotsInTimeline(
clonedEndDate.setHours(2, 0, 0, 0);
}
while (clonedStartDate.getTime() <= clonedEndDate.getTime()) {
// Force if interval is month and the start date is the same month as today
const shouldForce = () =>
interval === "month" &&
clonedStartDate.getFullYear() === today.getFullYear() &&
clonedStartDate.getMonth() === today.getMonth();
while (
shouldForce() ||
clonedStartDate.getTime() <= clonedEndDate.getTime()
) {
const getYear = (date: Date) => date.getFullYear();
const getMonth = (date: Date) => date.getMonth();
const getDay = (date: Date) => date.getDate();

View File

@@ -2,13 +2,13 @@ import { z } from "zod";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { zChartInput } from "@/utils/validation";
import { dateDifferanceInDays, getDaysOldDate } from "@/utils/date";
import { db } from "@/server/db";
import {
type IChartInput,
type IChartBreakdown,
type IChartEvent,
type IChartEventFilter,
type IChartRange,
} from "@/types";
import { type Report as DbReport } from "@prisma/client";
import { getProjectBySlug } from "@/server/services/project.service";
@@ -39,11 +39,10 @@ function transformReport(report: DbReport): IChartInput & { id: string } {
id: report.id,
events: (report.events as IChartEvent[]).map(transformEvent),
breakdowns: report.breakdowns as IChartBreakdown[],
startDate: getDaysOldDate(report.range).toISOString(),
endDate: new Date().toISOString(),
chartType: report.chart_type,
interval: report.interval,
name: report.name || 'Untitled',
range: report.range as IChartRange ?? 30,
};
}
@@ -103,7 +102,7 @@ export const reportRouter = createTRPCRouter({
interval: report.interval,
breakdowns: report.breakdowns,
chart_type: report.chartType,
range: dateDifferanceInDays(new Date(report.endDate), new Date(report.startDate)),
range: report.range,
},
});
@@ -130,7 +129,7 @@ export const reportRouter = createTRPCRouter({
interval: report.interval,
breakdowns: report.breakdowns,
chart_type: report.chartType,
range: dateDifferanceInDays(new Date(report.endDate), new Date(report.startDate)),
range: report.range,
},
});
}),

View File

@@ -1,5 +1,6 @@
import { type RouterOutputs } from "@/utils/api";
import { type zTimeInterval, type zChartBreakdown, type zChartEvent, type zChartInput, type zChartType } from "@/utils/validation";
import { type timeRanges } from "@/utils/constants";
import { type zTimeInterval, type zChartBreakdown, type zChartEvent, type zChartInput, type zChartType, type zChartInputWithDates } from "@/utils/validation";
import { type Client, type Project } from "@prisma/client";
import { type TooltipProps } from "recharts";
import { type z } from "zod";
@@ -7,6 +8,7 @@ import { type z } from "zod";
export type HtmlProps<T> = React.DetailedHTMLProps<React.HTMLAttributes<T>, T>;
export type IChartInput = z.infer<typeof zChartInput>
export type IChartInputWithDates = z.infer<typeof zChartInputWithDates>
export type IChartEvent = z.infer<typeof zChartEvent>
export type IChartEventFilter = IChartEvent['filters'][number]
export type IChartEventFilterValue = IChartEvent['filters'][number]['value'][number]
@@ -14,7 +16,7 @@ export type IChartBreakdown = z.infer<typeof zChartBreakdown>
export type IInterval = z.infer<typeof zTimeInterval>
export type IChartType = z.infer<typeof zChartType>
export type IChartData = RouterOutputs["chart"]["chart"];
export type IChartRange = typeof timeRanges[number]['range'];
export type IToolTipProps<T> = Omit<TooltipProps<number, string>, 'payload'> & {
payload?: Array<T>
}

View File

@@ -19,15 +19,26 @@ export const intervals = {
month: "Month",
};
export const alphabetIds = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"] as const;
export const alphabetIds = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
] as const;
export const timeRanges = {
'today': 'Today',
1: '24 hours',
7: '7 days',
14: '14 days',
30: '30 days',
90: '3 months',
180: '6 months',
365: '1 year',
}
export const timeRanges = [
{ range: 0, title: "Today" },
{ range: 1, title: "24 hours" },
{ range: 7, title: "7 days" },
{ range: 14, title: "14 days" },
{ range: 30, title: "30 days" },
{ range: 90, title: "3 months" },
{ range: 180, title: "6 months" },
{ range: 365, title: "1 year" },
] as const

View File

@@ -15,10 +15,4 @@ export function toDots(
[`${path}${key}`]: value,
};
}, {});
}
export function entries<K extends string | number | symbol, V>(
obj: Record<K, V>,
): [K, V][] {
return Object.entries(obj) as [K, V][];
}
}

View File

@@ -1,9 +1,9 @@
import { z } from "zod";
import { operators, chartTypes, intervals } from "./constants";
function objectToZodEnums<K extends string> ( obj: Record<K, any> ): [ K, ...K[] ] {
const [ firstKey, ...otherKeys ] = Object.keys( obj ) as K[]
return [ firstKey!, ...otherKeys ]
function objectToZodEnums<K extends string>(obj: Record<K, any>): [K, ...K[]] {
const [firstKey, ...otherKeys] = Object.keys(obj) as K[];
return [firstKey!, ...otherKeys];
}
export const zChartEvent = z.object({
@@ -15,13 +15,7 @@ export const zChartEvent = z.object({
id: z.string(),
name: z.string(),
operator: z.enum(objectToZodEnums(operators)),
value: z.array(
z
.string()
.or(z.number())
.or(z.boolean())
.or(z.null())
),
value: z.array(z.string().or(z.number()).or(z.boolean()).or(z.null())),
}),
),
});
@@ -39,10 +33,22 @@ export const zTimeInterval = z.enum(objectToZodEnums(intervals));
export const zChartInput = z.object({
name: z.string(),
startDate: z.string(),
endDate: z.string(),
chartType: zChartType,
interval: zTimeInterval,
events: zChartEvents,
breakdowns: zChartBreakdowns,
range: z
.literal(0)
.or(z.literal(1))
.or(z.literal(7))
.or(z.literal(14))
.or(z.literal(30))
.or(z.literal(90))
.or(z.literal(180))
.or(z.literal(365)),
});
export const zChartInputWithDates = zChartInput.extend({
startDate: z.string().nullish(),
endDate: z.string().nullable(),
});