@@ -188,7 +173,6 @@ export default function Overview() {
labelFormatter={(value) =>
format(parseISO(value as string), "MMM dd, yyyy")
}
- formatter={(value: number, name: string) => [formatCurrency(value), name]}
contentStyle={{
backgroundColor: "rgba(0, 0, 0, 0.8)",
border: "none",
@@ -198,19 +182,19 @@ export default function Overview() {
/>
@@ -221,63 +205,39 @@ export default function Overview() {
-
- Activity Trends
+
+ Recent Activity
-
-
-
-
- format(parseISO(value), "MMM dd")}
- stroke="#888888"
- />
-
-
- format(parseISO(value as string), "MMM dd, yyyy")
- }
- contentStyle={{
- backgroundColor: "rgba(0, 0, 0, 0.8)",
- border: "none",
- borderRadius: "4px",
- color: "#fff",
- }}
- />
-
-
-
-
+
+ {recentActivity.map((activity, index) => (
+
+ {activity.type === "update" &&
}
+ {activity.type === "deck" &&
}
+ {activity.type === "investor" &&
}
+ {activity.type === "share" &&
}
+
+
{activity.title}
+
+ {activity.metric}
+ •
+ {activity.time}
+
+
+
+ ))}
-
- Growth Overview
+
+ Content Performance
-
+
-
-
+
-
+
diff --git a/web/ui/src/client/Api.ts b/web/ui/src/client/Api.ts
index aa34ab29..0c49ab8d 100644
--- a/web/ui/src/client/Api.ts
+++ b/web/ui/src/client/Api.ts
@@ -104,6 +104,29 @@ export enum MalakContactShareItemType {
export type MalakCustomContactMetadata = Record;
+export interface MalakDashboard {
+ chart_count?: number;
+ created_at?: string;
+ description?: string;
+ id?: string;
+ reference?: string;
+ title?: string;
+ updated_at?: string;
+ workspace_id?: string;
+}
+
+export interface MalakDashboardChart {
+ chart?: MalakIntegrationChart;
+ chart_id?: string;
+ created_at?: string;
+ dashboard_id?: string;
+ id?: string;
+ reference?: string;
+ updated_at?: string;
+ workspace_id?: string;
+ workspace_integration_id?: string;
+}
+
export interface MalakDeck {
created_at?: string;
created_by?: string;
@@ -147,6 +170,35 @@ export interface MalakIntegration {
updated_at?: string;
}
+export interface MalakIntegrationChart {
+ chart_type?: MalakIntegrationChartType;
+ created_at?: string;
+ id?: string;
+ internal_name?: MalakIntegrationChartInternalNameType;
+ metadata?: MalakIntegrationChartMetadata;
+ reference?: string;
+ updated_at?: string;
+ user_facing_name?: string;
+ workspace_id?: string;
+ workspace_integration_id?: string;
+}
+
+export enum MalakIntegrationChartInternalNameType {
+ IntegrationChartInternalNameTypeMercuryAccount = "mercury_account",
+ IntegrationChartInternalNameTypeMercuryAccountTransaction = "mercury_account_transaction",
+ IntegrationChartInternalNameTypeBrexAccount = "brex_account",
+ IntegrationChartInternalNameTypeBrexAccountTransaction = "brex_account_transaction",
+}
+
+export interface MalakIntegrationChartMetadata {
+ provider_id?: string;
+}
+
+export enum MalakIntegrationChartType {
+ IntegrationChartTypeBar = "bar",
+ IntegrationChartTypePie = "pie",
+}
+
export interface MalakIntegrationMetadata {
endpoint?: string;
}
@@ -188,6 +240,7 @@ export interface MalakPlan {
export interface MalakPlanMetadata {
dashboard?: {
embed_dashboard?: boolean;
+ max_charts_per_dashboard?: number;
share_dashboard_via_link?: boolean;
};
data_room?: {
@@ -386,6 +439,10 @@ export interface ServerAPIStatus {
message: string;
}
+export interface ServerAddChartToDashboardRequest {
+ chart_reference: string;
+}
+
export interface ServerAddContactToListRequest {
reference?: string;
}
@@ -409,6 +466,11 @@ export interface ServerCreateContactRequest {
last_name?: string;
}
+export interface ServerCreateDashboardRequest {
+ description: string;
+ title: string;
+}
+
export interface ServerCreateDeckRequest {
deck_url?: string;
title?: string;
@@ -466,6 +528,11 @@ export interface ServerFetchContactResponse {
message: string;
}
+export interface ServerFetchDashboardResponse {
+ dashboard: MalakDashboard;
+ message: string;
+}
+
export interface ServerFetchDeckResponse {
deck: MalakDeck;
message: string;
@@ -509,6 +576,23 @@ export interface ServerListContactsResponse {
meta: ServerMeta;
}
+export interface ServerListDashboardChartsResponse {
+ charts: MalakDashboardChart[];
+ dashboard: MalakDashboard;
+ message: string;
+}
+
+export interface ServerListDashboardResponse {
+ dashboards: MalakDashboard[];
+ message: string;
+ meta: ServerMeta;
+}
+
+export interface ServerListIntegrationChartsResponse {
+ charts: MalakIntegrationChart[];
+ message: string;
+}
+
export interface ServerListIntegrationResponse {
integrations: MalakWorkspaceIntegration[];
message: string;
@@ -923,6 +1007,100 @@ export class Api extends HttpClient
+ this.request({
+ path: `/dashboards`,
+ method: "GET",
+ query: query,
+ format: "json",
+ ...params,
+ }),
+
+ /**
+ * No description
+ *
+ * @tags dashboards
+ * @name DashboardsCreate
+ * @summary create a new dashboard
+ * @request POST:/dashboards
+ */
+ dashboardsCreate: (data: ServerCreateDashboardRequest, params: RequestParams = {}) =>
+ this.request({
+ path: `/dashboards`,
+ method: "POST",
+ body: data,
+ type: ContentType.Json,
+ format: "json",
+ ...params,
+ }),
+
+ /**
+ * No description
+ *
+ * @tags dashboards
+ * @name DashboardsDetail
+ * @summary fetch dashboard
+ * @request GET:/dashboards/{reference}
+ */
+ dashboardsDetail: (reference: string, params: RequestParams = {}) =>
+ this.request({
+ path: `/dashboards/${reference}`,
+ method: "GET",
+ format: "json",
+ ...params,
+ }),
+
+ /**
+ * No description
+ *
+ * @tags dashboards
+ * @name ChartsUpdate
+ * @summary add a chart to a dashboard
+ * @request PUT:/dashboards/{reference}/charts
+ */
+ chartsUpdate: (reference: string, data: ServerAddChartToDashboardRequest, params: RequestParams = {}) =>
+ this.request({
+ path: `/dashboards/${reference}/charts`,
+ method: "PUT",
+ body: data,
+ type: ContentType.Json,
+ format: "json",
+ ...params,
+ }),
+
+ /**
+ * No description
+ *
+ * @tags dashboards
+ * @name ChartsList
+ * @summary List charts
+ * @request GET:/dashboards/charts
+ */
+ chartsList: (params: RequestParams = {}) =>
+ this.request({
+ path: `/dashboards/charts`,
+ method: "GET",
+ format: "json",
+ ...params,
+ }),
+ };
decks = {
/**
* No description
diff --git a/web/ui/src/components/providers/user.tsx b/web/ui/src/components/providers/user.tsx
index ac446cad..fbee70d5 100644
--- a/web/ui/src/components/providers/user.tsx
+++ b/web/ui/src/components/providers/user.tsx
@@ -102,7 +102,13 @@ export default function UserProvider({
}, [token, isRehydrated]);
if (loading) {
- return Loading...
;
+ return (
+
+ );
}
return children;
diff --git a/web/ui/src/components/ui/custom/loader/skeleton.tsx b/web/ui/src/components/ui/custom/loader/skeleton.tsx
index 59a1af3c..6bbf1776 100644
--- a/web/ui/src/components/ui/custom/loader/skeleton.tsx
+++ b/web/ui/src/components/ui/custom/loader/skeleton.tsx
@@ -3,13 +3,15 @@ import BaseSkeleton from "react-loading-skeleton";
import "react-loading-skeleton/dist/skeleton.css";
const Skeleton = ({ count }: { count: number }) => {
+ const { theme } = useTheme();
- const { theme } = useTheme()
-
- return ;
+ return (
+
+ );
};
export default Skeleton;
diff --git a/web/ui/src/components/ui/dashboards/create-modal.tsx b/web/ui/src/components/ui/dashboards/create-modal.tsx
new file mode 100644
index 00000000..45fded32
--- /dev/null
+++ b/web/ui/src/components/ui/dashboards/create-modal.tsx
@@ -0,0 +1,153 @@
+"use client"
+
+import type { ServerAPIStatus } from "@/client/Api";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { RiAddLine } from "@remixicon/react";
+import { useState } from "react";
+import { type SubmitHandler, useForm } from "react-hook-form";
+import { toast } from "sonner";
+import * as yup from "yup";
+import { yupResolver } from "@hookform/resolvers/yup";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import type { AxiosError } from "axios";
+import client from "@/lib/client";
+import { CREATE_DASHBOARD, LIST_DASHBOARDS } from "@/lib/query-constants";
+
+type CreateDashboardInput = {
+ name: string;
+ description?: string;
+};
+
+const schema = yup.object().shape({
+ name: yup.string().required("Dashboard name is required"),
+ description: yup.string(),
+});
+
+export default function CreateDashboardModal() {
+ const [open, setOpen] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const queryClient = useQueryClient();
+
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ reset,
+ } = useForm({
+ resolver: yupResolver(schema),
+ });
+
+ const createMutation = useMutation({
+ mutationKey: [CREATE_DASHBOARD],
+ mutationFn: (data: CreateDashboardInput) => {
+ return client.dashboards.dashboardsCreate({
+ title: data.name,
+ ...data
+ })
+ },
+ onSuccess: ({ data }) => {
+ toast.success(data.message);
+ setOpen(false);
+ reset();
+ queryClient.invalidateQueries({ queryKey: [LIST_DASHBOARDS] });
+ },
+ onError: (err: AxiosError) => {
+ let msg = err.message;
+ if (err.response?.data) {
+ msg = err.response.data.message;
+ }
+ toast.error(msg);
+ },
+ retry: false,
+ gcTime: Number.POSITIVE_INFINITY,
+ onSettled: () => setLoading(false),
+ });
+
+ const onSubmit: SubmitHandler = (data) => {
+ setLoading(true);
+ createMutation.mutate(data);
+ };
+
+ return (
+
+ );
+}
diff --git a/web/ui/src/components/ui/dashboards/list.tsx b/web/ui/src/components/ui/dashboards/list.tsx
new file mode 100644
index 00000000..ab04163e
--- /dev/null
+++ b/web/ui/src/components/ui/dashboards/list.tsx
@@ -0,0 +1,135 @@
+"use client"
+
+import { Card } from "@/components/ui/card";
+import { RiDashboardLine } from "@remixicon/react";
+import { format } from "date-fns";
+import Link from "next/link";
+import { useQuery } from "@tanstack/react-query";
+import client from "@/lib/client";
+import { LIST_DASHBOARDS } from "@/lib/query-constants";
+import { Button } from "@/components/ui/button";
+import { RiArrowLeftLine, RiArrowRightLine } from "@remixicon/react";
+import { useState } from "react";
+import type { MalakDashboard, ServerListDashboardResponse } from "@/client/Api";
+
+export default function ListDashboards() {
+ const [page, setPage] = useState(1);
+ const perPage = 12;
+
+ const { data, isLoading, isError } = useQuery({
+ queryKey: [LIST_DASHBOARDS, page],
+ queryFn: async () => {
+ const response = await client.dashboards.dashboardsList({
+ page,
+ per_page: perPage,
+ });
+ return response.data;
+ },
+ });
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+
+ Loading dashboards...
+
+
+
+ );
+ }
+
+ if (isError) {
+ return (
+
+
+
+
+
+
+ Error loading dashboards
+
+
+ Please try again later.
+
+
+
+ );
+ }
+
+ if (!data?.dashboards?.length) {
+ return (
+
+
+
+
+
+
+ No dashboards yet
+
+
+ Create your first dashboard to visualize data from your integrations.
+
+
+
+ );
+ }
+
+ const totalPages = Math.ceil(data.meta.paging.total / perPage);
+
+ return (
+
+
+ {data.dashboards.map((dashboard: MalakDashboard) => (
+
+
+
+
{dashboard.title}
+
{dashboard.description}
+
+
+
+
+
+ {dashboard.chart_count} charts
+
+
Created {format(new Date(dashboard.created_at!), "MMM d, yyyy")}
+
+
+
+ ))}
+
+
+ {totalPages > 1 && (
+
+
+
+ Page {page} of {totalPages}
+
+
+
+ )}
+
+ );
+}
diff --git a/web/ui/src/components/ui/navigation/ModalAddWorkspace.tsx b/web/ui/src/components/ui/navigation/ModalAddWorkspace.tsx
index dd9116e7..a69cd9d7 100644
--- a/web/ui/src/components/ui/navigation/ModalAddWorkspace.tsx
+++ b/web/ui/src/components/ui/navigation/ModalAddWorkspace.tsx
@@ -19,11 +19,11 @@ import client from "@/lib/client";
import { CREATE_WORKSPACE } from "@/lib/query-constants";
import useWorkspacesStore from "@/store/workspace";
import { yupResolver } from "@hookform/resolvers/yup";
-import { RiAddBoxLine, RiAddLargeLine } from "@remixicon/react";
+import { RiAddLargeLine } from "@remixicon/react";
import { useMutation } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { useRouter } from "next/navigation";
-import { useState, useRef, useEffect } from "react";
+import { useState, useEffect } from "react";
import { type SubmitHandler, useForm } from "react-hook-form";
import { toast } from "sonner";
import * as yup from "yup";
diff --git a/web/ui/src/components/ui/navigation/navlist.ts b/web/ui/src/components/ui/navigation/navlist.ts
index 76794350..6a840ffc 100644
--- a/web/ui/src/components/ui/navigation/navlist.ts
+++ b/web/ui/src/components/ui/navigation/navlist.ts
@@ -2,6 +2,7 @@ import {
RiArchiveStackLine,
RiBook3Line,
RiContactsLine,
+ RiDashboardHorizontalLine,
RiHome2Line,
RiMoneyDollarCircleLine,
RiPieChartLine,
@@ -30,6 +31,16 @@ export const links = [
url: "/contacts",
icon: RiContactsLine,
},
+ {
+ title: "Integrations",
+ url: "/integrations",
+ icon: RiPlug2Line
+ },
+ {
+ title: "Data Dashboards",
+ url: "/dashboards",
+ icon: RiDashboardHorizontalLine
+ },
{
title: "Fundraising",
url: "/fundraising",
@@ -40,11 +51,6 @@ export const links = [
url: "/captable",
icon: RiPieChartLine,
},
- {
- title: "Integrations",
- url: "/integrations",
- icon: RiPlug2Line
- },
{
title: "Settings",
url: "/settings",
diff --git a/web/ui/src/lib/query-constants.ts b/web/ui/src/lib/query-constants.ts
index cc708315..d7945960 100644
--- a/web/ui/src/lib/query-constants.ts
+++ b/web/ui/src/lib/query-constants.ts
@@ -31,3 +31,7 @@ export const PING_INTEGRATION = 'PING_INTEGRATION' as const;
export const ENABLE_INTEGRATION = 'ENABLE_INTEGRATION' as const;
export const UPDATE_INTEGRATION_SETTINGS = 'UPDATE_INTEGRATION_SETTINGS' as const;
export const DISABLE_INTEGRATION = 'DISABLE_INTEGRATION' as const;
+export const CREATE_DASHBOARD = 'CREATE_DASHBOARD' as const;
+export const LIST_DASHBOARDS = "LIST_DASHBOARDS" as const;
+export const LIST_CHARTS = "LIST_CHARTS";
+export const DASHBOARD_DETAIL = "DASHBOARD_DETAIL" as const;
diff --git a/web/ui/tailwind.config.ts b/web/ui/tailwind.config.ts
index dd183a06..4cadff75 100644
--- a/web/ui/tailwind.config.ts
+++ b/web/ui/tailwind.config.ts
@@ -117,55 +117,55 @@ const config: Config = {
sm: 'calc(var(--radius) - 4px)'
},
colors: {
- background: 'hsl(var(--background))',
- foreground: 'hsl(var(--foreground))',
+ background: "hsl(0 0% 100%)",
+ foreground: "hsl(222 47% 11%)",
card: {
- DEFAULT: 'hsl(var(--card))',
- foreground: 'hsl(var(--card-foreground))'
+ DEFAULT: "hsl(0 0% 100%)",
+ foreground: "hsl(222 47% 11%)",
},
popover: {
- DEFAULT: 'hsl(var(--popover))',
- foreground: 'hsl(var(--popover-foreground))'
+ DEFAULT: "hsl(0 0% 100%)",
+ foreground: "hsl(222 47% 11%)",
},
primary: {
- DEFAULT: 'hsl(var(--primary))',
- foreground: 'hsl(var(--primary-foreground))'
+ DEFAULT: "hsl(160 84% 39%)",
+ foreground: "hsl(0 0% 100%)",
},
secondary: {
- DEFAULT: 'hsl(var(--secondary))',
- foreground: 'hsl(var(--secondary-foreground))'
+ DEFAULT: "hsl(210 40% 96.1%)",
+ foreground: "hsl(222 47% 11%)",
},
muted: {
- DEFAULT: 'hsl(var(--muted))',
- foreground: 'hsl(var(--muted-foreground))'
+ DEFAULT: "hsl(210 40% 96.1%)",
+ foreground: "hsl(215.4 16.3% 46.9%)",
},
accent: {
- DEFAULT: 'hsl(var(--accent))',
- foreground: 'hsl(var(--accent-foreground))'
+ DEFAULT: "hsl(210 40% 96.1%)",
+ foreground: "hsl(222 47% 11%)",
},
destructive: {
- DEFAULT: 'hsl(var(--destructive))',
- foreground: 'hsl(var(--destructive-foreground))'
+ DEFAULT: "hsl(0 84.2% 60.2%)",
+ foreground: "hsl(210 40% 98%)",
},
- border: 'hsl(var(--border))',
- input: 'hsl(var(--input))',
- ring: 'hsl(var(--ring))',
+ border: "hsl(214.3 31.8% 91.4%)",
+ input: "hsl(214.3 31.8% 91.4%)",
+ ring: "hsl(222 47% 11%)",
chart: {
- '1': 'hsl(var(--chart-1))',
- '2': 'hsl(var(--chart-2))',
- '3': 'hsl(var(--chart-3))',
- '4': 'hsl(var(--chart-4))',
- '5': 'hsl(var(--chart-5))'
+ "1": "hsl(222 47% 11%)",
+ "2": "hsl(215.4 16.3% 46.9%)",
+ "3": "hsl(214.3 31.8% 91.4%)",
+ "4": "hsl(210 40% 96.1%)",
+ "5": "hsl(0 0% 100%)",
},
sidebar: {
- DEFAULT: 'hsl(var(--sidebar-background))',
- foreground: 'hsl(var(--sidebar-foreground))',
- primary: 'hsl(var(--sidebar-primary))',
- 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
- accent: 'hsl(var(--sidebar-accent))',
- 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
- border: 'hsl(var(--sidebar-border))',
- ring: 'hsl(var(--sidebar-ring))'
+ DEFAULT: "hsl(0 0% 100%)",
+ foreground: "hsl(222 47% 11%)",
+ primary: "hsl(222 47% 11%)",
+ "primary-foreground": "hsl(0 0% 100%)",
+ accent: "hsl(210 40% 96.1%)",
+ "accent-foreground": "hsl(222 47% 11%)",
+ border: "hsl(214.3 31.8% 91.4%)",
+ ring: "hsl(222 47% 11%)",
}
}
}