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:
Carl-Gerhard Lindesvärd
2025-10-16 12:27:44 +02:00
committed by GitHub
parent 436e81ecc9
commit 81a7e5d62e
741 changed files with 32695 additions and 16996 deletions

View 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),
),
);
}

View 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());
};

View 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(),
}
: {},
);
}

View 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));
}

View 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();
}

View 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;
}

View 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;
}

View File

@@ -0,0 +1 @@
export * from '@openpanel/common/src/math';

View 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,
});

View 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;
}

View File

@@ -0,0 +1 @@
export * from '@openpanel/common/src/slug';

View 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}`);
}

View 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]!;
}

View 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;

View File

@@ -0,0 +1,6 @@
export function truncate(str: string, len: number) {
if (str.length <= len) {
return str;
}
return `${str.slice(0, len)}...`;
}