added tooling (eslint, typescript and prettier)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -176,3 +176,4 @@ dist
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
*.tsbuildinfo
|
||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"yoavbls.pretty-ts-errors"
|
||||
]
|
||||
}
|
||||
23
.vscode/settings.json
vendored
Normal file
23
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"editor.codeActionsOnSave": { "source.fixAll.eslint": true },
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"[handlebars]": {
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": { "source.fixAll.eslint": false }
|
||||
},
|
||||
"files.associations": { "*.hbs": "handlebars" },
|
||||
"eslint.workingDirectories": [
|
||||
{ "pattern": "apps/*/" },
|
||||
{ "pattern": "packages/*/" },
|
||||
{ "pattern": "tooling/*/" }
|
||||
],
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"[yaml]": {
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 2,
|
||||
"editor.autoIndent": "advanced"
|
||||
},
|
||||
"editor.inlineSuggest.enabled": true
|
||||
}
|
||||
@@ -13,4 +13,4 @@
|
||||
"components": "@/components",
|
||||
"utils": "@/utils/cn"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
|
||||
* for Docker builds.
|
||||
*/
|
||||
await import("./src/env.mjs");
|
||||
await import('./src/env.mjs');
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const config = {
|
||||
@@ -14,8 +14,8 @@ const config = {
|
||||
* @see https://github.com/vercel/next.js/issues/41980
|
||||
*/
|
||||
i18n: {
|
||||
locales: ["en"],
|
||||
defaultLocale: "en",
|
||||
locales: ['en'],
|
||||
defaultLocale: 'en',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"db:push": "prisma db push",
|
||||
"dev": "next dev",
|
||||
"postinstall": "prisma generate",
|
||||
"lint": "next lint",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"start": "next start"
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.3.2",
|
||||
@@ -60,26 +60,32 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mixan/eslint-config": "workspace:*",
|
||||
"@mixan/prettier-config": "workspace:*",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
"@types/eslint": "^8.44.2",
|
||||
"@types/node": "^18.16.0",
|
||||
"@types/ramda": "^0.29.6",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-syntax-highlighter": "^15.5.9",
|
||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
||||
"@typescript-eslint/parser": "^6.3.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-next": "^13.5.4",
|
||||
"postcss": "^8.4.27",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.1",
|
||||
"prisma": "^5.1.1",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.1.6"
|
||||
"typescript": "^5.2.0"
|
||||
},
|
||||
"ct3aMetadata": {
|
||||
"initVersion": "7.21.0"
|
||||
}
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@mixan/eslint-config/base",
|
||||
"@mixan/eslint-config/react"
|
||||
]
|
||||
},
|
||||
"prettier": "@mixan/prettier-config"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').options} */
|
||||
const config = {
|
||||
plugins: ["prettier-plugin-tailwindcss"],
|
||||
plugins: ['prettier-plugin-tailwindcss'],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { default } from "next-auth/middleware"
|
||||
export { default } from 'next-auth/middleware';
|
||||
|
||||
export const config = { matcher: ["/dashboard"] }
|
||||
export const config = { matcher: ['/dashboard'] };
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useQueryParams } from "@/hooks/useQueryParams";
|
||||
import { z } from "zod";
|
||||
import { useQueryParams } from '@/hooks/useQueryParams';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const useReportId = () =>
|
||||
useQueryParams(
|
||||
z.object({
|
||||
reportId: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import {
|
||||
type IChartInput,
|
||||
type IChartBreakdown,
|
||||
type IChartEvent,
|
||||
type IInterval,
|
||||
type IChartType,
|
||||
type IChartInput,
|
||||
type IChartRange,
|
||||
} from "@/types";
|
||||
import { alphabetIds } from "@/utils/constants";
|
||||
import { type PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
type IChartType,
|
||||
type IInterval,
|
||||
} from '@/types';
|
||||
import { alphabetIds } from '@/utils/constants';
|
||||
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
type InitialState = IChartInput & {
|
||||
startDate: string | null;
|
||||
endDate: string | null;
|
||||
}
|
||||
startDate: string | null;
|
||||
endDate: string | null;
|
||||
};
|
||||
|
||||
// First approach: define the initial state using that type
|
||||
const initialState: InitialState = {
|
||||
name: "screen_view",
|
||||
chartType: "linear",
|
||||
interval: "day",
|
||||
name: 'screen_view',
|
||||
chartType: 'linear',
|
||||
interval: 'day',
|
||||
breakdowns: [],
|
||||
events: [],
|
||||
range: 30,
|
||||
@@ -27,21 +27,21 @@ const initialState: InitialState = {
|
||||
};
|
||||
|
||||
export const reportSlice = createSlice({
|
||||
name: "counter",
|
||||
name: 'counter',
|
||||
initialState,
|
||||
reducers: {
|
||||
reset() {
|
||||
return initialState
|
||||
return initialState;
|
||||
},
|
||||
setReport(state, action: PayloadAction<IChartInput>) {
|
||||
setReport(state, action: PayloadAction<IChartInput>) {
|
||||
return {
|
||||
...action.payload,
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
}
|
||||
};
|
||||
},
|
||||
// Events
|
||||
addEvent: (state, action: PayloadAction<Omit<IChartEvent, "id">>) => {
|
||||
addEvent: (state, action: PayloadAction<Omit<IChartEvent, 'id'>>) => {
|
||||
state.events.push({
|
||||
id: alphabetIds[state.events.length]!,
|
||||
...action.payload,
|
||||
@@ -51,10 +51,10 @@ export const reportSlice = createSlice({
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
id: string;
|
||||
}>,
|
||||
}>
|
||||
) => {
|
||||
state.events = state.events.filter(
|
||||
(event) => event.id !== action.payload.id,
|
||||
(event) => event.id !== action.payload.id
|
||||
);
|
||||
},
|
||||
changeEvent: (state, action: PayloadAction<IChartEvent>) => {
|
||||
@@ -69,7 +69,7 @@ export const reportSlice = createSlice({
|
||||
// Breakdowns
|
||||
addBreakdown: (
|
||||
state,
|
||||
action: PayloadAction<Omit<IChartBreakdown, "id">>,
|
||||
action: PayloadAction<Omit<IChartBreakdown, 'id'>>
|
||||
) => {
|
||||
state.breakdowns.push({
|
||||
id: alphabetIds[state.breakdowns.length]!,
|
||||
@@ -80,10 +80,10 @@ export const reportSlice = createSlice({
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
id: string;
|
||||
}>,
|
||||
}>
|
||||
) => {
|
||||
state.breakdowns = state.breakdowns.filter(
|
||||
(event) => event.id !== action.payload.id,
|
||||
(event) => event.id !== action.payload.id
|
||||
);
|
||||
},
|
||||
changeBreakdown: (state, action: PayloadAction<IChartBreakdown>) => {
|
||||
@@ -99,7 +99,7 @@ export const reportSlice = createSlice({
|
||||
changeInterval: (state, action: PayloadAction<IInterval>) => {
|
||||
state.interval = action.payload;
|
||||
},
|
||||
|
||||
|
||||
// Chart type
|
||||
changeChartType: (state, action: PayloadAction<IChartType>) => {
|
||||
state.chartType = action.payload;
|
||||
@@ -116,15 +116,15 @@ export const reportSlice = createSlice({
|
||||
},
|
||||
|
||||
changeDateRanges: (state, action: PayloadAction<IChartRange>) => {
|
||||
state.range = action.payload
|
||||
state.range = action.payload;
|
||||
if (action.payload === 0.3 || action.payload === 0.6) {
|
||||
state.interval = "minute";
|
||||
state.interval = 'minute';
|
||||
} else if (action.payload === 0 || action.payload === 1) {
|
||||
state.interval = "hour";
|
||||
state.interval = 'hour';
|
||||
} else if (action.payload <= 30) {
|
||||
state.interval = "day";
|
||||
state.interval = 'day';
|
||||
} else {
|
||||
state.interval = "month";
|
||||
state.interval = 'month';
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,104 +1,100 @@
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from "react"
|
||||
import * as React from 'react';
|
||||
import type { ToastActionElement, ToastProps } from '@/components/ui/toast';
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
const TOAST_LIMIT = 1;
|
||||
const TOAST_REMOVE_DELAY = 1000000;
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
id: string;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
};
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
ADD_TOAST: 'ADD_TOAST',
|
||||
UPDATE_TOAST: 'UPDATE_TOAST',
|
||||
DISMISS_TOAST: 'DISMISS_TOAST',
|
||||
REMOVE_TOAST: 'REMOVE_TOAST',
|
||||
} as const;
|
||||
|
||||
let count = 0
|
||||
let count = 0;
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_VALUE
|
||||
return count.toString()
|
||||
count = (count + 1) % Number.MAX_VALUE;
|
||||
return count.toString();
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
type ActionType = typeof actionTypes;
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
type: ActionType['ADD_TOAST'];
|
||||
toast: ToasterToast;
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
type: ActionType['UPDATE_TOAST'];
|
||||
toast: Partial<ToasterToast>;
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
type: ActionType['DISMISS_TOAST'];
|
||||
toastId?: ToasterToast['id'];
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
type: ActionType['REMOVE_TOAST'];
|
||||
toastId?: ToasterToast['id'];
|
||||
};
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
toasts: ToasterToast[];
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
toastTimeouts.delete(toastId);
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
type: 'REMOVE_TOAST',
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
});
|
||||
}, TOAST_REMOVE_DELAY);
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
toastTimeouts.set(toastId, timeout);
|
||||
};
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
case 'ADD_TOAST':
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
};
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
case 'UPDATE_TOAST':
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
case 'DISMISS_TOAST': {
|
||||
const { toastId } = action;
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
addToRemoveQueue(toastId);
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
addToRemoveQueue(toast.id);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -111,82 +107,82 @@ export const reducer = (state: State, action: Action): State => {
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
};
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
case 'REMOVE_TOAST':
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
const listeners: Array<(state: State) => void> = [];
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
let memoryState: State = { toasts: [] };
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
memoryState = reducer(memoryState, action);
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
listener(memoryState);
|
||||
});
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
type Toast = Omit<ToasterToast, 'id'>;
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
const id = genId();
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
type: 'UPDATE_TOAST',
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
});
|
||||
const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
type: 'ADD_TOAST',
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
if (!open) dismiss();
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
const [state, setState] = React.useState<State>(memoryState);
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
listeners.push(setState);
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
const index = listeners.indexOf(setState);
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
|
||||
};
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
export { useToast, toast };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createEnv } from "@t3-oss/env-nextjs";
|
||||
import { z } from "zod";
|
||||
import { createEnv } from '@t3-oss/env-nextjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const env = createEnv({
|
||||
/**
|
||||
@@ -11,14 +11,14 @@ export const env = createEnv({
|
||||
.string()
|
||||
.url()
|
||||
.refine(
|
||||
(str) => !str.includes("YOUR_MYSQL_URL_HERE"),
|
||||
"You forgot to change the default URL"
|
||||
(str) => !str.includes('YOUR_MYSQL_URL_HERE'),
|
||||
'You forgot to change the default URL'
|
||||
),
|
||||
NODE_ENV: z
|
||||
.enum(["development", "test", "production"])
|
||||
.default("development"),
|
||||
.enum(['development', 'test', 'production'])
|
||||
.default('development'),
|
||||
NEXTAUTH_SECRET:
|
||||
process.env.NODE_ENV === "production"
|
||||
process.env.NODE_ENV === 'production'
|
||||
? z.string()
|
||||
: z.string().optional(),
|
||||
NEXTAUTH_URL: z.preprocess(
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import { type IInterval } from "@/types";
|
||||
|
||||
import { type IInterval } from '@/types';
|
||||
|
||||
export function formatDateInterval(interval: IInterval, date: Date): string {
|
||||
if (interval === "hour" || interval === "minute") {
|
||||
return new Intl.DateTimeFormat("en-GB", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
if (interval === 'hour' || interval === 'minute') {
|
||||
return new Intl.DateTimeFormat('en-GB', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
if (interval === "month") {
|
||||
return new Intl.DateTimeFormat("en-GB", { month: "short" }).format(date);
|
||||
|
||||
if (interval === 'month') {
|
||||
return new Intl.DateTimeFormat('en-GB', { month: 'short' }).format(date);
|
||||
}
|
||||
|
||||
if (interval === "day") {
|
||||
return new Intl.DateTimeFormat("en-GB", { weekday: "short", day: '2-digit', month: '2-digit' }).format(
|
||||
date,
|
||||
);
|
||||
if (interval === 'day') {
|
||||
return new Intl.DateTimeFormat('en-GB', {
|
||||
weekday: 'short',
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
|
||||
return date.toISOString();
|
||||
}
|
||||
|
||||
export function useFormatDateInterval(interval: IInterval) {
|
||||
return (date: Date | string) => formatDateInterval(interval, typeof date === "string" ? new Date(date) : date);
|
||||
}
|
||||
return (date: Date | string) =>
|
||||
formatDateInterval(
|
||||
interval,
|
||||
typeof date === 'string' ? new Date(date) : date
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import mappings from '@/mappings.json'
|
||||
import mappings from '@/mappings.json';
|
||||
|
||||
export function useMappings() {
|
||||
return (val: string) => {
|
||||
return mappings.find((item) => item.id === val)?.name ?? val
|
||||
}
|
||||
}
|
||||
return mappings.find((item) => item.id === val)?.name ?? val;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { useQueryParams } from "./useQueryParams";
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useQueryParams } from './useQueryParams';
|
||||
|
||||
export function useOrganizationParams() {
|
||||
return useQueryParams(
|
||||
@@ -8,6 +9,6 @@ export function useOrganizationParams() {
|
||||
project: z.string(),
|
||||
dashboard: z.string(),
|
||||
profileId: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import { type z } from "zod";
|
||||
import { useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { type z } from 'zod';
|
||||
|
||||
export function useQueryParams<Z extends z.ZodTypeAny = z.ZodNever>(zod: Z) {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
export function useRefetchActive() {
|
||||
const client = useQueryClient()
|
||||
return () => client.refetchQueries({type: 'active'})
|
||||
}
|
||||
const client = useQueryClient();
|
||||
return () => client.refetchQueries({ type: 'active' });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export function useRouterBeforeLeave(callback: () => void) {
|
||||
const router = useRouter();
|
||||
@@ -8,14 +8,14 @@ export function useRouterBeforeLeave(callback: () => void) {
|
||||
useEffect(() => {
|
||||
const handleRouteChange = (url: string) => {
|
||||
if (prevUrl.current !== url) {
|
||||
callback()
|
||||
callback();
|
||||
}
|
||||
prevUrl.current = url;
|
||||
};
|
||||
|
||||
router.events.on("routeChangeStart", handleRouteChange);
|
||||
router.events.on('routeChangeStart', handleRouteChange);
|
||||
return () => {
|
||||
router.events.off("routeChangeStart", handleRouteChange);
|
||||
router.events.off('routeChangeStart', handleRouteChange);
|
||||
};
|
||||
}, [router, callback]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,146 +1,146 @@
|
||||
[
|
||||
{
|
||||
"id": "clbow140n0228u99ui1t24l85",
|
||||
"name": "Mjölkproteinfritt"
|
||||
},
|
||||
{
|
||||
"id": "cl04a3trn0015ix50tzy40pyf",
|
||||
"name": "Måltider"
|
||||
},
|
||||
{
|
||||
"id": "cl04a5p7s0401ix505n75yfcn",
|
||||
"name": "Svårighetsgrad"
|
||||
},
|
||||
{
|
||||
"id": "cl04a7k3i0813ix50aau6yxqg",
|
||||
"name": "Tid"
|
||||
},
|
||||
{
|
||||
"id": "cl04a47fu0103ix50dqckz2vc",
|
||||
"name": "Frukost"
|
||||
},
|
||||
{
|
||||
"id": "cl04a4hvu0152ix50o0w4iy8l",
|
||||
"name": "Mellis"
|
||||
},
|
||||
{
|
||||
"id": "cl04a58ju0281ix50kdmcwst6",
|
||||
"name": "Dessert"
|
||||
},
|
||||
{
|
||||
"id": "cl04a5fjc0321ix50xiwhuydy",
|
||||
"name": "Smakportioner"
|
||||
},
|
||||
{
|
||||
"id": "cl04a5kcu0361ix50bnmbhoxz",
|
||||
"name": "Plockmat"
|
||||
},
|
||||
{
|
||||
"id": "cl04a60sk0496ix50et419drf",
|
||||
"name": "Medium"
|
||||
},
|
||||
{
|
||||
"id": "cl04a67lx0536ix50sstoxnhi",
|
||||
"name": "Avancerat"
|
||||
},
|
||||
{
|
||||
"id": "cl04a7qi60850ix50je7vaxo3",
|
||||
"name": "0-10 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a7vxi0890ix50veumcuyu",
|
||||
"name": "10-20 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a82bj0930ix50bboh3tl9",
|
||||
"name": "20-30 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a8a7a0970ix50uet02cqh",
|
||||
"name": "30-40 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a8g151010ix50z4cnf2kg",
|
||||
"name": "40-50 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a8mqy1050ix50z0d1ho1a",
|
||||
"name": "50-60 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a5ujg0447ix50vd3vor87",
|
||||
"name": "Lätt"
|
||||
},
|
||||
{
|
||||
"id": "cl04a4qv60201ix50b8q5kn9r",
|
||||
"name": "Lunch & Middag"
|
||||
},
|
||||
{
|
||||
"id": "clak50jem0072ri9ugwygg5ko",
|
||||
"name": "Annat"
|
||||
},
|
||||
{
|
||||
"id": "clak510qm0120ri9upqkca39s",
|
||||
"name": "För hela familjen"
|
||||
},
|
||||
{
|
||||
"id": "clak59l8x0085yd9uzllcuci5",
|
||||
"name": "Under 3 ingredienser"
|
||||
},
|
||||
{
|
||||
"id": "clak59l8y0087yd9u53qperp8",
|
||||
"name": "Under 5 ingredienser"
|
||||
},
|
||||
{
|
||||
"id": "claslo2sg0404no9uo2tckm5i",
|
||||
"name": "Huvudingredienser"
|
||||
},
|
||||
{
|
||||
"id": "claslv9s20491no9ugo4fd9ns",
|
||||
"name": "Fisk"
|
||||
},
|
||||
{
|
||||
"id": "claslv9s30493no9umug5po29",
|
||||
"name": "Kyckling"
|
||||
},
|
||||
{
|
||||
"id": "claslv9s40495no9umor61pql",
|
||||
"name": "Kött"
|
||||
},
|
||||
{
|
||||
"id": "claslv9s40497no9uttwkt47n",
|
||||
"name": "Korv"
|
||||
},
|
||||
{
|
||||
"id": "claslv9s50499no9uch0lhs9i",
|
||||
"name": "Vegetariskt"
|
||||
},
|
||||
{
|
||||
"id": "clb1y44f40128np9uufck0iqf",
|
||||
"name": "Årstider"
|
||||
},
|
||||
{
|
||||
"id": "clb1y4ks80202np9uh43c84ts",
|
||||
"name": "Jul"
|
||||
},
|
||||
{
|
||||
"id": "clbovy0fd0081u99u8dr0yplr",
|
||||
"name": "Allergier"
|
||||
},
|
||||
{
|
||||
"id": "clbow140p0230u99uk9e7g1u1",
|
||||
"name": "Äggfritt"
|
||||
},
|
||||
{
|
||||
"id": "clbow140q0232u99uy3lwukvc",
|
||||
"name": "Vetefritt"
|
||||
},
|
||||
{
|
||||
"id": "clbow140q0234u99uiyrujxd4",
|
||||
"name": "Glutenfritt"
|
||||
},
|
||||
{
|
||||
"id": "clbow140r0236u99u5333gpei",
|
||||
"name": "Nötfritt"
|
||||
}
|
||||
]
|
||||
{
|
||||
"id": "clbow140n0228u99ui1t24l85",
|
||||
"name": "Mjölkproteinfritt"
|
||||
},
|
||||
{
|
||||
"id": "cl04a3trn0015ix50tzy40pyf",
|
||||
"name": "Måltider"
|
||||
},
|
||||
{
|
||||
"id": "cl04a5p7s0401ix505n75yfcn",
|
||||
"name": "Svårighetsgrad"
|
||||
},
|
||||
{
|
||||
"id": "cl04a7k3i0813ix50aau6yxqg",
|
||||
"name": "Tid"
|
||||
},
|
||||
{
|
||||
"id": "cl04a47fu0103ix50dqckz2vc",
|
||||
"name": "Frukost"
|
||||
},
|
||||
{
|
||||
"id": "cl04a4hvu0152ix50o0w4iy8l",
|
||||
"name": "Mellis"
|
||||
},
|
||||
{
|
||||
"id": "cl04a58ju0281ix50kdmcwst6",
|
||||
"name": "Dessert"
|
||||
},
|
||||
{
|
||||
"id": "cl04a5fjc0321ix50xiwhuydy",
|
||||
"name": "Smakportioner"
|
||||
},
|
||||
{
|
||||
"id": "cl04a5kcu0361ix50bnmbhoxz",
|
||||
"name": "Plockmat"
|
||||
},
|
||||
{
|
||||
"id": "cl04a60sk0496ix50et419drf",
|
||||
"name": "Medium"
|
||||
},
|
||||
{
|
||||
"id": "cl04a67lx0536ix50sstoxnhi",
|
||||
"name": "Avancerat"
|
||||
},
|
||||
{
|
||||
"id": "cl04a7qi60850ix50je7vaxo3",
|
||||
"name": "0-10 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a7vxi0890ix50veumcuyu",
|
||||
"name": "10-20 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a82bj0930ix50bboh3tl9",
|
||||
"name": "20-30 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a8a7a0970ix50uet02cqh",
|
||||
"name": "30-40 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a8g151010ix50z4cnf2kg",
|
||||
"name": "40-50 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a8mqy1050ix50z0d1ho1a",
|
||||
"name": "50-60 min"
|
||||
},
|
||||
{
|
||||
"id": "cl04a5ujg0447ix50vd3vor87",
|
||||
"name": "Lätt"
|
||||
},
|
||||
{
|
||||
"id": "cl04a4qv60201ix50b8q5kn9r",
|
||||
"name": "Lunch & Middag"
|
||||
},
|
||||
{
|
||||
"id": "clak50jem0072ri9ugwygg5ko",
|
||||
"name": "Annat"
|
||||
},
|
||||
{
|
||||
"id": "clak510qm0120ri9upqkca39s",
|
||||
"name": "För hela familjen"
|
||||
},
|
||||
{
|
||||
"id": "clak59l8x0085yd9uzllcuci5",
|
||||
"name": "Under 3 ingredienser"
|
||||
},
|
||||
{
|
||||
"id": "clak59l8y0087yd9u53qperp8",
|
||||
"name": "Under 5 ingredienser"
|
||||
},
|
||||
{
|
||||
"id": "claslo2sg0404no9uo2tckm5i",
|
||||
"name": "Huvudingredienser"
|
||||
},
|
||||
{
|
||||
"id": "claslv9s20491no9ugo4fd9ns",
|
||||
"name": "Fisk"
|
||||
},
|
||||
{
|
||||
"id": "claslv9s30493no9umug5po29",
|
||||
"name": "Kyckling"
|
||||
},
|
||||
{
|
||||
"id": "claslv9s40495no9umor61pql",
|
||||
"name": "Kött"
|
||||
},
|
||||
{
|
||||
"id": "claslv9s40497no9uttwkt47n",
|
||||
"name": "Korv"
|
||||
},
|
||||
{
|
||||
"id": "claslv9s50499no9uch0lhs9i",
|
||||
"name": "Vegetariskt"
|
||||
},
|
||||
{
|
||||
"id": "clb1y44f40128np9uufck0iqf",
|
||||
"name": "Årstider"
|
||||
},
|
||||
{
|
||||
"id": "clb1y4ks80202np9uh43c84ts",
|
||||
"name": "Jul"
|
||||
},
|
||||
{
|
||||
"id": "clbovy0fd0081u99u8dr0yplr",
|
||||
"name": "Allergier"
|
||||
},
|
||||
{
|
||||
"id": "clbow140p0230u99uk9e7g1u1",
|
||||
"name": "Äggfritt"
|
||||
},
|
||||
{
|
||||
"id": "clbow140q0232u99uy3lwukvc",
|
||||
"name": "Vetefritt"
|
||||
},
|
||||
{
|
||||
"id": "clbow140q0234u99uiyrujxd4",
|
||||
"name": "Glutenfritt"
|
||||
},
|
||||
{
|
||||
"id": "clbow140r0236u99u5333gpei",
|
||||
"name": "Nötfritt"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
import { type Session } from "next-auth";
|
||||
import { SessionProvider, getSession } from "next-auth/react";
|
||||
import App, {
|
||||
type AppContext,
|
||||
type AppInitialProps,
|
||||
type AppType,
|
||||
} from "next/app";
|
||||
import store from "@/redux";
|
||||
import { Provider as ReduxProvider } from "react-redux";
|
||||
import { Suspense } from "react";
|
||||
import { Space_Grotesk } from "next/font/google";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { Suspense } from 'react';
|
||||
import { Toaster } from '@/components/ui/toaster';
|
||||
import store from '@/redux';
|
||||
import { api } from '@/utils/api';
|
||||
import { type Session } from 'next-auth';
|
||||
import { SessionProvider } from 'next-auth/react';
|
||||
import { type AppType } from 'next/app';
|
||||
import { Space_Grotesk } from 'next/font/google';
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
|
||||
import { api } from "@/utils/api";
|
||||
import '@/styles/globals.css';
|
||||
|
||||
import "@/styles/globals.css";
|
||||
import { ModalProvider } from "@/modals";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { ModalProvider } from '@/modals';
|
||||
|
||||
const font = Space_Grotesk({
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
variable: "--text",
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
variable: '--text',
|
||||
});
|
||||
|
||||
const MixanApp: AppType<{ session: Session | null }> = ({
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import NextAuth from "next-auth";
|
||||
|
||||
import { authOptions } from "@/server/auth";
|
||||
import { authOptions } from '@/server/auth';
|
||||
import NextAuth from 'next-auth';
|
||||
|
||||
export default NextAuth(authOptions);
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import { validateSdkRequest } from '@/server/auth'
|
||||
import { db } from '@/server/db'
|
||||
import { createError, handleError } from '@/server/exceptions'
|
||||
import { type EventPayload } from '@mixan/types'
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import { validateSdkRequest } from '@/server/auth';
|
||||
import { db } from '@/server/db';
|
||||
import { createError, handleError } from '@/server/exceptions';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { type EventPayload } from '@mixan/types';
|
||||
|
||||
interface Request extends NextApiRequest {
|
||||
body: Array<EventPayload>
|
||||
body: Array<EventPayload>;
|
||||
}
|
||||
|
||||
export default async function handler(
|
||||
req: Request,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
if(req.method !== 'POST') {
|
||||
return handleError(res, createError(405, 'Method not allowed'))
|
||||
export default async function handler(req: Request, res: NextApiResponse) {
|
||||
if (req.method !== 'POST') {
|
||||
return handleError(res, createError(405, 'Method not allowed'));
|
||||
}
|
||||
|
||||
try {
|
||||
// Check client id & secret
|
||||
const projectId = await validateSdkRequest(req)
|
||||
const projectId = await validateSdkRequest(req);
|
||||
|
||||
await db.event.createMany({
|
||||
data: req.body.map((event) => ({
|
||||
@@ -27,11 +25,11 @@ export default async function handler(
|
||||
createdAt: event.time,
|
||||
project_id: projectId,
|
||||
profile_id: event.profileId,
|
||||
}))
|
||||
})
|
||||
|
||||
res.status(200).end()
|
||||
})),
|
||||
});
|
||||
|
||||
res.status(200).end();
|
||||
} catch (error) {
|
||||
handleError(res, error)
|
||||
handleError(res, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { validateSdkRequest } from "@/server/auth";
|
||||
import { createError, handleError } from "@/server/exceptions";
|
||||
import { tickProfileProperty } from "@/server/services/profile.service";
|
||||
import { type ProfileIncrementPayload } from "@mixan/types";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { validateSdkRequest } from '@/server/auth';
|
||||
import { createError, handleError } from '@/server/exceptions';
|
||||
import { tickProfileProperty } from '@/server/services/profile.service';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { type ProfileIncrementPayload } from '@mixan/types';
|
||||
|
||||
interface Request extends NextApiRequest {
|
||||
body: ProfileIncrementPayload;
|
||||
}
|
||||
|
||||
export default async function handler(req: Request, res: NextApiResponse) {
|
||||
if (req.method !== "PUT") {
|
||||
return handleError(res, createError(405, "Method not allowed"));
|
||||
if (req.method !== 'PUT') {
|
||||
return handleError(res, createError(405, 'Method not allowed'));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { validateSdkRequest } from "@/server/auth";
|
||||
import { createError, handleError } from "@/server/exceptions";
|
||||
import { tickProfileProperty } from "@/server/services/profile.service";
|
||||
import { type ProfileIncrementPayload } from "@mixan/types";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { validateSdkRequest } from '@/server/auth';
|
||||
import { createError, handleError } from '@/server/exceptions';
|
||||
import { tickProfileProperty } from '@/server/services/profile.service';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { type ProfileIncrementPayload } from '@mixan/types';
|
||||
|
||||
interface Request extends NextApiRequest {
|
||||
body: ProfileIncrementPayload;
|
||||
}
|
||||
|
||||
export default async function handler(req: Request, res: NextApiResponse) {
|
||||
if (req.method !== "PUT") {
|
||||
return handleError(res, createError(405, "Method not allowed"));
|
||||
if (req.method !== 'PUT') {
|
||||
return handleError(res, createError(405, 'Method not allowed'));
|
||||
}
|
||||
|
||||
try {
|
||||
// Check client id & secret
|
||||
await validateSdkRequest(req)
|
||||
await validateSdkRequest(req);
|
||||
|
||||
const profileId = req.query.profileId as string;
|
||||
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import { validateSdkRequest } from "@/server/auth";
|
||||
import { db } from "@/server/db";
|
||||
import { createError, handleError } from "@/server/exceptions";
|
||||
import { getProfile } from "@/server/services/profile.service";
|
||||
import { type ProfilePayload } from "@mixan/types";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { validateSdkRequest } from '@/server/auth';
|
||||
import { db } from '@/server/db';
|
||||
import { createError, handleError } from '@/server/exceptions';
|
||||
import { getProfile } from '@/server/services/profile.service';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { type ProfilePayload } from '@mixan/types';
|
||||
|
||||
interface Request extends NextApiRequest {
|
||||
body: ProfilePayload;
|
||||
}
|
||||
|
||||
export default async function handler(req: Request, res: NextApiResponse) {
|
||||
if (req.method !== "PUT" && req.method !== "POST") {
|
||||
return handleError(res, createError(405, "Method not allowed"));
|
||||
export default async function handler(req: Request, res: NextApiResponse) {
|
||||
if (req.method !== 'PUT' && req.method !== 'POST') {
|
||||
return handleError(res, createError(405, 'Method not allowed'));
|
||||
}
|
||||
|
||||
try {
|
||||
// Check client id & secret
|
||||
await validateSdkRequest(req)
|
||||
await validateSdkRequest(req);
|
||||
|
||||
const profileId = req.query.profileId as string;
|
||||
const profile = await getProfile(profileId)
|
||||
|
||||
const profile = await getProfile(profileId);
|
||||
|
||||
const { body } = req;
|
||||
await db.profile.update({
|
||||
where: {
|
||||
@@ -33,7 +34,7 @@ export default async function handler(req: Request, res: NextApiResponse) {
|
||||
last_name: body.last_name,
|
||||
avatar: body.avatar,
|
||||
properties: {
|
||||
...(typeof profile.properties === "object"
|
||||
...(typeof profile.properties === 'object'
|
||||
? profile.properties ?? {}
|
||||
: {}),
|
||||
...(body.properties ?? {}),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { validateSdkRequest } from "@/server/auth";
|
||||
import { db } from "@/server/db";
|
||||
import { createError, handleError } from "@/server/exceptions";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import randomAnimalName from "random-animal-name";
|
||||
import { validateSdkRequest } from '@/server/auth';
|
||||
import { db } from '@/server/db';
|
||||
import { createError, handleError } from '@/server/exceptions';
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import randomAnimalName from 'random-animal-name';
|
||||
|
||||
interface Request extends NextApiRequest {
|
||||
body: {
|
||||
@@ -12,8 +12,8 @@ interface Request extends NextApiRequest {
|
||||
}
|
||||
|
||||
export default async function handler(req: Request, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return handleError(res, createError(405, "Method not allowed"));
|
||||
if (req.method !== 'POST') {
|
||||
return handleError(res, createError(405, 'Method not allowed'));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { db } from "@/server/db";
|
||||
import { handleError } from "@/server/exceptions";
|
||||
import { hashPassword } from "@/server/services/hash.service";
|
||||
import { randomUUID } from "crypto";
|
||||
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||
import { randomUUID } from 'crypto';
|
||||
import { db } from '@/server/db';
|
||||
import { handleError } from '@/server/exceptions';
|
||||
import { hashPassword } from '@/server/services/hash.service';
|
||||
import { type NextApiRequest, type NextApiResponse } from 'next';
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
try {
|
||||
const counts = await db.$transaction([
|
||||
db.organization.count(),
|
||||
@@ -13,25 +16,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
]);
|
||||
|
||||
if (counts.some((count) => count > 0)) {
|
||||
return res.json("Setup already done");
|
||||
return res.json('Setup already done');
|
||||
}
|
||||
|
||||
const organization = await db.organization.create({
|
||||
data: {
|
||||
name: "Acme Inc.",
|
||||
name: 'Acme Inc.',
|
||||
},
|
||||
});
|
||||
|
||||
const project = await db.project.create({
|
||||
data: {
|
||||
name: "Acme Website",
|
||||
name: 'Acme Website',
|
||||
organization_id: organization.id,
|
||||
},
|
||||
});
|
||||
const secret = randomUUID();
|
||||
const client = await db.client.create({
|
||||
data: {
|
||||
name: "Acme Website Client",
|
||||
name: 'Acme Website Client',
|
||||
project_id: project.id,
|
||||
organization_id: organization.id,
|
||||
secret: await hashPassword(secret),
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import { createNextApiHandler } from "@trpc/server/adapters/next";
|
||||
|
||||
import { env } from "@/env.mjs";
|
||||
import { appRouter } from "@/server/api/root";
|
||||
import { createTRPCContext } from "@/server/api/trpc";
|
||||
import { env } from '@/env.mjs';
|
||||
import { appRouter } from '@/server/api/root';
|
||||
import { createTRPCContext } from '@/server/api/trpc';
|
||||
import { createNextApiHandler } from '@trpc/server/adapters/next';
|
||||
|
||||
// export API handler
|
||||
export default createNextApiHandler({
|
||||
router: appRouter,
|
||||
createContext: createTRPCContext,
|
||||
onError:
|
||||
env.NODE_ENV === "development"
|
||||
env.NODE_ENV === 'development'
|
||||
? ({ path, error }) => {
|
||||
console.error(
|
||||
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`
|
||||
`❌ tRPC failed on ${path ?? '<no-path>'}: ${error.message}`
|
||||
);
|
||||
}
|
||||
: undefined,
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { MainLayout } from "@/components/layouts/MainLayout";
|
||||
import { api } from "@/utils/api";
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { createServerSideProps } from "@/server/getServerSideProps";
|
||||
import { useEffect } from 'react';
|
||||
import { MainLayout } from '@/components/layouts/MainLayout';
|
||||
import { createServerSideProps } from '@/server/getServerSideProps';
|
||||
import { api } from '@/utils/api';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
export const getServerSideProps = createServerSideProps()
|
||||
export const getServerSideProps = createServerSideProps();
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
const query = api.organization.first.useQuery();
|
||||
const organization = query.data ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if(organization) {
|
||||
router.replace(`/${organization.slug}`)
|
||||
if (organization) {
|
||||
router.replace(`/${organization.slug}`);
|
||||
}
|
||||
}, [organization])
|
||||
}, [organization, router]);
|
||||
|
||||
return (
|
||||
<MainLayout>
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import reportSlice from '@/components/report/reportSlice'
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import { type TypedUseSelectorHook, useDispatch as useBaseDispatch, useSelector as useBaseSelector } from 'react-redux'
|
||||
import reportSlice from '@/components/report/reportSlice';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import {
|
||||
useDispatch as useBaseDispatch,
|
||||
useSelector as useBaseSelector,
|
||||
type TypedUseSelectorHook,
|
||||
} from 'react-redux';
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
report: reportSlice
|
||||
report: reportSlice,
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
export const useDispatch: () => AppDispatch = useBaseDispatch
|
||||
export const useSelector: TypedUseSelectorHook<RootState> = useBaseSelector
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export const useDispatch: () => AppDispatch = useBaseDispatch;
|
||||
export const useSelector: TypedUseSelectorHook<RootState> = useBaseSelector;
|
||||
|
||||
|
||||
export default store
|
||||
export default store;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { createTRPCRouter } from "@/server/api/trpc";
|
||||
import { chartRouter } from "./routers/chart";
|
||||
import { reportRouter } from "./routers/report";
|
||||
import { organizationRouter } from "./routers/organization";
|
||||
import { userRouter } from "./routers/user";
|
||||
import { projectRouter } from "./routers/project";
|
||||
import { clientRouter } from "./routers/client";
|
||||
import { dashboardRouter } from "./routers/dashboard";
|
||||
import { eventRouter } from "./routers/event";
|
||||
import { profileRouter } from "./routers/profile";
|
||||
import { createTRPCRouter } from '@/server/api/trpc';
|
||||
|
||||
import { chartRouter } from './routers/chart';
|
||||
import { clientRouter } from './routers/client';
|
||||
import { dashboardRouter } from './routers/dashboard';
|
||||
import { eventRouter } from './routers/event';
|
||||
import { organizationRouter } from './routers/organization';
|
||||
import { profileRouter } from './routers/profile';
|
||||
import { projectRouter } from './routers/project';
|
||||
import { reportRouter } from './routers/report';
|
||||
import { userRouter } from './routers/user';
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { z } from "zod";
|
||||
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 { zChartInputWithDates } from "@/utils/validation";
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db } from '@/server/db';
|
||||
import {
|
||||
type IChartInputWithDates,
|
||||
type IChartEvent,
|
||||
type IChartInputWithDates,
|
||||
type IChartRange,
|
||||
} from "@/types";
|
||||
import { getDaysOldDate } from "@/utils/date";
|
||||
} from '@/types';
|
||||
import { getDaysOldDate } from '@/utils/date';
|
||||
import { toDots } from '@/utils/object';
|
||||
import { zChartInputWithDates } from '@/utils/validation';
|
||||
import { last, pipe, sort, uniq } from 'ramda';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
@@ -21,7 +21,7 @@ export const chartRouter = createTRPCRouter({
|
||||
events: protectedProcedure.query(async () => {
|
||||
const events = await db.event.findMany({
|
||||
take: 500,
|
||||
distinct: ["name"],
|
||||
distinct: ['name'],
|
||||
});
|
||||
|
||||
return events;
|
||||
@@ -47,12 +47,12 @@ export const chartRouter = createTRPCRouter({
|
||||
const dotNotation = toDots(properties);
|
||||
return [...acc, ...Object.keys(dotNotation)];
|
||||
}, [] as string[])
|
||||
.map((item) => item.replace(/\.([0-9]+)\./g, ".*."))
|
||||
.map((item) => item.replace(/\.([0-9]+)/g, "[*]"));
|
||||
.map((item) => item.replace(/\.([0-9]+)\./g, '.*.'))
|
||||
.map((item) => item.replace(/\.([0-9]+)/g, '[*]'));
|
||||
|
||||
return pipe(
|
||||
sort<string>((a, b) => a.length - b.length),
|
||||
uniq,
|
||||
uniq
|
||||
)(properties);
|
||||
}),
|
||||
|
||||
@@ -62,10 +62,10 @@ export const chartRouter = createTRPCRouter({
|
||||
if (isJsonPath(input.property)) {
|
||||
const events = await db.$queryRawUnsafe<{ value: string }[]>(
|
||||
`SELECT ${selectJsonPath(
|
||||
input.property,
|
||||
input.property
|
||||
)} AS value from events WHERE name = '${
|
||||
input.event
|
||||
}' AND "createdAt" >= NOW() - INTERVAL '30 days'`,
|
||||
}' AND "createdAt" >= NOW() - INTERVAL '30 days'`
|
||||
);
|
||||
return {
|
||||
values: uniq(events.map((item) => item.value)),
|
||||
@@ -102,12 +102,12 @@ export const chartRouter = createTRPCRouter({
|
||||
...(await getChartData({
|
||||
...input,
|
||||
event,
|
||||
})),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const sorted = [...series].sort((a, b) => {
|
||||
if (input.chartType === "linear") {
|
||||
if (input.chartType === 'linear') {
|
||||
const sumA = a.data.reduce((acc, item) => acc + item.count, 0);
|
||||
const sumB = b.data.reduce((acc, item) => acc + item.count, 0);
|
||||
return sumB - sumA;
|
||||
@@ -132,8 +132,8 @@ export const chartRouter = createTRPCRouter({
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<(typeof series)[number]["event"]["id"], number>,
|
||||
),
|
||||
{} as Record<(typeof series)[number]['event']['id'], number>
|
||||
)
|
||||
).map(([id, count]) => ({
|
||||
count,
|
||||
...events.find((event) => event.id === id)!,
|
||||
@@ -148,13 +148,13 @@ export const chartRouter = createTRPCRouter({
|
||||
|
||||
function selectJsonPath(property: string) {
|
||||
const jsonPath = property
|
||||
.replace(/^properties\./, "")
|
||||
.replace(/\.\*\./g, ".**.");
|
||||
.replace(/^properties\./, '')
|
||||
.replace(/\.\*\./g, '.**.');
|
||||
return `jsonb_path_query(properties, '$.${jsonPath}')`;
|
||||
}
|
||||
|
||||
function isJsonPath(property: string) {
|
||||
return property.startsWith("properties");
|
||||
return property.startsWith('properties');
|
||||
}
|
||||
|
||||
type ResultItem = {
|
||||
@@ -164,12 +164,12 @@ type ResultItem = {
|
||||
};
|
||||
|
||||
function propertyNameToSql(name: string) {
|
||||
if (name.includes(".")) {
|
||||
if (name.includes('.')) {
|
||||
const str = name
|
||||
.split(".")
|
||||
.split('.')
|
||||
.map((item, index) => (index === 0 ? item : `'${item}'`))
|
||||
.join("->");
|
||||
const findLastOf = "->";
|
||||
.join('->');
|
||||
const findLastOf = '->';
|
||||
const lastArrow = str.lastIndexOf(findLastOf);
|
||||
if (lastArrow === -1) {
|
||||
return str;
|
||||
@@ -224,27 +224,34 @@ function getDatesFromRange(range: IChartRange) {
|
||||
};
|
||||
}
|
||||
|
||||
function getChartSql({ event, chartType, breakdowns, interval, startDate, endDate }: Omit<IGetChartDataInput, 'range'>) {
|
||||
function getChartSql({
|
||||
event,
|
||||
chartType,
|
||||
breakdowns,
|
||||
interval,
|
||||
startDate,
|
||||
endDate,
|
||||
}: Omit<IGetChartDataInput, 'range'>) {
|
||||
const select = [];
|
||||
const where = [];
|
||||
const groupBy = [];
|
||||
const orderBy = [];
|
||||
|
||||
if (event.segment === "event") {
|
||||
if (event.segment === 'event') {
|
||||
select.push(`count(*)::int as count`);
|
||||
} else {
|
||||
select.push(`count(DISTINCT profile_id)::int as count`);
|
||||
}
|
||||
|
||||
switch (chartType) {
|
||||
case "bar": {
|
||||
orderBy.push("count DESC");
|
||||
case 'bar': {
|
||||
orderBy.push('count DESC');
|
||||
break;
|
||||
}
|
||||
case "linear": {
|
||||
case 'linear': {
|
||||
select.push(`date_trunc('${interval}', "createdAt") as date`);
|
||||
groupBy.push("date");
|
||||
orderBy.push("date");
|
||||
groupBy.push('date');
|
||||
orderBy.push('date');
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -256,38 +263,38 @@ function getChartSql({ event, chartType, breakdowns, interval, startDate, endDat
|
||||
filters.forEach((filter) => {
|
||||
const { name, value } = filter;
|
||||
switch (filter.operator) {
|
||||
case "is": {
|
||||
if (name.includes(".*.") || name.endsWith("[*]")) {
|
||||
case 'is': {
|
||||
if (name.includes('.*.') || name.endsWith('[*]')) {
|
||||
where.push(
|
||||
`properties @? '$.${name
|
||||
.replace(/^properties\./, "")
|
||||
.replace(/\.\*\./g, "[*].")} ? (${value
|
||||
.replace(/^properties\./, '')
|
||||
.replace(/\.\*\./g, '[*].')} ? (${value
|
||||
.map((val) => `@ == "${val}"`)
|
||||
.join(" || ")})'`,
|
||||
.join(' || ')})'`
|
||||
);
|
||||
} else {
|
||||
where.push(
|
||||
`${propertyNameToSql(name)} in (${value
|
||||
.map((val) => `'${val}'`)
|
||||
.join(", ")})`,
|
||||
.join(', ')})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "isNot": {
|
||||
if (name.includes(".*.") || name.endsWith("[*]")) {
|
||||
case 'isNot': {
|
||||
if (name.includes('.*.') || name.endsWith('[*]')) {
|
||||
where.push(
|
||||
`properties @? '$.${name
|
||||
.replace(/^properties\./, "")
|
||||
.replace(/\.\*\./g, "[*].")} ? (${value
|
||||
.replace(/^properties\./, '')
|
||||
.replace(/\.\*\./g, '[*].')} ? (${value
|
||||
.map((val) => `@ != "${val}"`)
|
||||
.join(" && ")})'`,
|
||||
.join(' && ')})'`
|
||||
);
|
||||
} else if (name.includes(".")) {
|
||||
} else if (name.includes('.')) {
|
||||
where.push(
|
||||
`${propertyNameToSql(name)} not in (${value
|
||||
.map((val) => `'${val}'`)
|
||||
.join(", ")})`,
|
||||
.join(', ')})`
|
||||
);
|
||||
}
|
||||
break;
|
||||
@@ -322,24 +329,24 @@ function getChartSql({ event, chartType, breakdowns, interval, startDate, endDat
|
||||
}
|
||||
|
||||
const sql = [
|
||||
`SELECT ${select.join(", ")}`,
|
||||
`SELECT ${select.join(', ')}`,
|
||||
`FROM events`,
|
||||
`WHERE ${where.join(" AND ")}`,
|
||||
`WHERE ${where.join(' AND ')}`,
|
||||
];
|
||||
|
||||
if (groupBy.length) {
|
||||
sql.push(`GROUP BY ${groupBy.join(", ")}`);
|
||||
sql.push(`GROUP BY ${groupBy.join(', ')}`);
|
||||
}
|
||||
if (orderBy.length) {
|
||||
sql.push(`ORDER BY ${orderBy.join(", ")}`);
|
||||
sql.push(`ORDER BY ${orderBy.join(', ')}`);
|
||||
}
|
||||
|
||||
return sql.join("\n");
|
||||
return sql.join('\n');
|
||||
}
|
||||
|
||||
type IGetChartDataInput = {
|
||||
event: IChartEvent;
|
||||
} & Omit<IChartInputWithDates, "events" | 'name'>
|
||||
} & Omit<IChartInputWithDates, 'events' | 'name'>;
|
||||
|
||||
async function getChartData({
|
||||
chartType,
|
||||
@@ -365,23 +372,24 @@ async function getChartData({
|
||||
interval,
|
||||
startDate,
|
||||
endDate,
|
||||
})
|
||||
});
|
||||
|
||||
let result = await db.$queryRawUnsafe<ResultItem[]>(sql);
|
||||
|
||||
if(result.length === 0 && breakdowns.length > 0) {
|
||||
result = await db.$queryRawUnsafe<ResultItem[]>(getChartSql({
|
||||
chartType,
|
||||
event,
|
||||
breakdowns: [],
|
||||
interval,
|
||||
startDate,
|
||||
endDate,
|
||||
}));
|
||||
if (result.length === 0 && breakdowns.length > 0) {
|
||||
result = await db.$queryRawUnsafe<ResultItem[]>(
|
||||
getChartSql({
|
||||
chartType,
|
||||
event,
|
||||
breakdowns: [],
|
||||
interval,
|
||||
startDate,
|
||||
endDate,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
console.log(sql);
|
||||
|
||||
|
||||
// group by sql label
|
||||
const series = result.reduce(
|
||||
@@ -401,7 +409,7 @@ async function getChartData({
|
||||
...acc,
|
||||
};
|
||||
},
|
||||
{} as Record<string, ResultItem[]>,
|
||||
{} as Record<string, ResultItem[]>
|
||||
);
|
||||
|
||||
return Object.keys(series).map((key) => {
|
||||
@@ -416,7 +424,7 @@ async function getChartData({
|
||||
},
|
||||
totalCount: getTotalCount(data),
|
||||
data:
|
||||
chartType === "linear"
|
||||
chartType === 'linear'
|
||||
? fillEmptySpotsInTimeline(data, interval, startDate, endDate).map(
|
||||
(item) => {
|
||||
return {
|
||||
@@ -424,7 +432,7 @@ async function getChartData({
|
||||
count: item.count,
|
||||
date: new Date(item.date).toISOString(),
|
||||
};
|
||||
},
|
||||
}
|
||||
)
|
||||
: [],
|
||||
};
|
||||
@@ -435,17 +443,17 @@ function fillEmptySpotsInTimeline(
|
||||
items: ResultItem[],
|
||||
interval: string,
|
||||
startDate: string,
|
||||
endDate: string,
|
||||
endDate: string
|
||||
) {
|
||||
const result = [];
|
||||
const clonedStartDate = new Date(startDate);
|
||||
const clonedEndDate = new Date(endDate);
|
||||
const today = new Date();
|
||||
|
||||
if(interval === 'minute') {
|
||||
clonedStartDate.setSeconds(0, 0)
|
||||
if (interval === 'minute') {
|
||||
clonedStartDate.setSeconds(0, 0);
|
||||
clonedEndDate.setMinutes(clonedEndDate.getMinutes() + 1, 0, 0);
|
||||
} else if (interval === "hour") {
|
||||
} else if (interval === 'hour') {
|
||||
clonedStartDate.setMinutes(0, 0, 0);
|
||||
clonedEndDate.setMinutes(0, 0, 0);
|
||||
} else {
|
||||
@@ -455,7 +463,7 @@ function fillEmptySpotsInTimeline(
|
||||
|
||||
// Force if interval is month and the start date is the same month as today
|
||||
const shouldForce = () =>
|
||||
interval === "month" &&
|
||||
interval === 'month' &&
|
||||
clonedStartDate.getFullYear() === today.getFullYear() &&
|
||||
clonedStartDate.getMonth() === today.getMonth();
|
||||
|
||||
@@ -472,20 +480,20 @@ function fillEmptySpotsInTimeline(
|
||||
const item = items.find((item) => {
|
||||
const date = new Date(item.date);
|
||||
|
||||
if (interval === "month") {
|
||||
if (interval === 'month') {
|
||||
return (
|
||||
getYear(date) === getYear(clonedStartDate) &&
|
||||
getMonth(date) === getMonth(clonedStartDate)
|
||||
);
|
||||
}
|
||||
if (interval === "day") {
|
||||
if (interval === 'day') {
|
||||
return (
|
||||
getYear(date) === getYear(clonedStartDate) &&
|
||||
getMonth(date) === getMonth(clonedStartDate) &&
|
||||
getDay(date) === getDay(clonedStartDate)
|
||||
);
|
||||
}
|
||||
if (interval === "hour") {
|
||||
if (interval === 'hour') {
|
||||
return (
|
||||
getYear(date) === getYear(clonedStartDate) &&
|
||||
getMonth(date) === getMonth(clonedStartDate) &&
|
||||
@@ -493,7 +501,7 @@ function fillEmptySpotsInTimeline(
|
||||
getHour(date) === getHour(clonedStartDate)
|
||||
);
|
||||
}
|
||||
if (interval === "minute") {
|
||||
if (interval === 'minute') {
|
||||
return (
|
||||
getYear(date) === getYear(clonedStartDate) &&
|
||||
getMonth(date) === getMonth(clonedStartDate) &&
|
||||
@@ -515,19 +523,19 @@ function fillEmptySpotsInTimeline(
|
||||
}
|
||||
|
||||
switch (interval) {
|
||||
case "day": {
|
||||
case 'day': {
|
||||
clonedStartDate.setDate(clonedStartDate.getDate() + 1);
|
||||
break;
|
||||
}
|
||||
case "hour": {
|
||||
case 'hour': {
|
||||
clonedStartDate.setHours(clonedStartDate.getHours() + 1);
|
||||
break;
|
||||
}
|
||||
case "minute": {
|
||||
case 'minute': {
|
||||
clonedStartDate.setMinutes(clonedStartDate.getMinutes() + 1);
|
||||
break;
|
||||
}
|
||||
case "month": {
|
||||
case 'month': {
|
||||
clonedStartDate.setMonth(clonedStartDate.getMonth() + 1);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import { hashPassword } from "@/server/services/hash.service";
|
||||
import { randomUUID } from "crypto";
|
||||
import { getOrganizationBySlug } from "@/server/services/organization.service";
|
||||
import { randomUUID } from 'crypto';
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db } from '@/server/db';
|
||||
import { hashPassword } from '@/server/services/hash.service';
|
||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const clientRouter = createTRPCRouter({
|
||||
list: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
organizationSlug: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const organization = await getOrganizationBySlug(input.organizationSlug);
|
||||
@@ -28,7 +27,7 @@ export const clientRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.query(({ input }) => {
|
||||
return db.client.findUniqueOrThrow({
|
||||
@@ -42,7 +41,7 @@ export const clientRouter = createTRPCRouter({
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(({ input }) => {
|
||||
return db.client.update({
|
||||
@@ -60,7 +59,7 @@ export const clientRouter = createTRPCRouter({
|
||||
name: z.string(),
|
||||
projectId: z.string(),
|
||||
organizationSlug: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const organization = await getOrganizationBySlug(input.organizationSlug);
|
||||
@@ -83,7 +82,7 @@ export const clientRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
await db.client.delete({
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import { getProjectBySlug } from "@/server/services/project.service";
|
||||
import { slug } from "@/utils/slug";
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db } from '@/server/db';
|
||||
import { getProjectBySlug } from '@/server/services/project.service';
|
||||
import { slug } from '@/utils/slug';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const dashboardRouter = createTRPCRouter({
|
||||
list: protectedProcedure
|
||||
@@ -15,12 +14,12 @@ export const dashboardRouter = createTRPCRouter({
|
||||
.or(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
)
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
let projectId = null;
|
||||
if ("projectId" in input) {
|
||||
if ('projectId' in input) {
|
||||
projectId = input.projectId;
|
||||
} else {
|
||||
projectId = (await getProjectBySlug(input.projectSlug)).id;
|
||||
@@ -37,7 +36,7 @@ export const dashboardRouter = createTRPCRouter({
|
||||
z.object({
|
||||
name: z.string(),
|
||||
projectId: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input: { projectId, name } }) => {
|
||||
return db.dashboard.create({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db } from '@/server/db';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
@@ -16,7 +16,7 @@ export const eventRouter = createTRPCRouter({
|
||||
take: z.number().default(100),
|
||||
skip: z.number().default(0),
|
||||
profileId: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { take, skip, projectSlug, profileId } }) => {
|
||||
const project = await db.project.findUniqueOrThrow({
|
||||
@@ -29,10 +29,10 @@ export const eventRouter = createTRPCRouter({
|
||||
skip,
|
||||
where: {
|
||||
project_id: project.id,
|
||||
profile_id: profileId
|
||||
profile_id: profileId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
createdAt: 'desc',
|
||||
},
|
||||
include: {
|
||||
profile: true,
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import { getOrganizationBySlug } from "@/server/services/organization.service";
|
||||
import { slug } from "@/utils/slug";
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db } from '@/server/db';
|
||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||
import { slug } from '@/utils/slug';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const organizationRouter = createTRPCRouter({
|
||||
first: protectedProcedure.query(({ ctx }) => {
|
||||
@@ -21,17 +20,17 @@ export const organizationRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
slug: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.query(({ input }) => {
|
||||
return getOrganizationBySlug(input.slug)
|
||||
return getOrganizationBySlug(input.slug);
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(({ input }) => {
|
||||
return db.organization.update({
|
||||
@@ -40,7 +39,7 @@ export const organizationRouter = createTRPCRouter({
|
||||
},
|
||||
data: {
|
||||
name: input.name,
|
||||
slug: slug(input.name)
|
||||
slug: slug(input.name),
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db } from '@/server/db';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
@@ -15,7 +15,7 @@ export const profileRouter = createTRPCRouter({
|
||||
projectSlug: z.string(),
|
||||
take: z.number().default(100),
|
||||
skip: z.number().default(0),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { take, skip, projectSlug } }) => {
|
||||
const project = await db.project.findUniqueOrThrow({
|
||||
@@ -30,7 +30,7 @@ export const profileRouter = createTRPCRouter({
|
||||
project_id: project.id,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import { getOrganizationBySlug } from "@/server/services/organization.service";
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db } from '@/server/db';
|
||||
import { getOrganizationBySlug } from '@/server/services/organization.service';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const projectRouter = createTRPCRouter({
|
||||
list: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
organizationSlug: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const organization = await getOrganizationBySlug(input.organizationSlug);
|
||||
@@ -23,7 +22,7 @@ export const projectRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.query(({ input }) => {
|
||||
return db.project.findUniqueOrThrow({
|
||||
@@ -37,7 +36,7 @@ export const projectRouter = createTRPCRouter({
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(({ input }) => {
|
||||
return db.project.update({
|
||||
@@ -54,7 +53,7 @@ export const projectRouter = createTRPCRouter({
|
||||
z.object({
|
||||
name: z.string(),
|
||||
organizationSlug: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const organization = await getOrganizationBySlug(input.organizationSlug);
|
||||
@@ -69,7 +68,7 @@ export const projectRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
await db.project.delete({
|
||||
|
||||
@@ -1,48 +1,54 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { zChartInput } from "@/utils/validation";
|
||||
import { db } from "@/server/db";
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db } from '@/server/db';
|
||||
import { getDashboardBySlug } from '@/server/services/dashboard.service';
|
||||
import { getProjectBySlug } from '@/server/services/project.service';
|
||||
import {
|
||||
type IChartInput,
|
||||
type IChartBreakdown,
|
||||
type IChartEvent,
|
||||
type IChartEventFilter,
|
||||
type IChartInput,
|
||||
type IChartRange,
|
||||
} from "@/types";
|
||||
import { type Report as DbReport } from "@prisma/client";
|
||||
import { getProjectBySlug } from "@/server/services/project.service";
|
||||
import { getDashboardBySlug } from "@/server/services/dashboard.service";
|
||||
import { alphabetIds } from "@/utils/constants";
|
||||
} from '@/types';
|
||||
import { alphabetIds } from '@/utils/constants';
|
||||
import { zChartInput } from '@/utils/validation';
|
||||
import { type Report as DbReport } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
|
||||
function transformFilter(filter: Partial<IChartEventFilter>, index: number): IChartEventFilter {
|
||||
function transformFilter(
|
||||
filter: Partial<IChartEventFilter>,
|
||||
index: number
|
||||
): IChartEventFilter {
|
||||
return {
|
||||
id: filter.id ?? alphabetIds[index]!,
|
||||
name: filter.name ?? 'Unknown Filter',
|
||||
operator: filter.operator ?? 'is',
|
||||
value: typeof filter.value === 'string' ? [filter.value] : filter.value ?? [],
|
||||
}
|
||||
value:
|
||||
typeof filter.value === 'string' ? [filter.value] : filter.value ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
function transformEvent(event: Partial<IChartEvent>, index: number): IChartEvent {
|
||||
function transformEvent(
|
||||
event: Partial<IChartEvent>,
|
||||
index: number
|
||||
): IChartEvent {
|
||||
return {
|
||||
segment: event.segment ?? 'event',
|
||||
filters: (event.filters ?? []).map(transformFilter),
|
||||
id: event.id ?? alphabetIds[index]!,
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
name: event.name || 'Untitled',
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function transformReport(report: DbReport): IChartInput & { id: string } {
|
||||
return {
|
||||
id: report.id,
|
||||
events: (report.events as IChartEvent[]).map(transformEvent),
|
||||
events: (report.events as IChartEvent[]).map(transformEvent),
|
||||
breakdowns: report.breakdowns as IChartBreakdown[],
|
||||
chartType: report.chart_type,
|
||||
interval: report.interval,
|
||||
name: report.name || 'Untitled',
|
||||
range: report.range as IChartRange ?? 30,
|
||||
range: (report.range as IChartRange) ?? 30,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,7 +57,7 @@ export const reportRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.query(({ input: { id } }) => {
|
||||
return db.report
|
||||
@@ -67,7 +73,7 @@ export const reportRouter = createTRPCRouter({
|
||||
z.object({
|
||||
projectSlug: z.string(),
|
||||
dashboardSlug: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.query(async ({ input: { projectSlug, dashboardSlug } }) => {
|
||||
const project = await getProjectBySlug(projectSlug);
|
||||
@@ -82,7 +88,7 @@ export const reportRouter = createTRPCRouter({
|
||||
return {
|
||||
reports: reports.map(transformReport),
|
||||
dashboard,
|
||||
}
|
||||
};
|
||||
}),
|
||||
save: protectedProcedure
|
||||
.input(
|
||||
@@ -90,7 +96,7 @@ export const reportRouter = createTRPCRouter({
|
||||
report: zChartInput,
|
||||
projectId: z.string(),
|
||||
dashboardId: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(({ input: { report, projectId, dashboardId } }) => {
|
||||
return db.report.create({
|
||||
@@ -105,7 +111,6 @@ export const reportRouter = createTRPCRouter({
|
||||
range: report.range,
|
||||
},
|
||||
});
|
||||
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(
|
||||
@@ -114,7 +119,7 @@ export const reportRouter = createTRPCRouter({
|
||||
report: zChartInput,
|
||||
projectId: z.string(),
|
||||
dashboardId: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(({ input: { report, projectId, dashboardId, reportId } }) => {
|
||||
return db.report.update({
|
||||
|
||||
@@ -1,63 +1,59 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
} from "@/server/api/trpc";
|
||||
import { db } from "@/server/db";
|
||||
import { hashPassword, verifyPassword } from "@/server/services/hash.service";
|
||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||
import { db } from '@/server/db';
|
||||
import { hashPassword, verifyPassword } from '@/server/services/hash.service';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
current: protectedProcedure.query(({ ctx }) => {
|
||||
return db.user.findUniqueOrThrow({
|
||||
where: {
|
||||
id: ctx.session.user.id
|
||||
}
|
||||
})
|
||||
id: ctx.session.user.id,
|
||||
},
|
||||
});
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
email: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(({ input, ctx }) => {
|
||||
return db.user.update({
|
||||
where: {
|
||||
id: ctx.session.user.id
|
||||
id: ctx.session.user.id,
|
||||
},
|
||||
data: {
|
||||
name: input.name,
|
||||
email: input.email,
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
}),
|
||||
changePassword: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
password: z.string(),
|
||||
oldPassword: z.string(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const user = await db.user.findUniqueOrThrow({
|
||||
where: {
|
||||
id: ctx.session.user.id
|
||||
}
|
||||
})
|
||||
id: ctx.session.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if(!(await verifyPassword(input.oldPassword, user.password))) {
|
||||
throw new Error('Old password is incorrect')
|
||||
if (!(await verifyPassword(input.oldPassword, user.password))) {
|
||||
throw new Error('Old password is incorrect');
|
||||
}
|
||||
|
||||
|
||||
return db.user.update({
|
||||
where: {
|
||||
id: ctx.session.user.id
|
||||
id: ctx.session.user.id,
|
||||
},
|
||||
data: {
|
||||
password: await hashPassword(input.password),
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -7,14 +7,13 @@
|
||||
* need to use are documented accordingly near the end.
|
||||
*/
|
||||
|
||||
import { initTRPC, TRPCError } from "@trpc/server";
|
||||
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||
import { type Session } from "next-auth";
|
||||
import superjson from "superjson";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { getServerAuthSession } from "@/server/auth";
|
||||
import { db } from "@/server/db";
|
||||
import { getServerAuthSession } from '@/server/auth';
|
||||
import { db } from '@/server/db';
|
||||
import { initTRPC, TRPCError } from '@trpc/server';
|
||||
import { type CreateNextContextOptions } from '@trpc/server/adapters/next';
|
||||
import { type Session } from 'next-auth';
|
||||
import superjson from 'superjson';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
/**
|
||||
* 1. CONTEXT
|
||||
@@ -109,7 +108,7 @@ export const publicProcedure = t.procedure;
|
||||
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
||||
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
||||
if (!ctx.session?.user) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { type NextApiRequest, type GetServerSidePropsContext } from "next";
|
||||
import { db } from '@/server/db';
|
||||
import { verifyPassword } from '@/server/services/hash.service';
|
||||
import { type GetServerSidePropsContext, type NextApiRequest } from 'next';
|
||||
import {
|
||||
getServerSession,
|
||||
type DefaultSession,
|
||||
type NextAuthOptions,
|
||||
} from "next-auth";
|
||||
} from 'next-auth';
|
||||
import Credentials from 'next-auth/providers/credentials';
|
||||
|
||||
import { db } from "@/server/db";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import { createError } from "./exceptions";
|
||||
import { hashPassword, verifyPassword } from "@/server/services/hash.service";
|
||||
import { createError } from './exceptions';
|
||||
|
||||
/**
|
||||
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
|
||||
@@ -16,9 +16,9 @@ import { hashPassword, verifyPassword } from "@/server/services/hash.service";
|
||||
*
|
||||
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
|
||||
*/
|
||||
declare module "next-auth" {
|
||||
declare module 'next-auth' {
|
||||
interface Session extends DefaultSession {
|
||||
user: DefaultSession["user"] & {
|
||||
user: DefaultSession['user'] & {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
@@ -45,35 +45,35 @@ export const authOptions: NextAuthOptions = {
|
||||
}),
|
||||
},
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
strategy: 'jwt',
|
||||
},
|
||||
providers: [
|
||||
Credentials({
|
||||
name: "Credentials",
|
||||
name: 'Credentials',
|
||||
credentials: {
|
||||
email: { label: "Email", type: "text", placeholder: "jsmith" },
|
||||
password: { label: "Password", type: "password" },
|
||||
email: { label: 'Email', type: 'text', placeholder: 'jsmith' },
|
||||
password: { label: 'Password', type: 'password' },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if(!credentials?.password || !credentials?.email) {
|
||||
return null
|
||||
if (!credentials?.password || !credentials?.email) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await db.user.findFirst({
|
||||
where: { email: credentials?.email },
|
||||
});
|
||||
|
||||
if(!user) {
|
||||
return null
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!await verifyPassword(credentials.password, user.password)) {
|
||||
return null
|
||||
|
||||
if (!(await verifyPassword(credentials.password, user.password))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...user,
|
||||
image: 'https://api.dicebear.com/7.x/adventurer/svg?seed=Abby'
|
||||
image: 'https://api.dicebear.com/7.x/adventurer/svg?seed=Abby',
|
||||
};
|
||||
},
|
||||
}),
|
||||
@@ -95,22 +95,22 @@ export const authOptions: NextAuthOptions = {
|
||||
* @see https://next-auth.js.org/configuration/nextjs
|
||||
*/
|
||||
export const getServerAuthSession = (ctx: {
|
||||
req: GetServerSidePropsContext["req"];
|
||||
res: GetServerSidePropsContext["res"];
|
||||
req: GetServerSidePropsContext['req'];
|
||||
res: GetServerSidePropsContext['res'];
|
||||
}) => {
|
||||
return getServerSession(ctx.req, ctx.res, authOptions);
|
||||
};
|
||||
|
||||
export async function validateSdkRequest(req: NextApiRequest): Promise<string> {
|
||||
const clientId = req?.headers["mixan-client-id"] as string | undefined
|
||||
const clientSecret = req.headers["mixan-client-secret"] as string | undefined
|
||||
|
||||
const clientId = req?.headers['mixan-client-id'] as string | undefined;
|
||||
const clientSecret = req.headers['mixan-client-secret'] as string | undefined;
|
||||
|
||||
if (!clientId) {
|
||||
throw createError(401, "Misisng client id");
|
||||
throw createError(401, 'Misisng client id');
|
||||
}
|
||||
|
||||
if (!clientSecret) {
|
||||
throw createError(401, "Misisng client secret");
|
||||
throw createError(401, 'Misisng client secret');
|
||||
}
|
||||
|
||||
const client = await db.client.findUnique({
|
||||
@@ -120,14 +120,12 @@ export async function validateSdkRequest(req: NextApiRequest): Promise<string> {
|
||||
});
|
||||
|
||||
if (!client) {
|
||||
throw createError(401, "Invalid client id");
|
||||
throw createError(401, 'Invalid client id');
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!(await verifyPassword(clientSecret, client.secret))) {
|
||||
throw createError(401, "Invalid client secret");
|
||||
throw createError(401, 'Invalid client secret');
|
||||
}
|
||||
|
||||
return client.project_id
|
||||
return client.project_id;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
import { env } from "@/env.mjs";
|
||||
import { env } from '@/env.mjs';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
@@ -9,8 +8,7 @@ const globalForPrisma = globalThis as unknown as {
|
||||
export const db =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log:
|
||||
['error']
|
||||
log: ['error'],
|
||||
});
|
||||
|
||||
if (env.NODE_ENV !== "production") globalForPrisma.prisma = db;
|
||||
if (env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
|
||||
|
||||
@@ -1,55 +1,52 @@
|
||||
import {
|
||||
type MixanIssue,
|
||||
type MixanErrorResponse
|
||||
} from '@mixan/types'
|
||||
import { type NextApiResponse } from 'next'
|
||||
import { type NextApiResponse } from 'next';
|
||||
|
||||
import { type MixanErrorResponse, type MixanIssue } from '@mixan/types';
|
||||
|
||||
export class HttpError extends Error {
|
||||
public status: number
|
||||
public message: string
|
||||
public issues: MixanIssue[]
|
||||
public status: number;
|
||||
public message: string;
|
||||
public issues: MixanIssue[];
|
||||
|
||||
constructor(status: number, message: string | Error, issues?: MixanIssue[]) {
|
||||
super(message instanceof Error ? message.message : message)
|
||||
this.status = status
|
||||
this.message = message instanceof Error ? message.message : message
|
||||
this.issues = issues ?? []
|
||||
super(message instanceof Error ? message.message : message);
|
||||
this.status = status;
|
||||
this.message = message instanceof Error ? message.message : message;
|
||||
this.issues = issues ?? [];
|
||||
}
|
||||
|
||||
toJson(): MixanErrorResponse {
|
||||
toJson(): MixanErrorResponse {
|
||||
return {
|
||||
code: this.status,
|
||||
status: 'error',
|
||||
message: this.message,
|
||||
issues: this.issues.length ? this.issues : undefined,
|
||||
stack: process.env.NODE_ENV !== 'production' ? this.stack : undefined,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function createIssues(arr: Array<MixanIssue>) {
|
||||
throw new HttpError(400, 'Issues', arr)
|
||||
throw new HttpError(400, 'Issues', arr);
|
||||
}
|
||||
|
||||
export function createError(status = 500, error: unknown) {
|
||||
if(error instanceof Error || typeof error === 'string') {
|
||||
return new HttpError(status, error)
|
||||
}
|
||||
if (error instanceof Error || typeof error === 'string') {
|
||||
return new HttpError(status, error);
|
||||
}
|
||||
|
||||
return new HttpError(500, 'Unexpected error occured')
|
||||
return new HttpError(500, 'Unexpected error occured');
|
||||
}
|
||||
|
||||
export function handleError(res: NextApiResponse, error: unknown) {
|
||||
if(error instanceof HttpError) {
|
||||
return res.status(error.status).json(error.toJson())
|
||||
}
|
||||
|
||||
if(error instanceof Error) {
|
||||
const httpError = createError(500, error)
|
||||
res.status(httpError.status).json(httpError.toJson())
|
||||
if (error instanceof HttpError) {
|
||||
return res.status(error.status).json(error.toJson());
|
||||
}
|
||||
|
||||
|
||||
const httpError = createError(500, error)
|
||||
res.status(httpError.status).json(httpError.toJson())
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
const httpError = createError(500, error);
|
||||
res.status(httpError.status).json(httpError.toJson());
|
||||
}
|
||||
|
||||
const httpError = createError(500, error);
|
||||
res.status(httpError.status).json(httpError.toJson());
|
||||
}
|
||||
|
||||
@@ -1,45 +1,46 @@
|
||||
import {
|
||||
type GetServerSidePropsContext,
|
||||
type GetServerSidePropsResult,
|
||||
} from "next";
|
||||
import { getServerAuthSession } from "./auth";
|
||||
import { db } from "./db";
|
||||
} from 'next';
|
||||
|
||||
import { getServerAuthSession } from './auth';
|
||||
import { db } from './db';
|
||||
|
||||
export function createServerSideProps(
|
||||
cb?: (context: GetServerSidePropsContext) => Promise<any>,
|
||||
cb?: (context: GetServerSidePropsContext) => Promise<any>
|
||||
) {
|
||||
return async function getServerSideProps(
|
||||
context: GetServerSidePropsContext,
|
||||
context: GetServerSidePropsContext
|
||||
): Promise<GetServerSidePropsResult<any>> {
|
||||
const session = await getServerAuthSession(context);
|
||||
const session = await getServerAuthSession(context);
|
||||
|
||||
if(!session) {
|
||||
if (!session) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: "/api/auth/signin",
|
||||
destination: '/api/auth/signin',
|
||||
permanent: false,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if(context.params?.organization) {
|
||||
if (context.params?.organization) {
|
||||
const organization = await db.user.findFirst({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
organization: {
|
||||
slug: context.params.organization as string
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if(!organization) {
|
||||
slug: context.params.organization as string,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
return {
|
||||
notFound: true,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const res = await (typeof cb === "function"
|
||||
const res = await (typeof cb === 'function'
|
||||
? cb(context)
|
||||
: Promise.resolve({}));
|
||||
return {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { db } from "../db";
|
||||
import { db } from '../db';
|
||||
|
||||
export function getDashboardBySlug(slug: string) {
|
||||
return db.dashboard.findUniqueOrThrow({
|
||||
where: {
|
||||
slug
|
||||
slug,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
|
||||
import { randomBytes, scrypt, timingSafeEqual } from 'crypto';
|
||||
|
||||
const keyLength = 32;
|
||||
/**
|
||||
@@ -6,17 +6,17 @@ const keyLength = 32;
|
||||
* @param {string} password
|
||||
* @returns {string} The salt+hash
|
||||
*/
|
||||
export async function hashPassword (password: string): Promise<string> {
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// generate random 16 bytes long salt - recommended by NodeJS Docs
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const salt = randomBytes(16).toString('hex');
|
||||
scrypt(password, salt, keyLength, (err, derivedKey) => {
|
||||
if (err) reject(err);
|
||||
// derivedKey is of type Buffer
|
||||
resolve(`${salt}.${derivedKey.toString("hex")}`);
|
||||
resolve(`${salt}.${derivedKey.toString('hex')}`);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare a plain text password with a salt+hash password
|
||||
@@ -24,11 +24,14 @@ export async function hashPassword (password: string): Promise<string> {
|
||||
* @param {string} hash The hash+salt to check against
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export async function verifyPassword (password: string, hash: string): Promise<boolean> {
|
||||
export async function verifyPassword(
|
||||
password: string,
|
||||
hash: string
|
||||
): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const [salt, hashKey] = hash.split(".");
|
||||
const [salt, hashKey] = hash.split('.');
|
||||
// we need to pass buffer values to timingSafeEqual
|
||||
const hashKeyBuff = Buffer.from(hashKey!, "hex");
|
||||
const hashKeyBuff = Buffer.from(hashKey!, 'hex');
|
||||
scrypt(password, salt!, keyLength, (err, derivedKey) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
@@ -37,4 +40,4 @@ export async function verifyPassword (password: string, hash: string): Promise<b
|
||||
resolve(timingSafeEqual(hashKeyBuff, derivedKey));
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { db } from "../db";
|
||||
import { db } from '../db';
|
||||
|
||||
export function getOrganizationBySlug(slug: string) {
|
||||
return db.organization.findUniqueOrThrow({
|
||||
where: {
|
||||
slug
|
||||
slug,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { db } from "@/server/db"
|
||||
import { HttpError } from "@/server/exceptions"
|
||||
import { db } from '@/server/db';
|
||||
import { HttpError } from '@/server/exceptions';
|
||||
|
||||
export function getProfile(id: string) {
|
||||
return db.profile.findUniqueOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export async function tickProfileProperty({
|
||||
@@ -14,28 +14,31 @@ export async function tickProfileProperty({
|
||||
tick,
|
||||
name,
|
||||
}: {
|
||||
profileId: string
|
||||
tick: number
|
||||
name: string
|
||||
profileId: string;
|
||||
tick: number;
|
||||
name: string;
|
||||
}) {
|
||||
const profile = await getProfile(profileId)
|
||||
const profile = await getProfile(profileId);
|
||||
|
||||
if (!profile) {
|
||||
throw new HttpError(404, `Profile not found ${profileId}`)
|
||||
throw new HttpError(404, `Profile not found ${profileId}`);
|
||||
}
|
||||
|
||||
const properties = (
|
||||
typeof profile.properties === 'object' ? profile.properties ?? {} : {}
|
||||
) as Record<string, number>
|
||||
const value = name in properties ? properties[name] : 0
|
||||
) as Record<string, number>;
|
||||
const value = name in properties ? properties[name] : 0;
|
||||
|
||||
if (typeof value !== 'number') {
|
||||
throw new HttpError(400, `Property "${name}" on user is of type ${typeof value}`)
|
||||
throw new HttpError(
|
||||
400,
|
||||
`Property "${name}" on user is of type ${typeof value}`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (typeof tick !== 'number') {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
throw new HttpError(400, `Value is not a number ${tick} (${typeof tick})`)
|
||||
throw new HttpError(400, `Value is not a number ${tick} (${typeof tick})`);
|
||||
}
|
||||
|
||||
await db.profile.update({
|
||||
@@ -48,5 +51,5 @@ export async function tickProfileProperty({
|
||||
[name]: value + tick,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { db } from "../db";
|
||||
import { db } from '../db';
|
||||
|
||||
export function getProjectBySlug(slug: string) {
|
||||
return db.project.findUniqueOrThrow({
|
||||
where: {
|
||||
slug
|
||||
slug,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
import { type RouterOutputs } from "@/utils/api";
|
||||
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";
|
||||
import { type RouterOutputs } from '@/utils/api';
|
||||
import { type timeRanges } from '@/utils/constants';
|
||||
import {
|
||||
type zChartBreakdown,
|
||||
type zChartEvent,
|
||||
type zChartInput,
|
||||
type zChartInputWithDates,
|
||||
type zChartType,
|
||||
type zTimeInterval,
|
||||
} from '@/utils/validation';
|
||||
import { type Client, type Project } from '@prisma/client';
|
||||
import { type TooltipProps } from 'recharts';
|
||||
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]
|
||||
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 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];
|
||||
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>
|
||||
}
|
||||
payload?: Array<T>;
|
||||
};
|
||||
|
||||
export type IProject = Project
|
||||
export type IProject = Project;
|
||||
export type IClientWithProject = Client & {
|
||||
project: IProject
|
||||
}
|
||||
project: IProject;
|
||||
};
|
||||
|
||||
@@ -4,16 +4,15 @@
|
||||
*
|
||||
* We also create a few inference helpers for input and output types.
|
||||
*/
|
||||
import { type TRPCClientErrorBase, httpLink, loggerLink } from "@trpc/client";
|
||||
import { createTRPCNext } from "@trpc/next";
|
||||
import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
|
||||
import superjson from "superjson";
|
||||
|
||||
import { type AppRouter } from "@/server/api/root";
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
import { toast } from '@/components/ui/use-toast';
|
||||
import { type AppRouter } from '@/server/api/root';
|
||||
import { httpLink, loggerLink, type TRPCClientErrorBase } from '@trpc/client';
|
||||
import { createTRPCNext } from '@trpc/next';
|
||||
import { type inferRouterInputs, type inferRouterOutputs } from '@trpc/server';
|
||||
import superjson from 'superjson';
|
||||
|
||||
const getBaseUrl = () => {
|
||||
if (typeof window !== "undefined") return ""; // browser should use relative url
|
||||
if (typeof window !== 'undefined') return ''; // browser should use relative url
|
||||
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
|
||||
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
|
||||
};
|
||||
@@ -28,9 +27,9 @@ export const api = createTRPCNext<AppRouter>({
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
enabled: typeof window !== "undefined",
|
||||
enabled: typeof window !== 'undefined',
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Transformer used for data de-serialization from the server.
|
||||
@@ -47,8 +46,8 @@ export const api = createTRPCNext<AppRouter>({
|
||||
links: [
|
||||
loggerLink({
|
||||
enabled: (opts) =>
|
||||
process.env.NODE_ENV === "development" ||
|
||||
(opts.direction === "down" && opts.result instanceof Error),
|
||||
process.env.NODE_ENV === 'development' ||
|
||||
(opts.direction === 'down' && opts.result instanceof Error),
|
||||
}),
|
||||
httpLink({
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
@@ -78,10 +77,9 @@ export type RouterInputs = inferRouterInputs<AppRouter>;
|
||||
*/
|
||||
export type RouterOutputs = inferRouterOutputs<AppRouter>;
|
||||
|
||||
|
||||
export function handleError(error: TRPCClientErrorBase<any>) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.message,
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import { toast } from '@/components/ui/use-toast';
|
||||
|
||||
export function clipboard(value: string | number) {
|
||||
navigator.clipboard.writeText(value.toString())
|
||||
navigator.clipboard.writeText(value.toString());
|
||||
toast({
|
||||
title: "Copied to clipboard",
|
||||
title: 'Copied to clipboard',
|
||||
description: value.toString(),
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
export const operators = {
|
||||
is: "Is",
|
||||
isNot: "Is not",
|
||||
contains: "Contains",
|
||||
doesNotContain: "Not contains",
|
||||
is: 'Is',
|
||||
isNot: 'Is not',
|
||||
contains: 'Contains',
|
||||
doesNotContain: 'Not contains',
|
||||
};
|
||||
|
||||
export const chartTypes = {
|
||||
linear: "Linear",
|
||||
bar: "Bar",
|
||||
pie: "Pie",
|
||||
metric: "Metric",
|
||||
area: "Area",
|
||||
linear: 'Linear',
|
||||
bar: 'Bar',
|
||||
pie: 'Pie',
|
||||
metric: 'Metric',
|
||||
area: 'Area',
|
||||
};
|
||||
|
||||
export const intervals = {
|
||||
minute: "Minute",
|
||||
day: "Day",
|
||||
hour: "Hour",
|
||||
month: "Month",
|
||||
minute: 'Minute',
|
||||
day: 'Day',
|
||||
hour: 'Hour',
|
||||
month: 'Month',
|
||||
};
|
||||
|
||||
export const alphabetIds = [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G",
|
||||
"H",
|
||||
"I",
|
||||
"J",
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
'E',
|
||||
'F',
|
||||
'G',
|
||||
'H',
|
||||
'I',
|
||||
'J',
|
||||
] as const;
|
||||
|
||||
export const timeRanges = [
|
||||
{ range: 0.3, title: "30m" },
|
||||
{ range: 0.6, title: "1h" },
|
||||
{ range: 0, title: "Today" },
|
||||
{ range: 1, title: "24h" },
|
||||
{ range: 7, title: "7d" },
|
||||
{ range: 14, title: "14d" },
|
||||
{ range: 30, title: "30d" },
|
||||
{ range: 90, title: "3mo" },
|
||||
{ range: 180, title: "6mo" },
|
||||
{ range: 365, title: "1y" },
|
||||
{ range: 0.3, title: '30m' },
|
||||
{ range: 0.6, title: '1h' },
|
||||
{ range: 0, title: 'Today' },
|
||||
{ range: 1, title: '24h' },
|
||||
{ range: 7, title: '7d' },
|
||||
{ range: 14, title: '14d' },
|
||||
{ range: 30, title: '30d' },
|
||||
{ range: 90, title: '3mo' },
|
||||
{ range: 180, title: '6mo' },
|
||||
{ range: 365, title: '1y' },
|
||||
] as const;
|
||||
|
||||
@@ -10,11 +10,11 @@ export function dateDifferanceInDays(date1: Date, date2: Date) {
|
||||
}
|
||||
|
||||
export function getLocale() {
|
||||
if (typeof navigator === "undefined") {
|
||||
return "en-US";
|
||||
if (typeof navigator === 'undefined') {
|
||||
return 'en-US';
|
||||
}
|
||||
|
||||
return navigator.language ?? "en-US";
|
||||
return navigator.language ?? 'en-US';
|
||||
}
|
||||
|
||||
export function formatDate(date: Date) {
|
||||
@@ -23,10 +23,10 @@ export function formatDate(date: Date) {
|
||||
|
||||
export function formatDateTime(date: Date) {
|
||||
return new Intl.DateTimeFormat(getLocale(), {
|
||||
day: "numeric",
|
||||
month: "numeric",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
day: 'numeric',
|
||||
month: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { type IChartRange } from "@/types";
|
||||
import { timeRanges } from "./constants";
|
||||
import { type IChartRange } from '@/types';
|
||||
|
||||
import { timeRanges } from './constants';
|
||||
|
||||
export function getRangeLabel(range: IChartRange) {
|
||||
return timeRanges.find(
|
||||
(item) => item.range === range,
|
||||
)?.title ?? null
|
||||
}
|
||||
return timeRanges.find((item) => item.range === range)?.title ?? null;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export function toDots(
|
||||
obj: Record<string, unknown>,
|
||||
path = "",
|
||||
path = ''
|
||||
): Record<string, number | string | boolean> {
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
if (typeof value === "object" && value !== null) {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return {
|
||||
...acc,
|
||||
...toDots(value as Record<string, unknown>, `${path}${key}.`),
|
||||
@@ -15,4 +15,4 @@ export function toDots(
|
||||
[`${path}${key}`]: value,
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _slugify from 'slugify'
|
||||
import _slugify from 'slugify';
|
||||
|
||||
const slugify = (str: string) => {
|
||||
return _slugify(
|
||||
@@ -9,10 +9,10 @@ const slugify = (str: string) => {
|
||||
.replace('Å', 'A')
|
||||
.replace('Ä', 'A')
|
||||
.replace('Ö', 'O'),
|
||||
{ lower: true, strict: true, trim: true },
|
||||
)
|
||||
}
|
||||
{ lower: true, strict: true, trim: true }
|
||||
);
|
||||
};
|
||||
|
||||
export function slug(str: string): string {
|
||||
return slugify(str)
|
||||
return slugify(str);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import resolveConfig from "tailwindcss/resolveConfig";
|
||||
import tailwinConfig from "../../tailwind.config";
|
||||
import resolveConfig from 'tailwindcss/resolveConfig';
|
||||
|
||||
import tailwinConfig from '../../tailwind.config';
|
||||
|
||||
// @ts-expect-error
|
||||
const config = resolveConfig(tailwinConfig);
|
||||
|
||||
@@ -7,7 +9,7 @@ export const theme = config.theme as any;
|
||||
|
||||
export function getChartColor(index: number): string {
|
||||
const chartColors: string[] = Object.keys(theme?.colors ?? {})
|
||||
.filter((key) => key.startsWith("chart-"))
|
||||
.filter((key) => key.startsWith('chart-'))
|
||||
.map((key) => theme.colors[key] as string);
|
||||
|
||||
return chartColors[index % chartColors.length]!;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { operators, chartTypes, intervals } from "./constants";
|
||||
import { z } from 'zod';
|
||||
|
||||
import { chartTypes, intervals, operators } from './constants';
|
||||
|
||||
function objectToZodEnums<K extends string>(obj: Record<K, any>): [K, ...K[]] {
|
||||
const [firstKey, ...otherKeys] = Object.keys(obj) as K[];
|
||||
@@ -9,14 +10,14 @@ function objectToZodEnums<K extends string>(obj: Record<K, any>): [K, ...K[]] {
|
||||
export const zChartEvent = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
segment: z.enum(["event", "user"]),
|
||||
segment: z.enum(['event', 'user']),
|
||||
filters: z.array(
|
||||
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())),
|
||||
}),
|
||||
})
|
||||
),
|
||||
});
|
||||
export const zChartBreakdown = z.object({
|
||||
|
||||
@@ -1,72 +1,70 @@
|
||||
const colors = [
|
||||
"#7856ff",
|
||||
"#ff7557",
|
||||
"#7fe1d8",
|
||||
"#f8bc3c",
|
||||
"#b3596e",
|
||||
"#72bef4",
|
||||
"#ffb27a",
|
||||
"#0f7ea0",
|
||||
"#3ba974",
|
||||
"#febbb2",
|
||||
"#cb80dc",
|
||||
"#5cb7af",
|
||||
'#7856ff',
|
||||
'#ff7557',
|
||||
'#7fe1d8',
|
||||
'#f8bc3c',
|
||||
'#b3596e',
|
||||
'#72bef4',
|
||||
'#ffb27a',
|
||||
'#0f7ea0',
|
||||
'#3ba974',
|
||||
'#febbb2',
|
||||
'#cb80dc',
|
||||
'#5cb7af',
|
||||
];
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
const config = {
|
||||
safelist: [
|
||||
...colors.map((color) => `chart-${color}`),
|
||||
],
|
||||
darkMode: ["class"],
|
||||
safelist: [...colors.map((color) => `chart-${color}`)],
|
||||
darkMode: ['class'],
|
||||
content: [
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
"./app/**/*.{ts,tsx}",
|
||||
"./src/**/*.{ts,tsx}",
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
padding: '2rem',
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
'2xl': '1400px',
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
...colors.reduce((acc, color, index) => {
|
||||
return {
|
||||
@@ -76,30 +74,30 @@ const config = {
|
||||
}, {}),
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
boxShadow: {
|
||||
DEFAULT: '0 5px 10px rgb(0 0 0 / 5%)'
|
||||
DEFAULT: '0 5px 10px rgb(0 0 0 / 5%)',
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
'accordion-down': {
|
||||
from: { height: 0 },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: 'var(--radix-accordion-content-height)' },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
'accordion-up': {
|
||||
from: { height: 'var(--radix-accordion-content-height)' },
|
||||
to: { height: 0 },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
plugins: [require('tailwindcss-animate')],
|
||||
};
|
||||
|
||||
export default config
|
||||
export default config;
|
||||
|
||||
18
package.json
18
package.json
@@ -1,19 +1,25 @@
|
||||
{
|
||||
"name": "@mixan/root",
|
||||
"version": "1.0.0",
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"author": "Carl-Gerhard Lindesvärd",
|
||||
"packageManager": "pnpm@8.7.6",
|
||||
"license": "ISC",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "pnpm -r dev"
|
||||
"dev": "pnpm -r dev",
|
||||
"format": "pnpm -r format --cache --cache-location=\"node_modules/.cache/.prettiercache\"",
|
||||
"format:fix": "pnpm -r format --write --cache --cache-location=\"node_modules/.cache/.prettiercache\"",
|
||||
"lint": "pnpm -r lint",
|
||||
"lint:fix": "pnpm -r lint --fix",
|
||||
"lint:workspace": "pnpm dlx sherif@latest",
|
||||
"typecheck": "pnpm -r typecheck"
|
||||
},
|
||||
"devDependencies": {
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
"typescript": "^5.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
import {
|
||||
import type {
|
||||
EventPayload,
|
||||
MixanErrorResponse,
|
||||
ProfilePayload,
|
||||
} from '@mixan/types'
|
||||
} from '@mixan/types';
|
||||
|
||||
type NewMixanOptions = {
|
||||
url: string
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
batchInterval?: number
|
||||
maxBatchSize?: number
|
||||
sessionTimeout?: number
|
||||
verbose?: boolean
|
||||
saveProfileId: (profiId: string) => void
|
||||
getProfileId: () => (string | null)
|
||||
removeProfileId: () => void
|
||||
interface NewMixanOptions {
|
||||
url: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
batchInterval?: number;
|
||||
maxBatchSize?: number;
|
||||
sessionTimeout?: number;
|
||||
verbose?: boolean;
|
||||
saveProfileId: (profiId: string) => void;
|
||||
getProfileId: () => string | null;
|
||||
removeProfileId: () => void;
|
||||
}
|
||||
type MixanOptions = Required<NewMixanOptions>
|
||||
type MixanOptions = Required<NewMixanOptions>;
|
||||
|
||||
class Fetcher {
|
||||
private url: string
|
||||
private clientId: string
|
||||
private clientSecret: string
|
||||
private logger: (...args: any[]) => void
|
||||
private url: string;
|
||||
private clientId: string;
|
||||
private clientSecret: string;
|
||||
private logger: (...args: any[]) => void;
|
||||
|
||||
constructor(options: MixanOptions) {
|
||||
this.url = options.url
|
||||
this.clientId = options.clientId
|
||||
this.clientSecret = options.clientSecret
|
||||
this.logger = options.verbose ? console.log : () => {}
|
||||
this.url = options.url;
|
||||
this.clientId = options.clientId;
|
||||
this.clientSecret = options.clientSecret;
|
||||
this.logger = options.verbose ? console.log : () => {};
|
||||
}
|
||||
|
||||
post<Response extends unknown>(
|
||||
post<PostData, PostResponse>(
|
||||
path: string,
|
||||
data: Record<string, any> = {},
|
||||
options: RequestInit = {}
|
||||
): Promise<Response | null> {
|
||||
const url = `${this.url}${path}`
|
||||
this.logger(`Mixan request: ${url}`, JSON.stringify(data, null, 2))
|
||||
data?: PostData,
|
||||
options?: RequestInit
|
||||
): Promise<PostResponse | null> {
|
||||
const url = `${this.url}${path}`;
|
||||
this.logger(`Mixan request: ${url}`, JSON.stringify(data, null, 2));
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
['mixan-client-id']: this.clientId,
|
||||
@@ -45,103 +45,109 @@ class Fetcher {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
...options,
|
||||
body: JSON.stringify(data ?? {}),
|
||||
...(options ?? {}),
|
||||
})
|
||||
.then(async (res) => {
|
||||
const response = await res.json() as (MixanErrorResponse | Response)
|
||||
const response = (await res.json()) as
|
||||
| MixanErrorResponse
|
||||
| PostResponse;
|
||||
|
||||
if(!response) {
|
||||
return null
|
||||
if (!response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof response === 'object' && 'status' in response && response.status === 'error') {
|
||||
if (
|
||||
typeof response === 'object' &&
|
||||
'status' in response &&
|
||||
response.status === 'error'
|
||||
) {
|
||||
this.logger(
|
||||
`Mixan request failed: [${options.method || 'POST'}] ${url}`,
|
||||
`Mixan request failed: [${options?.method ?? 'POST'}] ${url}`,
|
||||
JSON.stringify(response, null, 2)
|
||||
)
|
||||
return null
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return response as Response
|
||||
return response as PostResponse;
|
||||
})
|
||||
.catch(() => {
|
||||
this.logger(
|
||||
`Mixan request failed: [${options.method || 'POST'}] ${url}`
|
||||
)
|
||||
return null
|
||||
})
|
||||
`Mixan request failed: [${options?.method ?? 'POST'}] ${url}`
|
||||
);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Batcher<T extends any> {
|
||||
queue: T[] = []
|
||||
timer?: NodeJS.Timeout
|
||||
callback: (queue: T[]) => void
|
||||
maxBatchSize: number
|
||||
batchInterval: number
|
||||
class Batcher<T> {
|
||||
queue: T[] = [];
|
||||
timer?: NodeJS.Timeout;
|
||||
callback: (queue: T[]) => void;
|
||||
maxBatchSize: number;
|
||||
batchInterval: number;
|
||||
|
||||
constructor(options: MixanOptions, callback: (queue: T[]) => void) {
|
||||
this.callback = callback
|
||||
this.maxBatchSize = options.maxBatchSize
|
||||
this.batchInterval = options.batchInterval
|
||||
this.callback = callback;
|
||||
this.maxBatchSize = options.maxBatchSize;
|
||||
this.batchInterval = options.batchInterval;
|
||||
}
|
||||
|
||||
add(payload: T) {
|
||||
this.queue.push(payload)
|
||||
this.flush()
|
||||
this.queue.push(payload);
|
||||
this.flush();
|
||||
}
|
||||
|
||||
flush() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
if (this.queue.length === 0) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.queue.length >= this.maxBatchSize) {
|
||||
this.send()
|
||||
return
|
||||
this.send();
|
||||
return;
|
||||
}
|
||||
|
||||
this.timer = setTimeout(this.send.bind(this), this.batchInterval)
|
||||
this.timer = setTimeout(this.send.bind(this), this.batchInterval);
|
||||
}
|
||||
|
||||
send() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
if (this.queue.length > 0) {
|
||||
this.callback(this.queue)
|
||||
this.queue = []
|
||||
this.callback(this.queue);
|
||||
this.queue = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Mixan {
|
||||
private fetch: Fetcher
|
||||
private eventBatcher: Batcher<EventPayload>
|
||||
private profileId?: string
|
||||
private options: MixanOptions
|
||||
private logger: (...args: any[]) => void
|
||||
private globalProperties: Record<string, any> = {}
|
||||
private lastEventAt?: string
|
||||
private lastScreenViewAt?: string
|
||||
private fetch: Fetcher;
|
||||
private eventBatcher: Batcher<EventPayload>;
|
||||
private profileId?: string;
|
||||
private options: MixanOptions;
|
||||
private logger: (...args: any[]) => void;
|
||||
private globalProperties: Record<string, unknown> = {};
|
||||
private lastEventAt?: string;
|
||||
private lastScreenViewAt?: string;
|
||||
|
||||
constructor(options: NewMixanOptions) {
|
||||
this.logger = options.verbose ? console.log : () => {}
|
||||
this.logger = options.verbose ? console.log : () => {};
|
||||
this.options = {
|
||||
sessionTimeout: 1000 * 60 * 30,
|
||||
verbose: false,
|
||||
batchInterval: 10000,
|
||||
maxBatchSize: 10,
|
||||
...options,
|
||||
}
|
||||
};
|
||||
|
||||
this.fetch = new Fetcher(this.options)
|
||||
this.fetch = new Fetcher(this.options);
|
||||
this.eventBatcher = new Batcher(this.options, (queue) => {
|
||||
this.fetch.post(
|
||||
'/events',
|
||||
@@ -151,103 +157,106 @@ export class Mixan {
|
||||
...this.globalProperties,
|
||||
...item.properties,
|
||||
},
|
||||
profileId: item.profileId || this.profileId || null,
|
||||
profileId: item.profileId ?? this.profileId ?? null,
|
||||
}))
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
timestamp() {
|
||||
return new Date().toISOString()
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.logger('Mixan: Init')
|
||||
this.setAnonymousUser()
|
||||
this.logger('Mixan: Init');
|
||||
this.setAnonymousUser();
|
||||
}
|
||||
|
||||
event(name: string, properties: Record<string, any> = {}) {
|
||||
const now = new Date()
|
||||
event(name: string, properties: Record<string, unknown> = {}) {
|
||||
const now = new Date();
|
||||
const isSessionStart =
|
||||
now.getTime() - new Date(this.lastEventAt ?? '1970-01-01').getTime() >
|
||||
this.options.sessionTimeout
|
||||
this.options.sessionTimeout;
|
||||
|
||||
if (isSessionStart) {
|
||||
this.logger('Mixan: Session start')
|
||||
this.logger('Mixan: Session start');
|
||||
this.eventBatcher.add({
|
||||
name: 'session_start',
|
||||
time: this.timestamp(),
|
||||
properties: {},
|
||||
profileId: this.profileId || null,
|
||||
})
|
||||
profileId: this.profileId ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
this.logger('Mixan: Queue event', name)
|
||||
this.logger('Mixan: Queue event', name);
|
||||
this.eventBatcher.add({
|
||||
name,
|
||||
properties,
|
||||
time: this.timestamp(),
|
||||
profileId: this.profileId || null,
|
||||
})
|
||||
this.lastEventAt = this.timestamp()
|
||||
profileId: this.profileId ?? null,
|
||||
});
|
||||
this.lastEventAt = this.timestamp();
|
||||
}
|
||||
|
||||
private async setAnonymousUser(retryCount: number = 0) {
|
||||
const profileId = this.options.getProfileId()
|
||||
private async setAnonymousUser(retryCount = 0) {
|
||||
const profileId = this.options.getProfileId();
|
||||
if (profileId) {
|
||||
this.profileId = profileId
|
||||
this.logger('Mixan: Use existing profile', this.profileId)
|
||||
this.profileId = profileId;
|
||||
this.logger('Mixan: Use existing profile', this.profileId);
|
||||
} else {
|
||||
const res = await this.fetch.post<{id: string}>('/profiles')
|
||||
|
||||
if(res) {
|
||||
this.profileId = res.id
|
||||
this.options.saveProfileId(res.id)
|
||||
this.logger('Mixan: Create new profile', this.profileId)
|
||||
} else if(retryCount < 2) {
|
||||
const res = await this.fetch.post<undefined, { id: string }>('/profiles');
|
||||
|
||||
if (res) {
|
||||
this.profileId = res.id;
|
||||
this.options.saveProfileId(res.id);
|
||||
this.logger('Mixan: Create new profile', this.profileId);
|
||||
} else if (retryCount < 2) {
|
||||
setTimeout(() => {
|
||||
this.setAnonymousUser(retryCount + 1)
|
||||
this.setAnonymousUser(retryCount + 1);
|
||||
}, 500);
|
||||
} else {
|
||||
this.logger('Mixan: Failed to create new profile')
|
||||
this.logger('Mixan: Failed to create new profile');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async setUser(profile: ProfilePayload) {
|
||||
if (!this.profileId) {
|
||||
return this.logger('Mixan: Set user failed, no profileId')
|
||||
return this.logger('Mixan: Set user failed, no profileId');
|
||||
}
|
||||
this.logger('Mixan: Set user', profile)
|
||||
this.logger('Mixan: Set user', profile);
|
||||
await this.fetch.post(`/profiles/${this.profileId}`, profile, {
|
||||
method: 'PUT',
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async setUserProperty(name: string, value: any) {
|
||||
async setUserProperty(
|
||||
name: string,
|
||||
value: string | number | boolean | Record<string, unknown> | unknown[]
|
||||
) {
|
||||
if (!this.profileId) {
|
||||
return this.logger('Mixan: Set user property, no profileId')
|
||||
return this.logger('Mixan: Set user property, no profileId');
|
||||
}
|
||||
this.logger('Mixan: Set user property', name, value)
|
||||
this.logger('Mixan: Set user property', name, value);
|
||||
await this.fetch.post(`/profiles/${this.profileId}`, {
|
||||
properties: {
|
||||
[name]: value,
|
||||
},
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async setGlobalProperties(properties: Record<string, any>) {
|
||||
this.logger('Mixan: Set global properties', properties)
|
||||
this.globalProperties = properties ?? {}
|
||||
setGlobalProperties(properties: Record<string, unknown>) {
|
||||
this.logger('Mixan: Set global properties', properties);
|
||||
this.globalProperties = properties ?? {};
|
||||
}
|
||||
|
||||
async increment(name: string, value: number = 1) {
|
||||
async increment(name: string, value = 1) {
|
||||
if (!this.profileId) {
|
||||
this.logger('Mixan: Increment failed, no profileId')
|
||||
return
|
||||
this.logger('Mixan: Increment failed, no profileId');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger('Mixan: Increment user property', name, value)
|
||||
this.logger('Mixan: Increment user property', name, value);
|
||||
await this.fetch.post(
|
||||
`/profiles/${this.profileId}/increment`,
|
||||
{
|
||||
@@ -257,16 +266,16 @@ export class Mixan {
|
||||
{
|
||||
method: 'PUT',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async decrement(name: string, value: number = 1) {
|
||||
async decrement(name: string, value = 1) {
|
||||
if (!this.profileId) {
|
||||
this.logger('Mixan: Decrement failed, no profileId')
|
||||
return
|
||||
this.logger('Mixan: Decrement failed, no profileId');
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger('Mixan: Decrement user property', name, value)
|
||||
this.logger('Mixan: Decrement user property', name, value);
|
||||
await this.fetch.post(
|
||||
`/profiles/${this.profileId}/decrement`,
|
||||
{
|
||||
@@ -276,38 +285,38 @@ export class Mixan {
|
||||
{
|
||||
method: 'PUT',
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async screenView(route: string, _properties?: Record<string, any>) {
|
||||
const properties = _properties ?? {}
|
||||
const now = new Date()
|
||||
screenView(route: string, _properties?: Record<string, unknown>) {
|
||||
const properties = _properties ?? {};
|
||||
const now = new Date();
|
||||
|
||||
if (this.lastScreenViewAt) {
|
||||
const last = new Date(this.lastScreenViewAt)
|
||||
const diff = now.getTime() - last.getTime()
|
||||
this.logger(`Mixan: Screen view duration: ${diff}ms`)
|
||||
properties['duration'] = diff
|
||||
const last = new Date(this.lastScreenViewAt);
|
||||
const diff = now.getTime() - last.getTime();
|
||||
this.logger(`Mixan: Screen view duration: ${diff}ms`);
|
||||
properties.duration = diff;
|
||||
}
|
||||
|
||||
this.lastScreenViewAt = now.toISOString()
|
||||
await this.event('screen_view', {
|
||||
this.lastScreenViewAt = now.toISOString();
|
||||
this.event('screen_view', {
|
||||
...properties,
|
||||
route,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
flush() {
|
||||
this.logger('Mixan: Flushing events queue')
|
||||
this.eventBatcher.send()
|
||||
this.lastScreenViewAt = undefined
|
||||
this.logger('Mixan: Flushing events queue');
|
||||
this.eventBatcher.send();
|
||||
this.lastScreenViewAt = undefined;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.logger('Mixan: Clear, send remaining events and remove profileId')
|
||||
this.eventBatcher.send()
|
||||
this.options.removeProfileId()
|
||||
this.profileId = undefined
|
||||
this.setAnonymousUser()
|
||||
this.logger('Mixan: Clear, send remaining events and remove profileId');
|
||||
this.eventBatcher.send();
|
||||
this.options.removeProfileId();
|
||||
this.profileId = undefined;
|
||||
this.setAnonymousUser();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,25 @@
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mixan/types": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.5",
|
||||
"@mixan/eslint-config": "workspace:*",
|
||||
"@mixan/prettier-config": "workspace:*",
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
"typescript": "^5.2.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@mixan/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@mixan/prettier-config"
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": true,
|
||||
|
||||
|
||||
"outDir": "dist",
|
||||
"allowImportingTsExtensions": false,
|
||||
"noEmit": false,
|
||||
"noEmit": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig } from "tsup";
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["index.ts"],
|
||||
format: ["cjs", "esm"], // Build for commonJS and ESmodules
|
||||
entry: ['index.ts'],
|
||||
format: ['cjs', 'esm'], // Build for commonJS and ESmodules
|
||||
dts: true, // Generate declaration file (.d.ts)
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
export type MixanJson = Record<string, any>
|
||||
export type MixanJson = Record<string, any>;
|
||||
|
||||
export type EventPayload = {
|
||||
name: string
|
||||
time: string
|
||||
profileId: string | null
|
||||
properties: MixanJson
|
||||
export interface EventPayload {
|
||||
name: string;
|
||||
time: string;
|
||||
profileId: string | null;
|
||||
properties: MixanJson;
|
||||
}
|
||||
|
||||
export type ProfilePayload = {
|
||||
first_name?: string
|
||||
last_name?: string
|
||||
email?: string
|
||||
avatar?: string
|
||||
id?: string
|
||||
properties?: MixanJson
|
||||
export interface ProfilePayload {
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
id?: string;
|
||||
properties?: MixanJson;
|
||||
}
|
||||
|
||||
export type ProfileIncrementPayload = {
|
||||
name: string
|
||||
value: number
|
||||
id: string
|
||||
export interface ProfileIncrementPayload {
|
||||
name: string;
|
||||
value: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type ProfileDecrementPayload = {
|
||||
name: string
|
||||
value: number
|
||||
id: string
|
||||
export interface ProfileDecrementPayload {
|
||||
name: string;
|
||||
value: number;
|
||||
id: string;
|
||||
}
|
||||
|
||||
// Batching
|
||||
export type BatchEvent = {
|
||||
type: 'event'
|
||||
payload: EventPayload
|
||||
export interface BatchEvent {
|
||||
type: 'event';
|
||||
payload: EventPayload;
|
||||
}
|
||||
|
||||
export type BatchProfile = {
|
||||
type: 'profile'
|
||||
payload: ProfilePayload
|
||||
export interface BatchProfile {
|
||||
type: 'profile';
|
||||
payload: ProfilePayload;
|
||||
}
|
||||
|
||||
export type BatchProfileIncrement = {
|
||||
type: 'profile_increment'
|
||||
payload: ProfileIncrementPayload
|
||||
export interface BatchProfileIncrement {
|
||||
type: 'profile_increment';
|
||||
payload: ProfileIncrementPayload;
|
||||
}
|
||||
|
||||
export type BatchProfileDecrement = {
|
||||
type: 'profile_decrement'
|
||||
payload: ProfileDecrementPayload
|
||||
export interface BatchProfileDecrement {
|
||||
type: 'profile_decrement';
|
||||
payload: ProfileDecrementPayload;
|
||||
}
|
||||
|
||||
export type BatchItem =
|
||||
| BatchEvent
|
||||
| BatchProfile
|
||||
| BatchProfileIncrement
|
||||
| BatchProfileDecrement
|
||||
export type BatchPayload = Array<BatchItem>
|
||||
| BatchProfileDecrement;
|
||||
export type BatchPayload = BatchItem[];
|
||||
|
||||
export type MixanIssue = {
|
||||
field: string
|
||||
message: string
|
||||
value: any
|
||||
export interface MixanIssue {
|
||||
field: string;
|
||||
message: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export type MixanErrorResponse = {
|
||||
status: 'error'
|
||||
code: number
|
||||
message: string
|
||||
issues?: Array<MixanIssue> | undefined
|
||||
stack?: string | undefined
|
||||
export interface MixanErrorResponse {
|
||||
status: 'error';
|
||||
code: number;
|
||||
message: string;
|
||||
issues?: MixanIssue[] | undefined;
|
||||
stack?: string | undefined;
|
||||
}
|
||||
|
||||
export type MixanResponse<T> = {
|
||||
result: T
|
||||
status: 'ok'
|
||||
export interface MixanResponse<T> {
|
||||
result: T;
|
||||
status: 'ok';
|
||||
}
|
||||
|
||||
@@ -3,8 +3,22 @@
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"module": "index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mixan/eslint-config": "workspace:*",
|
||||
"@mixan/prettier-config": "workspace:*",
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
"typescript": "^5.2.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"@mixan/eslint-config/base"
|
||||
]
|
||||
},
|
||||
"prettier": "@mixan/prettier-config"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig } from "tsup";
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["index.ts"],
|
||||
format: ["cjs", "esm"], // Build for commonJS and ESmodules
|
||||
entry: ['index.ts'],
|
||||
format: ['cjs', 'esm'], // Build for commonJS and ESmodules
|
||||
dts: true, // Generate declaration file (.d.ts)
|
||||
splitting: false,
|
||||
sourcemap: false,
|
||||
|
||||
687
pnpm-lock.yaml
generated
687
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
packages:
|
||||
- 'apps/*'
|
||||
- 'packages/*'
|
||||
- 'tooling/*'
|
||||
|
||||
20
publish.ts
20
publish.ts
@@ -1,7 +1,7 @@
|
||||
import sdkPkg from './packages/sdk/package.json'
|
||||
import typesPkg from './packages/types/package.json'
|
||||
import fs from 'node:fs'
|
||||
import {execSync} from 'node:child_process'
|
||||
import { execSync } from 'node:child_process'
|
||||
import semver from 'semver'
|
||||
|
||||
function savePackageJson(path: string, data: Record<string, any>) {
|
||||
@@ -10,15 +10,15 @@ function savePackageJson(path: string, data: Record<string, any>) {
|
||||
|
||||
function main() {
|
||||
const [version] = process.argv.slice(2)
|
||||
|
||||
if(!version) {
|
||||
|
||||
if (!version) {
|
||||
return console.error('Missing version')
|
||||
}
|
||||
|
||||
if(!semver.valid(version)) {
|
||||
|
||||
if (!semver.valid(version)) {
|
||||
return console.error('Version is not valid')
|
||||
}
|
||||
|
||||
|
||||
const properties = {
|
||||
private: false,
|
||||
version,
|
||||
@@ -53,12 +53,12 @@ function main() {
|
||||
execSync('pnpm dlx tsup', {
|
||||
cwd: './packages/types',
|
||||
})
|
||||
} catch(error) {
|
||||
console.log('Build failed');
|
||||
console.log(error);
|
||||
} catch (error) {
|
||||
console.log('Build failed')
|
||||
console.log(error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
execSync('npm publish --access=public', {
|
||||
cwd: './packages/sdk',
|
||||
})
|
||||
|
||||
50
tooling/eslint/base.js
Normal file
50
tooling/eslint/base.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
const config = {
|
||||
extends: [
|
||||
'turbo',
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended-type-checked',
|
||||
'plugin:@typescript-eslint/stylistic-type-checked',
|
||||
'prettier',
|
||||
],
|
||||
env: {
|
||||
es2022: true,
|
||||
node: true,
|
||||
},
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: true,
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'import'],
|
||||
rules: {
|
||||
'turbo/no-undeclared-env-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'warn',
|
||||
{ prefer: 'type-imports', fixStyle: 'separate-type-imports' },
|
||||
],
|
||||
'@typescript-eslint/no-misused-promises': [
|
||||
2,
|
||||
{ checksVoidReturn: { attributes: false } },
|
||||
],
|
||||
'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/ban-ts-comment": "off"
|
||||
},
|
||||
ignorePatterns: [
|
||||
'**/.eslintrc.cjs',
|
||||
'**/*.config.js',
|
||||
'**/*.config.cjs',
|
||||
'.next',
|
||||
'dist',
|
||||
'pnpm-lock.yaml',
|
||||
],
|
||||
reportUnusedDisableDirectives: true,
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
40
tooling/eslint/package.json
Normal file
40
tooling/eslint/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@mixan/eslint-config",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"./base.js",
|
||||
"./react.js"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||
"@typescript-eslint/parser": "^6.6.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-config-turbo": "^1.10.13",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/eslint": "^8.44.2",
|
||||
"@mixan/prettier-config": "workspace:*",
|
||||
"@mixan/tsconfig": "workspace:*",
|
||||
"eslint": "^8.48.0",
|
||||
"typescript": "^5.2.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"extends": [
|
||||
"./base.js"
|
||||
]
|
||||
},
|
||||
"prettier": "@mixan/prettier-config"
|
||||
}
|
||||
24
tooling/eslint/react.js
vendored
Normal file
24
tooling/eslint/react.js
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
const config = {
|
||||
extends: [
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
],
|
||||
rules: {
|
||||
'react/prop-types': 'off',
|
||||
},
|
||||
globals: {
|
||||
React: 'writable',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
8
tooling/eslint/tsconfig.json
Normal file
8
tooling/eslint/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@mixan/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
28
tooling/prettier/index.mjs
Normal file
28
tooling/prettier/index.mjs
Normal file
@@ -0,0 +1,28 @@
|
||||
/** @typedef {import("prettier").Config} PrettierConfig */
|
||||
/** @typedef {import("@ianvs/prettier-plugin-sort-imports").PluginConfig} SortImportsConfig */
|
||||
|
||||
/** @type { PrettierConfig | SortImportsConfig } */
|
||||
const config = {
|
||||
plugins: [
|
||||
'@ianvs/prettier-plugin-sort-imports',
|
||||
],
|
||||
importOrder: [
|
||||
'^(react/(.*)$)|^(react$)|^(react-native(.*)$)',
|
||||
'<THIRD_PARTY_MODULES>',
|
||||
'',
|
||||
'^@mixan/(.*)$',
|
||||
'',
|
||||
'^~/',
|
||||
'^[../]',
|
||||
'^[./]',
|
||||
],
|
||||
importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'],
|
||||
importOrderTypeScriptVersion: '4.4.0',
|
||||
singleQuote: true,
|
||||
semi: true,
|
||||
trailingComma: 'es5',
|
||||
printWidth: 80,
|
||||
tabWidth: 2,
|
||||
};
|
||||
|
||||
export default config;
|
||||
17
tooling/prettier/package.json
Normal file
17
tooling/prettier/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@mixan/prettier-config",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "index.mjs",
|
||||
"scripts": {
|
||||
"clean": "rm -rf .turbo node_modules"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.1.0",
|
||||
"prettier": "^3.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mixan/tsconfig": "workspace:*",
|
||||
"typescript": "^5.2.0"
|
||||
}
|
||||
}
|
||||
8
tooling/prettier/tsconfig.json
Normal file
8
tooling/prettier/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@mixan/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
22
tooling/typescript/base.json
Normal file
22
tooling/typescript/base.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"noUncheckedIndexedAccess": true
|
||||
},
|
||||
"exclude": ["node_modules", "build", "dist"]
|
||||
}
|
||||
8
tooling/typescript/package.json
Normal file
8
tooling/typescript/package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "@mixan/tsconfig",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"files": [
|
||||
"base.json"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user