diff --git a/.github/workflows/sdk-e2e.yml b/.github/workflows/sdk-e2e.yml
new file mode 100644
index 0000000000..a4a5800abe
--- /dev/null
+++ b/.github/workflows/sdk-e2e.yml
@@ -0,0 +1,63 @@
+name: SDK E2E
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - ".github/workflows/sdk-e2e.yml"
+ - "packages/sdk/**"
+ - "packages/tracker/**"
+ - "bun.lock"
+ - "package.json"
+ - "turbo.json"
+ pull_request:
+ branches: [main, staging]
+ paths:
+ - ".github/workflows/sdk-e2e.yml"
+ - "packages/sdk/**"
+ - "packages/tracker/**"
+ - "bun.lock"
+ - "package.json"
+ - "turbo.json"
+
+permissions:
+ contents: read
+
+concurrency:
+ group: sdk-e2e-${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref_name }}
+ cancel-in-progress: true
+
+jobs:
+ sdk-e2e:
+ name: SDK Playwright
+ runs-on: blacksmith-4vcpu-ubuntu-2404
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
+ - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
+ with:
+ bun-version: "1.3.14"
+ - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
+ with:
+ path: ~/.bun/install/cache
+ key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }}
+ restore-keys: ${{ runner.os }}-bun-
+ - run: bun install --frozen-lockfile --ignore-scripts
+ - name: Build sdk and tracker
+ run: bunx turbo run build --filter @databuddy/sdk --filter @databuddy/tracker
+ - name: Install Playwright browser
+ run: bun run --cwd packages/sdk playwright install --with-deps chromium
+ - name: Run sdk E2E
+ run: bun run --cwd packages/sdk playwright test --project=chromium
+ - name: Run tracker E2E
+ run: bun run --cwd packages/tracker playwright test --project=chromium
+ - name: Upload Playwright artifacts
+ if: failure()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: sdk-e2e-artifacts
+ path: |
+ packages/sdk/test-results
+ packages/tracker/test-results
+ if-no-files-found: ignore
+ retention-days: 7
diff --git a/apps/api/package.json b/apps/api/package.json
index b165224748..9af45a747a 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -10,7 +10,6 @@
"test:watch": "TZ=UTC bunx --bun vitest"
},
"dependencies": {
- "@ai-sdk/provider": "^3.0.5",
"@contextcompany/otel": "^1.0.13",
"@databuddy/ai": "workspace:*",
"@databuddy/api-keys": "workspace:*",
@@ -22,7 +21,6 @@
"@databuddy/notifications": "workspace:*",
"@databuddy/redis": "workspace:*",
"@databuddy/rpc": "workspace:*",
- "@databuddy/sdk": "workspace:*",
"@databuddy/services": "workspace:*",
"@databuddy/shared": "workspace:*",
"@databuddy/validation": "workspace:*",
@@ -37,21 +35,17 @@
"ai": "^6.0.188",
"autumn-js": "catalog:",
"bullmq": "^5.78.0",
- "dayjs": "^1.11.19",
"elysia": "catalog:",
"evlog": "catalog:",
- "jszip": "^3.10.1",
"keypal": "0.2.0",
"lru-cache": "^11.2.7",
"resend": "^4.0.1",
- "supermemory": "^4.17.0",
"svix": "^1.84.1",
"zod": "catalog:"
},
"devDependencies": {
"@databuddy/test": "workspace:*",
"@types/bun": "catalog:",
- "mitata": "^1.0.34",
"vitest": "^4.1.5"
},
"peerDependencies": {
diff --git a/apps/api/src/http/cors.ts b/apps/api/src/http/cors.ts
index 4a9b0e78d3..95dd3cb265 100644
--- a/apps/api/src/http/cors.ts
+++ b/apps/api/src/http/cors.ts
@@ -4,7 +4,7 @@ const DATABUDDY_HOST_RE = /(?:^|\.)databuddy\.cc$/;
const allowedApiOrigins = new Set(config.cors.apiOrigins);
const MCP_PATHS = new Set(["/v1/mcp", "/v1/mcp/", "/mcp", "/mcp/"]);
-export function isMcpRequest(request: Request): boolean {
+function isMcpRequest(request: Request): boolean {
return MCP_PATHS.has(new URL(request.url).pathname);
}
diff --git a/apps/api/src/integration/cache-auth-bypass.test.ts b/apps/api/src/integration/cache-auth-bypass.test.ts
index 4c42718133..cd94c66e35 100644
--- a/apps/api/src/integration/cache-auth-bypass.test.ts
+++ b/apps/api/src/integration/cache-auth-bypass.test.ts
@@ -1,7 +1,7 @@
import "@databuddy/test/env";
import { flags, targetGroups } from "@databuddy/db/schema";
-import { appRouter, type Context } from "@databuddy/rpc";
+import { appRouter } from "@databuddy/rpc";
import {
addToOrganization,
apiKeyContext,
@@ -16,16 +16,12 @@ import {
signUp,
userContext,
} from "@databuddy/test";
-import { createProcedureClient } from "@orpc/server";
import { randomUUIDv7 } from "bun";
import { afterAll, beforeEach, describe, expect, it } from "vitest";
+import { call } from "./helpers";
const iit = hasTestDb ? it : it.skip;
-function call(procedure: T, ctx: Context) {
- return createProcedureClient(procedure as any, { context: ctx });
-}
-
async function setupOwnedSite(siteOverrides?: { isPublic?: boolean }) {
const user = await signUp();
const org = await insertOrganization();
diff --git a/apps/api/src/integration/helpers.ts b/apps/api/src/integration/helpers.ts
new file mode 100644
index 0000000000..4e6540c6eb
--- /dev/null
+++ b/apps/api/src/integration/helpers.ts
@@ -0,0 +1,6 @@
+import type { Context } from "@databuddy/rpc";
+import { type AnyProcedure, createProcedureClient } from "@orpc/server";
+
+export function call(procedure: T, context: Context) {
+ return createProcedureClient(procedure, { context });
+}
diff --git a/apps/api/src/integration/insights-handlers.test.ts b/apps/api/src/integration/insights-handlers.test.ts
index 0988e88f5c..5d72f3d3ba 100644
--- a/apps/api/src/integration/insights-handlers.test.ts
+++ b/apps/api/src/integration/insights-handlers.test.ts
@@ -13,7 +13,6 @@ import {
appRouter,
createInternalPrincipal,
createRPCContext,
- type Context,
} from "@databuddy/rpc";
import {
closeInsightsQueue,
@@ -33,9 +32,9 @@ import {
signUp,
userContext,
} from "@databuddy/test";
-import { createProcedureClient, type AnyProcedure } from "@orpc/server";
import { randomUUIDv7 } from "bun";
import { afterAll, beforeEach, describe, expect, it } from "vitest";
+import { call } from "./helpers";
const iit = hasTestDb ? it : it.skip;
@@ -67,10 +66,6 @@ function investigationOutcome(nextType: "act" | "watch"): InvestigationOutcome {
};
}
-function call(procedure: T, context: Context) {
- return createProcedureClient(procedure, { context });
-}
-
async function seedExecutableGoalAction() {
const member = await signUp();
const organization = await insertOrganization();
diff --git a/apps/api/src/integration/link-handlers.test.ts b/apps/api/src/integration/link-handlers.test.ts
index 82abc8a22b..5ec6c7558a 100644
--- a/apps/api/src/integration/link-handlers.test.ts
+++ b/apps/api/src/integration/link-handlers.test.ts
@@ -1,8 +1,7 @@
import "@databuddy/test/env";
import { describe, it, expect, beforeEach, afterAll } from "vitest";
-import { createProcedureClient } from "@orpc/server";
-import { appRouter, type Context } from "@databuddy/rpc";
+import { appRouter } from "@databuddy/rpc";
import {
reset,
cleanup,
@@ -14,13 +13,10 @@ import {
signUp,
addToOrganization,
} from "@databuddy/test";
+import { call } from "./helpers";
const iit = hasTestDb ? it : it.skip;
-function call(procedure: T, ctx: Context) {
- return createProcedureClient(procedure as any, { context: ctx });
-}
-
beforeEach(() => reset());
afterAll(() => cleanup());
diff --git a/apps/api/src/integration/profile-handlers.test.ts b/apps/api/src/integration/profile-handlers.test.ts
index c3524c2dea..0645c6593e 100644
--- a/apps/api/src/integration/profile-handlers.test.ts
+++ b/apps/api/src/integration/profile-handlers.test.ts
@@ -6,7 +6,7 @@ import {
profileTraitChanges,
} from "@databuddy/db/schema";
import { eq } from "@databuddy/db";
-import { appRouter, type Context } from "@databuddy/rpc";
+import { appRouter } from "@databuddy/rpc";
import {
getTraitDistribution,
resolveTraitSegment,
@@ -25,15 +25,11 @@ import {
signUp,
userContext,
} from "@databuddy/test";
-import { createProcedureClient } from "@orpc/server";
import { afterAll, beforeEach, describe, expect, it } from "vitest";
+import { call } from "./helpers";
const iit = hasTestDb ? it : it.skip;
-function call(procedure: T, ctx: Context) {
- return createProcedureClient(procedure as any, { context: ctx });
-}
-
beforeEach(() => reset());
afterAll(() => cleanup());
diff --git a/apps/api/src/integration/uptime-handlers.test.ts b/apps/api/src/integration/uptime-handlers.test.ts
index 1affb5bc35..3e249e057c 100644
--- a/apps/api/src/integration/uptime-handlers.test.ts
+++ b/apps/api/src/integration/uptime-handlers.test.ts
@@ -23,10 +23,10 @@ import {
signUp,
userContext,
} from "@databuddy/test";
-import { createProcedureClient, type AnyProcedure } from "@orpc/server";
import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest";
import { randomUUIDv7 } from "bun";
import type { Job } from "bullmq";
+import { call } from "./helpers";
const canRun =
hasTestDb &&
@@ -35,10 +35,6 @@ const canRun =
const iit = canRun ? it : it.skip;
const scheduleIds = new Set();
-function call(procedure: T, context: Context) {
- return createProcedureClient(procedure, { context });
-}
-
beforeEach(async () => {
await reset();
scheduleIds.clear();
diff --git a/apps/api/src/integration/with-workspace.test.ts b/apps/api/src/integration/with-workspace.test.ts
index 484f45472d..8572ccd524 100644
--- a/apps/api/src/integration/with-workspace.test.ts
+++ b/apps/api/src/integration/with-workspace.test.ts
@@ -1,13 +1,7 @@
import "@databuddy/test/env";
import { afterAll, beforeEach, describe, expect, it } from "vitest";
-import { createProcedureClient } from "@orpc/server";
-import {
- withWorkspace,
- withPublicWorkspace,
- appRouter,
- type Context,
-} from "@databuddy/rpc";
+import { withWorkspace, withPublicWorkspace, appRouter } from "@databuddy/rpc";
import {
reset,
cleanup,
@@ -21,13 +15,10 @@ import {
signUp,
addToOrganization,
} from "@databuddy/test";
+import { call } from "./helpers";
const iit = hasTestDb ? it : it.skip;
-function call(procedure: T, ctx: Context) {
- return createProcedureClient(procedure as any, { context: ctx });
-}
-
beforeEach(() => reset());
afterAll(() => cleanup());
diff --git a/apps/api/src/lib/api-key.test.ts b/apps/api/src/lib/api-key.test.ts
index dbbba42d7d..761fbce91e 100644
--- a/apps/api/src/lib/api-key.test.ts
+++ b/apps/api/src/lib/api-key.test.ts
@@ -1,5 +1,41 @@
-import { describe, expect, it } from "vitest";
-import { hasScope, isExpired } from "keypal";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const state = vi.hoisted(() => ({
+ findFirst: vi.fn(async () => state.row),
+ lastUsedWrites: 0,
+ lockReply: "OK" as "OK" | null,
+ redisSet: vi.fn(async () => state.lockReply),
+ row: null as unknown,
+}));
+
+vi.mock("@databuddy/db", () => ({
+ db: {
+ transaction: async (fn: (tx: unknown) => Promise) =>
+ fn({
+ execute: async () => undefined,
+ query: { apikey: { findFirst: state.findFirst } },
+ }),
+ update: () => ({
+ set: () => ({
+ where: async () => {
+ state.lastUsedWrites += 1;
+ },
+ }),
+ }),
+ },
+ eq: vi.fn(),
+ sql: () => "",
+}));
+
+vi.mock("@databuddy/db/schema", () => ({ apikey: {} }));
+
+vi.mock("@databuddy/redis", () => ({
+ cacheNamespaces: { apiKeyByHash: "api-key-by-hash" },
+ cacheable: (fn: (...args: never[]) => unknown) =>
+ Object.assign(fn, { invalidate: vi.fn(async () => undefined) }),
+ redis: { set: state.redisSet },
+}));
+
import {
type ApiKeyRow,
extractSecret,
@@ -13,158 +49,214 @@ import {
hasWebsiteAnyScope,
hasWebsiteScope,
isApiKeyPresent,
+ resolveApiKey,
+ resolveApiKeySecret,
resolveEffectiveScopesForWebsite,
} from "@databuddy/api-keys/resolve";
-const createMockKey = (overrides: Partial = {}): ApiKeyRow =>
- ({
+const VALID_SECRET = "dbdy_test123";
+
+function createMockKey(overrides: Partial = {}): ApiKeyRow {
+ const now = new Date("2026-08-01T00:00:00.000Z");
+ return {
+ createdAt: now,
+ enabled: true,
+ expiresAt: null,
id: "key-123",
- name: "Test Key",
- prefix: "dbdy",
- start: "dbdy_abc",
keyHash: "hashed",
- userId: "user-1",
+ lastUsedAt: null,
+ metadata: {},
+ name: "Test Key",
organizationId: null,
- type: "user",
- scopes: ["read:data", "write:data"],
- enabled: true,
- revokedAt: null,
- expiresAt: null,
+ prefix: "dbdy",
rateLimitEnabled: true,
rateLimitMax: null,
rateLimitTimeWindow: null,
- metadata: {},
- createdAt: new Date(),
- updatedAt: new Date(),
+ revokedAt: null,
+ scopes: ["read:data", "write:data"],
+ start: "dbdy_abc",
+ type: "user",
+ updatedAt: now,
+ userId: "user-1",
...overrides,
- }) as ApiKeyRow;
+ };
+}
+
+beforeEach(() => {
+ state.findFirst.mockClear();
+ state.lastUsedWrites = 0;
+ state.lockReply = "OK";
+ state.redisSet.mockClear();
+ state.row = null;
+});
describe("isApiKeyPresent", () => {
- it("returns true when x-api-key header is present", () => {
- const headers = new Headers({ "x-api-key": "dbdy_test123" });
- expect(isApiKeyPresent(headers)).toBe(true);
- });
-
- it("returns true when Bearer token is present", () => {
- const headers = new Headers({ authorization: "Bearer dbdy_test123" });
- expect(isApiKeyPresent(headers)).toBe(true);
- });
-
- it("returns false when no API key headers", () => {
- const headers = new Headers({});
- expect(isApiKeyPresent(headers)).toBe(false);
- });
-
- it("returns false for non-Bearer authorization", () => {
- const headers = new Headers({ authorization: "Basic dXNlcjpwYXNz" });
- expect(isApiKeyPresent(headers)).toBe(false);
- });
-
- it("returns true for lowercase bearer", () => {
- const headers = new Headers({ authorization: "bearer dbdy_test123" });
- expect(isApiKeyPresent(headers)).toBe(true);
- });
-
- it("returns false for empty x-api-key", () => {
- const headers = new Headers({ "x-api-key": "" });
- expect(isApiKeyPresent(headers)).toBe(false);
+ it.each([
+ ["x-api-key header", { "x-api-key": VALID_SECRET }, true],
+ ["Bearer token", { authorization: `Bearer ${VALID_SECRET}` }, true],
+ ["lowercase bearer", { authorization: `bearer ${VALID_SECRET}` }, true],
+ ["no headers", {}, false],
+ ["Basic authorization", { authorization: "Basic dXNlcjpwYXNz" }, false],
+ ["empty x-api-key", { "x-api-key": "" }, false],
+ ])("%s -> %s", (_name, headers, expected) => {
+ expect(isApiKeyPresent(new Headers(headers))).toBe(expected);
});
});
describe("extractSecret", () => {
- it("extracts x-api-key header", () => {
- const headers = new Headers({ "x-api-key": "dbdy_test123" });
- expect(extractSecret(headers)).toBe("dbdy_test123");
- });
-
- it("extracts Bearer token from authorization", () => {
- const headers = new Headers({ authorization: "Bearer dbdy_test123" });
- expect(extractSecret(headers)).toBe("dbdy_test123");
+ it.each([
+ ["x-api-key header", { "x-api-key": VALID_SECRET }, VALID_SECRET],
+ [
+ "Bearer token",
+ { authorization: `Bearer ${VALID_SECRET}` },
+ VALID_SECRET,
+ ],
+ [
+ "x-api-key over Bearer",
+ {
+ authorization: "Bearer dbdy_bearer_token",
+ "x-api-key": "dbdy_xapikey_token",
+ },
+ "dbdy_xapikey_token",
+ ],
+ [
+ "whitespace around x-api-key",
+ { "x-api-key": ` ${VALID_SECRET} ` },
+ VALID_SECRET,
+ ],
+ [
+ "whitespace around Bearer token",
+ { authorization: `Bearer ${VALID_SECRET} ` },
+ VALID_SECRET,
+ ],
+ [
+ "case-insensitive Bearer",
+ { authorization: `BEARER ${VALID_SECRET}` },
+ VALID_SECRET,
+ ],
+ ["no headers", {}, null],
+ ["Basic authorization", { authorization: "Basic dXNlcjpwYXNz" }, null],
+ ["whitespace-only x-api-key", { "x-api-key": " " }, null],
+ ["empty Bearer token", { authorization: "Bearer " }, null],
+ [
+ "Bearer token without dbdy_ prefix",
+ { authorization: "Bearer invalid_token" },
+ null,
+ ],
+ ["Bearer token below minimum length", { authorization: "Bearer dbdy_" }, null],
+ [
+ "Bearer token above maximum length",
+ { authorization: `Bearer dbdy_${"a".repeat(200)}` },
+ null,
+ ],
+ ["x-api-key without dbdy_ prefix", { "x-api-key": "invalid_token" }, null],
+ ["x-api-key below minimum length", { "x-api-key": "dbdy_" }, null],
+ [
+ "x-api-key above maximum length",
+ { "x-api-key": `dbdy_${"a".repeat(200)}` },
+ null,
+ ],
+ ])("%s", (_name, headers, expected) => {
+ expect(extractSecret(new Headers(headers))).toBe(expected);
});
+});
- it("prefers x-api-key over Bearer", () => {
- const headers = new Headers({
- "x-api-key": "dbdy_xapikey",
- authorization: "Bearer dbdy_bearer",
+describe("resolveApiKeySecret", () => {
+ it.each([
+ ["wrong prefix", `sk_live_${"a".repeat(20)}`],
+ ["below minimum length", "dbdy_a"],
+ ["above maximum length", `dbdy_${"a".repeat(200)}`],
+ ])("rejects %s without hitting the database", async (_name, secret) => {
+ await expect(resolveApiKeySecret(secret)).resolves.toEqual({
+ key: null,
+ outcome: "invalid",
});
- expect(extractSecret(headers)).toBe("dbdy_xapikey");
+ expect(state.findFirst).not.toHaveBeenCalled();
});
- it("returns null when no API key present", () => {
- const headers = new Headers({});
- expect(extractSecret(headers)).toBeNull();
- });
+ it("reports unknown secrets as invalid with prefix diagnostics", async () => {
+ const secret = "dbdy_unknown_secret_value";
- it("returns null for non-Bearer authorization", () => {
- const headers = new Headers({ authorization: "Basic dXNlcjpwYXNz" });
- expect(extractSecret(headers)).toBeNull();
+ await expect(resolveApiKeySecret(secret)).resolves.toEqual({
+ key: null,
+ outcome: "invalid",
+ prefix: "dbdy",
+ start: secret.slice(0, 8),
+ });
+ expect(state.findFirst).toHaveBeenCalledTimes(1);
});
- it("trims whitespace from x-api-key", () => {
- const headers = new Headers({ "x-api-key": " dbdy_test123 " });
- expect(extractSecret(headers)).toBe("dbdy_test123");
- });
+ it.each([
+ ["disabled", { enabled: false }],
+ ["revoked", { revokedAt: new Date("2026-08-01T00:00:00.000Z") }],
+ ["expired", { expiresAt: new Date(Date.now() - 1000) }],
+ ])("returns no key for a %s key", async (outcome, overrides) => {
+ state.row = createMockKey(overrides);
- it("trims whitespace from Bearer token", () => {
- const headers = new Headers({ authorization: "Bearer dbdy_test123 " });
- expect(extractSecret(headers)).toBe("dbdy_test123");
- });
+ const result = await resolveApiKeySecret(VALID_SECRET);
- it("handles case-insensitive Bearer", () => {
- const headers = new Headers({ authorization: "bearer dbdy_test123" });
- expect(extractSecret(headers)).toBe("dbdy_test123");
+ expect(result.outcome).toBe(outcome);
+ expect(result.key).toBeNull();
});
- it("returns null for empty x-api-key after trim", () => {
- const headers = new Headers({ "x-api-key": " " });
- expect(extractSecret(headers)).toBeNull();
- });
+ it("resolves an enabled key with a future expiration", async () => {
+ state.row = createMockKey({
+ expiresAt: new Date(Date.now() + 86_400_000),
+ });
- it("handles BEARER in uppercase", () => {
- const headers = new Headers({ authorization: "BEARER dbdy_test123" });
- expect(extractSecret(headers)).toBe("dbdy_test123");
- });
+ const result = await resolveApiKeySecret(VALID_SECRET);
- it("rejects Bearer token without dbdy_ prefix", () => {
- const headers = new Headers({ authorization: "Bearer invalid_token" });
- expect(extractSecret(headers)).toBeNull();
+ expect(result.outcome).toBe("ok");
+ expect(result.key?.id).toBe("key-123");
+ expect(result.prefix).toBe("dbdy");
+ expect(result.start).toBe(VALID_SECRET.slice(0, 8));
});
- it("rejects Bearer token that is too short", () => {
- const headers = new Headers({ authorization: "Bearer dbdy_" });
- expect(extractSecret(headers)).toBeNull();
- });
+ it("records last-used once per debounce window without blocking resolution", async () => {
+ state.row = createMockKey();
- it("rejects Bearer token that is too long", () => {
- const longToken = "dbdy_" + "a".repeat(200);
- const headers = new Headers({ authorization: `Bearer ${longToken}` });
- expect(extractSecret(headers)).toBeNull();
- });
+ await resolveApiKeySecret(VALID_SECRET);
+ await vi.waitFor(() => expect(state.lastUsedWrites).toBe(1));
+ expect(state.redisSet).toHaveBeenCalledWith(
+ "api-key:last-used-lock:key-123",
+ "1",
+ "EX",
+ expect.any(Number),
+ "NX"
+ );
- it("rejects empty Bearer token", () => {
- const headers = new Headers({ authorization: "Bearer " });
- expect(extractSecret(headers)).toBeNull();
+ state.lockReply = null;
+ await resolveApiKeySecret(VALID_SECRET);
+ await vi.waitFor(() => expect(state.redisSet).toHaveBeenCalledTimes(2));
+ expect(state.lastUsedWrites).toBe(1);
});
+});
- it("rejects Bearer token with only whitespace", () => {
- const headers = new Headers({ authorization: "Bearer " });
- expect(extractSecret(headers)).toBeNull();
+describe("resolveApiKey", () => {
+ it("returns missing when no API key headers are present", async () => {
+ await expect(resolveApiKey(new Headers())).resolves.toEqual({
+ key: null,
+ outcome: "missing",
+ });
+ expect(state.findFirst).not.toHaveBeenCalled();
});
- it("rejects x-api-key without dbdy_ prefix", () => {
- const headers = new Headers({ "x-api-key": "invalid_token" });
- expect(extractSecret(headers)).toBeNull();
+ it("returns invalid when a header is present but malformed", async () => {
+ await expect(
+ resolveApiKey(new Headers({ "x-api-key": "invalid_token" }))
+ ).resolves.toEqual({ key: null, outcome: "invalid" });
+ expect(state.findFirst).not.toHaveBeenCalled();
});
- it("rejects x-api-key that is too short", () => {
- const headers = new Headers({ "x-api-key": "dbdy_" });
- expect(extractSecret(headers)).toBeNull();
- });
+ it("resolves a well-formed header against the database", async () => {
+ state.row = createMockKey();
+
+ const result = await resolveApiKey(
+ new Headers({ "x-api-key": VALID_SECRET })
+ );
- it("rejects x-api-key that is too long", () => {
- const longToken = "dbdy_" + "a".repeat(200);
- const headers = new Headers({ "x-api-key": longToken });
- expect(extractSecret(headers)).toBeNull();
+ expect(result.outcome).toBe("ok");
+ expect(result.key?.id).toBe("key-123");
});
});
@@ -173,519 +265,209 @@ describe("getEffectiveScopes", () => {
expect(getEffectiveScopes(null)).toEqual([]);
});
- it("returns key scopes when no resources", () => {
+ it("returns base scopes when metadata has no resources", () => {
const key = createMockKey({ scopes: ["read:data", "write:data"] });
- const scopes = getEffectiveScopes(key);
- expect(scopes).toContain("read:data");
- expect(scopes).toContain("write:data");
- expect(scopes).toHaveLength(2);
- });
-
- it("returns key scopes when resources is empty", () => {
- const key = createMockKey({
- scopes: ["read:data"],
- metadata: { resources: {} },
- });
- const scopes = getEffectiveScopes(key);
- expect(scopes).toEqual(["read:data"]);
+ expect(getEffectiveScopes(key).sort()).toEqual(["read:data", "write:data"]);
});
- it("includes global resource scopes", () => {
+ it("handles null metadata", () => {
const key = createMockKey({
+ metadata: null as unknown as Record,
scopes: ["read:data"],
- metadata: { resources: { global: ["admin:apikeys"] } },
});
- const scopes = getEffectiveScopes(key);
- expect(scopes).toContain("read:data");
- expect(scopes).toContain("admin:apikeys");
+ expect(getEffectiveScopes(key)).toEqual(["read:data"]);
});
- it("includes resource-specific scopes when resource matches", () => {
+ it("combines base, global, and matching resource scopes", () => {
const key = createMockKey({
- scopes: ["read:data"],
metadata: {
resources: {
+ global: ["track:events"],
"website:site-123": ["write:data", "read:analytics"],
},
},
- });
-
- const scopes = getEffectiveScopes(key, "website:site-123");
- expect(scopes).toContain("read:data");
- expect(scopes).toContain("write:data");
- expect(scopes).toContain("read:analytics");
- });
-
- it("does not include resource scopes when resource does not match", () => {
- const key = createMockKey({
scopes: ["read:data"],
- metadata: {
- resources: {
- "website:site-123": ["write:data"],
- },
- },
});
- const scopes = getEffectiveScopes(key, "website:site-456");
- expect(scopes).toContain("read:data");
- expect(scopes).not.toContain("write:data");
+ expect(getEffectiveScopes(key, "website:site-123").sort()).toEqual([
+ "read:analytics",
+ "read:data",
+ "track:events",
+ "write:data",
+ ]);
});
- it("combines global and resource-specific scopes", () => {
+ it("excludes scopes of non-matching resources", () => {
const key = createMockKey({
+ metadata: { resources: { "website:site-123": ["write:data"] } },
scopes: ["read:data"],
- metadata: {
- resources: {
- global: ["track:events"],
- "website:site-123": ["write:data"],
- },
- },
});
- const scopes = getEffectiveScopes(key, "website:site-123");
- expect(scopes).toContain("read:data");
- expect(scopes).toContain("track:events");
- expect(scopes).toContain("write:data");
+ expect(getEffectiveScopes(key, "website:site-456")).toEqual(["read:data"]);
});
- it("deduplicates scopes", () => {
+ it("deduplicates scopes repeated across base and resources", () => {
const key = createMockKey({
- scopes: ["read:data"],
metadata: {
resources: {
global: ["read:data"],
"website:site-123": ["read:data"],
},
},
- });
-
- const scopes = getEffectiveScopes(key, "website:site-123");
- expect(scopes.filter((s) => s === "read:data")).toHaveLength(1);
- });
-
- it("handles key with empty scopes array", () => {
- const key = createMockKey({
- scopes: [],
- metadata: { resources: { global: ["read:data"] } },
- });
- const scopes = getEffectiveScopes(key);
- expect(scopes).toEqual(["read:data"]);
- });
-
- it("handles null metadata", () => {
- const key = createMockKey({
scopes: ["read:data"],
- metadata: null as unknown as Record,
});
- const scopes = getEffectiveScopes(key);
- expect(scopes).toEqual(["read:data"]);
- });
-});
-describe("hasKeyScope", () => {
- it("returns false for null key", () => {
- expect(hasKeyScope(null, "read:data")).toBe(false);
- });
-
- it("returns true when key has scope in base scopes", () => {
- const key = createMockKey({ scopes: ["read:data", "write:data"] });
- expect(hasKeyScope(key, "read:data")).toBe(true);
- });
-
- it("returns false when key does not have scope", () => {
- const key = createMockKey({ scopes: ["read:data"] });
- expect(hasKeyScope(key, "admin:apikeys")).toBe(false);
- });
-
- it("checks resource-specific scopes with matching resource", () => {
- const key = createMockKey({
- scopes: [],
- metadata: {
- resources: { "website:site-123": ["read:analytics"] },
- },
- });
-
- expect(hasKeyScope(key, "read:analytics", "website:site-123")).toBe(true);
- });
-
- it("returns false for resource-specific scopes with non-matching resource", () => {
- const key = createMockKey({
- scopes: [],
- metadata: {
- resources: { "website:site-123": ["read:analytics"] },
- },
- });
-
- expect(hasKeyScope(key, "read:analytics", "website:site-456")).toBe(false);
- });
-
- it("checks global scopes even when resource is specified", () => {
- const key = createMockKey({
- scopes: ["read:data"],
- metadata: {},
- });
-
- expect(hasKeyScope(key, "read:data", "website:site-123")).toBe(true);
+ expect(getEffectiveScopes(key, "website:site-123")).toEqual(["read:data"]);
});
});
-describe("hasKeyAnyScope", () => {
- it("returns false for null key", () => {
+describe("scope predicates", () => {
+ it("all predicates deny a null key", () => {
+ expect(hasKeyScope(null, "read:data")).toBe(false);
expect(hasKeyAnyScope(null, ["read:data"])).toBe(false);
+ expect(hasKeyAllScopes(null, ["read:data"])).toBe(false);
+ expect(hasWebsiteScope(null, "site-123", "read:data")).toBe(false);
+ expect(hasWebsiteAnyScope(null, "site-123", ["read:data"])).toBe(false);
+ expect(hasWebsiteAllScopes(null, "site-123", ["read:data"])).toBe(false);
+ expect(hasGlobalAccess(null)).toBe(false);
+ expect(resolveEffectiveScopesForWebsite(null, "site-123").size).toBe(0);
+ expect(getAccessibleWebsiteIds(null)).toEqual([]);
});
- it("returns true when key has any of the scopes", () => {
- const key = createMockKey({ scopes: ["read:data"] });
- expect(hasKeyAnyScope(key, ["read:data", "write:data"])).toBe(true);
- });
-
- it("returns false when key has none of the scopes", () => {
- const key = createMockKey({ scopes: ["track:events"] });
- expect(hasKeyAnyScope(key, ["read:data", "write:data"])).toBe(false);
- });
+ it("hasKeyScope checks base, resource, and global scopes", () => {
+ const base = createMockKey({ scopes: ["read:data"] });
+ expect(hasKeyScope(base, "read:data")).toBe(true);
+ expect(hasKeyScope(base, "admin:apikeys")).toBe(false);
+ expect(hasKeyScope(base, "read:data", "website:site-123")).toBe(true);
- it("checks resource-specific scopes", () => {
- const key = createMockKey({
- scopes: [],
+ const scoped = createMockKey({
metadata: { resources: { "website:site-123": ["read:analytics"] } },
+ scopes: [],
});
- expect(
- hasKeyAnyScope(key, ["read:analytics", "write:data"], "website:site-123")
- ).toBe(true);
- });
-});
-
-describe("hasKeyAllScopes", () => {
- it("returns false for null key", () => {
- expect(hasKeyAllScopes(null, ["read:data"])).toBe(false);
- });
-
- it("returns true when key has all scopes", () => {
- const key = createMockKey({ scopes: ["read:data", "write:data"] });
- expect(hasKeyAllScopes(key, ["read:data", "write:data"])).toBe(true);
+ expect(hasKeyScope(scoped, "read:analytics", "website:site-123")).toBe(
+ true
+ );
+ expect(hasKeyScope(scoped, "read:analytics", "website:site-456")).toBe(
+ false
+ );
});
- it("returns false when key is missing a scope", () => {
+ it("hasKeyAnyScope passes when any scope matches", () => {
const key = createMockKey({ scopes: ["read:data"] });
- expect(hasKeyAllScopes(key, ["read:data", "write:data"])).toBe(false);
+ expect(hasKeyAnyScope(key, ["read:data", "write:data"])).toBe(true);
+ expect(hasKeyAnyScope(key, ["track:events", "write:data"])).toBe(false);
});
- it("combines base and resource scopes", () => {
+ it("hasKeyAllScopes requires every scope across base and resources", () => {
const key = createMockKey({
- scopes: ["read:data"],
metadata: { resources: { "website:site-123": ["write:data"] } },
+ scopes: ["read:data"],
});
expect(
hasKeyAllScopes(key, ["read:data", "write:data"], "website:site-123")
).toBe(true);
+ expect(hasKeyAllScopes(key, ["read:data", "write:data"])).toBe(false);
});
});
-describe("resolveEffectiveScopesForWebsite", () => {
- it("returns empty set for null key", () => {
- const scopes = resolveEffectiveScopesForWebsite(null, "site-123");
- expect(scopes.size).toBe(0);
- });
-
- it("returns scopes for website resource", () => {
- const key = createMockKey({
- scopes: ["read:data"],
- metadata: {
- resources: { "website:site-123": ["write:data"] },
- },
- });
-
- const scopes = resolveEffectiveScopesForWebsite(key, "site-123");
- expect(scopes.has("read:data")).toBe(true);
- expect(scopes.has("write:data")).toBe(true);
- });
-
- it("formats websiteId with website: prefix", () => {
- const key = createMockKey({
- scopes: [],
- metadata: {
- resources: { "website:my-site": ["read:analytics"] },
- },
- });
-
- const scopes = resolveEffectiveScopesForWebsite(key, "my-site");
- expect(scopes.has("read:analytics")).toBe(true);
- });
-
- it("includes global scopes", () => {
- const key = createMockKey({
- scopes: [],
- metadata: {
- resources: { global: ["track:events"] },
- },
- });
-
- const scopes = resolveEffectiveScopesForWebsite(key, "site-123");
- expect(scopes.has("track:events")).toBe(true);
- });
-});
-
-describe("hasWebsiteScope", () => {
- it("returns false for null key", () => {
- expect(hasWebsiteScope(null, "site-123", "read:data")).toBe(false);
- });
-
- it("returns true when key has website-specific scope", () => {
+describe("website scope helpers", () => {
+ it("hasWebsiteScope resolves the website resource prefix", () => {
const key = createMockKey({
+ metadata: { resources: { "website:site-123": ["read:analytics"] } },
scopes: [],
- metadata: {
- resources: { "website:site-123": ["read:analytics"] },
- },
});
-
expect(hasWebsiteScope(key, "site-123", "read:analytics")).toBe(true);
+ expect(hasWebsiteScope(key, "site-456", "read:analytics")).toBe(false);
});
- it("returns true when key has scope in base scopes", () => {
- const key = createMockKey({ scopes: ["read:data"] });
- expect(hasWebsiteScope(key, "site-123", "read:data")).toBe(true);
- });
-
- it("returns true when key has scope in global resources", () => {
- const key = createMockKey({
- scopes: [],
- metadata: { resources: { global: ["track:events"] } },
- });
- expect(hasWebsiteScope(key, "site-123", "track:events")).toBe(true);
- });
-
- it("returns false when key lacks required scope", () => {
- const key = createMockKey({ scopes: ["read:data"] });
- expect(hasWebsiteScope(key, "site-123", "admin:apikeys")).toBe(false);
- });
-
- it("returns false when scope exists for different website", () => {
- const key = createMockKey({
- scopes: [],
- metadata: {
- resources: { "website:site-456": ["read:analytics"] },
- },
- });
-
- expect(hasWebsiteScope(key, "site-123", "read:analytics")).toBe(false);
- });
-});
-
-describe("hasWebsiteAnyScope", () => {
- it("returns false for null key", () => {
- expect(hasWebsiteAnyScope(null, "site-123", ["read:data"])).toBe(false);
- });
-
- it("returns true when key has any of the scopes for website", () => {
- const key = createMockKey({
- scopes: [],
- metadata: { resources: { "website:site-123": ["read:analytics"] } },
- });
+ it("hasWebsiteScope accepts base and global scopes for any website", () => {
expect(
- hasWebsiteAnyScope(key, "site-123", ["read:analytics", "write:data"])
+ hasWebsiteScope(
+ createMockKey({ scopes: ["read:data"] }),
+ "site-123",
+ "read:data"
+ )
).toBe(true);
- });
-
- it("returns false when key has none of the scopes", () => {
- const key = createMockKey({ scopes: ["track:events"] });
expect(
- hasWebsiteAnyScope(key, "site-123", ["read:analytics", "write:data"])
- ).toBe(false);
- });
-});
-
-describe("hasWebsiteAllScopes", () => {
- it("returns false for null key", () => {
- expect(hasWebsiteAllScopes(null, "site-123", ["read:data"])).toBe(false);
+ hasWebsiteScope(
+ createMockKey({
+ metadata: { resources: { global: ["track:events"] } },
+ scopes: [],
+ }),
+ "site-123",
+ "track:events"
+ )
+ ).toBe(true);
});
- it("returns true when key has all scopes for website", () => {
+ it("hasWebsiteAnyScope and hasWebsiteAllScopes evaluate against the website resource", () => {
const key = createMockKey({
+ metadata: { resources: { "website:site-123": ["read:analytics"] } },
scopes: ["read:data"],
- metadata: { resources: { "website:site-123": ["write:data"] } },
});
expect(
- hasWebsiteAllScopes(key, "site-123", ["read:data", "write:data"])
+ hasWebsiteAnyScope(key, "site-123", ["read:analytics", "write:data"])
+ ).toBe(true);
+ expect(hasWebsiteAnyScope(key, "site-456", ["read:analytics"])).toBe(false);
+ expect(
+ hasWebsiteAllScopes(key, "site-123", ["read:data", "read:analytics"])
).toBe(true);
- });
-
- it("returns false when key is missing a scope", () => {
- const key = createMockKey({
- scopes: [],
- metadata: { resources: { "website:site-123": ["read:analytics"] } },
- });
expect(
hasWebsiteAllScopes(key, "site-123", ["read:analytics", "write:data"])
).toBe(false);
});
-});
-
-describe("key validity simulation (matches getApiKeyFromHeader logic)", () => {
- const isKeyValid = (key: ApiKeyRow | null): boolean => {
- if (!key?.enabled || key.revokedAt || isExpired(key.expiresAt)) {
- return false;
- }
- return true;
- };
-
- it("returns false for null key", () => {
- expect(isKeyValid(null)).toBe(false);
- });
-
- it("returns false for disabled key", () => {
- const key = createMockKey({ enabled: false });
- expect(isKeyValid(key)).toBe(false);
- });
-
- it("returns false for revoked key", () => {
- const key = createMockKey({ revokedAt: new Date() });
- expect(isKeyValid(key)).toBe(false);
- });
-
- it("returns false for expired key", () => {
- const key = createMockKey({
- expiresAt: new Date(Date.now() - 1000).toISOString(),
- });
- expect(isKeyValid(key)).toBe(false);
- });
-
- it("returns true for valid enabled key", () => {
- const key = createMockKey({
- enabled: true,
- revokedAt: null,
- expiresAt: null,
- });
- expect(isKeyValid(key)).toBe(true);
- });
- it("returns true for key with future expiration", () => {
+ it("resolveEffectiveScopesForWebsite returns the combined scope set", () => {
const key = createMockKey({
- enabled: true,
- expiresAt: new Date(Date.now() + 86_400_000).toISOString(),
- });
- expect(isKeyValid(key)).toBe(true);
- });
-
- it("returns false for disabled key even with valid expiration", () => {
- const key = createMockKey({
- enabled: false,
- expiresAt: new Date(Date.now() + 86_400_000).toISOString(),
+ metadata: {
+ resources: {
+ global: ["track:events"],
+ "website:site-123": ["write:data"],
+ },
+ },
+ scopes: ["read:data"],
});
- expect(isKeyValid(key)).toBe(false);
- });
- it("returns false for revoked key even if enabled", () => {
- const key = createMockKey({
- enabled: true,
- revokedAt: new Date(),
- });
- expect(isKeyValid(key)).toBe(false);
+ expect(resolveEffectiveScopesForWebsite(key, "site-123")).toEqual(
+ new Set(["read:data", "track:events", "write:data"])
+ );
});
});
describe("hasGlobalAccess", () => {
- it("returns false for null key", () => {
- expect(hasGlobalAccess(null)).toBe(false);
- });
-
- it("returns false when no resources", () => {
- const key = createMockKey({ metadata: {} });
- expect(hasGlobalAccess(key)).toBe(false);
- });
-
- it("returns false when no global resource", () => {
- const key = createMockKey({
- metadata: { resources: { "website:site-123": ["read:data"] } },
- });
- expect(hasGlobalAccess(key)).toBe(false);
- });
-
- it("returns false when global resource is empty", () => {
- const key = createMockKey({
- metadata: { resources: { global: [] } },
- });
- expect(hasGlobalAccess(key)).toBe(false);
- });
-
- it("returns true when global resource has scopes", () => {
- const key = createMockKey({
- metadata: { resources: { global: ["read:data"] } },
- });
- expect(hasGlobalAccess(key)).toBe(true);
+ it.each([
+ ["no resources", {}, false],
+ ["only website resources", { resources: { "website:site-123": ["read:data"] } }, false],
+ ["empty global resource", { resources: { global: [] } }, false],
+ ["populated global resource", { resources: { global: ["read:data"] } }, true],
+ ])("%s -> %s", (_name, metadata, expected) => {
+ expect(hasGlobalAccess(createMockKey({ metadata }))).toBe(expected);
});
});
describe("getAccessibleWebsiteIds", () => {
- it("returns empty array for null key", () => {
- expect(getAccessibleWebsiteIds(null)).toEqual([]);
- });
-
- it("returns empty array when no resources", () => {
- const key = createMockKey({ metadata: {} });
- expect(getAccessibleWebsiteIds(key)).toEqual([]);
- });
-
- it("returns empty array when no website resources", () => {
- const key = createMockKey({
- metadata: { resources: { global: ["read:data"] } },
- });
- expect(getAccessibleWebsiteIds(key)).toEqual([]);
+ it("returns empty array when no website resources exist", () => {
+ expect(getAccessibleWebsiteIds(createMockKey({ metadata: {} }))).toEqual(
+ []
+ );
+ expect(
+ getAccessibleWebsiteIds(
+ createMockKey({ metadata: { resources: { global: ["read:data"] } } })
+ )
+ ).toEqual([]);
});
- it("returns website ids from resources", () => {
+ it("extracts ids from website resources only", () => {
const key = createMockKey({
metadata: {
resources: {
+ global: ["track:events"],
"website:site-1": ["read:data"],
"website:site-2": ["write:data"],
- global: ["track:events"],
},
},
});
- const ids = getAccessibleWebsiteIds(key);
- expect(ids).toContain("site-1");
- expect(ids).toContain("site-2");
- expect(ids).toHaveLength(2);
- });
-
- it("extracts id correctly from website:id format", () => {
- const key = createMockKey({
- metadata: {
- resources: { "website:my-long-id-123": ["read:data"] },
- },
- });
- expect(getAccessibleWebsiteIds(key)).toEqual(["my-long-id-123"]);
- });
-});
-
-describe("keypal utilities used in implementation", () => {
- it("hasScope returns true when scope exists", () => {
- expect(hasScope(["read:data", "write:data"], "read:data")).toBe(true);
- });
-
- it("hasScope returns false when scope does not exist", () => {
- expect(hasScope(["read:data"], "write:data")).toBe(false);
- });
-
- it("hasScope handles undefined scopes", () => {
- expect(hasScope(undefined, "read:data")).toBe(false);
- });
-
- it("isExpired returns false for null", () => {
- expect(isExpired(null)).toBe(false);
- });
-
- it("isExpired returns false for undefined", () => {
- expect(isExpired(undefined)).toBe(false);
- });
-
- it("isExpired returns true for past date", () => {
- const past = new Date(Date.now() - 1000).toISOString();
- expect(isExpired(past)).toBe(true);
- });
- it("isExpired returns false for future date", () => {
- const future = new Date(Date.now() + 100_000).toISOString();
- expect(isExpired(future)).toBe(false);
+ expect(getAccessibleWebsiteIds(key).sort()).toEqual(["site-1", "site-2"]);
});
});
diff --git a/apps/api/src/lib/autumn-mount.ts b/apps/api/src/lib/autumn-mount.ts
index 80e884013a..0c8d444c67 100644
--- a/apps/api/src/lib/autumn-mount.ts
+++ b/apps/api/src/lib/autumn-mount.ts
@@ -1,4 +1,3 @@
-/** Elysia `.mount("/api/autumn", …)` strips the prefix, so the inner pathname is `/attach` not `/api/autumn/attach`. Autumn's router matches full paths under `/api/autumn`. */
const AUTUMN_API_PREFIX = "/api/autumn";
export function withAutumnApiPath(request: Request): Request {
diff --git a/apps/api/src/lib/tcc-otel.ts b/apps/api/src/lib/tcc-otel.ts
index d5e653212d..78bf99db4b 100644
--- a/apps/api/src/lib/tcc-otel.ts
+++ b/apps/api/src/lib/tcc-otel.ts
@@ -8,12 +8,6 @@ import {
import pkg from "../../package.json";
let sdk: NodeSDK | null = null;
-
-/**
- * Registers OpenTelemetry with The Context Company's span processor so
- * Vercel AI SDK `experimental_telemetry` spans (ai.*) are exported to TCC.
- * No-op when TCC_API_KEY is unset (local dev without observability).
- */
export function initTccTracing(): void {
if (sdk || !process.env.TCC_API_KEY) {
return;
diff --git a/apps/api/src/middleware/api-key-rate-limit.ts b/apps/api/src/middleware/api-key-rate-limit.ts
index 3f51f4b868..3fe14a01fe 100644
--- a/apps/api/src/middleware/api-key-rate-limit.ts
+++ b/apps/api/src/middleware/api-key-rate-limit.ts
@@ -123,7 +123,6 @@ export function releaseApiKeyInFlight(
}
export interface EnforceApiKeyRateLimitOptions {
- /** undefined means auth was not pre-resolved; null means it resolved without a key. */
apiKey?: ApiKeyRow | null;
dependencies?: ApiKeyAdmissionDependencies;
}
@@ -196,12 +195,6 @@ export function enforceApiKeyInFlightLimit(
"Too many concurrent API key requests"
);
}
-
-/**
- * Enforce an API key's configured distributed rolling-window limit after auth
- * resolution. Every presented key has already passed through the local
- * in-flight gate above.
- */
export async function enforceApiKeyRateLimit(
request: Request,
setHeader: (name: string, value: string) => void,
diff --git a/apps/api/src/middleware/website-auth.ts b/apps/api/src/middleware/website-auth.ts
deleted file mode 100644
index 0199fcbbd3..0000000000
--- a/apps/api/src/middleware/website-auth.ts
+++ /dev/null
@@ -1,195 +0,0 @@
-import {
- getApiKeyFromHeader,
- hasWebsiteScopeForOrganization,
- isApiKeyPresent,
-} from "@databuddy/api-keys/resolve";
-import { auth } from "@databuddy/auth";
-import { db } from "@databuddy/db";
-import { Elysia } from "elysia";
-import { getResolvedAuth } from "../lib/auth-wide-event";
-import { getCachedWebsite, getTimezone } from "@databuddy/ai/lib/website-utils";
-
-interface SessionUser {
- email: string;
- id: string;
- name: string;
-}
-
-function json(status: number, body: unknown) {
- return new Response(JSON.stringify(body), {
- status,
- headers: { "Content-Type": "application/json" },
- });
-}
-
-function getSessionUser(
- session: Awaited> | null
-): SessionUser | null {
- if (!session?.user) {
- return null;
- }
-
- return {
- email: session.user.email,
- id: session.user.id,
- name: session.user.name,
- };
-}
-
-export function websiteAuth() {
- return new Elysia()
- .derive(async ({ request }) => {
- if (isPreflight(request)) {
- return {
- user: null,
- session: null,
- website: undefined,
- timezone: "UTC",
- _apiKey: null,
- _apiKeyPresent: false,
- _authChecked: true,
- } as const;
- }
-
- const url = new URL(request.url);
- const websiteId = url.searchParams.get("website_id");
-
- const preResolved = getResolvedAuth(request.headers);
- let sessionUser: SessionUser | null = null;
- let session: Awaited> | null =
- null;
- let apiKey: Awaited> | null = null;
- const apiKeyPresent = isApiKeyPresent(request.headers);
-
- if (preResolved) {
- session = preResolved.session;
- sessionUser = getSessionUser(session);
- apiKey = preResolved.apiKeyResult?.key ?? null;
- } else {
- const [resolvedApiKey, resolvedSession] = await Promise.all([
- apiKeyPresent ? getApiKeyFromHeader(request.headers) : null,
- auth.api.getSession({ headers: request.headers }),
- ]);
- session = resolvedSession;
- sessionUser = getSessionUser(session);
- apiKey = resolvedApiKey;
- }
-
- const website = websiteId ? await getCachedWebsite(websiteId) : undefined;
-
- const timezone = session?.user
- ? await getTimezone(request, session)
- : await getTimezone(request, null);
-
- return {
- user: sessionUser,
- session,
- website,
- timezone,
- _apiKey: apiKey,
- _apiKeyPresent: apiKeyPresent,
- _authChecked: true,
- } as const;
- })
- .onBeforeHandle(({ user, website, _apiKey, _apiKeyPresent, request }) => {
- if (isPreflight(request)) {
- return;
- }
-
- const url = new URL(request.url);
- const websiteId = url.searchParams.get("website_id");
-
- if (!websiteId) {
- if (user || _apiKey) {
- return null;
- }
- return json(401, {
- success: false,
- error: "Authentication required",
- code: "AUTH_REQUIRED",
- });
- }
-
- return checkWebsiteAuth(
- websiteId,
- user,
- website ?? null,
- _apiKey,
- _apiKeyPresent
- );
- });
-}
-
-function isPreflight(request: Request): boolean {
- return request.method === "OPTIONS" || request.method === "HEAD";
-}
-
-async function checkWebsiteAuth(
- _websiteId: string,
- sessionUser: SessionUser | null,
- website: Awaited> | null,
- apiKey: Awaited> | null,
- apiKeyPresent: boolean
-): Promise {
- if (!website) {
- return json(404, {
- success: false,
- error: "Website not found",
- code: "NOT_FOUND",
- });
- }
- if (website.isPublic) {
- return null;
- }
-
- if (sessionUser) {
- if (!website.organizationId) {
- return json(403, {
- success: false,
- error: "Website must belong to a workspace",
- code: "FORBIDDEN",
- });
- }
-
- const membership = await db.query.member.findFirst({
- where: { userId: sessionUser.id, organizationId: website.organizationId },
- columns: {
- id: true,
- },
- });
-
- if (membership) {
- return null;
- }
-
- return json(403, {
- success: false,
- error: "Access denied to this website",
- code: "FORBIDDEN",
- });
- }
-
- if (!apiKeyPresent) {
- return json(401, {
- success: false,
- error: "Authentication required",
- code: "AUTH_REQUIRED",
- });
- }
- if (!apiKey) {
- return json(401, {
- success: false,
- error: "Invalid or expired API key",
- code: "AUTH_REQUIRED",
- });
- }
- const ok = hasWebsiteScopeForOrganization(apiKey, website, "read:data");
- if (!ok) {
- return json(403, {
- success: false,
- error: "Insufficient permissions",
- code: "FORBIDDEN",
- });
- }
- return null;
-}
diff --git a/apps/api/src/routes/public/flags-boundary.test.ts b/apps/api/src/routes/public/flags-boundary.test.ts
index e8c33b877b..296c6900bb 100644
--- a/apps/api/src/routes/public/flags-boundary.test.ts
+++ b/apps/api/src/routes/public/flags-boundary.test.ts
@@ -3,6 +3,7 @@ import { Elysia } from "elysia";
import { describe, expect, it, vi } from "vitest";
const state = vi.hoisted(() => ({
+ findFirst: vi.fn(async () => null as unknown),
flags: [
{
defaultValue: true,
@@ -18,6 +19,7 @@ const state = vi.hoisted(() => ({
variants: null,
},
],
+ rateLimited: false,
}));
vi.mock("@databuddy/db", async (importOriginal) => ({
@@ -25,6 +27,7 @@ vi.mock("@databuddy/db", async (importOriginal) => ({
db: {
query: {
flags: {
+ findFirst: state.findFirst,
findMany: vi.fn(async () => state.flags),
},
},
@@ -37,8 +40,13 @@ vi.mock("@databuddy/redis", async (importOriginal) => ({
}));
vi.mock("@databuddy/redis/rate-limit", () => ({
- getRateLimitHeaders: () => ({}),
- ratelimit: async () => ({ success: true }),
+ getRateLimitHeaders: () => ({ "x-ratelimit-remaining": "0" }),
+ ratelimit: async () => ({
+ limit: 600,
+ remaining: state.rateLimited ? 0 : 599,
+ reset: Date.now() + 60_000,
+ success: !state.rateLimited,
+ }),
}));
const { flagsRoute } = await import("./flags");
@@ -111,4 +119,69 @@ describe("public bulk flags boundary", () => {
});
expect(postResponse.status).toBe(422);
});
+
+ it("rejects requests without a usable clientId", async () => {
+ const missing = await request("/v1/flags/bulk");
+ expect(missing.status).toBe(422);
+
+ const blank = await request("/v1/flags/bulk?clientId=");
+ expect(blank.status).toBe(400);
+ expect(await blank.json()).toMatchObject({
+ count: 0,
+ error: "Missing required clientId parameter",
+ });
+ });
+});
+
+describe("public flag evaluation boundary", () => {
+ it("rejects evaluation without a usable clientId or key", async () => {
+ const missingParams = await request("/v1/flags/evaluate?key=&clientId=");
+ expect(missingParams.status).toBe(400);
+ expect(await missingParams.json()).toMatchObject({
+ enabled: false,
+ reason: "MISSING_REQUIRED_PARAMS",
+ });
+
+ const missingClientId = await request("/v1/flags/evaluate?key=some-flag");
+ expect(missingClientId.status).toBe(422);
+ });
+
+ it("caches missing flags so repeated misses skip the database", async () => {
+ const path = "/v1/flags/evaluate?key=absent-flag&clientId=neg_cache_site";
+
+ const first = await request(path);
+ const second = await request(path);
+
+ expect(first.status).toBe(200);
+ expect(await first.json()).toMatchObject({
+ enabled: false,
+ reason: "FLAG_NOT_FOUND",
+ });
+ expect(await second.json()).toMatchObject({ reason: "FLAG_NOT_FOUND" });
+ expect(state.findFirst).toHaveBeenCalledTimes(1);
+ });
+
+ it("returns 429 with rate limit headers when the per-client budget is exhausted", async () => {
+ state.rateLimited = true;
+ try {
+ const evaluate = await request(
+ "/v1/flags/evaluate?key=some-flag&clientId=limited_site"
+ );
+ expect(evaluate.status).toBe(429);
+ expect(evaluate.headers.get("x-ratelimit-remaining")).toBe("0");
+ expect(await evaluate.json()).toMatchObject({
+ enabled: false,
+ reason: "RATE_LIMITED",
+ });
+
+ const bulk = await request("/v1/flags/bulk?clientId=limited_site");
+ expect(bulk.status).toBe(429);
+ expect(await bulk.json()).toMatchObject({
+ count: 0,
+ reason: "RATE_LIMITED",
+ });
+ } finally {
+ state.rateLimited = false;
+ }
+ });
});
diff --git a/apps/api/src/routes/public/flags.test.ts b/apps/api/src/routes/public/flags.test.ts
index f6ca750ad1..94febbd0ae 100644
--- a/apps/api/src/routes/public/flags.test.ts
+++ b/apps/api/src/routes/public/flags.test.ts
@@ -66,173 +66,25 @@ describe("evaluateStringRule", () => {
enabled: true,
batch: false,
});
-
- it("handles all operators correctly", () => {
- expect(evaluateStringRule("user-123", rule("equals", "user-123"))).toBe(
- true
- );
- expect(evaluateStringRule("other", rule("equals", "user-123"))).toBe(false);
-
- expect(
- evaluateStringRule("user@company.com", rule("contains", "@company"))
- ).toBe(true);
- expect(
- evaluateStringRule("user@other.com", rule("contains", "@company"))
- ).toBe(false);
-
- expect(
- evaluateStringRule("admin-user", rule("starts_with", "admin-"))
- ).toBe(true);
- expect(
- evaluateStringRule("user-admin", rule("starts_with", "admin-"))
- ).toBe(false);
-
- expect(evaluateStringRule("file.com", rule("ends_with", ".com"))).toBe(
- true
- );
- expect(evaluateStringRule("file.org", rule("ends_with", ".com"))).toBe(
- false
- );
-
- const vals = ["a", "b", "c"];
- expect(evaluateStringRule("b", rule("in", undefined, vals))).toBe(true);
- expect(evaluateStringRule("z", rule("in", undefined, vals))).toBe(false);
- expect(evaluateStringRule("z", rule("not_in", undefined, vals))).toBe(true);
- expect(evaluateStringRule("a", rule("not_in", undefined, vals))).toBe(
- false
- );
-
- expect(evaluateStringRule("test", rule("unknown_op", "test"))).toBe(false);
- expect(evaluateStringRule(undefined, rule("equals", "test"))).toBe(false);
- });
-});
-
-describe("evaluateStringRule - email pattern matching", () => {
- const emailRule = (op: string, val?: string, vals?: string[]) => ({
- type: "email" as const,
- operator: op,
- value: val,
- values: vals,
- enabled: true,
- batch: false,
- });
-
- it("handles ends_with for email domain patterns", () => {
- // Common use case: target users by email domain
- expect(
- evaluateStringRule(
- "user@databuddy.cc",
- emailRule("ends_with", "@databuddy.cc")
- )
- ).toBe(true);
- expect(
- evaluateStringRule(
- "admin@databuddy.cc",
- emailRule("ends_with", "@databuddy.cc")
- )
- ).toBe(true);
- expect(
- evaluateStringRule(
- "user@other.com",
- emailRule("ends_with", "@databuddy.cc")
- )
- ).toBe(false);
- expect(
- evaluateStringRule("user@company.io", emailRule("ends_with", ".io"))
- ).toBe(true);
- expect(
- evaluateStringRule("user@company.com", emailRule("ends_with", ".io"))
- ).toBe(false);
- });
-
- it("handles starts_with for email prefix patterns", () => {
- // Target emails starting with a prefix (e.g., admin@, support@)
- expect(
- evaluateStringRule(
- "admin@company.com",
- emailRule("starts_with", "admin@")
- )
- ).toBe(true);
- expect(
- evaluateStringRule("admin@other.org", emailRule("starts_with", "admin@"))
- ).toBe(true);
- expect(
- evaluateStringRule("user@company.com", emailRule("starts_with", "admin@"))
- ).toBe(false);
- expect(
- evaluateStringRule(
- "support@company.com",
- emailRule("starts_with", "support")
- )
- ).toBe(true);
- });
-
- it("handles contains for partial email matching", () => {
- // Target emails containing a substring
- expect(
- evaluateStringRule(
- "user@company.internal.com",
- emailRule("contains", "internal")
- )
- ).toBe(true);
- expect(
- evaluateStringRule(
- "internal-user@company.com",
- emailRule("contains", "internal")
- )
- ).toBe(true);
- expect(
- evaluateStringRule("user@company.com", emailRule("contains", "internal"))
- ).toBe(false);
- expect(
- evaluateStringRule(
- "beta-tester@company.com",
- emailRule("contains", "beta")
- )
- ).toBe(true);
- });
-
- it("handles exact match for full email addresses", () => {
- expect(
- evaluateStringRule(
- "user@databuddy.cc",
- emailRule("equals", "user@databuddy.cc")
- )
- ).toBe(true);
- expect(
- evaluateStringRule(
- "other@databuddy.cc",
- emailRule("equals", "user@databuddy.cc")
- )
- ).toBe(false);
- });
-
- it("handles in/not_in for email lists", () => {
- const allowedEmails = ["admin@co.com", "support@co.com", "dev@co.com"];
- expect(
- evaluateStringRule(
- "admin@co.com",
- emailRule("in", undefined, allowedEmails)
- )
- ).toBe(true);
- expect(
- evaluateStringRule(
- "random@co.com",
- emailRule("in", undefined, allowedEmails)
- )
- ).toBe(false);
- expect(
- evaluateStringRule(
- "random@co.com",
- emailRule("not_in", undefined, allowedEmails)
- )
- ).toBe(true);
- expect(
- evaluateStringRule(
- "admin@co.com",
- emailRule("not_in", undefined, allowedEmails)
- )
- ).toBe(false);
+ const vals = ["a", "b", "c"];
+
+ it.each([
+ ["user-123", rule("equals", "user-123"), true],
+ ["other", rule("equals", "user-123"), false],
+ ["user@company.com", rule("contains", "@company"), true],
+ ["user@other.com", rule("contains", "@company"), false],
+ ["admin-user", rule("starts_with", "admin-"), true],
+ ["user-admin", rule("starts_with", "admin-"), false],
+ ["file.com", rule("ends_with", ".com"), true],
+ ["file.org", rule("ends_with", ".com"), false],
+ ["b", rule("in", undefined, vals), true],
+ ["z", rule("in", undefined, vals), false],
+ ["z", rule("not_in", undefined, vals), true],
+ ["a", rule("not_in", undefined, vals), false],
+ ["test", rule("unknown_op", "test"), false],
+ [undefined, rule("equals", "test"), false],
+ ])("%j with %j -> %s", (value, testRule, expected) => {
+ expect(evaluateStringRule(value, testRule)).toBe(expected);
});
});
@@ -246,32 +98,24 @@ describe("evaluateValueRule", () => {
batch: false,
});
- it("handles all operators correctly", () => {
- expect(evaluateValueRule(25, rule("equals", 25))).toBe(true);
- expect(evaluateValueRule(30, rule("equals", 25))).toBe(false);
- expect(evaluateValueRule("professional", rule("contains", "pro"))).toBe(
- true
- );
- expect(evaluateValueRule("basic", rule("contains", "pro"))).toBe(false);
- expect(
- evaluateValueRule("pro", rule("in", undefined, ["pro", "ent"]))
- ).toBe(true);
- expect(
- evaluateValueRule("free", rule("in", undefined, ["pro", "ent"]))
- ).toBe(false);
- expect(
- evaluateValueRule("ok", rule("not_in", undefined, ["bad", "worse"]))
- ).toBe(true);
- expect(
- evaluateValueRule("bad", rule("not_in", undefined, ["bad", "worse"]))
- ).toBe(false);
- expect(evaluateValueRule("val", rule("exists"))).toBe(true);
- expect(evaluateValueRule(0, rule("exists"))).toBe(true);
- expect(evaluateValueRule(undefined, rule("exists"))).toBe(false);
- expect(evaluateValueRule(null, rule("exists"))).toBe(false);
- expect(evaluateValueRule(undefined, rule("not_exists"))).toBe(true);
- expect(evaluateValueRule("x", rule("not_exists"))).toBe(false);
- expect(evaluateValueRule("x", rule("unknown_op"))).toBe(false);
+ it.each([
+ [25, rule("equals", 25), true],
+ [30, rule("equals", 25), false],
+ ["professional", rule("contains", "pro"), true],
+ ["basic", rule("contains", "pro"), false],
+ ["pro", rule("in", undefined, ["pro", "ent"]), true],
+ ["free", rule("in", undefined, ["pro", "ent"]), false],
+ ["ok", rule("not_in", undefined, ["bad", "worse"]), true],
+ ["bad", rule("not_in", undefined, ["bad", "worse"]), false],
+ ["val", rule("exists"), true],
+ [0, rule("exists"), true],
+ [undefined, rule("exists"), false],
+ [null, rule("exists"), false],
+ [undefined, rule("not_exists"), true],
+ ["x", rule("not_exists"), false],
+ ["x", rule("unknown_op"), false],
+ ])("%j with %j -> %s", (value, testRule, expected) => {
+ expect(evaluateValueRule(value, testRule)).toBe(expected);
});
});
@@ -848,23 +692,6 @@ describe("edge cases and stress tests", () => {
}
});
- it("handles rapid sequential evaluations", () => {
- const flag = {
- key: "rapid",
- type: "rollout" as const,
- rolloutPercentage: 50,
- status: "active" as const,
- defaultValue: false,
- };
-
- const start = performance.now();
- for (let i = 0; i < 10_000; i += 1) {
- evaluateFlag(flag, { userId: `u${i}` });
- }
- const duration = performance.now() - start;
- expect(duration).toBeLessThan(1000);
- });
-
it("handles percentage edge values", () => {
for (let i = 0; i < 100; i += 1) {
const ctx = { userId: randomId() };
diff --git a/apps/api/src/routes/public/flags.ts b/apps/api/src/routes/public/flags.ts
index 04b33cc867..f40edad867 100644
--- a/apps/api/src/routes/public/flags.ts
+++ b/apps/api/src/routes/public/flags.ts
@@ -133,6 +133,24 @@ const bulkFlagBodySchema = t.Object({
environment: t.Optional(t.String()),
});
+interface TargetGroupJoin {
+ targetGroup: {
+ deletedAt: Date | null;
+ id: string;
+ rules: FlagRule[];
+ } | null;
+}
+
+function resolveTargetGroups(joins: TargetGroupJoin[]): TargetGroupData[] {
+ const resolved: TargetGroupData[] = [];
+ for (const { targetGroup } of joins) {
+ if (targetGroup && !targetGroup.deletedAt) {
+ resolved.push({ id: targetGroup.id, rules: targetGroup.rules });
+ }
+ }
+ return resolved;
+}
+
const getCachedFlag = cacheable(
async (key: string, clientId: string, environment?: string) => {
const flag = await db.query.flags.findFirst({
@@ -162,16 +180,9 @@ const getCachedFlag = cacheable(
return null;
}
- const resolvedTargetGroups: TargetGroupData[] = flag.flagsToTargetGroups
- .filter((ftg) => ftg.targetGroup && !ftg.targetGroup.deletedAt)
- .map((ftg) => ({
- id: ftg.targetGroup.id,
- rules: ftg.targetGroup.rules,
- }));
-
return {
...flag,
- resolvedTargetGroups,
+ resolvedTargetGroups: resolveTargetGroups(flag.flagsToTargetGroups),
};
},
{
@@ -210,19 +221,10 @@ const getCachedFlagsForClient = cacheable(
},
});
- return flagsList.map((flag) => {
- const resolvedTargetGroups: TargetGroupData[] = flag.flagsToTargetGroups
- .filter((ftg) => ftg.targetGroup && !ftg.targetGroup.deletedAt)
- .map((ftg) => ({
- id: ftg.targetGroup.id,
- rules: ftg.targetGroup.rules,
- }));
-
- return {
- ...flag,
- resolvedTargetGroups,
- };
- });
+ return flagsList.map((flag) => ({
+ ...flag,
+ resolvedTargetGroups: resolveTargetGroups(flag.flagsToTargetGroups),
+ }));
},
{
expireInSec: 30,
@@ -285,19 +287,10 @@ const getCachedFlagsForUser = cacheable(
},
});
- return flagsList.map((flag) => {
- const resolvedTargetGroups: TargetGroupData[] = flag.flagsToTargetGroups
- .filter((ftg) => ftg.targetGroup && !ftg.targetGroup.deletedAt)
- .map((ftg) => ({
- id: ftg.targetGroup.id,
- rules: ftg.targetGroup.rules,
- }));
-
- return {
- ...flag,
- resolvedTargetGroups,
- };
- });
+ return flagsList.map((flag) => ({
+ ...flag,
+ resolvedTargetGroups: resolveTargetGroups(flag.flagsToTargetGroups),
+ }));
},
{
expireInSec: 30,
@@ -587,10 +580,6 @@ export function evaluateFlag(
};
}
- let enabled = Boolean(flag.defaultValue);
- let value = enabled;
- let reason = "DEFAULT_VALUE";
-
if (flag.type === "rollout") {
let identifier: string;
@@ -605,24 +594,22 @@ export function evaluateFlag(
identifier = context.userId || context.email || "anonymous";
}
- const hash = hashString(`${flag.key}:${identifier}`);
- const percentage = hash % 100;
- const rolloutPercentage = flag.rolloutPercentage || 0;
-
- enabled = percentage < rolloutPercentage;
- value = enabled;
- reason = enabled ? "ROLLOUT_ENABLED" : "ROLLOUT_DISABLED";
- } else {
- enabled = Boolean(flag.defaultValue);
- value = enabled;
- reason = "BOOLEAN_DEFAULT";
+ const percentage = hashString(`${flag.key}:${identifier}`) % 100;
+ const enabled = percentage < (flag.rolloutPercentage || 0);
+ return {
+ enabled,
+ value: enabled,
+ payload: enabled ? flag.payload : null,
+ reason: enabled ? "ROLLOUT_ENABLED" : "ROLLOUT_DISABLED",
+ };
}
+ const enabled = Boolean(flag.defaultValue);
return {
enabled,
- value,
+ value: enabled,
payload: enabled ? flag.payload : null,
- reason,
+ reason: "BOOLEAN_DEFAULT",
};
}
diff --git a/apps/api/src/routes/webhooks/autumn-inbox.ts b/apps/api/src/routes/webhooks/autumn-inbox.ts
index b58778efdc..1b58868c94 100644
--- a/apps/api/src/routes/webhooks/autumn-inbox.ts
+++ b/apps/api/src/routes/webhooks/autumn-inbox.ts
@@ -25,7 +25,7 @@ const DEAD_LETTER_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
const RETRY_BASE_MS = 5 * 60 * 1000;
const RETRY_MAX_MS = 6 * 60 * 60 * 1000;
-export const AUTUMN_WEBHOOK_LEASE_MS = 5 * 60 * 1000;
+const AUTUMN_WEBHOOK_LEASE_MS = 5 * 60 * 1000;
export const AUTUMN_WEBHOOK_MAX_ATTEMPTS = 12;
export interface StoredAutumnWebhook {
diff --git a/apps/api/src/routes/webhooks/autumn.test.ts b/apps/api/src/routes/webhooks/autumn.test.ts
index b0f8ade46d..681a4261a9 100644
--- a/apps/api/src/routes/webhooks/autumn.test.ts
+++ b/apps/api/src/routes/webhooks/autumn.test.ts
@@ -128,11 +128,11 @@ vi.mock("./autumn-inbox", () => ({
vi.mock("@databuddy/db", () => ({
and: (...conditions: unknown[]) => ({ conditions }),
- db: {
- query: {
- member: {
- findMany: vi.fn(async () => state.ownedOrganizations),
- },
+ db: {
+ query: {
+ member: {
+ findMany: vi.fn(async () => state.ownedOrganizations),
+ },
organization: { findFirst: vi.fn(async () => null) },
user: { findFirst: vi.fn(async () => state.userRow) },
},
@@ -140,11 +140,11 @@ vi.mock("@databuddy/db", () => ({
eq: (field: unknown, value: unknown) => ({ field, op: "eq", value }),
gt: (field: unknown, value: unknown) => ({ field, op: "gt", value }),
isNull: (field: unknown) => ({ field, op: "isNull" }),
- normalizeEmailNotificationSettings: (raw?: {
- billing?: { usageWarnings?: boolean };
- }) => ({
- billing: { usageWarnings: raw?.billing?.usageWarnings ?? true },
- }),
+ normalizeEmailNotificationSettings: (raw?: {
+ billing?: { usageWarnings?: boolean };
+ }) => ({
+ billing: { usageWarnings: raw?.billing?.usageWarnings ?? true },
+ }),
or: (...conditions: unknown[]) => ({ conditions, op: "or" }),
sql: (strings: TemplateStringsArray, ...values: unknown[]) => ({
strings: Array.from(strings),
@@ -359,13 +359,13 @@ describe("sendAlertEmail", () => {
"send",
"insert",
]);
- expect(state.send).toHaveBeenCalledWith({
- from: "alerts@databuddy.cc",
- to: "member@example.com",
- subject: "Limit reached",
- html: " ",
- text: " ",
- });
+ expect(state.send).toHaveBeenCalledWith({
+ from: "alerts@databuddy.cc",
+ to: "member@example.com",
+ subject: "Limit reached",
+ html: " ",
+ text: " ",
+ });
expect(state.inserted).toEqual([
expect.objectContaining({
alertType: "included",
@@ -827,4 +827,49 @@ describe("Autumn webhook inbox", () => {
expect(state.send).toHaveBeenCalledTimes(1);
expect(state.storedWebhooks.get("msg-replay")?.status).toBe("completed");
});
+
+ it("fails a replay for an unknown webhook id without side effects", async () => {
+ await expect(replayDeferredAutumnWebhook("msg-unknown")).resolves.toEqual({
+ message: "Stored webhook not found",
+ success: false,
+ });
+ expect(state.send).not.toHaveBeenCalled();
+ });
+
+ it("leaves a webhook claimed by another worker queued", async () => {
+ state.storedWebhooks.set("msg-claimed", {
+ attempts: 3,
+ claimToken: "claim-other-worker",
+ id: "msg-claimed",
+ payload: {},
+ status: "processing",
+ type: "balances.limit_reached",
+ });
+
+ await expect(replayDeferredAutumnWebhook("msg-claimed")).resolves.toEqual({
+ disposition: "deferred",
+ message: "Webhook already queued for replay",
+ success: true,
+ });
+ expect(state.send).not.toHaveBeenCalled();
+ expect(state.storedWebhooks.get("msg-claimed")?.status).toBe("processing");
+ });
+
+ it("acknowledges dead-lettered webhooks without reprocessing them", async () => {
+ state.storedWebhooks.set("msg-dead", {
+ attempts: 12,
+ claimToken: null,
+ id: "msg-dead",
+ payload: {},
+ status: "dead_letter",
+ type: "balances.limit_reached",
+ });
+
+ await expect(replayDeferredAutumnWebhook("msg-dead")).resolves.toEqual({
+ disposition: "duplicate",
+ message: "Webhook retained for investigation",
+ success: true,
+ });
+ expect(state.send).not.toHaveBeenCalled();
+ });
});
diff --git a/apps/api/src/routes/webhooks/autumn.ts b/apps/api/src/routes/webhooks/autumn.ts
index 4035448505..09847a9d9a 100644
--- a/apps/api/src/routes/webhooks/autumn.ts
+++ b/apps/api/src/routes/webhooks/autumn.ts
@@ -195,7 +195,7 @@ const getBillingRecipient = cacheable(
}
);
-export async function resolveBillingOrganization(
+async function resolveBillingOrganization(
customerId: string,
entityId?: string
): Promise {
@@ -749,6 +749,35 @@ function webhookIdempotencyKey(svixId: string): string {
return createHash("sha256").update(svixId).digest("hex");
}
+async function recordRetryAttempt(
+ stored: ClaimedAutumnWebhook,
+ status: "deferred" | "pending",
+ message: string
+): Promise {
+ const outcome = await recordAutumnWebhookAttempt({
+ attempts: stored.attempts,
+ claimToken: stored.claimToken,
+ errorMessage: message,
+ id: stored.id,
+ status,
+ });
+ if (outcome === "completed") {
+ return {
+ disposition: "duplicate",
+ message: "Webhook already processed",
+ success: true,
+ };
+ }
+ if (outcome === "dead_letter") {
+ return {
+ disposition: "dead_letter",
+ message: "Webhook moved to dead letter",
+ success: true,
+ };
+ }
+ return null;
+}
+
async function processClaimedAutumnWebhook(
stored: ClaimedAutumnWebhook
): Promise {
@@ -763,82 +792,35 @@ async function processClaimedAutumnWebhook(
}
result = await dispatch(event, webhookIdempotencyKey(stored.id));
} catch (error) {
- const status = await recordAutumnWebhookAttempt({
- attempts: stored.attempts,
- claimToken: stored.claimToken,
- errorMessage: errorMessage(error),
- id: stored.id,
- status: "pending",
- });
- if (status === "completed") {
- return {
- disposition: "duplicate",
- message: "Webhook already processed",
- success: true,
- };
- }
- if (status === "dead_letter") {
- return {
- disposition: "dead_letter",
- message: "Webhook moved to dead letter",
- success: true,
- };
+ const settled = await recordRetryAttempt(
+ stored,
+ "pending",
+ errorMessage(error)
+ );
+ if (settled) {
+ return settled;
}
throw error;
}
if (result.disposition === "deferred") {
- const status = await recordAutumnWebhookAttempt({
- attempts: stored.attempts,
- claimToken: stored.claimToken,
- errorMessage: result.message,
- id: stored.id,
- status: "deferred",
- });
- if (status === "completed") {
- return {
- disposition: "duplicate",
- message: "Webhook already processed",
- success: true,
- };
- }
- if (status === "dead_letter") {
- return {
- disposition: "dead_letter",
- message: "Webhook moved to dead letter",
+ const settled = await recordRetryAttempt(
+ stored,
+ "deferred",
+ result.message
+ );
+ return (
+ settled ?? {
+ disposition: "deferred",
+ message: "Webhook stored for replay",
success: true,
- };
- }
- return {
- disposition: "deferred",
- message: "Webhook stored for replay",
- success: true,
- };
+ }
+ );
}
if (!result.success) {
- const status = await recordAutumnWebhookAttempt({
- attempts: stored.attempts,
- claimToken: stored.claimToken,
- errorMessage: result.message,
- id: stored.id,
- status: "pending",
- });
- if (status === "completed") {
- return {
- disposition: "duplicate",
- message: "Webhook already processed",
- success: true,
- };
- }
- if (status === "dead_letter") {
- return {
- disposition: "dead_letter",
- message: "Webhook moved to dead letter",
- success: true,
- };
- }
- return result;
+ const settled = await recordRetryAttempt(stored, "pending", result.message);
+ return settled ?? result;
}
await recordAutumnWebhookAttempt({
diff --git a/apps/api/src/rpc/handlers.ts b/apps/api/src/rpc/handlers.ts
index 4efe76d812..5e08689631 100644
--- a/apps/api/src/rpc/handlers.ts
+++ b/apps/api/src/rpc/handlers.ts
@@ -26,7 +26,7 @@ export const rpcHandler = new RPCHandler(appRouter, {
interceptors: [createAbortSignalInterceptor(), onError(logOrpcHandlerError)],
});
-export function createAuthenticatedOrpcContext(request: Request) {
+function createAuthenticatedOrpcContext(request: Request) {
const preResolvedAuth = getPreResolvedAuth(request.headers);
return createRPCContext(
{ headers: request.headers, requestId: getRequestId(request) },
@@ -34,7 +34,7 @@ export function createAuthenticatedOrpcContext(request: Request) {
);
}
-export function createAnonymousOrpcContext(request: Request) {
+function createAnonymousOrpcContext(request: Request) {
return createRPCContext(
{ headers: request.headers, requestId: getRequestId(request) },
ANONYMOUS_AUTH
diff --git a/apps/api/src/schemas/query-schemas.ts b/apps/api/src/schemas/query-schemas.ts
index 111920daa9..b5342e2843 100644
--- a/apps/api/src/schemas/query-schemas.ts
+++ b/apps/api/src/schemas/query-schemas.ts
@@ -6,14 +6,11 @@ const QUERY_BUILDER_TYPES = Object.keys(QueryBuilders) as Array<
keyof typeof QueryBuilders
>;
-export {
- DatePresets,
- type DatePreset,
-} from "@databuddy/ai/lib/date-presets";
+export { DatePresets } from "@databuddy/ai/lib/date-presets";
-export const DatePresetSchema = t.Enum(DatePresets);
+const DatePresetSchema = t.Enum(DatePresets);
-export const FilterSchema = t.Object({
+const FilterSchema = t.Object({
field: t.String(),
op: t.Enum({
eq: "eq",
@@ -31,7 +28,7 @@ export const FilterSchema = t.Object({
]),
});
-export const ParameterWithDatesSchema = t.Object({
+const ParameterWithDatesSchema = t.Object({
name: t.String(),
start_date: t.Optional(t.String()),
end_date: t.Optional(t.String()),
@@ -90,7 +87,7 @@ export const CompileRequestSchema = t.Object({
offset: t.Optional(t.Number({ minimum: 0 })),
});
-export interface FilterType {
+interface FilterType {
field: string;
op:
| "eq"
@@ -103,7 +100,7 @@ export interface FilterType {
value: string | number | Array;
}
-export interface ParameterWithDatesType {
+interface ParameterWithDatesType {
end_date?: string;
granularity?: "hourly" | "daily" | "hour" | "day";
id?: string;
diff --git a/apps/basket/package.json b/apps/basket/package.json
index 419e2ab675..7c7be39893 100644
--- a/apps/basket/package.json
+++ b/apps/basket/package.json
@@ -22,18 +22,13 @@
"@databuddy/shared": "workspace:*",
"@databuddy/validation": "workspace:*",
"@maxmind/geoip2-node": "^6.3.4",
- "@types/ua-parser-js": "^0.7.39",
- "async-mutex": "^0.5.0",
"effect": "^4.0.0-beta.90",
"elysia": "catalog:",
"evlog": "catalog:",
"kafkajs": "^2.2.4",
- "keypal": "0.2.0",
- "ua-parser-js": "^2.0.7",
"zod": "catalog:"
},
"devDependencies": {
- "bun-types": "catalog:",
"vitest": "^4.1.4"
},
"packageManager": "bun@1.3.14"
diff --git a/apps/basket/src/hooks/auth.ts b/apps/basket/src/hooks/auth.ts
index 3180727653..1f03ea1592 100644
--- a/apps/basket/src/hooks/auth.ts
+++ b/apps/basket/src/hooks/auth.ts
@@ -1,10 +1,3 @@
-/**
- * Website Authentication Hook for Analytics
- *
- * This hook provides authentication for website tracking by validating
- * client IDs and origins against registered websites.
- */
-
import { db } from "@databuddy/db";
import type { Website } from "@databuddy/db/schema";
import { cacheNamespaces } from "@databuddy/redis/cache-invalidation";
@@ -14,10 +7,7 @@ import { captureError, record } from "@lib/tracing";
import { isValidOriginFromSettings } from "@utils/origin-ip-validation";
import { createError, EvlogError } from "evlog";
-export {
- isValidIpFromSettings,
- isValidOriginFromSettings,
-} from "@utils/origin-ip-validation";
+export { isValidIpFromSettings } from "@utils/origin-ip-validation";
type WebsiteWithOwner = Website & {
ownerId: string | null;
@@ -72,14 +62,7 @@ export const resolveApiKeyOwnerId = cacheable(
staleTime: 60,
}
);
-
-/**
- * Validates if an origin header matches or is a subdomain of the allowed domain
- */
-export function isValidOrigin(
- originHeader: string,
- allowedDomain: string
-): boolean {
+function isValidOrigin(originHeader: string, allowedDomain: string): boolean {
const trimmedOrigin = originHeader?.trim();
if (!trimmedOrigin) {
return true;
@@ -108,11 +91,7 @@ export function isValidOrigin(
return false;
}
}
-
-/**
- * Normalizes a domain by removing the protocol, port, and "www." prefix.
- */
-export function normalizeDomain(domain: string): string {
+function normalizeDomain(domain: string): string {
if (!domain) {
return "";
}
@@ -153,17 +132,14 @@ export function normalizeDomain(domain: string): string {
}
}
-export function isSubdomain(
- originDomain: string,
- allowedDomain: string
-): boolean {
+function isSubdomain(originDomain: string, allowedDomain: string): boolean {
return (
originDomain.endsWith(`.${allowedDomain}`) &&
originDomain.length > allowedDomain.length + 1
);
}
-export function isValidDomainFormat(domain: string): boolean {
+function isValidDomainFormat(domain: string): boolean {
if (
!domain ||
domain.length > 253 ||
diff --git a/apps/basket/src/lib/api-key.ts b/apps/basket/src/lib/api-key.ts
index 37aca03315..c9f0f8be8b 100644
--- a/apps/basket/src/lib/api-key.ts
+++ b/apps/basket/src/lib/api-key.ts
@@ -10,7 +10,7 @@ import {
import { record } from "@lib/tracing";
import { useLogger } from "evlog/elysia";
-export type { ApiKeyRow, ApiScope } from "@databuddy/api-keys/resolve";
+export type { ApiKeyRow } from "@databuddy/api-keys/resolve";
export const hasKeyScope = _hasKeyScope;
export const hasGlobalAccess = _hasGlobalAccess;
diff --git a/apps/basket/src/lib/billing.test.ts b/apps/basket/src/lib/billing.test.ts
index 851876762f..e8e523f4b1 100644
--- a/apps/basket/src/lib/billing.test.ts
+++ b/apps/basket/src/lib/billing.test.ts
@@ -39,8 +39,6 @@ describe("checkAutumnUsage", () => {
mockLoggerWarn.mockReset();
});
- // ── Enforcement ──
-
test("allowed response → allowed", async () => {
mockCheck.mockResolvedValue({
allowed: true,
@@ -73,8 +71,6 @@ describe("checkAutumnUsage", () => {
});
});
- // ── Still calls Autumn (metering for paying customers) ──
-
test("calls autumn.check with sendEvent: true", async () => {
mockCheck.mockResolvedValue({
allowed: true,
@@ -107,8 +103,6 @@ describe("checkAutumnUsage", () => {
});
});
- // ── Logging ──
-
test("logs balance context from Autumn response", async () => {
mockCheck.mockResolvedValue({
allowed: true,
diff --git a/apps/basket/src/lib/cors-safe-json.ts b/apps/basket/src/lib/cors-safe-json.ts
index cf56a64da0..30d0065f9a 100644
--- a/apps/basket/src/lib/cors-safe-json.ts
+++ b/apps/basket/src/lib/cors-safe-json.ts
@@ -2,11 +2,6 @@ interface ParseContext {
contentType: string;
request: Request;
}
-
-/**
- * Unload beacons use text/plain so cross-origin delivery remains a CORS simple
- * request. Parse that JSON before the normal ingest schemas validate it.
- */
export async function parseCorsSafeJson({
contentType,
request,
diff --git a/apps/basket/src/lib/event-service.test.ts b/apps/basket/src/lib/event-service.test.ts
index 353fe536cd..d4ea4bcae5 100644
--- a/apps/basket/src/lib/event-service.test.ts
+++ b/apps/basket/src/lib/event-service.test.ts
@@ -2,8 +2,6 @@ import { describe, expect, test } from "vitest";
import { CONTROL_CHARS, longString, XSS_PAYLOADS } from "../test-helpers";
import { buildTrackEvent, type TrackEventContext } from "./event-service";
-// ── Fixtures ──
-
const NOW = 1_700_000_000_000;
const fullTrackData = {
@@ -65,89 +63,72 @@ const fullCtx: TrackEventContext = {
now: NOW,
};
-// ── Field mapping snapshot ──
-
describe("buildTrackEvent — field mapping", () => {
test("full input → every field mapped correctly", () => {
const result = buildTrackEvent(fullTrackData, fullCtx);
- // Identity
- expect(result.id).toBeTruthy(); // randomUUIDv7
- expect(result.client_id).toBe("ws_test");
-
- // Names & content
- expect(result.event_name).toBe("pageview");
- expect(result.title).toBe("Dashboard | App");
- expect(result.referrer).toBe("https://google.com");
- expect(result.path).toBe("/dashboard");
- expect(result.url).toBe("/dashboard"); // url === path
-
- // User identity
- expect(result.anonymous_id).toBe("salted_anon_1");
- expect(result.session_id).toBe("sess_abc123");
-
- // Timestamps — uses trackData values when numeric
- expect(result.timestamp).toBe(1_700_000_001_000);
- expect(result.time).toBe(1_700_000_001_000);
- expect(result.created_at).toBe(NOW);
-
- // Geo
- expect(result.ip).toBe("abc123def456");
- expect(result.country).toBe("United States");
- expect(result.region).toBe("California");
- expect(result.city).toBe("San Francisco");
-
- // UA
- expect(result.user_agent).toBe(""); // always empty (privacy)
- expect(result.browser_name).toBe("Chrome");
- expect(result.browser_version).toBe("120.0");
- expect(result.os_name).toBe("Windows");
- expect(result.os_version).toBe("10");
- expect(result.device_type).toBe("desktop");
- expect(result.device_brand).toBe("Dell");
- expect(result.device_model).toBe("XPS");
-
- // Client context — passthrough
- expect(result.viewport_size).toBe("1024x768");
- expect(result.language).toBe("en-US");
- expect(result.timezone).toBe("America/New_York");
-
- // Engagement
- expect(result.time_on_page).toBe(30_000);
- expect(result.scroll_depth).toBe(75);
- expect(result.interaction_count).toBe(12);
- expect(result.page_count).toBe(3);
-
- // UTM
- expect(result.utm_source).toBe("google");
- expect(result.utm_medium).toBe("cpc");
- expect(result.utm_campaign).toBe("summer");
- expect(result.utm_term).toBe("analytics");
- expect(result.utm_content).toBe("banner");
- expect(result.gclid).toBe("gclid_abc");
-
- // Performance — validated through validatePerformanceMetric
- expect(result.dom_ready_time).toBe(800);
- expect(result.ttfb).toBe(200);
- expect(result.render_time).toBe(100);
-
- // Properties
- expect(result.properties).toBe('{"plan":"pro","color":"blue"}');
+ expect(result).toMatchObject({
+ client_id: "ws_test",
+ event_name: "pageview",
+ title: "Dashboard | App",
+ referrer: "https://google.com",
+ path: "/dashboard",
+ url: "/dashboard",
+ anonymous_id: "salted_anon_1",
+ session_id: "sess_abc123",
+ timestamp: 1_700_000_001_000,
+ time: 1_700_000_001_000,
+ created_at: NOW,
+ ip: "abc123def456",
+ country: "United States",
+ region: "California",
+ city: "San Francisco",
+ user_agent: "",
+ browser_name: "Chrome",
+ browser_version: "120.0",
+ os_name: "Windows",
+ os_version: "10",
+ device_type: "desktop",
+ device_brand: "Dell",
+ device_model: "XPS",
+ viewport_size: "1024x768",
+ language: "en-US",
+ timezone: "America/New_York",
+ time_on_page: 30_000,
+ scroll_depth: 75,
+ interaction_count: 12,
+ page_count: 3,
+ utm_source: "google",
+ utm_medium: "cpc",
+ utm_campaign: "summer",
+ utm_term: "analytics",
+ utm_content: "banner",
+ gclid: "gclid_abc",
+ dom_ready_time: 800,
+ ttfb: 200,
+ render_time: 100,
+ properties: '{"plan":"pro","color":"blue"}',
+ });
+ expect(result.id).toMatch(
+ /^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
+ );
});
test("minimal input → defaults applied", () => {
const result = buildTrackEvent({ name: "click" }, fullCtx);
- expect(result.event_name).toBe("click");
- expect(result.timestamp).toBe(NOW); // falls back to ctx.now
- expect(result.time).toBe(NOW);
- expect(result.page_count).toBe(1); // default
- expect(result.properties).toBe("{}"); // empty
- expect(result.referrer).toBe("");
- expect(result.path).toBe("");
- expect(result.url).toBe("");
- expect(result.title).toBe("");
- expect(result.session_id).toBe("");
+ expect(result).toMatchObject({
+ event_name: "click",
+ timestamp: NOW,
+ time: NOW,
+ page_count: 1,
+ properties: "{}",
+ referrer: "",
+ path: "",
+ url: "",
+ title: "",
+ session_id: "",
+ });
});
test("missing geo fields → empty strings", () => {
@@ -175,9 +156,9 @@ describe("buildTrackEvent — field mapping", () => {
expect(result.timestamp).toBe(NOW);
});
- test("performance metrics validated (negative → undefined)", () => {
+ test("performance metrics over the 300s cap → undefined", () => {
const result = buildTrackEvent({ name: "x", ttfb: 999_999 }, fullCtx);
- expect(result.ttfb).toBeUndefined(); // >300000
+ expect(result.ttfb).toBeUndefined();
});
test("event_name sanitized (truncated to 255)", () => {
@@ -201,8 +182,6 @@ describe("buildTrackEvent — field mapping", () => {
});
});
-// ── Sanitization boundary ──
-
describe("buildTrackEvent — sanitization boundary", () => {
for (const payload of XSS_PAYLOADS) {
test(`XSS in name: ${payload.slice(0, 30)}…`, () => {
@@ -252,16 +231,14 @@ describe("buildTrackEvent — sanitization boundary", () => {
}
});
- test("properties with XSS are JSON-stringified (not sanitized — stored as JSON)", () => {
+ test("properties are JSON-stringified verbatim, not HTML-sanitized", () => {
const result = buildTrackEvent(
{ name: "x", properties: { evil: "" } },
fullCtx
);
- // Properties are JSON-stringified, not HTML-sanitized (they're stored as JSON in CH)
- expect(result.properties).toContain("script");
- expect(typeof result.properties).toBe("string");
- // But it's valid JSON
- expect(() => JSON.parse(result.properties as string)).not.toThrow();
+ expect(JSON.parse(result.properties as string)).toEqual({
+ evil: "",
+ });
});
test("passthrough fields (language, timezone, etc.) are NOT sanitized", () => {
@@ -277,76 +254,11 @@ describe("buildTrackEvent — sanitization boundary", () => {
expect(result.timezone).toBe("America/New_York");
});
- test("session_id validated (rejects special chars)", () => {
+ test("session_id with stripped tags still passes the session id charset", () => {
const result = buildTrackEvent(
{ name: "x", sessionId: "sess",
@@ -109,8 +71,6 @@ export function longString(n: number, char = "a"): string {
return char.repeat(n);
}
-// ── Request factory ──
-
export function req(
url = "https://example.com",
headers: Record = {}
diff --git a/apps/basket/src/utils/ip-geo.test.ts b/apps/basket/src/utils/ip-geo.test.ts
index 5b2f746b1f..3cc6eb629f 100644
--- a/apps/basket/src/utils/ip-geo.test.ts
+++ b/apps/basket/src/utils/ip-geo.test.ts
@@ -1,4 +1,4 @@
-import { afterAll, describe, expect, test } from "vitest";
+import { afterAll, describe, expect, test, vi } from "vitest";
import { randomIPv4, randomPublicIPv4, req } from "../test-helpers";
import {
anonymizeIp,
@@ -11,8 +11,6 @@ const HEX12 = /^[a-f0-9]{12}$/;
afterAll(() => closeGeoIPReader());
-// ── anonymizeIp ──
-
describe("anonymizeIp", () => {
test("empty → empty", () => expect(anonymizeIp("")).toBe(""));
@@ -43,8 +41,6 @@ describe("anonymizeIp", () => {
});
});
-// ── extractIpFromRequest ──
-
describe("extractIpFromRequest", () => {
const table: [string, Record, string][] = [
["cf-connecting-ip", { "cf-connecting-ip": "1.2.3.4" }, "1.2.3.4"],
@@ -89,8 +85,6 @@ describe("extractIpFromRequest", () => {
});
});
-// ── getGeo ──
-
describe("getGeo", () => {
test("empty IP → empty anonymizedIP, no geo", async () => {
const r = await getGeo("");
@@ -116,48 +110,51 @@ describe("getGeo", () => {
}
});
- test("200 random public IPs → valid structure", {
- timeout: 60_000,
- }, async () => {
- const probe = await Promise.race([
- getGeo("8.8.8.8"),
- new Promise((r) => setTimeout(() => r(null), 30_000)),
- ]);
- if (!(probe && probe.anonymizedIP)) {
- console.log("Skipping: GeoIP CDN unreachable");
- return;
+ test("falls back to the Cloudflare country header when MaxMind is unavailable", async () => {
+ closeGeoIPReader();
+ const fetchSpy = vi
+ .spyOn(globalThis, "fetch")
+ .mockRejectedValue(new Error("CDN unreachable"));
+ try {
+ const withHeader = await getGeo(
+ randomPublicIPv4(),
+ req("https://x.com", { "cf-ipcountry": "US" })
+ );
+ expect(withHeader.country).toBe("US");
+ expect(withHeader.region).toBeUndefined();
+ expect(withHeader.city).toBeUndefined();
+
+ const badHeader = await getGeo(
+ randomPublicIPv4(),
+ req("https://x.com", { "cf-ipcountry": "USA" })
+ );
+ expect(badHeader.country).toBeUndefined();
+ } finally {
+ fetchSpy.mockRestore();
+ closeGeoIPReader();
}
+ });
- const results = await Promise.all(
- Array.from({ length: 200 }, () => getGeo(randomPublicIPv4()))
- );
- for (const r of results) {
- expect(typeof r.anonymizedIP).toBe("string");
- if (r.country !== undefined) {
- expect(typeof r.country).toBe("string");
- }
- if (r.region !== undefined) {
- expect(typeof r.region).toBe("string");
- }
- if (r.city !== undefined) {
- expect(typeof r.city).toBe("string");
+ test("accepts compressed and ipv4-mapped IPv6 addresses", async () => {
+ closeGeoIPReader();
+ const fetchSpy = vi
+ .spyOn(globalThis, "fetch")
+ .mockRejectedValue(new Error("CDN unreachable"));
+ try {
+ for (const ip of [
+ "2a00:1450:4009:81f::200e",
+ "2001:db8::1",
+ "::ffff:8.8.8.8",
+ ]) {
+ const r = await getGeo(
+ ip,
+ req("https://x.com", { "cf-ipcountry": "DE" })
+ );
+ expect(r.country).toBe("DE");
}
+ } finally {
+ fetchSpy.mockRestore();
+ closeGeoIPReader();
}
});
-
- test("same IP → consistent results", async () => {
- const ip = randomPublicIPv4();
- const [a, b] = await Promise.all([getGeo(ip), getGeo(ip)]);
- expect(a.anonymizedIP).toBe(b.anonymizedIP);
- expect(a.country).toBe(b.country);
- });
-
- test("Cloudflare country fallback", async () => {
- const r = await getGeo(
- "not-valid-ip",
- req("https://x.com", { "cf-ipcountry": "US" })
- );
- // Should either return CF country or undefined (depends on reader state)
- expect(typeof r.anonymizedIP).toBe("string");
- });
});
diff --git a/apps/basket/src/utils/ip-geo.ts b/apps/basket/src/utils/ip-geo.ts
index e2cce394c0..d579098f00 100644
--- a/apps/basket/src/utils/ip-geo.ts
+++ b/apps/basket/src/utils/ip-geo.ts
@@ -1,4 +1,5 @@
import { createHash } from "node:crypto";
+import { isIP } from "node:net";
import { captureError, mergeWideEvent, record } from "@lib/tracing";
import type { City } from "@maxmind/geoip2-node";
import {
@@ -110,13 +111,8 @@ function loadDatabase() {
const ignore = ["127.0.0.1", "::1"];
-const ipv4Regex =
- /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
-
-const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/;
-
function isValidIp(ip: string): boolean {
- return Boolean(ip && (ipv4Regex.test(ip) || ipv6Regex.test(ip)));
+ return isIP(ip) !== 0;
}
function getCloudflareCountry(headers: Headers): string | undefined {
diff --git a/apps/basket/src/utils/origin-ip-validation.ts b/apps/basket/src/utils/origin-ip-validation.ts
index 22d4074455..9125560c76 100644
--- a/apps/basket/src/utils/origin-ip-validation.ts
+++ b/apps/basket/src/utils/origin-ip-validation.ts
@@ -82,12 +82,12 @@ export function isValidIpFromSettings(
ip: string,
allowedIps?: string[]
): boolean {
- if (!ip?.trim()) {
- return true;
- }
if (!allowedIps || allowedIps.length === 0) {
return true;
}
+ if (!ip?.trim()) {
+ return false;
+ }
const trimmedIp = ip.trim();
diff --git a/apps/basket/src/utils/parsing-helpers.test.ts b/apps/basket/src/utils/parsing-helpers.test.ts
index 62bb7ed9a8..16a1b59918 100644
--- a/apps/basket/src/utils/parsing-helpers.test.ts
+++ b/apps/basket/src/utils/parsing-helpers.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, test } from "vitest";
import type { z } from "zod";
+import { cases, longString } from "../test-helpers";
import {
batchBotIgnoredItem,
batchSchemaItemFailure,
@@ -8,72 +9,61 @@ import {
parseTimestamp,
} from "./parsing-helpers";
-// ── parseTimestamp ──
+cases(
+ "parseTimestamp keeps numeric timestamps verbatim",
+ [
+ ["positive epoch", 1_700_000_000, 1_700_000_000],
+ ["zero", 0, 0],
+ ["negative", -1, -1],
+ ],
+ (input) => parseTimestamp(input)
+);
-describe("parseTimestamp", () => {
- test("number → passthrough", () =>
- expect(parseTimestamp(1_700_000_000)).toBe(1_700_000_000));
- test("0 → 0", () => expect(parseTimestamp(0)).toBe(0));
- test("negative → passthrough", () => expect(parseTimestamp(-1)).toBe(-1));
- test("string → Date.now()", () => {
- const before = Date.now();
- const result = parseTimestamp("not-a-number");
- expect(result).toBeGreaterThanOrEqual(before);
- expect(result).toBeLessThanOrEqual(Date.now());
- });
- test("null → Date.now()", () => {
- const result = parseTimestamp(null);
- expect(typeof result).toBe("number");
- expect(result).toBeGreaterThan(0);
- });
- test("undefined → Date.now()", () => {
- const result = parseTimestamp(undefined);
- expect(typeof result).toBe("number");
- });
- test("object → Date.now()", () => {
- expect(typeof parseTimestamp({})).toBe("number");
- });
+describe("parseTimestamp replaces non-numeric input with the current time", () => {
+ test.each([["not-a-number"], [null], [undefined], [{}]])(
+ "%j falls back to Date.now()",
+ (input) => {
+ const before = Date.now();
+ const result = parseTimestamp(input);
+ expect(result).toBeGreaterThanOrEqual(before);
+ expect(result).toBeLessThanOrEqual(Date.now());
+ }
+ );
});
-// ── parseProperties ──
-
-describe("parseProperties", () => {
- test("object → JSON string", () =>
- expect(parseProperties({ a: 1 })).toBe('{"a":1}'));
- test("null → '{}'", () => expect(parseProperties(null)).toBe("{}"));
- test("undefined → '{}'", () => expect(parseProperties(undefined)).toBe("{}"));
- test("false → '{}'", () => expect(parseProperties(false)).toBe("{}"));
- test("0 → '{}'", () => expect(parseProperties(0)).toBe("{}"));
- test("empty string → '{}'", () => expect(parseProperties("")).toBe("{}"));
- test("non-empty string → JSON string", () =>
- expect(parseProperties("hello")).toBe('"hello"'));
- test("array → JSON array", () =>
- expect(parseProperties([1, 2])).toBe("[1,2]"));
- test("nested object", () =>
- expect(parseProperties({ a: { b: "c" } })).toBe('{"a":{"b":"c"}}'));
-});
-
-// ── parseEventId ──
+cases(
+ "parseProperties serializes truthy values and defaults the rest",
+ [
+ ["object", { a: 1 }, '{"a":1}'],
+ ["nested object", { a: { b: "c" } }, '{"a":{"b":"c"}}'],
+ ["array", [1, 2], "[1,2]"],
+ ["non-empty string", "hello", '"hello"'],
+ ["null", null, "{}"],
+ ["undefined", undefined, "{}"],
+ ["empty string", "", "{}"],
+ ],
+ (input) => parseProperties(input)
+);
describe("parseEventId", () => {
const gen = () => "generated-uuid";
- test("valid string → passthrough", () =>
- expect(parseEventId("evt_123", gen)).toBe("evt_123"));
- test("empty string → calls generator", () =>
- expect(parseEventId("", gen)).toBe("generated-uuid"));
- test("null → calls generator", () =>
- expect(parseEventId(null, gen)).toBe("generated-uuid"));
- test("undefined → calls generator", () =>
- expect(parseEventId(undefined, gen)).toBe("generated-uuid"));
- test("number → calls generator", () =>
- expect(parseEventId(123, gen)).toBe("generated-uuid"));
- test("long string → truncated to event id limit", () => {
- const long = "a".repeat(600);
- const result = parseEventId(long, gen);
- expect(result.length).toBe(512);
+ cases(
+ "keeps client ids and generates for unusable input",
+ [
+ ["valid string", "evt_123", "evt_123"],
+ ["empty string", "", "generated-uuid"],
+ ["null", null, "generated-uuid"],
+ ["number", 123, "generated-uuid"],
+ ],
+ (input) => parseEventId(input, gen)
+ );
+
+ test("truncated a long id to the event id limit", () => {
+ expect(parseEventId(longString(600), gen).length).toBe(512);
});
- test("generator called only when needed", () => {
+
+ test("did not invoke the generator for a usable id", () => {
let called = false;
parseEventId("valid", () => {
called = true;
@@ -83,15 +73,12 @@ describe("parseEventId", () => {
});
});
-// ── batchSchemaItemFailure ──
-
-describe("batchSchemaItemFailure", () => {
- test("returns structured error with issues", () => {
+describe("batch item failure shapes", () => {
+ test("schema failure flattens issue paths into field names", () => {
const issues = [
{ message: "bad", path: ["x"], code: "custom" as const },
] as z.core.$ZodIssue[];
- const result = batchSchemaItemFailure(issues, "track", "evt_1");
- expect(result).toEqual({
+ expect(batchSchemaItemFailure(issues, "track", "evt_1")).toEqual({
status: "error",
message: "Invalid event schema",
code: "INVALID_EVENT_SCHEMA",
@@ -100,16 +87,13 @@ describe("batchSchemaItemFailure", () => {
eventId: "evt_1",
});
});
-});
-
-// ── batchBotIgnoredItem ──
-describe("batchBotIgnoredItem", () => {
- test("returns bot-ignored structure", () => {
- const result = batchBotIgnoredItem("track");
- expect(result.status).toBe("error");
- expect(result.message).toBe("Bot detected");
- expect(result.eventType).toBe("track");
- expect(result.error).toBe("ignored");
+ test("bot-ignored item is an error marked as ignored", () => {
+ expect(batchBotIgnoredItem("track")).toEqual({
+ status: "error",
+ message: "Bot detected",
+ eventType: "track",
+ error: "ignored",
+ });
});
});
diff --git a/apps/basket/src/utils/parsing-helpers.ts b/apps/basket/src/utils/parsing-helpers.ts
index 0c45202599..3f59126acc 100644
--- a/apps/basket/src/utils/parsing-helpers.ts
+++ b/apps/basket/src/utils/parsing-helpers.ts
@@ -87,16 +87,9 @@ export function parseEventId(
eventId: unknown,
generateFn: () => string
): string {
- const sanitizeString = (str: unknown, maxLength: number): string => {
- if (typeof str !== "string") {
- return "";
- }
- return str.slice(0, maxLength);
- };
-
- const sanitized = sanitizeString(
- eventId,
- VALIDATION_LIMITS.EVENT_ID_MAX_LENGTH
- );
+ const sanitized =
+ typeof eventId === "string"
+ ? eventId.slice(0, VALIDATION_LIMITS.EVENT_ID_MAX_LENGTH)
+ : "";
return sanitized || generateFn();
}
diff --git a/apps/basket/src/utils/pixel.test.ts b/apps/basket/src/utils/pixel.test.ts
index 9c1856d132..684aa03898 100644
--- a/apps/basket/src/utils/pixel.test.ts
+++ b/apps/basket/src/utils/pixel.test.ts
@@ -1,8 +1,6 @@
import { describe, expect, test } from "vitest";
import { createPixelResponse, parsePixelQuery } from "./pixel";
-// ── createPixelResponse ──
-
describe("createPixelResponse", () => {
test("returns 200 image/gif with no-cache headers", async () => {
const r = createPixelResponse();
@@ -14,15 +12,11 @@ describe("createPixelResponse", () => {
const buf = await r.arrayBuffer();
expect(buf.byteLength).toBeGreaterThan(0);
- // GIF89a magic bytes
- expect(new Uint8Array(buf).slice(0, 3)).toEqual(
- new Uint8Array([0x47, 0x49, 0x46])
- );
+ const gifMagicBytes = new Uint8Array([0x47, 0x49, 0x46]);
+ expect(new Uint8Array(buf).slice(0, 3)).toEqual(gifMagicBytes);
});
});
-// ── parsePixelQuery ──
-
describe("parsePixelQuery", () => {
test("empty query → empty eventData, type=track", () => {
const { eventData, eventType } = parsePixelQuery({});
@@ -112,4 +106,25 @@ describe("parsePixelQuery", () => {
expect(eventData[`field_${i}`]).toBe(i);
}
});
+
+ test("flat key followed by nested key on the same name does not crash", () => {
+ const { eventData } = parsePixelQuery({ a: "1", "a[b]": "2" });
+ expect(eventData.a).toEqual({ b: 2 });
+ });
+
+ test("nested key deepened on a later param keeps the deepest write", () => {
+ const { eventData } = parsePixelQuery({ "a[b]": "1", "a[b][c]": "2" });
+ expect(eventData.a).toEqual({ b: { c: 2 } });
+ });
+
+ test("__proto__ and constructor paths are dropped without polluting prototypes", () => {
+ const { eventData } = parsePixelQuery({
+ "__proto__[polluted]": "yes",
+ "constructor[prototype][evil]": "1",
+ name: "pageview",
+ });
+ expect(eventData).toEqual({ name: "pageview" });
+ expect(({} as Record).polluted).toBeUndefined();
+ expect(({} as Record).evil).toBeUndefined();
+ });
});
diff --git a/apps/basket/src/utils/pixel.ts b/apps/basket/src/utils/pixel.ts
index 4b3f5b7186..7ec7cce2f3 100644
--- a/apps/basket/src/utils/pixel.ts
+++ b/apps/basket/src/utils/pixel.ts
@@ -1,18 +1,15 @@
-// Regex patterns for parsing query parameters
const NESTED_KEY_REGEX = /^([^[]+)(\[.*\])?$/;
const BRACKET_EXTRACT_REGEX = /\[([^\]]+)\]/g;
const INTEGER_REGEX = /^-?\d+$/;
const FLOAT_REGEX = /^-?\d*\.\d+$/;
-// 1x1 transparent GIF pixel (base64)
+const SKIPPED_KEYS = new Set(["sdk_name", "sdk_version", "client_id"]);
+const UNSAFE_KEY_SEGMENTS = new Set(["__proto__", "constructor", "prototype"]);
+
const TRANSPARENT_PIXEL = Buffer.from(
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",
"base64"
);
-
-/**
- * Returns a 1x1 transparent GIF response
- */
export function createPixelResponse(
options: { retryAfterSeconds?: number; status?: number } = {}
): Response {
@@ -30,10 +27,6 @@ export function createPixelResponse(
headers,
});
}
-
-/**
- * Parses string values to appropriate types
- */
function parseValue(value: string): string | number | boolean {
if (INTEGER_REGEX.test(value)) {
return Number.parseInt(value, 10);
@@ -49,11 +42,6 @@ function parseValue(value: string): string | number | boolean {
}
return value;
}
-
-/**
- * Converts pixel query parameters back into event data structure
- * Handles nested keys like "key[subkey]" and JSON-stringified properties
- */
export function parsePixelQuery(query: Record): {
eventData: Record;
eventType: string;
@@ -61,12 +49,10 @@ export function parsePixelQuery(query: Record): {
const result: Record = {};
for (const [key, value] of Object.entries(query)) {
- // Skip SDK metadata
- if (key === "sdk_name" || key === "sdk_version" || key === "client_id") {
+ if (SKIPPED_KEYS.has(key)) {
continue;
}
- // Handle JSON-stringified properties
if (key === "properties") {
try {
result.properties = JSON.parse(value);
@@ -77,43 +63,24 @@ export function parsePixelQuery(query: Record): {
}
const match = key.match(NESTED_KEY_REGEX);
- if (!match) {
- result[key] = parseValue(value);
- continue;
- }
-
- const baseKey = match[1];
- const nestedPath = match[2];
-
- if (!nestedPath) {
- result[baseKey] = parseValue(value);
- continue;
- }
-
- // Extract nested keys from brackets
+ const baseKey = match?.[1] ?? key;
const nestedKeys =
- nestedPath.match(BRACKET_EXTRACT_REGEX)?.map((k) => k.slice(1, -1)) || [];
-
- if (nestedKeys.length === 0) {
- result[baseKey] = parseValue(value);
+ match?.[2]?.match(BRACKET_EXTRACT_REGEX)?.map((k) => k.slice(1, -1)) ??
+ [];
+ const path = [baseKey, ...nestedKeys];
+ if (path.some((segment) => UNSAFE_KEY_SEGMENTS.has(segment))) {
continue;
}
- // Build nested structure
- if (!result[baseKey]) {
- result[baseKey] = {};
- }
-
- let current = result[baseKey] as Record;
- const lastIndex = nestedKeys.length - 1;
- for (let i = 0; i < lastIndex; i++) {
- const nestedKey = nestedKeys[i];
- if (!current[nestedKey]) {
- current[nestedKey] = {};
+ let current = result;
+ for (const segment of path.slice(0, -1)) {
+ const next = current[segment];
+ if (!next || typeof next !== "object" || Array.isArray(next)) {
+ current[segment] = {};
}
- current = current[nestedKey] as Record;
+ current = current[segment] as Record;
}
- current[nestedKeys[lastIndex]] = parseValue(value);
+ current[path.at(-1) ?? baseKey] = parseValue(value);
}
return {
diff --git a/apps/basket/src/utils/user-agent.test.ts b/apps/basket/src/utils/user-agent.test.ts
index 554eb72fcd..04f6756327 100644
--- a/apps/basket/src/utils/user-agent.test.ts
+++ b/apps/basket/src/utils/user-agent.test.ts
@@ -39,8 +39,6 @@ const { detectBot, parseUserAgent } = await import("./user-agent");
const dummyReq = new Request("https://example.com");
-// ── detectBot wrapper — tests the legacy category mapping ──
-
describe("detectBot", () => {
test("not a bot → passes through", () => {
mockDetectBotShared.mockReturnValue({
@@ -113,24 +111,8 @@ describe("detectBot", () => {
expect(result.reason).toBe("suspicious_pattern");
expect(result.result).toEqual(sharedResult);
});
-
- test("non-bot has no category", () => {
- mockDetectBotShared.mockReturnValue({
- isBot: false,
- category: undefined,
- action: undefined,
- confidence: 0,
- reason: undefined,
- name: undefined,
- });
- const result = detectBot("Chrome/120", dummyReq);
- expect(result.category).toBeUndefined();
- expect(result.botName).toBeUndefined();
- });
});
-// ── parseUserAgent wrapper ──
-
describe("parseUserAgent", () => {
test("returns parsed fields from shared function", async () => {
mockParseUserAgentShared.mockReturnValue({
diff --git a/apps/basket/src/utils/validation.test.ts b/apps/basket/src/utils/validation.test.ts
index 035a9d5886..f31a752d5c 100644
--- a/apps/basket/src/utils/validation.test.ts
+++ b/apps/basket/src/utils/validation.test.ts
@@ -16,10 +16,7 @@ import {
validateSessionId,
} from "./validation";
-// ── sanitizeString ──
-
describe("sanitizeString", () => {
- // non-string → ""
for (const input of [null, undefined, 123, true, {}, []]) {
test(`${JSON.stringify(input)} → ""`, () =>
expect(sanitizeString(input)).toBe(""));
@@ -38,13 +35,17 @@ describe("sanitizeString", () => {
expect(sanitizeString("bold text")).toBe("bold text"));
test("strips dangerous chars <>'\",&", () => {
- // Angle brackets in HTML-like patterns are removed by tag stripper,
- // and bare <, >, ', ", & are removed by char stripper
expect(sanitizeString("a'b\"c&d")).toBe("abcd");
expect(sanitizeString("hellovalue")).toBe("testvalue");
});
+ test("defeats stacked-tag bypasses that reassemble after one strip pass", () => {
+ const result = sanitizeString("ipt>alert(1) ipt>");
+ expect(result).toBe("iptalert(1)ipt");
+ expect(result.toLowerCase()).not.toContain("`;
}
-
-/**
- * Generate full NPM code example with import and usage
- */
export function generateNpmCode(
websiteId: string,
trackingOptions: TrackingOptions
diff --git a/apps/dashboard/app/(main)/websites/[id]/_components/utils/tab-layout.tsx b/apps/dashboard/app/(main)/websites/[id]/_components/utils/tab-layout.tsx
deleted file mode 100644
index 7ea43999db..0000000000
--- a/apps/dashboard/app/(main)/websites/[id]/_components/utils/tab-layout.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import type React from "react";
-import { Skeleton } from "@databuddy/ui";
-import { BORDER_RADIUS } from "./ui-components";
-
-interface TabLayoutProps {
- actions?: React.ReactNode;
- children: React.ReactNode;
- className?: string;
- description?: string;
- isLoading?: boolean;
- title?: string;
-}
-
-export function TabLayout({
- title,
- description,
- isLoading = false,
- children,
- className = "",
- actions,
-}: TabLayoutProps) {
- if (isLoading) {
- return ;
- }
-
- return (
-
- {(title || description || actions) && (
-
- {(title || description) && (
-
- {title &&
{title} }
- {description && (
-
{description}
- )}
-
- )}
- {actions &&
{actions}
}
-
- )}
- {children}
-
- );
-}
-
-export function TabLoadingSkeleton() {
- const loadingSkeletonIds = [
- "loading-skeleton-1",
- "loading-skeleton-2",
- "loading-skeleton-3",
- "loading-skeleton-4",
- ];
- return (
-
-
- {loadingSkeletonIds.map((id) => (
-
- ))}
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/dashboard/app/(main)/websites/[id]/_components/utils/technology-helpers.tsx b/apps/dashboard/app/(main)/websites/[id]/_components/utils/technology-helpers.tsx
index 27b8265e02..da6483ac5d 100644
--- a/apps/dashboard/app/(main)/websites/[id]/_components/utils/technology-helpers.tsx
+++ b/apps/dashboard/app/(main)/websites/[id]/_components/utils/technology-helpers.tsx
@@ -1,45 +1,12 @@
-import Image from "next/image";
-import type React from "react";
-import { BrowserIcon, OSIcon } from "@/components/icon";
import {
DesktopIcon,
DeviceMobileIcon,
DeviceTabletIcon,
- GlobeIcon,
LaptopIcon,
QuestionIcon,
TelevisionIcon,
} from "@databuddy/ui/icons";
-// Regex patterns for browser name processing
-const MOBILE_PREFIX_REGEX = /^Mobile\s+/;
-const MOBILE_SUFFIX_REGEX = /\s+Mobile$/;
-
-export interface DeviceTypeEntry {
- device_brand?: string;
- device_model?: string;
- device_type: string;
- pageviews?: number;
- visitors: number;
-}
-
-export interface BrowserVersionEntry {
- browser: string;
- count?: number;
- pageviews?: number;
- version?: string;
- visitors: number;
-}
-
-export interface TechnologyTableEntry {
- category?: string;
- icon?: string;
- iconComponent?: React.ReactNode;
- name: string;
- percentage: number;
- visitors: number;
-}
-
export const getDeviceTypeIcon = (
deviceType: string | null | undefined,
size: "sm" | "md" | "lg" = "md"
@@ -97,113 +64,3 @@ export const getDeviceTypeIcon = (
return ;
};
-
-export const processDeviceData = (
- deviceTypes: DeviceTypeEntry[]
-): TechnologyTableEntry[] => {
- const deviceGroups: Record = {};
-
- for (const item of deviceTypes) {
- const deviceType = item.device_type || "Unknown";
- const capitalizedType =
- deviceType.charAt(0).toUpperCase() + deviceType.slice(1);
- deviceGroups[capitalizedType] =
- (deviceGroups[capitalizedType] || 0) + (item.visitors || 0);
- }
-
- const totalVisitors = Object.values(deviceGroups).reduce(
- (sum, count) => sum + count,
- 0
- );
-
- return Object.entries(deviceGroups)
- .sort(([, a], [, b]) => (b as number) - (a as number))
- .slice(0, 10)
- .map(([name, visitors]) => ({
- name,
- visitors,
- percentage:
- totalVisitors > 0 ? Math.round((visitors / totalVisitors) * 100) : 0,
- iconComponent: getDeviceTypeIcon(name, "md"),
- category: "device",
- }));
-};
-
-export const processBrowserData = (
- browserVersions: BrowserVersionEntry[]
-): TechnologyTableEntry[] => {
- const browserGroups: Record = {};
-
- for (const item of browserVersions) {
- let browserName = item.browser || "Unknown";
- browserName = browserName
- .replace(MOBILE_PREFIX_REGEX, "")
- .replace(MOBILE_SUFFIX_REGEX, "");
- browserGroups[browserName] =
- (browserGroups[browserName] || 0) + (item.visitors || 0);
- }
-
- const totalVisitors = Object.values(browserGroups).reduce(
- (sum, count) => sum + count,
- 0
- );
-
- return Object.entries(browserGroups)
- .sort(([, a], [, b]) => (b as number) - (a as number))
- .slice(0, 10)
- .map(([name, visitors]) => ({
- name,
- visitors,
- percentage:
- totalVisitors > 0 ? Math.round((visitors / totalVisitors) * 100) : 0,
- iconComponent: ,
- category: "browser",
- }));
-};
-
-export const TechnologyIcon = ({
- entry,
- size = "md",
-}: {
- entry: TechnologyTableEntry;
- size?: "sm" | "md" | "lg";
-}) => {
- if (entry.iconComponent) {
- return <>{entry.iconComponent}>;
- }
-
- // Use unified icon components for better consistency
- if (entry.category === "browser") {
- return ;
- }
-
- if (entry.category === "os") {
- return ;
- }
-
- // Fallback for other categories or when no category is specified
- if (entry.icon) {
- const sizeMap = {
- sm: 12,
- md: 16,
- lg: 20,
- };
- const iconSize = sizeMap[size];
-
- return (
-
-
-
- );
- }
-
- return ;
-};
diff --git a/apps/dashboard/app/(main)/websites/[id]/_components/utils/tracking-helpers.ts b/apps/dashboard/app/(main)/websites/[id]/_components/utils/tracking-helpers.ts
deleted file mode 100644
index 0c3aecb1ae..0000000000
--- a/apps/dashboard/app/(main)/websites/[id]/_components/utils/tracking-helpers.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-import { RECOMMENDED_DEFAULTS } from "./tracking-defaults";
-import type { TrackingOptions } from "./types";
-
-/**
- * Toggle a specific tracking option
- */
-export function toggleTrackingOption(
- options: TrackingOptions,
- option: keyof TrackingOptions
-): TrackingOptions {
- return {
- ...options,
- [option]: !options[option],
- };
-}
-
-/**
- * Enable all basic tracking options
- */
-export function enableAllBasicTracking(
- options: TrackingOptions
-): TrackingOptions {
- return {
- ...options,
- trackInteractions: true,
- trackOutgoingLinks: true,
- };
-}
-
-/**
- * Enable all interaction tracking options
- */
-export function enableAllInteractionTracking(
- options: TrackingOptions
-): TrackingOptions {
- return {
- ...options,
- trackAttributes: true,
- trackOutgoingLinks: true,
- trackInteractions: true,
- trackHashChanges: true,
- };
-}
-
-/**
- * Enable all performance tracking options
- */
-export function enableAllPerformanceTracking(
- options: TrackingOptions
-): TrackingOptions {
- return {
- ...options,
- trackWebVitals: true,
- trackErrors: true,
- };
-}
-
-/**
- * Enable all advanced tracking options
- */
-export function enableAllAdvancedTracking(
- options: TrackingOptions
-): TrackingOptions {
- return {
- ...options,
- ...enableAllPerformanceTracking(options),
- trackErrors: true,
- trackWebVitals: true,
- };
-}
-
-/**
- * Enable batching with optimal settings
- */
-export function enableOptimalBatching(
- options: TrackingOptions
-): TrackingOptions {
- return {
- ...options,
- enableBatching: true,
- batchSize: 10,
- batchTimeout: 2000,
- };
-}
-
-/**
- * Enable all optimization options
- */
-export function enableAllOptimization(
- options: TrackingOptions
-): TrackingOptions {
- return {
- ...options,
- ...enableOptimalBatching(options),
- samplingRate: 1.0,
- enableRetries: true,
- maxRetries: 3,
- initialRetryDelay: 500,
- };
-}
-
-/**
- * Disable all tracking (privacy mode)
- */
-export function enablePrivacyMode(options: TrackingOptions): TrackingOptions {
- return {
- ...options,
- disabled: true,
- trackInteractions: false,
- trackOutgoingLinks: false,
- trackAttributes: false,
- trackWebVitals: false,
- trackErrors: false,
- };
-}
-
-/**
- * Reset options to recommended defaults
- */
-export function resetToDefaults(): TrackingOptions {
- return { ...RECOMMENDED_DEFAULTS };
-}
diff --git a/apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts b/apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts
index 31891590a6..1a11c95403 100644
--- a/apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts
+++ b/apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts
@@ -8,12 +8,12 @@ export interface DateRange {
timezone?: string;
}
-export interface BaseTabProps {
+interface BaseTabProps {
dateRange: DateRange;
websiteId: string;
}
-export type WebsiteData = ReturnType["data"];
+type WebsiteData = ReturnType["data"];
export type FullTabProps = BaseTabProps & {
websiteData: WebsiteData;
diff --git a/apps/dashboard/app/(main)/websites/[id]/_components/utils/ui-components.tsx b/apps/dashboard/app/(main)/websites/[id]/_components/utils/ui-components.tsx
index 3d0c666eee..a16f0eb092 100644
--- a/apps/dashboard/app/(main)/websites/[id]/_components/utils/ui-components.tsx
+++ b/apps/dashboard/app/(main)/websites/[id]/_components/utils/ui-components.tsx
@@ -1,160 +1,4 @@
import type React from "react";
-import { cn } from "@/lib/utils";
-import { PERFORMANCE_THRESHOLDS } from "./analytics-helpers";
-import { ArrowSquareOutIcon, QuestionIcon } from "@databuddy/ui/icons";
-import { StatusDot, Tooltip } from "@databuddy/ui";
-
-// Consistent border radius values
-export const BORDER_RADIUS = {
- sm: "rounded", // Small components like buttons
- md: "rounded", // Cards, panels
- lg: "rounded", // Large containers
- card: "rounded", // Standard card component
- container: "rounded", // Containers that hold cards
-};
-
-interface MetricToggleProps {
- checked: boolean;
- color: string;
- label: string;
- onChange: () => void;
-}
-
-export const MetricToggle: React.FC = ({
- label,
- checked,
- onChange,
- color,
-}) => {
- // Get proper hex values for the colors
- const getColorMap = (colorName: string) => {
- const colorMap: Record = {
- "blue-500": "#3b82f6",
- "green-500": "#22c55e",
- "emerald-500": "#10b981",
- "yellow-500": "#eab308",
- "red-500": "#ef4444",
- "purple-500": "#a855f7",
- "pink-500": "#ec4899",
- "indigo-500": "#6366f1",
- "orange-500": "#f97316",
- "sky-500": "#0ea5e9",
- "amber-500": "#fd9a00",
- };
-
- return colorMap[colorName] || "#3b82f6"; // Default to blue if color not found
- };
-
- const colorHex = getColorMap(color);
-
- return (
- {
- if (e.key === "Enter" || e.key === " ") {
- e.preventDefault();
- onChange();
- }
- }}
- tabIndex={0}
- type="button"
- >
-
-
- {label}
-
-
- );
-};
-
-interface MetricTogglesProps {
- colors: Record;
- labels?: Record;
- metrics: Record;
- onToggle: (metric: string) => void;
-}
-
-const EMPTY_LABELS: Record = {};
-
-export const MetricToggles: React.FC = ({
- metrics,
- onToggle,
- colors,
- labels = EMPTY_LABELS,
-}) => (
-
- {Object.keys(metrics).map((metric) => (
- onToggle(metric)}
- />
- ))}
-
-);
-
-interface ArrowSquareOutIconButtonProps {
- className?: string;
- href: string;
- label: string;
- showTooltip?: boolean;
- title?: string;
-}
-
-export const ArrowSquareOutIconButton: React.FC<
- ArrowSquareOutIconButtonProps
-> = ({
- href,
- label,
- title,
- className = "font-medium hover:text-primary hover:underline truncate max-w-[250px] flex items-center gap-1",
- showTooltip = true,
-}) => {
- const content = (
-
- {label}
-
-
- );
-
- if (!showTooltip) {
- return content;
- }
-
- return {content} ;
-};
export const EmptyState: React.FC<{
icon: React.ReactNode;
@@ -173,56 +17,3 @@ export const EmptyState: React.FC<{
);
-
-// Tooltip for performance metrics
-export const MetricTooltip = ({
- metricKey,
- label,
- children,
-}: {
- metricKey: keyof typeof PERFORMANCE_THRESHOLDS;
- label?: string;
- children: React.ReactNode;
-}) => {
- const threshold = PERFORMANCE_THRESHOLDS[metricKey];
- return (
-
-
- {label || String(metricKey).replace(/_/g, " ")}
-
-
-
-
-
- Good: < {threshold.good}
- {threshold.unit}
-
-
-
-
-
- Needs improvement: {threshold.good}
- {threshold.unit} - {threshold.average}
- {threshold.unit}
-
-
-
-
-
- Poor: > {threshold.average}
- {threshold.unit}
-
-
-
-
- }
- >
-
- {children}
-
-
-
- );
-};
diff --git a/apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-key.tsx b/apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-key.tsx
deleted file mode 100644
index d86c812ee9..0000000000
--- a/apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-key.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
-import { cn } from "@/lib/utils";
-import type { Flag } from "./types";
-import { CheckIcon, CopyIcon } from "@databuddy/ui/icons";
-import { Button, Tooltip } from "@databuddy/ui";
-
-export function FlagKey({
- flag,
- className,
- ...props
-}: { flag: Flag } & React.ComponentProps<"button">) {
- const { isCopied, copyToClipboard } = useCopyToClipboard();
-
- return (
-
- svg]:px-1.5 dark:text-foreground/70",
- className
- )}
- data-row-interactive="true"
- onClick={() => copyToClipboard(flag.key)}
- size="sm"
- variant="ghost"
- {...props}
- >
- {flag.key}
- {isCopied ? (
-
- ) : (
-
- )}
-
-
- );
-}
diff --git a/apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts b/apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts
index a0607ce4aa..ef03854e75 100644
--- a/apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts
+++ b/apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts
@@ -67,8 +67,6 @@ export interface TargetGroup {
websiteId: string;
}
-export type FlagStatus = "active" | "inactive" | "archived";
-
export interface FlagSheetProps {
flag?: Flag | null;
isOpen: boolean;
diff --git a/apps/dashboard/app/(main)/websites/[id]/funnels/_components/index.ts b/apps/dashboard/app/(main)/websites/[id]/funnels/_components/index.ts
index 619244defb..5a65f74772 100644
--- a/apps/dashboard/app/(main)/websites/[id]/funnels/_components/index.ts
+++ b/apps/dashboard/app/(main)/websites/[id]/funnels/_components/index.ts
@@ -1,9 +1,6 @@
-export { EditFunnelDialog } from "./edit-funnel-dialog";
export { FunnelAnalytics } from "./funnel-analytics";
export { FunnelAnalyticsByReferrer } from "./funnel-analytics-by-referrer";
-export { FunnelFlow } from "./funnel-flow";
export {
- FunnelItem,
type FunnelItemData,
FunnelItemSkeleton,
} from "./funnel-item";
diff --git a/apps/dashboard/app/(main)/websites/[id]/goals/_components/goal-analytics.tsx b/apps/dashboard/app/(main)/websites/[id]/goals/_components/goal-analytics.tsx
deleted file mode 100644
index 3940dbb308..0000000000
--- a/apps/dashboard/app/(main)/websites/[id]/goals/_components/goal-analytics.tsx
+++ /dev/null
@@ -1,207 +0,0 @@
-"use client";
-
-import { formatNumber } from "@/lib/formatters";
-import {
- ArrowClockwiseIcon as ArrowClockwise,
- TargetIcon as Target,
- TrendUpIcon as TrendUp,
- UsersIcon as Users,
-} from "@databuddy/ui/icons";
-import { Button, Card } from "@databuddy/ui";
-
-interface GoalAnalyticsProps {
- data: any;
- error: Error | null;
- isLoading: boolean;
- onRetry: () => void;
- summaryStats: {
- totalUsers: number;
- conversionRate: number;
- completions: number;
- };
-}
-
-export function GoalAnalytics({
- isLoading,
- error,
- data,
- summaryStats,
- onRetry,
-}: GoalAnalyticsProps) {
- if (isLoading) {
- return (
-
-
- {[...new Array(3)].map((_, i) => (
-
-
-
-
-
-
- ))}
-
-
- );
- }
-
- if (error) {
- return (
-
-
-
-
-
- Failed to load goal analytics
-
-
{error.message}
-
-
-
- Retry
-
-
-
-
- );
- }
-
- if (!(data?.success && data.data)) {
- return (
-
-
-
- No analytics data available
-
-
-
- );
- }
-
- const formatPercentage = (num: number) => `${num.toFixed(1)}%`;
-
- return (
-
-
-
-
-
-
-
-
-
-
- Total Users
-
-
- {formatNumber(summaryStats.totalUsers)}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Completions
-
-
- {formatNumber(summaryStats.completions)}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Conversion Rate
-
-
- {formatPercentage(summaryStats.conversionRate)}
-
-
-
-
-
-
-
-
-
- Goal Performance
-
-
-
-
-
- Performance Summary
-
- {data.date_range?.start_date} - {data.date_range?.end_date}
-
-
-
-
-
-
- Users who reached goal
-
-
- {formatNumber(summaryStats.completions)} /{" "}
- {formatNumber(summaryStats.totalUsers)}
-
-
-
-
- Success rate
-
-
- {formatPercentage(summaryStats.conversionRate)}
-
-
-
-
-
- {data.data.avg_completion_time > 0 && (
-
-
-
- Average Time to Complete
-
-
- {data.data.avg_completion_time_formatted ||
- `${Math.round(data.data.avg_completion_time)}s`}
-
-
-
- )}
-
-
-
-
- );
-}
diff --git a/apps/dashboard/app/(main)/websites/[id]/pulse/_components/monitor-card.tsx b/apps/dashboard/app/(main)/websites/[id]/pulse/_components/monitor-card.tsx
deleted file mode 100644
index fd74d7a15c..0000000000
--- a/apps/dashboard/app/(main)/websites/[id]/pulse/_components/monitor-card.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-"use client";
-
-import { useMutation } from "@tanstack/react-query";
-import { useState } from "react";
-import { toast } from "sonner";
-import { orpc } from "@/lib/orpc";
-import {
- DotsThreeIcon,
- HeartbeatIcon,
- PencilIcon,
- TrashIcon,
-} from "@databuddy/ui/icons";
-import { Card, GhostTriggerButton } from "@databuddy/ui";
-import { DropdownMenu } from "@databuddy/ui/client";
-
-const granularityLabels: Record = {
- minute: "1m",
- ten_minutes: "10m",
- thirty_minutes: "30m",
- hour: "1h",
- six_hours: "6h",
- twelve_hours: "12h",
- day: "Daily",
-};
-
-interface MonitorCardProps {
- onDeleteAction: () => void;
- onEditAction: () => void;
- onRefetchAction: () => void;
- schedule: {
- id: string;
- granularity: string;
- cron: string;
- isPaused: boolean;
- createdAt: Date | string;
- updatedAt: Date | string;
- };
-}
-
-export function MonitorCard({
- schedule,
- onEditAction,
- onDeleteAction,
- onRefetchAction,
-}: MonitorCardProps) {
- const [isPausing, setIsPausing] = useState(false);
-
- const pauseMutation = useMutation({
- ...orpc.uptime.pauseSchedule.mutationOptions(),
- });
- const resumeMutation = useMutation({
- ...orpc.uptime.resumeSchedule.mutationOptions(),
- });
-
- const handleTogglePause = async () => {
- setIsPausing(true);
- try {
- if (schedule.isPaused) {
- await resumeMutation.mutateAsync({ scheduleId: schedule.id });
- toast.success("Monitor resumed");
- } else {
- await pauseMutation.mutateAsync({ scheduleId: schedule.id });
- toast.success("Monitor paused");
- }
- onRefetchAction();
- } catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : "Failed to update monitor";
- toast.error(errorMessage);
- } finally {
- setIsPausing(false);
- }
- };
-
- return (
-
-
-
-
-
-
-
-
-
- Uptime Monitor
-
-
-
- Check Frequency:
-
- {granularityLabels[schedule.granularity] ||
- schedule.granularity}
-
-
-
- Status:
-
- {schedule.isPaused ? "Paused" : "Active"}
-
-
-
-
-
-
-
-
-
-
- }
- />
-
-
-
- Edit
-
-
-
- {schedule.isPaused ? "Resume" : "Pause"}
-
-
-
- Delete
-
-
-
-
-
-
- );
-}
diff --git a/apps/dashboard/app/(main)/websites/[id]/pulse/_components/recent-activity.tsx b/apps/dashboard/app/(main)/websites/[id]/pulse/_components/recent-activity.tsx
index 756681ebdc..811654cd86 100644
--- a/apps/dashboard/app/(main)/websites/[id]/pulse/_components/recent-activity.tsx
+++ b/apps/dashboard/app/(main)/websites/[id]/pulse/_components/recent-activity.tsx
@@ -78,33 +78,6 @@ function LoadMoreSkeletonRow() {
);
}
-function InitialTableSkeleton({ rows }: { rows: number }) {
- return (
-
-
-
-
- {Array.from({ length: rows }).map((_, i) => (
-
- ))}
-
-
-
- );
-}
-
-export function RecentActivityTableSkeleton({ rows = 8 }: { rows?: number }) {
- return ;
-}
-
export function RecentActivity({
checks,
hasMore = false,
diff --git a/apps/dashboard/app/(main)/websites/[id]/pulse/_components/status-header.tsx b/apps/dashboard/app/(main)/websites/[id]/pulse/_components/status-header.tsx
deleted file mode 100644
index 556c55917e..0000000000
--- a/apps/dashboard/app/(main)/websites/[id]/pulse/_components/status-header.tsx
+++ /dev/null
@@ -1,194 +0,0 @@
-"use client";
-
-import { useMutation } from "@tanstack/react-query";
-import { useState } from "react";
-import { toast } from "sonner";
-import { orpc } from "@/lib/orpc";
-import { cn } from "@/lib/utils";
-import {
- CircleIcon,
- PauseIcon,
- PencilIcon,
- PlayIcon,
- TrashIcon,
-} from "@databuddy/ui/icons";
-import { DropdownMenu } from "@databuddy/ui/client";
-import { Button, Card, GhostTriggerButton, fromNow } from "@databuddy/ui";
-
-const granularityLabels: Record = {
- minute: "Every minute",
- ten_minutes: "Every 10 minutes",
- thirty_minutes: "Every 30 minutes",
- hour: "Hourly",
- six_hours: "Every 6 hours",
- twelve_hours: "Every 12 hours",
- day: "Daily",
-};
-
-interface StatusHeaderProps {
- currentStatus?: "up" | "down" | "unknown";
- lastCheck?: {
- timestamp: string;
- status: number;
- probe_region?: string;
- };
- onDeleteAction: () => void;
- onEditAction: () => void;
- onRefetchAction: () => void;
- schedule: {
- id: string;
- granularity: string;
- cron: string;
- isPaused: boolean;
- createdAt: Date | string;
- updatedAt: Date | string;
- };
-}
-
-export function StatusHeader({
- schedule,
- currentStatus = "unknown",
- lastCheck,
- onEditAction,
- onDeleteAction,
- onRefetchAction,
-}: StatusHeaderProps) {
- const [isPausing, setIsPausing] = useState(false);
-
- const pauseMutation = useMutation({
- ...orpc.uptime.pauseSchedule.mutationOptions(),
- });
- const resumeMutation = useMutation({
- ...orpc.uptime.resumeSchedule.mutationOptions(),
- });
-
- const handleTogglePause = async () => {
- setIsPausing(true);
- try {
- if (schedule.isPaused) {
- await resumeMutation.mutateAsync({ scheduleId: schedule.id });
- toast.success("Monitor resumed");
- } else {
- await pauseMutation.mutateAsync({ scheduleId: schedule.id });
- toast.success("Monitor paused");
- }
- onRefetchAction();
- } catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : "Failed to update monitor";
- toast.error(errorMessage);
- } finally {
- setIsPausing(false);
- }
- };
-
- const isOperational =
- !schedule.isPaused &&
- (currentStatus === "up" || currentStatus === "unknown");
- const isDown = !schedule.isPaused && currentStatus === "down";
- const isPaused = schedule.isPaused;
-
- return (
-
-
-
-
-
-
- {isPaused
- ? "Monitoring Paused"
- : isDown
- ? "System Outage"
- : "All Systems Operational"}
-
-
-
-
- {granularityLabels[schedule.granularity] || schedule.granularity}
-
- {lastCheck ? (
- <>
- •
- Last checked {fromNow(lastCheck.timestamp)}
- {lastCheck.probe_region ? (
- <>
- •
- from {lastCheck.probe_region}
- >
- ) : null}
- >
- ) : null}
-
-
-
-
-
- {schedule.isPaused ? (
- <>
-
- Resume
- >
- ) : (
- <>
-
- Pause
- >
- )}
-
-
-
- Configure
-
-
-
-
-
- }
- />
-
-
-
- Delete Monitor
-
-
-
-
-
-
- );
-}
diff --git a/apps/dashboard/app/(main)/websites/[id]/realtime/_components/realtime-map.tsx b/apps/dashboard/app/(main)/websites/[id]/realtime/_components/realtime-map.tsx
index 0aa36e74de..d7266b49c6 100644
--- a/apps/dashboard/app/(main)/websites/[id]/realtime/_components/realtime-map.tsx
+++ b/apps/dashboard/app/(main)/websites/[id]/realtime/_components/realtime-map.tsx
@@ -186,7 +186,10 @@ export function RealtimeMap({ countries }: RealtimeMapProps) {
oy: number;
} | null>(null);
const [tooltip, setTooltip] = useState(null);
- countriesRef.current = countries;
+
+ useEffect(() => {
+ countriesRef.current = countries;
+ }, [countries]);
useEffect(() => {
const canvas = canvasRef.current;
diff --git a/apps/dashboard/app/(main)/websites/[id]/revenue/_components/revenue-attribution-tables.tsx b/apps/dashboard/app/(main)/websites/[id]/revenue/_components/revenue-attribution-tables.tsx
index cf949d468f..cbbb870508 100644
--- a/apps/dashboard/app/(main)/websites/[id]/revenue/_components/revenue-attribution-tables.tsx
+++ b/apps/dashboard/app/(main)/websites/[id]/revenue/_components/revenue-attribution-tables.tsx
@@ -13,6 +13,20 @@ import { useBatchDynamicQuery } from "@/hooks/use-dynamic-query";
import { WarningCircleIcon } from "@databuddy/ui/icons";
import { Card, EmptyState } from "@databuddy/ui";
+type BatchQueryResults = ReturnType["results"];
+
+function getRevenueData(
+ results: BatchQueryResults,
+ queryId: string,
+ parameter: string
+): RevenueEntry[] {
+ const result = results.find((r) => r.queryId === queryId);
+ if (!result?.success) {
+ return [];
+ }
+ return (result.data[parameter] as RevenueEntry[]) || [];
+}
+
interface RevenueAttributionTablesProps {
currency: string;
dateRange: DateRange;
@@ -70,7 +84,7 @@ export function RevenueAttributionTables({
[queryFilters]
);
- const { getDataForQuery, isError, isLoading, refetch } = useBatchDynamicQuery(
+ const { results, isError, isLoading, refetch } = useBatchDynamicQuery(
websiteId,
dateRange,
queries,
@@ -78,81 +92,57 @@ export function RevenueAttributionTables({
);
const productData = useMemo(
- () =>
- (getDataForQuery(
- "revenue-products",
- "revenue_by_product"
- ) as RevenueEntry[]) || [],
- [getDataForQuery]
+ () => getRevenueData(results, "revenue-products", "revenue_by_product"),
+ [results]
);
const trafficData = useMemo(
() => ({
- referrers:
- (getDataForQuery(
- "revenue-traffic",
- "revenue_by_referrer"
- ) as RevenueEntry[]) || [],
- utm_sources:
- (getDataForQuery(
- "revenue-traffic",
- "revenue_by_utm_source"
- ) as RevenueEntry[]) || [],
- utm_mediums:
- (getDataForQuery(
- "revenue-traffic",
- "revenue_by_utm_medium"
- ) as RevenueEntry[]) || [],
- utm_campaigns:
- (getDataForQuery(
- "revenue-traffic",
- "revenue_by_utm_campaign"
- ) as RevenueEntry[]) || [],
- entry_pages:
- (getDataForQuery(
- "revenue-traffic",
- "revenue_by_entry_page"
- ) as RevenueEntry[]) || [],
+ referrers: getRevenueData(
+ results,
+ "revenue-traffic",
+ "revenue_by_referrer"
+ ),
+ utm_sources: getRevenueData(
+ results,
+ "revenue-traffic",
+ "revenue_by_utm_source"
+ ),
+ utm_mediums: getRevenueData(
+ results,
+ "revenue-traffic",
+ "revenue_by_utm_medium"
+ ),
+ utm_campaigns: getRevenueData(
+ results,
+ "revenue-traffic",
+ "revenue_by_utm_campaign"
+ ),
+ entry_pages: getRevenueData(
+ results,
+ "revenue-traffic",
+ "revenue_by_entry_page"
+ ),
}),
- [getDataForQuery]
+ [results]
);
const geoData = useMemo(
() => ({
- countries:
- (getDataForQuery(
- "revenue-geo",
- "revenue_by_country"
- ) as RevenueEntry[]) || [],
- regions:
- (getDataForQuery(
- "revenue-geo",
- "revenue_by_region"
- ) as RevenueEntry[]) || [],
- cities:
- (getDataForQuery("revenue-geo", "revenue_by_city") as RevenueEntry[]) ||
- [],
+ countries: getRevenueData(results, "revenue-geo", "revenue_by_country"),
+ regions: getRevenueData(results, "revenue-geo", "revenue_by_region"),
+ cities: getRevenueData(results, "revenue-geo", "revenue_by_city"),
}),
- [getDataForQuery]
+ [results]
);
const techData = useMemo(
() => ({
- devices:
- (getDataForQuery(
- "revenue-tech",
- "revenue_by_device"
- ) as RevenueEntry[]) || [],
- browsers:
- (getDataForQuery(
- "revenue-tech",
- "revenue_by_browser"
- ) as RevenueEntry[]) || [],
- os:
- (getDataForQuery("revenue-tech", "revenue_by_os") as RevenueEntry[]) ||
- [],
+ devices: getRevenueData(results, "revenue-tech", "revenue_by_device"),
+ browsers: getRevenueData(results, "revenue-tech", "revenue_by_browser"),
+ os: getRevenueData(results, "revenue-tech", "revenue_by_os"),
}),
- [getDataForQuery]
+ [results]
);
const productColumns = useMemo(
diff --git a/apps/dashboard/app/(main)/websites/[id]/vitals/columns.tsx b/apps/dashboard/app/(main)/websites/[id]/vitals/columns.tsx
index 17095bed80..d7a6c57eae 100644
--- a/apps/dashboard/app/(main)/websites/[id]/vitals/columns.tsx
+++ b/apps/dashboard/app/(main)/websites/[id]/vitals/columns.tsx
@@ -159,38 +159,6 @@ export const createBrowserColumns = (): ColumnDef[] => [
...createMetricColumns(),
];
-export const createDeviceColumns = (): ColumnDef[] => [
- {
- id: "name",
- accessorKey: "name",
- header: "Device",
- cell: ({ getValue }) => {
- const name = getValue() as string;
- const deviceLabels: Record = {
- mobile: "Mobile",
- desktop: "Desktop",
- tablet: "Tablet",
- };
- return (
-
- {deviceLabels[name.toLowerCase()] || name}
-
- );
- },
- },
- {
- id: "visitors",
- accessorKey: "visitors",
- header: "Visitors",
- cell: ({ getValue }) => (
-
- {formatNumber((getValue() as number) || 0)}
-
- ),
- },
- ...createMetricColumns(),
-];
-
export const createRegionColumns = (): ColumnDef[] => {
const getRegionCountryIcon = (name: string) => {
if (typeof name !== "string" || !name.includes(",")) {
diff --git a/apps/dashboard/app/(main)/websites/_components/website-card.tsx b/apps/dashboard/app/(main)/websites/_components/website-card.tsx
index f10aee9644..26a95a8e91 100644
--- a/apps/dashboard/app/(main)/websites/_components/website-card.tsx
+++ b/apps/dashboard/app/(main)/websites/_components/website-card.tsx
@@ -357,28 +357,3 @@ export const WebsiteCard = memo(
);
WebsiteCard.displayName = "WebsiteCard";
-
-export function WebsiteCardSkeleton() {
- return (
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/dashboard/app/actions/users.ts b/apps/dashboard/app/actions/users.ts
index 9c24f6dbab..bd0e77acb3 100644
--- a/apps/dashboard/app/actions/users.ts
+++ b/apps/dashboard/app/actions/users.ts
@@ -2,7 +2,7 @@
import { auth } from "@databuddy/auth";
import { and, db, eq } from "@databuddy/db";
-import { account, user } from "@databuddy/db/schema";
+import { account } from "@databuddy/db/schema";
import { revalidatePath } from "next/cache";
import { headers } from "next/headers";
import { cache } from "react";
@@ -18,63 +18,11 @@ const getUser = cache(async () => {
return session.user;
});
-const profileUpdateSchema = z.object({
- firstName: z
- .string()
- .min(1, "First name is required")
- .max(50, "First name cannot exceed 50 characters"),
- lastName: z
- .string()
- .min(1, "Last name is required")
- .max(50, "Last name cannot exceed 50 characters"),
- image: z.url("Please enter a valid image URL").optional(),
-});
-
const passwordSchema = z
.string()
.min(8, "Password must be at least 8 characters")
.max(128, "Password cannot exceed 128 characters");
-export async function updateUserProfile(formData: FormData) {
- const currentUser = await getUser();
- if (!currentUser) {
- return { error: "Unauthorized" };
- }
-
- try {
- const firstName = formData.get("firstName");
- const lastName = formData.get("lastName");
- const image = formData.get("image");
-
- const validatedData = profileUpdateSchema.parse({
- firstName,
- lastName,
- image: image || undefined,
- });
-
- const _updated = await db
- .update(user)
- .set({
- firstName: validatedData.firstName,
- lastName: validatedData.lastName,
- image: validatedData.image,
- name: `${validatedData.firstName} ${validatedData.lastName}`,
- })
- .where(eq(user.id, currentUser.id))
- .returning();
-
- revalidatePath("/settings");
- return { success: true };
- } catch (error) {
- console.error("Profile update error:", error);
-
- if (error instanceof z.ZodError) {
- return { error: error.message };
- }
- return { error: "Failed to update profile" };
- }
-}
-
export async function setPasswordForOAuthUser(newPassword: string) {
const currentUser = await getUser();
if (!currentUser) {
diff --git a/apps/dashboard/app/global-error.tsx b/apps/dashboard/app/global-error.tsx
index ae5d65df79..461c1244e2 100644
--- a/apps/dashboard/app/global-error.tsx
+++ b/apps/dashboard/app/global-error.tsx
@@ -1,12 +1,6 @@
"use client";
import { Button } from "@databuddy/ui";
-
-/**
- * Root error boundary — must define its own and and cannot rely on
- * the root layout. Avoid `next/error` here: it expects Pages Router context and
- * breaks prerender (useContext) during `next build`.
- */
export default function GlobalError({
error,
reset,
diff --git a/apps/dashboard/app/public/[id]/layout.tsx b/apps/dashboard/app/public/[id]/layout.tsx
index 7ceb5b6f7a..83a4145488 100644
--- a/apps/dashboard/app/public/[id]/layout.tsx
+++ b/apps/dashboard/app/public/[id]/layout.tsx
@@ -15,8 +15,6 @@ import { PlanetIcon } from "@databuddy/ui/icons";
const poweredByLabelClass =
"shrink-0 text-balance font-medium text-muted-foreground text-sm";
-
-/** Header Powered by row: wraps cleanly on narrow viewports. */
const brandAttributionLinkClass =
"flex min-w-0 flex-wrap items-center gap-3 rounded transition-opacity hover:opacity-90";
diff --git a/apps/dashboard/app/public/public-dashboard-constants.ts b/apps/dashboard/app/public/public-dashboard-constants.ts
index fe280e4ec6..90e3623f5a 100644
--- a/apps/dashboard/app/public/public-dashboard-constants.ts
+++ b/apps/dashboard/app/public/public-dashboard-constants.ts
@@ -1,3 +1,2 @@
-/** Marketing site URL with ref for public dashboard header, footer CTA, and outbound links. */
export const publicDashboardMarketingHref =
"https://www.databuddy.cc?ref=public-dashboard" as const;
diff --git a/apps/dashboard/components/agent/agent-credit-balance.tsx b/apps/dashboard/components/agent/agent-credit-balance.tsx
index a7e67eaafc..29091fc591 100644
--- a/apps/dashboard/components/agent/agent-credit-balance.tsx
+++ b/apps/dashboard/components/agent/agent-credit-balance.tsx
@@ -30,7 +30,9 @@ export function AgentCreditBalance({
const prevStatusRef = useRef(status);
const refetchRef = useRef(refetch);
- refetchRef.current = refetch;
+ useEffect(() => {
+ refetchRef.current = refetch;
+ }, [refetch]);
useEffect(() => {
const prev = prevStatusRef.current;
diff --git a/apps/dashboard/components/agent/agent-input.tsx b/apps/dashboard/components/agent/agent-input.tsx
index e66e5af90a..2cce7df51f 100644
--- a/apps/dashboard/components/agent/agent-input.tsx
+++ b/apps/dashboard/components/agent/agent-input.tsx
@@ -78,7 +78,9 @@ export function AgentInput() {
const textareaRef = useRef(null);
const replayFrameRef = useRef(null);
const inputSyncRef = useRef(input);
- inputSyncRef.current = input;
+ useEffect(() => {
+ inputSyncRef.current = input;
+ }, [input]);
const cancelPlaceholderReplay = useCallback(() => {
if (replayFrameRef.current === null) {
diff --git a/apps/dashboard/components/agent/agent-text-switch.tsx b/apps/dashboard/components/agent/agent-text-switch.tsx
index dec03d1f9c..81ca5734b5 100644
--- a/apps/dashboard/components/agent/agent-text-switch.tsx
+++ b/apps/dashboard/components/agent/agent-text-switch.tsx
@@ -109,10 +109,12 @@ function DualStaggerStack({
const layerDoneRef = useRef({ bottom: false, top: false });
const previousFlipRef = useRef(false);
- if (flip && !previousFlipRef.current) {
- layerDoneRef.current = { bottom: false, top: false };
- }
- previousFlipRef.current = flip;
+ useEffect(() => {
+ if (flip && !previousFlipRef.current) {
+ layerDoneRef.current = { bottom: false, top: false };
+ }
+ previousFlipRef.current = flip;
+ }, [flip]);
const spring = stagger ? SPRING : SPRING_NO_STAGGER;
@@ -263,11 +265,7 @@ export function AgentTextSwitch({
}, [active, clearHold]);
useEffect(() => {
- if (!(active && inView)) {
- clearHold();
- return;
- }
- if (flip) {
+ if (!(active && inView) || flip) {
clearHold();
} else {
scheduleHold();
diff --git a/apps/dashboard/components/agent/hooks/use-agent-chat.ts b/apps/dashboard/components/agent/hooks/use-agent-chat.ts
index 8439d4ea52..9884562c76 100644
--- a/apps/dashboard/components/agent/hooks/use-agent-chat.ts
+++ b/apps/dashboard/components/agent/hooks/use-agent-chat.ts
@@ -4,7 +4,7 @@ import { publicConfig } from "@databuddy/env/public";
import { DefaultChatTransport, type UIMessage } from "ai";
import { useAtomValue } from "jotai";
-import { useMemo, useRef } from "react";
+import { useEffect, useMemo, useRef } from "react";
import { normalizeAIComponentMessages } from "@/lib/ai-components/message-parts";
import {
agentMentionsAtom,
@@ -25,9 +25,11 @@ export function useAgentChatTransport(
const thinkingRef = useRef(thinking);
const tierRef = useRef(tier);
const mentionsRef = useRef(mentions);
- thinkingRef.current = thinking;
- tierRef.current = tier;
- mentionsRef.current = mentions;
+ useEffect(() => {
+ thinkingRef.current = thinking;
+ tierRef.current = tier;
+ mentionsRef.current = mentions;
+ }, [thinking, tier, mentions]);
return useMemo(
() =>
diff --git a/apps/dashboard/components/ai-elements/ai-component.tsx b/apps/dashboard/components/ai-elements/ai-component.tsx
index 84c46ee580..8f97b91f87 100644
--- a/apps/dashboard/components/ai-elements/ai-component.tsx
+++ b/apps/dashboard/components/ai-elements/ai-component.tsx
@@ -61,12 +61,6 @@ interface AIComponentProps {
input: RawComponentInput;
streaming?: boolean;
}
-
-/**
- * Renders an AI-generated component based on its type.
- * During streaming, skips strict validation and shows a skeleton
- * if the data is too incomplete to render.
- */
export function AIComponent({ input, className, streaming }: AIComponentProps) {
if (!hasComponent(input.type)) {
return null;
diff --git a/apps/dashboard/components/ai-elements/canvas.tsx b/apps/dashboard/components/ai-elements/canvas.tsx
deleted file mode 100644
index 05e66d648a..0000000000
--- a/apps/dashboard/components/ai-elements/canvas.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Background, ReactFlow, type ReactFlowProps } from "@xyflow/react";
-import type { ReactNode } from "react";
-import "@xyflow/react/dist/style.css";
-
-type CanvasProps = ReactFlowProps & {
- children?: ReactNode;
-};
-
-export const Canvas = ({ children, ...props }: CanvasProps) => (
-
-
- {children}
-
-);
diff --git a/apps/dashboard/components/ai-elements/chain-of-thought.tsx b/apps/dashboard/components/ai-elements/chain-of-thought.tsx
deleted file mode 100644
index 23a68f6065..0000000000
--- a/apps/dashboard/components/ai-elements/chain-of-thought.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-"use client";
-
-import type { ComponentProps, ReactNode } from "react";
-import { memo } from "react";
-import { cn } from "@/lib/utils";
-import { CheckCircleIcon, CircleNotchIcon } from "@databuddy/ui/icons";
-
-export type ToolStepProps = ComponentProps<"div"> & {
- label: ReactNode;
- status?: "complete" | "active";
-};
-
-export const ToolStep = memo(
- ({ className, label, status = "complete", ...props }: ToolStepProps) => (
-
- {status === "complete" ? (
-
- ) : (
-
- )}
- {label}
-
- )
-);
-
-ToolStep.displayName = "ToolStep";
diff --git a/apps/dashboard/components/ai-elements/code-block.tsx b/apps/dashboard/components/ai-elements/code-block.tsx
index ddd6a50e8c..7aa4bb69e8 100644
--- a/apps/dashboard/components/ai-elements/code-block.tsx
+++ b/apps/dashboard/components/ai-elements/code-block.tsx
@@ -49,7 +49,7 @@ const lineNumberTransformer: ShikiTransformer = {
},
};
-export async function highlightCode(
+async function highlightCode(
code: string,
language: BundledLanguage,
showLineNumbers = false
diff --git a/apps/dashboard/components/ai-elements/connection.tsx b/apps/dashboard/components/ai-elements/connection.tsx
deleted file mode 100644
index 99a58bc512..0000000000
--- a/apps/dashboard/components/ai-elements/connection.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import type { ConnectionLineComponent } from "@xyflow/react";
-
-const HALF = 0.5;
-
-export const Connection: ConnectionLineComponent = ({
- fromX,
- fromY,
- toX,
- toY,
-}) => (
-
-
-
-
-);
diff --git a/apps/dashboard/components/ai-elements/context.tsx b/apps/dashboard/components/ai-elements/context.tsx
deleted file mode 100644
index eeaf3d7c30..0000000000
--- a/apps/dashboard/components/ai-elements/context.tsx
+++ /dev/null
@@ -1,422 +0,0 @@
-"use client";
-
-import {
- HoverCard,
- HoverCardContent,
- HoverCardTrigger,
-} from "@/components/ui/hover-card";
-import { cn } from "@/lib/utils";
-import type { LanguageModelUsage } from "ai";
-import { type ComponentProps, createContext, useContext } from "react";
-import type { SourceModel } from "tokenlens";
-import { computeTokenCostsForModel } from "tokenlens/helpers";
-import { vercelModels } from "tokenlens/providers/vercel";
-import { Button, Progress } from "@databuddy/ui";
-
-const lookupModel = (modelId: string): SourceModel | undefined => {
- const model =
- vercelModels.models[modelId as keyof typeof vercelModels.models];
- return model
- ? ({ canonical_id: model.id, ...model } satisfies SourceModel)
- : undefined;
-};
-
-const PERCENT_MAX = 100;
-const ICON_RADIUS = 10;
-const ICON_VIEWBOX = 24;
-const ICON_CENTER = 12;
-const ICON_STROKE_WIDTH = 2;
-
-type ModelId = string;
-
-interface ContextSchema {
- maxTokens: number;
- modelId?: ModelId;
- usage?: LanguageModelUsage;
- usedTokens: number;
-}
-
-const ContextContext = createContext(null);
-
-const useContextValue = () => {
- const context = useContext(ContextContext);
-
- if (!context) {
- throw new Error("Context components must be used within Context");
- }
-
- return context;
-};
-
-export type ContextProps = ComponentProps & ContextSchema;
-
-export const Context = ({
- usedTokens,
- maxTokens,
- usage,
- modelId,
- ...props
-}: ContextProps) => (
-
-
-
-);
-
-const ContextIcon = () => {
- const { usedTokens, maxTokens } = useContextValue();
- const circumference = 2 * Math.PI * ICON_RADIUS;
- const usedPercent = usedTokens / maxTokens;
- const dashOffset = circumference * (1 - usedPercent);
-
- return (
-
-
-
-
- );
-};
-
-export type ContextTriggerProps = ComponentProps;
-
-export const ContextTrigger = ({ children, ...props }: ContextTriggerProps) => {
- const { usedTokens, maxTokens } = useContextValue();
- const usedPercent = usedTokens / maxTokens;
- const renderedPercent = new Intl.NumberFormat("en-US", {
- style: "percent",
- maximumFractionDigits: 1,
- }).format(usedPercent);
-
- return (
-
- {children ?? (
-
-
- {renderedPercent}
-
-
-
- )}
-
- );
-};
-
-export type ContextContentProps = ComponentProps;
-
-export const ContextContent = ({
- className,
- ...props
-}: ContextContentProps) => (
-
-);
-
-export type ContextContentHeaderProps = ComponentProps<"div">;
-
-export const ContextContentHeader = ({
- children,
- className,
- ...props
-}: ContextContentHeaderProps) => {
- const { usedTokens, maxTokens } = useContextValue();
- const usedPercent = usedTokens / maxTokens;
- const displayPct = new Intl.NumberFormat("en-US", {
- style: "percent",
- maximumFractionDigits: 1,
- }).format(usedPercent);
- const used = new Intl.NumberFormat("en-US", {
- notation: "compact",
- }).format(usedTokens);
- const total = new Intl.NumberFormat("en-US", {
- notation: "compact",
- }).format(maxTokens);
-
- return (
-
- {children ?? (
- <>
-
-
{displayPct}
-
- {used} / {total}
-
-
-
- >
- )}
-
- );
-};
-
-export type ContextContentBodyProps = ComponentProps<"div">;
-
-export const ContextContentBody = ({
- children,
- className,
- ...props
-}: ContextContentBodyProps) => (
-
- {children}
-
-);
-
-export type ContextContentFooterProps = ComponentProps<"div">;
-
-export const ContextContentFooter = ({
- children,
- className,
- ...props
-}: ContextContentFooterProps) => {
- const { modelId, usage } = useContextValue();
- const model = modelId ? lookupModel(modelId) : undefined;
- const costUSD = model
- ? computeTokenCostsForModel({
- model,
- usage: {
- input_tokens: usage?.inputTokens ?? 0,
- output_tokens: usage?.outputTokens ?? 0,
- },
- }).totalTokenCostUSD
- : undefined;
- const totalCost = new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: "USD",
- }).format(costUSD ?? 0);
-
- return (
-
- {children ?? (
- <>
- Total cost
- {totalCost}
- >
- )}
-
- );
-};
-
-export type ContextInputUsageProps = ComponentProps<"div">;
-
-export const ContextInputUsage = ({
- className,
- children,
- ...props
-}: ContextInputUsageProps) => {
- const { usage, modelId } = useContextValue();
- const inputTokens = usage?.inputTokens ?? 0;
-
- if (children) {
- return children;
- }
-
- if (!inputTokens) {
- return null;
- }
-
- const inputModel = modelId ? lookupModel(modelId) : undefined;
- const inputCost = inputModel
- ? computeTokenCostsForModel({
- model: inputModel,
- usage: { input_tokens: inputTokens, output_tokens: 0 },
- }).totalTokenCostUSD
- : undefined;
- const inputCostText = new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: "USD",
- }).format(inputCost ?? 0);
-
- return (
-
- Input
-
-
- );
-};
-
-export type ContextOutputUsageProps = ComponentProps<"div">;
-
-export const ContextOutputUsage = ({
- className,
- children,
- ...props
-}: ContextOutputUsageProps) => {
- const { usage, modelId } = useContextValue();
- const outputTokens = usage?.outputTokens ?? 0;
-
- if (children) {
- return children;
- }
-
- if (!outputTokens) {
- return null;
- }
-
- const outputModel = modelId ? lookupModel(modelId) : undefined;
- const outputCost = outputModel
- ? computeTokenCostsForModel({
- model: outputModel,
- usage: { input_tokens: 0, output_tokens: outputTokens },
- }).totalTokenCostUSD
- : undefined;
- const outputCostText = new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: "USD",
- }).format(outputCost ?? 0);
-
- return (
-
- Output
-
-
- );
-};
-
-export type ContextReasoningUsageProps = ComponentProps<"div">;
-
-export const ContextReasoningUsage = ({
- className,
- children,
- ...props
-}: ContextReasoningUsageProps) => {
- const { usage, modelId } = useContextValue();
- const reasoningTokens = usage?.reasoningTokens ?? 0;
-
- if (children) {
- return children;
- }
-
- if (!reasoningTokens) {
- return null;
- }
-
- const reasoningModel = modelId ? lookupModel(modelId) : undefined;
- const reasoningCost = reasoningModel
- ? computeTokenCostsForModel({
- model: reasoningModel,
- usage: { reasoning_tokens: reasoningTokens },
- }).totalTokenCostUSD
- : undefined;
- const reasoningCostText = new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: "USD",
- }).format(reasoningCost ?? 0);
-
- return (
-
- Reasoning
-
-
- );
-};
-
-export type ContextCacheUsageProps = ComponentProps<"div">;
-
-export const ContextCacheUsage = ({
- className,
- children,
- ...props
-}: ContextCacheUsageProps) => {
- const { usage, modelId } = useContextValue();
- const cacheTokens = usage?.cachedInputTokens ?? 0;
-
- if (children) {
- return children;
- }
-
- if (!cacheTokens) {
- return null;
- }
-
- const cacheModel = modelId ? lookupModel(modelId) : undefined;
- const cacheCost = cacheModel
- ? computeTokenCostsForModel({
- model: cacheModel,
- usage: { cache_read_tokens: cacheTokens },
- }).totalTokenCostUSD
- : undefined;
- const cacheCostText = new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: "USD",
- }).format(cacheCost ?? 0);
-
- return (
-
- Cache
-
-
- );
-};
-
-const TokensWithCost = ({
- tokens,
- costText,
-}: {
- tokens?: number;
- costText?: string;
-}) => (
-
- {tokens === undefined
- ? "—"
- : new Intl.NumberFormat("en-US", {
- notation: "compact",
- }).format(tokens)}
- {costText ? (
- • {costText}
- ) : null}
-
-);
diff --git a/apps/dashboard/components/ai-elements/controls.tsx b/apps/dashboard/components/ai-elements/controls.tsx
deleted file mode 100644
index dec68eb229..0000000000
--- a/apps/dashboard/components/ai-elements/controls.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-"use client";
-
-import { Controls as ControlsPrimitive } from "@xyflow/react";
-import type { ComponentProps } from "react";
-import { cn } from "@/lib/utils";
-
-export type ControlsProps = ComponentProps;
-
-export const Controls = ({ className, ...props }: ControlsProps) => (
- button]:rounded-md [&>button]:border-none! [&>button]:bg-transparent! [&>button]:hover:bg-secondary!",
- className
- )}
- {...props}
- />
-);
diff --git a/apps/dashboard/components/ai-elements/conversation.tsx b/apps/dashboard/components/ai-elements/conversation.tsx
index fc606ffa50..3c9db12250 100644
--- a/apps/dashboard/components/ai-elements/conversation.tsx
+++ b/apps/dashboard/components/ai-elements/conversation.tsx
@@ -33,41 +33,6 @@ export const ConversationContent = ({
/>
);
-export type ConversationEmptyStateProps = ComponentProps<"div"> & {
- title?: string;
- description?: string;
- icon?: React.ReactNode;
-};
-
-export const ConversationEmptyState = ({
- className,
- title = "No messages yet",
- description = "Start a conversation to see messages here",
- icon,
- children,
- ...props
-}: ConversationEmptyStateProps) => (
-
- {children ?? (
- <>
- {icon &&
{icon}
}
-
-
{title}
- {description && (
-
{description}
- )}
-
- >
- )}
-
-);
-
export type ConversationScrollButtonProps = ComponentProps;
export const ConversationScrollButton = ({
diff --git a/apps/dashboard/components/ai-elements/edge.tsx b/apps/dashboard/components/ai-elements/edge.tsx
deleted file mode 100644
index 15fa15108e..0000000000
--- a/apps/dashboard/components/ai-elements/edge.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-import {
- BaseEdge,
- type EdgeProps,
- getBezierPath,
- getSimpleBezierPath,
- type InternalNode,
- type Node,
- Position,
- useInternalNode,
-} from "@xyflow/react";
-
-const Temporary = ({
- id,
- sourceX,
- sourceY,
- targetX,
- targetY,
- sourcePosition,
- targetPosition,
-}: EdgeProps) => {
- const [edgePath] = getSimpleBezierPath({
- sourceX,
- sourceY,
- sourcePosition,
- targetX,
- targetY,
- targetPosition,
- });
-
- return (
-
- );
-};
-
-const getHandleCoordsByPosition = (
- node: InternalNode,
- handlePosition: Position
-) => {
- // Choose the handle type based on position - Left is for target, Right is for source
- const handleType = handlePosition === Position.Left ? "target" : "source";
-
- const handle = node.internals.handleBounds?.[handleType]?.find(
- (h) => h.position === handlePosition
- );
-
- if (!handle) {
- return [0, 0] as const;
- }
-
- let offsetX = handle.width / 2;
- let offsetY = handle.height / 2;
-
- // this is a tiny detail to make the markerEnd of an edge visible.
- // The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
- // when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
- switch (handlePosition) {
- case Position.Left:
- offsetX = 0;
- break;
- case Position.Right:
- offsetX = handle.width;
- break;
- case Position.Top:
- offsetY = 0;
- break;
- case Position.Bottom:
- offsetY = handle.height;
- break;
- default:
- throw new Error(`Invalid handle position: ${handlePosition}`);
- }
-
- const x = node.internals.positionAbsolute.x + handle.x + offsetX;
- const y = node.internals.positionAbsolute.y + handle.y + offsetY;
-
- return [x, y] as const;
-};
-
-const getEdgeParams = (
- source: InternalNode,
- target: InternalNode
-) => {
- const sourcePos = Position.Right;
- const [sx, sy] = getHandleCoordsByPosition(source, sourcePos);
- const targetPos = Position.Left;
- const [tx, ty] = getHandleCoordsByPosition(target, targetPos);
-
- return {
- sx,
- sy,
- tx,
- ty,
- sourcePos,
- targetPos,
- };
-};
-
-const Animated = ({ id, source, target, markerEnd, style }: EdgeProps) => {
- const sourceNode = useInternalNode(source);
- const targetNode = useInternalNode(target);
-
- if (!(sourceNode && targetNode)) {
- return null;
- }
-
- const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
- sourceNode,
- targetNode
- );
-
- const [edgePath] = getBezierPath({
- sourceX: sx,
- sourceY: sy,
- sourcePosition: sourcePos,
- targetX: tx,
- targetY: ty,
- targetPosition: targetPos,
- });
-
- return (
- <>
-
-
-
-
- >
- );
-};
-
-export const Edge = {
- Temporary,
- Animated,
-};
diff --git a/apps/dashboard/components/ai-elements/image.tsx b/apps/dashboard/components/ai-elements/image.tsx
deleted file mode 100644
index 6c5850c791..0000000000
--- a/apps/dashboard/components/ai-elements/image.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { Experimental_GeneratedImage } from "ai";
-import { cn } from "@/lib/utils";
-
-export type ImageProps = Experimental_GeneratedImage & {
- alt?: string;
- className?: string;
- height?: number;
- width?: number;
-};
-
-export const Image = ({
- base64,
- mediaType,
- uint8Array,
- height = 256,
- width = 256,
- ...props
-}: ImageProps) => (
-
-);
diff --git a/apps/dashboard/components/ai-elements/loader.tsx b/apps/dashboard/components/ai-elements/loader.tsx
deleted file mode 100644
index 8f872b2d57..0000000000
--- a/apps/dashboard/components/ai-elements/loader.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import type { HTMLAttributes } from "react";
-import { cn } from "@/lib/utils";
-
-interface LoaderIconProps {
- size?: number;
-}
-
-const LoaderIcon = ({ size = 16 }: LoaderIconProps) => (
-
- Loader
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
-
-export type LoaderProps = HTMLAttributes & {
- size?: number;
-};
-
-export const Loader = ({ className, size = 16, ...props }: LoaderProps) => (
-
-
-
-);
diff --git a/apps/dashboard/components/ai-elements/message.tsx b/apps/dashboard/components/ai-elements/message.tsx
index 76caca0fc3..3d3a658e28 100644
--- a/apps/dashboard/components/ai-elements/message.tsx
+++ b/apps/dashboard/components/ai-elements/message.tsx
@@ -1,8 +1,8 @@
"use client";
-import type { FileUIPart, UIMessage } from "ai";
-import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
-import { createContext, memo, useContext, useEffect, useState } from "react";
+import type { UIMessage } from "ai";
+import type { ComponentProps, HTMLAttributes } from "react";
+import { memo } from "react";
import {
Streamdown,
TableCopyDropdown,
@@ -15,15 +15,7 @@ import {
MdTh,
MdThead,
} from "@/components/ai-elements/markdown-table";
-import { ButtonGroup, ButtonGroupText } from "@/components/ui/button-group";
import { cn } from "@/lib/utils";
-import {
- CaretLeftIcon,
- CaretRightIcon,
- PaperclipIcon,
- XMarkIcon as XIcon,
-} from "@databuddy/ui/icons";
-import { Button, Tooltip } from "@databuddy/ui";
export type MessageProps = HTMLAttributes & {
from: UIMessage["role"];
@@ -59,244 +51,6 @@ export const MessageContent = ({
{children}
);
-
-export type MessageActionsProps = ComponentProps<"div">;
-
-export const MessageActions = ({
- className,
- children,
- ...props
-}: MessageActionsProps) => (
-
- {children}
-
-);
-
-export type MessageActionProps = ComponentProps & {
- tooltip?: string;
- label?: string;
-};
-
-export const MessageAction = ({
- tooltip,
- children,
- label,
- variant = "ghost",
- size = "icon",
- ...props
-}: MessageActionProps) => {
- const button = (
-
- {children}
- {label || tooltip}
-
- );
-
- if (tooltip) {
- return {tooltip}
}>{button};
- }
-
- return button;
-};
-
-interface MessageBranchContextType {
- branches: ReactElement[];
- currentBranch: number;
- goToNext: () => void;
- goToPrevious: () => void;
- setBranches: (branches: ReactElement[]) => void;
- totalBranches: number;
-}
-
-const MessageBranchContext = createContext(
- null
-);
-
-const useMessageBranch = () => {
- const context = useContext(MessageBranchContext);
-
- if (!context) {
- throw new Error(
- "MessageBranch components must be used within MessageBranch"
- );
- }
-
- return context;
-};
-
-export type MessageBranchProps = HTMLAttributes & {
- defaultBranch?: number;
- onBranchChange?: (branchIndex: number) => void;
-};
-
-export const MessageBranch = ({
- defaultBranch = 0,
- onBranchChange,
- className,
- ...props
-}: MessageBranchProps) => {
- const [currentBranch, setCurrentBranch] = useState(defaultBranch);
- const [branches, setBranches] = useState([]);
-
- const handleBranchChange = (newBranch: number) => {
- setCurrentBranch(newBranch);
- onBranchChange?.(newBranch);
- };
-
- const goToPrevious = () => {
- const newBranch =
- currentBranch > 0 ? currentBranch - 1 : branches.length - 1;
- handleBranchChange(newBranch);
- };
-
- const goToNext = () => {
- const newBranch =
- currentBranch < branches.length - 1 ? currentBranch + 1 : 0;
- handleBranchChange(newBranch);
- };
-
- const contextValue: MessageBranchContextType = {
- currentBranch,
- totalBranches: branches.length,
- goToPrevious,
- goToNext,
- branches,
- setBranches,
- };
-
- return (
-
- div]:pb-0", className)}
- {...props}
- />
-
- );
-};
-
-export type MessageBranchContentProps = HTMLAttributes
;
-
-export const MessageBranchContent = ({
- children,
- ...props
-}: MessageBranchContentProps) => {
- const { currentBranch, setBranches, branches } = useMessageBranch();
- const childrenArray = Array.isArray(children) ? children : [children];
-
- // Use useEffect to update branches when they change
- useEffect(() => {
- if (branches.length !== childrenArray.length) {
- setBranches(childrenArray);
- }
- }, [childrenArray, branches, setBranches]);
-
- return childrenArray.map((branch, index) => (
- div]:pb-0",
- index === currentBranch ? "block" : "hidden"
- )}
- key={branch.key}
- {...props}
- >
- {branch}
-
- ));
-};
-
-export type MessageBranchSelectorProps = HTMLAttributes & {
- from: UIMessage["role"];
-};
-
-export const MessageBranchSelector = ({
- className,
- from,
- ...props
-}: MessageBranchSelectorProps) => {
- const { totalBranches } = useMessageBranch();
-
- // Don't render if there's only one branch
- if (totalBranches <= 1) {
- return null;
- }
-
- return (
-
- );
-};
-
-export type MessageBranchPreviousProps = ComponentProps;
-
-export const MessageBranchPrevious = ({
- children,
- ...props
-}: MessageBranchPreviousProps) => {
- const { goToPrevious, totalBranches } = useMessageBranch();
-
- return (
-
- {children ?? }
-
- );
-};
-
-export type MessageBranchNextProps = ComponentProps;
-
-export const MessageBranchNext = ({
- children,
- className,
- ...props
-}: MessageBranchNextProps) => {
- const { goToNext, totalBranches } = useMessageBranch();
-
- return (
-
- {children ?? }
-
- );
-};
-
-export type MessageBranchPageProps = HTMLAttributes;
-
-export const MessageBranchPage = ({
- className,
- ...props
-}: MessageBranchPageProps) => {
- const { currentBranch, totalBranches } = useMessageBranch();
-
- return (
-
- {currentBranch + 1} of {totalBranches}
-
- );
-};
-
export type MessageResponseProps = ComponentProps;
const TABLE_COMPONENTS = {
@@ -333,124 +87,3 @@ export const MessageResponse = memo(
);
MessageResponse.displayName = "MessageResponse";
-
-export type MessageAttachmentProps = HTMLAttributes & {
- data: FileUIPart;
- className?: string;
- onRemove?: () => void;
-};
-
-export function MessageAttachment({
- data,
- className,
- onRemove,
- ...props
-}: MessageAttachmentProps) {
- const filename = data.filename || "";
- const mediaType =
- data.mediaType?.startsWith("image/") && data.url ? "image" : "file";
- const isImage = mediaType === "image";
- const attachmentLabel = filename || (isImage ? "Image" : "Attachment");
-
- return (
-
- {isImage ? (
- <>
-
- {onRemove && (
-
{
- e.stopPropagation();
- onRemove();
- }}
- type="button"
- variant="ghost"
- >
-
- Remove
-
- )}
- >
- ) : (
- <>
-
{attachmentLabel}}>
-
-
- {onRemove && (
-
{
- e.stopPropagation();
- onRemove();
- }}
- type="button"
- variant="ghost"
- >
-
- Remove
-
- )}
- >
- )}
-
- );
-}
-
-export type MessageAttachmentsProps = ComponentProps<"div">;
-
-export function MessageAttachments({
- children,
- className,
- ...props
-}: MessageAttachmentsProps) {
- if (!children) {
- return null;
- }
-
- return (
-
- {children}
-
- );
-}
-
-export type MessageToolbarProps = ComponentProps<"div">;
-
-export const MessageToolbar = ({
- className,
- children,
- ...props
-}: MessageToolbarProps) => (
-
- {children}
-
-);
diff --git a/apps/dashboard/components/ai-elements/model-selector.tsx b/apps/dashboard/components/ai-elements/model-selector.tsx
deleted file mode 100644
index 9d38947318..0000000000
--- a/apps/dashboard/components/ai-elements/model-selector.tsx
+++ /dev/null
@@ -1,204 +0,0 @@
-import type { ComponentProps, ReactNode } from "react";
-import {
- Command,
- CommandDialog,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- CommandSeparator,
- CommandShortcut,
-} from "@/components/ui/command";
-import { cn } from "@/lib/utils";
-import { Dialog } from "@databuddy/ui/client";
-
-export type ModelSelectorProps = ComponentProps;
-
-export const ModelSelector = (props: ModelSelectorProps) => (
-
-);
-
-export type ModelSelectorTriggerProps = ComponentProps;
-
-export const ModelSelectorTrigger = (props: ModelSelectorTriggerProps) => (
-
-);
-
-export type ModelSelectorContentProps = ComponentProps<
- typeof Dialog.Content
-> & {
- title?: ReactNode;
-};
-
-export const ModelSelectorContent = ({
- className,
- children,
- title = "Model Selector",
- ...props
-}: ModelSelectorContentProps) => (
-
- {title}
-
-
- {children}
-
-
-
-);
-
-export type ModelSelectorDialogProps = ComponentProps;
-
-export const ModelSelectorDialog = (props: ModelSelectorDialogProps) => (
-
-);
-
-export type ModelSelectorInputProps = ComponentProps;
-
-export const ModelSelectorInput = ({
- className,
- ...props
-}: ModelSelectorInputProps) => (
-
-);
-
-export type ModelSelectorListProps = ComponentProps;
-
-export const ModelSelectorList = (props: ModelSelectorListProps) => (
-
-);
-
-export type ModelSelectorEmptyProps = ComponentProps;
-
-export const ModelSelectorEmpty = (props: ModelSelectorEmptyProps) => (
-
-);
-
-export type ModelSelectorGroupProps = ComponentProps;
-
-export const ModelSelectorGroup = (props: ModelSelectorGroupProps) => (
-
-);
-
-export type ModelSelectorItemProps = ComponentProps;
-
-export const ModelSelectorItem = (props: ModelSelectorItemProps) => (
-
-);
-
-export type ModelSelectorShortcutProps = ComponentProps;
-
-export const ModelSelectorShortcut = (props: ModelSelectorShortcutProps) => (
-
-);
-
-export type ModelSelectorSeparatorProps = ComponentProps<
- typeof CommandSeparator
->;
-
-export const ModelSelectorSeparator = (props: ModelSelectorSeparatorProps) => (
-
-);
-
-export type ModelSelectorLogoProps = Omit<
- ComponentProps<"img">,
- "src" | "alt"
-> & {
- provider:
- | "moonshotai-cn"
- | "lucidquery"
- | "moonshotai"
- | "zai-coding-plan"
- | "alibaba"
- | "xai"
- | "vultr"
- | "nvidia"
- | "upstage"
- | "groq"
- | "github-copilot"
- | "mistral"
- | "vercel"
- | "nebius"
- | "deepseek"
- | "alibaba-cn"
- | "google-vertex-anthropic"
- | "venice"
- | "chutes"
- | "cortecs"
- | "github-models"
- | "togetherai"
- | "azure"
- | "baseten"
- | "huggingface"
- | "opencode"
- | "fastrouter"
- | "google"
- | "google-vertex"
- | "cloudflare-workers-ai"
- | "inception"
- | "wandb"
- | "openai"
- | "zhipuai-coding-plan"
- | "perplexity"
- | "openrouter"
- | "zenmux"
- | "v0"
- | "iflowcn"
- | "synthetic"
- | "deepinfra"
- | "zhipuai"
- | "submodel"
- | "zai"
- | "inference"
- | "requesty"
- | "morph"
- | "lmstudio"
- | "anthropic"
- | "aihubmix"
- | "fireworks-ai"
- | "modelscope"
- | "llama"
- | "scaleway"
- | "amazon-bedrock"
- | "cerebras"
- | (string & {});
-};
-
-export const ModelSelectorLogo = ({
- provider,
- className,
- ...props
-}: ModelSelectorLogoProps) => (
-
-);
-
-export type ModelSelectorLogoGroupProps = ComponentProps<"div">;
-
-export const ModelSelectorLogoGroup = ({
- className,
- ...props
-}: ModelSelectorLogoGroupProps) => (
- img]:rounded-full [&>img]:bg-background [&>img]:p-px [&>img]:ring-1 dark:[&>img]:bg-foreground",
- className
- )}
- {...props}
- />
-);
-
-export type ModelSelectorNameProps = ComponentProps<"span">;
-
-export const ModelSelectorName = ({
- className,
- ...props
-}: ModelSelectorNameProps) => (
-
-);
diff --git a/apps/dashboard/components/ai-elements/node.tsx b/apps/dashboard/components/ai-elements/node.tsx
deleted file mode 100644
index f1c467e3a9..0000000000
--- a/apps/dashboard/components/ai-elements/node.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Handle, Position } from "@xyflow/react";
-import type { ComponentProps } from "react";
-import { cn } from "@/lib/utils";
-import { Card } from "@databuddy/ui";
-
-export type NodeProps = ComponentProps
& {
- handles: {
- target: boolean;
- source: boolean;
- };
-};
-
-export const Node = ({ handles, className, ...props }: NodeProps) => (
-
- {handles.target && }
- {handles.source && }
- {props.children}
-
-);
-
-export type NodeHeaderProps = ComponentProps;
-
-export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => (
-
-);
-
-export type NodeTitleProps = ComponentProps;
-
-export const NodeTitle = (props: NodeTitleProps) => ;
-
-export type NodeDescriptionProps = ComponentProps;
-
-export const NodeDescription = (props: NodeDescriptionProps) => (
-
-);
-
-export type NodeActionProps = ComponentProps;
-
-export const NodeAction = (props: NodeActionProps) => (
-
-);
-
-export type NodeContentProps = ComponentProps;
-
-export const NodeContent = ({ className, ...props }: NodeContentProps) => (
-
-);
-
-export type NodeFooterProps = ComponentProps;
-
-export const NodeFooter = ({ className, ...props }: NodeFooterProps) => (
-
-);
diff --git a/apps/dashboard/components/ai-elements/panel.tsx b/apps/dashboard/components/ai-elements/panel.tsx
deleted file mode 100644
index 5c769a5465..0000000000
--- a/apps/dashboard/components/ai-elements/panel.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Panel as PanelPrimitive } from "@xyflow/react";
-import type { ComponentProps } from "react";
-import { cn } from "@/lib/utils";
-
-type PanelProps = ComponentProps;
-
-export const Panel = ({ className, ...props }: PanelProps) => (
-
-);
diff --git a/apps/dashboard/components/ai-elements/reasoning.tsx b/apps/dashboard/components/ai-elements/reasoning.tsx
index fbd966a89a..d9eb8adab8 100644
--- a/apps/dashboard/components/ai-elements/reasoning.tsx
+++ b/apps/dashboard/components/ai-elements/reasoning.tsx
@@ -27,7 +27,7 @@ interface ReasoningContextValue {
const ReasoningContext = createContext(null);
-export const useReasoning = () => {
+const useReasoning = () => {
const context = useContext(ReasoningContext);
if (!context) {
throw new Error("Reasoning components must be used within Reasoning");
diff --git a/apps/dashboard/components/ai-elements/suggestion.tsx b/apps/dashboard/components/ai-elements/suggestion.tsx
deleted file mode 100644
index e8b82fc9ad..0000000000
--- a/apps/dashboard/components/ai-elements/suggestion.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-"use client";
-
-import type { ComponentProps } from "react";
-import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
-import { cn } from "@/lib/utils";
-import { Button } from "@databuddy/ui";
-
-export type SuggestionsProps = ComponentProps;
-
-export const Suggestions = ({
- className,
- children,
- ...props
-}: SuggestionsProps) => (
-
-
- {children}
-
-
-
-);
-
-export type SuggestionProps = Omit, "onClick"> & {
- suggestion: string;
- onClick?: (suggestion: string) => void;
-};
-
-export const Suggestion = ({
- suggestion,
- onClick,
- className,
- variant = "secondary",
- size = "sm",
- children,
- ...props
-}: SuggestionProps) => {
- const handleClick = () => {
- onClick?.(suggestion);
- };
-
- return (
-
- {children || suggestion}
-
- );
-};
diff --git a/apps/dashboard/components/ai-elements/toolbar.tsx b/apps/dashboard/components/ai-elements/toolbar.tsx
deleted file mode 100644
index 3b4fbf0aea..0000000000
--- a/apps/dashboard/components/ai-elements/toolbar.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { NodeToolbar, Position } from "@xyflow/react";
-import type { ComponentProps } from "react";
-import { cn } from "@/lib/utils";
-
-type ToolbarProps = ComponentProps;
-
-export const Toolbar = ({ className, ...props }: ToolbarProps) => (
-
-);
diff --git a/apps/dashboard/components/analytics/index.tsx b/apps/dashboard/components/analytics/index.tsx
index 7921cc02f1..f975e4b9d1 100644
--- a/apps/dashboard/components/analytics/index.tsx
+++ b/apps/dashboard/components/analytics/index.tsx
@@ -2,8 +2,6 @@
export { DeviceTypeCell } from "./device-type-cell";
export { EventLimitIndicator } from "./event-limit-indicator";
-export { FaviconImage } from "./favicon-image";
export { LiveUserIndicator } from "./live-user-indicator";
-export { MapComponent } from "./map-component";
export { StatCard } from "./stat-card";
export { UnauthorizedAccessError } from "./unauthorized-access-error";
diff --git a/apps/dashboard/components/analytics/live-user-indicator.tsx b/apps/dashboard/components/analytics/live-user-indicator.tsx
index 18bc97f496..d6b782ac53 100644
--- a/apps/dashboard/components/analytics/live-user-indicator.tsx
+++ b/apps/dashboard/components/analytics/live-user-indicator.tsx
@@ -30,6 +30,12 @@ export function LiveUserIndicator({ websiteId }: LiveUserIndicatorProps) {
}
prevCountRef.current = count;
+
+ return () => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+ };
}, [count]);
return (
diff --git a/apps/dashboard/components/analytics/map-component.tsx b/apps/dashboard/components/analytics/map-component.tsx
index 77a2079e6a..cd4e548aa3 100644
--- a/apps/dashboard/components/analytics/map-component.tsx
+++ b/apps/dashboard/components/analytics/map-component.tsx
@@ -6,7 +6,11 @@ import type { CountryData, LocationData } from "@/types/website";
import { GlobeIcon } from "@databuddy/ui/icons";
import { scalePow } from "d3-scale";
import type { Feature, GeoJsonObject } from "geojson";
-import type { Layer, Map as LeafletMap } from "leaflet";
+import type {
+ Layer,
+ LeafletEventHandlerFnMap,
+ Map as LeafletMap,
+} from "leaflet";
import "leaflet/dist/leaflet.css";
import { useTheme } from "next-themes";
import dynamic from "next/dynamic";
@@ -154,7 +158,7 @@ export function MapComponent({
const handleEachFeature = useCallback(
(feature: Feature, layer: Layer) => {
- layer.on({
+ const handlers: LeafletEventHandlerFnMap = {
mouseover: () => {
const code = feature.properties?.ISO_A2;
setHoveredId(code);
@@ -186,7 +190,9 @@ export function MapComponent({
);
}
},
- });
+ };
+ layer.on(handlers);
+ layer.once("remove", () => layer.off(handlers));
},
[countryData?.data]
);
diff --git a/apps/dashboard/components/analytics/res-gauge-card.tsx b/apps/dashboard/components/analytics/res-gauge-card.tsx
index b629800d87..caf6c7df5c 100644
--- a/apps/dashboard/components/analytics/res-gauge-card.tsx
+++ b/apps/dashboard/components/analytics/res-gauge-card.tsx
@@ -30,19 +30,12 @@ interface PercentileOption {
}
interface RESGaugeCardProps {
- /** Additional class names */
className?: string;
- /** Loading state */
isLoading?: boolean;
- /** Current period metrics with p75 values */
metrics: MetricInput[];
- /** Callback when percentile changes */
onPercentileChangeAction?: (value: string) => void;
- /** Percentile options for the selector */
percentileOptions?: PercentileOption[];
- /** Previous period metrics for trend comparison */
previousMetrics?: MetricInput[];
- /** Currently selected percentile */
selectedPercentile?: string;
}
diff --git a/apps/dashboard/components/analytics/stat-card.tsx b/apps/dashboard/components/analytics/stat-card.tsx
index 5642a6c61c..fac7eeb9fb 100644
--- a/apps/dashboard/components/analytics/stat-card.tsx
+++ b/apps/dashboard/components/analytics/stat-card.tsx
@@ -30,7 +30,7 @@ interface Trend {
previousPeriod: { start: string; end: string };
}
-export type StatCardDisplayMode = "compact" | "chart" | "text";
+type StatCardDisplayMode = "compact" | "chart" | "text";
interface StatCardProps {
chartData?: MiniChartDataPoint[];
diff --git a/apps/dashboard/components/atomic/FormattedNumber.tsx b/apps/dashboard/components/atomic/FormattedNumber.tsx
deleted file mode 100644
index 26c167a458..0000000000
--- a/apps/dashboard/components/atomic/FormattedNumber.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-"use client";
-
-import type React from "react";
-import { formatNumber } from "@/lib/formatters";
-
-interface FormattedNumberProps {
- className?: string;
- id?: string;
- value: number;
-}
-
-export const FormattedNumber: React.FC = ({
- id,
- value,
- className,
-}) => (
-
- {formatNumber(value)}
-
-);
-
-export default FormattedNumber;
diff --git a/apps/dashboard/components/atomic/PageLinkCell.tsx b/apps/dashboard/components/atomic/PageLinkCell.tsx
deleted file mode 100644
index 4cc10acba5..0000000000
--- a/apps/dashboard/components/atomic/PageLinkCell.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-"use client";
-
-import type React from "react";
-import { formatDomainLink } from "@/app/(main)/websites/[id]/_components/utils/analytics-helpers";
-import { cn } from "@/lib/utils";
-import { ArrowSquareOutIcon, FileTextIcon } from "@databuddy/ui/icons";
-
-export interface PageLinkCellData {
- id?: string;
- path: string;
- websiteDomain?: string;
-}
-
-type PageLinkCellProps = PageLinkCellData & {
- className?: string;
- iconClassName?: string;
- textClassName?: string;
- maxLength?: number;
-};
-
-export const PageLinkCell: React.FC = ({
- id,
- path,
- websiteDomain,
- className,
- iconClassName = "size-4 text-muted-foreground",
- textClassName = "text-sm",
- maxLength = 35,
-}) => {
- if (!path) {
- return (
-
- (not set)
-
- );
- }
-
- const { href, display } = formatDomainLink(path, websiteDomain, maxLength);
- const isExternal = href.startsWith("http");
-
- return (
-
-
-
- {display}
-
- {isExternal && (
-
- )}
-
- );
-};
-
-export default PageLinkCell;
diff --git a/apps/dashboard/components/autumn/pricing-table.tsx b/apps/dashboard/components/autumn/pricing-table.tsx
index dade28a0c4..2beefbdf99 100644
--- a/apps/dashboard/components/autumn/pricing-table.tsx
+++ b/apps/dashboard/components/autumn/pricing-table.tsx
@@ -778,5 +778,3 @@ function StaticFeatureItem({ label }: { label: string }) {
);
}
-
-export { FeatureItem as PricingFeatureItem, PricingCard };
diff --git a/apps/dashboard/components/bits/Aurora.tsx b/apps/dashboard/components/bits/Aurora.tsx
deleted file mode 100644
index 428a502505..0000000000
--- a/apps/dashboard/components/bits/Aurora.tsx
+++ /dev/null
@@ -1,216 +0,0 @@
-import { Color, Mesh, Program, Renderer, Triangle } from "ogl";
-import { useEffect, useRef } from "react";
-
-export interface CommonProps {
- onReady?: () => void;
-}
-
-export interface TimeProps {
- speed?: number;
- time?: number;
-}
-
-export interface ControlProps {
- paused?: boolean;
-}
-
-export interface AuroraProps extends CommonProps, TimeProps, ControlProps {
- amplitude?: number;
- colorStops?: [string, string, string];
-}
-
-const VERT = `#version 300 es
-in vec2 uv;
-in vec2 position;
-
-out vec2 vUv;
-
-void main() {
- vUv = uv;
- gl_Position = vec4(position, 0.0, 1.0);
-}
-`;
-
-const FRAG = `#version 300 es
-precision highp float;
-
-uniform float uTime;
-uniform float uAmplitude;
-uniform vec3 uColorStops[3];
-
-in vec2 vUv;
-out vec4 fragColor;
-
-vec3 permute(vec3 x) {
- return mod(((x * 34.0) + 1.0) * x, 289.0);
-}
-
-float snoise(vec2 v){
- const vec4 C = vec4(
- 0.211324865405187, 0.366025403784439,
- -0.577350269189626, 0.024390243902439
- );
- vec2 i = floor(v + dot(v, C.yy));
- vec2 x0 = v - i + dot(i, C.xx);
- vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
- vec4 x12 = x0.xyxy + C.xxzz;
- x12.xy -= i1;
- i = mod(i, 289.0);
-
- vec3 p = permute(
- permute(i.y + vec3(0.0, i1.y, 1.0))
- + i.x + vec3(0.0, i1.x, 1.0)
- );
-
- vec3 m = max(
- 0.5 - vec3(
- dot(x0, x0),
- dot(x12.xy, x12.xy),
- dot(x12.zw, x12.zw)
- ),
- 0.0
- );
- m = m * m;
- m = m * m;
-
- vec3 x = 2.0 * fract(p * C.www) - 1.0;
- vec3 h = abs(x) - 0.5;
- vec3 ox = floor(x + 0.5);
- vec3 a0 = x - ox;
- m *= 1.79284291400159 - 0.85373472095314 * (a0*a0 + h*h);
-
- vec3 g;
- g.x = a0.x * x0.x + h.x * x0.y;
- g.yz = a0.yz * x12.xz + h.yz * x12.yw;
- return 130.0 * dot(m, g);
-}
-
-struct ColorStop {
- vec3 color;
- float position;
-};
-
-/**
- * Helper macro to blend between consecutive ColorStops
- * based on vUv.x
- */
-#define COLOR_RAMP(colors, factor, finalColor) { \
- int index = 0; \
- for (int i = 0; i < colors.length() - 1; i++) { \
- ColorStop currentColor = colors[i]; \
- bool isInBetween = currentColor.position <= factor; \
- index = int(mix(float(index), float(i), float(isInBetween))); \
- } \
- ColorStop currentColor = colors[index]; \
- ColorStop nextColor = colors[index + 1]; \
- float range = nextColor.position - currentColor.position; \
- float lerpFactor = (factor - currentColor.position) / range; \
- finalColor = mix(currentColor.color, nextColor.color, lerpFactor); \
-}
-
-void main() {
- // Build our three color stops from uniform array uColorStops
- ColorStop colors[3];
- colors[0] = ColorStop(uColorStops[0], 0.0);
- colors[1] = ColorStop(uColorStops[1], 0.5);
- colors[2] = ColorStop(uColorStops[2], 1.0);
-
- // Interpolate color along vUv.x
- vec3 rampColor;
- COLOR_RAMP(colors, vUv.x, rampColor);
-
- // Noise-based "height," scaled by amplitude
- float height = snoise(vec2(vUv.x * 2.0 + uTime * 0.1, uTime * 0.25))
- * 0.5
- * uAmplitude;
- height = exp(height);
- height = (vUv.y * 2.0 - height + 0.2);
-
- fragColor.rgb = 0.6 * height * rampColor;
- fragColor.a = 1.0;
-}
-`;
-
-export default function Aurora(props: AuroraProps) {
- const { colorStops = ["#00d8ff", "#7cff67", "#00d8ff"], amplitude = 1.0 } =
- props;
-
- const propsRef = useRef(props);
- propsRef.current = props;
-
- const ctnDom = useRef(null);
-
- useEffect(() => {
- const ctn = ctnDom.current;
- if (!ctn) {
- return;
- }
-
- const renderer = new Renderer();
- const gl = renderer.gl;
- gl.clearColor(1, 1, 1, 1);
-
- function resize() {
- if (!ctn) {
- return;
- }
- renderer.setSize(ctn.offsetWidth, ctn.offsetHeight);
- }
- window.addEventListener("resize", resize);
- resize();
-
- const geometry = new Triangle(gl);
- geometry.addAttribute("uv", {
- size: 2,
- data: new Float32Array([0, 0, 2, 0, 0, 2]),
- });
-
- const colorStopsArray = colorStops.map((hex) => {
- const c = new Color(hex);
- return [c.r, c.g, c.b] as [number, number, number];
- });
-
- const program = new Program(gl, {
- vertex: VERT,
- fragment: FRAG,
- uniforms: {
- uTime: { value: 0 },
- uAmplitude: { value: amplitude },
- uColorStops: { value: colorStopsArray },
- },
- });
-
- const mesh = new Mesh(gl, { geometry, program });
- ctn.appendChild(gl.canvas);
-
- let animateId = 0;
-
- const update = (t: number) => {
- animateId = requestAnimationFrame(update);
-
- const { time = t * 0.01, speed = 1.0 } = propsRef.current;
- program.uniforms.uTime.value = time * speed * 0.1;
-
- program.uniforms.uAmplitude.value = propsRef.current.amplitude ?? 1.0;
- const stops = propsRef.current.colorStops ?? colorStops;
- program.uniforms.uColorStops.value = stops.map((hex) => {
- const c = new Color(hex);
- return [c.r, c.g, c.b] as [number, number, number];
- });
-
- renderer.render({ scene: mesh });
- };
- animateId = requestAnimationFrame(update);
-
- return () => {
- cancelAnimationFrame(animateId);
- window.removeEventListener("resize", resize);
-
- ctn?.removeChild(gl.canvas);
-
- gl.getExtension("WEBGL_lose_context")?.loseContext();
- };
- }, [amplitude, colorStops]);
-
- return
;
-}
diff --git a/apps/dashboard/components/charts/gauge-chart.tsx b/apps/dashboard/components/charts/gauge-chart.tsx
index 8d44b7f710..5efc489781 100644
--- a/apps/dashboard/components/charts/gauge-chart.tsx
+++ b/apps/dashboard/components/charts/gauge-chart.tsx
@@ -7,23 +7,14 @@ import { cn } from "@/lib/utils";
type GaugeRating = "good" | "needs-improvement" | "poor";
interface GaugeChartProps {
- /** Format the center label value */
formatValue?: (value: number) => string;
- /** Maximum value for the gauge (100% fill) */
max: number;
- /** Rating determines the color */
rating: GaugeRating;
- /** Size of the chart in pixels */
size?: number;
- /** Starting angle in degrees (-90 = top, 0 = right, 90 = bottom) */
startAngle?: number;
- /** Sweep angle in degrees (360 = full circle) */
sweepAngle?: number;
- /** Number of tick marks */
tickCount?: number;
- /** Optional unit to display below the value */
unit?: string;
- /** Current value to display */
value: number;
}
diff --git a/apps/dashboard/components/charts/metrics-constants.ts b/apps/dashboard/components/charts/metrics-constants.ts
index c0a1a5d6a4..da6df4d41b 100644
--- a/apps/dashboard/components/charts/metrics-constants.ts
+++ b/apps/dashboard/components/charts/metrics-constants.ts
@@ -152,12 +152,11 @@ export interface ChartDataRow {
sessions?: number;
unique_visitors?: number;
visitors?: number;
- /** Stable category for Recharts X-axis; usually rawDate (YYYY-MM-DD or hourly key) */
xKey?: string;
[key: string]: unknown;
}
-export interface MetricConfig {
+interface MetricConfig {
category?: "analytics" | "performance" | "core_web_vitals";
color: string;
formatValue?: (value: number, row: ChartDataRow) => string;
@@ -168,7 +167,7 @@ export interface MetricConfig {
yAxisId: string;
}
-export const formatPerformanceTime = (value: number): string => {
+const formatPerformanceTime = (value: number): string => {
if (!value || value === 0) {
return "N/A";
}
@@ -181,7 +180,7 @@ export const formatPerformanceTime = (value: number): string => {
: `${seconds.toFixed(1)}s`;
};
-export const formatCLS = (value: number): string => {
+const formatCLS = (value: number): string => {
if (value === null || value === undefined || Number.isNaN(value)) {
return "N/A";
}
@@ -206,7 +205,7 @@ const createMetric = (
category,
});
-export const ANALYTICS_METRICS: MetricConfig[] = [
+const ANALYTICS_METRICS: MetricConfig[] = [
createMetric("pageviews", "Pageviews", "pageviews", EyeIcon, (value) =>
formatLocaleNumber(value)
),
@@ -235,7 +234,7 @@ export const ANALYTICS_METRICS: MetricConfig[] = [
),
];
-export const PERFORMANCE_METRICS: MetricConfig[] = [
+const PERFORMANCE_METRICS: MetricConfig[] = [
createMetric(
"avg_load_time",
"Avg Load Time",
@@ -254,7 +253,7 @@ export const PERFORMANCE_METRICS: MetricConfig[] = [
),
];
-export const CORE_WEB_VITALS_METRICS: MetricConfig[] = [
+const CORE_WEB_VITALS_METRICS: MetricConfig[] = [
createMetric(
"avg_fcp",
"FCP (Avg)",
@@ -337,7 +336,7 @@ export const CORE_WEB_VITALS_METRICS: MetricConfig[] = [
),
];
-export const ERROR_METRICS: MetricConfig[] = [
+const ERROR_METRICS: MetricConfig[] = [
createMetric(
"total_errors",
"Total Errors",
diff --git a/apps/dashboard/components/charts/pie-chart.tsx b/apps/dashboard/components/charts/pie-chart.tsx
index ab15026c4e..d77b7bc1f4 100644
--- a/apps/dashboard/components/charts/pie-chart.tsx
+++ b/apps/dashboard/components/charts/pie-chart.tsx
@@ -24,7 +24,7 @@ interface PieChartDataPoint {
value: number;
}
-export type PieChartVariant = "pie" | "donut";
+type PieChartVariant = "pie" | "donut";
interface MiniPieChartProps {
className?: string;
diff --git a/apps/dashboard/components/charts/range-selection-popup.tsx b/apps/dashboard/components/charts/range-selection-popup.tsx
index 45e72100d8..10a8e38143 100644
--- a/apps/dashboard/components/charts/range-selection-popup.tsx
+++ b/apps/dashboard/components/charts/range-selection-popup.tsx
@@ -12,7 +12,6 @@ interface RangeSelectionPopupProps {
onAddAnnotationAction: () => void;
onCloseAction: () => void;
onZoomAction: (dateRange: { startDate: Date; endDate: Date }) => void;
- /** When false, only “Zoom to range” is shown (e.g. annotations disabled on chart). Default: true. */
showAnnotationAction?: boolean;
}
diff --git a/apps/dashboard/components/charts/simple-metrics-chart.tsx b/apps/dashboard/components/charts/simple-metrics-chart.tsx
index b9f42a5369..360d589967 100644
--- a/apps/dashboard/components/charts/simple-metrics-chart.tsx
+++ b/apps/dashboard/components/charts/simple-metrics-chart.tsx
@@ -21,9 +21,7 @@ interface SimpleMetricsChartProps {
height?: number;
isLoading?: boolean;
metrics: MetricConfig[];
- /** When true, the last segment (incomplete period) uses a dashed stroke, matching the overview traffic trends chart. Applies to area and line, not bar. */
partialLastSegment?: boolean;
- /** Area (default), line, or grouped bar. */
seriesKind?: ChartSeriesKind;
title?: string;
}
diff --git a/apps/dashboard/components/dev-tools/dev-tools-drawer.tsx b/apps/dashboard/components/dev-tools/dev-tools-drawer.tsx
deleted file mode 100644
index 95473ddc93..0000000000
--- a/apps/dashboard/components/dev-tools/dev-tools-drawer.tsx
+++ /dev/null
@@ -1,1092 +0,0 @@
-"use client";
-
-import { publicConfig } from "@databuddy/env/public";
-import { useQueryClient } from "@tanstack/react-query";
-import { useTheme } from "next-themes";
-import { useCallback, useEffect, useState } from "react";
-import { toast } from "sonner";
-import type {
- ChartCurveType,
- ChartSeriesKind,
-} from "@/components/ui/composables/chart";
-import {
- CHART_LOCATION_LABELS,
- CHART_LOCATIONS,
- type ChartLocation,
- useAllChartPreferences,
-} from "@/hooks/use-chart-preferences";
-import { cn } from "@/lib/utils";
-import { Button } from "../ui/button";
-import {
- Drawer,
- DrawerClose,
- DrawerContent,
- DrawerDescription,
- DrawerHeader,
- DrawerTitle,
-} from "../ui/drawer";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "../ui/select";
-import { Separator } from "../ui/separator";
-import {
- BugIcon,
- CaretDownIcon,
- ChartBarIcon,
- ChartLineIcon,
- CheckCircleIcon,
- ClipboardIcon,
- CopyIcon,
- DatabaseIcon,
- DesktopIcon,
- FunnelIcon,
- GearIcon,
- InfoIcon,
- LightningIcon,
- MonitorIcon,
- MoonIcon,
- PresentationChartIcon,
- SpinnerIcon,
- SquaresFourIcon,
- StackIcon,
- SunIcon,
- TrashIcon,
- WarningCircleIcon,
- XMarkIcon as XIcon,
-} from "@databuddy/ui/icons";
-import type { NavIcon } from "@/components/layout/navigation/types";
-
-function InfoSection({
- title,
- children,
-}: {
- title: string;
- children: React.ReactNode;
-}) {
- return (
-
-
-
- {title}
-
-
{children}
-
- );
-}
-
-function ActionButton({
- icon: Icon,
- label,
- onClick,
- variant = "outline",
-}: {
- icon: NavIcon;
- label: string;
- onClick: () => void;
- variant?: "outline" | "destructive";
-}) {
- return (
-
-
- {label}
-
- );
-}
-
-function EnvironmentInfo() {
- const apiUrl = publicConfig.urls.api;
- const env = process.env.NODE_ENV || "development";
-
- return (
-
-
-
- API URL:
- {apiUrl}
-
-
- Environment:
- {env}
-
-
- Hostname:
- {window.location.hostname}
-
-
-
- );
-}
-
-function ToastPreview() {
- return (
-
-
- Trigger Sonner to preview dashboard styling.
-
-
-
toast.success("Success")}
- />
- toast.error("Error")}
- variant="destructive"
- />
- toast.warning("Warning")}
- />
- toast.info("Info")}
- />
- toast.loading("Loading")}
- />
- toast("Default")}
- />
-
-
- );
-}
-
-function ReactQueryCache() {
- const queryClient = useQueryClient();
- const [cacheStats, setCacheStats] = useState({
- queries: 0,
- mutations: 0,
- });
-
- const updateStats = useCallback(() => {
- const cache = queryClient.getQueryCache();
- const mutationCache = queryClient.getMutationCache();
- setCacheStats({
- queries: cache.getAll().length,
- mutations: mutationCache.getAll().length,
- });
- }, [queryClient]);
-
- useEffect(() => {
- updateStats();
- const interval = setInterval(updateStats, 1000);
- return () => clearInterval(interval);
- }, [updateStats]);
-
- const handleClearCache = () => {
- queryClient.clear();
- updateStats();
- toast.success("React Query cache cleared");
- };
-
- const handleInvalidateAll = () => {
- queryClient.invalidateQueries();
- toast.success("All queries invalidated");
- };
-
- return (
-
-
-
- Active Queries:
- {cacheStats.queries}
-
-
- Active Mutations:
- {cacheStats.mutations}
-
-
-
-
- );
-}
-
-function StorageManagement() {
- const [storageStats, setStorageStats] = useState({
- localStorage: 0,
- sessionStorage: 0,
- });
-
- const updateStats = useCallback(() => {
- let localStorageSize = 0;
- let sessionStorageSize = 0;
-
- for (const key in localStorage) {
- if (Object.hasOwn(localStorage, key)) {
- localStorageSize += localStorage[key].length + key.length;
- }
- }
-
- for (const key in sessionStorage) {
- if (Object.hasOwn(sessionStorage, key)) {
- sessionStorageSize += sessionStorage[key].length + key.length;
- }
- }
-
- setStorageStats({
- localStorage: localStorageSize,
- sessionStorage: sessionStorageSize,
- });
- }, []);
-
- useEffect(() => {
- updateStats();
- const interval = setInterval(updateStats, 2000);
- return () => clearInterval(interval);
- }, [updateStats]);
-
- const formatBytes = (bytes: number) => {
- if (bytes === 0) {
- return "0 B";
- }
- const k = 1024;
- const sizes = ["B", "KB", "MB"];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return `${(bytes / k ** i).toFixed(2)} ${sizes[i]}`;
- };
-
- const handleClearLocalStorage = () => {
- localStorage.clear();
- updateStats();
- toast.success("LocalStorage cleared");
- };
-
- const handleClearSessionStorage = () => {
- sessionStorage.clear();
- updateStats();
- toast.success("SessionStorage cleared");
- };
-
- return (
-
-
-
- LocalStorage:
-
- {formatBytes(storageStats.localStorage)}
-
-
-
- SessionStorage:
-
- {formatBytes(storageStats.sessionStorage)}
-
-
-
-
-
- );
-}
-
-function PerformanceInfo() {
- const queryClient = useQueryClient();
- const [memoryInfo, setMemoryInfo] = useState<{
- usedJSHeapSize?: number;
- totalJSHeapSize?: number;
- jsHeapSizeLimit?: number;
- }>({});
- const [advancedMemory, setAdvancedMemory] = useState<{
- bytes?: number;
- breakdown?: Array<{
- bytes: number;
- attribution: Array<{
- url: string;
- scope: string;
- }>;
- types: string[];
- }>;
- } | null>(null);
- const [isMeasuring, setIsMeasuring] = useState(false);
- const [breakdown, setBreakdown] = useState<{
- domNodes: number;
- eventListeners: number;
- reactQueryCache: number;
- storage: number;
- images: number;
- scripts: number;
- timers: number;
- intervals: number;
- websockets: number;
- workers: number;
- }>({
- domNodes: 0,
- eventListeners: 0,
- reactQueryCache: 0,
- storage: 0,
- images: 0,
- scripts: 0,
- timers: 0,
- intervals: 0,
- websockets: 0,
- workers: 0,
- });
-
- const calculateBreakdown = useCallback(() => {
- const domNodes = document.querySelectorAll("*").length;
-
- let eventListeners = 0;
- const allElements = document.querySelectorAll("*");
- for (const el of allElements) {
- if (
- el instanceof HTMLElement &&
- (el.onclick ||
- el.onmouseover ||
- el.onfocus ||
- el.getAttribute("onclick"))
- ) {
- eventListeners++;
- }
- }
-
- const cache = queryClient.getQueryCache();
- const queries = cache.getAll();
- let reactQueryCache = 0;
- for (const query of queries) {
- const state = query.state;
- if (state.data) {
- try {
- reactQueryCache += JSON.stringify(state.data).length;
- } catch {
- reactQueryCache += Object.keys(state.data).length * 100;
- }
- }
- }
-
- let storage = 0;
- for (const key in localStorage) {
- if (Object.hasOwn(localStorage, key)) {
- storage += localStorage[key].length + key.length;
- }
- }
- for (const key in sessionStorage) {
- if (Object.hasOwn(sessionStorage, key)) {
- storage += sessionStorage[key].length + key.length;
- }
- }
-
- const images = document.querySelectorAll("img");
- let imagesSize = 0;
- for (const img of images) {
- if (img.complete && img.naturalWidth && img.naturalHeight) {
- imagesSize += img.naturalWidth * img.naturalHeight * 4;
- }
- }
-
- const scripts = document.querySelectorAll("script").length;
-
- const timers = 0;
- const intervals = 0;
-
- const websockets = 0;
-
- const workers = 0;
-
- setBreakdown({
- domNodes,
- eventListeners,
- reactQueryCache,
- storage,
- images: imagesSize,
- scripts,
- timers,
- intervals,
- websockets,
- workers,
- });
- }, [queryClient]);
-
- const measureAdvancedMemory = useCallback(async () => {
- if (
- typeof performance !== "undefined" &&
- "measureUserAgentSpecificMemory" in performance &&
- typeof performance.measureUserAgentSpecificMemory === "function"
- ) {
- setIsMeasuring(true);
- try {
- const result = await performance.measureUserAgentSpecificMemory();
- setAdvancedMemory(result);
- toast.success("Advanced memory measurement completed");
- } catch (error) {
- if (error instanceof DOMException) {
- if (error.name === "SecurityError") {
- toast.error(
- "Memory measurement requires cross-origin isolation. Enable COOP/COEP headers."
- );
- } else {
- toast.error(`Memory measurement failed: ${error.message}`);
- }
- } else {
- toast.error("Failed to measure memory");
- }
- console.error("Memory measurement error:", error);
- } finally {
- setIsMeasuring(false);
- }
- } else {
- toast.error(
- "Advanced memory API not available. Use Chrome 89+ with cross-origin isolation."
- );
- }
- }, []);
-
- const openDevToolsMemory = useCallback(() => {
- toast.info(
- "Open Chrome DevTools → Memory tab → Take heap snapshot for detailed analysis"
- );
- console.log(
- "💡 Tip: Open Chrome DevTools (F12) → Memory tab → Take heap snapshot to see detailed JavaScript object memory usage"
- );
- }, []);
-
- useEffect(() => {
- let animationFrameId: number;
- let lastUpdate = 0;
- const throttleMs = 500;
-
- const updateMemory = (timestamp: number) => {
- if (timestamp - lastUpdate >= throttleMs) {
- if ("memory" in performance) {
- const mem = (performance as { memory?: typeof memoryInfo }).memory;
- if (mem) {
- setMemoryInfo({
- usedJSHeapSize: mem.usedJSHeapSize,
- totalJSHeapSize: mem.totalJSHeapSize,
- jsHeapSizeLimit: mem.jsHeapSizeLimit,
- });
- }
- }
- calculateBreakdown();
- lastUpdate = timestamp;
- }
-
- animationFrameId = requestAnimationFrame(updateMemory);
- };
-
- if ("memory" in performance) {
- const mem = (performance as { memory?: typeof memoryInfo }).memory;
- if (mem) {
- setMemoryInfo({
- usedJSHeapSize: mem.usedJSHeapSize,
- totalJSHeapSize: mem.totalJSHeapSize,
- jsHeapSizeLimit: mem.jsHeapSizeLimit,
- });
- }
- }
- calculateBreakdown();
-
- animationFrameId = requestAnimationFrame(updateMemory);
-
- return () => {
- cancelAnimationFrame(animationFrameId);
- };
- }, [calculateBreakdown]);
-
- const formatBytes = (bytes?: number) => {
- if (!bytes) {
- return "N/A";
- }
- const k = 1024;
- const sizes = ["B", "KB", "MB"];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return `${(bytes / k ** i).toFixed(2)} ${sizes[i]}`;
- };
-
- const formatNumber = (num: number) => {
- if (num >= 1_000_000) {
- return `${(num / 1_000_000).toFixed(2)}M`;
- }
- if (num >= 1000) {
- return `${(num / 1000).toFixed(2)}K`;
- }
- return num.toString();
- };
-
- const canMeasureAdvanced =
- typeof performance !== "undefined" &&
- "measureUserAgentSpecificMemory" in performance &&
- typeof performance.measureUserAgentSpecificMemory === "function" &&
- typeof crossOriginIsolated !== "undefined" &&
- crossOriginIsolated;
-
- if (Object.keys(memoryInfo).length === 0) {
- return null;
- }
-
- return (
-
-
-
-
- Used Heap:
-
- {formatBytes(memoryInfo.usedJSHeapSize)}
-
-
-
- Total Heap:
-
- {formatBytes(memoryInfo.totalJSHeapSize)}
-
-
-
- Heap Limit:
-
- {formatBytes(memoryInfo.jsHeapSizeLimit)}
-
-
-
-
- {canMeasureAdvanced && (
- <>
-
-
-
-
- Advanced Measurement
-
-
- {isMeasuring ? "Measuring..." : "Measure Memory"}
-
-
- {advancedMemory && (
-
-
- Total Memory:
-
- {formatBytes(advancedMemory.bytes)}
-
-
- {advancedMemory.breakdown && (
-
-
- Breakdown:
-
- {advancedMemory.breakdown.map((item, idx) => (
-
-
- {item.attribution[0]?.url || "Unknown"}:
-
-
- {formatBytes(item.bytes)}
-
-
- ))}
-
- )}
-
- )}
-
- >
- )}
-
-
-
-
-
-
- Tracked Memory Usage
-
-
-
-
-
-
- These are measurable contributions. For detailed JavaScript object
- analysis, use Chrome DevTools Memory profiler (click icon above).
-
-
-
- React Query Cache:
-
- {formatBytes(breakdown.reactQueryCache)}
-
-
-
- Storage (LS/SS):
-
- {formatBytes(breakdown.storage)}
-
-
-
- Images (estimated):
-
- {formatBytes(breakdown.images)}
-
-
-
- DOM Nodes:
-
- {formatNumber(breakdown.domNodes)} nodes
-
-
-
- Scripts:
-
- {breakdown.scripts} loaded
-
-
-
-
-
-
- );
-}
-
-const THEME_OPTIONS = [
- {
- id: "light",
- name: "Light",
- icon: SunIcon,
- },
- {
- id: "dark",
- name: "Dark",
- icon: MoonIcon,
- },
- {
- id: "system",
- name: "System",
- icon: DesktopIcon,
- },
-] as const;
-
-const CHART_TYPE_OPTIONS: {
- id: ChartSeriesKind;
- name: string;
- icon: NavIcon;
-}[] = [
- { id: "bar", name: "Bar", icon: ChartBarIcon },
- { id: "line", name: "Line", icon: ChartLineIcon },
- { id: "area", name: "Area", icon: StackIcon },
-];
-
-const STEP_TYPE_OPTIONS: { id: ChartCurveType; name: string }[] = [
- { id: "monotone", name: "Smooth" },
- { id: "linear", name: "Linear" },
- { id: "step", name: "Step" },
- { id: "stepBefore", name: "Step Before" },
- { id: "stepAfter", name: "Step After" },
-];
-
-const LOCATION_ICONS: Record = {
- "overview-stats": SquaresFourIcon,
- "overview-main": PresentationChartIcon,
- funnels: FunnelIcon,
- "website-list": ChartLineIcon,
- events: ChartBarIcon,
-};
-
-function AppearanceSettings() {
- const { theme, setTheme } = useTheme();
- const { preferences, updateLocationPreferences, updateAllPreferences } =
- useAllChartPreferences();
- const [showGranular, setShowGranular] = useState(false);
- const [mounted, setMounted] = useState(false);
-
- useEffect(() => {
- setMounted(true);
- }, []);
-
- if (!mounted) {
- return null;
- }
-
- const currentTheme = theme ?? "system";
- const globalPrefs = preferences["overview-stats"] ?? {
- chartType: "area" as ChartSeriesKind,
- chartStepType: "monotone" as ChartCurveType,
- };
- const isGlobalBar = globalPrefs.chartType === "bar";
-
- return (
-
-
-
-
- Theme
-
-
- {THEME_OPTIONS.map(({ id, name, icon: Icon }) => (
- setTheme(id)}
- size="sm"
- variant={currentTheme === id ? "default" : "outline"}
- >
-
- {name}
-
- ))}
-
-
-
-
-
-
-
-
- Charts
-
-
-
-
-
All Charts
-
-
- updateAllPreferences({ chartType: v })
- }
- value={globalPrefs.chartType}
- >
-
-
-
-
- {CHART_TYPE_OPTIONS.map(({ id, name, icon: OptIcon }) => (
-
-
-
- {name}
-
-
- ))}
-
-
-
- updateAllPreferences({ chartStepType: v })
- }
- value={globalPrefs.chartStepType}
- >
-
-
-
-
- {STEP_TYPE_OPTIONS.map(({ id, name }) => (
-
- {name}
-
- ))}
-
-
-
-
-
-
setShowGranular(!showGranular)}
- size="sm"
- variant="ghost"
- >
-
- {showGranular ? "Hide per-location" : "Per-location settings"}
-
-
-
-
- {showGranular ? (
-
- {CHART_LOCATIONS.map((location) => {
- const prefs = preferences[location] ?? {
- chartType: "area" as ChartSeriesKind,
- chartStepType: "monotone" as ChartCurveType,
- };
- const isBar = prefs.chartType === "bar";
- const Icon = LOCATION_ICONS[location];
-
- return (
-
-
-
-
- {CHART_LOCATION_LABELS[location]}
-
-
-
-
- updateLocationPreferences(location, {
- chartType: v,
- })
- }
- value={prefs.chartType}
- >
- e.stopPropagation()}
- size="sm"
- >
-
-
-
- {CHART_TYPE_OPTIONS.map(
- ({ id, name, icon: OptIcon }) => (
-
-
-
- {name}
-
-
- )
- )}
-
-
-
- updateLocationPreferences(location, {
- chartStepType: v,
- })
- }
- value={prefs.chartStepType}
- >
- e.stopPropagation()}
- size="sm"
- >
-
-
-
- {STEP_TYPE_OPTIONS.map(({ id, name }) => (
-
- {name}
-
- ))}
-
-
-
-
- );
- })}
-
- ) : null}
-
-
-
- );
-}
-
-function QuickActions() {
- const handleCopyUrl = () => {
- navigator.clipboard.writeText(window.location.href);
- toast.success("URL copied to clipboard");
- };
-
- const handleCopyState = () => {
- const state = {
- url: window.location.href,
- timestamp: new Date().toISOString(),
- userAgent: navigator.userAgent,
- viewport: {
- width: window.innerWidth,
- height: window.innerHeight,
- },
- };
- console.table(state);
- navigator.clipboard.writeText(JSON.stringify(state, null, 2));
- toast.success("State copied to clipboard and logged to console");
- };
-
- const handleClearConsole = () => {
- console.clear();
- toast.success("Console cleared");
- };
-
- const handleReload = () => {
- window.location.reload();
- };
-
- return (
-
-
-
- Quick Actions
-
-
-
- );
-}
-
-export function DevToolsDrawer() {
- const [mounted, setMounted] = useState(false);
- const [open, setOpen] = useState(false);
- const [isLocalhost, setIsLocalhost] = useState(false);
-
- useEffect(() => {
- setMounted(true);
- const hostname = window.location.hostname;
- setIsLocalhost(hostname === "localhost" || hostname === "127.0.0.1");
- }, []);
-
- useEffect(() => {
- if (!isLocalhost) {
- return;
- }
-
- const handleKeyDown = (e: KeyboardEvent) => {
- if ((e.metaKey || e.ctrlKey) && e.key === ".") {
- e.preventDefault();
- setOpen((prev) => !prev);
- }
- };
-
- window.addEventListener("keydown", handleKeyDown);
- return () => window.removeEventListener("keydown", handleKeyDown);
- }, [isLocalhost]);
-
- if (!(mounted && isLocalhost)) {
- return null;
- }
-
- return (
- <>
- setOpen(true)}
- size="icon"
- variant="outline"
- >
-
-
-
-
-
-
-
-
-
- Dev Tools
-
-
-
-
-
-
-
-
- Development tools and debugging utilities
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Tip: Press{" "}
-
- ⌘
- {" "}
-
- .
- {" "}
- to toggle this drawer
-
-
-
-
-
-
- >
- );
-}
diff --git a/apps/dashboard/components/ds/context-menu.tsx b/apps/dashboard/components/ds/context-menu.tsx
index 00ab0e5e96..dd02e96ccd 100644
--- a/apps/dashboard/components/ds/context-menu.tsx
+++ b/apps/dashboard/components/ds/context-menu.tsx
@@ -9,13 +9,6 @@ const ContextMenuPositioner = ContextMenu.Positioner;
const ContextMenuPopup = ContextMenu.Popup;
const ContextMenuItem = ContextMenu.Item;
const ContextMenuSeparator = ContextMenu.Separator;
-const ContextMenuGroup = ContextMenu.Group;
-const ContextMenuGroupLabel = ContextMenu.GroupLabel;
-const ContextMenuRadioGroup = ContextMenu.RadioGroup;
-const ContextMenuRadioItem = ContextMenu.RadioItem;
-const ContextMenuRadioItemIndicator = ContextMenu.RadioItemIndicator;
-const ContextMenuCheckboxItem = ContextMenu.CheckboxItem;
-const ContextMenuCheckboxItemIndicator = ContextMenu.CheckboxItemIndicator;
export {
ContextMenuRoot,
@@ -25,11 +18,4 @@ export {
ContextMenuPopup,
ContextMenuItem,
ContextMenuSeparator,
- ContextMenuGroup,
- ContextMenuGroupLabel,
- ContextMenuRadioGroup,
- ContextMenuRadioItem,
- ContextMenuRadioItemIndicator,
- ContextMenuCheckboxItem,
- ContextMenuCheckboxItemIndicator,
};
diff --git a/apps/dashboard/components/empty-state.tsx b/apps/dashboard/components/empty-state.tsx
deleted file mode 100644
index b7d74bc240..0000000000
--- a/apps/dashboard/components/empty-state.tsx
+++ /dev/null
@@ -1,292 +0,0 @@
-"use client";
-
-import {
- cloneElement,
- memo,
- type ReactElement,
- type ReactNode,
- type SVGProps,
-} from "react";
-import { cn } from "@/lib/utils";
-import { PlusIcon } from "@databuddy/ui/icons";
-import { Button, Card } from "@databuddy/ui";
-
-export interface EmptyStateAction {
- label: string;
- onClick: () => void;
- size?: "sm" | "md" | "lg";
- tone?: "destructive";
- variant?: "primary" | "secondary" | "ghost";
-}
-
-export interface EmptyStateProps {
- /** Primary action button */
- action?: EmptyStateAction;
- /** Custom aria-label for screen readers */
- "aria-label"?: string;
- /** Custom className */
- className?: string;
- /** Description text */
- description?: string | ReactNode;
- /** Main icon to display */
- icon: ReactElement<
- SVGProps & { size?: number | string; weight?: string }
- >;
- /** Whether this is the main content area */
- isMainContent?: boolean;
- /** Custom padding */
- padding?: "sm" | "md" | "lg";
- /** Custom role for accessibility (defaults to 'region') */
- role?: "region" | "complementary" | "main";
- /** Secondary action button */
- secondaryAction?: EmptyStateAction;
- /** Whether to show the plus badge on the icon */
- showPlusBadge?: boolean;
- /** Main heading */
- title: string;
- /** Custom styling variants */
- variant?: "default" | "simple" | "minimal" | "error";
-}
-
-export const EmptyState = memo(function EmptyState({
- icon,
- title,
- description,
- action,
- secondaryAction,
- variant = "minimal",
- className,
- showPlusBadge = true,
- padding = "lg",
- role = "region",
- "aria-label": ariaLabel,
- isMainContent = false,
-}: EmptyStateProps) {
- const getPadding = () => {
- switch (padding) {
- case "sm":
- return "px-6 py-12";
- case "md":
- return "px-8 py-14";
- case "lg":
- return "px-8";
- default:
- return "px-8";
- }
- };
-
- const renderIcon = () => {
- if (!icon || typeof icon !== "object" || !("type" in icon)) {
- return null;
- }
-
- const iconProps = icon.props || {};
-
- if (variant === "simple" || variant === "minimal" || variant === "error") {
- return (
-
- {cloneElement(icon, {
- ...iconProps,
- className: cn(
- "size-6 text-accent",
- variant === "error" && "text-destructive",
- iconProps.className
- ),
- "aria-hidden": "true",
- size: 24,
- weight: "fill",
- })}
-
- );
- }
-
- return (
-
-
- {cloneElement(icon, {
- ...iconProps,
- className: cn("size-6 text-primary", iconProps.className),
- "aria-hidden": "true",
- size: 24,
- weight: "duotone",
- })}
-
- {showPlusBadge && (
-
{
- e.stopPropagation();
- action?.onClick();
- }}
- size="icon"
- variant="secondary"
- >
-
-
- )}
-
- );
- };
-
- const renderCard = () => {
- const cardClasses = cn(
- variant === "default" &&
- "rounded-xl border-2 border-dashed bg-gradient-to-br from-background to-muted/10",
- variant === "simple" && "rounded border-dashed bg-muted/10",
- variant === "minimal" &&
- "flex flex-1 rounded border-none bg-transparent shadow-none",
- variant === "error" &&
- "flex flex-1 rounded border-none bg-transparent shadow-none",
- "safe-area-inset-4 sm:safe-area-inset-6 lg:safe-area-inset-8",
- className
- );
-
- const contentClasses = cn(
- "flex flex-1 flex-col items-center justify-center text-center",
- getPadding(),
- "px-6 sm:px-8 lg:px-12"
- );
-
- return (
-
-
- {renderIcon()}
-
- {isMainContent ? (
-
- {title}
-
- ) : (
-
-
- {title}
-
-
{description}
-
- )}
- {(action || secondaryAction) && (
-
- {action && (
-
- {variant === "default" && (
-
- )}
- {variant === "default" && (
-
- )}
-
- {action.label}
-
-
- )}
- {secondaryAction && (
-
- {secondaryAction.label}
-
- )}
-
- )}
-
-
-
- );
- };
-
- return renderCard();
-});
-
-EmptyState.displayName = "EmptyState";
-
-export function FeatureEmptyState({
- icon,
- title,
- description,
- actionLabel,
- onAction,
-}: {
- icon: ReactElement<
- SVGProps & { size?: number | string; weight?: string }
- >;
- title: string;
- description: string;
- actionLabel: string;
- onAction: () => void;
-}) {
- return (
-
- );
-}
diff --git a/apps/dashboard/components/events/custom-events/index.ts b/apps/dashboard/components/events/custom-events/index.ts
index 5db3ed5fb7..672ec142b4 100644
--- a/apps/dashboard/components/events/custom-events/index.ts
+++ b/apps/dashboard/components/events/custom-events/index.ts
@@ -1,22 +1,9 @@
-export {
- classifyEventProperties,
- getPropertyTypeLabel,
-} from "./classify-properties";
+export { classifyEventProperties } from "./classify-properties";
export { EventsOverviewContent } from "./events-overview-content";
export {
EventsStatsGrid,
ORGANIZATION_EVENTS_METRICS,
WEBSITE_EVENTS_METRICS,
} from "./events-stats-grid";
-export { EventsTrendChart } from "./events-trend-chart";
-export {
- formatDateLabel,
- generateDateRange,
- getGranularity,
- normalizeDateKey,
- safePercentage,
-} from "./events-utils";
-export { PropertySummary } from "./property-summary";
export { PropertyValueCard } from "./property-value-card";
-export { useCustomEventsOverview } from "./use-custom-events-overview";
export type * from "./types";
diff --git a/apps/dashboard/components/events/custom-events/property-value-card.tsx b/apps/dashboard/components/events/custom-events/property-value-card.tsx
index 958cbe7f79..145ec75cc7 100644
--- a/apps/dashboard/components/events/custom-events/property-value-card.tsx
+++ b/apps/dashboard/components/events/custom-events/property-value-card.tsx
@@ -6,7 +6,7 @@ import { safePercentage } from "./events-utils";
import { FunnelIcon, TagIcon } from "@databuddy/ui/icons";
import { Badge } from "@databuddy/ui";
-export interface PropertyValueCardValue {
+interface PropertyValueCardValue {
count: number;
percentage: number;
property_value: string;
diff --git a/apps/dashboard/components/events/custom-events/types.ts b/apps/dashboard/components/events/custom-events/types.ts
index 6e9fd04746..a364e61209 100644
--- a/apps/dashboard/components/events/custom-events/types.ts
+++ b/apps/dashboard/components/events/custom-events/types.ts
@@ -56,7 +56,7 @@ export interface MiniChartDataPoint {
value: number;
}
-export type PropertyInferredType =
+type PropertyInferredType =
| "boolean"
| "numeric"
| "datetime"
@@ -66,7 +66,7 @@ export type PropertyInferredType =
| "text"
| "high_cardinality";
-export type PropertyRenderStrategy =
+type PropertyRenderStrategy =
| "distribution_bar"
| "top_n_chart"
| "top_n_with_other"
diff --git a/apps/dashboard/components/events/custom-events/use-custom-events-overview.ts b/apps/dashboard/components/events/custom-events/use-custom-events-overview.ts
index 5108f75de0..54cc5f717f 100644
--- a/apps/dashboard/components/events/custom-events/use-custom-events-overview.ts
+++ b/apps/dashboard/components/events/custom-events/use-custom-events-overview.ts
@@ -216,5 +216,3 @@ export function useCustomEventsOverview({
todayUsers: todayEvent?.unique_users ?? 0,
};
}
-
-export { getRawData };
diff --git a/apps/dashboard/components/events/events-stream-content.tsx b/apps/dashboard/components/events/events-stream-content.tsx
index 23e776e7e9..2a918f0a76 100644
--- a/apps/dashboard/components/events/events-stream-content.tsx
+++ b/apps/dashboard/components/events/events-stream-content.tsx
@@ -56,7 +56,7 @@ import {
export type { RecentCustomEvent } from "@/components/events/custom-events";
-export interface EventsStreamData {
+interface EventsStreamData {
error: Error | null;
events: StreamCustomEvent[] | undefined;
isError: boolean;
@@ -231,7 +231,9 @@ export function EventsStreamContent({
);
const pageRef = useRef(page);
- pageRef.current = page;
+ useEffect(() => {
+ pageRef.current = page;
+ }, [page]);
const justResetRef = useRef(false);
useEffect(() => {
@@ -325,9 +327,9 @@ export function EventsStreamContent({
}
const values = new Set();
for (const event of allEvents) {
- const val = event.properties[selectedPropertyKey];
- if (val !== undefined && val !== null) {
- values.add(String(val));
+ const propertyValue = event.properties[selectedPropertyKey];
+ if (propertyValue !== undefined && propertyValue !== null) {
+ values.add(String(propertyValue));
}
}
return Array.from(values).sort();
diff --git a/apps/dashboard/components/icon.tsx b/apps/dashboard/components/icon.tsx
index 895eaf222a..8d096ccb4a 100644
--- a/apps/dashboard/components/icon.tsx
+++ b/apps/dashboard/components/icon.tsx
@@ -68,9 +68,7 @@ const OS_ICON_EXT: Record = {
const BROWSER_ICONS = Object.keys(BROWSER_ICON_EXT);
const OS_ICONS = Object.keys(OS_ICON_EXT);
-export type BrowserIconName = keyof typeof BROWSER_ICON_EXT;
-export type OSIconName = keyof typeof OS_ICON_EXT;
-export type IconType = "browser" | "os";
+type IconType = "browser" | "os";
interface PublicIconProps {
className?: string;
@@ -148,7 +146,7 @@ function createFallbackIcon(
);
}
-export function PublicIcon({
+function PublicIcon({
type,
name,
size = "md",
diff --git a/apps/dashboard/components/layout/help-dialog.tsx b/apps/dashboard/components/layout/help-dialog.tsx
deleted file mode 100644
index 80ed58a37b..0000000000
--- a/apps/dashboard/components/layout/help-dialog.tsx
+++ /dev/null
@@ -1,158 +0,0 @@
-"use client";
-
-import Link from "next/link";
-import { useState } from "react";
-import { KeyboardShortcuts } from "@/components/ui/keyboard-shortcuts";
-import { cn } from "@/lib/utils";
-import {
- BookOpenIcon,
- ChatTextIcon as ChatCircleIcon,
- CommandIcon as KeyboardIcon,
- PlayIcon,
-} from "@databuddy/ui/icons";
-import { Button, Text } from "@databuddy/ui";
-import { Dialog } from "@databuddy/ui/client";
-
-interface HelpDialogProps {
- onOpenChangeAction: (open: boolean) => void;
- open: boolean;
-}
-
-const HELP_ITEMS = [
- {
- href: "https://www.databuddy.cc/docs",
- icon: BookOpenIcon,
- title: "Documentation",
- description: "Read guides and API references",
- external: true,
- },
- {
- href: "mailto:support@databuddy.cc",
- icon: ChatCircleIcon,
- title: "Contact Support",
- description: "Get help from our support team",
- external: false,
- },
- {
- href: "https://www.youtube.com/@trydatabuddy",
- icon: PlayIcon,
- title: "Tutorials",
- description: "Learn Databuddy step by step",
- external: true,
- },
-] as const;
-
-function HelpRow({
- children,
- className,
- ...rest
-}: React.ButtonHTMLAttributes) {
- return (
-
- {children}
-
- );
-}
-
-export function HelpDialog({ open, onOpenChangeAction }: HelpDialogProps) {
- const [showShortcuts, setShowShortcuts] = useState(false);
-
- return (
- {
- if (!o) {
- setShowShortcuts(false);
- }
- onOpenChangeAction(o);
- }}
- open={open}
- >
-
-
- Help & Resources
-
- Get assistance and learn more about Databuddy
-
-
-
-
- {showShortcuts ? (
-
-
- Keyboard Shortcuts
- setShowShortcuts(false)}
- size="sm"
- variant="ghost"
- >
- Back
-
-
-
-
- ) : (
-
-
setShowShortcuts(true)}>
-
-
-
-
- Keyboard Shortcuts
-
- View all available keyboard shortcuts
-
-
-
-
- {HELP_ITEMS.map((item) => {
- const Icon = item.icon;
- return (
-
-
-
-
-
- {item.title}
-
- {item.description}
-
-
-
- );
- })}
-
- )}
-
-
-
- );
-}
diff --git a/apps/dashboard/components/layout/logo.tsx b/apps/dashboard/components/layout/logo.tsx
index b241f603f5..a161494972 100644
--- a/apps/dashboard/components/layout/logo.tsx
+++ b/apps/dashboard/components/layout/logo.tsx
@@ -2,7 +2,6 @@ import Link from "next/link";
import { Branding } from "../logo/branding";
export { Branding } from "../logo/branding";
-export type { BrandingProps, BrandVariant } from "../logo/branding";
export function Logo() {
return (
diff --git a/apps/dashboard/components/layout/navigation/navigation-config.tsx b/apps/dashboard/components/layout/navigation/navigation-config.tsx
index b34271a267..2b2a9156fc 100644
--- a/apps/dashboard/components/layout/navigation/navigation-config.tsx
+++ b/apps/dashboard/components/layout/navigation/navigation-config.tsx
@@ -40,7 +40,7 @@ import {
} from "@databuddy/ui/icons";
import type { NavigationGroup, NavigationItem } from "./types";
-export const createNavItem = (
+const createNavItem = (
name: string,
icon: NavigationItem["icon"],
href: string,
diff --git a/apps/dashboard/components/layout/navigation/types.ts b/apps/dashboard/components/layout/navigation/types.ts
index 0ea43c7ef8..e95232c7e3 100644
--- a/apps/dashboard/components/layout/navigation/types.ts
+++ b/apps/dashboard/components/layout/navigation/types.ts
@@ -34,7 +34,7 @@ export interface NavigationItem {
tag?: string;
}
-export interface NavigationSearchItem {
+interface NavigationSearchItem {
disabled?: boolean;
external?: boolean;
href?: string;
@@ -44,13 +44,6 @@ export interface NavigationSearchItem {
searchTags?: string[];
}
-export interface NavigationSection {
- flag?: string;
- icon: NavIcon;
- items: NavigationItem[];
- title: string;
-}
-
export interface NavigationGroup {
back?: { href: string; label: string };
flag?: string;
@@ -58,5 +51,3 @@ export interface NavigationGroup {
label: string;
pinToBottom?: boolean;
}
-
-export type NavigationEntry = NavigationSection | NavigationItem;
diff --git a/apps/dashboard/components/layout/profile-button-client.tsx b/apps/dashboard/components/layout/profile-button-client.tsx
index 4f2f8d7240..1a84a16197 100644
--- a/apps/dashboard/components/layout/profile-button-client.tsx
+++ b/apps/dashboard/components/layout/profile-button-client.tsx
@@ -13,7 +13,7 @@ import {
SpinnerGapIcon,
} from "@databuddy/ui/icons";
import { Avatar, DropdownMenu } from "@databuddy/ui/client";
-import { Text, Tooltip } from "@databuddy/ui";
+import { Text } from "@databuddy/ui";
import { clearPersistedQueryCache } from "@/lib/query-client";
export interface ProfileButtonUser {
@@ -237,39 +237,3 @@ export function ProfileDropdownContent({
);
}
-
-export function ProfileButtonClient({
- user,
-}: {
- user: ProfileButtonUser | null;
-}) {
- const [isOpen, setIsOpen] = useState(false);
-
- if (!user) {
- return null;
- }
-
- return (
-
-
- }
- >
-
-
-
- setIsOpen(false)}
- user={user}
- />
-
- );
-}
diff --git a/apps/dashboard/components/layout/sidebar.tsx b/apps/dashboard/components/layout/sidebar.tsx
index b6e713dc12..afc9feb463 100644
--- a/apps/dashboard/components/layout/sidebar.tsx
+++ b/apps/dashboard/components/layout/sidebar.tsx
@@ -301,22 +301,20 @@ function useGroupCollapse(groupKey: string, hasActiveChild: boolean) {
}, [groupKey, hasActiveChild]);
const toggle = useCallback(() => {
- setIsCollapsed((prev) => {
- const next = !prev;
- try {
- const stored = JSON.parse(
- localStorage.getItem(COLLAPSED_GROUPS_KEY) || "{}"
- );
- if (next) {
- stored[groupKey] = true;
- } else {
- delete stored[groupKey];
- }
- localStorage.setItem(COLLAPSED_GROUPS_KEY, JSON.stringify(stored));
- } catch {}
- return next;
- });
- }, [groupKey]);
+ const next = !isCollapsed;
+ setIsCollapsed(next);
+ try {
+ const stored = JSON.parse(
+ localStorage.getItem(COLLAPSED_GROUPS_KEY) || "{}"
+ );
+ if (next) {
+ stored[groupKey] = true;
+ } else {
+ delete stored[groupKey];
+ }
+ localStorage.setItem(COLLAPSED_GROUPS_KEY, JSON.stringify(stored));
+ } catch {}
+ }, [groupKey, isCollapsed]);
return { isCollapsed, toggle };
}
diff --git a/apps/dashboard/components/layout/top-bar.tsx b/apps/dashboard/components/layout/top-bar.tsx
index c8d845eaea..7dc8b1a2e7 100644
--- a/apps/dashboard/components/layout/top-bar.tsx
+++ b/apps/dashboard/components/layout/top-bar.tsx
@@ -89,7 +89,9 @@ function useTopBarSlot(name: string, content: ReactNode) {
const store = useStore();
const id = useId();
const contentRef = useRef(content);
- contentRef.current = content;
+ useEffect(() => {
+ contentRef.current = content;
+ }, [content]);
useEffect(() => {
store.setSlot(name, id, contentRef.current);
diff --git a/apps/dashboard/components/logo/branding.tsx b/apps/dashboard/components/logo/branding.tsx
index ebfb16ce83..348c4f2c8e 100644
--- a/apps/dashboard/components/logo/branding.tsx
+++ b/apps/dashboard/components/logo/branding.tsx
@@ -1,20 +1,14 @@
import Image from "next/image";
import { cn } from "@/lib/utils";
-export type BrandVariant =
- | "logomark"
- | "wordmark"
- | "primary-logo"
- | "secondary-logo";
+type BrandVariant = "logomark" | "wordmark" | "primary-logo" | "secondary-logo";
export interface BrandingProps {
className?: string;
- /** Height of the primary asset in pixels (width follows the SVG viewBox aspect ratio). */
heightPx?: number;
imageClassName?: string;
priority?: boolean;
variant: BrandVariant;
- /** When `variant` is `logomark`, also show the wordmark asset beside the icon. */
wordmark?: boolean;
}
@@ -33,7 +27,6 @@ const BRAND_PATH: Record = {
};
interface ThemeBrandImageProps {
- /** Primary image alt; the dark-mode twin is decorative. */
alt: string;
basePath: string;
className?: string;
diff --git a/apps/dashboard/components/logo/section-brand-overlay.tsx b/apps/dashboard/components/logo/section-brand-overlay.tsx
index edbbff03a6..5f28c30419 100644
--- a/apps/dashboard/components/logo/section-brand-overlay.tsx
+++ b/apps/dashboard/components/logo/section-brand-overlay.tsx
@@ -3,9 +3,7 @@ import { cn } from "@/lib/utils";
export interface SectionBrandOverlayProps {
className?: string;
- /** Corner overlay vs inline (e.g. chart card header). */
layout?: "overlay" | "inline";
- /** When `layout` is `overlay`: horizontal corner. */
position?: "start" | "end";
}
diff --git a/apps/dashboard/components/monitors/collapsible-section.tsx b/apps/dashboard/components/monitors/collapsible-section.tsx
deleted file mode 100644
index a012d0302c..0000000000
--- a/apps/dashboard/components/monitors/collapsible-section.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-"use client";
-
-import { AnimatePresence, motion } from "motion/react";
-import { cn } from "@/lib/utils";
-import { CaretDownIcon } from "@databuddy/ui/icons";
-import { Button } from "@databuddy/ui";
-
-interface CollapsibleSectionProps {
- badge?: number;
- children: React.ReactNode;
- icon: React.ComponentType<{ size?: number; weight?: "duotone" | "fill" }>;
- isExpanded: boolean;
- onToggleAction: () => void;
- title: string;
-}
-
-export function CollapsibleSection({
- icon: Icon,
- title,
- badge,
- isExpanded,
- onToggleAction,
- children,
-}: CollapsibleSectionProps) {
- return (
-
-
-
-
- {title}
- {badge !== undefined && badge > 0 && (
-
- {badge}
-
- )}
-
-
-
-
-
- {isExpanded && (
-
- {children}
-
- )}
-
-
- );
-}
diff --git a/apps/dashboard/components/monitors/monitor-sheet.tsx b/apps/dashboard/components/monitors/monitor-sheet.tsx
index 5cf93f3c60..cb33f17e99 100644
--- a/apps/dashboard/components/monitors/monitor-sheet.tsx
+++ b/apps/dashboard/components/monitors/monitor-sheet.tsx
@@ -389,8 +389,10 @@ export function MonitorSheet({
max={120}
min={1}
onChange={(e) => {
- const val = e.target.value;
- setTimeoutMs(val ? Number(val) * 1000 : null);
+ const seconds = e.target.value;
+ setTimeoutMs(
+ seconds ? Number(seconds) * 1000 : null
+ );
}}
placeholder="30"
suffix="sec"
diff --git a/apps/dashboard/components/openai-ads-pixel.tsx b/apps/dashboard/components/openai-ads-pixel.tsx
index 987adac43b..eff4363c7c 100644
--- a/apps/dashboard/components/openai-ads-pixel.tsx
+++ b/apps/dashboard/components/openai-ads-pixel.tsx
@@ -89,7 +89,7 @@ export function OpenAiAdsPixel() {
return null;
}
-export function measureOpenAiRegistrationCompleted(eventId?: string) {
+function measureOpenAiRegistrationCompleted(eventId?: string) {
if (!initOpenAiQueue()) {
return;
}
diff --git a/apps/dashboard/components/organizations/api-key-types.ts b/apps/dashboard/components/organizations/api-key-types.ts
index 48eaaf2af0..59f59c4f96 100644
--- a/apps/dashboard/components/organizations/api-key-types.ts
+++ b/apps/dashboard/components/organizations/api-key-types.ts
@@ -39,23 +39,6 @@ export function formatMaskedApiKey({
return `${startIncludesPrefix ? start : `${cleanPrefix}_${start}`}••••`;
}
-export type ApiResourceType =
- | "global"
- | "website"
- | "ab_experiment"
- | "feature_flag"
- | "analytics_data"
- | "error_data"
- | "web_vitals"
- | "custom_events"
- | "export_data";
-
-export interface ApiKeyAccessEntry {
- resourceId?: string | null;
- resourceType: ApiResourceType;
- scopes: ApiScope[];
-}
-
export interface ApiKeyListItem {
createdAt: Date;
description?: string | null;
@@ -79,20 +62,3 @@ export interface ApiKeyListItem {
type: "user" | "sdk" | "automation";
updatedAt: Date;
}
-
-export interface ApiKeyDetail extends ApiKeyListItem {
- access: Array<{ id: string } & ApiKeyAccessEntry>;
-}
-
-export interface CreateApiKeyInput {
- access?: ApiKeyAccessEntry[];
- expiresAt?: string;
- globalScopes?: ApiScope[];
- metadata?: Record;
- name: string;
- organizationId: string;
- rateLimitEnabled?: boolean;
- rateLimitMax?: number;
- rateLimitTimeWindow?: number;
- type?: "user" | "sdk" | "automation";
-}
diff --git a/apps/dashboard/components/providers/billing-provider.tsx b/apps/dashboard/components/providers/billing-provider.tsx
index 095612cd67..2dbb5ef83e 100644
--- a/apps/dashboard/components/providers/billing-provider.tsx
+++ b/apps/dashboard/components/providers/billing-provider.tsx
@@ -22,7 +22,7 @@ type HookCustomer = NonNullable["data"]>;
type HookPlan = NonNullable["data"]>[number];
type HookBalance = NonNullable[string];
-export interface FeatureAccess {
+interface FeatureAccess {
allowed: boolean;
balance: number;
limit: number;
@@ -30,7 +30,7 @@ export interface FeatureAccess {
usagePercent: number | null;
}
-export interface GatedFeatureAccess {
+interface GatedFeatureAccess {
allowed: boolean;
minPlan: PlanId | null;
upgradeMessage: string | null;
diff --git a/apps/dashboard/components/table/data-table.tsx b/apps/dashboard/components/table/data-table.tsx
index 1a378bc1a9..62bf43ea9a 100644
--- a/apps/dashboard/components/table/data-table.tsx
+++ b/apps/dashboard/components/table/data-table.tsx
@@ -98,6 +98,7 @@ export function DataTable({
const [activeTab, setActiveTab] = useState(tabs?.[0]?.id || "");
const { fullScreen, setFullScreen, hasMounted, modalRef } = useFullScreen();
+ const portalTarget = typeof document === "undefined" ? null : document.body;
const currentTabData = tabs?.find((tab) => tab.id === activeTab);
const tableData = currentTabData?.data || data || [];
@@ -175,6 +176,7 @@ export function DataTable({
{hasMounted &&
fullScreen &&
+ portalTarget &&
ReactDOM.createPortal(
({
/>
,
- document.body
+ portalTarget
)}
>
);
diff --git a/apps/dashboard/components/table/rows/icon-text-row.tsx b/apps/dashboard/components/table/rows/icon-text-row.tsx
index 3d026a4401..cb0ff5c3b5 100644
--- a/apps/dashboard/components/table/rows/icon-text-row.tsx
+++ b/apps/dashboard/components/table/rows/icon-text-row.tsx
@@ -1,76 +1 @@
-import type { CellContext, ColumnDef } from "@tanstack/react-table";
-import type { ReactNode } from "react";
-import { formatNumber } from "@/lib/formatters";
-import { PercentageBadge } from "@databuddy/ui";
-
-export interface IconTextEntry {
- name: string;
- pageviews?: number;
- percentage?: number;
- visitors: number;
-}
-
-interface IconTextRowProps {
- accessorKey?: string;
- getIcon: (name: string, entry?: IconTextEntry) => ReactNode;
- getSubtitle?: (entry: IconTextEntry) => string | undefined;
- header: string;
- includeMetrics?: boolean;
-}
-
-export function createIconTextColumns({
- header,
- accessorKey = "name",
- getIcon,
- getSubtitle,
- includeMetrics = true,
-}: IconTextRowProps): ColumnDef[] {
- const columns: ColumnDef[] = [
- {
- id: accessorKey,
- accessorKey,
- header,
- cell: (info: CellContext) => {
- const name = (info.getValue() as string) || "";
- const entry = info.row.original;
- const subtitle = getSubtitle?.(entry);
-
- return (
-
- {getIcon(name, entry)}
-
-
{name}
- {subtitle && (
-
{subtitle}
- )}
-
-
- );
- },
- },
- ];
-
- if (includeMetrics) {
- columns.push(
- {
- id: "visitors",
- accessorKey: "visitors",
- header: "Visitors",
- cell: (info: CellContext) => (
- {formatNumber(info.getValue())}
- ),
- },
- {
- id: "percentage",
- accessorKey: "percentage",
- header: "Share",
- cell: (info: CellContext) => {
- const percentage = info.getValue() as number;
- return ;
- },
- }
- );
- }
-
- return columns;
-}
+export {};
diff --git a/apps/dashboard/components/table/rows/referrer-row.tsx b/apps/dashboard/components/table/rows/referrer-row.tsx
index 711b17ed39..e8e1eb78a4 100644
--- a/apps/dashboard/components/table/rows/referrer-row.tsx
+++ b/apps/dashboard/components/table/rows/referrer-row.tsx
@@ -30,7 +30,7 @@ const DEFAULT_REFERRER_METRICS: ReferrerMetricColumn[] = [
{ id: "pageviews", header: "Views" },
];
-export function getReferrerDisplayValue(row: ReferrerSourceCellData): string {
+function getReferrerDisplayValue(row: ReferrerSourceCellData): string {
return row.name || row.source || row.referrer || "Direct";
}
diff --git a/apps/dashboard/components/ui/alert.tsx b/apps/dashboard/components/ui/alert.tsx
index 240bf847df..3a01dbfb30 100644
--- a/apps/dashboard/components/ui/alert.tsx
+++ b/apps/dashboard/components/ui/alert.tsx
@@ -60,4 +60,4 @@ function AlertDescription({
);
}
-export { Alert, AlertDescription, AlertTitle };
+export { Alert, AlertDescription, };
diff --git a/apps/dashboard/components/ui/aspect-ratio.tsx b/apps/dashboard/components/ui/aspect-ratio.tsx
deleted file mode 100644
index 956e8bb05d..0000000000
--- a/apps/dashboard/components/ui/aspect-ratio.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-"use client";
-
-import { AspectRatio as AspectRatioPrimitive } from "radix-ui";
-
-function AspectRatio({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-export { AspectRatio };
diff --git a/apps/dashboard/components/ui/badge.tsx b/apps/dashboard/components/ui/badge.tsx
deleted file mode 100644
index 3c6f47bfdd..0000000000
--- a/apps/dashboard/components/ui/badge.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Slot } from "@radix-ui/react-slot";
-import { cva, type VariantProps } from "class-variance-authority";
-import type * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-const badgeVariants = cva(
- "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded border px-2 py-0.5 font-medium text-xs transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
- {
- variants: {
- variant: {
- default:
- "border border-brand-purple/35 bg-brand-purple text-white dark:border-brand-purple/55 dark:bg-brand-purple dark:text-white [a&]:hover:bg-brand-purple/90",
- gray: "border border-border bg-muted text-muted-foreground dark:border-border dark:bg-secondary dark:text-muted-foreground [a&]:hover:bg-muted/90",
- blue: "border border-brand-purple/25 bg-brand-purple/10 text-brand-purple dark:border-brand-purple/40 dark:bg-brand-purple/18 dark:text-[#C9BFE8] [a&]:hover:bg-brand-purple/15",
- green:
- "border border-emerald-600/25 bg-emerald-50 text-emerald-800 dark:border-emerald-500/35 dark:bg-emerald-950/50 dark:text-emerald-300 [a&]:hover:bg-emerald-100/90",
- amber:
- "border border-brand-amber/30 bg-brand-amber/12 text-amber-950 dark:border-brand-amber/40 dark:bg-brand-amber/14 dark:text-amber-300 [a&]:hover:bg-brand-amber/18",
- secondary:
- "border border-foreground/15 bg-foreground text-background dark:border-foreground/25 dark:bg-foreground dark:text-background [a&]:hover:bg-foreground/90",
- destructive:
- "border border-brand-coral/30 bg-brand-coral/12 text-brand-coral focus-visible:ring-brand-coral/20 dark:border-brand-coral/45 dark:bg-brand-coral/22 dark:text-[#E8A8BE] [a&]:hover:bg-brand-coral/18",
- outline:
- "border border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-);
-
-function Badge({
- className,
- variant,
- asChild = false,
- ...props
-}: React.ComponentProps<"span"> &
- VariantProps & { asChild?: boolean }) {
- const Comp = asChild ? Slot : "span";
-
- return (
-
- );
-}
-
-export { Badge, badgeVariants };
diff --git a/apps/dashboard/components/ui/button-group.tsx b/apps/dashboard/components/ui/button-group.tsx
deleted file mode 100644
index 69c616fcd6..0000000000
--- a/apps/dashboard/components/ui/button-group.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import { Slot } from "@radix-ui/react-slot";
-import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "@/lib/utils";
-import { Divider } from "@databuddy/ui";
-
-const buttonGroupVariants = cva(
- "flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
- {
- variants: {
- orientation: {
- horizontal:
- "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
- vertical:
- "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
- },
- },
- defaultVariants: {
- orientation: "horizontal",
- },
- }
-);
-
-function ButtonGroup({
- className,
- orientation,
- ...props
-}: React.ComponentProps<"div"> & VariantProps) {
- return (
-
- );
-}
-
-function ButtonGroupText({
- className,
- asChild = false,
- ...props
-}: React.ComponentProps<"div"> & {
- asChild?: boolean;
-}) {
- const Comp = asChild ? Slot : "div";
-
- return (
-
- );
-}
-
-function ButtonGroupSeparator({
- className,
- orientation = "vertical",
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-export {
- ButtonGroup,
- ButtonGroupSeparator,
- ButtonGroupText,
- buttonGroupVariants,
-};
diff --git a/apps/dashboard/components/ui/button.tsx b/apps/dashboard/components/ui/button.tsx
index 5948bdef7d..88423af602 100644
--- a/apps/dashboard/components/ui/button.tsx
+++ b/apps/dashboard/components/ui/button.tsx
@@ -65,4 +65,4 @@ function Button({
);
}
-export { Button, buttonVariants };
+export { Button, };
diff --git a/apps/dashboard/components/ui/card.tsx b/apps/dashboard/components/ui/card.tsx
deleted file mode 100644
index aa9c6f8a63..0000000000
--- a/apps/dashboard/components/ui/card.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import type * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-function Card({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function CardAction({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function CardContent({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-export {
- Card,
- CardAction,
- CardContent,
- CardDescription,
- CardFooter,
- CardHeader,
- CardTitle,
-};
diff --git a/apps/dashboard/components/ui/carousel.tsx b/apps/dashboard/components/ui/carousel.tsx
deleted file mode 100644
index b4134c2143..0000000000
--- a/apps/dashboard/components/ui/carousel.tsx
+++ /dev/null
@@ -1,249 +0,0 @@
-"use client";
-
-import useEmblaCarousel, {
- type UseEmblaCarouselType,
-} from "embla-carousel-react";
-import * as React from "react";
-import { cn } from "@/lib/utils";
-import {
- ArrowLeftIcon,
- ArrowRightIcon,
-} from "@databuddy/ui/icons";
-import { Button } from "@databuddy/ui";
-
-type CarouselApi = UseEmblaCarouselType[1];
-type UseCarouselParameters = Parameters;
-type CarouselOptions = UseCarouselParameters[0];
-type CarouselPlugin = UseCarouselParameters[1];
-
-type CarouselProps = {
- opts?: CarouselOptions;
- plugins?: CarouselPlugin;
- orientation?: "horizontal" | "vertical";
- setApi?: (api: CarouselApi) => void;
-};
-
-type CarouselContextProps = {
- carouselRef: ReturnType[0];
- api: ReturnType[1];
- scrollPrev: () => void;
- scrollNext: () => void;
- canScrollPrev: boolean;
- canScrollNext: boolean;
-} & CarouselProps;
-
-const CarouselContext = React.createContext(null);
-
-function useCarousel() {
- const context = React.useContext(CarouselContext);
-
- if (!context) {
- throw new Error("useCarousel must be used within a ");
- }
-
- return context;
-}
-
-function Carousel({
- orientation = "horizontal",
- opts,
- setApi,
- plugins,
- className,
- children,
- ...props
-}: React.ComponentProps<"div"> & CarouselProps) {
- const [carouselRef, api] = useEmblaCarousel(
- {
- ...opts,
- axis: orientation === "horizontal" ? "x" : "y",
- },
- plugins
- );
- const [canScrollPrev, setCanScrollPrev] = React.useState(false);
- const [canScrollNext, setCanScrollNext] = React.useState(false);
-
- const onSelect = React.useCallback((api: CarouselApi) => {
- if (!api) {
- return;
- }
- setCanScrollPrev(api.canScrollPrev());
- setCanScrollNext(api.canScrollNext());
- }, []);
-
- const scrollPrev = React.useCallback(() => {
- api?.scrollPrev();
- }, [api]);
-
- const scrollNext = React.useCallback(() => {
- api?.scrollNext();
- }, [api]);
-
- const handleKeyDown = React.useCallback(
- (event: React.KeyboardEvent) => {
- if (event.key === "ArrowLeft") {
- event.preventDefault();
- scrollPrev();
- } else if (event.key === "ArrowRight") {
- event.preventDefault();
- scrollNext();
- }
- },
- [scrollPrev, scrollNext]
- );
-
- React.useEffect(() => {
- if (!(api && setApi)) {
- return;
- }
- setApi(api);
- }, [api, setApi]);
-
- React.useEffect(() => {
- if (!api) {
- return;
- }
- onSelect(api);
- api.on("reInit", onSelect);
- api.on("select", onSelect);
-
- return () => {
- api?.off("select", onSelect);
- };
- }, [api, onSelect]);
-
- return (
-
-
- {children}
-
-
- );
-}
-
-function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
- const { carouselRef, orientation } = useCarousel();
-
- return (
-
- );
-}
-
-function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
- const { orientation } = useCarousel();
-
- return (
-
- );
-}
-
-function CarouselPrevious({
- className,
- variant = "secondary",
- size = "sm",
- ...props
-}: React.ComponentProps) {
- const { orientation, scrollPrev, canScrollPrev } = useCarousel();
-
- return (
-
-
- Previous slide
-
- );
-}
-
-function CarouselNext({
- className,
- variant = "secondary",
- size = "sm",
- ...props
-}: React.ComponentProps) {
- const { orientation, scrollNext, canScrollNext } = useCarousel();
-
- return (
-
-
- Next slide
-
- );
-}
-
-export {
- Carousel,
- type CarouselApi,
- CarouselContent,
- CarouselItem,
- CarouselNext,
- CarouselPrevious,
-};
diff --git a/apps/dashboard/components/ui/command.tsx b/apps/dashboard/components/ui/command.tsx
deleted file mode 100644
index ab33153220..0000000000
--- a/apps/dashboard/components/ui/command.tsx
+++ /dev/null
@@ -1,174 +0,0 @@
-"use client";
-
-import { Command as CommandPrimitive } from "cmdk";
-import type * as React from "react";
-import { cn } from "@/lib/utils";
-import {
- MagnifyingGlassIcon,
-} from "@databuddy/ui/icons";
-import { Dialog } from "@databuddy/ui/client";
-
-function Command({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function CommandDialog({
- title = "Command Palette",
- description = "Search for a command to run...",
- children,
- ...props
-}: Omit, "children"> & {
- title?: string;
- description?: string;
- children?: React.ReactNode;
-}) {
- return (
-
-
-
- {title}
- {description}
-
-
-
- {children}
-
-
-
-
- );
-}
-
-function CommandInput({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
-
- );
-}
-
-function CommandList({
- className,
- ...props
-}: React.ComponentProps) {
- return (
- e.stopPropagation()}
- onWheel={(e) => e.stopPropagation()}
- {...props}
- />
- );
-}
-
-function CommandEmpty({
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function CommandGroup({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function CommandSeparator({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function CommandItem({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function CommandShortcut({
- className,
- ...props
-}: React.ComponentProps<"span">) {
- return (
-
- );
-}
-
-export {
- Command,
- CommandDialog,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- CommandSeparator,
- CommandShortcut,
-};
diff --git a/apps/dashboard/components/ui/composables/chart.tsx b/apps/dashboard/components/ui/composables/chart.tsx
index f96a3abb6e..c8bbb00b65 100644
--- a/apps/dashboard/components/ui/composables/chart.tsx
+++ b/apps/dashboard/components/ui/composables/chart.tsx
@@ -69,8 +69,6 @@ import {
type EmptyStateProps,
} from "@databuddy/ui";
-// ── Tooltip primitives ──────────────────────────────────────────────────
-
interface TooltipEntry {
color: string;
formattedValue?: string;
@@ -245,13 +243,6 @@ function formatTooltipDate(dateStr: string): string {
}
return parsed.format("MMM D");
}
-
-// ── Chart types ─────────────────────────────────────────────────────────
-
-/**
- * Series key → color/label map (e.g. `buildChartConfig` in AI chart renderers).
- * Theme variant uses light/dark CSS color strings.
- */
export type ChartConfig = {
[k in string]: {
label?: ReactNode;
@@ -304,7 +295,6 @@ export function mergeChartInteractiveFeatures(
}
export interface RechartsSingleValueTooltipParams {
- /** Overrides default `formatTooltipDate` for the tooltip subtitle line. */
formatLabelAction?: (label: string) => string;
formatValue?: (value: number) => string;
valueSuffixLabel?: string;
@@ -367,8 +357,6 @@ function readTooltipNumericValue(
}
return null;
}
-
-/** Recharts ` ` for single-series charts (`ChartTooltip` + `formatTooltipDate`). */
export function createRechartsSingleValueTooltip(
params: RechartsSingleValueTooltipParams
) {
@@ -417,12 +405,10 @@ interface ChartSingleSeriesProps {
fallbackClassName?: string;
height: number;
id: string;
- /** Recharts margin; defaults to `Chart.zeroMargin`. */
margin?: { bottom?: number; left?: number; right?: number; top?: number };
partialLastSegment?: boolean;
seriesKind?: ChartSeriesKind;
tooltip?: RechartsSingleValueTooltipParams | false;
- /** Passed to `YAxis` `domain` (e.g. mini charts use `dataMin - 5` / `dataMax + 5`). */
yDomain?: [number | string, number | string];
}
@@ -552,24 +538,15 @@ interface ChartCartesianAreaProps {
dataKey: string;
dateKey?: string;
fallbackClassName?: string;
- /** Tooltip title line (formatted date/time). */
formatTooltipLabel: (label: string) => string;
height: number;
id: string;
margin?: { bottom?: number; left?: number; right?: number; top?: number };
showGrid?: boolean;
strokeWidth?: number;
- /** Legend row label in the tooltip (e.g. “Clicks”). */
valueLabel: string;
- /** X tick labels (e.g. dayjs). */
xTickFormatter: (value: string) => string;
}
-
-/**
- * Single-series area chart with visible axes, optional horizontal grid, and
- * `Chart.Tooltip` multi-row layout—use instead of hand-rolling `AreaChart` +
- * `CartesianGrid` + `XAxis` + `YAxis` for standard dashboard line/area pages.
- */
function ChartCartesianArea({
data,
dataKey,
@@ -662,14 +639,11 @@ export interface ChartMultiSeriesDataPoint {
}
interface ChartMultiSeriesProps {
- /** Grouped (default) or stacked bars; only applies when `seriesKind` is `bar`. */
barLayout?: "grouped" | "stacked";
- /** `stackId` for stacked bars (default `"stack"`). */
barStackId?: string;
curveType?: ChartCurveType;
data: ChartMultiSeriesDataPoint[];
height: number;
- /** When false (default), shows date ticks on the X axis. Mini charts often hide this. */
hideXAxis?: boolean;
metrics: Array;
partialLastSegment?: boolean;
@@ -938,8 +912,6 @@ interface ChartPlotProps {
children: ReactNode;
className?: string;
}
-
-/** Chart drawing region (e.g. dotted background + ResponsiveContainer). */
function ChartPlot({ children, className }: ChartPlotProps) {
return (
({
}
ChartRoot.displayName = "Chart";
-
-/**
- * Recharts primitives for custom charts. Prefer `Chart.SingleSeries` / `Chart.MultiSeries`
- * when the use case matches; use these for pie, brush, reference lines, dual axes, etc.
- * `Legend` here is Recharts’ legend; `Chart.Legend` is the dashboard metric pills.
- */
const chartRecharts = {
Area,
AreaChart,
diff --git a/apps/dashboard/components/ui/composables/list.tsx b/apps/dashboard/components/ui/composables/list.tsx
index 6a23713c79..50ba61e90b 100644
--- a/apps/dashboard/components/ui/composables/list.tsx
+++ b/apps/dashboard/components/ui/composables/list.tsx
@@ -135,17 +135,11 @@ function ListCell({
interface ListContentBaseProps
{
children: (items: T[]) => ReactNode;
- /** Shown when outcome is empty; overrides emptyProps */
empty?: ReactNode;
- /** Passed to EmptyState when outcome is empty (unless `empty` is set) */
emptyProps?: EmptyStateProps;
- /** Shown when outcome is error; overrides errorProps */
error?: ReactNode;
- /** Passed to EmptyState with variant `error` when outcome is error (unless `error` is set) */
errorProps?: EmptyStateProps;
- /** Shown when outcome is loading; defaults to List.DefaultLoading */
loading?: ReactNode;
- /** Wrapper for default EmptyState branches (not applied to custom `empty` / `error` nodes) */
stateWrapperClassName?: string;
}
diff --git a/apps/dashboard/components/ui/dialog.tsx b/apps/dashboard/components/ui/dialog.tsx
deleted file mode 100644
index 485ee171d9..0000000000
--- a/apps/dashboard/components/ui/dialog.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-"use client";
-
-import { XMarkIcon as XIcon } from "@databuddy/ui/icons";
-import { Dialog as DialogPrimitive } from "radix-ui";
-import type * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-function Dialog({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function DialogTrigger({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function DialogPortal({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function DialogClose({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function DialogOverlay({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function DialogContent({
- className,
- children,
- showCloseButton = true,
- ...props
-}: React.ComponentProps & {
- showCloseButton?: boolean;
-}) {
- return (
-
-
-
- {children}
- {showCloseButton && (
-
-
- Close
-
- )}
-
-
- );
-}
-
-function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function DialogTitle({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function DialogDescription({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-export {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogOverlay,
- DialogPortal,
- DialogTitle,
- DialogTrigger,
-};
diff --git a/apps/dashboard/components/ui/dotmatrix/core.tsx b/apps/dashboard/components/ui/dotmatrix/core.tsx
index 2edcf57981..bb1200665f 100644
--- a/apps/dashboard/components/ui/dotmatrix/core.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/core.tsx
@@ -4,7 +4,7 @@ import type { CSSProperties } from "react";
import { useDotMatrixPhases, usePrefersReducedMotion } from "./hooks";
import type { DotMatrixPhase } from "./types";
-export type { DotMatrixPhase } from "./types";
+;
export type MatrixPattern = "diamond" | "full" | "outline" | "rose" | "cross" | "rings";
@@ -28,7 +28,7 @@ export interface DotMatrixCommonProps {
minSize?: number;
}
-export interface DotAnimationContext {
+interface DotAnimationContext {
index: number;
row: number;
col: number;
@@ -41,7 +41,7 @@ export interface DotAnimationContext {
reducedMotion: boolean;
}
-export interface DotAnimationState {
+interface DotAnimationState {
className?: string;
style?: CSSProperties;
}
@@ -57,30 +57,30 @@ const CENTER = Math.floor(MATRIX_SIZE / 2);
const RANGE = Array.from({ length: MATRIX_SIZE }, (_, index) => index);
const MAX_RADIUS = Math.hypot(CENTER, CENTER);
-export const FULL_INDEXES = RANGE.flatMap((row) => RANGE.map((col) => rowMajorIndex(row, col)));
+const FULL_INDEXES = RANGE.flatMap((row) => RANGE.map((col) => rowMajorIndex(row, col)));
-export const DIAMOND_INDEXES = FULL_INDEXES.filter((index) => {
+const DIAMOND_INDEXES = FULL_INDEXES.filter((index) => {
const { row, col } = indexToCoord(index);
return Math.abs(row - CENTER) + Math.abs(col - CENTER) <= 2;
});
-export const OUTLINE_INDEXES = FULL_INDEXES.filter((index) => {
+const OUTLINE_INDEXES = FULL_INDEXES.filter((index) => {
const { row, col } = indexToCoord(index);
return row === 0 || row === MATRIX_SIZE - 1 || col === 0 || col === MATRIX_SIZE - 1;
});
-export const CROSS_INDEXES = FULL_INDEXES.filter((index) => {
+const CROSS_INDEXES = FULL_INDEXES.filter((index) => {
const { row, col } = indexToCoord(index);
return row === CENTER || col === CENTER;
});
-export const RINGS_INDEXES = FULL_INDEXES.filter((index) => {
+const RINGS_INDEXES = FULL_INDEXES.filter((index) => {
const { row, col } = indexToCoord(index);
const radius = Math.hypot(row - CENTER, col - CENTER);
return Math.round(radius) === 1 || Math.round(radius) === 2;
});
-export const ROSE_INDEXES = FULL_INDEXES.filter((index) => {
+const ROSE_INDEXES = FULL_INDEXES.filter((index) => {
const { row, col } = indexToCoord(index);
const dx = col - CENTER;
const dy = row - CENTER;
@@ -99,7 +99,7 @@ const PATTERN_INDEXES: Record = {
rings: RINGS_INDEXES
};
-export function getPatternIndexes(pattern: MatrixPattern = "diamond"): number[] {
+function getPatternIndexes(pattern: MatrixPattern = "diamond"): number[] {
return PATTERN_INDEXES[pattern];
}
@@ -107,43 +107,43 @@ export function rowMajorIndex(row: number, col: number): number {
return row * MATRIX_SIZE + col;
}
-export function indexToCoord(index: number): { row: number; col: number } {
+function indexToCoord(index: number): { row: number; col: number } {
return {
row: Math.floor(index / MATRIX_SIZE),
col: index % MATRIX_SIZE
};
}
-export function distanceFromCenter(index: number): number {
+function distanceFromCenter(index: number): number {
const { row, col } = indexToCoord(index);
return Math.hypot(row - CENTER, col - CENTER);
}
-export function rowDistance(index: number): number {
+function rowDistance(index: number): number {
const { row } = indexToCoord(index);
return Math.abs(row - CENTER);
}
-export function polarAngle(index: number): number {
+function polarAngle(index: number): number {
const { row, col } = indexToCoord(index);
return Math.atan2(row - CENTER, col - CENTER);
}
-export function normalizedRadius(index: number): number {
+function normalizedRadius(index: number): number {
const { row, col } = indexToCoord(index);
return Math.hypot(row - CENTER, col - CENTER) / MAX_RADIUS;
}
-export function manhattanDistance(index: number): number {
+function manhattanDistance(index: number): number {
const { row, col } = indexToCoord(index);
return Math.abs(row - CENTER) + Math.abs(col - CENTER);
}
-export function harmonicPhase(row: number, col: number, a: number, b: number): number {
+function harmonicPhase(row: number, col: number, a: number, b: number): number {
return Math.sin((row + 1) * a + (col + 1) * b);
}
-export function lissajousOffset(
+function lissajousOffset(
row: number,
col: number,
amplitude = 2.25
@@ -154,7 +154,7 @@ export function lissajousOffset(
return { x, y, phase };
}
-export function spiralOffset(
+function spiralOffset(
angle: number,
radiusNormalizedValue: number,
amplitude = 2.8
@@ -167,7 +167,7 @@ export function spiralOffset(
return { x, y, phase };
}
-export function isPrime(value: number): boolean {
+function isPrime(value: number): boolean {
if (value <= 1) {
return false;
}
@@ -220,11 +220,11 @@ function buildSnakeOrderToIndexMap(): number[] {
const SNAKE_ORDER: readonly number[] = buildSnakeOrderToIndexMap();
-export function snakePathNormFromIndex(index: number): number {
+function snakePathNormFromIndex(index: number): number {
return SNAKE_ORDER[index]! / (CELLS - 1);
}
-export function snakePathOrderValue(index: number): number {
+function snakePathOrderValue(index: number): number {
return SNAKE_ORDER[index]!;
}
@@ -419,20 +419,20 @@ function buildRowWaveSnakeOrderToIndexMap(): number[] {
const ROW_WAVE_SNAKE_ORDER: readonly number[] = buildRowWaveSnakeOrderToIndexMap();
const ROW_WAVE_SNAKE_MAX_ORDER = Math.max(...ROW_WAVE_SNAKE_ORDER);
-export function rowWaveOrderValue(index: number): number {
+function rowWaveOrderValue(index: number): number {
return ROW_WAVE_SNAKE_ORDER[index]!;
}
-export function rowWaveNormFromIndex(index: number): number {
+function rowWaveNormFromIndex(index: number): number {
return ROW_WAVE_SNAKE_MAX_ORDER > 0 ? rowWaveOrderValue(index) / ROW_WAVE_SNAKE_MAX_ORDER : 0;
}
-export function colWaveNormFromIndex(index: number): number {
+function colWaveNormFromIndex(index: number): number {
const { col } = indexToCoord(index);
return N > 1 ? col / (N - 1) : 0;
}
-export function concentricRingNormFromIndex(index: number): number {
+function concentricRingNormFromIndex(index: number): number {
const { row, col } = indexToCoord(index);
return Math.max(Math.abs(row - C), Math.abs(col - C)) / C;
}
@@ -752,7 +752,7 @@ export function DotMatrixBase({
type NormFn = (ctx: Pick) => number;
-export function createPathWaveResolver(getPathNorm: NormFn): DotAnimationResolver {
+function createPathWaveResolver(getPathNorm: NormFn): DotAnimationResolver {
return ({ isActive, row, col, index, reducedMotion, phase }) => {
if (!isActive) {
return { className: "dmx-inactive" };
@@ -776,7 +776,7 @@ export function createPathWaveResolver(getPathNorm: NormFn): DotAnimationResolve
type PathWaveComponentProps = DotMatrixCommonProps;
-export function createPathWaveComponent(displayName: string, getPathNorm: NormFn) {
+function createPathWaveComponent(displayName: string, getPathNorm: NormFn) {
const resolve = createPathWaveResolver(getPathNorm);
function PathWaveComponent({
diff --git a/apps/dashboard/components/ui/dotmatrix/index.ts b/apps/dashboard/components/ui/dotmatrix/index.ts
index bac705ca2d..9bd60770bc 100644
--- a/apps/dashboard/components/ui/dotmatrix/index.ts
+++ b/apps/dashboard/components/ui/dotmatrix/index.ts
@@ -1,21 +1,4 @@
-export { DotMatrixBase } from "./core";
-export type {
- DotAnimationContext,
- DotAnimationResolver,
- DotAnimationState,
- DotMatrixCommonProps,
-} from "./core";
-
-export {
- useCyclePhase,
- useDotMatrixPhases,
- usePrefersReducedMotion,
- useSteppedCycle,
-} from "./hooks";
-
export {
DotMatrixLoader,
useRandomDotMatrixLoader,
- DOT_MATRIX_LOADER_NAMES,
} from "./loader";
-export type { DotMatrixLoaderName, DotMatrixLoaderProps } from "./loader";
diff --git a/apps/dashboard/components/ui/dotmatrix/loader.tsx b/apps/dashboard/components/ui/dotmatrix/loader.tsx
index 82f1c22041..41360d7390 100644
--- a/apps/dashboard/components/ui/dotmatrix/loader.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/loader.tsx
@@ -27,7 +27,7 @@ function capitalize(s: string) {
return s[0]!.toUpperCase() + s.slice(1);
}
-export const DOT_MATRIX_LOADER_NAMES = SHAPES.flatMap((shape) =>
+const DOT_MATRIX_LOADER_NAMES = SHAPES.flatMap((shape) =>
Array.from({ length: VARIANT_COUNT }, (_, i) => buildLoaderName(shape, i + 1))
);
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/1.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/1.tsx
index 6f1b4864a8..1f087dfc88 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/1.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/1.tsx
@@ -9,7 +9,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular1Props = DotMatrixCommonProps;
+type DotmCircular1Props = DotMatrixCommonProps;
const BASE_OPACITY = 0.08;
const STRAND_OPACITY = 1;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/10.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/10.tsx
index 1926213146..6725b2b664 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/10.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/10.tsx
@@ -9,7 +9,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular10Props = DotMatrixCommonProps;
+type DotmCircular10Props = DotMatrixCommonProps;
const STEP_COUNT = 30;
const BASE_OPACITY = 0.06;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/11.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/11.tsx
index 31c3f3111e..bb5e019fa6 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/11.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/11.tsx
@@ -9,7 +9,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular11Props = DotMatrixCommonProps;
+type DotmCircular11Props = DotMatrixCommonProps;
const BASE_OPACITY = 0.07;
const MID_OPACITY = 0.3;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/12.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/12.tsx
index ec009a3b43..70732789b4 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/12.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/12.tsx
@@ -9,7 +9,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular12Props = DotMatrixCommonProps;
+type DotmCircular12Props = DotMatrixCommonProps;
const STEP_COUNT = 36;
const BASE_OPACITY = 0.06;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/13.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/13.tsx
index 1e9a47c97e..594cacc3de 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/13.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/13.tsx
@@ -9,7 +9,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular13Props = DotMatrixCommonProps;
+type DotmCircular13Props = DotMatrixCommonProps;
const STEP_COUNT = 28;
const BASE_OPACITY = 0.07;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/14.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/14.tsx
index ce53091c41..2996e62537 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/14.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/14.tsx
@@ -9,7 +9,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular14Props = DotMatrixCommonProps;
+type DotmCircular14Props = DotMatrixCommonProps;
const STEP_COUNT = 30;
const BASE_OPACITY = 0.07;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/15.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/15.tsx
index 6725c73254..15f4e4ac8b 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/15.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/15.tsx
@@ -9,7 +9,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular15Props = DotMatrixCommonProps;
+type DotmCircular15Props = DotMatrixCommonProps;
const STEP_COUNT = 24;
const BASE_OPACITY = 0.07;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/16.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/16.tsx
index 425e74354d..fd9c913c29 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/16.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/16.tsx
@@ -9,7 +9,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular16Props = DotMatrixCommonProps;
+type DotmCircular16Props = DotMatrixCommonProps;
const STEP_COUNT = 25;
const BASE_OPACITY = 0.07;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/17.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/17.tsx
index 5920fc676a..e91bc7c079 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/17.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/17.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useMemo, useRef } from "react";
+import { useEffect, useMemo, useRef } from "react";
import { DotMatrixBase } from "../../core";
import { useDotMatrixPhases } from "../../hooks";
@@ -9,12 +9,11 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular17Props = DotMatrixCommonProps;
+type DotmCircular17Props = DotMatrixCommonProps;
const BASE_OPACITY = 0.07;
const MID_OPACITY = 0.34;
const HIGH_OPACITY = 0.95;
-/** Discrete checker frames per loop (must stay integer for `(row + col + t) % 2`). */
const CHECKER_STEPS = 4;
export function DotmCircular17({
@@ -36,7 +35,9 @@ export function DotmCircular17({
});
const animPhaseRef = useRef(animPhase);
- animPhaseRef.current = animPhase;
+ useEffect(() => {
+ animPhaseRef.current = animPhase;
+ }, [animPhase]);
const resolver = useMemo(() => {
return ({ row, col, phase: dmxPhase }) => {
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/18.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/18.tsx
index 7f6c76ac7a..d5f7645a35 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/18.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/18.tsx
@@ -9,7 +9,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular18Props = DotMatrixCommonProps;
+type DotmCircular18Props = DotMatrixCommonProps;
const BASE_OPACITY = 0.07;
const MID_OPACITY = 0.33;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/19.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/19.tsx
index e77d9fb269..280e52ea64 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/19.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/19.tsx
@@ -9,7 +9,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular19Props = DotMatrixCommonProps;
+type DotmCircular19Props = DotMatrixCommonProps;
const STEP_COUNT = 24;
const BASE_OPACITY = 0.07;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/2.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/2.tsx
index 5dd57543ed..1885ec497c 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/2.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/2.tsx
@@ -9,7 +9,7 @@ import { rowMajorIndex } from "../../core";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular2Props = DotMatrixCommonProps;
+type DotmCircular2Props = DotMatrixCommonProps;
const RING_PATH: readonly number[] = [
rowMajorIndex(0, 1),
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/20.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/20.tsx
index 5ccc364de8..cdda774ced 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/20.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/20.tsx
@@ -9,7 +9,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular20Props = DotMatrixCommonProps;
+type DotmCircular20Props = DotMatrixCommonProps;
const STEP_COUNT = 30;
const BASE_OPACITY = 0.07;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/3.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/3.tsx
index 1968575457..7bd7f30e60 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/3.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/3.tsx
@@ -10,7 +10,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular3Props = DotMatrixCommonProps;
+type DotmCircular3Props = DotMatrixCommonProps;
const STEP_COUNT = 24;
const BASE_OPACITY = 0.08;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/4.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/4.tsx
index dafa92e4ff..562b90c30a 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/4.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/4.tsx
@@ -9,7 +9,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular4Props = DotMatrixCommonProps;
+type DotmCircular4Props = DotMatrixCommonProps;
const BASE_OPACITY = 0.08;
const SWEEP_OPACITY = 0.96;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/5.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/5.tsx
index 092b943501..f18f959eaf 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/5.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/5.tsx
@@ -9,7 +9,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular5Props = DotMatrixCommonProps;
+type DotmCircular5Props = DotMatrixCommonProps;
const BASE_OPACITY = 0.08;
const BLADE_OPACITY = 0.94;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/6.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/6.tsx
index 0cbc56bccd..702041e12d 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/6.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/6.tsx
@@ -9,7 +9,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular6Props = DotMatrixCommonProps;
+type DotmCircular6Props = DotMatrixCommonProps;
const BASE_OPACITY = 0.08;
const ORBIT_OPACITY = 0.96;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/7.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/7.tsx
index 8575d1de39..921ffc166e 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/7.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/7.tsx
@@ -9,7 +9,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular7Props = DotMatrixCommonProps;
+type DotmCircular7Props = DotMatrixCommonProps;
const BASE_OPACITY = 0.08;
const GATE_OPACITY = 0.92;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/8.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/8.tsx
index 6e01efc881..7948416c25 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/8.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/8.tsx
@@ -9,7 +9,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular8Props = DotMatrixCommonProps;
+type DotmCircular8Props = DotMatrixCommonProps;
const BASE_OPACITY = 0.08;
const PULSE_CORE = 0.95;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/circular/9.tsx b/apps/dashboard/components/ui/dotmatrix/variants/circular/9.tsx
index 2b510b96f7..8a358f024b 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/circular/9.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/circular/9.tsx
@@ -9,7 +9,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmCircular9Props = DotMatrixCommonProps;
+type DotmCircular9Props = DotMatrixCommonProps;
const STEP_COUNT = 36;
const BASE_OPACITY = 0.07;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/1.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/1.tsx
index cbed3f4dca..ac0d1c0e90 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/1.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/1.tsx
@@ -9,7 +9,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver } from "../../core";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmSquare1Props = DotMatrixCommonProps;
+type DotmSquare1Props = DotMatrixCommonProps;
const animationResolver: DotAnimationResolver = ({ isActive, index, row, col, reducedMotion, phase }) => {
if (!isActive) {
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/10.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/10.tsx
index ff095367a8..6b3830b50d 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/10.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/10.tsx
@@ -9,7 +9,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare10Props = DotMatrixCommonProps;
+type DotmSquare10Props = DotMatrixCommonProps;
const ROWS = MATRIX_SIZE;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/11.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/11.tsx
index fa1b460b97..505279c17b 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/11.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/11.tsx
@@ -7,7 +7,7 @@ import { useDotMatrixPhases } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare11Props = DotMatrixCommonProps;
+type DotmSquare11Props = DotMatrixCommonProps;
const animationResolver: DotAnimationResolver = ({ isActive, manhattanDistance, reducedMotion, phase }) => {
if (!isActive) {
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/12.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/12.tsx
index f102fcd118..874ecf48f0 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/12.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/12.tsx
@@ -7,7 +7,7 @@ import { useDotMatrixPhases } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare12Props = DotMatrixCommonProps;
+type DotmSquare12Props = DotMatrixCommonProps;
// User-defined origin is cell (2,2) in a 1-based 5x5 grid => (row=1,col=1) in zero-based coords.
const ORIGIN_ROW = 1;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/13.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/13.tsx
index 9343e7a5f7..17b4e2bc0e 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/13.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/13.tsx
@@ -9,7 +9,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare13Props = DotMatrixCommonProps;
+type DotmSquare13Props = DotMatrixCommonProps;
type FrameCell = "." | "o" | "x";
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/14.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/14.tsx
index 7060be32f9..5ac542286d 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/14.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/14.tsx
@@ -9,7 +9,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare14Props = DotMatrixCommonProps;
+type DotmSquare14Props = DotMatrixCommonProps;
type FrameCell = "." | "o" | "x";
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/15.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/15.tsx
index c25e1b6bc6..7eb3ab57f7 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/15.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/15.tsx
@@ -8,13 +8,12 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare15Props = DotMatrixCommonProps;
+type DotmSquare15Props = DotMatrixCommonProps;
const BASE_OPACITY = 0.08;
const STRAND_OPACITY = 1;
const BRIDGE_OPACITY = 0.58;
const NEAR_STRAND_OPACITY = 0.24;
-/** Integer full sin periods per matrix cycle so phase 0 ≡ phase 1 (no wrap glitch). */
const STRAND_LOOPS = 2;
export function DotmSquare15({
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/16.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/16.tsx
index 7791d7d9bf..ae50b94d02 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/16.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/16.tsx
@@ -8,7 +8,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare16Props = DotMatrixCommonProps;
+type DotmSquare16Props = DotMatrixCommonProps;
const BASE_OPACITY = 0.08;
const STRAND_OPACITY = 1;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/17.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/17.tsx
index 95293ca76d..0d40a2934c 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/17.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/17.tsx
@@ -8,7 +8,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare17Props = DotMatrixCommonProps;
+type DotmSquare17Props = DotMatrixCommonProps;
const BASE_OPACITY = 0.08;
const STRAND_OPACITY = 1;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/18.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/18.tsx
index eee1ebcf80..ab7e70a5fe 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/18.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/18.tsx
@@ -8,7 +8,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare18Props = DotMatrixCommonProps;
+type DotmSquare18Props = DotMatrixCommonProps;
const BASE_OPACITY = 0.08;
const LIT_OPACITY = 0.94;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/19.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/19.tsx
index 35e4739750..b3a6e3d0c3 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/19.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/19.tsx
@@ -8,7 +8,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare19Props = DotMatrixCommonProps;
+type DotmSquare19Props = DotMatrixCommonProps;
const STEP_COUNT = 48;
const BASE_OPACITY = 0.08;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/2.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/2.tsx
index db70ef8ba7..1b4c4a6cab 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/2.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/2.tsx
@@ -9,7 +9,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare2Props = DotMatrixCommonProps;
+type DotmSquare2Props = DotMatrixCommonProps;
const SNAKE_TAIL = [1, 0.82, 0.68, 0.54, 0.42, 0.31, 0.22, 0.14] as const;
const BASE_OPACITY = 0.08;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/20.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/20.tsx
index a0741f2cd3..d860dd5c99 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/20.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/20.tsx
@@ -9,9 +9,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare20Props = DotMatrixCommonProps;
-
-/** Clockwise perimeter: one closed loop you can trace with your eye. */
+type DotmSquare20Props = DotMatrixCommonProps;
const PERIMETER_PATH: readonly number[] = [
rowMajorIndex(0, 0),
rowMajorIndex(0, 1),
@@ -39,8 +37,6 @@ const BASE_OPACITY = 0.08;
const TWIST_INNER_OPACITY = 0.52;
const SEAM_PULSE_OPACITY = 0.55;
const IDLE_RING_OPACITY = 0.48;
-
-/** Corner steps on the loop → one cell “inside” the strip at the fold (half-twist cue). */
const TWIST_INNER_BY_HEAD_STEP: ReadonlyMap = new Map([
[0, rowMajorIndex(1, 1)],
[4, rowMajorIndex(1, 3)],
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/3.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/3.tsx
index 70b570eecd..656a5a2fe9 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/3.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/3.tsx
@@ -8,7 +8,7 @@ import { spiralInwardNormFromIndex, spiralInwardOrderValue } from "../../core";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare3Props = DotMatrixCommonProps;
+type DotmSquare3Props = DotMatrixCommonProps;
const animationResolver: DotAnimationResolver = ({ isActive, index, reducedMotion, phase }) => {
if (!isActive) {
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/4.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/4.tsx
index 59c26df05e..adb34797e2 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/4.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/4.tsx
@@ -13,7 +13,7 @@ import {
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare4Props = DotMatrixCommonProps;
+type DotmSquare4Props = DotMatrixCommonProps;
const animationResolver: DotAnimationResolver = ({ isActive, index, row, col, reducedMotion, phase }) => {
if (!isActive) {
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/5.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/5.tsx
index b56b5bdae8..6cc7f792b7 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/5.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/5.tsx
@@ -8,7 +8,7 @@ import { diagonalSnakeNormFromIndex, diagonalSnakeOrderValue } from "../../core"
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare5Props = DotMatrixCommonProps;
+type DotmSquare5Props = DotMatrixCommonProps;
const animationResolver: DotAnimationResolver = ({ isActive, index, reducedMotion, phase }) => {
if (!isActive) {
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/6.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/6.tsx
index 34e219dae8..7d5d9cded8 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/6.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/6.tsx
@@ -7,7 +7,7 @@ import { useDotMatrixPhases } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare6Props = DotMatrixCommonProps;
+type DotmSquare6Props = DotMatrixCommonProps;
const COLUMN_HEIGHT = 5;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/7.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/7.tsx
index b3c756229a..a2d859f1a2 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/7.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/7.tsx
@@ -9,7 +9,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare7Props = DotMatrixCommonProps;
+type DotmSquare7Props = DotMatrixCommonProps;
type FrameCell = "." | "o" | "x" | "c";
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/8.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/8.tsx
index 79298ba004..27717a2b5b 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/8.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/8.tsx
@@ -9,20 +9,16 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare8Props = DotMatrixCommonProps;
+type DotmSquare8Props = DotMatrixCommonProps;
const ROWS = MATRIX_SIZE;
const COLS = MATRIX_SIZE;
-
-/** Steps 0..FILL_LAST: column `c` gains one row from the bottom each tick, delayed by `c` (col 0 full at `ROWS`, last col at `ROWS + COLS - 1`). */
const FILL_LAST = ROWS + COLS - 1;
const BLINK_STEPS = 4;
const BLINK_OPACITIES = [0.38, 1, 0.38, 1] as const;
const DRAIN_LAST = FILL_LAST;
-
-/** fillTick 0..FILL_LAST → drainTick 0..DRAIN_LAST → + blink in between */
const SEQUENCE_LEN = FILL_LAST + 1 + BLINK_STEPS + DRAIN_LAST + 1;
const BASE_OPACITY = 0.08;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/square/9.tsx b/apps/dashboard/components/ui/dotmatrix/variants/square/9.tsx
index c423cf5508..8295d030fe 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/square/9.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/square/9.tsx
@@ -7,22 +7,13 @@ import { useDotMatrixPhases } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotAnimationResolver, DotMatrixCommonProps } from "../../core";
-export type DotmSquare9Props = DotMatrixCommonProps;
-
-/**
- * Dots 1–6 in Unicode / ISO braille numbering (matches U+2800 + mask):
- * 1·4
- * 2·5
- * 3·6
- */
+type DotmSquare9Props = DotMatrixCommonProps;
const D1 = 0x01;
const D2 = 0x02;
const D3 = 0x04;
const D4 = 0x08;
const D5 = 0x10;
const D6 = 0x20;
-
-/** Left column “odd” / right column “even” — classic 2×3 checkerboard. */
const CHECK_A = D1 | D3 | D5;
const BASE_OPACITY = 0.08;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/1.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/1.tsx
index beab9ed346..e7d296e642 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/1.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/1.tsx
@@ -10,7 +10,7 @@ import { remapOpacityToTriplet } from "../../core";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle1Props = DotMatrixCommonProps;
+type DotmTriangle1Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
const STEP_COUNT = 30;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/10.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/10.tsx
index 589f825897..cae018583d 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/10.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/10.tsx
@@ -10,7 +10,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle10Props = DotMatrixCommonProps;
+type DotmTriangle10Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
const STEP_COUNT = 36;
@@ -29,8 +29,6 @@ const TRIANGLE_CELLS = new Set([
"4,4",
"4,6"
]);
-
-/** Bottom-to-top within each column, columns 0→6 — only triangle cells appear in the path. */
const COLUMN_RAKE_PATH: ReadonlyArray = [
[4, 0],
[3, 1],
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/11.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/11.tsx
index 22b213ebb2..7eb080a9d6 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/11.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/11.tsx
@@ -10,7 +10,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle11Props = DotMatrixCommonProps;
+type DotmTriangle11Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
@@ -53,11 +53,6 @@ function smoothstep01(edge0: number, edge1: number, x: number): number {
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
return t * t * (3 - 2 * t);
}
-
-/**
- * Bright bands move down the triangle by tier: phase keys on Manhattan distance from the apex,
- * not the heart cell — reads as stacked horizontal “shelves” lighting in sequence.
- */
function opacityForCell(row: number, col: number, phase: number): number {
const tier = manhattanFromApex(row, col);
const maxTier = 6;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/12.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/12.tsx
index 1cb997846f..a25b838755 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/12.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/12.tsx
@@ -10,7 +10,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle12Props = DotMatrixCommonProps;
+type DotmTriangle12Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
@@ -46,11 +46,6 @@ function smoothstep01(edge0: number, edge1: number, x: number): number {
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
return t * t * (3 - 2 * t);
}
-
-/**
- * Anti-diagonal harmonics on `row - col`: bands glide along NE–SW lines through the mask,
- * opposite oblique motion to loaders keyed on `row + col`.
- */
function opacityForCell(row: number, col: number, phase: number): number {
const skew = row - col;
const t = phase * Math.PI * 2;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/13.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/13.tsx
index a72c74541e..001912bc0b 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/13.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/13.tsx
@@ -10,7 +10,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle13Props = DotMatrixCommonProps;
+type DotmTriangle13Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
const BASE_OPACITY = 0.13;
@@ -28,8 +28,6 @@ const TRIANGLE_CELLS = new Set([
"4,4",
"4,6"
]);
-
-/** Row serpent: base row left→right, row 3 right→left, mid rows alternate — reads as a zigzag zip. */
const SERPENT_PATH: ReadonlyArray = [
[4, 0],
[4, 2],
@@ -44,7 +42,6 @@ const SERPENT_PATH: ReadonlyArray = [
];
const PATH_LEN = SERPENT_PATH.length;
-/** Soft tail length in path units (Braille-style ramp, not discrete steps). */
const TRAIL_SPAN = 4.25;
function isWithinTriangleMask(row: number, col: number): boolean {
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/14.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/14.tsx
index ef7e34e5ee..8ba6b5d596 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/14.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/14.tsx
@@ -10,7 +10,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle14Props = DotMatrixCommonProps;
+type DotmTriangle14Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
@@ -46,11 +46,6 @@ function smoothstep01(edge0: number, edge1: number, x: number): number {
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
return t * t * (3 - 2 * t);
}
-
-/**
- * A soft vertical “pillar” of brightness sweeps column 0→6; only masked dots respond,
- * so the triangle appears to light one vertical slice at a time (not a cell path).
- */
function opacityForCell(row: number, col: number, phase: number): number {
const beamCenter = phase * 7.2 - 0.35;
const dist = Math.abs(col - beamCenter);
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/15.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/15.tsx
index fcd3478e9a..1d861fcb7a 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/15.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/15.tsx
@@ -10,7 +10,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle15Props = DotMatrixCommonProps;
+type DotmTriangle15Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
@@ -30,8 +30,6 @@ const TRIANGLE_CELLS = new Set([
"4,4",
"4,6"
]);
-
-/** Apex and the two base corners — the three natural vertices of the silhouette. */
const HUBS: ReadonlyArray = [
[1, 3],
[4, 0],
@@ -62,11 +60,6 @@ function falloffFromHub(row: number, col: number, hub: readonly [number, number]
const d = manhattan(row, col, hub[0], hub[1]);
return 1 - smoothstep01(0, 5.4, d);
}
-
-/**
- * Energy orbits the three triangle vertices (apex → left base → right base) on a continuous phase,
- * with soft Manhattan falloff — no lattice mod groups.
- */
function opacityForCell(row: number, col: number, phase: number): number {
const t = phase * Math.PI * 2;
const sharp = 4;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/16.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/16.tsx
index 009e1c8eab..ace236a1b2 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/16.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/16.tsx
@@ -10,17 +10,13 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle16Props = DotMatrixCommonProps;
+type DotmTriangle16Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
const BASE_OPACITY = 0.1;
const MID_OPACITY = 0.36;
const HIGH_OPACITY = 0.96;
-/**
- * Inverted-V coordinate: same row is lower on the left/right flanks than in the center column,
- * so a moving front forms a V rising toward the apex (not a flat row band like Row Sweep).
- */
const WING = 0.52;
const FRONT_SIGMA = 0.88;
@@ -52,12 +48,6 @@ function smoothstep01(edge0: number, edge1: number, x: number): number {
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
return t * t * (3 - 2 * t);
}
-
-/**
- * Brightness peaks along a V-shaped isopleth: `row - wing * |col - 3|`.
- * The "front" oscillates in that space, so the highlight rides up the two lower legs
- * and meets at the top — convective lift, not a horizontal scanline.
- */
function opacityForCell(row: number, col: number, phase: number): number {
const t = phase * Math.PI * 2;
const v = row - WING * Math.abs(col - 3);
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/17.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/17.tsx
index 448da15cbc..75b5c292e3 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/17.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/17.tsx
@@ -10,7 +10,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle17Props = DotMatrixCommonProps;
+type DotmTriangle17Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
const BASE_OPACITY = 0.06;
@@ -28,11 +28,6 @@ const TRIANGLE_CELLS = new Set([
"4,4",
"4,6"
]);
-
-/**
- * Visits every triangle cell once per lap: up the left rim to the apex, down the right rim,
- * then cuts through (4,4) → center → (4,2) — reads as a crossing “∞” on the silhouette.
- */
const INFINITY_PATH: ReadonlyArray = [
[4, 0],
[3, 1],
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/18.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/18.tsx
index a58d65fddf..a02f1f9eb6 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/18.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/18.tsx
@@ -10,7 +10,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle18Props = DotMatrixCommonProps;
+type DotmTriangle18Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
@@ -46,11 +46,6 @@ function smoothstep01(edge0: number, edge1: number, x: number): number {
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
return t * t * (3 - 2 * t);
}
-
-/**
- * Heart cell stays dim while the outer shell breathes in sync — inverted emphasis vs center-led
- * corona loaders.
- */
function opacityForCell(row: number, col: number, phase: number): number {
if (row === 3 && col === 3) {
return CORE_DIM;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/19.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/19.tsx
index 9ba123dffa..a277df6067 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/19.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/19.tsx
@@ -10,7 +10,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle19Props = DotMatrixCommonProps;
+type DotmTriangle19Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
@@ -33,8 +33,6 @@ const TRIANGLE_CELLS = new Set([
"4,4",
"4,6"
]);
-
-/** Wider wedge core (radians) for smoother rotation like Braille ramps. */
const BEAM_SIGMA = 0.58;
function isWithinTriangleMask(row: number, col: number): boolean {
@@ -63,11 +61,6 @@ function smoothstep01(edge0: number, edge1: number, x: number): number {
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
return t * t * (3 - 2 * t);
}
-
-/**
- * A soft **rotating wedge** from the heart cell: brightness peaks where polar angle matches the
- * spinning phase — reads as a searchlight pivot, not a cosine product field.
- */
function opacityForCell(row: number, col: number, phase: number): number {
if (row === CENTER_ROW && col === CENTER_COL) {
const hub = 0.5 + 0.5 * Math.sin(phase * Math.PI * 2);
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/2.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/2.tsx
index 50e1e3546b..0fc2b702f8 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/2.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/2.tsx
@@ -10,7 +10,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle2Props = DotMatrixCommonProps;
+type DotmTriangle2Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
const STEP_COUNT = 36;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/20.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/20.tsx
index 2879dfcb76..13b05b37dc 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/20.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/20.tsx
@@ -10,7 +10,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle20Props = DotMatrixCommonProps;
+type DotmTriangle20Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
const BASE_OPACITY = 0.08;
@@ -29,8 +29,6 @@ const TRIANGLE_CELLS = new Set([
"4,4",
"4,6"
]);
-
-/** Same perimeter ring as DotmTriangle1 — center is not on this loop. */
const PERIMETER_PATH: ReadonlyArray = [
[1, 3],
[2, 2],
@@ -89,10 +87,6 @@ function glowAlongPath(s: number, idx: number | null, L: number): number {
const g = 1 - smoothstep01(0, TRAIL_SPAN, d);
return BASE_OPACITY + g * (HIGH_OPACITY - BASE_OPACITY);
}
-
-/**
- * Two heads chase the perimeter **half a lap apart**, each with its own soft tail — center stays dim.
- */
function opacityForCell(row: number, col: number, phase: number): number {
if (row === 3 && col === 3) {
return CENTER_DIM;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/3.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/3.tsx
index 51b678d735..6155614513 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/3.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/3.tsx
@@ -10,7 +10,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle3Props = DotMatrixCommonProps;
+type DotmTriangle3Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
const STEP_COUNT = 36;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/4.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/4.tsx
index 4819f0328c..048d2125a4 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/4.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/4.tsx
@@ -10,7 +10,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle4Props = DotMatrixCommonProps;
+type DotmTriangle4Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
const STEP_COUNT = 28;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/5.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/5.tsx
index a66547b584..04abe83e6b 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/5.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/5.tsx
@@ -10,7 +10,7 @@ import { usePrefersReducedMotion } from "../../hooks";
import { useSteppedCycle } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle5Props = DotMatrixCommonProps;
+type DotmTriangle5Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
const STEP_COUNT = 42;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/6.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/6.tsx
index 809d7b278c..6e44cc41ff 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/6.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/6.tsx
@@ -10,11 +10,9 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle6Props = DotMatrixCommonProps;
+type DotmTriangle6Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
-
-/** Unicode / ISO braille dot numbering (same as `DotmSquare9`). */
const D1 = 0x01;
const D2 = 0x02;
const D3 = 0x04;
@@ -25,11 +23,7 @@ const D6 = 0x20;
const LOW_OPACITY = 0.07;
const MID_OPACITY = 0.36;
const HIGH_OPACITY = 0.96;
-
-/** Half-width of the traveling ramp (larger = softer, more “gradient” overlap). */
const WAVE_HALF = 0.82;
-
-/** Phase splits (must sum to 1): smooth intro wave, blink, fade reset. */
const INTRO_PHASE = 0.52;
const BLINK_PHASE = 0.36;
const RESET_PHASE = 0.12;
@@ -54,8 +48,6 @@ function smoothstep01(edge0: number, edge1: number, x: number): number {
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
return t * t * (3 - 2 * t);
}
-
-/** Six fills (D1..D6 order) from a single traveling wave front. */
function waveFills(introT: number): number[] {
const waveCenter = -WAVE_HALF + introT * (5 + 2 * WAVE_HALF);
return [0, 1, 2, 3, 4, 5].map((i) =>
@@ -70,8 +62,6 @@ function isWithinTriangleMask(row: number, col: number): boolean {
return TRIANGLE_CELLS.has(`${row},${col}`);
}
-
-/** Map triangle cell → braille bit (ISO 2×3), or null for accent cells. */
function brailleBitForTriangle(row: number, col: number): number | null {
if (row === 2 && col === 2) {
return D1;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/7.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/7.tsx
index 4c3600e353..5da47621e9 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/7.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/7.tsx
@@ -10,7 +10,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle7Props = DotMatrixCommonProps;
+type DotmTriangle7Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
@@ -38,8 +38,6 @@ function isWithinTriangleMask(row: number, col: number): boolean {
return TRIANGLE_CELLS.has(`${row},${col}`);
}
-
-/** Sliding diagonal bands: same `row + col` share a phase so stripes read as continuous diagonals. */
function opacityForCell(row: number, col: number, phase: number): number {
const diag = row + col;
const t = phase * Math.PI * 2;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/8.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/8.tsx
index 2350acd6ff..bc65ee9d9e 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/8.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/8.tsx
@@ -10,7 +10,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle8Props = DotMatrixCommonProps;
+type DotmTriangle8Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
@@ -60,11 +60,6 @@ function sectorForCell(row: number, col: number): Sector {
}
return "none";
}
-
-/**
- * Alternating emphasis on the two lower wings (split by the apex column), with the apex and
- * heart dot brightest when energy crosses the middle (both sides briefly equal).
- */
function opacityForCell(row: number, col: number, phase: number): number {
const p = 0.5 - 0.5 * Math.cos(phase * Math.PI * 2);
const leftLift = p * p;
diff --git a/apps/dashboard/components/ui/dotmatrix/variants/triangle/9.tsx b/apps/dashboard/components/ui/dotmatrix/variants/triangle/9.tsx
index 2d5da38f16..62590cebc1 100644
--- a/apps/dashboard/components/ui/dotmatrix/variants/triangle/9.tsx
+++ b/apps/dashboard/components/ui/dotmatrix/variants/triangle/9.tsx
@@ -10,7 +10,7 @@ import { useCyclePhase } from "../../hooks";
import { usePrefersReducedMotion } from "../../hooks";
import type { DotMatrixCommonProps } from "../../core";
-export type DotmTriangle9Props = DotMatrixCommonProps;
+type DotmTriangle9Props = DotMatrixCommonProps;
const MATRIX_SIZE = 7;
@@ -89,11 +89,6 @@ function smoothstep01(edge0: number, edge1: number, x: number): number {
const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
return t * t * (3 - 2 * t);
}
-
-/**
- * Concentric tiers from the heart (8-connected). One soft bright band travels outward/inward;
- * smoothstep softens the cosine so ring-to-ring steps do not read as harsh pops between discrete phase steps.
- */
function opacityForCell(row: number, col: number, phase: number): number {
const ring = BFS_RING.get(`${row},${col}`) ?? 0;
const span = Math.max(1, MAX_RING);
diff --git a/apps/dashboard/components/ui/drawer.tsx b/apps/dashboard/components/ui/drawer.tsx
index 2f245eac00..68c5986ae1 100644
--- a/apps/dashboard/components/ui/drawer.tsx
+++ b/apps/dashboard/components/ui/drawer.tsx
@@ -126,13 +126,13 @@ function DrawerDescription({
export {
Drawer,
- DrawerClose,
+
DrawerContent,
- DrawerDescription,
- DrawerFooter,
- DrawerHeader,
- DrawerOverlay,
- DrawerPortal,
+
+
+
+
+
DrawerTitle,
- DrawerTrigger,
+
};
diff --git a/apps/dashboard/components/ui/elastic-slider.tsx b/apps/dashboard/components/ui/elastic-slider.tsx
deleted file mode 100644
index 0e06f221b4..0000000000
--- a/apps/dashboard/components/ui/elastic-slider.tsx
+++ /dev/null
@@ -1,224 +0,0 @@
-"use client";
-
-import {
- motion,
- useMotionValue,
- useMotionValueEvent,
- useTransform,
-} from "motion/react";
-import { useCallback, useRef, useState } from "react";
-import { cn } from "@/lib/utils";
-import {
- MinusIcon,
- PlusIcon,
-} from "@databuddy/ui/icons";
-
-const MAX_OVERFLOW = 30;
-
-interface SliderProps {
- className?: string;
- disabled?: boolean;
- leftIcon?: React.ReactNode;
- max?: number;
- min?: number;
- onValueChange?: (value: number) => void;
- rightIcon?: React.ReactNode;
- showValue?: boolean;
- step?: number;
- value?: number;
-}
-
-function decay(value: number, maxValue: number): number {
- if (maxValue === 0) {
- return 0;
- }
- const entry = value / maxValue;
- const sigmoid = 2 * (1 / (1 + Math.exp(-entry)) - 0.5);
- return sigmoid * maxValue;
-}
-
-export function Slider({
- value = 0,
- onValueChange,
- min = 0,
- max = 100,
- step = 1,
- className,
- leftIcon = ,
- rightIcon = ,
- showValue = true,
- disabled = false,
-}: SliderProps) {
- const [internalValue, setInternalValue] = useState(value);
- const sliderRef = useRef(null);
- const [region, setRegion] = useState<"left" | "middle" | "right">("middle");
- const [isDragging, setIsDragging] = useState(false);
-
- const clientX = useMotionValue(0);
- const overflow = useMotionValue(0);
-
- const percentage = ((internalValue - min) / (max - min || 1)) * 100;
-
- useMotionValueEvent(clientX, "change", (latest: number) => {
- if (!(sliderRef.current && isDragging)) {
- return;
- }
-
- const { left, right } = sliderRef.current.getBoundingClientRect();
- let newOverflow = 0;
-
- if (latest < left) {
- setRegion("left");
- newOverflow = left - latest;
- } else if (latest > right) {
- setRegion("right");
- newOverflow = latest - right;
- } else {
- setRegion("middle");
- }
-
- overflow.jump(decay(newOverflow, MAX_OVERFLOW));
- });
-
- const updateValue = useCallback(
- (clientXPos: number) => {
- if (!sliderRef.current) {
- return;
- }
-
- const { left, width } = sliderRef.current.getBoundingClientRect();
- let newValue = min + ((clientXPos - left) / width) * (max - min);
-
- if (step > 0) {
- newValue = Math.round(newValue / step) * step;
- }
-
- newValue = Math.min(Math.max(newValue, min), max);
- setInternalValue(newValue);
- onValueChange?.(newValue);
- clientX.jump(clientXPos);
- },
- [min, max, step, onValueChange, clientX]
- );
-
- const handlePointerDown = (e: React.PointerEvent) => {
- if (disabled) {
- return;
- }
-
- setIsDragging(true);
- updateValue(e.clientX);
- e.currentTarget.setPointerCapture(e.pointerId);
- document.body.style.cursor = "grabbing";
- };
-
- const handlePointerMove = (e: React.PointerEvent) => {
- if (!isDragging || disabled) {
- return;
- }
- updateValue(e.clientX);
- };
-
- const handlePointerUp = () => {
- setIsDragging(false);
- setRegion("middle");
- overflow.jump(0);
- document.body.style.cursor = "";
- };
-
- return (
-
-
-
- region === "left" ? -overflow.get() / 2 : 0
- ),
- scale: region === "left" ? 1.3 : 1,
- }}
- >
- {leftIcon}
-
-
-
-
{
- if (!sliderRef.current) {
- return 1;
- }
- const { width } = sliderRef.current.getBoundingClientRect();
- return 1 + overflow.get() / width;
- }),
- scaleY: useTransform(overflow, [0, MAX_OVERFLOW], [1, 0.7]),
- transformOrigin: useTransform(() => {
- if (!sliderRef.current) {
- return "center";
- }
- const { left, width } =
- sliderRef.current.getBoundingClientRect();
- return clientX.get() < left + width / 2 ? "right" : "left";
- }),
- }}
- >
-
-
-
-
-
-
-
- region === "right" ? overflow.get() / 2 : 0
- ),
- scale: region === "right" ? 1.3 : 1,
- }}
- >
- {rightIcon}
-
-
-
- {showValue && (
-
-
- {Math.round(internalValue)}
- {max === 100 && "%"}
-
-
- )}
-
- );
-}
diff --git a/apps/dashboard/components/ui/fluid-orb.tsx b/apps/dashboard/components/ui/fluid-orb.tsx
index ff043c3239..b6b0648a9f 100644
--- a/apps/dashboard/components/ui/fluid-orb.tsx
+++ b/apps/dashboard/components/ui/fluid-orb.tsx
@@ -4,7 +4,7 @@ import React, { useEffect, useRef } from 'react'
import { cn } from '@/lib/utils'
-export type FluidOrbProps = React.ComponentProps<'div'> & {
+type FluidOrbProps = React.ComponentProps<'div'> & {
size?: number
color?: string
}
diff --git a/apps/dashboard/components/ui/form-dialog.tsx b/apps/dashboard/components/ui/form-dialog.tsx
deleted file mode 100644
index b60a7fd759..0000000000
--- a/apps/dashboard/components/ui/form-dialog.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-"use client";
-
-import {
- Drawer,
- DrawerContent,
- DrawerDescription,
- DrawerFooter,
- DrawerHeader,
- DrawerTitle,
-} from "@/components/ui/drawer";
-import { useIsMobile } from "@/hooks/use-mobile";
-import { Button } from "@databuddy/ui";
-import { Dialog } from "@databuddy/ui/client";
-
-interface FormDialogProps {
- cancelLabel?: string;
- children: React.ReactNode;
- description?: string;
- icon?: React.ReactNode;
- isSubmitting?: boolean;
- onOpenChange: (open: boolean) => void;
- onSubmit: () => void;
- open: boolean;
- size?: "sm" | "md" | "lg";
- submitDisabled?: boolean;
- submitLabel?: string;
- title: string;
-}
-
-export function FormDialog({
- open,
- onOpenChange,
- title,
- description,
- children,
- onSubmit,
- submitLabel = "Save",
- cancelLabel = "Cancel",
- isSubmitting = false,
- submitDisabled = false,
- icon,
- size = "md",
-}: FormDialogProps) {
- const isMobile = useIsMobile();
-
- const sizeClasses = {
- sm: "w-[95vw] max-w-sm sm:w-full",
- md: "w-[95vw] max-w-md sm:w-full",
- lg: "w-[95vw] max-w-lg sm:w-full",
- };
-
- const drawerHeaderContent = icon ? (
-
-
- {icon}
-
-
- {title}
- {description && (
- {description}
- )}
-
-
- ) : null;
-
- const formContent = (
-
- {children}
-
- );
-
- const footerContent = (
- <>
- onOpenChange(false)}
- variant="secondary"
- >
- {cancelLabel}
-
-
- {submitLabel}
-
- >
- );
-
- if (isMobile) {
- return (
-
-
- {icon ? (
- {drawerHeaderContent}
- ) : (
-
- {title}
- {description && (
- {description}
- )}
-
- )}
- {formContent}
-
- {footerContent}
-
-
-
- );
- }
-
- return (
-
-
-
- {icon ? (
-
-
- {icon}
-
-
- {title}
- {description && (
- {description}
- )}
-
-
- ) : (
- <>
- {title}
- {description && (
- {description}
- )}
- >
- )}
-
- {formContent}
- {footerContent}
-
-
-
- );
-}
diff --git a/apps/dashboard/components/ui/form.tsx b/apps/dashboard/components/ui/form.tsx
deleted file mode 100644
index f1dd097369..0000000000
--- a/apps/dashboard/components/ui/form.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-"use client";
-
-import { type Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui";
-import * as React from "react";
-
-import {
- Controller,
- type ControllerProps,
- type FieldPath,
- type FieldValues,
- FormProvider,
- useFormContext,
- useFormState,
-} from "react-hook-form";
-import { cn } from "@/lib/utils";
-import { Field } from "@databuddy/ui";
-
-const Form = FormProvider;
-
-type FormFieldContextValue<
- TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath = FieldPath,
-> = {
- name: TName;
-};
-
-const FormFieldContext = React.createContext(
- {} as FormFieldContextValue
-);
-
-const FormField = <
- TFieldValues extends FieldValues = FieldValues,
- TName extends FieldPath = FieldPath,
->({
- ...props
-}: ControllerProps) => {
- return (
-
-
-
- );
-};
-
-const useFormField = () => {
- const fieldContext = React.useContext(FormFieldContext);
- const itemContext = React.useContext(FormItemContext);
- const { getFieldState } = useFormContext();
- const formState = useFormState({ name: fieldContext.name });
- const fieldState = getFieldState(fieldContext.name, formState);
-
- if (!fieldContext) {
- throw new Error("useFormField should be used within ");
- }
-
- const { id } = itemContext;
-
- return {
- id,
- name: fieldContext.name,
- formItemId: `${id}-form-item`,
- formDescriptionId: `${id}-form-item-description`,
- formMessageId: `${id}-form-item-message`,
- ...fieldState,
- };
-};
-
-type FormItemContextValue = {
- id: string;
-};
-
-const FormItemContext = React.createContext(
- {} as FormItemContextValue
-);
-
-function FormItem({ className, ...props }: React.ComponentProps<"div">) {
- const id = React.useId();
-
- return (
-
-
-
- );
-}
-
-function FormLabel({
- className,
- ...props
-}: React.ComponentProps) {
- const { error, formItemId } = useFormField();
-
- return (
-
- );
-}
-
-function FormControl({
- ...props
-}: React.ComponentProps) {
- const { error, formItemId, formDescriptionId, formMessageId } =
- useFormField();
-
- return (
-
- );
-}
-
-function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
- const { formDescriptionId } = useFormField();
-
- return (
-
- );
-}
-
-function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
- const { error, formMessageId } = useFormField();
- const body = error ? String(error?.message ?? "") : props.children;
-
- if (!body) {
- return null;
- }
-
- return (
-
- {body}
-
- );
-}
-
-export {
- Form,
- FormControl,
- FormDescription,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
- useFormField,
-};
diff --git a/apps/dashboard/components/ui/inline-toggle.tsx b/apps/dashboard/components/ui/inline-toggle.tsx
deleted file mode 100644
index b8941f6eaa..0000000000
--- a/apps/dashboard/components/ui/inline-toggle.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-"use client";
-
-import type { ReactNode } from "react";
-import { cn } from "@/lib/utils";
-
-type InlineToggleOption = {
- value: T;
- label: ReactNode;
- ariaLabel?: string;
-};
-
-type InlineToggleProps = {
- options: InlineToggleOption[];
- value: T;
- onValueChangeAction: (value: T) => void;
- className?: string;
- disabled?: boolean;
-};
-
-export function InlineToggle({
- options,
- value,
- onValueChangeAction,
- className,
- disabled = false,
-}: InlineToggleProps) {
- return (
-
- {options.map((option) => {
- const isSelected = option.value === value;
- return (
- onValueChangeAction(option.value)}
- role="radio"
- type="button"
- >
- {option.label}
-
- );
- })}
-
- );
-}
diff --git a/apps/dashboard/components/ui/input-group.tsx b/apps/dashboard/components/ui/input-group.tsx
deleted file mode 100644
index 8ee6478bd6..0000000000
--- a/apps/dashboard/components/ui/input-group.tsx
+++ /dev/null
@@ -1,169 +0,0 @@
-"use client";
-
-import { cva, type VariantProps } from "class-variance-authority";
-import type * as React from "react";
-import { Input } from "@/components/ui/input";
-import { Textarea, type TextareaProps } from "@/components/ui/textarea";
-import { cn } from "@/lib/utils";
-import { Button } from "@databuddy/ui";
-
-function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
- return (
- textarea]:h-auto",
-
- // Variants based on alignment.
- "has-[>[data-align=inline-start]]:[&>input]:pl-2",
- "has-[>[data-align=inline-end]]:[&>input]:pr-2",
- "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
- "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
-
- // Focus state.
- "has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50",
-
- // Error state.
- "has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
-
- className
- )}
- data-slot="input-group"
- role="group"
- {...props}
- />
- );
-}
-
-const inputGroupAddonVariants = cva(
- "flex h-auto cursor-text select-none items-center justify-center gap-2 py-1.5 font-medium text-muted-foreground text-sm group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
- {
- variants: {
- align: {
- "inline-start":
- "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
- "inline-end":
- "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
- "block-start":
- "order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3",
- "block-end":
- "order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3",
- },
- },
- defaultVariants: {
- align: "inline-start",
- },
- }
-);
-
-function InputGroupAddon({
- className,
- align = "inline-start",
- ...props
-}: React.ComponentProps<"div"> & VariantProps
) {
- return (
- {
- if ((e.target as HTMLElement).closest("button")) {
- return;
- }
- e.currentTarget.parentElement?.querySelector("input")?.focus();
- }}
- role="group"
- {...props}
- />
- );
-}
-
-const inputGroupButtonVariants = cva(
- "flex items-center gap-2 text-sm shadow-none",
- {
- variants: {
- size: {
- xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
- sm: "h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5",
- "icon-xs":
- "size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
- "icon-sm": "size-8 p-0 has-[>svg]:p-0",
- },
- },
- defaultVariants: {
- size: "xs",
- },
- }
-);
-
-function InputGroupButton({
- className,
- type = "button",
- variant = "ghost",
- size = "xs",
- ...props
-}: Omit
, "size"> &
- VariantProps) {
- return (
-
- );
-}
-
-function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
- return (
-
- );
-}
-
-function InputGroupInput({
- className,
- ...props
-}: React.ComponentProps<"input">) {
- return (
-
- );
-}
-
-function InputGroupTextarea({
- className,
- ...props
-}: TextareaProps) {
- return (
-
- );
-}
-
-export {
- InputGroup,
- InputGroupAddon,
- InputGroupButton,
- InputGroupInput,
- InputGroupText,
- InputGroupTextarea,
-};
diff --git a/apps/dashboard/components/ui/input.tsx b/apps/dashboard/components/ui/input.tsx
deleted file mode 100644
index 54af6e32c3..0000000000
--- a/apps/dashboard/components/ui/input.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-"use client";
-
-import { forwardRef } from "react";
-import { cn } from "@/lib/utils";
-import { useFieldContext } from "@databuddy/ui";
-
-type InputProps = Omit, "prefix" | "suffix"> & {
- variant?: "default" | "ghost";
- showFocusIndicator?: boolean;
- wrapperClassName?: string;
- prefix?: React.ReactNode;
- suffix?: React.ReactNode;
-};
-
-const Input = forwardRef(
- (
- {
- className,
- type,
- variant = "default",
- showFocusIndicator = true,
- wrapperClassName,
- prefix,
- suffix,
- id,
- ...props
- },
- ref
- ) => {
- const field = useFieldContext();
- const hasError =
- field?.error ||
- props["aria-invalid"] === true ||
- props["aria-invalid"] === "true";
- const ariaDescribedBy =
- props["aria-describedby"] ??
- (field
- ? [field.error && field.errorId, field.descriptionId]
- .filter(Boolean)
- .join(" ") || undefined
- : undefined);
- const resolvedId = id ?? field?.id;
-
- const hasPrefix = !!prefix;
- const hasSuffix = !!suffix;
-
- const isSmallHeight = className?.includes("h-8");
- const heightClass = isSmallHeight ? "h-8" : "h-9";
-
- if (hasPrefix || hasSuffix) {
- return (
-
- {hasPrefix && (
-
- {prefix}
-
- )}
-
- {hasSuffix && (
-
- {suffix}
-
- )}
-
- );
- }
-
- return (
-
-
- {showFocusIndicator ? null : null}
-
- );
- }
-);
-
-Input.displayName = "Input";
-
-export type { InputProps };
-export { Input };
diff --git a/apps/dashboard/components/ui/keyboard-shortcuts.tsx b/apps/dashboard/components/ui/keyboard-shortcuts.tsx
deleted file mode 100644
index 71fb470f3f..0000000000
--- a/apps/dashboard/components/ui/keyboard-shortcuts.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-"use client";
-
-import { useMemo } from "react";
-import { cn } from "@/lib/utils";
-
-type ShortcutGroup = {
- title: string;
- shortcuts: {
- label: string;
- keys: string;
- macKeys?: string;
- }[];
-};
-
-const SHORTCUT_GROUPS: ShortcutGroup[] = [
- {
- title: "General",
- shortcuts: [{ label: "Search", keys: "Ctrl+K", macKeys: "⌘K" }],
- },
- {
- title: "Forms & Dialogs",
- shortcuts: [
- { label: "Submit form", keys: "Enter" },
- { label: "Close dialog", keys: "Esc" },
- ],
- },
- {
- title: "Date Ranges",
- shortcuts: [
- { label: "Last 24 hours", keys: "1" },
- { label: "Last 7 days", keys: "2" },
- { label: "Last 30 days", keys: "3" },
- { label: "Last 90 days", keys: "4" },
- { label: "Last 180 days", keys: "5" },
- { label: "Last 365 days", keys: "6" },
- ],
- },
- {
- title: "Chart Selection",
- shortcuts: [
- { label: "Zoom to range", keys: "Z" },
- { label: "Add annotation", keys: "A" },
- ],
- },
-];
-
-function isMac() {
- if (typeof window === "undefined") {
- return false;
- }
- return navigator.platform.toUpperCase().indexOf("MAC") >= 0;
-}
-
-type KeyboardShortcutsProps = {
- groups?: ShortcutGroup[];
- compact?: boolean;
-};
-
-export function KeyboardShortcuts({
- groups = SHORTCUT_GROUPS,
- compact = false,
-}: KeyboardShortcutsProps) {
- const isMacOS = useMemo(() => isMac(), []);
-
- return (
-
- {groups.map((group) => (
-
- {!compact && (
-
- {group.title}
-
- )}
-
- {group.shortcuts.map((shortcut) => {
- const displayKeys =
- isMacOS && shortcut.macKeys ? shortcut.macKeys : shortcut.keys;
- return (
-
-
- {shortcut.label}
-
-
- {displayKeys}
-
-
- );
- })}
-
-
- ))}
-
- );
-}
diff --git a/apps/dashboard/components/ui/label.tsx b/apps/dashboard/components/ui/label.tsx
deleted file mode 100644
index 5eeb8e3976..0000000000
--- a/apps/dashboard/components/ui/label.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-"use client";
-
-import { Label as LabelPrimitive } from "radix-ui";
-import type * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-function Label({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-export { Label };
diff --git a/apps/dashboard/components/ui/percentage-badge.tsx b/apps/dashboard/components/ui/percentage-badge.tsx
deleted file mode 100644
index e6f2ffff03..0000000000
--- a/apps/dashboard/components/ui/percentage-badge.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { cn } from "@/lib/utils";
-
-interface PercentageBadgeProps {
- className?: string;
- percentage: number;
-}
-
-export function PercentageBadge({
- percentage,
- className,
-}: PercentageBadgeProps) {
- const getColorClass = (pct: number) => {
- if (pct >= 50) {
- return "bg-green-100 border border-green-800/50 green-angled-rectangle-gradient text-green-800 dark:bg-green-900/30 dark:text-green-400";
- }
- if (pct >= 25) {
- return "bg-brand-purple/10 border border-brand-purple/30 blue-angled-rectangle-gradient text-brand-purple dark:bg-brand-purple/20 dark:text-[#8B80BF]";
- }
- if (pct >= 10) {
- return "bg-amber-100 border border-amber-800/40 amber-angled-rectangle-gradient text-amber-800 dark:bg-amber-900/30 dark:text-amber-400";
- }
- return "bg-accent-brighter border border-accent-foreground/30 badge-angled-rectangle-gradient text-accent-foreground";
- };
-
- const safePercentage =
- percentage == null || Number.isNaN(percentage) ? 0 : percentage;
-
- return (
-
- {safePercentage.toFixed(1)}%
-
- );
-}
diff --git a/apps/dashboard/components/ui/popover.tsx b/apps/dashboard/components/ui/popover.tsx
index e51054d839..843d3352db 100644
--- a/apps/dashboard/components/ui/popover.tsx
+++ b/apps/dashboard/components/ui/popover.tsx
@@ -48,4 +48,4 @@ function PopoverAnchor({
return ;
}
-export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };
+export { Popover, PopoverContent, PopoverTrigger };
diff --git a/apps/dashboard/components/ui/progress.tsx b/apps/dashboard/components/ui/progress.tsx
deleted file mode 100644
index 3375039378..0000000000
--- a/apps/dashboard/components/ui/progress.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-"use client";
-
-import * as ProgressPrimitive from "@radix-ui/react-progress";
-import type * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-function Progress({
- className,
- value,
- ...props
-}: React.ComponentProps) {
- const clamped = Math.min(Math.max(value || 0, 0), 100);
-
- return (
-
-
-
- );
-}
-
-export { Progress };
diff --git a/apps/dashboard/components/ui/scroll-area.tsx b/apps/dashboard/components/ui/scroll-area.tsx
index 256c8a9388..039d5bce49 100644
--- a/apps/dashboard/components/ui/scroll-area.tsx
+++ b/apps/dashboard/components/ui/scroll-area.tsx
@@ -55,4 +55,4 @@ function ScrollBar({
);
}
-export { ScrollArea, ScrollBar };
+export { ScrollArea, };
diff --git a/apps/dashboard/components/ui/segmented-control.tsx b/apps/dashboard/components/ui/segmented-control.tsx
deleted file mode 100644
index 86c906dcff..0000000000
--- a/apps/dashboard/components/ui/segmented-control.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-"use client";
-
-import { motion } from "motion/react";
-import { useId } from "react";
-import { cn } from "@/lib/utils";
-
-type SegmentedControlOption = {
- value: T;
- label: string;
-};
-
-type SegmentedControlProps = {
- options: SegmentedControlOption[];
- value: T;
- onValueChangeAction: (value: T) => void;
- className?: string;
- size?: "sm" | "default";
-};
-
-export function SegmentedControl({
- options,
- value,
- onValueChangeAction,
- className,
- size = "default",
-}: SegmentedControlProps) {
- const layoutId = useId();
-
- return (
-
- {options.map((option) => {
- const isSelected = option.value === value;
-
- return (
- onValueChangeAction(option.value)}
- role="radio"
- type="button"
- >
- {isSelected && (
-
- )}
- {option.label}
-
- );
- })}
-
- );
-}
diff --git a/apps/dashboard/components/ui/select.tsx b/apps/dashboard/components/ui/select.tsx
index 1f0d9c5267..fd65832233 100644
--- a/apps/dashboard/components/ui/select.tsx
+++ b/apps/dashboard/components/ui/select.tsx
@@ -208,12 +208,12 @@ function SelectScrollDownButton({
export {
Select,
SelectContent,
- SelectGroup,
+
SelectItem,
- SelectLabel,
- SelectScrollDownButton,
- SelectScrollUpButton,
- SelectSeparator,
+
+
+
+
SelectTrigger,
SelectValue,
};
diff --git a/apps/dashboard/components/ui/separator.tsx b/apps/dashboard/components/ui/separator.tsx
deleted file mode 100644
index db7059bc62..0000000000
--- a/apps/dashboard/components/ui/separator.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-"use client";
-
-import { Separator as SeparatorPrimitive } from "radix-ui";
-import type * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-function Separator({
- className,
- orientation = "horizontal",
- decorative = true,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-export { Separator };
diff --git a/apps/dashboard/components/ui/sheet.tsx b/apps/dashboard/components/ui/sheet.tsx
deleted file mode 100644
index 99e802e891..0000000000
--- a/apps/dashboard/components/ui/sheet.tsx
+++ /dev/null
@@ -1,153 +0,0 @@
-"use client";
-
-import { XMarkIcon as X } from "@databuddy/ui/icons";
-import { Dialog as SheetPrimitive } from "radix-ui";
-import type * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-function Sheet({ ...props }: React.ComponentProps) {
- return ;
-}
-
-function SheetTrigger({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function SheetClose({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function SheetPortal({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function SheetOverlay({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function SheetContent({
- className,
- children,
- side = "right",
- ...props
-}: React.ComponentProps & {
- side?: "top" | "right" | "bottom" | "left";
-}) {
- return (
-
-
-
- {children}
-
-
- Close
-
-
-
- );
-}
-
-function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function SheetBody({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function SheetTitle({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function SheetDescription({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-export {
- Sheet,
- SheetBody,
- SheetClose,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
- SheetTrigger,
-};
diff --git a/apps/dashboard/components/ui/skeleton.tsx b/apps/dashboard/components/ui/skeleton.tsx
deleted file mode 100644
index 5b322564d8..0000000000
--- a/apps/dashboard/components/ui/skeleton.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { cn } from "@/lib/utils";
-
-function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-export { Skeleton };
diff --git a/apps/dashboard/components/ui/slider.tsx b/apps/dashboard/components/ui/slider.tsx
deleted file mode 100644
index f7e6bc9d4c..0000000000
--- a/apps/dashboard/components/ui/slider.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-"use client";
-
-import { Slider as SliderPrimitive } from "radix-ui";
-import { useMemo } from "react";
-
-import { cn } from "@/lib/utils";
-
-function Slider({
- className,
- defaultValue,
- value,
- min = 0,
- max = 100,
- ...props
-}: React.ComponentProps) {
- const _values = useMemo(() => {
- if (Array.isArray(value)) {
- return value;
- }
- if (Array.isArray(defaultValue)) {
- return defaultValue;
- }
- return [min, max];
- }, [value, defaultValue, min, max]);
-
- return (
-
-
-
-
- {_values.map((value, index) => (
-
- ))}
-
- );
-}
-
-export { Slider };
diff --git a/apps/dashboard/components/ui/spinner.tsx b/apps/dashboard/components/ui/spinner.tsx
deleted file mode 100644
index 5b7b63e02c..0000000000
--- a/apps/dashboard/components/ui/spinner.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-"use client";
-
-import { CircleNotchIcon } from "@databuddy/ui/icons";
-import { cn } from "../../lib/utils";
-
-export const Spinner = ({ className }: { className?: string }) => {
- return (
-
- );
-};
diff --git a/apps/dashboard/components/ui/switch.tsx b/apps/dashboard/components/ui/switch.tsx
deleted file mode 100644
index 6d407e73e6..0000000000
--- a/apps/dashboard/components/ui/switch.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-"use client";
-
-import { Switch as SwitchPrimitive } from "radix-ui";
-import type * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-function Switch({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
- );
-}
-
-export { Switch };
diff --git a/apps/dashboard/components/ui/table.tsx b/apps/dashboard/components/ui/table.tsx
index 6e85f8be8b..9319afffa2 100644
--- a/apps/dashboard/components/ui/table.tsx
+++ b/apps/dashboard/components/ui/table.tsx
@@ -101,9 +101,9 @@ function TableCaption({
export {
Table,
TableBody,
- TableCaption,
+
TableCell,
- TableFooter,
+
TableHead,
TableHeader,
TableRow,
diff --git a/apps/dashboard/components/ui/tabs.tsx b/apps/dashboard/components/ui/tabs.tsx
deleted file mode 100644
index 52c810fb80..0000000000
--- a/apps/dashboard/components/ui/tabs.tsx
+++ /dev/null
@@ -1,311 +0,0 @@
-"use client";
-
-import { Tabs as TabsPrimitive } from "radix-ui";
-import type * as React from "react";
-import {
- createContext,
- useContext,
- useEffect,
- useLayoutEffect,
- useRef,
- useState,
-} from "react";
-
-import { cn } from "@/lib/utils";
-
-type TabsVariant = "default" | "underline" | "pills" | "navigation";
-
-const TabsContext = createContext<{
- variant: TabsVariant;
- registerTrigger: (value: string, element: HTMLButtonElement | null) => void;
- activeValue: string | undefined;
-}>({
- variant: "default",
- registerTrigger: () => {},
- activeValue: undefined,
-});
-
-function Tabs({
- className,
- variant = "default",
- defaultValue,
- value,
- onValueChange,
- ...props
-}: React.ComponentProps & {
- variant?: TabsVariant;
-}) {
- const [activeValue, setActiveValue] = useState(value ?? defaultValue);
- const triggersRef = useRef>(new Map());
-
- // Sync internal state when parent updates the controlled value prop.
- // Without this, the indicator/context lags behind URL-driven tab changes.
- useEffect(() => {
- if (value !== undefined) {
- setActiveValue(value);
- }
- }, [value]);
-
- const registerTrigger = (val: string, element: HTMLButtonElement | null) => {
- if (element) {
- triggersRef.current.set(val, element);
- } else {
- triggersRef.current.delete(val);
- }
- };
-
- const handleValueChange = (newValue: string) => {
- setActiveValue(newValue);
- onValueChange?.(newValue);
- };
-
- return (
-
-
-
- );
-}
-
-function TabsList({
- className,
- ...props
-}: React.ComponentProps) {
- const { variant, activeValue } = useContext(TabsContext);
- const listRef = useRef(null);
- const [indicatorStyle, setIndicatorStyle] = useState({});
-
- useLayoutEffect(() => {
- if (
- (variant !== "underline" && variant !== "navigation") ||
- !listRef.current ||
- !activeValue
- ) {
- return;
- }
-
- const activeTab = listRef.current.querySelector(
- `[data-state="active"]`
- ) as HTMLElement;
-
- if (activeTab) {
- const listRect = listRef.current.getBoundingClientRect();
- const tabRect = activeTab.getBoundingClientRect();
-
- setIndicatorStyle({
- width: tabRect.width,
- transform: `translateX(${tabRect.left - listRect.left}px)`,
- });
- }
- }, [activeValue, variant]);
-
- if (variant === "navigation") {
- return (
-
- );
- }
-
- if (variant === "underline") {
- return (
-
- );
- }
-
- if (variant === "pills") {
- return (
-
- );
- }
-
- // Default variant
- return (
-
- );
-}
-
-function TabsTrigger({
- className,
- value,
- ...props
-}: React.ComponentProps) {
- const { variant, registerTrigger } = useContext(TabsContext);
- const triggerRef = useRef(null);
-
- useLayoutEffect(() => {
- if (value) {
- registerTrigger(value, triggerRef.current);
- }
- return () => {
- if (value) {
- registerTrigger(value, null);
- }
- };
- }, [value, registerTrigger]);
-
- if (variant === "navigation") {
- return (
-
- );
- }
-
- if (variant === "underline") {
- return (
-
- );
- }
-
- if (variant === "pills") {
- return (
-
- );
- }
-
- // Default variant
- return (
-
- );
-}
-
-function TabsContent({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function TabsBadge({
- children,
- forValue,
- className,
-}: {
- children: React.ReactNode;
- forValue: string;
- className?: string;
-}) {
- const { activeValue } = useContext(TabsContext);
- const isActive = activeValue === forValue;
-
- return (
-
- {children}
-
- );
-}
-
-export { Tabs, TabsBadge, TabsContent, TabsList, TabsTrigger };
diff --git a/apps/dashboard/components/ui/tags.tsx b/apps/dashboard/components/ui/tags.tsx
deleted file mode 100644
index a8a1c05fde..0000000000
--- a/apps/dashboard/components/ui/tags.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-"use client";
-
-import { useCallback, useMemo, useRef, useState } from "react";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { cn } from "@/lib/utils";
-import {
- PlusIcon,
- XCircleIcon,
-} from "@databuddy/ui/icons";
-
-export interface TagsChatProps {
- allowDuplicates?: boolean;
- className?: string;
- maxTags?: number;
- onChange: (next: string[]) => void;
- placeholder?: string;
- suggestions?: string[];
- values: string[];
-}
-
-function normalizeTag(raw: string): string {
- return raw.trim();
-}
-
-export function TagsChat({
- values,
- onChange,
- placeholder = "Type a tag and press Enter…",
- suggestions,
- maxTags,
- allowDuplicates = false,
- className,
-}: TagsChatProps) {
- const [draft, setDraft] = useState("");
- const areaRef = useRef(null);
-
- const canAddMore =
- typeof maxTags === "number" ? values.length < maxTags : true;
-
- const addTag = useCallback(
- (tag: string) => {
- const normalized = normalizeTag(tag);
- if (!normalized) {
- return;
- }
- if (!allowDuplicates && values.some((t) => t === normalized)) {
- setDraft("");
- return;
- }
- if (!canAddMore) {
- return;
- }
- onChange([...values, normalized]);
- setDraft("");
- // Scroll to bottom like chat
- queueMicrotask(() => {
- areaRef.current?.scrollTo({
- top: areaRef.current.scrollHeight,
- behavior: "smooth",
- });
- });
- },
- [allowDuplicates, canAddMore, onChange, values]
- );
-
- const removeTag = useCallback(
- (index: number) => {
- const next = values.slice();
- next.splice(index, 1);
- onChange(next);
- },
- [onChange, values]
- );
-
- const visibleSuggestions = useMemo(() => {
- if (!suggestions || suggestions.length === 0) {
- return [] as string[];
- }
- const needle = draft.toLowerCase();
- const pool = suggestions.filter((s) => s.toLowerCase().includes(needle));
- return pool.slice(0, 6);
- }, [draft, suggestions]);
-
- const handleKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Enter" || e.key === ",") {
- e.preventDefault();
- addTag(draft);
- }
- if (e.key === "Backspace" && draft.length === 0 && values.length > 0) {
- e.preventDefault();
- removeTag(values.length - 1);
- }
- };
-
- return (
-
-
- {values.length === 0 ? (
-
- No tags yet. Start typing below.
-
- ) : (
-
- {values.map((tag, index) => (
-
- {tag}
- removeTag(index)}
- size="icon"
- type="button"
- variant="ghost"
- >
-
-
-
- ))}
-
- )}
-
-
-
-
-
setDraft(e.target.value)}
- onKeyDown={handleKeyDown}
- placeholder={placeholder}
- value={draft}
- />
-
addTag(draft)}
- type="button"
- >
- Add
-
-
-
- {visibleSuggestions.length > 0 && (
-
- {visibleSuggestions.map((s) => (
- {
- e.preventDefault();
- addTag(s);
- }}
- type="button"
- >
- {s}
-
- ))}
-
- )}
-
-
- );
-}
diff --git a/apps/dashboard/components/ui/textarea.tsx b/apps/dashboard/components/ui/textarea.tsx
deleted file mode 100644
index 0433d3e7dd..0000000000
--- a/apps/dashboard/components/ui/textarea.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-"use client";
-
-import type { ComponentProps } from "react";
-import { forwardRef } from "react";
-import TextareaAutosize from "react-textarea-autosize";
-import { cn } from "@/lib/utils";
-import { useFieldContext } from "@databuddy/ui";
-
-type TextareaProps = ComponentProps & {
- showFocusIndicator?: boolean;
- wrapperClassName?: string;
-};
-
-const Textarea = forwardRef(
- (
- {
- className,
- showFocusIndicator = true,
- wrapperClassName,
- id,
- ...props
- },
- ref
- ) => {
- const field = useFieldContext();
- const hasError =
- field?.error ||
- props["aria-invalid"] === true ||
- props["aria-invalid"] === "true";
- const ariaDescribedBy =
- props["aria-describedby"] ??
- (field
- ? [field.error && field.errorId, field.descriptionId]
- .filter(Boolean)
- .join(" ") || undefined
- : undefined);
- const resolvedId = id ?? field?.id;
-
- return (
-
-
- {showFocusIndicator ? null : null}
-
- );
- }
-);
-
-Textarea.displayName = "Textarea";
-
-export type { TextareaProps };
-export { Textarea };
diff --git a/apps/dashboard/components/ui/toggle-group.tsx b/apps/dashboard/components/ui/toggle-group.tsx
deleted file mode 100644
index d427abe815..0000000000
--- a/apps/dashboard/components/ui/toggle-group.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-"use client";
-
-import type { VariantProps } from "class-variance-authority";
-import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui";
-import * as React from "react";
-import { toggleVariants } from "@/components/ui/toggle";
-import { cn } from "@/lib/utils";
-
-const ToggleGroupContext = React.createContext<
- VariantProps
->({
- size: "default",
- variant: "default",
-});
-
-function ToggleGroup({
- className,
- variant,
- size,
- children,
- ...props
-}: React.ComponentProps &
- VariantProps) {
- return (
-
-
- {children}
-
-
- );
-}
-
-function ToggleGroupItem({
- className,
- children,
- variant,
- size,
- ...props
-}: React.ComponentProps &
- VariantProps) {
- const context = React.useContext(ToggleGroupContext);
-
- return (
-
- {children}
-
- );
-}
-
-export { ToggleGroup, ToggleGroupItem };
diff --git a/apps/dashboard/components/ui/toggle.tsx b/apps/dashboard/components/ui/toggle.tsx
deleted file mode 100644
index 679f8c9d92..0000000000
--- a/apps/dashboard/components/ui/toggle.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-"use client";
-
-import { cva, type VariantProps } from "class-variance-authority";
-import { Toggle as TogglePrimitive } from "radix-ui";
-import type * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-const toggleVariants = cva(
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded font-medium text-foreground text-sm outline-none transition-[color,box-shadow] hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-primary data-[state=on]:text-primary-foreground data-[state=on]:hover:bg-primary/90 data-[state=on]:hover:text-primary-foreground dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
- {
- variants: {
- variant: {
- default: "bg-transparent",
- outline:
- "border border-input bg-transparent shadow-xs hover:bg-muted hover:text-foreground data-[state=on]:border-primary",
- },
- size: {
- default: "h-9 min-w-9 px-2",
- sm: "h-8 min-w-8 px-1.5",
- lg: "h-10 min-w-10 px-2.5",
- },
- },
- defaultVariants: {
- variant: "default",
- size: "default",
- },
- }
-);
-
-function Toggle({
- className,
- variant,
- size,
- ...props
-}: React.ComponentProps &
- VariantProps) {
- return (
-
- );
-}
-
-export { Toggle, toggleVariants };
diff --git a/apps/dashboard/components/ui/tooltip-bubble.tsx b/apps/dashboard/components/ui/tooltip-bubble.tsx
deleted file mode 100644
index 26068a3f43..0000000000
--- a/apps/dashboard/components/ui/tooltip-bubble.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import type React from "react";
-import { cn } from "@/lib/utils";
-
-interface TooltipBubbleProps {
- children: React.ReactNode;
- className?: string;
-}
-
-export const TooltipBubble: React.FC = ({
- children,
- className,
-}) => {
- return (
-
- {children}
-
- );
-};
diff --git a/apps/dashboard/components/ui/tooltip.tsx b/apps/dashboard/components/ui/tooltip.tsx
deleted file mode 100644
index 555447f7f6..0000000000
--- a/apps/dashboard/components/ui/tooltip.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-"use client";
-
-import * as TooltipPrimitive from "@radix-ui/react-tooltip";
-import type * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-function TooltipProvider({
- delayDuration = 0,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function Tooltip({
- skipProvider = false,
- ...props
-}: React.ComponentProps & {
- skipProvider?: boolean;
-}) {
- const content = ;
-
- if (skipProvider) {
- return content;
- }
-
- return {content} ;
-}
-
-function TooltipTrigger({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function TooltipContent({
- className,
- sideOffset = 0,
- children,
- ...props
-}: React.ComponentProps) {
- return (
-
-
- {children}
-
-
-
- );
-}
-
-export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
diff --git a/apps/dashboard/components/user-avatar.tsx b/apps/dashboard/components/user-avatar.tsx
deleted file mode 100644
index b043949aa4..0000000000
--- a/apps/dashboard/components/user-avatar.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-"use client";
-
-interface UserAvatarProps {
- className?: string;
- size?: "sm" | "md" | "lg";
- visitorId: string;
-}
-
-function hashCode(str: string): number {
- let hash = 5381;
- for (const char of str) {
- hash = Math.imul(hash, 33) + char.charCodeAt(0);
- }
- return Math.abs(hash);
-}
-
-function hslToHex(h: number, s: number, l: number): string {
- const sNorm = s / 100;
- const lNorm = l / 100;
- const a = sNorm * Math.min(lNorm, 1 - lNorm);
-
- const f = (n: number) => {
- const k = (n + h / 30) % 12;
- const color = lNorm - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
- return Math.round(255 * color)
- .toString(16)
- .padStart(2, "0");
- };
-
- return `#${f(0)}${f(8)}${f(4)}`;
-}
-
-function getGradient(visitorId: string): {
- from: string;
- to: string;
- angle: number;
-} {
- const hash1 = hashCode(visitorId);
- const hash2 = hashCode(visitorId.split("").reverse().join(""));
- const hash3 = hashCode(`${visitorId}salt`);
-
- // Generate base hue from hash (0-360)
- const baseHue = hash1 % 360;
-
- // Hue shift strategies for nice color combinations
- const hueShifts = [30, 45, 60, 90, 120, 150, 180];
- const hueShift = hueShifts[hash2 % hueShifts.length] ?? 60;
-
- // Saturation and lightness variations
- const saturations = [65, 70, 75, 80, 85];
- const lightnesses = [55, 60, 65, 70];
-
- const sat1 = saturations[hash1 % saturations.length] ?? 75;
- const sat2 = saturations[hash2 % saturations.length] ?? 70;
- const light1 = lightnesses[hash2 % lightnesses.length] ?? 60;
- const light2 = lightnesses[hash3 % lightnesses.length] ?? 65;
-
- const from = hslToHex(baseHue, sat1, light1);
- const to = hslToHex((baseHue + hueShift) % 360, sat2, light2);
-
- // Angle variation
- const angle = 120 + (hash3 % 60);
-
- return { from, to, angle };
-}
-
-export function UserAvatar({
- visitorId,
- size = "md",
- className,
-}: UserAvatarProps) {
- const { from, to, angle } = getGradient(visitorId);
-
- const sizeClasses = {
- sm: "size-6",
- md: "size-8",
- lg: "size-10",
- };
-
- const sizeClass = sizeClasses[size];
-
- return (
-
- );
-}
diff --git a/apps/dashboard/contexts/chat-context.tsx b/apps/dashboard/contexts/chat-context.tsx
index 9a839ee832..e0b73c3ce0 100644
--- a/apps/dashboard/contexts/chat-context.tsx
+++ b/apps/dashboard/contexts/chat-context.tsx
@@ -96,7 +96,9 @@ export function ChatProvider({
});
const chatRef = useRef(chat);
- chatRef.current = chat;
+ useEffect(() => {
+ chatRef.current = chat;
+ }, [chat]);
const [hasRestored, setHasRestored] = useState(false);
const [persistedUserMessageIds, setPersistedUserMessageIds] = useState<
diff --git a/apps/dashboard/error.tsx b/apps/dashboard/error.tsx
deleted file mode 100644
index b01052760f..0000000000
--- a/apps/dashboard/error.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-"use client";
-
-import { useEffect } from "react";
-import { ArrowClockwiseIcon, WarningIcon } from "@databuddy/ui/icons";
-import { Button, Card } from "@databuddy/ui";
-
-interface ErrorPageProps {
- error: Error & { digest?: string };
- reset: () => void;
-}
-
-export default function ErrorPage({ error, reset }: ErrorPageProps) {
- useEffect(() => {
- console.error(error);
- }, [error]);
-
- return (
-
-
-
-
-
- Something went wrong
-
-
-
-
- We encountered an unexpected error. Please try again. If the problem
- persists, please contact support.
-
- {error.digest && (
-
- Reference: {error.digest}
-
- )}
- reset()} size="sm">
-
- Try again
-
-
-
-
- );
-}
diff --git a/apps/dashboard/hooks/use-chart-preferences.ts b/apps/dashboard/hooks/use-chart-preferences.ts
index 2e30575f68..5dd6f326e1 100644
--- a/apps/dashboard/hooks/use-chart-preferences.ts
+++ b/apps/dashboard/hooks/use-chart-preferences.ts
@@ -5,10 +5,6 @@ import type {
import { usePersistentState } from "@databuddy/ui";
const CHART_PREFERENCES_STORAGE_KEY = "databuddy-chart-preferences";
-
-/**
- * Chart location identifiers - where charts appear in the app
- */
export type ChartLocation =
| "overview-stats" // Small stat cards on the overview tab (visitors, pageviews, etc.)
| "overview-main" // Large main chart on the overview tab
@@ -32,14 +28,6 @@ export const CHART_LOCATION_LABELS: Record = {
events: "Events Stats",
};
-export const CHART_LOCATION_DESCRIPTIONS: Record = {
- "overview-stats": "Small stat cards showing visitors, pageviews, etc.",
- "overview-main": "Large main chart on the overview tab",
- funnels: "Stat cards in the funnel analytics section",
- "website-list": "Mini charts on the websites list page",
- events: "Stat cards in the events analytics section",
-};
-
function isValidChartSeriesKind(value: unknown): value is ChartSeriesKind {
return (
typeof value === "string" &&
@@ -93,10 +81,6 @@ const getDefaultPreferences = (): AllPreferences => {
}
return defaults;
};
-
-/**
- * Hook to get chart preferences for a specific location
- */
export function useChartPreferences(location: ChartLocation) {
const [storedPreferences] = usePersistentState(
CHART_PREFERENCES_STORAGE_KEY,
@@ -117,10 +101,6 @@ export function useChartPreferences(location: ChartLocation) {
chartStepType: locationPrefs.chartStepType,
};
}
-
-/**
- * Hook to get and update all chart preferences (for settings page)
- */
export function useAllChartPreferences() {
const [storedPreferences, setStoredPreferences] =
usePersistentState(
diff --git a/apps/dashboard/hooks/use-date-filters.ts b/apps/dashboard/hooks/use-date-filters.ts
index 625493c891..52c6357af3 100644
--- a/apps/dashboard/hooks/use-date-filters.ts
+++ b/apps/dashboard/hooks/use-date-filters.ts
@@ -5,10 +5,13 @@ import {
getDefaultDatesFromPreset,
} from "@/hooks/use-default-date-range";
import { dayjs } from "@databuddy/ui";
-import type {
- DateRangeState,
- TimeGranularity,
-} from "@/stores/jotai/filterAtoms";
+
+export interface DateRangeState {
+ endDate: Date;
+ startDate: Date;
+}
+
+export type TimeGranularity = "daily" | "hourly";
const MAX_HOURLY_DAYS = 7;
const AUTO_HOURLY_DAYS = 2;
diff --git a/apps/dashboard/hooks/use-default-date-range.ts b/apps/dashboard/hooks/use-default-date-range.ts
index e2fbcf3067..2cf1e36d89 100644
--- a/apps/dashboard/hooks/use-default-date-range.ts
+++ b/apps/dashboard/hooks/use-default-date-range.ts
@@ -3,7 +3,7 @@ import { dayjs } from "@databuddy/ui";
const DEFAULT_DATE_RANGE_STORAGE_KEY = "databuddy-default-date-range";
-export const DEFAULT_DATE_RANGE_PRESETS = [
+const DEFAULT_DATE_RANGE_PRESETS = [
"24h",
"7d",
"30d",
@@ -21,12 +21,6 @@ function isValidPreset(value: unknown): value is DefaultDateRangePreset {
DEFAULT_DATE_RANGE_PRESETS.includes(value as DefaultDateRangePreset)
);
}
-
-/**
- * Reads the default date range preset from localStorage synchronously.
- * Used by use-date-filters for the initial default when URL has no params.
- * Returns "30d" during SSR or when storage is unavailable.
- */
export function getDefaultDateRangePresetSync(): DefaultDateRangePreset {
if (typeof window === "undefined") {
return "30d";
diff --git a/apps/dashboard/hooks/use-filters.ts b/apps/dashboard/hooks/use-filters.ts
index 77914c91bc..ecb6a0126c 100644
--- a/apps/dashboard/hooks/use-filters.ts
+++ b/apps/dashboard/hooks/use-filters.ts
@@ -16,7 +16,7 @@ export const goalFunnelOperatorOptions = [
{ value: "contains", label: "contains" },
] as const;
-export const operatorLabels: Record = {
+const operatorLabels: Record = {
eq: "=",
ne: "≠",
contains: "contains",
diff --git a/apps/dashboard/hooks/use-goals.ts b/apps/dashboard/hooks/use-goals.ts
index 3aa60b0f12..8c26180083 100644
--- a/apps/dashboard/hooks/use-goals.ts
+++ b/apps/dashboard/hooks/use-goals.ts
@@ -11,7 +11,7 @@ import { orpc } from "@/lib/orpc";
export type Goal = InferSelectModel;
-export interface GoalAnalyticsData {
+interface GoalAnalyticsData {
avg_completion_time: number;
avg_completion_time_formatted: string;
biggest_dropoff_rate: number;
@@ -236,27 +236,6 @@ export function useGoal(goalId: string, enabled = true) {
});
}
-export function useGoalAnalytics(
- websiteId: string,
- goalId: string,
- dateRange: { start_date: string; end_date: string },
- filters: GoalFilter[] = [],
- options: { enabled: boolean } = { enabled: true }
-) {
- return useQuery({
- ...orpc.goals.getAnalytics.queryOptions({
- input: {
- goalId,
- websiteId,
- startDate: dateRange?.start_date,
- endDate: dateRange?.end_date,
- filters,
- },
- }),
- enabled: options.enabled && !!websiteId && !!goalId,
- });
-}
-
export function useBulkGoalAnalytics(
websiteId: string,
goalIds: string[],
diff --git a/apps/dashboard/hooks/use-links.ts b/apps/dashboard/hooks/use-links.ts
index 9313d41ed5..226f67e298 100644
--- a/apps/dashboard/hooks/use-links.ts
+++ b/apps/dashboard/hooks/use-links.ts
@@ -62,7 +62,7 @@ const EMPTY_LINK_FOLDERS: LinkFolder[] = [];
export type LinkSortOption = "newest" | "oldest" | "name-asc" | "name-desc";
export type LinkTypeFilter = "all" | "short" | "deep";
-export const LINKS_PAGE_SIZE = 50;
+const LINKS_PAGE_SIZE = 50;
export interface LinksPageParams {
folderId?: string | null;
@@ -402,30 +402,3 @@ export function useCreateLinkFolder() {
},
});
}
-
-export function useUpdateLinkFolder() {
- const queryClient = useQueryClient();
-
- return useMutation({
- ...orpc.linkFolders.update.mutationOptions(),
- onSuccess: () => {
- queryClient.invalidateQueries({
- queryKey: foldersRootKey,
- });
- },
- });
-}
-
-export function useDeleteLinkFolder() {
- const queryClient = useQueryClient();
-
- return useMutation({
- ...orpc.linkFolders.delete.mutationOptions(),
- onSuccess: () => {
- queryClient.invalidateQueries({
- queryKey: foldersRootKey,
- });
- queryClient.invalidateQueries({ queryKey: linksPaginatedRootKey });
- },
- });
-}
diff --git a/apps/dashboard/hooks/use-websites.ts b/apps/dashboard/hooks/use-websites.ts
index 7194c33a0f..a588324119 100644
--- a/apps/dashboard/hooks/use-websites.ts
+++ b/apps/dashboard/hooks/use-websites.ts
@@ -26,7 +26,7 @@ export const getWebsitesListKey = (): QueryKey =>
const EMPTY_WEBSITES: Website[] = [];
-export const updateWebsiteInList = (
+const updateWebsiteInList = (
old: WebsitesListData | undefined,
updatedWebsite: Website
): WebsitesListData | undefined => {
diff --git a/apps/dashboard/lib/ai-components/index.ts b/apps/dashboard/lib/ai-components/index.ts
index c1a200d6e7..cbfa105ea9 100644
--- a/apps/dashboard/lib/ai-components/index.ts
+++ b/apps/dashboard/lib/ai-components/index.ts
@@ -2,33 +2,6 @@
export { parseContentSegments } from "./parser";
-export { componentRegistry, getComponent, hasComponent } from "./registry";
+export { getComponent, hasComponent } from "./registry";
-export {
- AI_COMPONENT_DATA_PART_NAME,
- AI_COMPONENT_DATA_PART_TYPE,
- getAIComponentInputFromPart,
- getAIComponentInputFromToolOutput,
- normalizeAIComponentMessageParts,
- normalizeAIComponentMessages,
-} from "./message-parts";
-export type { AIComponentDataPart } from "./message-parts";
-
-export type {
- BaseComponentProps,
- ChartComponentProps,
- ComponentDefinition,
- ComponentRegistry,
- ContentSegment,
- CountryItem,
- DashboardActionsInput,
- DataTableInput,
- DistributionInput,
- LinksListInput,
- MiniMapInput,
- ParsedSegments,
- RawComponentInput,
- ReferrerItem,
- ReferrersListInput,
- TimeSeriesInput,
-} from "./types";
+export type { RawComponentInput } from "./types";
diff --git a/apps/dashboard/lib/ai-components/message-parts.ts b/apps/dashboard/lib/ai-components/message-parts.ts
index 8ffa267921..4c4ad3b28b 100644
--- a/apps/dashboard/lib/ai-components/message-parts.ts
+++ b/apps/dashboard/lib/ai-components/message-parts.ts
@@ -2,7 +2,7 @@ import { parseContentSegments } from "./parser";
import { validateComponentJSON } from "./schemas";
import type { RawComponentInput } from "./types";
-export const AI_COMPONENT_DATA_PART_NAME = "aiComponent";
+const AI_COMPONENT_DATA_PART_NAME = "aiComponent";
export const AI_COMPONENT_DATA_PART_TYPE = `data-${AI_COMPONENT_DATA_PART_NAME}`;
interface MessageLike {
@@ -16,7 +16,7 @@ interface TextPartLike {
[key: string]: unknown;
}
-export interface AIComponentDataPart {
+interface AIComponentDataPart {
data: RawComponentInput;
id?: string;
type: typeof AI_COMPONENT_DATA_PART_TYPE;
@@ -107,7 +107,7 @@ function expandTextPart(part: TextPartLike): unknown[] | null {
return expanded.length > 0 ? expanded : null;
}
-export function normalizeAIComponentMessageParts(
+function normalizeAIComponentMessageParts(
message: TMessage
): TMessage {
if (message.role !== "assistant") {
diff --git a/apps/dashboard/lib/ai-components/parser.ts b/apps/dashboard/lib/ai-components/parser.ts
index 32bcdf86c8..ff46fdbd8f 100644
--- a/apps/dashboard/lib/ai-components/parser.ts
+++ b/apps/dashboard/lib/ai-components/parser.ts
@@ -25,7 +25,7 @@ function isRawComponentInput(obj: unknown): obj is RawComponentInput {
return valid;
}
-export function repairPartialJSON(input: string): string | null {
+function repairPartialJSON(input: string): string | null {
if (input.length < 10) {
return null;
}
diff --git a/apps/dashboard/lib/ai-components/registry.tsx b/apps/dashboard/lib/ai-components/registry.tsx
index c41d4bea7d..357e56b6ee 100644
--- a/apps/dashboard/lib/ai-components/registry.tsx
+++ b/apps/dashboard/lib/ai-components/registry.tsx
@@ -492,7 +492,7 @@ function toMiniMapProps(input: MiniMapInput): MiniMapProps {
};
}
-export const componentRegistry: ComponentRegistry = {
+const componentRegistry: ComponentRegistry = {
"line-chart": {
validate: isTimeSeriesInput,
transform: toTimeSeriesProps,
diff --git a/apps/dashboard/lib/ai-components/renderers/config.ts b/apps/dashboard/lib/ai-components/renderers/config.ts
deleted file mode 100644
index 8b56acc8a8..0000000000
--- a/apps/dashboard/lib/ai-components/renderers/config.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { chartSeriesColorAtIndex } from "@/lib/chart-presentation";
-
-/**
- * Get a theme-aware chart color by index.
- * Uses the dashboard's CSS variable palette for consistency.
- */
-export const getChartColor = chartSeriesColorAtIndex;
diff --git a/apps/dashboard/lib/ai-components/renderers/data-table.tsx b/apps/dashboard/lib/ai-components/renderers/data-table.tsx
index 8355c70adb..56e0348bd6 100644
--- a/apps/dashboard/lib/ai-components/renderers/data-table.tsx
+++ b/apps/dashboard/lib/ai-components/renderers/data-table.tsx
@@ -5,7 +5,7 @@ import type { BaseComponentProps } from "../types";
import { TableIcon } from "@databuddy/ui/icons";
import { Badge, Card } from "@databuddy/ui";
-export interface DataTableColumn {
+interface DataTableColumn {
align?: "left" | "center" | "right";
header: string;
key: string;
diff --git a/apps/dashboard/lib/ai-components/renderers/mini-map.tsx b/apps/dashboard/lib/ai-components/renderers/mini-map.tsx
index b42189cabf..bb5b4b707a 100644
--- a/apps/dashboard/lib/ai-components/renderers/mini-map.tsx
+++ b/apps/dashboard/lib/ai-components/renderers/mini-map.tsx
@@ -2,7 +2,6 @@
import type { LocationData } from "@/types/website";
import dynamic from "next/dynamic";
-import { motion } from "motion/react";
import { useId, useMemo, useState } from "react";
import { CountryFlag } from "@/components/icon";
import { formatNumber } from "@/lib/formatters";
@@ -34,7 +33,7 @@ const MapComponent = dynamic(
}
);
-export interface CountryItem {
+interface CountryItem {
country_code?: string;
name: string;
pageviews?: number;
@@ -174,74 +173,75 @@ export function MiniMapRenderer({ title, countries, className }: MiniMapProps) {
/>
-
-
- {topCountries.length > 0 ? (
- topCountries.map((country) => {
- const safeVisitors =
- country.visitors == null ||
- Number.isNaN(country.visitors)
- ? 0
- : country.visitors;
- const safeTotalVisitors =
- totalVisitors == null || Number.isNaN(totalVisitors)
- ? 0
- : totalVisitors;
- const percentage =
- safeTotalVisitors > 0 &&
- !Number.isNaN(safeVisitors) &&
- !Number.isNaN(safeTotalVisitors)
- ? (safeVisitors / safeTotalVisitors) * 100
- : 0;
- const countryCode =
- country.country_code?.toUpperCase() ||
- country.country.toUpperCase();
+
+
+ {topCountries.length > 0 ? (
+ topCountries.map((country) => {
+ const safeVisitors =
+ country.visitors == null ||
+ Number.isNaN(country.visitors)
+ ? 0
+ : country.visitors;
+ const safeTotalVisitors =
+ totalVisitors == null ||
+ Number.isNaN(totalVisitors)
+ ? 0
+ : totalVisitors;
+ const percentage =
+ safeTotalVisitors > 0 &&
+ !Number.isNaN(safeVisitors) &&
+ !Number.isNaN(safeTotalVisitors)
+ ? (safeVisitors / safeTotalVisitors) * 100
+ : 0;
+ const countryCode =
+ country.country_code?.toUpperCase() ||
+ country.country.toUpperCase();
- return (
-
-
-
- {country.country}
-
-
-
- {formatNumber(country.visitors)}
-
-
- {percentage.toFixed(0)}%
+ return (
+
+
+
+ {country.country}
+
+
+ {formatNumber(country.visitors)}
+
+
+ {percentage.toFixed(0)}%
+
+
-
- );
- })
- ) : (
-
-
-
- No location data
-
-
- )}
-
-
+ );
+ })
+ ) : (
+
+
+
+ No location data
+
+
+ )}
+
+
+
diff --git a/apps/dashboard/lib/ai-components/renderers/referrers-list.tsx b/apps/dashboard/lib/ai-components/renderers/referrers-list.tsx
index 70a3897960..e36166907a 100644
--- a/apps/dashboard/lib/ai-components/renderers/referrers-list.tsx
+++ b/apps/dashboard/lib/ai-components/renderers/referrers-list.tsx
@@ -7,7 +7,7 @@ import type { BaseComponentProps } from "../types";
import { GlobeIcon } from "@databuddy/ui/icons";
import { Card } from "@databuddy/ui";
-export interface ReferrerItem {
+interface ReferrerItem {
domain?: string;
name: string;
pageviews?: number;
diff --git a/apps/dashboard/lib/ai-components/schemas.ts b/apps/dashboard/lib/ai-components/schemas.ts
index 19f73f34c8..fc601cca8e 100644
--- a/apps/dashboard/lib/ai-components/schemas.ts
+++ b/apps/dashboard/lib/ai-components/schemas.ts
@@ -6,7 +6,7 @@ const linkSlugSchema = z.string().refine(isPublicLinkSlug, {
"Slug must be 3-50 characters and use only letters, numbers, hyphens, or underscores",
});
-export const timeSeriesSchema = z
+const timeSeriesSchema = z
.object({
type: z.string(),
title: z.string().optional(),
@@ -15,7 +15,7 @@ export const timeSeriesSchema = z
})
.passthrough();
-export const distributionSchema = z
+const distributionSchema = z
.object({
type: z.string(),
title: z.string().optional(),
@@ -23,7 +23,7 @@ export const distributionSchema = z
})
.passthrough();
-export const dataTableSchema = z
+const dataTableSchema = z
.object({
type: z.literal("data-table"),
title: z.string().optional(),
@@ -48,7 +48,7 @@ const referrerItemSchema = z
})
.passthrough();
-export const referrersListSchema = z
+const referrersListSchema = z
.object({
type: z.literal("referrers-list"),
title: z.string().optional(),
@@ -66,7 +66,7 @@ const countryItemSchema = z
})
.passthrough();
-export const miniMapSchema = z
+const miniMapSchema = z
.object({
type: z.literal("mini-map"),
title: z.string().optional(),
@@ -102,7 +102,7 @@ export const linksListSchema = z
})
.passthrough();
-export const linkPreviewSchema = z
+const linkPreviewSchema = z
.object({
type: z.literal("link-preview"),
mode: z.enum(["create", "update", "delete"]),
@@ -122,7 +122,7 @@ export const linkPreviewSchema = z
})
.passthrough();
-export const feedbackPreviewSchema = z
+const feedbackPreviewSchema = z
.object({
type: z.literal("feedback-preview"),
mode: z.enum(["offer", "sent"]),
@@ -156,7 +156,7 @@ const funnelItemSchema = z
})
.passthrough();
-export const funnelsListSchema = z
+const funnelsListSchema = z
.object({
type: z.literal("funnels-list"),
title: z.string().optional(),
@@ -164,7 +164,7 @@ export const funnelsListSchema = z
})
.passthrough();
-export const funnelPreviewSchema = z
+const funnelPreviewSchema = z
.object({
type: z.literal("funnel-preview"),
mode: z.enum(["create", "update", "delete"]),
@@ -247,7 +247,7 @@ const goalItemSchema = z
})
.passthrough();
-export const goalsListSchema = z
+const goalsListSchema = z
.object({
type: z.literal("goals-list"),
title: z.string().optional(),
@@ -255,7 +255,7 @@ export const goalsListSchema = z
})
.passthrough();
-export const goalPreviewSchema = z
+const goalPreviewSchema = z
.object({
type: z.literal("goal-preview"),
mode: z.enum(["create", "update", "delete"]),
@@ -285,7 +285,7 @@ const annotationItemSchema = z
})
.passthrough();
-export const annotationsListSchema = z
+const annotationsListSchema = z
.object({
type: z.literal("annotations-list"),
title: z.string().optional(),
@@ -293,7 +293,7 @@ export const annotationsListSchema = z
})
.passthrough();
-export const annotationPreviewSchema = z
+const annotationPreviewSchema = z
.object({
type: z.literal("annotation-preview"),
mode: z.enum(["create", "update", "delete"]),
@@ -311,7 +311,7 @@ export const annotationPreviewSchema = z
})
.passthrough();
-export const componentSchemaMap: Record = {
+const componentSchemaMap: Record = {
"line-chart": timeSeriesSchema,
"bar-chart": timeSeriesSchema,
"area-chart": timeSeriesSchema,
diff --git a/apps/dashboard/lib/ai-components/types.ts b/apps/dashboard/lib/ai-components/types.ts
index cd0bfe46f5..77fc3a6335 100644
--- a/apps/dashboard/lib/ai-components/types.ts
+++ b/apps/dashboard/lib/ai-components/types.ts
@@ -22,11 +22,6 @@ export interface ComponentDefinition<
export type ComponentRegistry = Record>;
-export interface ParsedContent {
- components: RawComponentInput[];
- text: string;
-}
-
export type ContentSegment =
| { type: "text"; content: string }
| { type: "component"; content: RawComponentInput }
@@ -228,7 +223,7 @@ export interface DataTableInput {
type: "data-table";
}
-export interface ReferrerItem {
+interface ReferrerItem {
domain?: string;
name: string;
pageviews?: number;
@@ -243,7 +238,7 @@ export interface ReferrersListInput {
type: "referrers-list";
}
-export interface CountryItem {
+interface CountryItem {
country_code?: string;
name: string;
pageviews?: number;
diff --git a/apps/dashboard/lib/annotation-constants.ts b/apps/dashboard/lib/annotation-constants.ts
index c02756d3bb..3d930d4130 100644
--- a/apps/dashboard/lib/annotation-constants.ts
+++ b/apps/dashboard/lib/annotation-constants.ts
@@ -1,8 +1,4 @@
import type { AnnotationColor, AnnotationTag } from "@/types/annotations";
-
-/**
- * Available colors for annotations
- */
export const ANNOTATION_COLORS: AnnotationColor[] = [
{ value: "#3B82F6", label: "Blue" },
{ value: "#EF4444", label: "Red" },
@@ -11,10 +7,6 @@ export const ANNOTATION_COLORS: AnnotationColor[] = [
{ value: "#8B5CF6", label: "Purple" },
{ value: "#EC4899", label: "Pink" },
];
-
-/**
- * Common tags for quick selection
- */
export const COMMON_ANNOTATION_TAGS: AnnotationTag[] = [
{ label: "Campaign", value: "campaign", color: "#3B82F6" },
{ label: "Launch", value: "launch", color: "#10B981" },
@@ -25,33 +17,12 @@ export const COMMON_ANNOTATION_TAGS: AnnotationTag[] = [
{ label: "Marketing", value: "marketing", color: "#06B6D4" },
{ label: "Update", value: "update", color: "#84CC16" },
];
-
-/**
- * Default annotation values
- */
export const DEFAULT_ANNOTATION_VALUES = {
color: "#3B82F6",
isPublic: false,
maxTextLength: 500,
tags: [] as string[],
} as const;
-
-/**
- * Chart annotation styling constants
- */
-export const CHART_ANNOTATION_STYLES = {
- strokeWidth: 3,
- strokeDasharray: "5 5",
- fillOpacity: 0.08,
- strokeOpacity: 0.6,
- fontSize: 11,
- fontWeight: 600,
- offset: 10,
-} as const;
-
-/**
- * Local storage keys for annotation preferences
- */
export const ANNOTATION_STORAGE_KEYS = {
visibility: (websiteId: string) => `chart-annotations-visible-${websiteId}`,
tipDismissed: (websiteId: string) =>
diff --git a/apps/dashboard/lib/annotation-utils.ts b/apps/dashboard/lib/annotation-utils.ts
index 91690fc777..5d29b72d79 100644
--- a/apps/dashboard/lib/annotation-utils.ts
+++ b/apps/dashboard/lib/annotation-utils.ts
@@ -2,26 +2,13 @@ import { dayjs } from "@databuddy/ui";
import type { Annotation } from "@/types/annotations";
type Granularity = "hourly" | "daily" | "weekly" | "monthly";
-
-/**
- * Formats a date to a readable string
- * Shows time if the date is within a 24-hour period or spans less than a day
- */
-export function formatAnnotationDate(
- date: Date | string,
- showTime = false
-): string {
+function formatAnnotationDate(date: Date | string, showTime = false): string {
const dateObj = dayjs(date);
if (showTime) {
return dateObj.format("MMM D, h:mm A");
}
return dateObj.format("MMM D, YYYY");
}
-
-/**
- * Formats a date range for annotations
- * Automatically detects if hourly format is needed based on granularity or date range
- */
export function formatAnnotationDateRange(
start: Date | string,
end: Date | string | null,
@@ -49,10 +36,6 @@ export function formatAnnotationDateRange(
return `${formatAnnotationDate(start, showTime)} - ${formatAnnotationDate(end as Date | string, showTime)}`;
}
-
-/**
- * Checks if an annotation is a single-day range
- */
export function isSingleDayAnnotation(annotation: Annotation): boolean {
if (annotation.annotationType !== "range" || !annotation.xEndValue) {
return false;
@@ -63,24 +46,6 @@ export function isSingleDayAnnotation(annotation: Annotation): boolean {
return startTime === endTime;
}
-
-/**
- * Gets the display date for chart rendering
- * Matches the format used by formatDateByGranularity
- */
-export function getChartDisplayDate(
- date: Date | string,
- granularity: Granularity = "daily"
-): string {
- const dateObj = dayjs(date);
- return granularity === "hourly"
- ? dateObj.format("MMM D, h:mm A")
- : dateObj.format("MMM D");
-}
-
-/**
- * Validates annotation form data
- */
export function validateAnnotationForm(data: {
text: string;
tags: string[];
@@ -105,27 +70,6 @@ export function validateAnnotationForm(data: {
errors,
};
}
-
-/**
- * Generates a unique annotation ID
- */
-export function generateAnnotationId(): string {
- return `annotation_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
-}
-
-/**
- * Sanitizes annotation text
- */
export function sanitizeAnnotationText(text: string): string {
return text.trim().slice(0, 500);
}
-
-/**
- * Formats annotation tags for display
- */
-export function formatAnnotationTags(tags: string[] | null): string[] {
- if (!tags || tags.length === 0) {
- return [];
- }
- return tags.filter((tag) => tag.trim().length > 0);
-}
diff --git a/apps/dashboard/lib/app-events.ts b/apps/dashboard/lib/app-events.ts
index 16be651a0e..7edfa6c7a2 100644
--- a/apps/dashboard/lib/app-events.ts
+++ b/apps/dashboard/lib/app-events.ts
@@ -19,7 +19,6 @@ import {
export {
APP_EVENTS,
readMarketingProperties,
- readUtmProperties,
} from "@databuddy/shared/custom-events";
export type {
OnboardingAttributionProperties,
diff --git a/apps/dashboard/lib/app-url.ts b/apps/dashboard/lib/app-url.ts
index a351f09dd7..0d8255c494 100644
--- a/apps/dashboard/lib/app-url.ts
+++ b/apps/dashboard/lib/app-url.ts
@@ -2,7 +2,7 @@ import { publicConfig } from "@databuddy/env/public";
export const APP_URL = publicConfig.urls.dashboard;
-export const STATUS_URL = publicConfig.urls.status;
+const STATUS_URL = publicConfig.urls.status;
export function getStatusPageUrl(slug: string): string {
return `${STATUS_URL}/${slug}`;
diff --git a/apps/dashboard/lib/autumn/attach-content.tsx b/apps/dashboard/lib/autumn/attach-content.tsx
deleted file mode 100644
index 937888d076..0000000000
--- a/apps/dashboard/lib/autumn/attach-content.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { PreviewAttachResponse } from "autumn-js";
-
-export const getAttachContent = (_preview: PreviewAttachResponse) => {
- const planName = "this plan";
-
- return {
- title: Confirm Subscription
,
- message: (
-
- By clicking confirm, you will be subscribed to {planName} and your
- payment method will be charged.
-
- ),
- };
-};
diff --git a/apps/dashboard/lib/chart-presentation.ts b/apps/dashboard/lib/chart-presentation.ts
index 705d50f669..ad63ce04d9 100644
--- a/apps/dashboard/lib/chart-presentation.ts
+++ b/apps/dashboard/lib/chart-presentation.ts
@@ -1,27 +1,13 @@
import type { CSSProperties } from "react";
import { cn } from "@/lib/utils";
-
-/** Root shell for chart cards (matches `Chart` default root). */
export const chartSurfaceClassName =
"flex w-full min-w-0 flex-col gap-0 overflow-hidden rounded border border-border bg-card";
-
-/** Same as `chartSurfaceClassName` but no border (inset charts, e.g. LLM panel). */
export const chartSurfaceBorderlessClassName =
"w-full min-w-0 flex flex-col gap-0 overflow-hidden rounded border-0 bg-card";
-
-/** Plot background behind Recharts (matches `Chart.Plot`). */
export const chartPlotRegionClassName = "dotted-bg bg-accent";
-
-/** Default `YAxis` width when labels need room (most dashboards). */
export const chartAxisYWidthDefault = 45;
-
-/** Narrow charts (trends with tight left margin). */
export const chartAxisYWidthCompact = 32;
-
-/** Recharts `Legend` swatch size — use for every `Legend` `iconSize`. */
export const chartRechartsLegendIconSize = 8;
-
-/** Interactive legend (click to toggle series): centered row, matches pill typography rhythm. */
export const chartRechartsLegendInteractiveWrapperStyle = {
cursor: "pointer",
display: "flex",
@@ -31,8 +17,6 @@ export const chartRechartsLegendInteractiveWrapperStyle = {
lineHeight: 1.2,
paddingTop: "20px",
} as const satisfies CSSProperties;
-
-/** Read-only Recharts legend (no toggle). */
export const chartRechartsLegendStaticWrapperStyle = {
display: "flex",
fontSize: "12px",
@@ -55,8 +39,6 @@ export function chartRechartsInteractiveLegendLabelClassName(
export const chartRechartsLegendStaticLabelClassName =
"text-pretty text-muted-foreground text-xs";
-
-/** `Chart.Legend` metric pills (footer) — keep in sync with Recharts legend labels. */
export const chartLegendPillRowClassName =
"flex shrink-0 flex-wrap justify-end gap-1.5";
@@ -67,28 +49,20 @@ export const chartLegendPillDotClassName = "size-2 shrink-0 rounded-full";
export const chartLegendPillLabelClassName =
"text-muted-foreground text-xs leading-none";
-
-/** Single-row legend above a chart (e.g. retention) — same dot + label scale as pills. */
export const chartLegendInlineRowClassName =
"flex flex-wrap items-center gap-x-4 gap-y-1";
export const chartLegendInlineItemClassName = "flex items-center gap-2";
-
-/** Recharts `tick` prop for X/Y axes — use for every dashboard chart. */
export const chartAxisTickDefault = {
fontSize: 11,
fill: "var(--muted-foreground)",
} as const;
-
-/** Horizontal `CartesianGrid` — single token for all Cartesian charts. */
export const chartCartesianGridDefault = {
stroke: "var(--border)",
strokeDasharray: "2 4",
strokeOpacity: 0.35,
vertical: false,
} as const;
-
-/** Theme series colors (rotate for N metrics without semantic hex). */
export const chartSeriesPalette = [
"var(--color-chart-1)",
"var(--color-chart-2)",
@@ -100,25 +74,15 @@ export const chartSeriesPalette = [
export function chartSeriesColorAtIndex(index: number): string {
return chartSeriesPalette[index % chartSeriesPalette.length];
}
-
-/** Outer shell for `Chart.Tooltip` multi-series layout (matches composable defaults). */
export const chartTooltipMultiShellClassName =
"min-w-[180px] rounded border border-border bg-popover p-2.5 shadow-lg";
-
-/** Outer shell for single-value tooltip. */
export const chartTooltipSingleShellClassName =
"rounded border border-border bg-popover px-2.5 py-1.5 shadow-lg";
-
-/** Date/label row inside multi-series tooltip. */
export const chartTooltipHeaderRowClassName =
"mb-2 flex items-center gap-2 border-b border-border pb-2";
-
-/** Custom Recharts `Tooltip` content wrappers (when not using `Chart.Tooltip`). */
export function chartTooltipCustomSurfaceClassName(className?: string) {
return cn(chartTooltipMultiShellClassName, "p-3", className);
}
-
-/** Merges static legend placement tweaks (e.g. pie `bottom`, scrollable X bottom offset). */
export function chartRechartsLegendStaticWrapperStyleMerge(
extra: CSSProperties
): CSSProperties {
diff --git a/apps/dashboard/lib/chart-query-outcome.ts b/apps/dashboard/lib/chart-query-outcome.ts
index 7248f6203e..448cde1f28 100644
--- a/apps/dashboard/lib/chart-query-outcome.ts
+++ b/apps/dashboard/lib/chart-query-outcome.ts
@@ -1,9 +1,4 @@
import type { UseQueryResult } from "@tanstack/react-query";
-
-/**
- * Derives chart UI state from TanStack Query (or manual props). Use with
- * `Chart.Content` so pages don’t hand-roll loading → flash → chart.
- */
export type ChartQueryOutcome =
| { status: "empty" }
| { status: "error" }
@@ -51,8 +46,6 @@ export type ChartQuerySlice = Pick<
UseQueryResult,
"data" | "isPending" | "isError" | "isSuccess"
>;
-
-/** Infers chart state from a TanStack query — pass to `Chart.Content` as `query`. */
export function chartQueryOutcomeFromQuery(
query: ChartQuerySlice,
options?: { gatePending?: boolean; isEmpty?: (data: T) => boolean }
diff --git a/apps/dashboard/lib/dashboard-navigation-actions.ts b/apps/dashboard/lib/dashboard-navigation-actions.ts
index 4249401287..54701c9097 100644
--- a/apps/dashboard/lib/dashboard-navigation-actions.ts
+++ b/apps/dashboard/lib/dashboard-navigation-actions.ts
@@ -2,7 +2,7 @@ import type { DynamicQueryFilter } from "@/types/api";
export const DASHBOARD_FILTERS_QUERY_PARAM = "filters";
-export type DashboardActionParamValue =
+type DashboardActionParamValue =
| (boolean | number | string)[]
| boolean
| null
@@ -37,7 +37,7 @@ export const DASHBOARD_ACTION_TARGETS = [
"website.vitals",
] as const;
-export type DashboardActionTarget = (typeof DASHBOARD_ACTION_TARGETS)[number];
+type DashboardActionTarget = (typeof DASHBOARD_ACTION_TARGETS)[number];
const DASHBOARD_ACTION_TARGET_SET = new Set(DASHBOARD_ACTION_TARGETS);
@@ -193,7 +193,7 @@ function isSafeDashboardPath(pathname: string) {
return topLevel ? ALLOWED_TOP_LEVEL_SEGMENTS.has(topLevel) : false;
}
-export function normalizeDashboardHref(
+function normalizeDashboardHref(
href: string,
websiteId?: string | null
): string | null {
diff --git a/apps/dashboard/lib/flags/get-examples-strategy.ts b/apps/dashboard/lib/flags/get-examples-strategy.ts
deleted file mode 100644
index 14f55b033a..0000000000
--- a/apps/dashboard/lib/flags/get-examples-strategy.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-"use server";
-
-import { publicConfig } from "@databuddy/env/public";
-import { createServerFlagsManager } from "@databuddy/sdk/node";
-
-export interface ExamplesDisplayStrategy {
- dependencies?: {
- prerequisiteFlag: string;
- prerequisiteEnabled: boolean;
- };
- environment?: string;
- exampleCount: number; // 0, 3, or 6
- schedule?: {
- hasSchedule: boolean;
- nextChange?: string;
- };
- testCondition?: string; // Optional human-readable test condition
- variant: string; // Variant key (for debugging)
- variantValue: any; // The actual variant value
-}
-
-export async function getExamplesDisplayStrategy(
- websiteId: string,
- userId?: string,
- environment: string = process.env.NODE_ENV || "development"
-): Promise {
- const flagsManager = createServerFlagsManager({
- clientId: websiteId,
- apiUrl: publicConfig.urls.api,
- user: { userId },
- debug: process.env.NODE_ENV === "development",
- environment,
- });
-
- // Wait for initialization (important in serverless)
- await flagsManager.waitForInit();
-
- try {
- const result = await flagsManager.getFlag("flag-examples-display-strategy");
-
- console.log("🚀 Flag result:", result);
-
- const variantKey = (result.payload?.variantKey as string) || "unknown";
- const variantValue = result.value;
- const exampleCount = typeof variantValue === "number" ? variantValue : 0;
-
- return {
- exampleCount,
- variant: variantKey,
- variantValue,
- testCondition: "multi-variant-sticky-assignment",
- environment,
- };
- } catch (error) {
- console.error("❌ Error fetching examples display flag:", error);
-
- // Graceful fallback
- return {
- exampleCount: 6,
- variant: "error-fallback",
- variantValue: 6,
- testCondition: "error",
- environment,
- };
- }
-}
-
-export const getShouldShowExamples = async (
- websiteId: string,
- userId: string,
- environment: string
-) => {
- const flagsManager = createServerFlagsManager({
- clientId: websiteId,
- apiUrl: publicConfig.urls.api,
- user: { userId },
- debug: process.env.NODE_ENV === "development",
- environment,
- });
- await flagsManager.waitForInit();
- const flag = await flagsManager.getFlag("enable-flag-examples");
- return flag.value;
-};
diff --git a/apps/dashboard/lib/format-locale-number.ts b/apps/dashboard/lib/format-locale-number.ts
index 205235690a..e9c739eceb 100644
--- a/apps/dashboard/lib/format-locale-number.ts
+++ b/apps/dashboard/lib/format-locale-number.ts
@@ -1,6 +1,4 @@
const LOCALE = "en-US" as const;
-
-/** Stable number formatting for SSR + client (avoids locale mismatch hydration). */
export function formatLocaleNumber(value: number): string {
return value.toLocaleString(LOCALE);
}
diff --git a/apps/dashboard/lib/formatters.ts b/apps/dashboard/lib/formatters.ts
index bb88c00c01..3a1325ea3d 100644
--- a/apps/dashboard/lib/formatters.ts
+++ b/apps/dashboard/lib/formatters.ts
@@ -1,5 +1,3 @@
-import { dayjs } from "@databuddy/ui";
-
export const formatNumber = (value: number | null | undefined): string => {
if (value == null || Number.isNaN(value)) {
return "0";
@@ -24,56 +22,3 @@ export const formatCurrency = (
currency,
}).format(amount);
};
-
-// Predefined date formats for consistency across the app
-export const DATE_FORMATS = {
- DATE_ONLY: "MMM D, YYYY", // Jul 6, 2025
- DATE_TIME: "MMM D, YYYY HH:mm", // Jul 6, 2025 14:30
- DATE_TIME_SECONDS: "MMM D, YYYY HH:mm:ss", // Jul 6, 2025 14:30:25
- DATE_MONTH_DAY: "MMM D", // Jul 6
- ISO_DATE: "YYYY-MM-DD", // 2025-07-06
- TIME_ONLY: "HH:mm", // 14:30
- DATE_TIME_NO_YEAR: "MMM D, HH:mm", // Jul 6, 14:30
- DATE_TIME_12H: "MMM D, YYYY h:mm A", // Jul 6, 2025 2:30 PM
-} as const;
-
-// Global date formatting functions
-export const formatDate = (
- dateString: string | Date | undefined | null,
- format: string = DATE_FORMATS.DATE_ONLY
-): string => {
- if (!dateString) {
- return "";
- }
-
- try {
- const date = dayjs(dateString);
- if (!date.isValid()) {
- console.warn("Invalid date:", dateString);
- return "";
- }
- return date.format(format);
- } catch (error) {
- console.warn("Failed to format date:", dateString, error);
- return "";
- }
-};
-
-// Helper function for date ranges
-export const formatDateRange = (
- startDate: string | Date | undefined | null,
- endDate: string | Date | undefined | null,
- format: string = DATE_FORMATS.DATE_ONLY
-): string => {
- const start = formatDate(startDate, format);
- const end = formatDate(endDate, format);
-
- if (!(start && end)) {
- return "";
- }
- if (start === end) {
- return start;
- }
-
- return `${start} - ${end}`;
-};
diff --git a/apps/dashboard/lib/geo.ts b/apps/dashboard/lib/geo.ts
index b838c85184..01d1af27f6 100644
--- a/apps/dashboard/lib/geo.ts
+++ b/apps/dashboard/lib/geo.ts
@@ -1,24 +1,6 @@
import { useQuery } from "@tanstack/react-query";
const countriesGeoUrl = "https://cdn.databuddy.cc/geojson/countries.geojson";
-const subdivisionsGeoUrl = "https://cdn.databuddy.cc/geojson/subdivisions.json";
-
-export interface Subdivisions {
- features: Array<{
- type: string;
- properties: {
- name: string;
- iso_3166_2: string;
- admin: string;
- border: number;
- };
- geometry: {
- type: string;
- coordinates: number[][][];
- };
- }>;
- type: string;
-}
export interface Country {
features: Array<{
@@ -37,25 +19,8 @@ export interface Country {
type: string;
}
-export const useSubdivisions = () =>
- useQuery({
- queryKey: ["subdivisions"],
- queryFn: () => fetch(subdivisionsGeoUrl).then((res) => res.json()),
- });
-
export const useCountries = () =>
useQuery({
queryKey: ["countries"],
queryFn: () => fetch(countriesGeoUrl).then((res) => res.json()),
});
-
-export const useGetRegionName = () => {
- const { data: subdivisions } = useSubdivisions();
-
- return {
- getRegionName: (region: string) =>
- subdivisions?.features.find(
- (feature) => feature.properties.iso_3166_2 === region
- )?.properties.name,
- };
-};
diff --git a/apps/dashboard/lib/is-abort-error.ts b/apps/dashboard/lib/is-abort-error.ts
index 910dafae97..9048c5d119 100644
--- a/apps/dashboard/lib/is-abort-error.ts
+++ b/apps/dashboard/lib/is-abort-error.ts
@@ -1,4 +1,3 @@
-/** True when a fetch or query was cancelled (Strict Mode remount, navigation, etc.). */
export function isAbortError(error: unknown): boolean {
if (error instanceof Error) {
return (
diff --git a/apps/dashboard/lib/list-query-outcome.ts b/apps/dashboard/lib/list-query-outcome.ts
index 49986f1547..5f50c2c24b 100644
--- a/apps/dashboard/lib/list-query-outcome.ts
+++ b/apps/dashboard/lib/list-query-outcome.ts
@@ -1,9 +1,4 @@
import type { UseQueryResult } from "@tanstack/react-query";
-
-/**
- * Single place to derive list UI state from TanStack Query + optional gate loading.
- * Use with List.Content so pages don’t hand-roll loading → null → content flashes.
- */
export type ListQueryOutcome =
| { status: "empty" }
| { status: "error" }
@@ -40,8 +35,6 @@ export type ListQuerySlice = Pick<
UseQueryResult,
"data" | "isPending" | "isError" | "isSuccess"
>;
-
-/** Infers list state from a TanStack array query — pass to List.Content as `query` instead of calling listQueryOutcome yourself. */
export function listQueryOutcomeFromQuery(
query: ListQuerySlice,
options?: { gatePending?: boolean }
diff --git a/apps/dashboard/lib/orpc-public.ts b/apps/dashboard/lib/orpc-public.ts
deleted file mode 100644
index 00c891484a..0000000000
--- a/apps/dashboard/lib/orpc-public.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { publicConfig } from "@databuddy/env/public";
-import type { AppRouter } from "@databuddy/rpc";
-import { createORPCClient } from "@orpc/client";
-import { RPCLink } from "@orpc/client/fetch";
-import type { RouterClient } from "@orpc/server";
-
-const link = new RPCLink({
- url: `${publicConfig.urls.api}/rpc`,
-});
-
-export const publicRPCClient = createORPCClient(
- link
-) as RouterClient;
diff --git a/apps/dashboard/lib/query-client.ts b/apps/dashboard/lib/query-client.ts
index 4539de6ab5..fa28482694 100644
--- a/apps/dashboard/lib/query-client.ts
+++ b/apps/dashboard/lib/query-client.ts
@@ -65,7 +65,7 @@ function reportError(error: unknown, showToast = true) {
});
}
-export function makeQueryClient() {
+function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
diff --git a/apps/dashboard/lib/topup-math.ts b/apps/dashboard/lib/topup-math.ts
index 6c345c8b79..5b1c1d62b4 100644
--- a/apps/dashboard/lib/topup-math.ts
+++ b/apps/dashboard/lib/topup-math.ts
@@ -11,7 +11,7 @@ export const TOPUP_TIERS: readonly TopupTier[] = [
];
export const TOPUP_MIN_QUANTITY = 10;
-export const TOPUP_MAX_PURCHASE_USD = 5000;
+const TOPUP_MAX_PURCHASE_USD = 5000;
export const TOPUP_MAX_QUANTITY = maxTopupQuantityForAmount(
TOPUP_MAX_PURCHASE_USD
);
@@ -23,7 +23,7 @@ function tierTop(tier: TopupTier): number {
return tier.to === "inf" ? Number.POSITIVE_INFINITY : tier.to;
}
-export function maxTopupQuantityForAmount(
+function maxTopupQuantityForAmount(
maxAmount: number,
tiers: readonly TopupTier[] = TOPUP_TIERS
): number {
@@ -144,7 +144,7 @@ export function nextTierNudge(
return { unitsUntilNextTier, nextRate: info.nextRate };
}
-export const TOPUP_SLIDER_BOUNDARIES: readonly number[] = [
+const TOPUP_SLIDER_BOUNDARIES: readonly number[] = [
TOPUP_MIN_QUANTITY,
100,
1000,
diff --git a/apps/dashboard/lib/utils.ts b/apps/dashboard/lib/utils.ts
index c6feaa63ab..836d55c08a 100644
--- a/apps/dashboard/lib/utils.ts
+++ b/apps/dashboard/lib/utils.ts
@@ -28,14 +28,3 @@ export function formatDuration(seconds: number): string {
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
}
-
-export function getDefaultDateRange() {
- const today = new Date();
- const thirtyDaysAgo = new Date(today);
- thirtyDaysAgo.setDate(today.getDate() - 30);
- return {
- start_date: thirtyDaysAgo.toISOString().split("T")[0],
- end_date: today.toISOString().split("T")[0],
- granularity: "daily" as "hourly" | "daily",
- };
-}
diff --git a/apps/dashboard/lib/vitals-scoring.ts b/apps/dashboard/lib/vitals-scoring.ts
index ebb4171a64..ddf4f3b038 100644
--- a/apps/dashboard/lib/vitals-scoring.ts
+++ b/apps/dashboard/lib/vitals-scoring.ts
@@ -1,12 +1,12 @@
// RES uses Lighthouse 10 mobile weights: FCP 15%, LCP 30%, INP 30%, CLS 25%.
-export const RES_WEIGHTS = {
+const RES_WEIGHTS = {
FCP: 0.15,
LCP: 0.3,
INP: 0.3,
CLS: 0.25,
} as const;
-export type RESMetric = keyof typeof RES_WEIGHTS;
+type RESMetric = keyof typeof RES_WEIGHTS;
// Google Core Web Vitals good/poor thresholds.
const METRIC_THRESHOLDS = {
@@ -24,7 +24,7 @@ const SCORE_CURVES = {
CLS: { median: 0.1, p10: 0.25 },
} as const;
-export function calculateMetricScore(
+function calculateMetricScore(
value: number | null | undefined,
metric: RESMetric
): number | null {
diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json
index 3e297c7508..6a05f5c287 100644
--- a/apps/dashboard/package.json
+++ b/apps/dashboard/package.json
@@ -33,34 +33,24 @@
"@databuddy/ui": "workspace:*",
"@hello-pangea/dnd": "^18.0.1",
"@hookform/resolvers": "^5.2.2",
- "@json-render/react": "0.19.0",
"@orpc/client": "^1.14.0",
"@orpc/tanstack-query": "^1.14.0",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-collapsible": "^1.1.12",
- "@radix-ui/react-dialog": "^1.1.15",
- "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
- "@radix-ui/react-popover": "^1.1.15",
- "@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
- "@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
- "@radix-ui/react-tooltip": "^1.2.8",
"@radix-ui/react-use-controllable-state": "^1.2.2",
- "@tanstack/query-async-storage-persister": "^5.99.2",
"@tanstack/query-sync-storage-persister": "5.100.10",
"@tanstack/react-pacer": "0.22.1",
"@tanstack/react-query": "^5.99.2",
"@tanstack/react-query-persist-client": "5.100.10",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.14.5",
- "@tokenlens/models": "catalog:",
"@types/d3-scale": "^4.0.9",
"@types/geojson": "^7946.0.16",
"@types/leaflet": "^1.9.21",
- "@xyflow/react": "^12.10.1",
"ai": "^6.0.188",
"atmn": "^1.1.8",
"autumn-js": "catalog:",
@@ -71,14 +61,10 @@
"cmdk": "^1.1.1",
"cnfast": "^0.0.8",
"d3-scale": "^4.0.2",
- "dayjs": "^1.11.20",
- "embla-carousel-react": "^8.6.0",
"flag-icons": "^7.5.0",
"geist": "^1.7.0",
- "input-otp": "^1.4.2",
"jotai": "^2.18.1",
"leaflet": "^1.9.4",
- "maplibre-gl": "^5.20.2",
"motion": "^12.38.0",
"nanoid": "^5.1.7",
"next": "^16.2.6",
@@ -95,35 +81,30 @@
"react-hotkeys-hook": "^5.2.4",
"react-leaflet": "^5.0.0",
"react-qrcode-logo": "^4.0.0",
- "react-textarea-autosize": "^8.5.9",
"recharts": "^3.9.0",
"shiki": "^4.3.0",
"sonner": "^2.0.7",
"streamdown": "^2.5.0",
- "tokenlens": "catalog:",
"tw-animate-css": "^1.4.0",
"use-stick-to-bottom": "^1.1.3",
"vaul": "^1.1.2",
"zod": "catalog:"
},
"devDependencies": {
- "@biomejs/biome": "2.5.1",
"@orpc/server": "^1.14.0",
+ "@playwright/test": "^1.58.2",
"@tailwindcss/postcss": "^4.2.2",
- "@tanstack/react-query-devtools": "^5.99.2",
"@types/d3-geo": "^3.1.0",
"@types/node": "^26.0.0",
"@types/pg": "^8.18.0",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
- "@types/react-simple-maps": "^3.0.6",
"@types/topojson-client": "^3.1.5",
"husky": "^9.1.7",
"lint-staged": "^16.4.0",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3",
- "ultracite": "catalog:",
- "@playwright/test": "^1.58.2"
+ "ultracite": "catalog:"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,jsonc,css,scss}": [
diff --git a/apps/dashboard/stores/jotai/chartAtoms.ts b/apps/dashboard/stores/jotai/chartAtoms.ts
index e85e164286..3ba6be77a8 100644
--- a/apps/dashboard/stores/jotai/chartAtoms.ts
+++ b/apps/dashboard/stores/jotai/chartAtoms.ts
@@ -32,13 +32,6 @@ export const toggleMetricAtom = atom(
}
);
-export const visibleMetricsAtom = atom((get) => {
- const visibility = get(metricVisibilityAtom);
- return Object.entries(visibility)
- .filter(([, isVisible]) => isVisible)
- .map(([metric]) => metric);
-});
-
export interface RevenueMetricVisibilityState {
avg_transaction: boolean;
customers: boolean;
@@ -70,5 +63,3 @@ export const toggleRevenueMetricAtom = atom(
}));
}
);
-
-export const isRefreshingAtom = atom(false);
diff --git a/apps/dashboard/stores/jotai/filterAtoms.ts b/apps/dashboard/stores/jotai/filterAtoms.ts
index 8a440cc1ae..46fe8ef274 100644
--- a/apps/dashboard/stores/jotai/filterAtoms.ts
+++ b/apps/dashboard/stores/jotai/filterAtoms.ts
@@ -1,204 +1,9 @@
-import { dayjs, guessTimezone } from "@databuddy/ui";
import { atom } from "jotai";
import type { DynamicQueryFilter } from "./filter-types";
export type { DynamicQueryFilter } from "./filter-types";
import { RECOMMENDED_DEFAULTS } from "../../app/(main)/websites/[id]/_components/utils/tracking-defaults";
-import {
- enableAllAdvancedTracking,
- enableAllBasicTracking,
- enableAllOptimization,
-} from "../../app/(main)/websites/[id]/_components/utils/tracking-helpers";
import type { TrackingOptions } from "../../app/(main)/websites/[id]/_components/utils/types";
-export interface DateRangeState {
- endDate: Date;
- startDate: Date;
-}
-
-const initialStartDate = dayjs().subtract(30, "day").toDate();
-const initialEndDate = new Date();
-
-export const dateRangeAtom = atom({
- startDate: initialStartDate,
- endDate: initialEndDate,
-});
-
-export const formattedDateRangeAtom = atom((get) => {
- const { startDate, endDate } = get(dateRangeAtom);
- return {
- startDate: dayjs(startDate).isValid()
- ? dayjs(startDate).format("YYYY-MM-DD")
- : "",
- endDate: dayjs(endDate).isValid()
- ? dayjs(endDate).format("YYYY-MM-DD")
- : "",
- };
-});
-
-export type TimeGranularity = "daily" | "hourly";
-
-const MAX_HOURLY_DAYS = 7;
-const AUTO_HOURLY_DAYS = 2;
-
-export const timeGranularityAtom = atom("daily");
-
-export const setDateRangeAndAdjustGranularityAtom = atom(
- null,
- (_get, set, newRange: DateRangeState) => {
- set(dateRangeAtom, newRange);
-
- const rangeDays = dayjs(newRange.endDate).diff(newRange.startDate, "day");
-
- if (rangeDays > MAX_HOURLY_DAYS) {
- set(timeGranularityAtom, "daily");
- } else if (rangeDays <= AUTO_HOURLY_DAYS) {
- set(timeGranularityAtom, "hourly");
- }
- }
-);
-
-export const timezoneAtom = atom(guessTimezone());
-
-export type BasicFilterValue =
- | string[]
- | number[]
- | string
- | number
- | boolean
- | undefined;
-export interface BasicFilters {
- [key: string]: BasicFilterValue;
-}
-
-export const basicFiltersAtom = atom({});
-
-export type FilterOperator =
- | "is"
- | "isNot"
- | "contains"
- | "doesNotContain"
- | "startsWith"
- | "endsWith"
- | "greaterThan"
- | "lessThan"
- | "in"
- | "notIn"
- | "isSet"
- | "isNotSet";
-
-export interface ComplexFilter {
- field: string;
- id: string;
- operator: FilterOperator;
- value?: string | number | boolean | Array;
-}
-
-export const complexFiltersAtom = atom([]);
-
-export const setBasicFilterAtom = atom(
- null,
- (_get, set, { key, value }: { key: string; value: BasicFilterValue }) => {
- set(basicFiltersAtom, (prev) => {
- if (value === undefined) {
- const { [key]: _, ...rest } = prev;
- return rest;
- }
- return { ...prev, [key]: value };
- });
- }
-);
-
-export const clearBasicFilterAtom = atom(null, (_get, set, key?: string) => {
- if (key) {
- set(basicFiltersAtom, (prev) => {
- const { [key]: _, ...rest } = prev;
- return rest;
- });
- } else {
- set(basicFiltersAtom, {});
- }
-});
-
-export const upsertComplexFilterAtom = atom(
- null,
- (_get, set, filter: ComplexFilter) => {
- set(complexFiltersAtom, (prev) => {
- const existingIndex = prev.findIndex((f) => f.id === filter.id);
- if (existingIndex > -1) {
- const updatedFilters = [...prev];
- updatedFilters[existingIndex] = filter;
- return updatedFilters;
- }
- return [...prev, filter];
- });
- }
-);
-
-export const removeComplexFilterAtom = atom(
- null,
- (_get, set, filterId: string) => {
- set(complexFiltersAtom, (prev) => prev.filter((f) => f.id !== filterId));
- }
-);
-
-export const clearComplexFiltersAtom = atom(null, (_get, set) => {
- set(complexFiltersAtom, []);
-});
-
-export const clearAllFiltersAtom = atom(null, (_get, set) => {
- set(dateRangeAtom, { startDate: initialStartDate, endDate: initialEndDate });
- set(timeGranularityAtom, "daily");
- set(basicFiltersAtom, {});
- set(complexFiltersAtom, []);
-});
-
-export const activeFiltersForApiAtom = atom((get) => {
- const { startDate: fmtStartDate, endDate: fmtEndDate } = get(
- formattedDateRangeAtom
- );
- const granularityValue = get(timeGranularityAtom);
- const basicFiltersValue = get(basicFiltersAtom);
- const complexFiltersValue = get(complexFiltersAtom);
- const timezoneValue = get(timezoneAtom);
-
- const apiReadyBasicFilters: Record<
- string,
- string | number | boolean | undefined
- > = {};
- for (const key in basicFiltersValue) {
- if (Object.hasOwn(basicFiltersValue, key)) {
- const value = basicFiltersValue[key];
- if (Array.isArray(value)) {
- apiReadyBasicFilters[key] = value.join(",");
- } else {
- apiReadyBasicFilters[key] = value;
- }
- }
- }
-
- return {
- dateRange: { startDate: fmtStartDate, endDate: fmtEndDate },
- granularity: granularityValue,
- timezone: timezoneValue,
- basicFilters: apiReadyBasicFilters,
- complexFilters: complexFiltersValue,
- };
-});
-
-export const selectBasicFilterValueAtom = (key: string) =>
- atom((get) => get(basicFiltersAtom)[key]);
-
-export const selectComplexFilterByIdAtom = (id: string) =>
- atom((get) =>
- get(complexFiltersAtom).find((filter) => filter.id === id)
- );
-
-export const hasActiveSubFiltersAtom = atom((get) => {
- const basic = get(basicFiltersAtom);
- const complex = get(complexFiltersAtom);
- return Object.keys(basic).length > 0 || complex.length > 0;
-});
-
export const isAnalyticsRefreshingAtom = atom(false);
const dynamicQueryFiltersBaseAtom = atom<{
@@ -261,10 +66,6 @@ export const removeDynamicFilterAtom = atom(
}
);
-export const clearDynamicFiltersAtom = atom(null, (_get, set) => {
- set(dynamicQueryFiltersAtom, []);
-});
-
export type EditingSavedFilter = {
id: string;
name: string;
@@ -303,13 +104,6 @@ export const savedFiltersAtom = atom(
export const trackingOptionsAtom = atom(RECOMMENDED_DEFAULTS);
-export const setTrackingOptionsAtom = atom(
- null,
- (_get, set, newOptions: TrackingOptions) => {
- set(trackingOptionsAtom, newOptions);
- }
-);
-
export const toggleTrackingOptionAtom = atom(
null,
(get, set, option: keyof TrackingOptions) => {
@@ -320,22 +114,3 @@ export const toggleTrackingOptionAtom = atom(
});
}
);
-
-export const resetTrackingOptionsAtom = atom(null, (_get, set) => {
- set(trackingOptionsAtom, RECOMMENDED_DEFAULTS);
-});
-
-export const enableAllBasicTrackingAtom = atom(null, (get, set) => {
- const current = get(trackingOptionsAtom);
- set(trackingOptionsAtom, enableAllBasicTracking(current));
-});
-
-export const enableAllAdvancedTrackingAtom = atom(null, (get, set) => {
- const current = get(trackingOptionsAtom);
- set(trackingOptionsAtom, enableAllAdvancedTracking(current));
-});
-
-export const enableAllOptimizationAtom = atom(null, (get, set) => {
- const current = get(trackingOptionsAtom);
- set(trackingOptionsAtom, enableAllOptimization(current));
-});
diff --git a/apps/dashboard/test/e2e/utils/dashboard.ts b/apps/dashboard/test/e2e/utils/dashboard.ts
index cc0a63df3b..43f6ae7a54 100644
--- a/apps/dashboard/test/e2e/utils/dashboard.ts
+++ b/apps/dashboard/test/e2e/utils/dashboard.ts
@@ -39,7 +39,7 @@ export function idFromPath(url: string, segment: "links" | "websites"): string {
return match[1];
}
-export function organizationSelector(page: Page): Locator {
+function organizationSelector(page: Page): Locator {
return page.getByRole("button", { name: ORGANIZATION_TRIGGER_RE });
}
diff --git a/apps/dashboard/types/annotations.ts b/apps/dashboard/types/annotations.ts
index 7e5b0a2464..8be20e970d 100644
--- a/apps/dashboard/types/annotations.ts
+++ b/apps/dashboard/types/annotations.ts
@@ -1,10 +1,6 @@
-/**
- * Annotation types and interfaces for the chart annotations system
- */
+type AnnotationType = "point" | "line" | "range";
-export type AnnotationType = "point" | "line" | "range";
-
-export type ChartType = "metrics";
+type ChartType = "metrics";
export interface Annotation {
annotationType: AnnotationType;
@@ -54,14 +50,6 @@ export interface CreateAnnotationData {
yValue?: number;
}
-export interface UpdateAnnotationData {
- color?: string;
- id: string;
- isPublic?: boolean;
- tags?: string[];
- text?: string;
-}
-
export interface AnnotationColor {
label: string;
value: string;
@@ -73,12 +61,6 @@ export interface AnnotationTag {
value: string;
}
-export interface ListAnnotationsInput {
- chartContext: ChartContext;
- chartType: ChartType;
- websiteId: string;
-}
-
export interface AnnotationFormData {
color: string;
isPublic: boolean;
diff --git a/apps/dashboard/types/api.ts b/apps/dashboard/types/api.ts
index 7d3427183a..0b3942d022 100644
--- a/apps/dashboard/types/api.ts
+++ b/apps/dashboard/types/api.ts
@@ -1,6 +1,6 @@
import type { DataFilter } from "@databuddy/db/schema";
-export interface ParameterWithDates {
+interface ParameterWithDates {
end_date?: string;
granularity?: "hourly" | "daily";
id?: string;
diff --git a/apps/dashboard/types/billing.ts b/apps/dashboard/types/billing.ts
index c79088d9fe..0a94f29305 100644
--- a/apps/dashboard/types/billing.ts
+++ b/apps/dashboard/types/billing.ts
@@ -1,4 +1,4 @@
-export interface DailyUsageRow {
+interface DailyUsageRow {
date: string;
event_count: number;
}
@@ -9,7 +9,7 @@ export interface DailyUsageByTypeRow {
event_count: number;
}
-export interface EventTypeBreakdown {
+interface EventTypeBreakdown {
event_category: string;
event_count: number;
}
diff --git a/apps/dashboard/types/funnels.ts b/apps/dashboard/types/funnels.ts
index eea0aee80e..5ada1e94b2 100644
--- a/apps/dashboard/types/funnels.ts
+++ b/apps/dashboard/types/funnels.ts
@@ -32,7 +32,7 @@ export interface CreateFunnelData {
steps: FunnelStep[];
}
-export interface StepErrorInsight {
+interface StepErrorInsight {
count: number;
error_type: string;
message: string;
@@ -53,7 +53,7 @@ export interface FunnelStepAnalytics {
users: number;
}
-export interface FunnelErrorInsights {
+interface FunnelErrorInsights {
available: boolean;
dropoffs_with_errors: number;
error_correlation_rate: number;
diff --git a/apps/dashboard/types/invitations.ts b/apps/dashboard/types/invitations.ts
deleted file mode 100644
index 85b4cc8a49..0000000000
--- a/apps/dashboard/types/invitations.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-export type InvitationStatus = "pending" | "accepted" | "rejected" | "canceled";
-
-export type InvitationPageStatus =
- | "loading"
- | "ready"
- | "accepting"
- | "success"
- | "error"
- | "expired"
- | "already-accepted";
-
-export interface InvitationData {
- email: string;
- expiresAt: Date;
- id: string;
- inviterEmail: string;
- inviterId: string;
- organizationId: string;
- organizationName: string;
- organizationSlug: string;
- role: string;
- status: InvitationStatus;
- teamId?: string;
-}
diff --git a/apps/dashboard/types/outbound-links.ts b/apps/dashboard/types/outbound-links.ts
index 61dbed6a37..2af4cf6c6c 100644
--- a/apps/dashboard/types/outbound-links.ts
+++ b/apps/dashboard/types/outbound-links.ts
@@ -17,7 +17,7 @@ export interface OutboundDomainRow {
unique_users: number;
}
-export interface OutboundLinksSectionData {
+interface OutboundLinksSectionData {
outbound_domains: unknown[];
outbound_links: unknown[];
}
diff --git a/apps/dashboard/types/performance.ts b/apps/dashboard/types/performance.ts
deleted file mode 100644
index c1264b5fa4..0000000000
--- a/apps/dashboard/types/performance.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-export interface PerformanceEntry {
- _uniqueKey?: string;
- avg_cls?: number;
- avg_dom_ready_time?: number;
- avg_fcp?: number;
- avg_fid?: number;
- avg_inp?: number;
- avg_lcp?: number;
- avg_load_time: number;
- avg_render_time?: number;
- avg_ttfb?: number;
- country_code?: string;
- country_name?: string;
- measurements?: number;
- name: string;
- p50_cls?: number;
- p50_fcp?: number;
- p50_lcp?: number;
- p50_load_time?: number;
- pageviews?: number;
- visitors: number;
-}
-
-export interface PerformanceSummary {
- avgCLS?: number;
- avgFCP?: number;
- avgFID?: number;
- avgINP?: number;
- avgLCP?: number;
- avgLoadTime: number;
- fastPages: number;
- performanceScore: number;
- slowPages: number;
- totalPages: number;
-}
diff --git a/apps/dashboard/types/pulse-features.ts b/apps/dashboard/types/pulse-features.ts
deleted file mode 100644
index 63bc6ba4c1..0000000000
--- a/apps/dashboard/types/pulse-features.ts
+++ /dev/null
@@ -1,325 +0,0 @@
-import { PLAN_IDS, type PlanId } from "@databuddy/shared/types/features";
-
-/**
- * Pulse plan tiers - ordered from lowest to highest
- */
-export const PULSE_PLAN_IDS = {
- FREE: "pulse_free",
- PRO: "pulse_pro",
- BUSINESS: "pulse_business",
-} as const;
-
-export type PulsePlanId = (typeof PULSE_PLAN_IDS)[keyof typeof PULSE_PLAN_IDS];
-
-/** Plan tier hierarchy (index = tier level, higher = more features) */
-export const PULSE_PLAN_HIERARCHY: PulsePlanId[] = [
- PULSE_PLAN_IDS.FREE,
- PULSE_PLAN_IDS.PRO,
- PULSE_PLAN_IDS.BUSINESS,
-];
-
-/** Gated features - locked behind specific plans */
-export const PULSE_GATED_FEATURES = {
- // Basic checks
- BASIC_UPTIME_CHECKS: "basic_uptime_checks",
- EMAIL_ALERTS: "email_alerts",
- WEBHOOKS: "webhooks",
- PUBLIC_STATUS_PAGE: "public_status_page",
- DASHBOARD_INTEGRATION: "dashboard_integration",
- // Advanced checks
- SSL_CERTIFICATE_CHECKS: "ssl_certificate_checks",
- KEYWORD_CONTENT_MATCH: "keyword_content_match",
- MULTI_LOCATION_CHECKS: "multi_location_checks",
- ONE_MINUTE_FREQUENCY: "one_minute_frequency",
- // Enterprise features
- THIRTY_SECOND_FREQUENCY: "thirty_second_frequency",
- SYNTHETIC_TRANSACTIONS: "synthetic_transactions",
- ALERT_ESCALATION: "alert_escalation",
- SMS_VOICE_ALERTS: "sms_voice_alerts",
- HEARTBEAT_MONITORING: "heartbeat_monitoring",
- N_OUT_OF_M_FALSE_POSITIVE_REDUCTION: "n_out_of_m_false_positive_reduction",
-} as const;
-
-export type PulseGatedFeatureId =
- (typeof PULSE_GATED_FEATURES)[keyof typeof PULSE_GATED_FEATURES];
-
-/**
- * Plan feature matrix - edit this to control which features are enabled per plan
- */
-export const PULSE_PLAN_FEATURES: Record<
- PulsePlanId,
- Record
-> = {
- [PULSE_PLAN_IDS.FREE]: {
- [PULSE_GATED_FEATURES.BASIC_UPTIME_CHECKS]: true,
- [PULSE_GATED_FEATURES.EMAIL_ALERTS]: true,
- [PULSE_GATED_FEATURES.WEBHOOKS]: true,
- [PULSE_GATED_FEATURES.PUBLIC_STATUS_PAGE]: true,
- [PULSE_GATED_FEATURES.DASHBOARD_INTEGRATION]: true,
- [PULSE_GATED_FEATURES.SSL_CERTIFICATE_CHECKS]: false,
- [PULSE_GATED_FEATURES.KEYWORD_CONTENT_MATCH]: false,
- [PULSE_GATED_FEATURES.MULTI_LOCATION_CHECKS]: false,
- [PULSE_GATED_FEATURES.ONE_MINUTE_FREQUENCY]: false,
- [PULSE_GATED_FEATURES.THIRTY_SECOND_FREQUENCY]: false,
- [PULSE_GATED_FEATURES.SYNTHETIC_TRANSACTIONS]: false,
- [PULSE_GATED_FEATURES.ALERT_ESCALATION]: false,
- [PULSE_GATED_FEATURES.SMS_VOICE_ALERTS]: false,
- [PULSE_GATED_FEATURES.HEARTBEAT_MONITORING]: false,
- [PULSE_GATED_FEATURES.N_OUT_OF_M_FALSE_POSITIVE_REDUCTION]: false,
- },
- [PULSE_PLAN_IDS.PRO]: {
- [PULSE_GATED_FEATURES.BASIC_UPTIME_CHECKS]: true,
- [PULSE_GATED_FEATURES.EMAIL_ALERTS]: true,
- [PULSE_GATED_FEATURES.WEBHOOKS]: true,
- [PULSE_GATED_FEATURES.PUBLIC_STATUS_PAGE]: true,
- [PULSE_GATED_FEATURES.DASHBOARD_INTEGRATION]: true,
- [PULSE_GATED_FEATURES.SSL_CERTIFICATE_CHECKS]: true,
- [PULSE_GATED_FEATURES.KEYWORD_CONTENT_MATCH]: true,
- [PULSE_GATED_FEATURES.MULTI_LOCATION_CHECKS]: true,
- [PULSE_GATED_FEATURES.ONE_MINUTE_FREQUENCY]: true,
- [PULSE_GATED_FEATURES.THIRTY_SECOND_FREQUENCY]: false,
- [PULSE_GATED_FEATURES.SYNTHETIC_TRANSACTIONS]: false,
- [PULSE_GATED_FEATURES.ALERT_ESCALATION]: false,
- [PULSE_GATED_FEATURES.SMS_VOICE_ALERTS]: false,
- [PULSE_GATED_FEATURES.HEARTBEAT_MONITORING]: false,
- [PULSE_GATED_FEATURES.N_OUT_OF_M_FALSE_POSITIVE_REDUCTION]: false,
- },
- [PULSE_PLAN_IDS.BUSINESS]: {
- [PULSE_GATED_FEATURES.BASIC_UPTIME_CHECKS]: true,
- [PULSE_GATED_FEATURES.EMAIL_ALERTS]: true,
- [PULSE_GATED_FEATURES.WEBHOOKS]: true,
- [PULSE_GATED_FEATURES.PUBLIC_STATUS_PAGE]: true,
- [PULSE_GATED_FEATURES.DASHBOARD_INTEGRATION]: true,
- [PULSE_GATED_FEATURES.SSL_CERTIFICATE_CHECKS]: true,
- [PULSE_GATED_FEATURES.KEYWORD_CONTENT_MATCH]: true,
- [PULSE_GATED_FEATURES.MULTI_LOCATION_CHECKS]: true,
- [PULSE_GATED_FEATURES.ONE_MINUTE_FREQUENCY]: true,
- [PULSE_GATED_FEATURES.THIRTY_SECOND_FREQUENCY]: true,
- [PULSE_GATED_FEATURES.SYNTHETIC_TRANSACTIONS]: true,
- [PULSE_GATED_FEATURES.ALERT_ESCALATION]: true,
- [PULSE_GATED_FEATURES.SMS_VOICE_ALERTS]: true,
- [PULSE_GATED_FEATURES.HEARTBEAT_MONITORING]: true,
- [PULSE_GATED_FEATURES.N_OUT_OF_M_FALSE_POSITIVE_REDUCTION]: true,
- },
-};
-
-/** Plan limits and configuration */
-export interface PulsePlanLimits {
- /** Check frequency in minutes (or seconds for Business) */
- checkFrequencyMinutes?: number;
- checkFrequencySeconds?: number;
- /** Number of check locations for multi-location checks */
- checkLocations?: number;
- /** Data retention period */
- dataRetentionDays?: number;
- dataRetentionMonths?: number;
- /** Number of monitors included */
- includedMonitors: number;
-}
-
-/** Plan metadata including pricing and target audience */
-export interface PulsePlanMetadata {
- limits: PulsePlanLimits;
- name: string;
- priceUsdMonthly: number;
- targetUser: string;
-}
-
-/**
- * Plan metadata - pricing, limits, and target audience
- */
-export const PULSE_PLAN_METADATA: Record = {
- [PULSE_PLAN_IDS.FREE]: {
- name: "Pulse Free",
- priceUsdMonthly: 0,
- targetUser: "Hobbyists, Personal Projects",
- limits: {
- includedMonitors: 5,
- checkFrequencyMinutes: 5,
- dataRetentionDays: 30,
- },
- },
- [PULSE_PLAN_IDS.PRO]: {
- name: "Pulse Pro",
- priceUsdMonthly: 15,
- targetUser: "SMBs, Small Agencies",
- limits: {
- includedMonitors: 50,
- checkFrequencyMinutes: 1,
- dataRetentionMonths: 12,
- checkLocations: 3,
- },
- },
- [PULSE_PLAN_IDS.BUSINESS]: {
- name: "Pulse Business",
- priceUsdMonthly: 49,
- targetUser: "Growing SaaS, Dev Teams",
- limits: {
- includedMonitors: 200,
- checkFrequencySeconds: 30,
- dataRetentionMonths: 24,
- },
- },
-};
-
-interface PulseFeatureMeta {
- description: string;
- minPlan?: PulsePlanId;
- name: string;
- upgradeMessage: string;
-}
-
-export const PULSE_FEATURE_METADATA: Record<
- PulseGatedFeatureId,
- PulseFeatureMeta
-> = {
- [PULSE_GATED_FEATURES.BASIC_UPTIME_CHECKS]: {
- name: "Basic Uptime Checks",
- description: "HTTP/S, Ping, and Port monitoring",
- upgradeMessage: "Basic uptime checks are available on all plans",
- },
- [PULSE_GATED_FEATURES.EMAIL_ALERTS]: {
- name: "Email Alerts",
- description: "Receive email notifications when monitors go down",
- upgradeMessage: "Email alerts are available on all plans",
- },
- [PULSE_GATED_FEATURES.WEBHOOKS]: {
- name: "Webhooks",
- description: "Integrate with external services via webhooks",
- upgradeMessage: "Webhooks are available on all plans",
- },
- [PULSE_GATED_FEATURES.PUBLIC_STATUS_PAGE]: {
- name: "Public Status Page",
- description: "Share your service status publicly",
- upgradeMessage: "Public status pages are available on all plans",
- },
- [PULSE_GATED_FEATURES.DASHBOARD_INTEGRATION]: {
- name: "Dashboard Integration",
- description: "View monitor status in your dashboard",
- upgradeMessage: "Dashboard integration is available on all plans",
- },
- [PULSE_GATED_FEATURES.SSL_CERTIFICATE_CHECKS]: {
- name: "SSL Certificate Expiry Checks",
- description: "Monitor SSL certificate expiration dates",
- upgradeMessage: "Upgrade to Pro for SSL certificate checks",
- minPlan: PULSE_PLAN_IDS.PRO,
- },
- [PULSE_GATED_FEATURES.KEYWORD_CONTENT_MATCH]: {
- name: "Keyword/Content Match",
- description: "Verify specific content appears on monitored pages",
- upgradeMessage: "Upgrade to Pro for keyword/content matching",
- minPlan: PULSE_PLAN_IDS.PRO,
- },
- [PULSE_GATED_FEATURES.MULTI_LOCATION_CHECKS]: {
- name: "Multi-Location Checks",
- description: "Monitor from multiple geographic locations",
- upgradeMessage: "Upgrade to Pro for multi-location checks",
- minPlan: PULSE_PLAN_IDS.PRO,
- },
- [PULSE_GATED_FEATURES.ONE_MINUTE_FREQUENCY]: {
- name: "1-Minute Check Frequency",
- description: "Check your monitors every minute",
- upgradeMessage: "Upgrade to Pro for 1-minute check frequency",
- minPlan: PULSE_PLAN_IDS.PRO,
- },
- [PULSE_GATED_FEATURES.THIRTY_SECOND_FREQUENCY]: {
- name: "30-Second Check Frequency",
- description: "Check your monitors every 30 seconds",
- upgradeMessage: "Upgrade to Business for 30-second check frequency",
- minPlan: PULSE_PLAN_IDS.BUSINESS,
- },
- [PULSE_GATED_FEATURES.SYNTHETIC_TRANSACTIONS]: {
- name: "Synthetic Transactions",
- description: "Multi-step checks that simulate user workflows",
- upgradeMessage: "Upgrade to Business for synthetic transactions",
- minPlan: PULSE_PLAN_IDS.BUSINESS,
- },
- [PULSE_GATED_FEATURES.ALERT_ESCALATION]: {
- name: "Alert Escalation Policies",
- description: "Configure alert escalation rules",
- upgradeMessage: "Upgrade to Business for alert escalation",
- minPlan: PULSE_PLAN_IDS.BUSINESS,
- },
- [PULSE_GATED_FEATURES.SMS_VOICE_ALERTS]: {
- name: "SMS/Voice Call Alerts",
- description: "Receive alerts via SMS or voice calls via Twilio",
- upgradeMessage: "Upgrade to Business for SMS/voice alerts",
- minPlan: PULSE_PLAN_IDS.BUSINESS,
- },
- [PULSE_GATED_FEATURES.HEARTBEAT_MONITORING]: {
- name: "Heartbeat Monitoring",
- description: "Monitor applications that send heartbeat signals",
- upgradeMessage: "Upgrade to Business for heartbeat monitoring",
- minPlan: PULSE_PLAN_IDS.BUSINESS,
- },
- [PULSE_GATED_FEATURES.N_OUT_OF_M_FALSE_POSITIVE_REDUCTION]: {
- name: "N-out-of-M False Positive Reduction",
- description:
- "Reduce false positives by requiring N failures out of M checks",
- upgradeMessage:
- "Upgrade to Business for N-out-of-M false positive reduction",
- minPlan: PULSE_PLAN_IDS.BUSINESS,
- },
-};
-
-/**
- * Map Pulse plan to equivalent regular plan for feature checks
- * Pulse Free maps to regular Free plan
- */
-export function getRegularPlanForPulsePlan(
- pulsePlanId: PulsePlanId | string | null
-): PlanId {
- const pulsePlan = (pulsePlanId ?? PULSE_PLAN_IDS.FREE) as PulsePlanId;
-
- if (pulsePlan === PULSE_PLAN_IDS.FREE) {
- return PLAN_IDS.FREE;
- }
-
- // For other Pulse plans, return free as default
- // You can extend this mapping if needed
- return PLAN_IDS.FREE;
-}
-
-/** Check if a plan has access to a gated feature */
-export function isPulsePlanFeatureEnabled(
- planId: PulsePlanId | string | null,
- feature: PulseGatedFeatureId
-): boolean {
- const plan = (planId ?? PULSE_PLAN_IDS.FREE) as PulsePlanId;
- return PULSE_PLAN_FEATURES[plan]?.[feature] ?? false;
-}
-
-/** Get the minimum plan required for a feature */
-export function getMinimumPulsePlanForFeature(
- feature: PulseGatedFeatureId
-): PulsePlanId | null {
- for (const plan of PULSE_PLAN_HIERARCHY) {
- if (PULSE_PLAN_FEATURES[plan][feature]) {
- return plan;
- }
- }
- return null;
-}
-
-/** Get plan metadata */
-export function getPulsePlanMetadata(
- planId: PulsePlanId | string | null
-): PulsePlanMetadata {
- const plan = (planId ?? PULSE_PLAN_IDS.FREE) as PulsePlanId;
- return PULSE_PLAN_METADATA[plan] ?? PULSE_PLAN_METADATA[PULSE_PLAN_IDS.FREE];
-}
-
-/** Get plan limits */
-export function getPulsePlanLimits(
- planId: PulsePlanId | string | null
-): PulsePlanLimits {
- const metadata = getPulsePlanMetadata(planId);
- return metadata.limits;
-}
-
-/** Product information */
-export const PULSE_PRODUCT_INFO = {
- productName: "Databuddy Pulse",
- coreValueProposition:
- "Integrated, Privacy-First Uptime Monitoring: Know when your site is down, and why, without compromising user privacy.",
-} as const;
diff --git a/apps/dashboard/types/sessions.ts b/apps/dashboard/types/sessions.ts
index e3eab1b3f0..4261b6cf0f 100644
--- a/apps/dashboard/types/sessions.ts
+++ b/apps/dashboard/types/sessions.ts
@@ -1,8 +1,4 @@
-export type SessionEventSource =
- | "analytics"
- | "custom"
- | "error"
- | "outgoing_link";
+type SessionEventSource = "analytics" | "custom" | "error" | "outgoing_link";
export interface SessionEvent {
event_id: string;
@@ -13,7 +9,7 @@ export interface SessionEvent {
time: string;
}
-export interface SessionWebVital {
+interface SessionWebVital {
metric_name: string;
metric_value: number;
path: string;
@@ -100,4 +96,4 @@ export type RawSessionEventTuple = [
SessionEventSource?,
];
-export type RawSessionWebVitalTuple = [string, number, string, string];
+type RawSessionWebVitalTuple = [string, number, string, string];
diff --git a/apps/dashboard/types/website.ts b/apps/dashboard/types/website.ts
index 5c9825df32..c8fb34369c 100644
--- a/apps/dashboard/types/website.ts
+++ b/apps/dashboard/types/website.ts
@@ -1,4 +1,4 @@
-export interface MiniChartDataPoint {
+interface MiniChartDataPoint {
date: string;
value: number;
}
@@ -31,9 +31,3 @@ export interface LocationData {
countries: CountryData[];
regions: RegionData[];
}
-
-export interface WebsiteBasic {
- domain: string;
- id: string;
- name?: string | null;
-}
diff --git a/apps/docs/app/(home)/api/actions.ts b/apps/docs/app/(home)/api/actions.ts
index f6640b21a3..4cb319e023 100644
--- a/apps/docs/app/(home)/api/actions.ts
+++ b/apps/docs/app/(home)/api/actions.ts
@@ -8,7 +8,7 @@ interface QueryConfig {
defaultLimit: number;
}
-export interface QueryConfigWithMeta extends QueryConfig {
+interface QueryConfigWithMeta extends QueryConfig {
meta?: QueryBuilderMeta;
}
diff --git a/apps/docs/app/(home)/api/query-builder.ts b/apps/docs/app/(home)/api/query-builder.ts
index 472fdb92e3..3a9209e8ce 100644
--- a/apps/docs/app/(home)/api/query-builder.ts
+++ b/apps/docs/app/(home)/api/query-builder.ts
@@ -136,41 +136,6 @@ async function executeDynamicQuery(
}
}
-export async function executeQuery(
- startDate: string,
- endDate: string,
- queryRequest: DynamicQueryRequest,
- timezone = "UTC"
-): Promise {
- try {
- const result = await executeDynamicQuery(
- startDate,
- endDate,
- queryRequest,
- timezone
- );
-
- if ("batch" in result) {
- throw new Error("Unexpected batch response for single query");
- }
-
- return result;
- } catch {
- return {
- success: false,
- queryId: queryRequest.id,
- data: [],
- meta: {
- parameters: queryRequest.parameters,
- total_parameters: queryRequest.parameters.length,
- page: queryRequest.page || 1,
- limit: queryRequest.limit || 100,
- filters_applied: queryRequest.filters?.length || 0,
- },
- };
- }
-}
-
export async function executeBatchQueries(
startDate: string,
endDate: string,
diff --git a/apps/docs/app/(home)/api/types-query-builder.ts b/apps/docs/app/(home)/api/types-query-builder.ts
index efa77fe342..558e26271c 100644
--- a/apps/docs/app/(home)/api/types-query-builder.ts
+++ b/apps/docs/app/(home)/api/types-query-builder.ts
@@ -1,4 +1,4 @@
-export type QueryFieldType =
+type QueryFieldType =
| "string"
| "number"
| "boolean"
@@ -6,7 +6,7 @@ export type QueryFieldType =
| "datetime"
| "json";
-export interface QueryOutputField {
+interface QueryOutputField {
description?: string;
example?: string | number | boolean | null;
label?: string;
@@ -15,7 +15,7 @@ export interface QueryOutputField {
unit?: string;
}
-export type VisualizationType =
+type VisualizationType =
| "table"
| "timeseries"
| "bar"
diff --git a/apps/docs/app/(home)/api/types.ts b/apps/docs/app/(home)/api/types.ts
index ae56299b21..88ea2d6942 100644
--- a/apps/docs/app/(home)/api/types.ts
+++ b/apps/docs/app/(home)/api/types.ts
@@ -1,4 +1,4 @@
-export interface DynamicQueryFilter {
+interface DynamicQueryFilter {
field: string;
operator: string;
value: string | number | boolean;
@@ -16,7 +16,7 @@ export interface DynamicQueryRequest {
timeZone?: string;
}
-export interface ParameterResult {
+interface ParameterResult {
data: unknown[];
error?: string;
parameter: string;
diff --git a/apps/docs/app/(home)/calculator/_components/calculator-engine.ts b/apps/docs/app/(home)/calculator/_components/calculator-engine.ts
index 3d51b93d9c..94999e223b 100644
--- a/apps/docs/app/(home)/calculator/_components/calculator-engine.ts
+++ b/apps/docs/app/(home)/calculator/_components/calculator-engine.ts
@@ -4,8 +4,6 @@ import type { NormalizedPlan } from "@/app/(home)/pricing/_pricing/types";
import { RAW_PLANS } from "@/app/(home)/pricing/data";
const PLANS: NormalizedPlan[] = normalizePlans(RAW_PLANS);
-
-/** Literature-aligned band: share of visits without measurable consent / analytics visibility */
export const VISITOR_DATA_LOSS_RANGE_LOW = 0.4;
export const VISITOR_DATA_LOSS_RANGE_HIGH = 0.7;
@@ -50,9 +48,7 @@ export interface CalculatorOutputs {
lostConversions: number;
lostRevenueMonthly: number;
lostRevenueYearly: number;
- /** Same inputs, upper bound of literature band (visitor data loss) */
lostRevenueYearlyRangeHigh: number;
- /** Same inputs, lower bound of literature band (visitor data loss) */
lostRevenueYearlyRangeLow: number;
lostVisitors: number;
}
@@ -179,16 +175,6 @@ export const SCENARIOS: Scenario[] = SCENARIO_CONFIGS.map((config) => ({
outputs: calculateCookieBannerCost(config.inputs),
}));
-export function formatCurrency(value: number): string {
- if (value >= 1_000_000) {
- return `$${(value / 1_000_000).toFixed(1)}M`;
- }
- if (value >= 1000) {
- return `$${(value / 1000).toFixed(1)}K`;
- }
- return `$${Math.round(value).toLocaleString()}`;
-}
-
export function formatCurrencyFull(value: number): string {
return `$${Math.round(value).toLocaleString()}`;
}
diff --git a/apps/docs/app/(home)/calculator/page.tsx b/apps/docs/app/(home)/calculator/page.tsx
index 8afb03d754..3de41b7435 100644
--- a/apps/docs/app/(home)/calculator/page.tsx
+++ b/apps/docs/app/(home)/calculator/page.tsx
@@ -9,8 +9,6 @@ import { ScenariosSection } from "./_components/scenarios-section";
const TITLE = "Cookie Banner Cost Calculator";
const DESCRIPTION =
"Model unattributed revenue from the cookie-consent measurement gap: traffic, visitor-to-paid, revenue per conversion, and a 40–70% band. Not P&L impact.";
-
-/** Matches defaults: 50k visitors, 55% data loss, 1.5% visitor-to-paid, $50 - ~$248k/yr; ~$11/mo Databuddy at this volume */
const DEFAULT_OG_PARAMS = "revenue=247500&visitors=50000&cost=11";
interface PageProps {
diff --git a/apps/docs/app/(home)/manifesto/manifesto-data.ts b/apps/docs/app/(home)/manifesto/manifesto-data.ts
index 4d6d2da8ed..7f4d34dd8b 100644
--- a/apps/docs/app/(home)/manifesto/manifesto-data.ts
+++ b/apps/docs/app/(home)/manifesto/manifesto-data.ts
@@ -3,7 +3,7 @@ export type ManifestoBlock =
| { type: "callout"; text: string }
| { type: "prompts"; items: readonly string[] };
-export type ManifestoChapterId =
+type ManifestoChapterId =
| "analytics-is-broken"
| "context-is-everything"
| "privacy-is-the-default"
diff --git a/apps/docs/app/(home)/pricing/_pricing/best-plan.ts b/apps/docs/app/(home)/pricing/_pricing/best-plan.ts
index f2dc85f03b..ac0cd4f5d1 100644
--- a/apps/docs/app/(home)/pricing/_pricing/best-plan.ts
+++ b/apps/docs/app/(home)/pricing/_pricing/best-plan.ts
@@ -64,7 +64,7 @@ export function selectBestPlan(
return bestPlan;
}
-export function computeEnterpriseThreshold(plans: NormalizedPlan[]): number {
+function computeEnterpriseThreshold(plans: NormalizedPlan[]): number {
const sorted = [...plans].sort(
(a, b) => a.includedEventsMonthly - b.includedEventsMonthly
);
diff --git a/apps/docs/app/(home)/pricing/_pricing/estimator-scale.ts b/apps/docs/app/(home)/pricing/_pricing/estimator-scale.ts
index 804bdc2381..d3c214c27b 100644
--- a/apps/docs/app/(home)/pricing/_pricing/estimator-scale.ts
+++ b/apps/docs/app/(home)/pricing/_pricing/estimator-scale.ts
@@ -1,10 +1,10 @@
-export const SLIDER_THRESHOLDS: number[] = [
+const SLIDER_THRESHOLDS: number[] = [
0, 10_000, 100_000, 1_000_000, 10_000_000, 100_000_000, 250_000_000,
];
const SLIDER_SEGMENT_PCT = 100 / (SLIDER_THRESHOLDS.length - 1);
-export const clamp = (value: number, min: number, max: number): number =>
+const clamp = (value: number, min: number, max: number): number =>
Math.min(Math.max(value, min), max);
export function eventsToSliderValue(events: number): number {
diff --git a/apps/docs/app/(home)/pricing/_pricing/gated-feature-rows.tsx b/apps/docs/app/(home)/pricing/_pricing/gated-feature-rows.tsx
index 3c28940e5f..5d4afff7af 100644
--- a/apps/docs/app/(home)/pricing/_pricing/gated-feature-rows.tsx
+++ b/apps/docs/app/(home)/pricing/_pricing/gated-feature-rows.tsx
@@ -12,8 +12,6 @@ import {
import { CheckIcon, XMarkIcon as XIcon } from "@databuddy/ui/icons";
import Link from "next/link";
import type { ReactNode } from "react";
-
-/** Docs pricing column ids → shared plan ids (enterprise maps to Scale limits). */
const TABLE_PLAN_TO_SHARED: Record = {
free: PLAN_IDS.FREE,
hobby: PLAN_IDS.HOBBY,
diff --git a/apps/docs/app/(home)/pricing/_pricing/normalize.ts b/apps/docs/app/(home)/pricing/_pricing/normalize.ts
index f3543882d8..2027977729 100644
--- a/apps/docs/app/(home)/pricing/_pricing/normalize.ts
+++ b/apps/docs/app/(home)/pricing/_pricing/normalize.ts
@@ -1,7 +1,7 @@
import type { RawItem, RawPlan } from "../data";
import type { NormalizedPlan } from "./types";
-export function getPriceMonthly(items: RawItem[]): number {
+function getPriceMonthly(items: RawItem[]): number {
for (const item of items) {
if (item.type === "price") {
return item.price;
@@ -10,7 +10,7 @@ export function getPriceMonthly(items: RawItem[]): number {
return 0;
}
-export function getEventsInfo(items: RawItem[]): {
+function getEventsInfo(items: RawItem[]): {
included: number;
tiers: Array<{ to: number | "inf"; amount: number }> | null;
} {
@@ -33,7 +33,7 @@ export function getEventsInfo(items: RawItem[]): {
return { included, tiers };
}
-export function getAgentCreditsByInterval(
+function getAgentCreditsByInterval(
items: RawItem[],
interval: "day" | "month"
): number | null {
diff --git a/apps/docs/app/(home)/pricing/data.ts b/apps/docs/app/(home)/pricing/data.ts
index a8b46b0f7e..f25507fdaa 100644
--- a/apps/docs/app/(home)/pricing/data.ts
+++ b/apps/docs/app/(home)/pricing/data.ts
@@ -1,10 +1,10 @@
import { DATABUNNY_USAGE } from "@databuddy/shared/billing";
-export interface FeatureDisplay {
+interface FeatureDisplay {
plural: string;
singular: string;
}
-export interface RawFeature {
+interface RawFeature {
display: FeatureDisplay;
id: string;
name: string;
diff --git a/apps/docs/app/(home)/roadmap/roadmap-data.ts b/apps/docs/app/(home)/roadmap/roadmap-data.ts
index 0f74f5e94a..a1d0341228 100644
--- a/apps/docs/app/(home)/roadmap/roadmap-data.ts
+++ b/apps/docs/app/(home)/roadmap/roadmap-data.ts
@@ -1,7 +1,6 @@
import type {
RoadmapItem,
RoadmapMilestone,
- RoadmapQuarter,
RoadmapStats,
} from "./roadmap-types";
@@ -351,118 +350,7 @@ export const roadmapItems: RoadmapItem[] = [
},
];
-export const roadmapQuarters: RoadmapQuarter[] = [
- {
- id: "q4-2024",
- name: "Q4 2024",
- startDate: "2024-10-01",
- endDate: "2024-12-31",
- items: roadmapItems.filter(
- (item) =>
- item.targetDate &&
- item.targetDate >= "2024-10-01" &&
- item.targetDate <= "2024-12-31"
- ),
- },
- {
- id: "q1-2025",
- name: "Q1 2025",
- startDate: "2025-01-01",
- endDate: "2025-03-31",
- items: roadmapItems.filter(
- (item) =>
- item.targetDate &&
- item.targetDate >= "2025-01-01" &&
- item.targetDate <= "2025-03-31"
- ),
- },
- {
- id: "q2-2025",
- name: "Q2 2025",
- startDate: "2025-04-01",
- endDate: "2025-06-30",
- items: roadmapItems.filter(
- (item) =>
- item.targetDate &&
- item.targetDate >= "2025-04-01" &&
- item.targetDate <= "2025-06-30"
- ),
- },
- {
- id: "q3-2025",
- name: "Q3 2025",
- startDate: "2025-07-01",
- endDate: "2025-09-30",
- items: roadmapItems.filter(
- (item) =>
- item.targetDate &&
- item.targetDate >= "2025-07-01" &&
- item.targetDate <= "2025-09-30"
- ),
- },
- {
- id: "q4-2025",
- name: "Q4 2025",
- startDate: "2025-10-01",
- endDate: "2025-12-31",
- items: roadmapItems.filter(
- (item) =>
- item.targetDate &&
- item.targetDate >= "2025-10-01" &&
- item.targetDate <= "2025-12-31"
- ),
- },
- {
- id: "q1-2026",
- name: "Q1 2026",
- startDate: "2026-01-01",
- endDate: "2026-03-31",
- items: roadmapItems.filter(
- (item) =>
- item.targetDate &&
- item.targetDate >= "2026-01-01" &&
- item.targetDate <= "2026-03-31"
- ),
- },
- {
- id: "q2-2026",
- name: "Q2 2026",
- startDate: "2026-04-01",
- endDate: "2026-06-30",
- items: roadmapItems.filter(
- (item) =>
- item.targetDate &&
- item.targetDate >= "2026-04-01" &&
- item.targetDate <= "2026-06-30"
- ),
- },
- {
- id: "q3-2026",
- name: "Q3 2026",
- startDate: "2026-07-01",
- endDate: "2026-09-30",
- items: roadmapItems.filter(
- (item) =>
- item.targetDate &&
- item.targetDate >= "2026-07-01" &&
- item.targetDate <= "2026-09-30"
- ),
- },
- {
- id: "q4-2026",
- name: "Q4 2026",
- startDate: "2026-10-01",
- endDate: "2026-12-31",
- items: roadmapItems.filter(
- (item) =>
- item.targetDate &&
- item.targetDate >= "2026-10-01" &&
- item.targetDate <= "2026-12-31"
- ),
- },
-];
-
-export const roadmapMilestones: RoadmapMilestone[] = [
+const roadmapMilestones: RoadmapMilestone[] = [
{
id: "core-platform",
title: "Core Platform",
diff --git a/apps/docs/app/(home)/roadmap/roadmap-types.ts b/apps/docs/app/(home)/roadmap/roadmap-types.ts
index f2600ebd0b..74a3ddc75e 100644
--- a/apps/docs/app/(home)/roadmap/roadmap-types.ts
+++ b/apps/docs/app/(home)/roadmap/roadmap-types.ts
@@ -7,7 +7,7 @@ export type RoadmapStatus =
export type RoadmapPriority = "critical" | "high" | "medium" | "low";
-export type RoadmapCategory =
+type RoadmapCategory =
| "analytics"
| "AI"
| "integrations"
@@ -31,14 +31,6 @@ export interface RoadmapItem {
title: string;
}
-export interface RoadmapQuarter {
- endDate: string;
- id: string;
- items: RoadmapItem[];
- name: string; // e.g., "Q1 2024"
- startDate: string;
-}
-
export interface RoadmapStats {
cancelledItems: number;
completedItems: number;
diff --git a/apps/docs/app/api/pricing/accept-markdown.ts b/apps/docs/app/api/pricing/accept-markdown.ts
index d2015462a7..eca367b501 100644
--- a/apps/docs/app/api/pricing/accept-markdown.ts
+++ b/apps/docs/app/api/pricing/accept-markdown.ts
@@ -1,4 +1,3 @@
-/** True when `text/markdown` beats `text/html` on the Accept header. */
export function acceptMarkdownOverHtml(accept: string): boolean {
const q = new Map();
for (const part of accept.split(",")) {
diff --git a/apps/docs/app/api/pricing/build-response.ts b/apps/docs/app/api/pricing/build-response.ts
index d3a69aed02..b3f9e9120b 100644
--- a/apps/docs/app/api/pricing/build-response.ts
+++ b/apps/docs/app/api/pricing/build-response.ts
@@ -119,5 +119,3 @@ export function buildPricingApiPayload(request: Request) {
currency: "USD" as const,
};
}
-
-export type PricingApiPayload = ReturnType;
diff --git a/apps/docs/app/util/constants.ts b/apps/docs/app/util/constants.ts
index 0ac2c60d8e..6b22f4d11e 100644
--- a/apps/docs/app/util/constants.ts
+++ b/apps/docs/app/util/constants.ts
@@ -7,6 +7,3 @@ export const OPENAPI_SPEC_URL = `${SITE_URL}/openapi.json`;
export const API_OPENAPI_SPEC_URL = `${API_URL}/openapi.json`;
export const MCP_SERVER_URL = `${API_URL}/v1/mcp/`;
export const MCP_MANIFEST_URL = `${SITE_URL}/.well-known/mcp.json`;
-
-export const CHATGPT_PROMPT_URL =
- "https://chatgpt.com/?hints=search&prompt=Read+these+3+pages%3A%0A%0A-+https%3A%2F%2Fwww.databuddy.cc%0A-+https%3A%2F%2Fwww.databuddy.cc%2Fdocs%0A-+https%3A%2F%2Fwww.databuddy.cc%2Fpricing%0A%0AThen+explain+in+simple+terms+what+this+app+does%2C+and+why+i+should+care";
diff --git a/apps/docs/components/bits/liquid.tsx b/apps/docs/components/bits/liquid.tsx
deleted file mode 100644
index 13ade1826a..0000000000
--- a/apps/docs/components/bits/liquid.tsx
+++ /dev/null
@@ -1,199 +0,0 @@
-import { Mesh, Program, Renderer, Triangle } from "ogl";
-import type React from "react";
-import { useEffect, useRef } from "react";
-
-interface LiquidChromeProps extends React.HTMLAttributes {
- /** Amplitude of the distortion. Default is 0.6. */
- amplitude?: number;
- /** Base color as an RGB array. Default is [0.1, 0.1, 0.1]. */
- baseColor?: [number, number, number];
- /** Frequency modifier for the x distortion. Default is 2.5. */
- frequencyX?: number;
- /** Frequency modifier for the y distortion. Default is 1.5. */
- frequencyY?: number;
- /** Enable mouse/touch interaction. Default is true. */
- interactive?: boolean;
- /** Animation speed multiplier. Default is 1.0. */
- speed?: number;
-}
-
-export const LiquidChrome: React.FC = ({
- baseColor = [0.1, 0.1, 0.1],
- speed = 0.2,
- amplitude = 0.5,
- frequencyX = 3,
- frequencyY = 2,
- interactive = true,
- ...props
-}) => {
- const containerRef = useRef(null);
-
- useEffect(() => {
- if (!containerRef.current) {
- return;
- }
-
- const container = containerRef.current;
- // Enable built-in antialiasing.
- const renderer = new Renderer({ antialias: true });
- const gl = renderer.gl;
- gl.clearColor(1, 1, 1, 1);
-
- // Vertex shader: passes along position and uv.
- const vertexShader = `
- attribute vec2 position;
- attribute vec2 uv;
- varying vec2 vUv;
- void main() {
- vUv = uv;
- gl_Position = vec4(position, 0.0, 1.0);
- }
- `;
-
- // Fragment shader with the original vibrant color calculation.
- const fragmentShader = `
- precision highp float;
- uniform float uTime;
- uniform vec3 uResolution;
- uniform vec3 uBaseColor;
- uniform float uAmplitude;
- uniform float uFrequencyX;
- uniform float uFrequencyY;
- uniform vec2 uMouse;
- varying vec2 vUv;
-
- // Render function for a given uv coordinate.
- vec4 renderImage(vec2 uvCoord) {
- // Convert uvCoord (in [0,1]) to a fragment coordinate.
- vec2 fragCoord = uvCoord * uResolution.xy;
- // Map fragCoord to a normalized space.
- vec2 uv = (2.0 * fragCoord - uResolution.xy) / min(uResolution.x, uResolution.y);
-
- // Iterative cosine-based distortions.
- for (float i = 1.0; i < 10.0; i++){
- uv.x += uAmplitude / i * cos(i * uFrequencyX * uv.y + uTime + uMouse.x * 3.14159);
- uv.y += uAmplitude / i * cos(i * uFrequencyY * uv.x + uTime + uMouse.y * 3.14159);
- }
-
- // Add a liquid ripple effect based on the mouse position.
- vec2 diff = (uvCoord - uMouse);
- float dist = length(diff);
- float falloff = exp(-dist * 20.0);
- float ripple = sin(10.0 * dist - uTime * 2.0) * 0.03;
- uv += (diff / (dist + 0.0001)) * ripple * falloff;
-
- // Original vibrant color computation.
- vec3 color = uBaseColor / abs(sin(uTime - uv.y - uv.x));
- return vec4(color, 1.0);
- }
-
- void main() {
- // 3x3 supersampling for anti-aliasing.
- vec4 col = vec4(0.0);
- int samples = 0;
- for (int i = -1; i <= 1; i++){
- for (int j = -1; j <= 1; j++){
- vec2 offset = vec2(float(i), float(j)) * (1.0 / min(uResolution.x, uResolution.y));
- col += renderImage(vUv + offset);
- samples++;
- }
- }
- gl_FragColor = col / float(samples);
- }
- `;
-
- // Create geometry and program with uniforms.
- const geometry = new Triangle(gl);
- const program = new Program(gl, {
- vertex: vertexShader,
- fragment: fragmentShader,
- uniforms: {
- uTime: { value: 0 },
- uResolution: {
- value: new Float32Array([
- gl.canvas.width,
- gl.canvas.height,
- gl.canvas.width / gl.canvas.height,
- ]),
- },
- uBaseColor: { value: new Float32Array(baseColor) },
- uAmplitude: { value: amplitude },
- uFrequencyX: { value: frequencyX },
- uFrequencyY: { value: frequencyY },
- uMouse: { value: new Float32Array([0, 0]) },
- },
- });
- const mesh = new Mesh(gl, { geometry, program });
-
- // Resize handler.
- function resize() {
- const scale = 1;
- renderer.setSize(
- container.offsetWidth * scale,
- container.offsetHeight * scale
- );
- const resUniform = program.uniforms.uResolution.value as Float32Array;
- resUniform[0] = gl.canvas.width;
- resUniform[1] = gl.canvas.height;
- resUniform[2] = gl.canvas.width / gl.canvas.height;
- }
- window.addEventListener("resize", resize);
- resize();
-
- // Mouse and touch move handlers for interactivity.
- function handleMouseMove(event: MouseEvent) {
- const rect = container.getBoundingClientRect();
- const x = (event.clientX - rect.left) / rect.width;
- const y = 1 - (event.clientY - rect.top) / rect.height;
- const mouseUniform = program.uniforms.uMouse.value as Float32Array;
- mouseUniform[0] = x;
- mouseUniform[1] = y;
- }
-
- function handleTouchMove(event: TouchEvent) {
- if (event.touches.length > 0) {
- const touch = event.touches[0];
- const rect = container.getBoundingClientRect();
- const x = (touch.clientX - rect.left) / rect.width;
- const y = 1 - (touch.clientY - rect.top) / rect.height;
- const mouseUniform = program.uniforms.uMouse.value as Float32Array;
- mouseUniform[0] = x;
- mouseUniform[1] = y;
- }
- }
-
- if (interactive) {
- container.addEventListener("mousemove", handleMouseMove);
- container.addEventListener("touchmove", handleTouchMove, {
- passive: true,
- });
- }
-
- // Animation loop.
- let animationId: number;
- function update(t: number) {
- animationId = requestAnimationFrame(update);
- // Multiply time by speed to adjust the animation rate.
- program.uniforms.uTime.value = t * 0.001 * speed;
- renderer.render({ scene: mesh });
- }
- animationId = requestAnimationFrame(update);
-
- container.appendChild(gl.canvas);
-
- return () => {
- cancelAnimationFrame(animationId);
- window.removeEventListener("resize", resize);
- if (interactive) {
- container.removeEventListener("mousemove", handleMouseMove);
- container.removeEventListener("touchmove", handleTouchMove);
- }
- if (gl.canvas.parentElement) {
- gl.canvas.parentElement.removeChild(gl.canvas);
- }
- gl.getExtension("WEBGL_lose_context")?.loseContext();
- };
- }, [baseColor, speed, amplitude, frequencyX, frequencyY, interactive]);
-
- return
;
-};
diff --git a/apps/docs/components/bits/squares.tsx b/apps/docs/components/bits/squares.tsx
deleted file mode 100644
index 4d661d3ea6..0000000000
--- a/apps/docs/components/bits/squares.tsx
+++ /dev/null
@@ -1,197 +0,0 @@
-import type React from "react";
-import { useEffect, useRef } from "react";
-
-type CanvasStrokeStyle = string | CanvasGradient | CanvasPattern;
-
-interface GridOffset {
- x: number;
- y: number;
-}
-
-interface SquaresProps {
- borderColor?: CanvasStrokeStyle;
- direction?: "diagonal" | "up" | "right" | "down" | "left";
- hoverFillColor?: CanvasStrokeStyle;
- speed?: number;
- squareSize?: number;
-}
-
-const Squares: React.FC = ({
- direction = "right",
- speed = 1,
- borderColor = "#999",
- squareSize = 40,
- hoverFillColor = "#222",
-}) => {
- const canvasRef = useRef(null);
- const requestRef = useRef(null);
- const numSquaresX = useRef(0);
- const numSquaresY = useRef(0);
- const gridOffset = useRef({ x: 0, y: 0 });
- const hoveredSquareRef = useRef(null);
-
- useEffect(() => {
- const canvas = canvasRef.current;
- if (!canvas) {
- return;
- }
- const ctx = canvas.getContext("2d");
-
- const resizeCanvas = () => {
- canvas.width = canvas.offsetWidth;
- canvas.height = canvas.offsetHeight;
- numSquaresX.current = Math.ceil(canvas.width / squareSize) + 1;
- numSquaresY.current = Math.ceil(canvas.height / squareSize) + 1;
- };
-
- const drawSquare = (
- x: number,
- y: number,
- squareX: number,
- squareY: number
- ) => {
- if (!ctx) {
- return;
- }
-
- const startX = Math.floor(gridOffset.current.x / squareSize) * squareSize;
- const startY = Math.floor(gridOffset.current.y / squareSize) * squareSize;
-
- const isHovered =
- hoveredSquareRef.current &&
- Math.floor((x - startX) / squareSize) === hoveredSquareRef.current.x &&
- Math.floor((y - startY) / squareSize) === hoveredSquareRef.current.y;
-
- if (isHovered) {
- ctx.fillStyle = hoverFillColor;
- ctx.fillRect(squareX, squareY, squareSize, squareSize);
- }
-
- ctx.strokeStyle = borderColor;
- ctx.strokeRect(squareX, squareY, squareSize, squareSize);
- };
-
- const drawGradient = () => {
- if (!ctx) {
- return;
- }
-
- const gradient = ctx.createRadialGradient(
- canvas.width / 2,
- canvas.height / 2,
- 0,
- canvas.width / 2,
- canvas.height / 2,
- Math.sqrt(canvas.width ** 2 + canvas.height ** 2) / 2
- );
- gradient.addColorStop(0, "rgba(0, 0, 0, 0)");
- gradient.addColorStop(1, "#060606");
- gradient.addColorStop(0.5, "rgba(0, 0, 0, 0.5)");
-
- ctx.fillStyle = gradient;
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- };
-
- const drawGrid = () => {
- if (!ctx) {
- return;
- }
-
- ctx.clearRect(0, 0, canvas.width, canvas.height);
-
- const startX = Math.floor(gridOffset.current.x / squareSize) * squareSize;
- const startY = Math.floor(gridOffset.current.y / squareSize) * squareSize;
-
- for (let x = startX; x < canvas.width + squareSize; x += squareSize) {
- for (let y = startY; y < canvas.height + squareSize; y += squareSize) {
- const squareX = x - (gridOffset.current.x % squareSize);
- const squareY = y - (gridOffset.current.y % squareSize);
- drawSquare(x, y, squareX, squareY);
- }
- }
-
- drawGradient();
- };
-
- const updateAnimation = () => {
- const effectiveSpeed = Math.max(speed, 0.1);
- switch (direction) {
- case "right":
- gridOffset.current.x =
- (gridOffset.current.x - effectiveSpeed + squareSize) % squareSize;
- break;
- case "left":
- gridOffset.current.x =
- (gridOffset.current.x + effectiveSpeed + squareSize) % squareSize;
- break;
- case "up":
- gridOffset.current.y =
- (gridOffset.current.y + effectiveSpeed + squareSize) % squareSize;
- break;
- case "down":
- gridOffset.current.y =
- (gridOffset.current.y - effectiveSpeed + squareSize) % squareSize;
- break;
- case "diagonal":
- gridOffset.current.x =
- (gridOffset.current.x - effectiveSpeed + squareSize) % squareSize;
- gridOffset.current.y =
- (gridOffset.current.y - effectiveSpeed + squareSize) % squareSize;
- break;
- default:
- break;
- }
-
- drawGrid();
- requestRef.current = requestAnimationFrame(updateAnimation);
- };
-
- const handleMouseMove = (event: MouseEvent) => {
- const rect = canvas.getBoundingClientRect();
- const mouseX = event.clientX - rect.left;
- const mouseY = event.clientY - rect.top;
-
- const startX = Math.floor(gridOffset.current.x / squareSize) * squareSize;
- const startY = Math.floor(gridOffset.current.y / squareSize) * squareSize;
-
- const hoveredSquareX = Math.floor(
- (mouseX + gridOffset.current.x - startX) / squareSize
- );
- const hoveredSquareY = Math.floor(
- (mouseY + gridOffset.current.y - startY) / squareSize
- );
-
- if (
- !hoveredSquareRef.current ||
- hoveredSquareRef.current.x !== hoveredSquareX ||
- hoveredSquareRef.current.y !== hoveredSquareY
- ) {
- hoveredSquareRef.current = { x: hoveredSquareX, y: hoveredSquareY };
- }
- };
-
- const handleMouseLeave = () => {
- hoveredSquareRef.current = null;
- };
-
- window.addEventListener("resize", resizeCanvas);
- resizeCanvas();
-
- canvas.addEventListener("mousemove", handleMouseMove);
- canvas.addEventListener("mouseleave", handleMouseLeave);
- requestRef.current = requestAnimationFrame(updateAnimation);
-
- return () => {
- window.removeEventListener("resize", resizeCanvas);
- if (requestRef.current) {
- cancelAnimationFrame(requestRef.current);
- }
- canvas.removeEventListener("mousemove", handleMouseMove);
- canvas.removeEventListener("mouseleave", handleMouseLeave);
- };
- }, [direction, speed, borderColor, hoverFillColor, squareSize]);
-
- return ;
-};
-
-export default Squares;
diff --git a/apps/docs/components/compare/amplitude-mark-icon.tsx b/apps/docs/components/compare/amplitude-mark-icon.tsx
index 67a8c66fbd..a61120b2cd 100644
--- a/apps/docs/components/compare/amplitude-mark-icon.tsx
+++ b/apps/docs/components/compare/amplitude-mark-icon.tsx
@@ -1,5 +1,3 @@
-/** Amplitude mark vector (Iconify `logos:amplitude-icon`), fill uses `currentColor`. */
-
const AMPLITUDE_LOGO_PATH_D =
"M128 0c70.683 0 128 57.317 128 128s-57.317 128-128 128S0 198.734 0 128S57.317 0 128 0m-16.912 39.12c-15.782.051-30.073 25.445-42.359 75.412c-8.687-.103-16.655-.257-24.109-.36h-1.13c-.926-.052-1.851 0-2.777.103c-4.215.77-7.248 4.472-7.248 8.739c0 4.37 3.239 8.122 7.557 8.79l.102.103H64.72a578 578 0 0 0-5.706 29.404l-.72 4.164v.205a5.84 5.84 0 0 0 2.724 4.935c2.725 1.748 6.375.926 8.123-1.799l.154.154l11.566-37.063h55.724c4.266 16.141 8.687 32.745 14.548 48.373c3.135 8.379 10.435 27.913 22.67 28.016h.154c18.917 0 26.32-30.587 31.203-50.84c1.08-4.37 1.953-8.123 2.827-10.899l.36-1.13l.052-.167c.4-1.445-.416-2.988-1.851-3.483c-1.491-.514-3.187.257-3.701 1.799l-.412 1.13c-1.593 4.473-3.084 8.637-4.42 12.39l-.103.308c-8.225 23.184-11.926 33.774-19.277 33.774h-.463c-9.407 0-18.198-38.143-21.54-52.486c-.565-2.467-1.079-4.78-1.593-6.785h60.659c1.08 0 2.159-.257 3.136-.771l.047-.039a2 2 0 0 1 .21-.115l.308-.206l.154-.103c.155-.103.309-.206.463-.36l.227-.188c1.114-.969 1.903-2.321 2.24-3.719c.772-3.65-1.644-7.248-5.294-7.967h-.309c-.36-.052-.668-.103-1.028-.103l-.925-.103c-21.436-1.542-43.49-2.16-64.206-2.57l-.051-.155c-10.024-37.783-22.62-76.388-39.582-76.388m-.669 17.015c.874 0 1.697.514 2.416 1.44c1.748 2.775 4.832 8.995 9.408 22.772c3.135 9.459 6.528 21.23 10.126 34.904c-13.673-.205-27.45-.36-40.816-.514l-6.785-.051c7.66-29.918 16.964-52.588 23.8-57.934c.566-.36 1.183-.617 1.851-.617";
diff --git a/apps/docs/components/compare/comparison-page-view.tsx b/apps/docs/components/compare/comparison-page-view.tsx
index acbbf49a4a..73def7a772 100644
--- a/apps/docs/components/compare/comparison-page-view.tsx
+++ b/apps/docs/components/compare/comparison-page-view.tsx
@@ -16,7 +16,7 @@ import type {
PricingTier,
} from "@/lib/comparison-config";
-export type ComparisonPageType = "compare" | "switch_from" | "alternatives";
+type ComparisonPageType = "compare" | "switch_from" | "alternatives";
interface ComparisonPageViewProps {
competitor: CompetitorInfo;
diff --git a/apps/docs/components/docs-navbar.tsx b/apps/docs/components/docs-navbar.tsx
deleted file mode 100644
index 47469c2f93..0000000000
--- a/apps/docs/components/docs-navbar.tsx
+++ /dev/null
@@ -1,264 +0,0 @@
-"use client";
-
-import { Branding } from "@/components/logo";
-import { cn } from "@/lib/utils";
-import { CaretDownIcon } from "@databuddy/ui/icons";
-import { AnimatePresence, motion } from "motion/react";
-import Link from "next/link";
-import { useState } from "react";
-import { navMenu } from "./navbar";
-import { NavbarGithubDesktopLink } from "./navbar-github-desktop-link";
-import { NavbarGithubMobileLink } from "./navbar-github-mobile-link";
-import { NavbarMobileMenuButton } from "./navbar-mobile-menu-button";
-import { contents, type SidebarItem } from "./sidebar-content";
-
-export interface DocsNavbarProps {
- stars?: number | null;
-}
-
-export const DocsNavbar = ({ stars }: DocsNavbarProps) => {
- const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
- const [openSection, setOpenSection] = useState(0);
-
- const docsSecondaryNav = navMenu.filter((menu) => menu.name !== "Docs");
-
- const toggleSection = (index: number) => {
- setOpenSection(openSection === index ? -1 : index);
- };
-
- const githubDelayMs = (contents.length * 5 + docsSecondaryNav.length) * 30;
-
- return (
-
-
-
-
-
-
-
-
-
-
- {navMenu.map((menu) => (
-
- {menu.name}
-
- ))}
-
-
-
-
-
setIsMobileMenuOpen((open) => !open)}
- />
-
-
-
-
-
-
-
-
- {contents.map((section, sectionIndex) => (
-
-
toggleSection(sectionIndex)}
- type="button"
- >
-
-
-
- {section.title}
-
-
-
-
-
-
-
- {openSection === sectionIndex && (
-
-
- {section.list.map((item, itemIndex) => (
-
- setIsMobileMenuOpen(false)
- }
- transitionDelayMs={
- (sectionIndex * section.list.length +
- itemIndex) *
- 30
- }
- />
- ))}
-
-
- )}
-
-
- ))}
-
-
-
-
-
- {docsSecondaryNav.map((menu, index) => (
- setIsMobileMenuOpen(false)}
- style={{
- transitionDelay: isMobileMenuOpen
- ? `${(contents.length * 5 + index) * 30}ms`
- : "0ms",
- }}
- >
- {menu.name}
-
- ))}
- setIsMobileMenuOpen(false)}
- stars={stars}
- transitionDelayMs={githubDelayMs}
- />
-
-
-
-
-
- );
-};
-
-function MobileSidebarItem({
- isMobileMenuOpen,
- item,
- level = 0,
- onNavigateAction,
- transitionDelayMs,
-}: {
- isMobileMenuOpen: boolean;
- item: SidebarItem;
- level?: number;
- onNavigateAction: () => void;
- transitionDelayMs: number;
-}) {
- if (item.group) {
- return (
-
- );
- }
-
- if (item.children) {
- return (
- 0 && "ml-2")}>
-
0 && "py-1.5 text-xs"
- )}
- >
- {item.icon ? (
-
- ) : null}
- {item.title}
- {item.isNew ? : null}
-
-
- {item.children.map((child, childIndex) => (
-
- ))}
-
-
- );
- }
-
- const isExternal = item.href?.startsWith("http");
-
- return (
- 0 && "py-1.5 text-xs",
- isMobileMenuOpen
- ? "translate-x-0 opacity-100"
- : "-translate-x-4 opacity-0"
- )}
- href={item.href || "#"}
- onClick={onNavigateAction}
- rel={isExternal ? "noopener noreferrer" : undefined}
- style={{
- transitionDelay: isMobileMenuOpen ? `${transitionDelayMs}ms` : "0ms",
- }}
- target={isExternal ? "_blank" : undefined}
- >
-
- {item.icon ? (
-
- ) : null}
- {item.title}
- {item.isNew ? : null}
-
-
- );
-}
-
-function MobileNewBadge() {
- return (
-
- New
-
- );
-}
diff --git a/apps/docs/components/docs/accordion.tsx b/apps/docs/components/docs/accordion.tsx
index 433ecde8ff..2799bb969a 100644
--- a/apps/docs/components/docs/accordion.tsx
+++ b/apps/docs/components/docs/accordion.tsx
@@ -2,7 +2,6 @@
import type * as React from "react";
import { cn } from "@databuddy/ui";
-import { Accordion as DSAccordion } from "@databuddy/ui/client";
import { docsSurface } from "@/components/docs/docs-styles";
function Accordion({
@@ -17,58 +16,6 @@ function Accordion({
);
}
-function AccordionItem({
- className,
- children,
- ...props
-}: React.ComponentProps<"div"> & { value: string }) {
- return (
-
- {children}
-
- );
-}
-
-function AccordionTrigger({
- className,
- children,
- ...props
-}: React.ComponentProps<"button">) {
- return (
-
- {children}
-
- );
-}
-
-function AccordionContent({
- className,
- children,
-}: React.ComponentProps<"div">) {
- return (
- *:first-child]:mt-0 [&>*:last-child]:mb-0",
- className
- )}
- >
- {children}
-
- );
-}
-
interface AccordionsProps extends React.ComponentProps<"div"> {
collapsible?: boolean;
type?: "single" | "multiple";
@@ -82,10 +29,4 @@ function Accordions({ className, children, ...props }: AccordionsProps) {
);
}
-export {
- Accordion,
- AccordionContent,
- AccordionItem,
- AccordionTrigger,
- Accordions,
-};
+export { Accordion, Accordions };
diff --git a/apps/docs/components/docs/index.ts b/apps/docs/components/docs/index.ts
index c22e09fa29..8891b77cc0 100644
--- a/apps/docs/components/docs/index.ts
+++ b/apps/docs/components/docs/index.ts
@@ -1,13 +1,10 @@
export {
Accordion,
- AccordionContent,
- AccordionItem,
Accordions,
- AccordionTrigger,
} from "./accordion";
export { Callout } from "./callout";
export { Card, Cards } from "./card";
-export { CodeBlock, InlineCode, PreWrapper } from "./code-block";
+export { CodeBlock } from "./code-block";
export { LeapComponent } from "./leap-component";
-export { CompletedStep, Step, Steps } from "./steps";
-export { Tab, Tabs, TabsContent, TabsList, TabsTrigger } from "./tabs";
+export { Step, Steps } from "./steps";
+export { Tab, Tabs } from "./tabs";
diff --git a/apps/docs/components/docs/leap-component.tsx b/apps/docs/components/docs/leap-component.tsx
index c3dd0e9f99..2397b1e60d 100644
--- a/apps/docs/components/docs/leap-component.tsx
+++ b/apps/docs/components/docs/leap-component.tsx
@@ -79,5 +79,3 @@ export const LeapComponent = () => {
);
};
-
-export default LeapComponent;
diff --git a/apps/docs/components/docs/steps.tsx b/apps/docs/components/docs/steps.tsx
index 945cd36ce9..7398e40ba2 100644
--- a/apps/docs/components/docs/steps.tsx
+++ b/apps/docs/components/docs/steps.tsx
@@ -1,4 +1,3 @@
-import { CheckIcon } from "@databuddy/ui/icons";
import { cn } from "@databuddy/ui";
import React from "react";
@@ -67,43 +66,4 @@ function Step({
);
}
-interface CompletedStepProps extends Omit {
- stepNumber?: number;
-}
-
-function CompletedStep({
- className,
- title,
- stepNumber,
- total,
- children,
- ...props
-}: CompletedStepProps) {
- const isLast = stepNumber === total;
-
- return (
-
-
-
-
-
-
- {title && (
-
{title}
- )}
-
- {children}
-
-
-
- );
-}
-
-export { CompletedStep, Step, Steps };
+export { Step, Steps };
diff --git a/apps/docs/components/docs/tabs.tsx b/apps/docs/components/docs/tabs.tsx
index df11cd9d5a..b563463ecc 100644
--- a/apps/docs/components/docs/tabs.tsx
+++ b/apps/docs/components/docs/tabs.tsx
@@ -112,4 +112,4 @@ function Tab({ children }: TabProps) {
return <>{children}>;
}
-export { Tab, Tabs, TabsContent, TabsList, TabsTrigger };
+export { Tab, Tabs };
diff --git a/apps/docs/components/features.tsx b/apps/docs/components/features.tsx
deleted file mode 100644
index cc48796b45..0000000000
--- a/apps/docs/components/features.tsx
+++ /dev/null
@@ -1,240 +0,0 @@
-"use client";
-
-// Credits to better-auth for the inspiration
-
-import {
- TriangleWarningIcon as AlertTriangle,
- ChartBarIcon as BarChart3,
- CodeIcon as Code,
- GlobeIcon as Globe2Icon,
- PackageIcon as Package,
- PlusIcon as Plus,
- ShieldCheckIcon as Shield,
- TrendUpIcon as TrendingUp,
- UsersIcon as Users,
-} from "@databuddy/ui/icons";
-
-import { cn } from "@/lib/utils";
-import Testimonials from "./landing/testimonials";
-
-const whyWeExist = [
- {
- id: 1,
- label: "Bloated and creepy",
- title:
- "Most analytics tools are either bloated and creepy (hi Google)",
- description:
- "Google Analytics tracks everything, slows down your site, and requires cookie banners that hurt conversion rates.",
- icon: AlertTriangle,
- },
- {
- id: 2,
- label: "Minimal but useless",
- title: "Or minimal but useless (hi SimpleAnalytics)",
- description:
- "Simple tools give you basic pageviews but lack the depth developers need to make informed decisions about their products.",
- icon: BarChart3,
- },
- {
- id: 3,
- label: "Complex product analytics",
- title:
- 'Or "product analytics" platforms that need a data team to set up (hi PostHog)',
- description:
- "Enterprise tools are powerful but require dedicated data engineers and complex setup processes that small teams can't handle.",
- icon: Users,
- },
-];
-
-const whatYouGet = [
- {
- id: 4,
- label: "Privacy-First Approach",
- title:
- "Build trust & reduce legal risk with built-in GDPR/CCPA compliance .",
- description:
- "No cookies required, complete data anonymization, and full GDPR/CCPA compliance out of the box. Build user trust while staying compliant.",
- icon: Shield,
- },
- {
- id: 5,
- label: "Real-time Analytics",
- title:
- "Make data-driven decisions instantly with live dashboards .",
- description:
- "See your data update in real-time with beautiful dashboards. No data sampling means 100% accurate data for confident decision making.",
- icon: TrendingUp,
- },
- {
- id: 6,
- label: "Data Ownership",
- title: "Full control of your valuable business data .",
- description:
- "Your data stays yours. Export raw data, integrate with existing tools, and maintain complete control over your analytics.",
- icon: Users,
- },
- {
- id: 7,
- label: "Energy Efficient",
- title:
- "Up to 10x more eco-friendly with lower carbon footprint .",
- description:
- "Reduce your environmental impact with our energy-efficient analytics platform while maintaining powerful insights.",
- icon: Globe2Icon,
- },
- {
- id: 8,
- label: "100% Transparency",
- title: "Fully transparent, no hidden fees or data games .",
- description:
- "Clear pricing, open about what data we collect, and honest about our limitations. No vendor lock-in, export your data anytime, and only pay for what you actually use.",
- icon: Code,
- },
- {
- id: 9,
- label: "Lightweight",
- title:
- "Lightweight, no cookies, no fingerprinting, no consent needed .",
- description:
- "Databuddy is lightweight, no cookies, no fingerprinting, no consent needed. It's GDPR compliant out of the box.",
- icon: Code,
- },
-];
-
-export default function Features() {
- return (
-
-
- {/* Why We Exist Section */}
-
-
-
-
-
-
- Most analytics tools are either:
-
-
-
-
-
-
-
-
- {whyWeExist.map((item) => (
-
-
-
-
-
- {item.description}
-
-
-
- ))}
-
-
- {/* What You Get Section */}
-
-
-
-
-
-
- Everything you need to understand your users:
-
-
-
-
-
-
-
-
- {whatYouGet.map((item) => (
-
-
-
-
-
- {item.description}
-
-
-
- ))}
-
-
- {/* For Who Section */}
-
-
-
-
-
-
- If you're a developer, indie hacker, or small team who wants
- to:
-
-
-
-
- • Stop blindly shipping features
- • Stay GDPR-compliant without paying a lawyer
- • Avoid tracking your users like it's 2010
-
-
- Then Databuddy is for you.
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/docs/components/github-nav-mark.tsx b/apps/docs/components/github-nav-mark.tsx
index cdb1742870..7491182f61 100644
--- a/apps/docs/components/github-nav-mark.tsx
+++ b/apps/docs/components/github-nav-mark.tsx
@@ -21,15 +21,3 @@ export function GithubNavMark({ className }: GithubNavMarkProps) {
);
}
-
-export interface GithubStarsBadgeProps {
- stars: number;
-}
-
-export function GithubStarsBadge({ stars }: GithubStarsBadgeProps) {
- return (
-
- {stars.toLocaleString()} ★
-
- );
-}
diff --git a/apps/docs/components/landing/demo-primitives.tsx b/apps/docs/components/landing/demo-primitives.tsx
index 64f0d05df2..c817c638cd 100644
--- a/apps/docs/components/landing/demo-primitives.tsx
+++ b/apps/docs/components/landing/demo-primitives.tsx
@@ -8,7 +8,6 @@ import { Button } from "@databuddy/ui";
import { cn } from "@/lib/utils";
export {
- CELL_TITLE_CLASS,
EASE,
TH,
TH_RIGHT,
diff --git a/apps/docs/components/landing/error-who-it-affects-artifacts.tsx b/apps/docs/components/landing/error-who-it-affects-artifacts.tsx
index 4aff134d9c..076691f188 100644
--- a/apps/docs/components/landing/error-who-it-affects-artifacts.tsx
+++ b/apps/docs/components/landing/error-who-it-affects-artifacts.tsx
@@ -108,47 +108,3 @@ export function ErrorImpactTableArtifact() {
);
}
-
-const RELEASES: { version: string; status: string; tone: "good" | "bad" }[] = [
- { version: "v1.2.3", status: "stable", tone: "good" },
- { version: "v1.2.4", status: "14 new errors", tone: "bad" },
- { version: "v1.2.5", status: "resolved", tone: "good" },
-];
-
-export function ReleaseTimelineArtifact() {
- return (
-
-
- {RELEASES.map((rel, index) => (
-
- {index < RELEASES.length - 1 ? (
-
- ) : null}
-
-
-
- {rel.version}
- {rel.tone === "bad" ? (
- {rel.status}
- ) : (
-
- {rel.status}
-
- )}
-
-
-
- ))}
-
-
- );
-}
diff --git a/apps/docs/components/landing/faq-section.tsx b/apps/docs/components/landing/faq-section.tsx
index 0747ecc3fe..b7735d8dc3 100644
--- a/apps/docs/components/landing/faq-section.tsx
+++ b/apps/docs/components/landing/faq-section.tsx
@@ -9,7 +9,7 @@ import {
import { cn } from "@/lib/utils";
import { SectionBullet } from "../icons/section-bullet";
-export interface FaqItem {
+interface FaqItem {
answer: string;
question: string;
}
diff --git a/apps/docs/components/landing/home-insights-showcase.tsx b/apps/docs/components/landing/home-insights-showcase.tsx
deleted file mode 100644
index 8c23be0889..0000000000
--- a/apps/docs/components/landing/home-insights-showcase.tsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import { HouseIcon, LightbulbIcon } from "@databuddy/ui/icons";
-import Image from "next/image";
-
-const HOME_INSIGHTS_SCREENSHOT = "/brand/dashboard-home-insights.png";
-
-export function HomeInsightsShowcase() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Dashboard home
-
-
- Your home page opens with the answer.
-
-
- The first thing you see is the work Databuddy already did:
- priority, evidence, and what to do next.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Investigation cards live here
-
-
-
- The dashboard starts with the highest-impact answers, not an
- empty analytics canvas.
-
-
-
-
-
-
- );
-}
diff --git a/apps/docs/components/landing/trusted-by.tsx b/apps/docs/components/landing/trusted-by.tsx
index 330ee66251..2b8cdbc942 100644
--- a/apps/docs/components/landing/trusted-by.tsx
+++ b/apps/docs/components/landing/trusted-by.tsx
@@ -222,5 +222,3 @@ export function TrustedBy() {
);
}
-
-export default TrustedBy;
diff --git a/apps/docs/components/landing/uptime-landing-visuals.tsx b/apps/docs/components/landing/uptime-landing-visuals.tsx
index 40bfc5b3d1..74dcfca254 100644
--- a/apps/docs/components/landing/uptime-landing-visuals.tsx
+++ b/apps/docs/components/landing/uptime-landing-visuals.tsx
@@ -6,10 +6,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { feature } from "topojson-client";
import type { GeometryCollection, Topology } from "topojson-specification";
import worldTopo from "world-atlas/countries-110m.json";
-
-// ---------------------------------------------------------------------------
// UptimeRegionsHubDiagram — animated D3 canvas world map with zone dithering
-// ---------------------------------------------------------------------------
const ZONE_DEFS = [
{ ids: new Set([840, 124, 484, 304]) },
{ ids: new Set([826, 372, 250, 724, 620, 380, 56, 528, 300, 196]) },
@@ -224,10 +221,7 @@ export function UptimeRegionsHubDiagram() {
);
}
-
-// ---------------------------------------------------------------------------
// UptimeAlertsStackVisual — focus-aware carousel
-// ---------------------------------------------------------------------------
const ROW_H = 62;
const GAP = 8;
const STRIDE = ROW_H + GAP;
@@ -443,10 +437,7 @@ export function UptimeAlertsStackVisual() {
);
}
-
-// ---------------------------------------------------------------------------
// UptimeStatusPageMiniVisual
-// ---------------------------------------------------------------------------
const DEMO_MONITORS = [
{
name: "Dashboard",
@@ -556,10 +547,7 @@ export function UptimeStatusPageMiniVisual() {
);
}
-
-// ---------------------------------------------------------------------------
// UptimeIncidentTimelineVisual
-// ---------------------------------------------------------------------------
const TIMELINE = [
{ time: "14:32", label: "Downtime detected", tone: "red" as const },
{ time: "14:33", label: "Team alerted via Slack", tone: "amber" as const },
diff --git a/apps/docs/components/landing/web-vitals-breakdown-demo.tsx b/apps/docs/components/landing/web-vitals-breakdown-demo.tsx
index 8fb6aa64f1..08ee52f2d7 100644
--- a/apps/docs/components/landing/web-vitals-breakdown-demo.tsx
+++ b/apps/docs/components/landing/web-vitals-breakdown-demo.tsx
@@ -438,8 +438,6 @@ const breakdownShellClass =
const breakdownMaskClass =
"pointer-events-none absolute inset-0 z-0 rounded border border-border/50 [-webkit-mask-image:linear-gradient(to_bottom,black_0%,black_60%,transparent_100%)] [mask-image:linear-gradient(to_bottom,black_0%,black_60%,transparent_100%)]";
-
-/** Short bottom fade — matches browser + page breakdown + percentile demos. */
const breakdownFadeClass =
"pointer-events-none absolute inset-x-0 bottom-0 z-10 h-24 bg-linear-to-t from-background/100 via-background/50 to-transparent";
diff --git a/apps/docs/components/landing/web-vitals-graphs-demo.tsx b/apps/docs/components/landing/web-vitals-graphs-demo.tsx
index 62e2f8eb02..ac1b0af9dd 100644
--- a/apps/docs/components/landing/web-vitals-graphs-demo.tsx
+++ b/apps/docs/components/landing/web-vitals-graphs-demo.tsx
@@ -9,8 +9,6 @@ const STATUS_STROKE = {
"needs-improvement": "stroke-amber-400",
poor: "stroke-red-400",
} as const;
-
-/** Ring fill (0–100) and display values aligned with the marketing gauge reference. */
const GAUGES = [
{
label: "LCP",
diff --git a/apps/docs/components/logo.tsx b/apps/docs/components/logo.tsx
index 6be6c13cbd..c3ef51f07b 100644
--- a/apps/docs/components/logo.tsx
+++ b/apps/docs/components/logo.tsx
@@ -1,9 +1,6 @@
import Link from "next/link";
import { Branding } from "./logo/branding";
-export type { BrandingProps, BrandVariant } from "./logo/branding";
-export { Branding } from "./logo/branding";
-
export function LogoContent() {
return (
diff --git a/apps/docs/components/logo/branding.tsx b/apps/docs/components/logo/branding.tsx
index ebfb16ce83..348c4f2c8e 100644
--- a/apps/docs/components/logo/branding.tsx
+++ b/apps/docs/components/logo/branding.tsx
@@ -1,20 +1,14 @@
import Image from "next/image";
import { cn } from "@/lib/utils";
-export type BrandVariant =
- | "logomark"
- | "wordmark"
- | "primary-logo"
- | "secondary-logo";
+type BrandVariant = "logomark" | "wordmark" | "primary-logo" | "secondary-logo";
export interface BrandingProps {
className?: string;
- /** Height of the primary asset in pixels (width follows the SVG viewBox aspect ratio). */
heightPx?: number;
imageClassName?: string;
priority?: boolean;
variant: BrandVariant;
- /** When `variant` is `logomark`, also show the wordmark asset beside the icon. */
wordmark?: boolean;
}
@@ -33,7 +27,6 @@ const BRAND_PATH: Record
= {
};
interface ThemeBrandImageProps {
- /** Primary image alt; the dark-mode twin is decorative. */
alt: string;
basePath: string;
className?: string;
diff --git a/apps/docs/components/nav-link.tsx b/apps/docs/components/nav-link.tsx
index 3369375076..417251a85e 100644
--- a/apps/docs/components/nav-link.tsx
+++ b/apps/docs/components/nav-link.tsx
@@ -1,7 +1,7 @@
import Link from "next/link";
import type { CSSProperties, ReactNode } from "react";
-export type NavSection =
+type NavSection =
| "navbar"
| "navbar_mobile"
| "navbar_features"
diff --git a/apps/docs/components/navbar-github-desktop-link.tsx b/apps/docs/components/navbar-github-desktop-link.tsx
deleted file mode 100644
index 20631a3e66..0000000000
--- a/apps/docs/components/navbar-github-desktop-link.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Button } from "@databuddy/ui";
-import {
- GithubNavMark,
- GithubStarsBadge,
- githubRepoUrl,
-} from "./github-nav-mark";
-
-interface NavbarGithubDesktopLinkProps {
- stars?: number | null;
-}
-
-export function NavbarGithubDesktopLink({
- stars,
-}: NavbarGithubDesktopLinkProps) {
- return (
-
-
-
- {typeof stars === "number" && }
-
-
- );
-}
diff --git a/apps/docs/components/navbar-github-mobile-link.tsx b/apps/docs/components/navbar-github-mobile-link.tsx
deleted file mode 100644
index dabbdad537..0000000000
--- a/apps/docs/components/navbar-github-mobile-link.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import Link from "next/link";
-import { docsNavMobileItem } from "@/components/docs-nav-styles";
-import { cn } from "@/lib/utils";
-import {
- GithubNavMark,
- GithubStarsBadge,
- githubRepoUrl,
-} from "./github-nav-mark";
-
-interface NavbarGithubMobileLinkProps {
- density?: "default" | "compact";
- isMenuOpen: boolean;
- onCloseAction: () => void;
- stars?: number | null;
- transitionDelayMs: number;
-}
-
-export function NavbarGithubMobileLink({
- stars,
- isMenuOpen,
- transitionDelayMs,
- onCloseAction,
- density = "default",
-}: NavbarGithubMobileLinkProps) {
- const densityClass =
- density === "compact" ? "px-3 py-2 text-sm" : "px-4 py-3 text-base";
-
- return (
-
-
-
- GitHub
- {typeof stars === "number" && }
-
-
- );
-}
diff --git a/apps/docs/components/navbar.tsx b/apps/docs/components/navbar.tsx
index f148dc96e4..3eafb5c7d2 100644
--- a/apps/docs/components/navbar.tsx
+++ b/apps/docs/components/navbar.tsx
@@ -191,15 +191,13 @@ export const Navbar = ({ stars, variant = "default" }: NavbarProps) => {
);
};
-export { iconBtn as navIconBtn };
-
-export interface NavMenuItem {
+interface NavMenuItem {
name: string;
path: string;
trackId: string;
}
-export const navMenu: NavMenuItem[] = [
+const navMenu: NavMenuItem[] = [
{ name: "Docs", path: "/docs", trackId: "docs" },
{ name: "Pricing", path: "/pricing", trackId: "pricing" },
{ name: "Compare", path: "/compare", trackId: "compare" },
diff --git a/apps/docs/components/structured-data.tsx b/apps/docs/components/structured-data.tsx
index 58b69d923e..4ed20d34af 100644
--- a/apps/docs/components/structured-data.tsx
+++ b/apps/docs/components/structured-data.tsx
@@ -50,8 +50,6 @@ type ElementItem =
interface StructuredDataProps {
baseUrl?: string; // default: https://www.databuddy.cc
-
- /** Mixed, repeatable elements */
elements?: ElementItem[];
logoUrl?: string; // default: {baseUrl}/logo.png
diff --git a/apps/docs/components/theme-toggle.tsx b/apps/docs/components/theme-toggle.tsx
deleted file mode 100644
index 4b073b31b8..0000000000
--- a/apps/docs/components/theme-toggle.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-"use client";
-
-import { MoonIcon, SunIcon } from "@databuddy/ui/icons";
-import { useTheme } from "next-themes";
-import { useEffect, useState } from "react";
-
-interface ThemeToggleProps {
- className?: string;
-}
-
-export function ThemeToggle({ className }: ThemeToggleProps) {
- const { resolvedTheme, setTheme } = useTheme();
- const [mounted, setMounted] = useState(false);
-
- useEffect(() => setMounted(true), []);
-
- const cycle = () => {
- const next = resolvedTheme === "light" ? "dark" : "light";
- if ("startViewTransition" in document) {
- document.startViewTransition(() => setTheme(next));
- } else {
- setTheme(next);
- }
- };
-
- const Icon = mounted && resolvedTheme === "dark" ? MoonIcon : SunIcon;
-
- return (
-
-
-
- );
-}
diff --git a/apps/docs/components/ui/alert-dialog.tsx b/apps/docs/components/ui/alert-dialog.tsx
deleted file mode 100644
index 59257878bc..0000000000
--- a/apps/docs/components/ui/alert-dialog.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-"use client";
-
-import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
-import type * as React from "react";
-import { buttonVariants } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-
-function AlertDialog({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function AlertDialogTrigger({
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function AlertDialogPortal({
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function AlertDialogOverlay({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function AlertDialogContent({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
-
- );
-}
-
-function AlertDialogHeader({
- className,
- ...props
-}: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function AlertDialogFooter({
- className,
- ...props
-}: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function AlertDialogTitle({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function AlertDialogDescription({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function AlertDialogAction({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function AlertDialogCancel({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-export {
- AlertDialog,
- AlertDialogAction,
- AlertDialogCancel,
- AlertDialogContent,
- AlertDialogDescription,
- AlertDialogFooter,
- AlertDialogHeader,
- AlertDialogOverlay,
- AlertDialogPortal,
- AlertDialogTitle,
- AlertDialogTrigger,
-};
diff --git a/apps/docs/components/ui/alert.tsx b/apps/docs/components/ui/alert.tsx
deleted file mode 100644
index 068be82a2a..0000000000
--- a/apps/docs/components/ui/alert.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { cva, type VariantProps } from "class-variance-authority";
-import type * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-const alertVariants = cva(
- "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
- {
- variants: {
- variant: {
- default: "bg-card text-card-foreground",
- destructive:
- "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
- },
- },
- defaultVariants: {
- variant: "default",
- },
- }
-);
-
-function Alert({
- className,
- variant,
- ...props
-}: React.ComponentProps<"div"> & VariantProps) {
- return (
-
- );
-}
-
-function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-function AlertDescription({
- className,
- ...props
-}: React.ComponentProps<"div">) {
- return (
-
- );
-}
-
-export { Alert, AlertDescription, AlertTitle };
diff --git a/apps/docs/components/ui/aspect-ratio.tsx b/apps/docs/components/ui/aspect-ratio.tsx
deleted file mode 100644
index 51fa2c6285..0000000000
--- a/apps/docs/components/ui/aspect-ratio.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-"use client";
-
-import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
-
-function AspectRatio({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-export { AspectRatio };
diff --git a/apps/docs/components/ui/badge.tsx b/apps/docs/components/ui/badge.tsx
index 35e81718a2..dd579b1266 100644
--- a/apps/docs/components/ui/badge.tsx
+++ b/apps/docs/components/ui/badge.tsx
@@ -46,4 +46,4 @@ function Badge({
);
}
-export { Badge, badgeVariants };
+export { Badge, };
diff --git a/apps/docs/components/ui/breadcrumb.tsx b/apps/docs/components/ui/breadcrumb.tsx
deleted file mode 100644
index 1146057a65..0000000000
--- a/apps/docs/components/ui/breadcrumb.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import { Slot } from "@radix-ui/react-slot";
-import { CaretRightIcon as ChevronRight, DotsThreeIcon as MoreHorizontal } from "@databuddy/ui/icons";
-import type * as React from "react";
-
-import { cn } from "@/lib/utils";
-
-function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
- return ;
-}
-
-function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
- return (
-
- );
-}
-
-function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
- return (
-
- );
-}
-
-function BreadcrumbLink({
- asChild,
- className,
- ...props
-}: React.ComponentProps<"a"> & {
- asChild?: boolean;
-}) {
- const Comp = asChild ? Slot : "a";
-
- return (
-
- );
-}
-
-function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
- return (
-
- );
-}
-
-function BreadcrumbSeparator({
- children,
- className,
- ...props
-}: React.ComponentProps<"li">) {
- return (
- svg]:size-3.5", className)}
- data-slot="breadcrumb-separator"
- role="presentation"
- {...props}
- >
- {children ?? }
-
- );
-}
-
-function BreadcrumbEllipsis({
- className,
- ...props
-}: React.ComponentProps<"span">) {
- return (
-
-
- More
-
- );
-}
-
-export {
- Breadcrumb,
- BreadcrumbEllipsis,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbList,
- BreadcrumbPage,
- BreadcrumbSeparator,
-};
diff --git a/apps/docs/components/ui/button.tsx b/apps/docs/components/ui/button.tsx
index e5f586a8f9..0e78bdb332 100644
--- a/apps/docs/components/ui/button.tsx
+++ b/apps/docs/components/ui/button.tsx
@@ -56,4 +56,4 @@ function Button({
);
}
-export { Button, buttonVariants };
+export { Button, };
diff --git a/apps/docs/components/ui/calendar.tsx b/apps/docs/components/ui/calendar.tsx
deleted file mode 100644
index 5f7e385697..0000000000
--- a/apps/docs/components/ui/calendar.tsx
+++ /dev/null
@@ -1,211 +0,0 @@
-"use client";
-
-import { CaretDownIcon as ChevronDownIcon, CaretLeftIcon as ChevronLeftIcon, CaretRightIcon as ChevronRightIcon } from "@databuddy/ui/icons";
-import { useEffect, useRef } from "react";
-import {
- type DayButton,
- DayPicker,
- getDefaultClassNames,
-} from "react-day-picker";
-import { Button, buttonVariants } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-
-function Calendar({
- className,
- classNames,
- showOutsideDays = true,
- captionLayout = "label",
- buttonVariant = "ghost",
- formatters,
- components,
- ...props
-}: React.ComponentProps & {
- buttonVariant?: React.ComponentProps["variant"];
-}) {
- const defaultClassNames = getDefaultClassNames();
-
- return (
- svg]:rotate-180`,
- String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
- className
- )}
- classNames={{
- root: cn("w-fit", defaultClassNames.root),
- months: cn(
- "relative flex flex-col gap-4 md:flex-row",
- defaultClassNames.months
- ),
- month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
- nav: cn(
- "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
- defaultClassNames.nav
- ),
- button_previous: cn(
- buttonVariants({ variant: buttonVariant }),
- "size-(--cell-size) select-none p-0 aria-disabled:opacity-50",
- defaultClassNames.button_previous
- ),
- button_next: cn(
- buttonVariants({ variant: buttonVariant }),
- "size-(--cell-size) select-none p-0 aria-disabled:opacity-50",
- defaultClassNames.button_next
- ),
- month_caption: cn(
- "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
- defaultClassNames.month_caption
- ),
- dropdowns: cn(
- "flex h-(--cell-size) w-full items-center justify-center gap-1.5 font-medium text-sm",
- defaultClassNames.dropdowns
- ),
- dropdown_root: cn(
- "relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50",
- defaultClassNames.dropdown_root
- ),
- dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown),
- caption_label: cn(
- "select-none font-medium",
- captionLayout === "label"
- ? "text-sm"
- : "flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
- defaultClassNames.caption_label
- ),
- month_grid: cn("w-full border-collapse", defaultClassNames.month_grid),
- weekdays: cn("flex", defaultClassNames.weekdays),
- weekday: cn(
- "flex-1 select-none rounded-md font-normal text-[0.8rem] text-muted-foreground",
- defaultClassNames.weekday
- ),
- week: cn("mt-2 flex w-full", defaultClassNames.week),
- week_number_header: cn(
- "w-(--cell-size) select-none",
- defaultClassNames.week_number_header
- ),
- week_number: cn(
- "select-none text-[0.8rem] text-muted-foreground",
- defaultClassNames.week_number
- ),
- day: cn(
- "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
- defaultClassNames.day
- ),
- range_start: cn(
- "rounded-l-md bg-accent",
- defaultClassNames.range_start
- ),
- range_middle: cn("rounded-none", defaultClassNames.range_middle),
- range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
- today: cn(
- "rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none",
- defaultClassNames.today
- ),
- outside: cn(
- "text-muted-foreground aria-selected:text-muted-foreground",
- defaultClassNames.outside
- ),
- disabled: cn(
- "text-muted-foreground opacity-50",
- defaultClassNames.disabled
- ),
- hidden: cn("invisible", defaultClassNames.hidden),
- ...classNames,
- }}
- components={{
- Root: ({ className, rootRef, ...props }) => {
- return (
-
- );
- },
- Chevron: ({ className, orientation, ...props }) => {
- if (orientation === "left") {
- return (
-
- );
- }
-
- if (orientation === "right") {
- return (
-
- );
- }
-
- return (
-
- );
- },
- DayButton: CalendarDayButton,
- WeekNumber: ({ children, ...props }) => {
- return (
-
-
- {children}
-
-
- );
- },
- ...components,
- }}
- formatters={{
- formatMonthDropdown: (date) =>
- date.toLocaleString("default", { month: "short" }),
- ...formatters,
- }}
- showOutsideDays={showOutsideDays}
- {...props}
- />
- );
-}
-
-function CalendarDayButton({
- className,
- day,
- modifiers,
- ...props
-}: React.ComponentProps) {
- const defaultClassNames = getDefaultClassNames();
-
- const ref = useRef(null);
- useEffect(() => {
- if (modifiers.focused) {
- ref.current?.focus();
- }
- }, [modifiers.focused]);
-
- return (
- span]:text-xs [&>span]:opacity-70",
- defaultClassNames.day,
- className
- )}
- data-day={day.date.toLocaleDateString()}
- data-range-end={modifiers.range_end}
- data-range-middle={modifiers.range_middle}
- data-range-start={modifiers.range_start}
- data-selected-single={
- modifiers.selected &&
- !modifiers.range_start &&
- !modifiers.range_end &&
- !modifiers.range_middle
- }
- ref={ref}
- size="icon"
- variant="ghost"
- {...props}
- />
- );
-}
-
-export { Calendar, CalendarDayButton };
diff --git a/apps/docs/components/ui/card.tsx b/apps/docs/components/ui/card.tsx
index 1441de84ef..b21d56c533 100644
--- a/apps/docs/components/ui/card.tsx
+++ b/apps/docs/components/ui/card.tsx
@@ -83,10 +83,10 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
export {
Card,
- CardAction,
+
CardContent,
CardDescription,
- CardFooter,
+
CardHeader,
CardTitle,
};
diff --git a/apps/docs/components/ui/carousel.tsx b/apps/docs/components/ui/carousel.tsx
deleted file mode 100644
index 34e52c93b2..0000000000
--- a/apps/docs/components/ui/carousel.tsx
+++ /dev/null
@@ -1,251 +0,0 @@
-"use client";
-
-import useEmblaCarousel, {
- type UseEmblaCarouselType,
-} from "embla-carousel-react";
-import { ArrowLeftIcon as ArrowLeft, ArrowRightIcon as ArrowRight } from "@databuddy/ui/icons";
-import {
- type ComponentProps,
- createContext,
- useCallback,
- useContext,
- useEffect,
- useState,
-} from "react";
-import { Button } from "@/components/ui/button";
-import { cn } from "@/lib/utils";
-
-type CarouselApi = UseEmblaCarouselType[1];
-type UseCarouselParameters = Parameters;
-type CarouselOptions = UseCarouselParameters[0];
-type CarouselPlugin = UseCarouselParameters[1];
-
-type CarouselProps = {
- opts?: CarouselOptions;
- plugins?: CarouselPlugin;
- orientation?: "horizontal" | "vertical";
- setApi?: (api: CarouselApi) => void;
-};
-
-type CarouselContextProps = {
- carouselRef: ReturnType[0];
- api: ReturnType[1];
- scrollPrev: () => void;
- scrollNext: () => void;
- canScrollPrev: boolean;
- canScrollNext: boolean;
-} & CarouselProps;
-
-const CarouselContext = createContext(null);
-
-function useCarousel() {
- const context = useContext(CarouselContext);
-
- if (!context) {
- throw new Error("useCarousel must be used within a ");
- }
-
- return context;
-}
-
-function Carousel({
- orientation = "horizontal",
- opts,
- setApi,
- plugins,
- className,
- children,
- ...props
-}: ComponentProps<"div"> & CarouselProps) {
- const [carouselRef, api] = useEmblaCarousel(
- {
- ...opts,
- axis: orientation === "horizontal" ? "x" : "y",
- },
- plugins
- );
- const [canScrollPrev, setCanScrollPrev] = useState(false);
- const [canScrollNext, setCanScrollNext] = useState(false);
-
- const onSelect = useCallback((carouselApi: CarouselApi) => {
- if (!carouselApi) {
- return;
- }
- setCanScrollPrev(carouselApi.canScrollPrev());
- setCanScrollNext(carouselApi.canScrollNext());
- }, []);
-
- const scrollPrev = useCallback(() => {
- api?.scrollPrev();
- }, [api]);
-
- const scrollNext = useCallback(() => {
- api?.scrollNext();
- }, [api]);
-
- const handleKeyDown = useCallback(
- (event: React.KeyboardEvent) => {
- if (event.key === "ArrowLeft") {
- event.preventDefault();
- scrollPrev();
- } else if (event.key === "ArrowRight") {
- event.preventDefault();
- scrollNext();
- }
- },
- [scrollPrev, scrollNext]
- );
-
- useEffect(() => {
- if (!(api && setApi)) {
- return;
- }
- setApi(api);
- }, [api, setApi]);
-
- useEffect(() => {
- if (!api) {
- return;
- }
- onSelect(api);
- api.on("reInit", onSelect);
- api.on("select", onSelect);
-
- return () => {
- api?.off("select", onSelect);
- };
- }, [api, onSelect]);
-
- return (
-
-
-
- );
-}
-
-function CarouselContent({ className, ...props }: ComponentProps<"div">) {
- const { carouselRef, orientation } = useCarousel();
-
- return (
-
- );
-}
-
-function CarouselItem({ className, ...props }: ComponentProps<"div">) {
- const { orientation } = useCarousel();
-
- return (
-
- );
-}
-
-function CarouselPrevious({
- className,
- variant = "outline",
- size = "icon",
- ...props
-}: ComponentProps) {
- const { orientation, scrollPrev, canScrollPrev } = useCarousel();
-
- return (
-
-
- Previous slide
-
- );
-}
-
-function CarouselNext({
- className,
- variant = "outline",
- size = "icon",
- ...props
-}: ComponentProps) {
- const { orientation, scrollNext, canScrollNext } = useCarousel();
-
- return (
-
-
- Next slide
-
- );
-}
-
-export {
- Carousel,
- type CarouselApi,
- CarouselContent,
- CarouselItem,
- CarouselNext,
- CarouselPrevious,
-};
diff --git a/apps/docs/components/ui/chart.tsx b/apps/docs/components/ui/chart.tsx
deleted file mode 100644
index 874ec2f3e4..0000000000
--- a/apps/docs/components/ui/chart.tsx
+++ /dev/null
@@ -1,362 +0,0 @@
-"use client";
-
-import {
- type ComponentProps,
- type CSSProperties,
- createContext,
- useContext,
- useId,
- useMemo,
-} from "react";
-import * as RechartsPrimitive from "recharts";
-
-import { cn } from "@/lib/utils";
-
-// Format: { THEME_NAME: CSS_SELECTOR }
-const THEMES = { light: "", dark: ".dark" } as const;
-
-export type ChartConfig = {
- [k in string]: {
- label?: React.ReactNode;
- icon?: React.ComponentType;
- } & (
- | { color?: string; theme?: never }
- | { color?: never; theme: Record }
- );
-};
-
-type ChartContextProps = {
- config: ChartConfig;
-};
-
-const ChartContext = createContext(null);
-
-function useChart() {
- const context = useContext(ChartContext);
-
- if (!context) {
- throw new Error("useChart must be used within a ");
- }
-
- return context;
-}
-
-function ChartContainer({
- id,
- className,
- children,
- config,
- ...props
-}: ComponentProps<"div"> & {
- config: ChartConfig;
- children: ComponentProps<
- typeof RechartsPrimitive.ResponsiveContainer
- >["children"];
-}) {
- const uniqueId = useId();
- const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
-
- return (
-
-
-
-
- {children}
-
-
-
- );
-}
-
-const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
- const colorConfig = Object.entries(config).filter(
- ([, itemConfig]) => itemConfig.theme || itemConfig.color
- );
-
- if (!colorConfig.length) {
- return null;
- }
-
- return (
-