feat: dashboard v2, esm, upgrades (#211)
* esm * wip * wip * wip * wip * wip * wip * subscription notice * wip * wip * wip * fix envs * fix: update docker build * fix * esm/types * delete dashboard :D * add patches to dockerfiles * update packages + catalogs + ts * wip * remove native libs * ts * improvements * fix redirects and fetching session * try fix favicon * fixes * fix * order and resize reportds within a dashboard * improvements * wip * added userjot to dashboard * fix * add op * wip * different cache key * improve date picker * fix table * event details loading * redo onboarding completely * fix login * fix * fix * extend session, billing and improve bars * fix * reduce price on 10M
This commit is contained in:
committed by
GitHub
parent
436e81ecc9
commit
81a7e5d62e
12
apps/start/src/utils/are-props-equal.ts
Normal file
12
apps/start/src/utils/are-props-equal.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { pathOr } from 'ramda';
|
||||
import { shallowEqual } from 'react-redux';
|
||||
|
||||
export function arePropsEqual(paths: string[]) {
|
||||
return (prevProps: any, nextProps: any) =>
|
||||
paths.every((path) =>
|
||||
shallowEqual(
|
||||
pathOr(undefined, path.split('.'), prevProps),
|
||||
pathOr(undefined, path.split('.'), nextProps),
|
||||
),
|
||||
);
|
||||
}
|
||||
9
apps/start/src/utils/casing.ts
Normal file
9
apps/start/src/utils/casing.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const camelCaseToWords = (str: string) => {
|
||||
return str
|
||||
.replaceAll('_', ' ')
|
||||
.trim()
|
||||
.replaceAll(/([A-Z])/g, ' $1')
|
||||
.trim()
|
||||
.replace(/^./, (str) => str.toUpperCase())
|
||||
.replaceAll(/\s./g, (str) => str.toUpperCase());
|
||||
};
|
||||
13
apps/start/src/utils/clipboard.ts
Normal file
13
apps/start/src/utils/clipboard.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function clipboard(value: string | number, description?: null | string) {
|
||||
navigator.clipboard.writeText(value.toString());
|
||||
toast(
|
||||
'Copied to clipboard',
|
||||
description !== null
|
||||
? {
|
||||
description: description ?? value.toString(),
|
||||
}
|
||||
: {},
|
||||
);
|
||||
}
|
||||
7
apps/start/src/utils/cn.ts
Normal file
7
apps/start/src/utils/cn.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { clsx } from 'clsx';
|
||||
import type { ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
68
apps/start/src/utils/date.ts
Normal file
68
apps/start/src/utils/date.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { differenceInDays, differenceInHours, isSameDay } from 'date-fns';
|
||||
import type { FormatStyleName } from 'javascript-time-ago';
|
||||
import TimeAgo from 'javascript-time-ago';
|
||||
import en from 'javascript-time-ago/locale/en';
|
||||
|
||||
export function dateDifferanceInDays(date1: Date, date2: Date) {
|
||||
const diffTime = Math.abs(date2.getTime() - date1.getTime());
|
||||
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
}
|
||||
|
||||
export function getLocale() {
|
||||
if (typeof navigator === 'undefined') {
|
||||
return 'en-US';
|
||||
}
|
||||
|
||||
return navigator.language ?? 'en-US';
|
||||
}
|
||||
|
||||
export function formatDate(date: Date) {
|
||||
const day = date.getDate();
|
||||
const month = new Intl.DateTimeFormat(getLocale(), { month: 'short' })
|
||||
.format(date)
|
||||
.replace('.', '')
|
||||
.toLowerCase();
|
||||
|
||||
return `${day} ${month}`;
|
||||
}
|
||||
|
||||
export function formatDateTime(date: Date) {
|
||||
const datePart = formatDate(date);
|
||||
const timePart = new Intl.DateTimeFormat(getLocale(), {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}).format(date);
|
||||
|
||||
return `${datePart}, ${timePart}`;
|
||||
}
|
||||
|
||||
export function formatTime(date: Date) {
|
||||
return new Intl.DateTimeFormat(getLocale(), {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
TimeAgo.addDefaultLocale(en);
|
||||
const ta = new TimeAgo(getLocale());
|
||||
|
||||
export function timeAgo(date: Date, style?: FormatStyleName) {
|
||||
return ta.format(new Date(date), style);
|
||||
}
|
||||
|
||||
export function formatTimeAgoOrDateTime(date: Date) {
|
||||
if (Math.abs(differenceInHours(date, new Date())) < 3) {
|
||||
return timeAgo(date);
|
||||
}
|
||||
|
||||
return isSameDay(date, new Date()) ? formatTime(date) : formatDateTime(date);
|
||||
}
|
||||
|
||||
export function utc(date: string) {
|
||||
if (date.match(/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}.\d{3}$/)) {
|
||||
return new Date(`${date}Z`);
|
||||
}
|
||||
return new Date(date).toISOString();
|
||||
}
|
||||
35
apps/start/src/utils/getDbId.ts
Normal file
35
apps/start/src/utils/getDbId.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { db } from '@openpanel/db';
|
||||
|
||||
import { slug } from './slug';
|
||||
|
||||
export async function getId(tableName: 'project' | 'dashboard', name: string) {
|
||||
const newId = slug(name);
|
||||
if (!db[tableName]) {
|
||||
throw new Error('Table does not exists');
|
||||
}
|
||||
|
||||
if (!('findUnique' in db[tableName])) {
|
||||
throw new Error('findUnique does not exists');
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
const existingProject = await db[tableName].findUnique({
|
||||
where: {
|
||||
id: newId,
|
||||
},
|
||||
});
|
||||
|
||||
function random(str: string) {
|
||||
const numbers = Math.floor(1000 + Math.random() * 9000);
|
||||
if (str.match(/-\d{4}$/g)) {
|
||||
return str.replace(/-\d{4}$/g, `-${numbers}`);
|
||||
}
|
||||
return `${str}-${numbers}`;
|
||||
}
|
||||
|
||||
if (existingProject) {
|
||||
return getId(tableName, random(name));
|
||||
}
|
||||
|
||||
return newId;
|
||||
}
|
||||
33
apps/start/src/utils/getters.ts
Normal file
33
apps/start/src/utils/getters.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { IServiceProfile } from '@openpanel/db';
|
||||
|
||||
export type GetProfileNameProps = Partial<
|
||||
Pick<
|
||||
IServiceProfile,
|
||||
'firstName' | 'lastName' | 'email' | 'isExternal' | 'id'
|
||||
>
|
||||
>;
|
||||
export function getProfileName(
|
||||
profile: GetProfileNameProps | undefined | null,
|
||||
short = true,
|
||||
) {
|
||||
if (!profile) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!profile.isExternal) {
|
||||
return 'Anonymous';
|
||||
}
|
||||
|
||||
const name =
|
||||
[profile.firstName, profile.lastName].filter(Boolean).join(' ') ||
|
||||
profile.email;
|
||||
|
||||
if (!name) {
|
||||
if (short && profile?.id && profile.id.length > 10) {
|
||||
return `${profile?.id?.slice(0, 4)}...${profile?.id?.slice(-4)}`;
|
||||
}
|
||||
return profile?.id ?? 'Unknown';
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
1
apps/start/src/utils/math.ts
Normal file
1
apps/start/src/utils/math.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '@openpanel/common/src/math';
|
||||
8
apps/start/src/utils/op.ts
Normal file
8
apps/start/src/utils/op.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { OpenPanel } from '@openpanel/web';
|
||||
|
||||
export const op = new OpenPanel({
|
||||
clientId: '301c6dc1-424c-4bc3-9886-a8beab09b615',
|
||||
trackScreenViews: true,
|
||||
trackOutgoingLinks: true,
|
||||
trackAttributes: true,
|
||||
});
|
||||
13
apps/start/src/utils/should-ignore-keypress.ts
Normal file
13
apps/start/src/utils/should-ignore-keypress.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export function shouldIgnoreKeypress(event: KeyboardEvent) {
|
||||
const feedbackWidget =
|
||||
typeof window !== 'undefined' && 'uj' in window
|
||||
? (window.uj as any).getWidgetState()
|
||||
: null;
|
||||
const tagName = (event?.target as HTMLElement)?.tagName;
|
||||
const modifierPressed =
|
||||
event.ctrlKey || event.metaKey || event.altKey || event.keyCode === 229;
|
||||
const isTyping =
|
||||
event.isComposing || tagName === 'INPUT' || tagName === 'TEXTAREA';
|
||||
|
||||
return modifierPressed || isTyping || feedbackWidget?.isOpen === true;
|
||||
}
|
||||
1
apps/start/src/utils/slug.ts
Normal file
1
apps/start/src/utils/slug.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from '@openpanel/common/src/slug';
|
||||
23
apps/start/src/utils/storage.ts
Normal file
23
apps/start/src/utils/storage.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
const prefix = '@op';
|
||||
|
||||
export function getStorageItem<T>(key: string): T | null;
|
||||
export function getStorageItem<T>(key: string, defaultValue: T): T;
|
||||
export function getStorageItem<T>(key: string, defaultValue?: T): T | null {
|
||||
if (typeof window === 'undefined') return defaultValue ?? null;
|
||||
const item = localStorage.getItem(`${prefix}:${key}`);
|
||||
if (item === null) {
|
||||
return defaultValue ?? null;
|
||||
}
|
||||
|
||||
return item as T;
|
||||
}
|
||||
|
||||
export function setStorageItem(key: string, value: unknown) {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem(`${prefix}:${key}`, value as string);
|
||||
}
|
||||
|
||||
export function removeStorageItem(key: string) {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.removeItem(`${prefix}:${key}`);
|
||||
}
|
||||
33
apps/start/src/utils/theme.ts
Normal file
33
apps/start/src/utils/theme.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// import resolveConfig from 'tailwindcss/resolveConfig';
|
||||
|
||||
// import tailwinConfig from '../../tailwind.config';
|
||||
|
||||
// export const resolvedTailwindConfig = resolveConfig(tailwinConfig);
|
||||
|
||||
// export const theme = resolvedTailwindConfig.theme as Record<string, any>;
|
||||
|
||||
const chartColors = [
|
||||
'#2563EB',
|
||||
'#ff7557',
|
||||
'#7fe1d8',
|
||||
'#f8bc3c',
|
||||
'#b3596e',
|
||||
'#72bef4',
|
||||
'#ffb27a',
|
||||
'#0f7ea0',
|
||||
'#3ba974',
|
||||
'#febbb2',
|
||||
'#cb80dc',
|
||||
'#5cb7af',
|
||||
'#7856ff',
|
||||
];
|
||||
|
||||
export function getChartColor(index: number): string {
|
||||
// const colors = theme?.colors ?? {};
|
||||
// const chartColors: string[] = Object.keys(colors)
|
||||
// .filter((key) => key.startsWith('chart-'))
|
||||
// .map((key) => colors[key])
|
||||
// .filter((item): item is string => typeof item === 'string');
|
||||
|
||||
return chartColors[index % chartColors.length]!;
|
||||
}
|
||||
125
apps/start/src/utils/title.ts
Normal file
125
apps/start/src/utils/title.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Utility functions for generating page titles
|
||||
*/
|
||||
|
||||
const BASE_TITLE = 'OpenPanel.dev';
|
||||
|
||||
/**
|
||||
* Creates a hierarchical title with the format: "Page Title | Section | OpenPanel.dev"
|
||||
*/
|
||||
export function createTitle(
|
||||
pageTitle: string,
|
||||
section?: string,
|
||||
baseTitle = BASE_TITLE,
|
||||
): string {
|
||||
const parts = [pageTitle];
|
||||
if (section) {
|
||||
parts.push(section);
|
||||
}
|
||||
parts.push(baseTitle);
|
||||
return parts.join(' | ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a title for organization-level pages
|
||||
*/
|
||||
export function createOrganizationTitle(
|
||||
pageTitle: string,
|
||||
organizationName?: string,
|
||||
): string {
|
||||
if (organizationName) {
|
||||
return createTitle(pageTitle, organizationName);
|
||||
}
|
||||
return createTitle(pageTitle, 'Organization');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a title for project-level pages
|
||||
*/
|
||||
export function createProjectTitle(
|
||||
pageTitle: string,
|
||||
projectName?: string,
|
||||
organizationName?: string,
|
||||
): string {
|
||||
const parts = [pageTitle];
|
||||
if (projectName) {
|
||||
parts.push(projectName);
|
||||
}
|
||||
if (organizationName) {
|
||||
parts.push(organizationName);
|
||||
}
|
||||
parts.push(BASE_TITLE);
|
||||
return parts.join(' | ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a title for specific entity pages (reports, sessions, etc.)
|
||||
*/
|
||||
export function createEntityTitle(
|
||||
entityName: string,
|
||||
entityType: string,
|
||||
projectName?: string,
|
||||
organizationName?: string,
|
||||
): string {
|
||||
const parts = [entityName, entityType];
|
||||
if (projectName) {
|
||||
parts.push(projectName);
|
||||
}
|
||||
if (organizationName) {
|
||||
parts.push(organizationName);
|
||||
}
|
||||
parts.push(BASE_TITLE);
|
||||
return parts.join(' | ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Common page titles
|
||||
*/
|
||||
export const PAGE_TITLES = {
|
||||
// Main sections
|
||||
DASHBOARD: 'Dashboard',
|
||||
EVENTS: 'Events',
|
||||
SESSIONS: 'Sessions',
|
||||
PROFILES: 'Profiles',
|
||||
PAGES: 'Pages',
|
||||
REPORTS: 'Reports',
|
||||
NOTIFICATIONS: 'Notifications',
|
||||
SETTINGS: 'Settings',
|
||||
INTEGRATIONS: 'Integrations',
|
||||
MEMBERS: 'Members',
|
||||
BILLING: 'Billing',
|
||||
CHAT: 'AI Assistant',
|
||||
REALTIME: 'Realtime',
|
||||
REFERENCES: 'References',
|
||||
|
||||
// Sub-sections
|
||||
CONVERSIONS: 'Conversions',
|
||||
STATS: 'Statistics',
|
||||
ANONYMOUS: 'Anonymous',
|
||||
IDENTIFIED: 'Identified',
|
||||
POWER_USERS: 'Power Users',
|
||||
CLIENTS: 'Clients',
|
||||
DETAILS: 'Details',
|
||||
AVAILABLE: 'Available',
|
||||
INSTALLED: 'Installed',
|
||||
INVITATIONS: 'Invitations',
|
||||
|
||||
// Actions
|
||||
CREATE: 'Create',
|
||||
EDIT: 'Edit',
|
||||
DELETE: 'Delete',
|
||||
|
||||
// Onboarding
|
||||
ONBOARDING: 'Getting Started',
|
||||
CONNECT: 'Connect',
|
||||
VERIFY: 'Verify',
|
||||
PROJECT: 'Project',
|
||||
PROJECTS: 'Projects',
|
||||
|
||||
// Auth
|
||||
LOGIN: 'Login',
|
||||
RESET_PASSWORD: 'Reset Password',
|
||||
|
||||
// Share
|
||||
SHARE: 'Shared Dashboard',
|
||||
} as const;
|
||||
6
apps/start/src/utils/truncate.ts
Normal file
6
apps/start/src/utils/truncate.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function truncate(str: string, len: number) {
|
||||
if (str.length <= len) {
|
||||
return str;
|
||||
}
|
||||
return `${str.slice(0, len)}...`;
|
||||
}
|
||||
Reference in New Issue
Block a user