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