use range instead of dates across the web
This commit is contained in:
@@ -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]">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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')} />
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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][];
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user