├── .eslintrc.json
├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── app
├── about
│ └── page.tsx
├── action.tsx
├── actions.ts
├── actions
│ ├── ai.ts
│ ├── kpi.ts
│ └── query.ts
├── dashboard
│ └── page.tsx
├── favicon.ico
├── globals.css
├── layout.tsx
├── opengraph-image.png
└── page.tsx
├── bun.lockb
├── components.json
├── components
├── CampaignFilter.tsx
├── Charts.tsx
├── Chat.tsx
├── ClearParams.tsx
├── DashCards
│ ├── AudienceCard.tsx
│ ├── ContentCard.tsx
│ ├── EngagementCard.tsx
│ ├── LocationDonut.tsx
│ ├── PlatformCard.tsx
│ └── index.tsx
├── Header.tsx
├── MapCard.tsx
├── PlatformFilter.tsx
├── ai
│ ├── GoodOverBad.tsx
│ ├── GrowthRate.tsx
│ └── SubsOverTime.tsx
├── charts
│ ├── BarChart.tsx
│ ├── BarListChart.tsx
│ ├── DonutChart.tsx
│ ├── LineChart.tsx
│ ├── MapChart.tsx
│ ├── TableChart.tsx
│ ├── rawBarList.tsx
│ ├── scatterChart.tsx
│ ├── sparkChart.tsx
│ └── world_countries.json
├── chat-list.tsx
├── dashboard.tsx
├── empty-screen.tsx
├── kpi
│ ├── BudgetCard.tsx
│ ├── ClicksCard.tsx
│ ├── ImpressionCard.tsx
│ ├── RevenueCard.tsx
│ ├── SubscriberCard.tsx
│ └── index.tsx
├── llm-charts
│ ├── AreaChartComponent.tsx
│ ├── AreaSkeleton.tsx
│ ├── BarChartComponent.tsx
│ ├── DonutChartComponent.tsx
│ ├── LineChartComponent.tsx
│ ├── NumberChartComponent.tsx
│ ├── NumberSkeleton.tsx
│ ├── ScatterChartComponent.tsx
│ ├── TableChartComponent.tsx
│ ├── index.tsx
│ └── markdown.tsx
├── message.tsx
└── ui
│ ├── avatar.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── combobox.tsx
│ ├── command.tsx
│ ├── dialog.tsx
│ ├── dropdown-menu.tsx
│ ├── icons.tsx
│ ├── input.tsx
│ ├── popover.tsx
│ ├── scroll-area.tsx
│ ├── select.tsx
│ ├── separator.tsx
│ ├── sheet.tsx
│ ├── skeleton.tsx
│ ├── sparkles.tsx
│ ├── spinner.tsx
│ ├── table.tsx
│ └── tooltip.tsx
├── drizzle.config.ts
├── lib
├── db.ts
├── execute.ts
├── hooks
│ ├── chat-scroll-anchor.tsx
│ ├── use-at-bottom.tsx
│ └── use-enter-submit.tsx
├── redis.ts
├── supabase
│ ├── browser.ts
│ └── server.ts
├── tool-definition.ts
├── utils.ts
└── validation
│ └── index.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── line-chart.png
├── next.svg
└── vercel.svg
├── supabase
├── .gitignore
├── config.toml
├── functions
│ └── .vscode
│ │ ├── extensions.json
│ │ └── settings.json
└── migrations
│ └── 20240407221836_remote_schema.sql
├── tailwind.config.ts
├── tsconfig.json
└── types
├── database.types.ts
└── global.d.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | .env.local
39 | .env.*
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench.colorCustomizations": {
3 | "activityBar.activeBackground": "#ac6cfe",
4 | "activityBar.background": "#ac6cfe",
5 | "activityBar.foreground": "#15202b",
6 | "activityBar.inactiveForeground": "#15202b99",
7 | "activityBarBadge.background": "#fec9a0",
8 | "activityBarBadge.foreground": "#15202b",
9 | "commandCenter.border": "#e7e7e799",
10 | "sash.hoverBorder": "#ac6cfe",
11 | "statusBar.background": "#903afd",
12 | "statusBar.foreground": "#e7e7e7",
13 | "statusBarItem.hoverBackground": "#ac6cfe",
14 | "statusBarItem.remoteBackground": "#903afd",
15 | "statusBarItem.remoteForeground": "#e7e7e7",
16 | "titleBar.activeBackground": "#903afd",
17 | "titleBar.activeForeground": "#e7e7e7",
18 | "titleBar.inactiveBackground": "#903afd99",
19 | "titleBar.inactiveForeground": "#e7e7e799"
20 | },
21 | "peacock.color": "#903afd",
22 | "editor.fontSize": 10
23 | }
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | # or
14 | bun dev
15 | ```
16 |
17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18 |
19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20 |
21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
22 |
23 | ## Learn More
24 |
25 | To learn more about Next.js, take a look at the following resources:
26 |
27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29 |
30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
31 |
32 | ## Deploy on Vercel
33 |
34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35 |
36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
37 |
--------------------------------------------------------------------------------
/app/about/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import React from "react";
3 |
4 | function Keyword({ children }: { children: React.ReactNode }) {
5 | return (
6 |
7 | {children}
8 |
9 | );
10 | }
11 |
12 | export default function AboutPage() {
13 | return (
14 |
15 |
16 | Data Visualization for{" "}
17 | Campaign and{" "}
18 | Subscriber Analysis
19 |
20 |
21 | Our project focuses on data visualization for
22 | analyzing campaign and subscriber {" "}
23 | datasets. Through interactive visualizations , we aim
24 | to uncover insights and patterns {" "}
25 | that drive campaign success and subscriber engagement. By leveraging{" "}
26 | data analytics and{" "}
27 | visualization techniques , we empower businesses to
28 | make data-driven decisions and optimize their{" "}
29 | marketing strategies .
30 |
31 |
Who We Are
32 |
33 | We are a team of four data professionals based in{" "}
34 | Auckland, NZ . The team members
35 | are{" "}
36 |
41 |
42 | Kaarthik Andavar
43 |
44 |
45 | ,{" "}
46 |
51 |
52 | Olivia Yang
53 |
54 |
55 | ,{" "}
56 |
61 |
62 | Mohit Saini
63 |
64 |
65 | , and{" "}
66 |
71 |
72 | Ding Wang
73 |
74 |
75 | . We are dedicated to harnessing the power of data for{" "}
76 | actionable insights . With expertise in{" "}
77 | data engineering , analysis , and{" "}
78 | visualization , we bring a unique blend of skills to
79 | transform complex datasets into{" "}
80 | meaningful visual narratives . Our passion lies in
81 | helping businesses unlock the full potential of their data and drive
82 | growth through informed strategies .
83 |
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/app/action.tsx:
--------------------------------------------------------------------------------
1 | import "server-only";
2 |
3 | import { createAI, createStreamableUI, getMutableAIState } from "ai/rsc";
4 | import OpenAI from "openai";
5 |
6 | import { GoodOverBad } from "@/components/ai/GoodOverBad";
7 | import { GrowthRateChartCard } from "@/components/ai/GrowthRate";
8 | import { SubsOverTimeCard } from "@/components/ai/SubsOverTime";
9 | import { Chart } from "@/components/llm-charts";
10 | import AreaSkeleton from "@/components/llm-charts/AreaSkeleton";
11 | import { BotCard, BotMessage } from "@/components/message";
12 | import { spinner } from "@/components/ui/spinner";
13 | import { runQuery } from "@/lib/db";
14 | import { runOpenAICompletion } from "@/lib/utils";
15 | import { FQueryResponse } from "@/lib/validation";
16 | import { Code } from "bright";
17 | import { format as sql_format } from "sql-formatter";
18 | import { z } from "zod";
19 | import { GoodOverBadquery } from "./actions/query";
20 |
21 | const openai = new OpenAI({
22 | apiKey: process.env.OPENAI_API_KEY || "",
23 | baseURL: `https://gateway.ai.cloudflare.com/v1/${process.env.CLOUDFLARE_ACCOUNT_TAG}/snowbrain/openai`,
24 | });
25 |
26 | type OpenAIQueryResponse = z.infer;
27 |
28 | export interface QueryResult {
29 | columns: string[];
30 | data: Array<{ [key: string]: any }>;
31 | }
32 |
33 | async function submitUserMessage(content: string) {
34 | "use server";
35 |
36 | const aiState = getMutableAIState();
37 | aiState.update([
38 | ...aiState.get(),
39 | {
40 | role: "user",
41 | content,
42 | },
43 | ]);
44 |
45 | const reply = createStreamableUI(
46 | {spinner}
47 | );
48 |
49 | const completion = runOpenAICompletion(openai, {
50 | model: "gpt-4o-mini",
51 | stream: true,
52 | messages: [
53 | {
54 | role: "system",
55 | content: `
56 | You are a friendly AI assistant. You can help users with their queries and provide information about the two datasets.
57 |
58 | dataset 1 is about campaign -
59 | sample data \n
60 | CampaignID StartDate EndDate Budget Platform ContentID ContentType AudienceType Impressions Clicks NewSubscriptions Subscription Cost Revenue
61 | 1000 07.01.2023 22.02.2023 5904.29 Instagram 74308 Movie Families 80886 1909 870 10 8700
62 |
63 | \n
64 | dataset 2 is about Subscribers -
65 | Sample data \n
66 | SubscriberID CampaignID SubscriptionDate Age Gender Location AudienceType Satisfaction SubscriptionDays EngagementRate ViewingTime
67 | S00001 1000 19.01.2023 60 Male Asia Families Very Satisfied 376 3 3008
68 |
69 | These are the two tables that you can use to answer user queries, which is stored in postgresql supabase database. Preserve case for the column names.
70 |
71 | create table
72 | public.campaign (
73 | "CampaignID" bigint not null,
74 | "StartDate" text null,
75 | "EndDate" text null,
76 | "Budget" double precision null,
77 | "Platform" text null,
78 | "ContentID" bigint null,
79 | "ContentType" text null,
80 | "AudienceType" text null,
81 | "Impressions" bigint null,
82 | "Clicks" bigint null,
83 | "NewSubscriptions" bigint null,
84 | "Subscription Cost" bigint null,
85 | "Revenue" bigint null,
86 | constraint campaign_pkey primary key ("CampaignID")
87 | ) tablespace pg_default;
88 |
89 | create table
90 | public.subscriber (
91 | "SubscriberID" text not null,
92 | "CampaignID" bigint null,
93 | "SubscriptionDate" text null,
94 | "Age" bigint null,
95 | "Gender" text null,
96 | "Location" text null,
97 | "AudienceType" text null,
98 | "Satisfaction" text null,
99 | "SubscriptionDays" bigint null,
100 | "EngagementRate" bigint null,
101 | "ViewingTime" bigint null,
102 | constraint subscriber_pkey primary key ("SubscriberID"),
103 | constraint public_subscriber_CampaignID_fkey foreign key ("CampaignID") references campaign ("CampaignID")
104 | ) tablespace pg_default;
105 |
106 | You can only read data and make analysis, you cannot write or update data at any cost.
107 |
108 | if they ask about "Show me Subscribers growth over time?" then call the function \`growth_card\` to show the growth rate over time.
109 | if they ask about "Give me number of subscriptions over time? using line chart" then call the function \`subs_card\` to show the number of subscribers over time.
110 | if they ask about "Could you count good vs bad campaigns?" then call the function \`good_vs_bad_campaign\` to show the number of good vs bad campaigns using a table.
111 |
112 | Messages inside [] means that it's a UI element or a user event. For example:
113 | - "[Showing Subscribers growth over time card- using line chart]" means that the UI is showing a card with the title "Subscribers growth over time".
114 | - "[Showing number of subscribers over time (monthly) card- using line chart]" means that the UI is showing a card with the title "Number of subscribers over time".
115 | - "[Results for query: query with format: format and title: title and description: description. with data" means that a chart/table/number card is shown to that user.
116 |
117 |
118 | `,
119 | },
120 | ...aiState.get().map((info: any) => ({
121 | role: info.role,
122 | content: info.content,
123 | name: info.name,
124 | })),
125 | ],
126 | functions: [
127 | {
128 | name: "query_data",
129 | description:
130 | "Query the data from the snowflake database and return the results.",
131 | parameters: FQueryResponse,
132 | },
133 | {
134 | name: "growth_card",
135 | description:
136 | "Show the growth rate of subscribers over time using a line chart.",
137 | parameters: z.object({
138 | month: z.string().optional(),
139 | }),
140 | },
141 | {
142 | name: "subs_card",
143 | description:
144 | "Show the number of subscribers over time using a line chart.",
145 | parameters: z.object({
146 | month: z.string().optional(),
147 | }),
148 | },
149 | {
150 | name: "good_vs_bad_campaign",
151 | description: "Show the number of good vs bad campaigns using a table.",
152 | parameters: z.object({
153 | month: z.string().optional(),
154 | }),
155 | },
156 | ],
157 | temperature: 0,
158 | });
159 |
160 | completion.onTextContent((content: string, isFinal: boolean) => {
161 | // console.log(content);
162 |
163 | reply.update({content} );
164 | if (isFinal) {
165 | reply.done();
166 | aiState.done([...aiState.get(), { role: "assistant", content }]);
167 | }
168 | });
169 |
170 | completion.onFunctionCall("growth_card", async () => {
171 | reply.update(
172 |
173 |
174 |
175 | );
176 |
177 | reply.done(
178 |
179 |
180 |
181 | );
182 |
183 | aiState.done([
184 | ...aiState.get(),
185 | {
186 | role: "function",
187 | name: "growth_card",
188 | content: `[Snowflake query results for code: Showing Subscribers growth over time card- using line chart]`,
189 | },
190 | ]);
191 | });
192 |
193 | completion.onFunctionCall("subs_card", async () => {
194 | reply.update(
195 |
196 |
197 |
198 | );
199 |
200 | reply.done(
201 |
202 |
203 |
204 | );
205 |
206 | aiState.done([
207 | ...aiState.get(),
208 | {
209 | role: "function",
210 | name: "subs_card",
211 | content: `[Snowflake query results for code: Showing number of subscribers over time (monthly) card- using line chart]`,
212 | },
213 | ]);
214 | });
215 |
216 | completion.onFunctionCall("good_vs_bad_campaign", async () => {
217 | reply.update(
218 |
219 |
220 |
221 | );
222 |
223 | reply.done(
224 |
225 |
226 |
227 | );
228 |
229 | aiState.done([
230 | ...aiState.get(),
231 | {
232 | role: "function",
233 | name: "good_vs_bad_campaign",
234 | content: `[Snowflake query results for code: Showing number of good vs bad campaigns using a table] - the sql query used was ${GoodOverBadquery} and the data is ${goodBadData}`,
235 | },
236 | ]);
237 | });
238 |
239 | completion.onFunctionCall(
240 | "query_data",
241 | async (input: OpenAIQueryResponse) => {
242 | reply.update(
243 |
244 |
245 |
246 | );
247 | const { format, title, timeField, categories, index, yaxis, size } =
248 | input;
249 | // console.log("Received timeField:", timeField);
250 | // console.log("Received format:", format);
251 | // console.log("Received title:", title);
252 | // console.log("Received categories:", categories);
253 | // console.log("Received index:", index);
254 | // console.log("Received yaxis:", yaxis);
255 | // console.log("Received size:", size);
256 | let query = input.query;
257 |
258 | const format_query = sql_format(query, { language: "sql" });
259 | const res = await runQuery(format_query);
260 | // const res = testquery;
261 | const compatibleQueryResult: QueryResult = {
262 | columns: res.columns,
263 | data: res.data,
264 | };
265 |
266 | reply.done(
267 |
268 |
269 |
279 |
280 |
281 | {format_query}
282 |
283 |
284 |
285 |
286 | );
287 |
288 | aiState.done([
289 | ...aiState.get(),
290 | {
291 | role: "function",
292 | name: "query_data",
293 | content: `[Snowflake query results for code: ${query} and chart format: ${format} with categories: ${categories} and data ${res.columns} ${res.data}]`,
294 | },
295 | ]);
296 | }
297 | );
298 | return {
299 | id: Date.now(),
300 | display: reply.value,
301 | };
302 | }
303 |
304 | const initialAIState: {
305 | role: "user" | "assistant" | "system" | "function";
306 | content: string;
307 | id?: string;
308 | name?: string;
309 | }[] = [];
310 |
311 | const initialUIState: {
312 | id: number;
313 | display: React.ReactNode;
314 | }[] = [];
315 |
316 | export const AI = createAI({
317 | actions: {
318 | submitUserMessage,
319 | },
320 | initialUIState,
321 | initialAIState,
322 | });
323 |
324 | const goodBadData = [
325 | {
326 | CampaignType: "Good Campaign",
327 | CampaignCount: 7,
328 | },
329 | {
330 | CampaignType: "Bad Campaign",
331 | CampaignCount: 43,
332 | },
333 | ];
334 |
--------------------------------------------------------------------------------
/app/actions/ai.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { runQuery } from "@/lib/db";
4 | import { format } from "date-fns";
5 | import { revalidatePath } from "next/cache";
6 | import {
7 | CampaignData,
8 | SubscriberData,
9 | fetchCampaignData,
10 | fetchSubscriberData,
11 | } from "./kpi";
12 | import { GoodOverBadquery } from "./query";
13 |
14 | interface SubscriptionsOverTimeData {
15 | Month: string;
16 | NewSubscriptions: number;
17 | }
18 | function groupSubscribersByMonth(subscriberData: SubscriberData[]): {
19 | [month: string]: number;
20 | } {
21 | const result: { [monthKey: string]: number } = {};
22 |
23 | subscriberData.forEach((subscriber) => {
24 | const subscriptionDate = new Date(subscriber.SubscriptionDate);
25 | const formattedMonth = format(subscriptionDate, "yyyy-MM");
26 |
27 | if (!result[formattedMonth]) {
28 | result[formattedMonth] = 0;
29 | }
30 | // Increment subscriber count for the month
31 | result[formattedMonth]++;
32 | });
33 |
34 | return result;
35 | }
36 |
37 | export async function fetchSubscriptionsOverTime(
38 | audience: string | null,
39 | contentType: string | null,
40 | month: string | null = "all",
41 | satisfaction: string | null
42 | ): Promise {
43 | const subscriberData = await fetchSubscriberData(
44 | satisfaction || null,
45 | audience,
46 | null,
47 | null,
48 | month
49 | );
50 |
51 | const subscribersByMonth = groupSubscribersByMonth(subscriberData);
52 |
53 | // Get all unique months from the subscriber data
54 | const allMonths = Array.from(
55 | new Set(
56 | subscriberData.map((subscriber) =>
57 | format(new Date(subscriber.SubscriptionDate), "yyyy-MM")
58 | )
59 | )
60 | ).sort();
61 |
62 | // Calculate new subscriptions for each month
63 | const subscriptionsOverTime: SubscriptionsOverTimeData[] = allMonths.map(
64 | (month) => ({
65 | Month: month,
66 | NewSubscriptions: subscribersByMonth[month] || 0,
67 | })
68 | );
69 |
70 | return subscriptionsOverTime;
71 | }
72 |
73 | interface GrowthRateOverTimeData {
74 | Month: string;
75 | GrowthRate: number;
76 | }
77 |
78 | export async function calculateGrowthRateForChart(
79 | monthlySubscriptions: SubscriptionsOverTimeData[]
80 | ): Promise {
81 | let growthData: GrowthRateOverTimeData[] = monthlySubscriptions.map(
82 | (data, index) => {
83 | if (index === 0) {
84 | return { Month: data.Month, GrowthRate: 0 }; // Initial month has no growth rate
85 | }
86 | const prevSubscriptions =
87 | monthlySubscriptions[index - 1].NewSubscriptions;
88 | const currentSubscriptions = data.NewSubscriptions;
89 | const growthRate =
90 | prevSubscriptions === 0
91 | ? 0
92 | : ((currentSubscriptions - prevSubscriptions) / prevSubscriptions) *
93 | 100;
94 | return {
95 | Month: data.Month,
96 | GrowthRate: parseFloat(growthRate.toFixed(2)),
97 | }; // Round to 2 decimal places
98 | }
99 | );
100 | return growthData;
101 | }
102 |
103 | export async function GoodOverBadCampaign() {
104 | const result = await runQuery(GoodOverBadquery);
105 | return result;
106 | }
107 |
--------------------------------------------------------------------------------
/app/actions/query.ts:
--------------------------------------------------------------------------------
1 | export const GoodOverBadquery = `
2 | WITH AvgCampaignMetrics AS (
3 | SELECT
4 | AVG(c."Impressions") AS "AvgImpressions",
5 | AVG(c."Clicks") AS "AvgClicks",
6 | AVG(c."NewSubscriptions") AS "AvgNewSubscriptions",
7 | AVG(c."Revenue") AS "AvgRevenue",
8 | AVG(c."Subscription Cost") AS "AvgSubscriptionCost",
9 | AVG(c."Revenue") - AVG(c."Budget") AS "AvgROI"
10 | FROM
11 | Campaign c
12 | ),
13 |
14 | CampaignMetrics AS (
15 | SELECT
16 | c."CampaignID",
17 | c."Impressions",
18 | c."Clicks",
19 | c."NewSubscriptions",
20 | c."Revenue",
21 | c."Subscription Cost",
22 | (c."Revenue" - c."Budget") AS "ROI",
23 | acm."AvgImpressions",
24 | acm."AvgClicks",
25 | acm."AvgNewSubscriptions",
26 | acm."AvgRevenue",
27 | acm."AvgSubscriptionCost",
28 | acm."AvgROI"
29 | FROM
30 | Campaign c
31 | CROSS JOIN
32 | AvgCampaignMetrics acm
33 | ),
34 |
35 | CategorizedCampaigns AS (
36 | SELECT
37 | cm."CampaignID",
38 | cm."Impressions",
39 | cm."Clicks",
40 | cm."NewSubscriptions",
41 | cm."Revenue",
42 | cm."Subscription Cost",
43 | cm."ROI",
44 | CASE
45 | WHEN cm."Impressions" > cm."AvgImpressions"
46 | AND cm."Clicks" > cm."AvgClicks"
47 | AND cm."NewSubscriptions" > cm."AvgNewSubscriptions"
48 | AND cm."Revenue" > cm."AvgRevenue"
49 | THEN 'Good Campaign'
50 | ELSE 'Bad Campaign'
51 | END AS "CampaignType"
52 | FROM
53 | CampaignMetrics cm
54 | )
55 |
56 | SELECT
57 | cc."CampaignType",
58 | COUNT(*) AS "CampaignCount"
59 | FROM
60 | CategorizedCampaigns cc
61 | GROUP BY
62 | cc."CampaignType";
63 |
64 | `;
65 |
--------------------------------------------------------------------------------
/app/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import { Dashboard } from "@/components/dashboard";
2 | import { Suspense } from "react";
3 | import {
4 | fetchAgeDistributionByLocation,
5 | fetchAudienceData,
6 | fetchContentData,
7 | fetchEngagementData,
8 | fetchPlatformData,
9 | fetchSubscribersByLocation,
10 | } from "../actions";
11 | import {
12 | fetchBudgetData,
13 | fetchClicksData,
14 | fetchImpressionData,
15 | fetchRevenueData,
16 | fetchSubsData,
17 | } from "../actions/kpi";
18 |
19 | export type SearchParams = {
20 | month?: string | "all";
21 | audience?: string | null;
22 | contentType?: string | null;
23 | satisfaction?: string | null;
24 | location?: string | null;
25 | age?: string | null;
26 | platform?: string | null;
27 | campaignId?: string | null;
28 | };
29 |
30 | // export const runtime = "edge";
31 |
32 | export default async function Home({
33 | searchParams,
34 | }: {
35 | searchParams: Promise;
36 | }) {
37 | const resolvedSearchParams = await searchParams;
38 |
39 | const {
40 | month,
41 | audience,
42 | contentType,
43 | satisfaction,
44 | location,
45 | age,
46 | platform,
47 | campaignId,
48 | } = resolvedSearchParams;
49 |
50 | const [
51 | AudienceData,
52 | ContentData,
53 | subscribersByLocation,
54 | ageDistributionByLocation,
55 | RevenueData,
56 | BudgetData,
57 | ClicksData,
58 | ImpressionData,
59 | SubsData,
60 | EngagementData,
61 | PlatformData,
62 | ] = await Promise.all([
63 | fetchAudienceData(
64 | month,
65 | audience,
66 | contentType,
67 | satisfaction,
68 | location,
69 | age,
70 | platform,
71 | campaignId
72 | ),
73 | fetchContentData(
74 | month,
75 | audience,
76 | contentType,
77 | satisfaction,
78 | location,
79 | age,
80 | platform,
81 | campaignId
82 | ),
83 | fetchSubscribersByLocation(
84 | month,
85 | audience,
86 | contentType,
87 | satisfaction,
88 | location,
89 | age,
90 | platform,
91 | campaignId
92 | ),
93 | fetchAgeDistributionByLocation(
94 | month,
95 | audience,
96 | contentType,
97 | satisfaction,
98 | location,
99 | age,
100 | platform,
101 | campaignId
102 | ),
103 | fetchRevenueData(
104 | month,
105 | audience,
106 | contentType,
107 | satisfaction,
108 | location,
109 | age,
110 | platform,
111 | campaignId
112 | ),
113 | fetchBudgetData(
114 | month,
115 | audience,
116 | contentType,
117 | satisfaction,
118 | location,
119 | age,
120 | platform,
121 | campaignId
122 | ),
123 | fetchClicksData(
124 | month,
125 | audience,
126 | contentType,
127 | satisfaction,
128 | location,
129 | age,
130 | platform,
131 | campaignId
132 | ),
133 | fetchImpressionData(
134 | month,
135 | audience,
136 | contentType,
137 | satisfaction,
138 | location,
139 | age,
140 | platform,
141 | campaignId
142 | ),
143 | fetchSubsData(
144 | month,
145 | audience,
146 | contentType,
147 | satisfaction,
148 | location,
149 | age,
150 | platform,
151 | campaignId
152 | ),
153 | fetchEngagementData(
154 | month,
155 | audience,
156 | contentType,
157 | satisfaction,
158 | location,
159 | age,
160 | platform,
161 | campaignId
162 | ),
163 | fetchPlatformData(
164 | month,
165 | audience,
166 | contentType,
167 | satisfaction,
168 | location,
169 | age,
170 | platform,
171 | campaignId
172 | ),
173 | ]);
174 |
175 | return (
176 |
177 | }>
178 |
192 |
193 |
194 | );
195 | }
196 |
197 | async function DashSkeleton() {
198 | return (
199 |
200 |
201 | Hold on...
202 |
203 |
204 | );
205 | }
206 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaarthik108/supa-dash/b3a8d7321cda8764258a51244355e623cbbb13a9/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 222.2 84% 4.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 |
16 | --primary: 222.2 47.4% 11.2%;
17 | --primary-foreground: 210 40% 98%;
18 |
19 | --secondary: 210 40% 96.1%;
20 | --secondary-foreground: 222.2 47.4% 11.2%;
21 |
22 | --muted: 210 40% 96.1%;
23 | --muted-foreground: 215.4 16.3% 46.9%;
24 |
25 | --accent: 210 40% 96.1%;
26 | --accent-foreground: 222.2 47.4% 11.2%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 210 40% 98%;
30 |
31 | --border: 214.3 31.8% 91.4%;
32 | --input: 214.3 31.8% 91.4%;
33 | --ring: 222.2 84% 4.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 222.2 84% 4.9%;
40 | --foreground: 210 40% 98%;
41 |
42 | --card: 222.2 84% 4.9%;
43 | --card-foreground: 210 40% 98%;
44 |
45 | --popover: 222.2 84% 4.9%;
46 | --popover-foreground: 210 40% 98%;
47 |
48 | --primary: 210 40% 98%;
49 | --primary-foreground: 222.2 47.4% 11.2%;
50 |
51 | --secondary: 217.2 32.6% 17.5%;
52 | --secondary-foreground: 210 40% 98%;
53 |
54 | --muted: 217.2 32.6% 17.5%;
55 | --muted-foreground: 215 20.2% 65.1%;
56 |
57 | --accent: 217.2 32.6% 17.5%;
58 | --accent-foreground: 210 40% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 210 40% 98%;
62 |
63 | --border: 217.2 32.6% 17.5%;
64 | --input: 217.2 32.6% 17.5%;
65 | --ring: 212.7 26.8% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
77 |
78 | .custom-scrollbar::-webkit-scrollbar {
79 | width: 0.5rem;
80 | height: 0.5rem;
81 | }
82 |
83 | .custom-scrollbar::-webkit-scrollbar-thumb {
84 | background-color: transparent;
85 | border-radius: 0.25rem;
86 | }
87 |
88 | .custom-scrollbar::-webkit-scrollbar-track {
89 | background-color: transparent;
90 | }
91 |
92 | .custom-scrollbar:hover::-webkit-scrollbar-thumb {
93 | background-color: rgba(0, 0, 0, 0.2);
94 | }
95 |
96 | .custom-scrollbar:hover::-webkit-scrollbar-track {
97 | background-color: rgba(0, 0, 0, 0.1);
98 | }
99 |
100 | @keyframes fade-up {
101 | from {
102 | opacity: 0;
103 | transform: translateY(16px);
104 | }
105 | to {
106 | opacity: 1;
107 | transform: translateY(0px);
108 | }
109 | }
110 |
111 | @keyframes fade-right {
112 | from {
113 | opacity: 0;
114 | transform: translateX(-16px);
115 | }
116 | to {
117 | opacity: 1;
118 | transform: translateX(0px);
119 | }
120 | }
121 |
122 | .animate-fade-up {
123 | animation: fade-up 800ms cubic-bezier(0.34, 1.56, 0.64, 1);
124 | }
125 |
126 | .animate-fade-right {
127 | animation: fade-right 800ms cubic-bezier(0.34, 1.56, 0.64, 1);
128 | }
129 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { TopHeader } from "@/components/Header";
2 | import { IconGitHub } from "@/components/ui/icons";
3 | import { Analytics } from "@vercel/analytics/react";
4 | import type { Metadata } from "next";
5 | import { Roboto } from "next/font/google";
6 | import { Suspense } from "react";
7 | import { AI } from "./action";
8 | import "./globals.css";
9 |
10 | const robo = Roboto({
11 | weight: ["100", "400", "700"],
12 | subsets: ["latin"],
13 | });
14 |
15 | export const metadata: Metadata = {
16 | title: "Hack-Dash",
17 | description: "Hack-Dash is a dashboard built on top of Next.js and Supabase",
18 | metadataBase: new URL("https://supa-dash.vercel.app"),
19 | robots: {
20 | index: true,
21 | follow: true,
22 | googleBot: {
23 | index: true,
24 | follow: true,
25 | "max-video-preview": -1,
26 | "max-image-preview": "large",
27 | "max-snippet": -1,
28 | },
29 | },
30 | twitter: {
31 | title: "Hack-Dash",
32 | card: "summary_large_image",
33 | },
34 | };
35 |
36 | export default function RootLayout({
37 | children,
38 | }: {
39 | children: React.ReactNode;
40 | }) {
41 | return (
42 |
43 |
44 |
45 |
55 |
56 |
57 |
58 |
59 |
62 | Loading....
63 |
64 | }
65 | >
66 | {" "}
67 | {children}
68 |
69 |
70 |
71 |
101 |
102 |
103 |
104 | );
105 | }
106 |
--------------------------------------------------------------------------------
/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaarthik108/supa-dash/b3a8d7321cda8764258a51244355e623cbbb13a9/app/opengraph-image.png
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 |
3 | // export const runtime = "edge";
4 |
5 | export default async function Home() {
6 | redirect("/dashboard/?month=all");
7 | }
8 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaarthik108/supa-dash/b3a8d7321cda8764258a51244355e623cbbb13a9/bun.lockb
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/components/CampaignFilter.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { fetchCampaignIds } from "@/app/actions";
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | Command,
6 | CommandEmpty,
7 | CommandGroup,
8 | CommandInput,
9 | CommandItem,
10 | } from "@/components/ui/command";
11 | import {
12 | Popover,
13 | PopoverContent,
14 | PopoverTrigger,
15 | } from "@/components/ui/popover";
16 | import { ScrollArea } from "@/components/ui/scroll-area";
17 | import { cn } from "@/lib/utils";
18 | import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
19 | import { usePathname, useRouter, useSearchParams } from "next/navigation";
20 | import * as React from "react";
21 | import { useCallback, useEffect, useState } from "react";
22 |
23 | export function CampaignFilter() {
24 | const pathname = usePathname();
25 | const [open, setOpen] = useState(false);
26 | const router = useRouter();
27 | const searchParams = useSearchParams();
28 | const month = searchParams.get("month") || "all";
29 | const audience = searchParams.get("audience") || null;
30 | const contentType = searchParams.get("contentType") || null;
31 | const satisfaction = searchParams.get("satisfaction") || null;
32 | const location = searchParams.get("location") || null;
33 | const age = searchParams.get("age") || null;
34 | const platform = searchParams.get("platform") || null;
35 |
36 | const selectedCampaignId = searchParams.get("campaignId") || null;
37 | const [campaignIds, setCampaignIds] = useState([]);
38 | const [searchQuery, setSearchQuery] = useState("");
39 |
40 | useEffect(() => {
41 | const fetchData = async () => {
42 | const result = (await fetchCampaignIds(
43 | month,
44 | audience,
45 | contentType,
46 | satisfaction,
47 | location,
48 | age,
49 | platform
50 | )) as { CampaignID: string }[];
51 | const ids = result.map((item: { CampaignID: string }) => item.CampaignID);
52 | setCampaignIds(ids);
53 | };
54 | fetchData();
55 | }, [month, audience, contentType, satisfaction, location, age, platform]);
56 |
57 | const handleCampaignChange = useCallback(
58 | (value: string | null) => {
59 | const params = new URLSearchParams(searchParams.toString());
60 | if (value) {
61 | params.set("campaignId", value);
62 | } else {
63 | params.delete("campaignId");
64 | }
65 | router.push(`/dashboard?${params.toString()}`, { scroll: false });
66 | router.refresh();
67 | setOpen(false);
68 | },
69 | [router, searchParams]
70 | );
71 |
72 | const filteredCampaignIds = campaignIds.filter((campaignId) =>
73 | campaignId.toLowerCase().includes(searchQuery.toLowerCase())
74 | );
75 |
76 | if (!pathname?.startsWith("/dashboard")) {
77 | return null;
78 | }
79 |
80 | return (
81 |
82 |
83 |
89 | {selectedCampaignId || "Select Campaign"}
90 |
91 |
92 |
93 |
94 |
95 |
101 | No campaigns found.
102 |
103 |
104 | {filteredCampaignIds.map((campaignId) => (
105 | handleCampaignChange(campaignId)}
109 | className="cursor-pointer flex items-center"
110 | >
111 | {campaignId}
112 |
120 |
121 | ))}
122 |
123 |
124 |
125 |
126 |
127 | );
128 | }
129 |
--------------------------------------------------------------------------------
/components/Charts.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { AI, QueryResult } from "@/app/action";
3 | import { Card } from "@tremor/react";
4 |
5 | import { ChartType } from "@/lib/validation";
6 | import {
7 | AreaComp,
8 | BarComp,
9 | DonutComp,
10 | LineComp,
11 | NumberComp,
12 | ScatterComp,
13 | TableComp,
14 | } from "./llm-charts";
15 |
16 | interface ChartProps {
17 | queryResult: QueryResult;
18 | chartType: ChartType;
19 | title?: string;
20 | description?: string;
21 | timeField?: string;
22 | categories: string[];
23 | index?: string;
24 | yaxis?: string;
25 | size?: string;
26 | }
27 |
28 | export function Chart({
29 | queryResult,
30 | chartType,
31 | title,
32 | timeField,
33 | categories,
34 | index,
35 | yaxis,
36 | size,
37 | }: ChartProps) {
38 | try {
39 | switch (chartType) {
40 | case "area":
41 | return (
42 |
48 | );
49 | case "number":
50 | return ;
51 | case "table":
52 | return ;
53 | case "line":
54 | return (
55 |
61 | );
62 | case "bar":
63 | return (
64 |
70 | );
71 |
72 | case "scatter":
73 | return (
74 |
82 | );
83 | case "donut":
84 | return (
85 |
91 | );
92 |
93 | default:
94 | throw new Error(`Unsupported chart type: ${chartType}`);
95 | }
96 | } catch (error) {
97 | console.error(error);
98 | return (
99 |
100 |
101 |
102 | Error rendering chart
103 |
104 |
105 |
106 | );
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/components/Chat.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { AI } from "@/app/action";
3 | import { ChatList } from "@/components/chat-list";
4 | import { EmptyScreen } from "@/components/empty-screen";
5 | import { UserMessage } from "@/components/message";
6 | import { useEnterSubmit } from "@/lib/hooks/use-enter-submit";
7 | import { useActions, useUIState } from "ai/rsc";
8 | import { Send } from "lucide-react";
9 | import React, { useEffect, useRef, useState } from "react";
10 | import Textarea from "react-textarea-autosize";
11 | import { Button } from "./ui/button";
12 | import {
13 | Card,
14 | CardContent,
15 | CardDescription,
16 | CardFooter,
17 | CardHeader,
18 | CardTitle,
19 | } from "./ui/card";
20 |
21 | export function Chat() {
22 | const [messages, setMessages] = useUIState();
23 | const { submitUserMessage } = useActions();
24 | const [inputValue, setInputValue] = useState("");
25 | const { formRef, onKeyDown } = useEnterSubmit();
26 | const inputRef = useRef(null);
27 |
28 | useEffect(() => {
29 | const handleKeyDown = (e: KeyboardEvent) => {
30 | if (e.key === "/") {
31 | if (
32 | e.target &&
33 | ["INPUT", "TEXTAREA"].includes((e.target as any).nodeName)
34 | ) {
35 | return;
36 | }
37 | e.preventDefault();
38 | e.stopPropagation();
39 | if (inputRef?.current) {
40 | inputRef.current.focus();
41 | }
42 | }
43 | };
44 |
45 | document.addEventListener("keydown", handleKeyDown);
46 |
47 | return () => {
48 | document.removeEventListener("keydown", handleKeyDown);
49 | };
50 | }, [inputRef]);
51 |
52 | return (
53 |
54 |
55 |
56 | Chat
57 | {
60 | setMessages([]);
61 | }}
62 | >
63 | Reset
64 |
65 |
66 |
67 | Chat with our AI assistant to get help on your queries
68 |
69 |
70 |
71 | {messages.length ? (
72 |
73 | ) : (
74 | {
76 | setMessages((currentMessages) => [
77 | ...currentMessages,
78 | {
79 | id: Date.now(),
80 | display: {message} ,
81 | },
82 | ]);
83 | const responseMessage = await submitUserMessage(message);
84 | setMessages((currentMessages) => [
85 | ...currentMessages,
86 | responseMessage,
87 | ]);
88 | }}
89 | />
90 | )}
91 |
92 |
93 |
139 |
140 |
141 | );
142 | }
143 |
--------------------------------------------------------------------------------
/components/ClearParams.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useRouter, useSearchParams } from "next/navigation";
3 | import React from "react";
4 | import { Button } from "./ui/button";
5 |
6 | export function ClearParams() {
7 | const router = useRouter();
8 | const searchParams = useSearchParams();
9 |
10 | const hasSearchParams = searchParams.toString() !== "";
11 |
12 | if (!hasSearchParams) {
13 | return null;
14 | }
15 |
16 | return (
17 |
18 | {
22 | router.replace("/dashboard", { scroll: false });
23 | }}
24 | >
25 | Clear filters
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/components/DashCards/AudienceCard.tsx:
--------------------------------------------------------------------------------
1 | import { BarListChart } from "../charts/BarListChart";
2 | import { Card, CardContent, CardHeader } from "../ui/card";
3 |
4 | type BarListContentData = {
5 | name: string;
6 | value: number;
7 | };
8 |
9 | export async function AudienceCard({
10 | AudienceData,
11 | }: {
12 | AudienceData: BarListContentData[];
13 | }) {
14 | AudienceData.sort((a, b) => b.value - a.value);
15 |
16 | return (
17 |
21 |
22 | Audience
23 |
24 | Audience Type
25 | Revenue
26 |
27 |
28 |
29 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/components/DashCards/ContentCard.tsx:
--------------------------------------------------------------------------------
1 | import { BarListChart } from "../charts/BarListChart";
2 | import { Card, CardContent, CardHeader } from "../ui/card";
3 |
4 | type BarListContentData = {
5 | name: string;
6 | value: number;
7 | };
8 |
9 | export async function ContentCard({
10 | ContentData,
11 | }: {
12 | ContentData: BarListContentData[];
13 | }) {
14 | ContentData.sort((a, b) => b.value - a.value);
15 |
16 | return (
17 |
21 |
22 | Content
23 |
24 | Content Type
25 | Revenue
26 |
27 |
28 |
29 |
33 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/components/DashCards/EngagementCard.tsx:
--------------------------------------------------------------------------------
1 | import { EngagementData, EngagementScatterChart } from "../charts/scatterChart";
2 | import { Card, CardContent, CardHeader } from "../ui/card";
3 |
4 | export async function EngagementCard({
5 | rawData,
6 | }: {
7 | rawData: EngagementData[];
8 | }) {
9 | return (
10 |
11 |
12 | Engagement Rate vs. Satisfaction
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/components/DashCards/LocationDonut.tsx:
--------------------------------------------------------------------------------
1 | import { BarChartComponent } from "../charts/BarChart";
2 | import { DonutChartComponent } from "../charts/DonutChart";
3 | import { Card, CardContent, CardFooter, CardHeader } from "../ui/card";
4 |
5 | type LocationData = {
6 | name: string;
7 | value: number;
8 | };
9 |
10 | export async function LocationCard({
11 | subscribersByLocation,
12 | ageDistributionByLocation,
13 | location,
14 | }: {
15 | subscribersByLocation: Record;
16 | ageDistributionByLocation: Record;
17 | location: string | null;
18 | }) {
19 | const subscribersData: LocationData[] = Object.entries(subscribersByLocation)
20 | .filter(([_, count]) => !isNaN(count))
21 | .map(([locationName, count]) => ({
22 | name: locationName,
23 | value: count,
24 | }));
25 |
26 | const ageDistributionData: LocationData[] = Object.entries(
27 | ageDistributionByLocation
28 | )
29 | .filter(([_, count]) => !isNaN(count))
30 | .map(([ageGroup, count]) => ({
31 | name: ageGroup,
32 | value: count,
33 | }));
34 |
35 | return (
36 |
37 |
41 |
42 | Subscribers by Location
43 |
44 |
45 |
46 |
47 |
48 |
52 |
53 | Age Distribution
54 |
55 |
56 |
61 |
62 |
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/components/DashCards/PlatformCard.tsx:
--------------------------------------------------------------------------------
1 | import { PlatformTable } from "../charts/TableChart";
2 | import { Card, CardContent } from "../ui/card";
3 |
4 | export async function PlatformCard({ rawData }: { rawData: any }) {
5 | return (
6 |
10 |
11 |
12 |
13 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/components/DashCards/index.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { Card } from "../ui/card";
3 | import { Skeleton } from "../ui/skeleton";
4 | import { AudienceCard } from "./AudienceCard";
5 | import { ContentCard } from "./ContentCard";
6 | import { EngagementCard } from "./EngagementCard";
7 | import { LocationCard } from "./LocationDonut";
8 | import { PlatformCard } from "./PlatformCard";
9 |
10 | export function CardSkeleton({ height = 52 }) {
11 | return (
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | export {
19 | AudienceCard as AudienceComp,
20 | ContentCard as ContentComp,
21 | EngagementCard as EngagementComp,
22 | LocationCard as LocationComp,
23 | PlatformCard as PlatformComp,
24 | };
25 |
--------------------------------------------------------------------------------
/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 | import { Suspense } from "react";
4 | import { CampaignFilter } from "./CampaignFilter";
5 | import { ClearParams } from "./ClearParams";
6 | import { PlatformFilter } from "./PlatformFilter";
7 | import { MonthFilter } from "./ui/combobox";
8 |
9 | export function TopHeader() {
10 | return (
11 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/components/MapCard.tsx:
--------------------------------------------------------------------------------
1 | import { fetchSubscribersByLocation } from "@/app/actions";
2 | import { SearchParams } from "@/app/dashboard/page";
3 | import { MapChart } from "./charts/MapChart";
4 |
5 | const regionToCountryIdMap: { [key: string]: string[] } = {
6 | asia: [
7 | "AFG",
8 | "ARM",
9 | "AZE",
10 | "BHR",
11 | "BGD",
12 | "BTN",
13 | "BRN",
14 | "KHM",
15 | "CHN",
16 | "CYP",
17 | "GEO",
18 | "HKG",
19 | "IND",
20 | "IDN",
21 | "IRN",
22 | "IRQ",
23 | "ISR",
24 | "JPN",
25 | "JOR",
26 | "KAZ",
27 | "KWT",
28 | "KGZ",
29 | "LAO",
30 | "LBN",
31 | "MAC",
32 | "MYS",
33 | "MDV",
34 | "MNG",
35 | "MMR",
36 | "NPL",
37 | "PRK",
38 | "OMN",
39 | "PAK",
40 | "PSE",
41 | "PHL",
42 | "QAT",
43 | "SAU",
44 | "SGP",
45 | "KOR",
46 | "LKA",
47 | "SYR",
48 | "TWN",
49 | "TJK",
50 | "THA",
51 | "TLS",
52 | "TUR",
53 | "TKM",
54 | "ARE",
55 | "UZB",
56 | "VNM",
57 | "YEM",
58 | ],
59 | north_america: ["CAN", "USA", "MEX"],
60 | south_america: [
61 | "ARG",
62 | "BOL",
63 | "BRA",
64 | "CHL",
65 | "COL",
66 | "ECU",
67 | "GUY",
68 | "PRY",
69 | "PER",
70 | "SUR",
71 | "URY",
72 | "VEN",
73 | ],
74 | europe: [
75 | "ALB",
76 | "AND",
77 | "AUT",
78 | "BLR",
79 | "BEL",
80 | "BIH",
81 | "BGR",
82 | "HRV",
83 | "CYP",
84 | "CZE",
85 | "DNK",
86 | "EST",
87 | "FIN",
88 | "FRA",
89 | "DEU",
90 | "GRC",
91 | "HUN",
92 | "ISL",
93 | "IRL",
94 | "ITA",
95 | "LVA",
96 | "LIE",
97 | "LTU",
98 | "LUX",
99 | "MLT",
100 | "MCO",
101 | "MNE",
102 | "NLD",
103 | "NOR",
104 | "POL",
105 | "PRT",
106 | "ROU",
107 | "RUS",
108 | "SMR",
109 | "SRB",
110 | "SVK",
111 | "SVN",
112 | "ESP",
113 | "SWE",
114 | "CHE",
115 | "UKR",
116 | "GBR",
117 | "VAT",
118 | ],
119 | oceania: [
120 | "AUS",
121 | "FJI",
122 | "FSM",
123 | "GUM",
124 | "KIR",
125 | "MHL",
126 | "NRU",
127 | "NZL",
128 | "PLW",
129 | "PNG",
130 | "WSM",
131 | "SLB",
132 | "TLS",
133 | "TON",
134 | "TUV",
135 | "VUT",
136 | ],
137 | africa: [
138 | "DZA",
139 | "AGO",
140 | "BEN",
141 | "BWA",
142 | "BFA",
143 | "BDI",
144 | "CPV",
145 | "CMR",
146 | "CAF",
147 | "TCD",
148 | "COM",
149 | "COD",
150 | "COG",
151 | "CIV",
152 | "DJI",
153 | "EGY",
154 | "GNQ",
155 | "ERI",
156 | "ETH",
157 | "GAB",
158 | "GMB",
159 | "GHA",
160 | "GIN",
161 | "GNB",
162 | "KEN",
163 | "LSO",
164 | "LBR",
165 | "LBY",
166 | "MDG",
167 | "MWI",
168 | "MLI",
169 | "MRT",
170 | "MUS",
171 | "MAR",
172 | "MOZ",
173 | "NAM",
174 | "NER",
175 | "NGA",
176 | "RWA",
177 | "STP",
178 | "SEN",
179 | "SYC",
180 | "SLE",
181 | "SOM",
182 | "ZAF",
183 | "SSD",
184 | "SDN",
185 | "SWZ",
186 | "TZA",
187 | "TGO",
188 | "TUN",
189 | "UGA",
190 | "ZMB",
191 | "ZWE",
192 | ],
193 | };
194 |
195 | // export async function MapChartContainer({
196 | // month,
197 | // audience,
198 | // contentType,
199 | // satisfaction,
200 | // }: SearchParams) {
201 | // const subscribersByLocation = await fetchSubscribersByLocation(
202 | // month,
203 | // audience,
204 | // contentType,
205 | // satisfaction
206 | // );
207 |
208 | // // Map the data to the format expected by the MapChart component
209 | // const data = Object.entries(subscribersByLocation).flatMap(
210 | // ([region, count]) => {
211 | // const countryIds = regionToCountryIdMap[region.toLowerCase()];
212 | // if (countryIds) {
213 | // return countryIds.map((id) => ({ id, value: count }));
214 | // }
215 | // return [];
216 | // }
217 | // );
218 |
219 | // return (
220 | // <>
221 | //
222 | // >
223 | // );
224 | // }
225 |
--------------------------------------------------------------------------------
/components/PlatformFilter.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Button } from "@/components/ui/button";
3 | import {
4 | Command,
5 | CommandEmpty,
6 | CommandGroup,
7 | CommandInput,
8 | CommandItem,
9 | } from "@/components/ui/command";
10 | import {
11 | Popover,
12 | PopoverContent,
13 | PopoverTrigger,
14 | } from "@/components/ui/popover";
15 | import { cn } from "@/lib/utils";
16 | import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
17 | import { usePathname, useRouter, useSearchParams } from "next/navigation";
18 | import * as React from "react";
19 | import { useCallback } from "react";
20 | import { SocialIcon } from "react-social-icons";
21 |
22 | const platforms = ["Instagram", "TikTok", "YouTube", "Facebook", "Blogs"];
23 |
24 | export function PlatformFilter() {
25 | const [open, setOpen] = React.useState(false);
26 | const router = useRouter();
27 | const searchParams = useSearchParams();
28 | const selectedPlatform = searchParams.get("platform") || null;
29 | const pathname = usePathname();
30 |
31 | const handlePlatformChange = useCallback(
32 | (value: string | null) => {
33 | const params = new URLSearchParams(searchParams.toString());
34 | if (value) {
35 | params.set("platform", value);
36 | } else {
37 | params.delete("platform");
38 | }
39 | router.push(`/dashboard?${params.toString()}`, { scroll: false });
40 | router.refresh();
41 | setOpen(false);
42 | },
43 | [router, searchParams]
44 | );
45 | if (!pathname?.startsWith("/dashboard")) {
46 | return null;
47 | }
48 | return (
49 |
50 |
51 |
57 | {selectedPlatform || "Select Platform"}
58 |
59 |
60 |
61 |
62 |
63 |
67 | No Platform found.
68 |
69 | {platforms.map((platform) => (
70 | handlePlatformChange(platform)}
74 | className="cursor-pointer flex items-center"
75 | >
76 | {renderPlatformIcon(platform)}
77 | {platform}
78 |
84 |
85 | ))}
86 |
87 |
88 |
89 |
90 | );
91 | }
92 |
93 | const renderPlatformIcon = (platform: string) => {
94 | switch (platform) {
95 | case "Instagram":
96 | return (
97 |
98 | );
99 | case "TikTok":
100 | return ;
101 | case "YouTube":
102 | return ;
103 | case "Facebook":
104 | return (
105 |
106 | );
107 | case "Blogs":
108 | return ;
109 | default:
110 | return null;
111 | }
112 | };
113 |
--------------------------------------------------------------------------------
/components/ai/GoodOverBad.tsx:
--------------------------------------------------------------------------------
1 | import { GoodOverBadCampaign } from "@/app/actions/ai";
2 | import React from "react";
3 | import { TableChartComponent } from "../llm-charts/TableChartComponent";
4 | import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
5 |
6 | export async function GoodOverBad() {
7 | const data = await GoodOverBadCampaign();
8 |
9 | return (
10 |
11 |
12 | Good Vs Bad Campaign
13 |
14 | {`This analysis categorizes each campaign as 'Good' or 'Bad' based on its performance across key metrics—impressions, clicks, new subscriptions, and revenue—relative to the average performance of all campaigns. A 'Good Campaign' exceeds the average in all these areas, indicating a higher return on investment and overall effectiveness`}
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/components/ai/GrowthRate.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | calculateGrowthRateForChart,
3 | fetchSubscriptionsOverTime,
4 | } from "@/app/actions/ai";
5 | import { SearchParams } from "@/app/dashboard/page";
6 | import { LineChartHero } from "../charts/LineChart";
7 | import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
8 |
9 | export async function GrowthRateChartCard({
10 | month = "all",
11 | audience = null,
12 | contentType = null,
13 | satisfaction = null,
14 | }: SearchParams) {
15 | const subscriptionsData = await fetchSubscriptionsOverTime(
16 | audience || null,
17 | contentType || null,
18 | month || null,
19 | satisfaction || null
20 | );
21 |
22 | const growthRateData = await calculateGrowthRateForChart(subscriptionsData);
23 | const chartData = growthRateData.map((data) => ({
24 | Month: data.Month,
25 | Value: data.GrowthRate,
26 | }));
27 |
28 | return (
29 |
30 |
31 |
32 | Growth Rate Over Time
33 |
34 |
35 | The monthly percentage change in new subscriptions compared to the
36 | previous month.
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/components/ai/SubsOverTime.tsx:
--------------------------------------------------------------------------------
1 | import { fetchSubscriptionsOverTime } from "@/app/actions/ai";
2 | import { SearchParams } from "@/app/dashboard/page";
3 | import { LineChartHero } from "../charts/LineChart";
4 | import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
5 |
6 | export async function SubsOverTimeCard({
7 | month,
8 | audience,
9 | contentType,
10 | satisfaction,
11 | }: SearchParams) {
12 | const subscriptionsData = await fetchSubscriptionsOverTime(
13 | audience || null,
14 | contentType || null,
15 | month || null,
16 | satisfaction || null
17 | );
18 | const chartData = subscriptionsData.map((data) => ({
19 | Month: data.Month,
20 | Value: data.NewSubscriptions, // Map GrowthRate to Value for the chart
21 | }));
22 |
23 | return (
24 |
25 |
26 |
27 | Subscribers Over Time
28 |
29 |
30 |
31 |
32 |
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/components/charts/BarChart.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { BarChart } from "@tremor/react";
3 | import { useRouter, useSearchParams } from "next/navigation";
4 | import { useCallback, useEffect, useState } from "react";
5 |
6 | const dataFormatter = (number: number) =>
7 | Intl.NumberFormat("us").format(number).toString();
8 |
9 | export function BarChartComponent({
10 | data,
11 | }: {
12 | data: { name: string; value: number }[];
13 | }) {
14 | const router = useRouter();
15 | const searchParams = useSearchParams();
16 | const [clickedData, setClickedData] = useState<{
17 | name: string;
18 | value: number;
19 | } | null>(null);
20 |
21 | const transformedData = data.map((item) => ({
22 | name: item.name,
23 | Subscribers: item.value,
24 | }));
25 |
26 | useEffect(() => {
27 | const currentFilter = searchParams.get("location");
28 | if (currentFilter) {
29 | const clicked = data.find((item) => item.name === currentFilter);
30 | setClickedData(clicked || null);
31 | } else {
32 | setClickedData(null);
33 | }
34 | }, [searchParams, data]);
35 |
36 | const handleClick = useCallback(
37 | (payload: { name: string; Subscribers: number } | undefined) => {
38 | const params = new URLSearchParams(searchParams.toString());
39 | if (payload) {
40 | if (clickedData && clickedData.name === payload.name) {
41 | params.delete("location");
42 | setClickedData(null);
43 | } else {
44 | params.set("location", payload.name);
45 | setClickedData({ name: payload.name, value: payload.Subscribers });
46 | }
47 | } else {
48 | params.delete("location");
49 | setClickedData(null);
50 | }
51 | router.push(`/dashboard?${params.toString()}`, { scroll: false });
52 | },
53 | [router, searchParams, clickedData]
54 | );
55 |
56 | return (
57 |
65 | handleClick(
66 | payload as { name: string; Subscribers: number } | undefined
67 | )
68 | }
69 | autoMinValue
70 | showAnimation
71 | showTooltip
72 | barCategoryGap={20}
73 | />
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/components/charts/BarListChart.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useRouter, useSearchParams } from "next/navigation";
3 | import { useCallback, useEffect, useState } from "react";
4 | import { BarList } from "./rawBarList";
5 |
6 | type BarListContentData = {
7 | name: string;
8 | value: number;
9 | };
10 |
11 | type BarListChartProps = {
12 | data: BarListContentData[];
13 | filterType: "audience" | "contentType";
14 | };
15 |
16 | export function BarListChart({ data, filterType }: BarListChartProps) {
17 | const router = useRouter();
18 | const searchParams = useSearchParams();
19 | const [filteredData, setFilteredData] = useState(data);
20 |
21 | useEffect(() => {
22 | const currentFilter = searchParams.get(filterType);
23 | if (currentFilter) {
24 | const filtered = data.filter((item) => item.name === currentFilter);
25 | setFilteredData(filtered);
26 | } else {
27 | setFilteredData(data);
28 | }
29 | }, [searchParams, data, filterType]);
30 |
31 | const handleBarClick = useCallback(
32 | (payload: BarListContentData) => {
33 | const params = new URLSearchParams(searchParams.toString());
34 | const currentFilter = params.get(filterType);
35 | if (currentFilter === payload.name) {
36 | params.delete(filterType);
37 | router.refresh();
38 | } else {
39 | params.set(filterType, payload.name);
40 | }
41 | router.push(`/dashboard?${params.toString()}`, { scroll: false });
42 | router.refresh();
43 | },
44 | [router, searchParams, filterType]
45 | );
46 | const formatNumber = (value: number) => {
47 | return new Intl.NumberFormat("en-US", {
48 | notation: "compact",
49 | compactDisplay: "short",
50 | }).format(value);
51 | };
52 | return (
53 |
54 |
60 |
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/components/charts/DonutChart.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { DonutChart, Legend } from "@tremor/react";
3 | import { useRouter, useSearchParams } from "next/navigation";
4 | import { useCallback, useEffect, useState } from "react";
5 |
6 | type DonutChartComponentProps = {
7 | data: { name: string; value: number }[];
8 | variant: "donut" | "pie";
9 | filterType?: "location" | "age";
10 | selectedFilter?: string | null;
11 | };
12 |
13 | const dataFormatter = (number: number) =>
14 | `${Intl.NumberFormat("us").format(number)}`;
15 |
16 | export const DonutChartComponent = ({
17 | data,
18 | variant,
19 | filterType,
20 | selectedFilter,
21 | }: DonutChartComponentProps) => {
22 | const router = useRouter();
23 | const searchParams = useSearchParams();
24 | const [clickedData, setClickedData] = useState<{
25 | name: string;
26 | value: number;
27 | } | null>(null);
28 | const totalValue = data.reduce((sum, item) => sum + item.value, 0);
29 |
30 | useEffect(() => {
31 | const currentFilter = searchParams.get(filterType || "");
32 | if (currentFilter) {
33 | const clicked = data.find((item) => item.name === currentFilter);
34 | setClickedData(clicked || null);
35 | } else {
36 | setClickedData(null);
37 | }
38 | }, [searchParams, data, filterType]);
39 |
40 | const handleClick = useCallback(
41 | (payload: { name: string; value: number } | null) => {
42 | const params = new URLSearchParams(searchParams.toString());
43 | if (payload) {
44 | if (clickedData && clickedData.name === payload.name) {
45 | params.delete(filterType || "");
46 | router.refresh();
47 | setClickedData(null);
48 | } else {
49 | params.set(filterType || "", payload.name);
50 | setClickedData(payload);
51 | }
52 | } else {
53 | params.delete(filterType || "");
54 | setClickedData(null);
55 | }
56 | router.push(`/dashboard?${params.toString()}`, { scroll: false });
57 | router.refresh();
58 | },
59 | [router, searchParams, clickedData, filterType]
60 | );
61 |
62 | const filteredData = clickedData
63 | ? data.filter((d) => d.name === clickedData.name)
64 | : data;
65 |
66 | return (
67 |
68 |
86 | d.name)}
88 | className="text-tremor-content dark:text-dark-tremor-content text-xs mt-6 w-full"
89 | color="text-tremor-content"
90 | colors={[
91 | "teal-700",
92 | "teal-600",
93 | "teal-500",
94 | "teal-400",
95 | "teal-300",
96 | "teal-200",
97 | "teal-100",
98 | ]}
99 | />
100 |
101 | );
102 | };
103 |
--------------------------------------------------------------------------------
/components/charts/LineChart.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { LineChart } from "@tremor/react";
3 | import React from "react";
4 |
5 | interface ChartData {
6 | Month: string;
7 | Value: number;
8 | }
9 |
10 | interface LineChartHeroProps {
11 | chartData: ChartData[];
12 | title: string;
13 | }
14 |
15 | export function LineChartHero({ chartData, title }: LineChartHeroProps) {
16 | const formatNumber = (value: number) =>
17 | new Intl.NumberFormat("en-US", {
18 | notation: "compact",
19 | compactDisplay: "short",
20 | }).format(value);
21 |
22 | if (title === "Subscriptions Over Time") {
23 | return (
24 | ({ ...item, [title]: item.Value }))}
27 | index="Month"
28 | categories={[title]}
29 | colors={["teal"]}
30 | yAxisWidth={60}
31 | valueFormatter={(value) => formatNumber(value)}
32 | />
33 | );
34 | }
35 |
36 | return (
37 | ({ ...item, [title]: item.Value }))}
40 | index="Month"
41 | categories={[title]}
42 | colors={["teal"]}
43 | yAxisWidth={60}
44 | valueFormatter={(value) => `${value.toFixed(2)}%`}
45 | />
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/components/charts/MapChart.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { ResponsiveChoropleth } from "@nivo/geo";
3 | import { useRouter, useSearchParams } from "next/navigation";
4 | import { useCallback } from "react";
5 | import countries from "./world_countries.json";
6 |
7 | type MapChartProps = {
8 | data: { id: string; value: number }[];
9 | };
10 |
11 | export function MapChart({ data }: MapChartProps) {
12 | const router = useRouter();
13 | const searchParams = useSearchParams();
14 |
15 | const handleClick = useCallback(
16 | (feature: any) => {
17 | const location = feature.id;
18 | const params = new URLSearchParams(searchParams.toString());
19 | const currentLocation = params.get("location");
20 |
21 | if (currentLocation === location) {
22 | params.delete("location");
23 | } else {
24 | params.set("location", location);
25 | }
26 |
27 | router.push(`/dashboard?${params.toString()}`, { scroll: false });
28 | },
29 | [router, searchParams]
30 | );
31 |
32 | return (
33 | item.value))]}
39 | unknownColor="#ffffff"
40 | label="properties.name"
41 | valueFormat=".2s"
42 | projectionScale={147}
43 | projectionTranslation={[0.5, 0.6]}
44 | projectionRotation={[0, 0, 0]}
45 | borderWidth={0.5}
46 | borderColor="#fff"
47 | onClick={handleClick}
48 | />
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/components/charts/TableChart.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Badge } from "@/components/ui/badge";
3 | import {
4 | Table,
5 | TableBody,
6 | TableCell,
7 | TableHead,
8 | TableHeaderCell,
9 | TableRow,
10 | } from "@tremor/react";
11 | import { useRouter, useSearchParams } from "next/navigation";
12 | import { useCallback, useEffect, useState } from "react";
13 | import { SocialIcon } from "react-social-icons";
14 | import {
15 | Tooltip,
16 | TooltipContent,
17 | TooltipProvider,
18 | TooltipTrigger,
19 | } from "../ui/tooltip";
20 |
21 | type PlatformData = {
22 | platform: string;
23 | revenue: number;
24 | ROI: number;
25 | CPA: number;
26 | CTR: number;
27 | ConversionRate: number;
28 | };
29 |
30 | type PlatformTableProps = {
31 | data: PlatformData[];
32 | };
33 |
34 | export const PlatformTable = ({ data }: PlatformTableProps) => {
35 | const router = useRouter();
36 | const searchParams = useSearchParams();
37 | const [clickedPlatform, setClickedPlatform] = useState(null);
38 |
39 | useEffect(() => {
40 | const currentPlatform = searchParams.get("platform");
41 | setClickedPlatform(currentPlatform);
42 | }, [searchParams]);
43 |
44 | const formatNumber = (value: number) => {
45 | return new Intl.NumberFormat("en-US", {
46 | notation: "compact",
47 | compactDisplay: "short",
48 | }).format(value);
49 | };
50 |
51 | const handlePlatformClick = useCallback(
52 | (platform: string) => {
53 | const params = new URLSearchParams(searchParams.toString());
54 | if (clickedPlatform === platform) {
55 | params.delete("platform");
56 | setClickedPlatform(null);
57 | } else {
58 | params.set("platform", platform);
59 | setClickedPlatform(platform);
60 | }
61 | router.push(`/dashboard?${params.toString()}`, { scroll: false });
62 | router.refresh();
63 | },
64 | [router, searchParams, clickedPlatform]
65 | );
66 |
67 | return (
68 |
69 |
70 |
71 |
72 |
73 | {data.map((item) => (
74 |
78 | handlePlatformClick(item.platform)}
81 | >
82 | {renderPlatformIcon(item.platform)}
83 |
84 |
85 | ))}
86 |
87 |
88 |
89 |
90 | Revenue
91 | {data.map((item) => (
92 |
93 | {item.revenue < 0 ? (
94 |
95 | ${formatNumber(item.revenue)}
96 |
97 | ) : (
98 | `$${formatNumber(item.revenue)}`
99 | )}
100 |
101 | ))}
102 |
103 |
104 | Return on Investment (ROI)
105 | {data.map((item) => (
106 |
107 | {item.ROI < 0 ? (
108 |
109 | {formatNumber(item.ROI)}%
110 |
111 | ) : (
112 | `${formatNumber(item.ROI)}%`
113 | )}
114 |
115 | ))}
116 |
117 |
118 | Cost per Acquisition (CPA)
119 | {data.map((item) => (
120 |
121 | {item.CPA < 0 ? (
122 |
123 | {formatNumber(item.CPA)}%
124 |
125 | ) : (
126 | `${formatNumber(item.CPA)}%`
127 | )}
128 |
129 | ))}
130 |
131 |
132 | Click Through Rate (CTR)
133 | {data.map((item) => (
134 |
135 | {item.CTR < 0 ? (
136 |
137 | {formatNumber(item.CTR)}%
138 |
139 | ) : (
140 | `${formatNumber(item.CTR)}%`
141 | )}
142 |
143 | ))}
144 |
145 |
146 | ConversionRate
147 | {data.map((item) => (
148 |
149 | {item.ConversionRate < 0 ? (
150 |
151 | {formatNumber(item.ConversionRate)}%
152 |
153 | ) : (
154 | `${formatNumber(item.ConversionRate)}%`
155 | )}
156 |
157 | ))}
158 |
159 |
160 |
161 |
162 | );
163 | };
164 |
165 | const renderPlatformIcon = (platform: string) => {
166 | switch (platform) {
167 | case "Instagram":
168 | return (
169 |
170 |
171 |
172 |
173 |
177 |
178 |
179 |
180 | Instagram
181 |
182 |
183 |
184 | );
185 | case "TikTok":
186 | return (
187 |
188 |
189 |
190 |
191 |
195 |
196 |
197 |
198 | TikTok
199 |
200 |
201 |
202 | );
203 | case "YouTube":
204 | return (
205 |
206 |
207 |
208 |
209 |
213 |
214 |
215 |
216 | YouTube
217 |
218 |
219 |
220 | );
221 | case "Facebook":
222 | return (
223 |
224 |
225 |
226 |
227 |
231 |
232 |
233 |
234 | Facebook
235 |
236 |
237 |
238 | );
239 | case "Blogs":
240 | return (
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 | Blogs
250 |
251 |
252 |
253 | );
254 | default:
255 | return null;
256 | }
257 | };
258 |
--------------------------------------------------------------------------------
/components/charts/rawBarList.tsx:
--------------------------------------------------------------------------------
1 | // Tremor Raw BarList [v0.0.1]
2 |
3 | import { cn, focusRing } from "@/lib/utils";
4 | import React from "react";
5 |
6 | type Bar = T & {
7 | key?: string;
8 | href?: string;
9 | value: number;
10 | name: string;
11 | };
12 |
13 | interface BarListProps
14 | extends React.HTMLAttributes {
15 | data: Bar[];
16 | valueFormatter?: (value: number) => string;
17 | showAnimation?: boolean;
18 | onValueChange?: (payload: Bar) => void;
19 | sortOrder?: "ascending" | "descending";
20 | }
21 |
22 | function BarListInner(
23 | {
24 | data = [],
25 | valueFormatter = (value) => value.toString(),
26 | showAnimation = false,
27 | onValueChange,
28 | sortOrder = "descending",
29 | className,
30 | ...props
31 | }: BarListProps,
32 | forwardedRef: React.ForwardedRef
33 | ) {
34 | const Component = onValueChange ? "button" : "div";
35 | const sortedData = React.useMemo(() => {
36 | if (sortOrder) {
37 | return [...data].sort((a, b) => {
38 | return sortOrder === "ascending"
39 | ? a.value - b.value
40 | : b.value - a.value;
41 | });
42 | }
43 | return data;
44 | }, [data, sortOrder]);
45 |
46 | const widths = React.useMemo(() => {
47 | const maxValue = Math.max(...sortedData.map((item) => item.value), 0);
48 | return sortedData.map((item) =>
49 | item.value === 0 ? 0 : Math.max((item.value / maxValue) * 100, 2)
50 | );
51 | }, [sortedData]);
52 |
53 | const rowHeight = "h-8";
54 |
55 | return (
56 |
62 |
63 | {sortedData.map((item, index) => (
64 |
{
67 | onValueChange?.(item);
68 | }}
69 | className={cn(
70 | // base
71 | "group w-full rounded",
72 | // focus
73 | focusRing,
74 | onValueChange
75 | ? [
76 | "!-m-0 cursor-pointer",
77 | // hover
78 | "",
79 | ]
80 | : ""
81 | )}
82 | >
83 |
135 |
136 | ))}
137 |
138 |
139 | {sortedData.map((item, index) => (
140 |
148 |
156 | {valueFormatter(item.value)}
157 |
158 |
159 | ))}
160 |
161 |
162 | );
163 | }
164 |
165 | BarListInner.displayName = "BarList";
166 |
167 | const BarList = React.forwardRef(BarListInner) as (
168 | p: BarListProps & { ref?: React.ForwardedRef }
169 | ) => ReturnType;
170 |
171 | export { BarList, type BarListProps };
172 |
--------------------------------------------------------------------------------
/components/charts/scatterChart.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { ScatterChart } from "@tremor/react";
3 | import { useRouter, useSearchParams } from "next/navigation";
4 | import { useCallback, useEffect, useState } from "react";
5 |
6 | export type EngagementData = {
7 | satisfaction: string;
8 | engagementRate: number;
9 | subscribers: number;
10 | viewingTime: number;
11 | };
12 |
13 | type EngagementScatterChartProps = {
14 | data: EngagementData[];
15 | };
16 |
17 | const satisfactionColors: Record = {
18 | "Very Unsatisfied": "teal-800",
19 | Unsatisfied: "teal-600",
20 | Satisfied: "teal-400",
21 | Neutral: "teal-200",
22 | "Very Satisfied": "teal-100",
23 | };
24 |
25 | export function EngagementScatterChart({ data }: EngagementScatterChartProps) {
26 | const router = useRouter();
27 | const searchParams = useSearchParams();
28 | const [clickedData, setClickedData] = useState(null);
29 | useEffect(() => {
30 | const currentFilter = searchParams.get("satisfaction");
31 | if (currentFilter) {
32 | const clicked = data.find((item) => item.satisfaction === currentFilter);
33 | setClickedData(clicked || null);
34 | } else {
35 | setClickedData(null);
36 | }
37 | }, [searchParams, data]);
38 |
39 | const handleClick = useCallback(
40 | (payload: EngagementData | null) => {
41 | const params = new URLSearchParams(searchParams.toString());
42 | if (payload) {
43 | if (clickedData && clickedData.satisfaction === payload.satisfaction) {
44 | params.delete("satisfaction");
45 | setClickedData(null);
46 | } else {
47 | params.set("satisfaction", payload.satisfaction);
48 | setClickedData(payload);
49 | }
50 | } else {
51 | params.delete("satisfaction");
52 | setClickedData(null);
53 | }
54 | router.push(`/dashboard?${params.toString()}`, { scroll: false });
55 | },
56 | [router, searchParams, clickedData]
57 | );
58 |
59 | return (
60 | `${rate.toFixed(2)}%`,
74 | y: (count) => `${count} subs`,
75 | size: (time) => `${time.toFixed(2)} secs`,
76 | }}
77 | enableLegendSlider
78 | onValueChange={(payload) => handleClick(payload as EngagementData | null)}
79 | colors={data.map((item) => satisfactionColors[item.satisfaction])}
80 | />
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/components/charts/sparkChart.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { AreaChart } from "@tremor/react";
3 |
4 | type RevenueOverTimeProps = {
5 | chartData: { month: string; value: number | null }[];
6 | };
7 |
8 | export function RevenueOverTime({ chartData }: RevenueOverTimeProps) {
9 | const formatNumber = (value: number) => {
10 | return new Intl.NumberFormat("en-US", {
11 | notation: "compact",
12 | compactDisplay: "short",
13 | }).format(value);
14 | };
15 |
16 | const monthOrder = [
17 | "Jan",
18 | "Feb",
19 | "Mar",
20 | "Apr",
21 | "May",
22 | "Jun",
23 | "Jul",
24 | "Aug",
25 | "Sep",
26 | "Oct",
27 | "Nov",
28 | "Dec",
29 | ];
30 | chartData.sort((a, b) => {
31 | const aIndex = monthOrder.indexOf(a.month);
32 | const bIndex = monthOrder.indexOf(b.month);
33 | return aIndex - bIndex;
34 | });
35 |
36 | return (
37 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/components/chat-list.tsx:
--------------------------------------------------------------------------------
1 | export function ChatList({ messages }: { messages: any[] }) {
2 | if (!messages.length) {
3 | return null;
4 | }
5 |
6 | return (
7 |
11 | {messages.map((message, index) => (
12 |
13 | {message.display}
14 |
15 | ))}
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/components/dashboard.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 | import { Chat } from "./Chat";
3 | import {
4 | AudienceComp,
5 | CardSkeleton,
6 | ContentComp,
7 | EngagementComp,
8 | LocationComp,
9 | PlatformComp,
10 | } from "./DashCards";
11 |
12 | import {
13 | BudgetCard,
14 | ClicksCard,
15 | ImpressionCard,
16 | RevenueCard,
17 | SubscriberCard,
18 | } from "./kpi";
19 |
20 | export async function Dashboard({
21 | RevenueData,
22 | BudgetData,
23 | ImpressionData,
24 | ClicksData,
25 | SubsData,
26 | AudienceData,
27 | ContentData,
28 | subscribersByLocation,
29 | ageDistributionByLocation,
30 | EngagementData,
31 | location,
32 | PlatformData,
33 | }: {
34 | RevenueData: any;
35 | BudgetData: any;
36 | ImpressionData: any;
37 | ClicksData: any;
38 | SubsData: any;
39 | AudienceData: any;
40 | ContentData: any;
41 | subscribersByLocation: any;
42 | ageDistributionByLocation: any;
43 | EngagementData: any;
44 | location: any;
45 | PlatformData: any;
46 | }) {
47 | return (
48 |
49 |
50 |
51 | }>
52 |
53 |
54 | }>
55 |
56 |
57 | }>
58 |
59 |
60 | }>
61 |
62 |
63 | }>
64 |
65 |
66 |
67 |
68 |
69 |
74 |
75 | }>
76 |
81 |
82 |
83 |
84 | }>
85 |
86 |
87 |
88 |
89 |
90 |
91 |
}>
92 |
93 |
94 |
}>
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | );
108 | }
109 |
--------------------------------------------------------------------------------
/components/empty-screen.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { IconArrowRight } from "@/components/ui/icons";
3 |
4 | const exampleMessages = [
5 | {
6 | heading: "Show me Subscribers growth over time?",
7 | message: "Show me Subscribers growth over time?",
8 | },
9 | {
10 | heading: "What is the total subscriptions? show as number card",
11 | message: "What is the total subscriptions? show as number card",
12 | },
13 | {
14 | heading: "Give me number of subscriptions over time? using line chart",
15 | message: "Give me number of subscriptions over time? using line chart",
16 | },
17 | {
18 | heading: "Could you count good vs bad campaigns?",
19 | message: "Could you count good vs bad campaigns?",
20 | },
21 | ];
22 |
23 | export function EmptyScreen({
24 | submitMessage,
25 | }: {
26 | submitMessage: (message: string) => void;
27 | }) {
28 | return (
29 |
30 |
31 |
Chat based store
32 |
33 | This is a demo of a chat based store. You can ask questions the model
34 | would execute sql queries and show you the results in form of charts.
35 |
36 |
Try an example:
37 |
38 | {exampleMessages.map((message, index) => (
39 | {
44 | submitMessage(message.message);
45 | }}
46 | >
47 |
48 | {message.heading}
49 |
50 | ))}
51 |
52 |
53 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/components/kpi/BudgetCard.tsx:
--------------------------------------------------------------------------------
1 | import { CreditCard } from "lucide-react";
2 | import { RevenueOverTime } from "../charts/sparkChart";
3 | import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
4 | import { Separator } from "../ui/separator";
5 |
6 | export async function BudgetCard({
7 | rawData,
8 | }: {
9 | rawData: { CampaignMonth: string; Budget: string }[];
10 | }) {
11 | const BudgetData = rawData.map((item) => ({
12 | month: item.CampaignMonth || ("" as string),
13 | value: item.Budget ? parseInt(item.Budget, 10) : null,
14 | }));
15 |
16 | const totalRevenue = BudgetData.reduce(
17 | (sum, item) => sum + ((item.value as number) || 0),
18 | 0
19 | );
20 |
21 | const formatter = new Intl.NumberFormat("en-US", {
22 | style: "currency",
23 | currency: "USD",
24 | minimumFractionDigits: 0,
25 | maximumFractionDigits: 0,
26 | });
27 |
28 | const formattedTotalRevenue = formatter.format(totalRevenue);
29 | return (
30 |
34 |
35 |
36 | Total Budget
37 |
38 |
39 |
40 |
41 |
42 | {formattedTotalRevenue}
43 | {/* +20.1% from last month
*/}
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/components/kpi/ClicksCard.tsx:
--------------------------------------------------------------------------------
1 | import { MousePointer2 } from "lucide-react";
2 | import { RevenueOverTime } from "../charts/sparkChart";
3 | import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
4 | import { Separator } from "../ui/separator";
5 |
6 | export async function ClicksCard({
7 | rawData,
8 | }: {
9 | rawData: { CampaignMonth: string; Clicks: string }[];
10 | }) {
11 | const ClicksData = rawData.map((item) => ({
12 | month: item.CampaignMonth || ("" as string),
13 | value: item.Clicks ? parseInt(item.Clicks, 10) : null,
14 | }));
15 | const totalRevenue = ClicksData.reduce(
16 | (sum, item) => sum + ((item.value as number) || 0),
17 | 0
18 | );
19 |
20 | const formatter = new Intl.NumberFormat("en-US", {
21 | minimumFractionDigits: 0,
22 | maximumFractionDigits: 0,
23 | });
24 |
25 | const formattedTotalRevenue = formatter.format(totalRevenue);
26 | return (
27 |
31 |
32 |
33 | Total Clicks
34 |
35 |
36 |
37 |
38 |
39 | {formattedTotalRevenue}
40 | {/* +20.1% from last month
*/}
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/components/kpi/ImpressionCard.tsx:
--------------------------------------------------------------------------------
1 | import { Eye } from "lucide-react";
2 | import { RevenueOverTime } from "../charts/sparkChart";
3 | import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
4 | import { Separator } from "../ui/separator";
5 |
6 | export async function ImpressionCard({
7 | rawData,
8 | }: {
9 | rawData: { CampaignMonth: string; Impressions: string }[];
10 | }) {
11 | const ImpressionData = rawData.map((item) => ({
12 | month: item.CampaignMonth || ("" as string),
13 | value: item.Impressions ? parseInt(item.Impressions, 10) : null,
14 | }));
15 |
16 | const totalRevenue = ImpressionData.reduce(
17 | (sum, item) => sum + ((item.value as number) || 0),
18 | 0
19 | );
20 |
21 | const formatter = new Intl.NumberFormat("en-US", {
22 | minimumFractionDigits: 0,
23 | maximumFractionDigits: 0,
24 | });
25 |
26 | const formattedTotalRevenue = formatter.format(totalRevenue);
27 |
28 | return (
29 |
33 |
34 |
35 | Total Impressions
36 |
37 |
38 |
39 |
40 |
41 | {formattedTotalRevenue}
42 | {/* +20.1% from last month
*/}
43 |
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/components/kpi/RevenueCard.tsx:
--------------------------------------------------------------------------------
1 | import { DollarSign } from "lucide-react";
2 | import { RevenueOverTime } from "../charts/sparkChart";
3 | import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
4 | import { Separator } from "../ui/separator";
5 |
6 | export async function RevenueCard({
7 | rawData,
8 | }: {
9 | rawData: { CampaignMonth: string; Revenue: string }[];
10 | }) {
11 | const revenueData = rawData.map((item) => ({
12 | month: item.CampaignMonth || ("" as string),
13 | value: item.Revenue ? parseInt(item.Revenue, 10) : null,
14 | }));
15 |
16 | const totalRevenue = revenueData.reduce(
17 | (sum, item) => sum + ((item.value as number) || 0),
18 | 0
19 | );
20 |
21 | const formatter = new Intl.NumberFormat("en-US", {
22 | style: "currency",
23 | currency: "USD",
24 | minimumFractionDigits: 0,
25 | maximumFractionDigits: 0,
26 | });
27 |
28 | const formattedTotalRevenue = formatter.format(totalRevenue);
29 |
30 | return (
31 |
38 |
39 |
40 | Total Revenue
41 |
42 |
43 |
44 |
45 |
46 | {formattedTotalRevenue}
47 | {/* +20.1% from last month
*/}
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/components/kpi/SubscriberCard.tsx:
--------------------------------------------------------------------------------
1 | import { Users } from "lucide-react";
2 | import { RevenueOverTime } from "../charts/sparkChart";
3 | import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
4 | import { Separator } from "../ui/separator";
5 |
6 | export async function SubscriberCard({
7 | rawData,
8 | }: {
9 | rawData: { CampaignMonth: string; NewSubscriptions: string }[];
10 | }) {
11 | const SubsData = rawData.map((item) => ({
12 | month: item.CampaignMonth || ("" as string),
13 | value: item.NewSubscriptions ? parseInt(item.NewSubscriptions, 10) : null,
14 | }));
15 | const totalRevenue = SubsData.reduce(
16 | (sum, item) => sum + ((item.value as number) || 0),
17 | 0
18 | );
19 |
20 | const formatter = new Intl.NumberFormat("en-US", {
21 | minimumFractionDigits: 0,
22 | maximumFractionDigits: 0,
23 | });
24 |
25 | const formattedTotalSubs = formatter.format(totalRevenue);
26 |
27 | return (
28 |
32 |
33 |
34 | Total Subscribers
35 |
36 |
37 |
38 |
39 |
40 | {formattedTotalSubs}
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/components/kpi/index.tsx:
--------------------------------------------------------------------------------
1 | import { Card } from "../ui/card";
2 | import { Skeleton } from "../ui/skeleton";
3 | import { BudgetCard } from "./BudgetCard";
4 | import { ClicksCard } from "./ClicksCard";
5 | import { ImpressionCard } from "./ImpressionCard";
6 | import { RevenueCard } from "./RevenueCard";
7 | import { SubscriberCard } from "./SubscriberCard";
8 |
9 | function CardSkeleton() {
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | export { BudgetCard, ClicksCard, ImpressionCard, RevenueCard, SubscriberCard };
18 |
--------------------------------------------------------------------------------
/components/llm-charts/AreaChartComponent.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { QueryResult } from "@/app/action";
4 | import { AreaChart, Card } from "@tremor/react";
5 |
6 | interface FilteredEntry {
7 | [key: string]: number | string;
8 | }
9 |
10 | export function AreaChartComponent({
11 | queryResult,
12 | title,
13 | timeField,
14 | categories,
15 | }: {
16 | queryResult: QueryResult;
17 | title?: string;
18 | timeField?: string;
19 | categories: string[];
20 | onSelectionChange?: (selectedData: any) => void;
21 | }) {
22 | const dataFormatter = (number: number): string =>
23 | `$${Intl.NumberFormat("us").format(number)}`;
24 |
25 | if (!timeField) {
26 | console.error("timeField is undefined");
27 | return null;
28 | }
29 |
30 | const filteredData: FilteredEntry[] = queryResult.data.map(
31 | (entry): FilteredEntry => {
32 | const filteredEntry: FilteredEntry = {};
33 | for (const [key, value] of Object.entries(entry)) {
34 | const lowercaseKey = key.toLowerCase();
35 | if (lowercaseKey === timeField.toLowerCase()) {
36 | filteredEntry[timeField] = value as string;
37 | } else {
38 | const matchedCategory = categories.find(
39 | (category) => category.toLowerCase() === lowercaseKey
40 | );
41 | if (matchedCategory) {
42 | filteredEntry[matchedCategory] = value as number;
43 | }
44 | }
45 | }
46 | return filteredEntry;
47 | }
48 | );
49 |
50 | return (
51 | <>
52 |
53 |
54 | {title}
55 |
56 | console.log(v)}
67 | />
68 |
69 | >
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/components/llm-charts/AreaSkeleton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Card } from "@tremor/react";
3 | import React from "react";
4 | import { BotMessage, SystemMessage } from "../message";
5 | import { Skeleton } from "../ui/skeleton";
6 |
7 | export default function AreaSkeleton() {
8 | return (
9 | <>
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | >
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/components/llm-charts/BarChartComponent.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { QueryResult } from "@/app/action";
3 | import { BarChart, Card } from "@tremor/react";
4 |
5 | interface BarChartComponentProps {
6 | queryResult: QueryResult;
7 | title?: string;
8 | categories: string[];
9 | index?: string;
10 | }
11 |
12 | export function BarChartComponent({
13 | queryResult,
14 | title,
15 | categories,
16 | index,
17 | }: BarChartComponentProps) {
18 | const dataFormatter = (number: number) =>
19 | Intl.NumberFormat("us").format(number).toString();
20 |
21 | const filteredData = queryResult.data.map((entry) => {
22 | const filteredEntry: { [key: string]: string | number } = {};
23 |
24 | if (index) {
25 | filteredEntry[index] = entry[index.toUpperCase()];
26 | }
27 |
28 | categories.forEach((category) => {
29 | const upperCaseCategory = category.toUpperCase();
30 | if (entry.hasOwnProperty(upperCaseCategory)) {
31 | filteredEntry[category] = entry[upperCaseCategory];
32 | }
33 | });
34 |
35 | return filteredEntry;
36 | });
37 | // console.log("index is", index);
38 | // console.log("categories are", categories);
39 | // console.log("filteredData is", filteredData);
40 |
41 | return (
42 | <>
43 |
44 |
45 |
46 | {title}
47 |
48 |
console.log(v)}
56 | showAnimation={true}
57 | animationDuration={1000}
58 | />
59 |
60 |
61 | >
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/components/llm-charts/DonutChartComponent.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { QueryResult } from "@/app/action";
3 | import { Card, DonutChart } from "@tremor/react";
4 | import { useRef } from "react";
5 | import { Button } from "../ui/button";
6 |
7 | interface DonutChartComponentProps {
8 | queryResult: QueryResult;
9 | title?: string;
10 | index?: string;
11 | category?: string;
12 | }
13 |
14 | export function DonutChartComponent({
15 | queryResult,
16 | title,
17 | index,
18 | category,
19 | }: DonutChartComponentProps) {
20 | const dataFormatter = (number: number) =>
21 | Intl.NumberFormat("us").format(number).toString();
22 |
23 | const filteredData = queryResult.data.map((entry) => {
24 | const filteredEntry: { [key: string]: string | number } = {};
25 |
26 | if (index && entry.hasOwnProperty(index)) {
27 | filteredEntry[index] = entry[index];
28 | }
29 |
30 | if (category && entry.hasOwnProperty(category)) {
31 | filteredEntry[category] = entry[category];
32 | }
33 |
34 | return filteredEntry;
35 | });
36 |
37 | return (
38 | <>
39 |
40 |
41 | {title}
42 |
43 | console.log(v)}
51 | showAnimation={true}
52 | animationDuration={1000}
53 | />
54 |
55 | >
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/components/llm-charts/LineChartComponent.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { QueryResult } from "@/app/action";
3 | import { Card, LineChart } from "@tremor/react";
4 |
5 | export function LineChartComponent({
6 | queryResult,
7 | title,
8 | categories,
9 | index,
10 | }: {
11 | queryResult: QueryResult;
12 | title?: string;
13 | categories: string[];
14 | index?: string;
15 | }) {
16 | const dataFormatter = (number: number) =>
17 | `$${Intl.NumberFormat("us").format(number).toString()}`;
18 | const filteredData = queryResult.data.map((entry) => {
19 | const filteredEntry: { [key: string]: string | number } = {};
20 |
21 | if (index) {
22 | if (entry.hasOwnProperty(index)) {
23 | filteredEntry[index] = entry[index];
24 | }
25 | }
26 |
27 | categories.forEach((category) => {
28 | if (entry.hasOwnProperty(category)) {
29 | filteredEntry[category] = entry[category];
30 | }
31 | });
32 |
33 | return filteredEntry;
34 | });
35 | return (
36 | <>
37 |
38 |
39 | {title}
40 |
41 |
42 |
53 |
54 |
55 | >
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/components/llm-charts/NumberChartComponent.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { QueryResult } from "@/app/action";
4 | import { Card } from "@tremor/react";
5 |
6 | export function NumberChartComponent({
7 | queryResult,
8 | title,
9 | }: {
10 | queryResult: QueryResult;
11 | title?: string;
12 | }) {
13 | if (
14 | queryResult.data.length === 1 &&
15 | Object.keys(queryResult.data[0]).length === 1
16 | ) {
17 | const key = Object.keys(queryResult.data[0])[0];
18 | const value = queryResult.data[0][key];
19 | const isNumeric = !isNaN(parseFloat(value)) && isFinite(value);
20 |
21 | return (
22 |
27 |
28 | {title}
29 |
30 |
31 | {isNumeric ? `$${parseFloat(value).toFixed(2)}` : value}
32 |
33 |
34 | );
35 | } else {
36 | return (
37 |
38 |
39 | Expected data not found.
40 |
41 |
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/components/llm-charts/NumberSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Card } from "@tremor/react";
2 | import React from "react";
3 |
4 | function NumberSkeleton() {
5 | return (
6 |
11 |
12 | {"xxxxxx"}
13 |
14 |
15 | . . .
16 |
17 |
18 | );
19 | }
20 |
21 | export default NumberSkeleton;
22 |
--------------------------------------------------------------------------------
/components/llm-charts/ScatterChartComponent.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { QueryResult } from "@/app/action";
3 | import { Card, ScatterChart } from "@tremor/react";
4 |
5 | interface ScatterChartComponentProps {
6 | queryResult: QueryResult;
7 | title?: string;
8 | index?: string;
9 | category: string;
10 | yaxis: string;
11 | size: string;
12 | }
13 |
14 | export function ScatterChartComponent({
15 | queryResult,
16 | title,
17 | index,
18 | category,
19 | yaxis,
20 | size,
21 | }: ScatterChartComponentProps) {
22 | const filteredData = queryResult.data.map((entry) => {
23 | const filteredEntry: { [key: string]: string | number } = {};
24 |
25 | if (index && entry.hasOwnProperty(index)) {
26 | filteredEntry[index] = entry[index];
27 | }
28 |
29 | if (category && entry.hasOwnProperty(category)) {
30 | filteredEntry[category] = entry[category];
31 | }
32 |
33 | if (yaxis && entry.hasOwnProperty(yaxis)) {
34 | filteredEntry[yaxis] = entry[yaxis];
35 | }
36 |
37 | if (size && entry.hasOwnProperty(size)) {
38 | filteredEntry[size] = entry[size];
39 | }
40 |
41 | return filteredEntry;
42 | });
43 | // console.log("index is", index);
44 | // console.log("category is", category);
45 | // console.log("yaxis is", yaxis);
46 | // console.log("size is", size);
47 | // console.log("filteredData is", filteredData);
48 |
49 | return (
50 | <>
51 |
52 |
53 | {title}
54 |
55 | console.log(v)}
68 | showAnimation={true}
69 | animationDuration={1000}
70 | />
71 |
72 | >
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/components/llm-charts/TableChartComponent.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { QueryResult } from "@/app/action";
4 | import {
5 | Card,
6 | Table,
7 | TableBody,
8 | TableCell,
9 | TableHead,
10 | TableHeaderCell,
11 | TableRow,
12 | } from "@tremor/react";
13 |
14 | export function TableChartComponent({
15 | queryResult,
16 | title,
17 | }: {
18 | queryResult: QueryResult;
19 | title?: string;
20 | }) {
21 | return (
22 |
23 |
24 |
25 | {title}
26 |
27 |
28 |
29 |
30 | {queryResult.columns.map((column) => (
31 | {column}
32 | ))}
33 |
34 |
35 |
36 | {queryResult.data.map((item, rowIndex) => (
37 |
38 | {queryResult.columns.map((column) => (
39 |
40 | {item[column]}
41 |
42 | ))}
43 |
44 | ))}
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/components/llm-charts/index.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import dynamic from "next/dynamic";
4 | import AreaSkeleton from "./AreaSkeleton";
5 | import NumberSkeleton from "./NumberSkeleton";
6 |
7 | export { Chart } from "@/components/Charts";
8 |
9 | const AreaComp = dynamic(
10 | () => import("./AreaChartComponent").then((mod) => mod.AreaChartComponent),
11 | {
12 | ssr: false,
13 | loading: () => ,
14 | }
15 | );
16 |
17 | const NumberComp = dynamic(
18 | () =>
19 | import("./NumberChartComponent").then((mod) => mod.NumberChartComponent),
20 | {
21 | ssr: false,
22 | loading: () => ,
23 | }
24 | );
25 |
26 | const TableComp = dynamic(
27 | () => import("./TableChartComponent").then((mod) => mod.TableChartComponent),
28 | {
29 | ssr: false,
30 | loading: () => ,
31 | }
32 | );
33 |
34 | const LineComp = dynamic(
35 | () => import("./LineChartComponent").then((mod) => mod.LineChartComponent),
36 | {
37 | ssr: false,
38 | loading: () => ,
39 | }
40 | );
41 |
42 | const DonutComp = dynamic(
43 | () => import("./DonutChartComponent").then((mod) => mod.DonutChartComponent),
44 | {
45 | ssr: false,
46 | loading: () => ,
47 | }
48 | );
49 |
50 | const ScatterComp = dynamic(
51 | () =>
52 | import("./ScatterChartComponent").then((mod) => mod.ScatterChartComponent),
53 | {
54 | ssr: false,
55 | loading: () => ,
56 | }
57 | );
58 |
59 | const BarComp = dynamic(
60 | () => import("./BarChartComponent").then((mod) => mod.BarChartComponent),
61 | {
62 | ssr: false,
63 | loading: () => ,
64 | }
65 | );
66 |
67 | export {
68 | AreaComp,
69 | BarComp,
70 | DonutComp,
71 | LineComp,
72 | NumberComp,
73 | ScatterComp,
74 | TableComp,
75 | };
76 |
--------------------------------------------------------------------------------
/components/llm-charts/markdown.tsx:
--------------------------------------------------------------------------------
1 | import { FC, memo } from "react";
2 | // import ReactMarkdown, { Options } from "react-markdown";
3 |
4 | // export const MemoizedReactMarkdown: FC = memo(
5 | // ReactMarkdown,
6 | // (prevProps, nextProps) =>
7 | // prevProps.children === nextProps.children &&
8 | // prevProps.className === nextProps.className,
9 | // );
10 |
--------------------------------------------------------------------------------
/components/message.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { IconAI, IconUser } from "@/components/ui/icons";
4 | import { cn } from "@/lib/utils";
5 |
6 | // Different types of message bubbles.
7 |
8 | export function UserMessage({ children }: { children: React.ReactNode }) {
9 | return (
10 |
11 |
12 |
13 |
14 |
15 | {children}
16 |
17 |
18 | );
19 | }
20 |
21 | export function BotMessage({
22 | children,
23 | className,
24 | }: {
25 | children: React.ReactNode;
26 | className?: string;
27 | }) {
28 | return (
29 |
30 |
31 |
32 |
33 |
34 | {children}
35 |
36 |
37 | );
38 | }
39 |
40 | export function BotCard({
41 | children,
42 | showAvatar = true,
43 | }: {
44 | children: React.ReactNode;
45 | showAvatar?: boolean;
46 | }) {
47 | return (
48 |
49 |
55 |
56 |
57 |
{children}
58 |
59 | );
60 | }
61 |
62 | export function SystemMessage({ children }: { children: React.ReactNode }) {
63 | return (
64 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = "Card";
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = "CardHeader";
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ));
45 | CardTitle.displayName = "CardTitle";
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ));
57 | CardDescription.displayName = "CardDescription";
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ));
65 | CardContent.displayName = "CardContent";
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ));
77 | CardFooter.displayName = "CardFooter";
78 |
79 | export {
80 | Card,
81 | CardContent,
82 | CardDescription,
83 | CardFooter,
84 | CardHeader,
85 | CardTitle,
86 | };
87 |
--------------------------------------------------------------------------------
/components/ui/combobox.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Button } from "@/components/ui/button";
3 | import {
4 | Command,
5 | CommandEmpty,
6 | CommandGroup,
7 | CommandInput,
8 | CommandItem,
9 | } from "@/components/ui/command";
10 | import {
11 | Popover,
12 | PopoverContent,
13 | PopoverTrigger,
14 | } from "@/components/ui/popover";
15 | import { cn } from "@/lib/utils";
16 | import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
17 | import { usePathname, useRouter, useSearchParams } from "next/navigation";
18 | import * as React from "react";
19 | import { useCallback } from "react";
20 |
21 | const months = [
22 | "Jan",
23 | "Feb",
24 | "Mar",
25 | "Apr",
26 | "May",
27 | "Jun",
28 | "Jul",
29 | "Aug",
30 | "Sep",
31 | "Oct",
32 | ];
33 |
34 | export function MonthFilter() {
35 | const [open, setOpen] = React.useState(false);
36 | const router = useRouter();
37 | const searchParams = useSearchParams();
38 | const selectedMonth = searchParams.get("month") || "all";
39 | const pathname = usePathname();
40 |
41 | const handleMonthChange = useCallback(
42 | (value: string) => {
43 | const params = new URLSearchParams(searchParams.toString());
44 | params.set("month", value);
45 | router.push(`/dashboard?${params.toString()}`, { scroll: false });
46 | router.refresh();
47 | setOpen(false);
48 | },
49 | [router, searchParams]
50 | );
51 | if (!pathname?.startsWith("/dashboard")) {
52 | return null;
53 | }
54 | return (
55 |
56 |
57 |
63 | {selectedMonth === "all"
64 | ? "All Months"
65 | : months.find((month) => month === selectedMonth)}
66 |
67 |
68 |
69 |
70 |
71 |
75 | No Month found.
76 |
77 | handleMonthChange("all")}
81 | className="cursor-pointer"
82 | >
83 | All Months
84 |
90 |
91 | {months.map((month) => (
92 | handleMonthChange(month)}
96 | className="cursor-pointer"
97 | >
98 | {month}
99 |
105 |
106 | ))}
107 |
108 |
109 |
110 |
111 | );
112 | }
113 |
--------------------------------------------------------------------------------
/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { type DialogProps } from "@radix-ui/react-dialog"
5 | import { Command as CommandPrimitive } from "cmdk"
6 | import { Search } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | interface CommandDialogProps extends DialogProps {}
27 |
28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29 | return (
30 |
31 |
32 |
33 | {children}
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | const CommandInput = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
45 |
46 |
54 |
55 | ))
56 |
57 | CommandInput.displayName = CommandPrimitive.Input.displayName
58 |
59 | const CommandList = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, ...props }, ref) => (
63 |
68 | ))
69 |
70 | CommandList.displayName = CommandPrimitive.List.displayName
71 |
72 | const CommandEmpty = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >((props, ref) => (
76 |
81 | ))
82 |
83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
84 |
85 | const CommandGroup = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
97 | ))
98 |
99 | CommandGroup.displayName = CommandPrimitive.Group.displayName
100 |
101 | const CommandSeparator = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
112 |
113 | const CommandItem = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
125 | ))
126 |
127 | CommandItem.displayName = CommandPrimitive.Item.displayName
128 |
129 | const CommandShortcut = ({
130 | className,
131 | ...props
132 | }: React.HTMLAttributes) => {
133 | return (
134 |
141 | )
142 | }
143 | CommandShortcut.displayName = "CommandShortcut"
144 |
145 | export {
146 | Command,
147 | CommandDialog,
148 | CommandInput,
149 | CommandList,
150 | CommandEmpty,
151 | CommandGroup,
152 | CommandItem,
153 | CommandShortcut,
154 | CommandSeparator,
155 | }
156 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "@/lib/utils";
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | }
22 | );
23 | Input.displayName = "Input";
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ))
135 | SelectItem.displayName = SelectPrimitive.Item.displayName
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ))
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | }
161 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/components/ui/spinner.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | export const spinner = (
4 |
14 |
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className
48 | )}
49 | {...props}
50 | />
51 | ))
52 | TableFooter.displayName = "TableFooter"
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ))
67 | TableRow.displayName = "TableRow"
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 |
81 | ))
82 | TableHead.displayName = "TableHead"
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 |
93 | ))
94 | TableCell.displayName = "TableCell"
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ))
106 | TableCaption.displayName = "TableCaption"
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | }
118 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import * as dotenv from "dotenv";
2 | import type { Config } from "drizzle-kit";
3 | dotenv.config({ path: ".env.local" });
4 |
5 | if (!process.env.DATABASE_URL) {
6 | console.log("🔴 Cannot find database url");
7 | }
8 |
9 | export default {
10 | schema: "./src/lib/supabase/schema.ts",
11 | out: "./migrations",
12 | driver: "pg",
13 | dbCredentials: {
14 | connectionString: process.env.DATABASE_URL || "",
15 | },
16 | } satisfies Config;
17 |
--------------------------------------------------------------------------------
/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { sql as _sql } from "@vercel/postgres";
2 | import { sql } from "drizzle-orm";
3 | import { drizzle } from "drizzle-orm/vercel-postgres";
4 |
5 | const db = drizzle(_sql);
6 |
7 | export async function runQuery(query: string) {
8 | const result = await db.execute(sql`${sql.raw(query)}`);
9 |
10 | const data = result.rows;
11 |
12 | const columns = result.fields.map((field) => field.name);
13 |
14 | const formattedResult = {
15 | columns,
16 | data,
17 | };
18 |
19 | return formattedResult;
20 | }
21 |
22 | // async function testCurrentDate() {
23 | // const result = await runQuery("SELECT CURRENT_DATE");
24 | // console.log("Formatted Result:", result);
25 | // return result;
26 | // }
27 |
28 | // testCurrentDate();
29 |
--------------------------------------------------------------------------------
/lib/execute.ts:
--------------------------------------------------------------------------------
1 | // import { db } from "./db";
2 | // import { getDb } from "./db";
3 |
4 | export async function _runQuery(query: string) {
5 | // const db = await getDb();
6 | // const result = await db.execute(sql`${sql.raw(query)}`);
7 | console.log("Query:", query);
8 |
9 | // Check if we are in a production-like environment with a 'Result' object
10 | // if ("rows" in result) {
11 | // console.log("Query result (Production):", result);
12 | // const data = result.rows;
13 | // const columns = result.fields.map((field) => field.name); // Assuming 'fields' is always present in this case
14 | // return { columns, data };
15 | // } else {
16 | // // We are in development, handling the direct array result
17 | // const data = result;
18 | // // This assumes that the structure of objects in the result array is consistent,
19 | // // and that using the keys from the first object to determine the column names is accurate.
20 | // const columns = result.length > 0 ? Object.keys(result[0]) : [];
21 | // return { columns, data };
22 | // }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/hooks/chat-scroll-anchor.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { useInView } from "react-intersection-observer";
5 | import { useAtBottom } from "./use-at-bottom";
6 |
7 | interface ChatScrollAnchorProps {
8 | trackVisibility?: boolean;
9 | }
10 |
11 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) {
12 | const isAtBottom = useAtBottom();
13 | const { ref, entry, inView } = useInView({
14 | trackVisibility,
15 | delay: 100,
16 | rootMargin: "0px",
17 | threshold: 0.9,
18 | });
19 |
20 | React.useEffect(() => {
21 | if (isAtBottom && trackVisibility && !inView) {
22 | entry?.target.scrollIntoView({
23 | block: "start",
24 | });
25 | }
26 | }, [inView, entry, isAtBottom, trackVisibility]);
27 |
28 | return
;
29 | }
30 |
--------------------------------------------------------------------------------
/lib/hooks/use-at-bottom.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | export function useAtBottom(offset = 0) {
4 | const [isAtBottom, setIsAtBottom] = React.useState(false);
5 |
6 | React.useEffect(() => {
7 | const handleScroll = () => {
8 | setIsAtBottom(
9 | window.innerHeight + window.scrollY >=
10 | document.body.offsetHeight - offset
11 | );
12 | };
13 |
14 | window.addEventListener("scroll", handleScroll, { passive: true });
15 | handleScroll();
16 |
17 | return () => {
18 | window.removeEventListener("scroll", handleScroll);
19 | };
20 | }, [offset]);
21 |
22 | return isAtBottom;
23 | }
24 |
--------------------------------------------------------------------------------
/lib/hooks/use-enter-submit.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, type RefObject } from "react";
2 |
3 | export function useEnterSubmit(): {
4 | formRef: RefObject;
5 | onKeyDown: (event: React.KeyboardEvent) => void;
6 | } {
7 | const formRef = useRef(null);
8 |
9 | const handleKeyDown = (
10 | event: React.KeyboardEvent
11 | ): void => {
12 | if (
13 | event.key === "Enter" &&
14 | !event.shiftKey &&
15 | !event.nativeEvent.isComposing
16 | ) {
17 | formRef.current?.requestSubmit();
18 | event.preventDefault();
19 | }
20 | };
21 |
22 | return { formRef, onKeyDown: handleKeyDown };
23 | }
24 |
--------------------------------------------------------------------------------
/lib/redis.ts:
--------------------------------------------------------------------------------
1 | import { Redis } from "@upstash/redis";
2 |
3 | if (!process.env.UPSTASH_REDIS_URL || !process.env.UPSTASH_REDIS_TOKEN) {
4 | throw new Error(
5 | "Please link a KV instance or populate `UPSTASH_REDIS_URL` and `UPSTASH_REDIS_TOKEN`",
6 | );
7 | }
8 |
9 | export const redis = new Redis({
10 | url: process.env.UPSTASH_REDIS_URL,
11 | token: process.env.UPSTASH_REDIS_TOKEN,
12 | });
13 |
--------------------------------------------------------------------------------
/lib/supabase/browser.ts:
--------------------------------------------------------------------------------
1 | import { Database } from "@/types/database.types";
2 | import { createBrowserClient } from "@supabase/ssr";
3 |
4 | export function supabaseBrowser() {
5 | return createBrowserClient(
6 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
7 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/lib/supabase/server.ts:
--------------------------------------------------------------------------------
1 | import { Database } from "@/types/database.types";
2 | import { createServerClient, type CookieOptions } from "@supabase/ssr";
3 | import { cookies } from "next/headers";
4 |
5 | const createFetch =
6 | (options: Pick) =>
7 | (url: RequestInfo | URL, init?: RequestInit) => {
8 | return fetch(url, {
9 | ...init,
10 | ...options,
11 | });
12 | };
13 |
14 | export function supabaseServer(cacheTags: string[] = []) {
15 | const cookieStore = cookies();
16 |
17 | return createServerClient(
18 | process.env.NEXT_PUBLIC_SUPABASE_URL!,
19 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
20 | {
21 | cookies: {
22 | get(name: string) {
23 | return cookieStore.get(name)?.value;
24 | },
25 | set(name: string, value: string, options: CookieOptions) {
26 | cookieStore.set({ name, value, ...options });
27 | },
28 | remove(name: string, options: CookieOptions) {
29 | cookieStore.set({ name, value: "", ...options });
30 | },
31 | },
32 | global: {
33 | fetch: createFetch({
34 | next: { tags: ["supabase", ...cacheTags] },
35 | }),
36 | },
37 | }
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/lib/tool-definition.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | /**
4 | * A tool definition contains all information required for a language model to generate tool calls.
5 | */
6 | export interface ToolDefinition {
7 | /**
8 | * The name of the tool.
9 | * Should be understandable for language models and unique among the tools that they know.
10 | *
11 | * Note: Using generics to enable result type inference when there are multiple tool calls.
12 | */
13 | name: NAME;
14 |
15 | /**
16 | * A optional description of what the tool does. Will be used by the language model to decide whether to use the tool.
17 | */
18 | description?: string;
19 |
20 | /**
21 | * The schema of the input that the tool expects. The language model will use this to generate the input.
22 | * Use descriptions to make the input understandable for the language model.
23 | */
24 | parameters: z.Schema;
25 | }
26 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { ToolDefinition } from "@/lib/tool-definition";
2 | import { OpenAIStream } from "ai";
3 | import { clsx, type ClassValue } from "clsx";
4 | import type OpenAI from "openai";
5 | import { twMerge } from "tailwind-merge";
6 | import zodToJsonSchema from "zod-to-json-schema";
7 |
8 | export function cn(...inputs: ClassValue[]) {
9 | return twMerge(clsx(inputs));
10 | }
11 | export const focusInput = [
12 | // base
13 | "focus:ring-2",
14 | // ring color
15 | "focus:ring-blue-200 focus:dark:ring-blue-700/30",
16 | // border color
17 | "focus:border-blue-500 focus:dark:border-blue-700",
18 | ];
19 |
20 | // Tremor Raw focusRing [v0.0.1]
21 |
22 | export const focusRing = [
23 | // base
24 | "",
25 | // outline color
26 | "outline-blue-500 dark:outline-blue-500",
27 | ];
28 |
29 | export function groupByField(
30 | data: any[],
31 | groupField: string,
32 | valueField: string
33 | ): { month: string; value: number | null }[] {
34 | const groupedData: { [key: string]: number } = {};
35 |
36 | data.forEach((item) => {
37 | const groupValue = item[groupField];
38 | const value = item[valueField];
39 |
40 | if (groupValue && typeof value === "number") {
41 | const formattedGroupValue = formatDate(groupValue.toString());
42 | if (formattedGroupValue !== "Invalid Date") {
43 | if (groupedData[formattedGroupValue]) {
44 | groupedData[formattedGroupValue] += value;
45 | } else {
46 | groupedData[formattedGroupValue] = value;
47 | }
48 | }
49 | }
50 | });
51 |
52 | const result = Object.entries(groupedData).map(([month, value]) => ({
53 | month,
54 | value: value || null,
55 | }));
56 |
57 | result.sort((a, b) => {
58 | const monthA = new Date(a.month);
59 | const monthB = new Date(b.month);
60 | return monthA.getTime() - monthB.getTime();
61 | });
62 |
63 | return result;
64 | }
65 |
66 | function formatDate(dateString: string) {
67 | const dateParts = dateString.split(".");
68 | if (dateParts.length !== 3) {
69 | return "Invalid Date";
70 | }
71 |
72 | const [day, month, year] = dateParts;
73 | const formattedDate = new Date(`${month}.${day}.${year}`);
74 |
75 | if (isNaN(formattedDate.getTime())) {
76 | return "Invalid Date";
77 | }
78 |
79 | const formattedMonth = formattedDate.toLocaleString("default", {
80 | month: "short",
81 | });
82 | const formattedYear = formattedDate.getFullYear().toString().slice(-2);
83 | return `${formattedMonth} ${formattedYear}`;
84 | }
85 |
86 | const consumeStream = async (stream: ReadableStream) => {
87 | const reader = stream.getReader();
88 | while (true) {
89 | const { done } = await reader.read();
90 | if (done) break;
91 | }
92 | };
93 |
94 | export function runOpenAICompletion<
95 | T extends Omit<
96 | Parameters[0],
97 | "functions"
98 | > & {
99 | functions: ToolDefinition[];
100 | }
101 | >(openai: OpenAI, params: T) {
102 | let text = "";
103 | let hasFunction = false;
104 |
105 | type FunctionNames = T["functions"] extends Array
106 | ? T["functions"][number]["name"]
107 | : never;
108 |
109 | let onTextContent: (text: string, isFinal: boolean) => void = () => {};
110 |
111 | let onFunctionCall: Record) => void> = {};
112 |
113 | const { functions, ...rest } = params;
114 |
115 | (async () => {
116 | consumeStream(
117 | OpenAIStream(
118 | (await openai.chat.completions.create({
119 | ...rest,
120 | stream: true,
121 | functions: functions.map((fn) => ({
122 | name: fn.name,
123 | description: fn.description,
124 | parameters: zodToJsonSchema(fn.parameters) as Record<
125 | string,
126 | unknown
127 | >,
128 | })),
129 | })) as any,
130 | {
131 | async experimental_onFunctionCall(functionCallPayload) {
132 | hasFunction = true;
133 | onFunctionCall[
134 | functionCallPayload.name as keyof typeof onFunctionCall
135 | ]?.(functionCallPayload.arguments as Record);
136 | },
137 | onToken(token) {
138 | text += token;
139 | if (text.startsWith("{")) return;
140 | onTextContent(text, false);
141 | },
142 | onFinal() {
143 | if (hasFunction) return;
144 | onTextContent(text, true);
145 | },
146 | }
147 | )
148 | );
149 | })();
150 |
151 | return {
152 | onTextContent: (
153 | callback: (text: string, isFinal: boolean) => void | Promise
154 | ) => {
155 | onTextContent = callback;
156 | },
157 | onFunctionCall: (
158 | name: FunctionNames,
159 | callback: (args: any) => void | Promise
160 | ) => {
161 | onFunctionCall[name] = callback;
162 | },
163 | };
164 | }
165 |
166 | export const formatNumber = (value: number) =>
167 | new Intl.NumberFormat("en-US", {
168 | style: "currency",
169 | currency: "USD",
170 | }).format(value);
171 |
172 | export const runAsyncFnWithoutBlocking = (
173 | fn: (...args: any) => Promise
174 | ) => {
175 | fn();
176 | };
177 |
178 | export const sleep = (ms: number) =>
179 | new Promise((resolve) => setTimeout(resolve, ms));
180 |
--------------------------------------------------------------------------------
/lib/validation/index.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | const chartTypes = z.enum([
4 | "area",
5 | "number",
6 | "table",
7 | "bar",
8 | "line",
9 | "donut",
10 | "scatter",
11 | ]);
12 | export type ChartType = z.infer;
13 |
14 | export const FQueryResponse = z.object({
15 | query: z.string().describe(`
16 | Creates a postgres SQL query based on the context and given query.
17 | `),
18 | format: chartTypes.describe(
19 | "The format of the result, which determines the type of chart to generate."
20 | ),
21 | title: z
22 | .string()
23 | .describe(
24 | "The title for the chart, which is displayed prominently above the chart."
25 | ),
26 | timeField: z
27 | .string()
28 | .optional()
29 | .describe(
30 | "Used for time series data, designating the column that represents the time dimension. This field is used as the x-axis in charts like area and bar (if the bar chart is time-based), and potentially as the x-axis in scatter charts."
31 | ),
32 | categories: z
33 | .array(z.string())
34 | .describe(
35 | "An array of strings that represent the numerical data series names to be visualized on the chart for 'area', 'bar', and 'line' charts. These should correspond to fields in the data that contain numerical values to plot."
36 | ),
37 | index: z
38 | .string()
39 | .optional()
40 | .describe(
41 | "For 'bar' and 'scatter' charts, this denotes the primary categorical axis or the x-axis labels. For time series bar charts, this can often be the same as timeField."
42 | ),
43 | // Fields specific to scatter chart
44 | category: z
45 | .string()
46 | .optional()
47 | .describe(
48 | "The category field for scatter charts, defining how data points are grouped."
49 | ),
50 | yaxis: z
51 | .string()
52 | .optional()
53 | .describe(
54 | "The field representing the y-axis value in scatter charts. (THIS IS REQUIRED FOR SCATTER CHARTS)"
55 | ),
56 | size: z
57 | .string()
58 | .optional()
59 | .describe(
60 | "The field representing the size of the data points in scatter charts. (THIS IS REQUIRED FOR SCATTER CHARTS)"
61 | ),
62 | });
63 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | webpack: (config, { webpack }) => {
4 | config.plugins.push(
5 | new webpack.IgnorePlugin({
6 | resourceRegExp: /^pg-native$|^cloudflare:sockets$/,
7 | })
8 | );
9 |
10 | return config;
11 | },
12 | };
13 |
14 | export default nextConfig;
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "subs-dash",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@cloudflare/next-on-pages": "^1.11.0",
13 | "@headlessui/react": "^1.7.18",
14 | "@headlessui/tailwindcss": "^0.2.0",
15 | "@nivo/geo": "^0.85.1",
16 | "@radix-ui/react-avatar": "^1.0.4",
17 | "@radix-ui/react-dialog": "^1.0.5",
18 | "@radix-ui/react-dropdown-menu": "^2.0.6",
19 | "@radix-ui/react-icons": "^1.3.0",
20 | "@radix-ui/react-popover": "^1.0.7",
21 | "@radix-ui/react-scroll-area": "^1.0.5",
22 | "@radix-ui/react-select": "^2.0.0",
23 | "@radix-ui/react-separator": "^1.0.3",
24 | "@radix-ui/react-slot": "^1.0.2",
25 | "@radix-ui/react-tooltip": "^1.0.7",
26 | "@remixicon/react": "^4.2.0",
27 | "@supabase/ssr": "^0.1.0",
28 | "@supabase/supabase-js": "^2.39.8",
29 | "@tremor/react": "latest",
30 | "@tsparticles/engine": "^3.3.0",
31 | "@tsparticles/react": "^3.0.0",
32 | "@tsparticles/slim": "^3.3.0",
33 | "@upstash/redis": "^1.29.0",
34 | "@vercel/analytics": "^1.2.2",
35 | "@vercel/postgres": "^0.8.0",
36 | "ai": "^3.0.17",
37 | "bright": "^0.8.5",
38 | "class-variance-authority": "^0.7.0",
39 | "clsx": "^2.1.0",
40 | "cmdk": "0.2.0",
41 | "dotenv": "^16.4.5",
42 | "drizzle-orm": "^0.30.7",
43 | "framer-motion": "^11.0.20",
44 | "lucide-react": "^0.363.0",
45 | "next": "14.0.4",
46 | "openai": "^4.32.1",
47 | "pg": "^8.11.5",
48 | "react": "^18",
49 | "react-dom": "^18",
50 | "react-intersection-observer": "^9.8.1",
51 | "react-simple-maps": "^3.0.0",
52 | "react-social-icons": "^6.15.0",
53 | "react-textarea-autosize": "^8.5.3",
54 | "sql-formatter": "^15.3.0",
55 | "tailwind-merge": "^2.2.2",
56 | "tailwindcss-animate": "^1.0.7",
57 | "zod": "^3.22.4",
58 | "zod-to-json-schema": "^3.22.5"
59 | },
60 | "devDependencies": {
61 | "@tailwindcss/forms": "^0.5.7",
62 | "@types/node": "^20",
63 | "@types/pg": "^8.11.4",
64 | "@types/react": "^18",
65 | "@types/react-dom": "^18",
66 | "@types/react-simple-maps": "^3.0.4",
67 | "autoprefixer": "^10.0.1",
68 | "drizzle-kit": "^0.20.14",
69 | "eslint": "^8",
70 | "eslint-config-next": "14.1.4",
71 | "postcss": "^8",
72 | "tailwindcss": "^3.3.0",
73 | "typescript": "^5"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/public/line-chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaarthik108/supa-dash/b3a8d7321cda8764258a51244355e623cbbb13a9/public/line-chart.png
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/supabase/.gitignore:
--------------------------------------------------------------------------------
1 | # Supabase
2 | .branches
3 | .temp
4 | seed.sql
--------------------------------------------------------------------------------
/supabase/config.toml:
--------------------------------------------------------------------------------
1 | # A string used to distinguish different Supabase projects on the same host. Defaults to the working
2 | # directory name when running `supabase init`.
3 | project_id = "subs-dash"
4 |
5 | [api]
6 | # Port to use for the API URL.
7 | port = 54321
8 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
9 | # endpoints. public and storage are always included.
10 | schemas = ["public", "storage", "graphql_public"]
11 | # Extra schemas to add to the search_path of every request. public is always included.
12 | extra_search_path = ["public", "extensions"]
13 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
14 | # for accidental or malicious requests.
15 | max_rows = 40000
16 |
17 | [db]
18 | # Port to use for the local database URL.
19 | port = 54322
20 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW
21 | # server_version;` on the remote database to check.
22 | major_version = 15
23 |
24 | [db.pooler]
25 | enabled = false
26 | # Port to use for the local connection pooler.
27 | port = 54329
28 | # Specifies when a server connection can be reused by other clients.
29 | # Configure one of the supported pooler modes: `transaction`, `session`.
30 | pool_mode = "transaction"
31 | # How many server connections to allow per user/database pair.
32 | default_pool_size = 20
33 | # Maximum number of client connections allowed.
34 | max_client_conn = 100
35 |
36 |
37 | [studio]
38 | # Port to use for Supabase Studio.
39 | port = 54323
40 |
41 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
42 | # are monitored, and you can view the emails that would have been sent from the web interface.
43 | [inbucket]
44 | # Port to use for the email testing server web interface.
45 | port = 54324
46 | smtp_port = 54325
47 | pop3_port = 54326
48 |
49 | [storage]
50 | # The maximum file size allowed (e.g. "5MB", "500KB").
51 | file_size_limit = "50MiB"
52 |
53 | [auth]
54 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
55 | # in emails.
56 | site_url = "http://localhost:3000"
57 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
58 | additional_redirect_urls = ["https://localhost:3000"]
59 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 seconds (one
60 | # week).
61 | jwt_expiry = 3600
62 | # Allow/disallow new user signups to your project.
63 | enable_signup = true
64 |
65 | [auth.email]
66 | # Allow/disallow new user signups via email to your project.
67 | enable_signup = true
68 | # If enabled, a user will be required to confirm any email change on both the old, and new email
69 | # addresses. If disabled, only the new email is required to confirm.
70 | double_confirm_changes = true
71 | # If enabled, users need to confirm their email address before signing in.
72 | enable_confirmations = false
73 |
74 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
75 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin`, `notion`, `twitch`,
76 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`.
77 | [auth.external.apple]
78 | enabled = false
79 | client_id = ""
80 | secret = ""
81 | # Overrides the default auth redirectUrl.
82 | redirect_uri = ""
83 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
84 | # or any other third-party OIDC providers.
85 | url = ""
86 |
87 | [analytics]
88 | enabled = false
89 | port = 54327
90 | vector_port = 54328
91 | # Setup BigQuery project to enable log viewer on local development stack.
92 | # See: https://supabase.com/docs/guides/getting-started/local-development#enabling-local-logging
93 | gcp_project_id = ""
94 | gcp_project_number = ""
95 | gcp_jwt_path = "supabase/gcloud.json"
96 |
--------------------------------------------------------------------------------
/supabase/functions/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["denoland.vscode-deno"]
3 | }
4 |
--------------------------------------------------------------------------------
/supabase/functions/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "deno.lint": true,
4 | "editor.defaultFormatter": "denoland.vscode-deno"
5 | }
6 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 | import colors from "tailwindcss/colors";
3 |
4 | const config = {
5 | darkMode: ["class"],
6 | content: [
7 | "./pages/**/*.{ts,tsx}",
8 | "./components/**/*.{ts,tsx}",
9 | "./app/**/*.{ts,tsx}",
10 | "./src/**/*.{ts,tsx}",
11 | "./node_modules/@tremor/**/*.{js,ts,jsx,tsx}",
12 | ],
13 | prefix: "",
14 | theme: {
15 | transparent: "transparent",
16 | current: "currentColor",
17 | container: {
18 | center: true,
19 | padding: "2rem",
20 | screens: {
21 | "2xl": "1400px",
22 | },
23 | },
24 | extend: {
25 | colors: {
26 | border: "hsl(var(--border))",
27 | input: "hsl(var(--input))",
28 | ring: "hsl(var(--ring))",
29 | background: "hsl(var(--background))",
30 | foreground: "hsl(var(--foreground))",
31 | primary: {
32 | DEFAULT: "hsl(var(--primary))",
33 | foreground: "hsl(var(--primary-foreground))",
34 | },
35 | secondary: {
36 | DEFAULT: "hsl(var(--secondary))",
37 | foreground: "hsl(var(--secondary-foreground))",
38 | },
39 | destructive: {
40 | DEFAULT: "hsl(var(--destructive))",
41 | foreground: "hsl(var(--destructive-foreground))",
42 | },
43 | muted: {
44 | DEFAULT: "hsl(var(--muted))",
45 | foreground: "hsl(var(--muted-foreground))",
46 | },
47 | accent: {
48 | DEFAULT: "hsl(var(--accent))",
49 | foreground: "hsl(var(--accent-foreground))",
50 | },
51 | popover: {
52 | DEFAULT: "hsl(var(--popover))",
53 | foreground: "hsl(var(--popover-foreground))",
54 | },
55 | card: {
56 | DEFAULT: "hsl(var(--card))",
57 | foreground: "hsl(var(--card-foreground))",
58 | },
59 | tremor: {
60 | brand: {
61 | faint: colors.blue[50],
62 | muted: colors.blue[200],
63 | subtle: colors.blue[400],
64 | DEFAULT: colors.blue[500],
65 | emphasis: colors.blue[700],
66 | inverted: colors.white,
67 | green: colors.green,
68 | teal: colors.teal[500],
69 | },
70 | background: {
71 | muted: colors.gray[50],
72 | subtle: colors.gray[100],
73 | DEFAULT: colors.white,
74 | emphasis: colors.gray[700],
75 | },
76 | border: {
77 | DEFAULT: colors.gray[200],
78 | },
79 | ring: {
80 | DEFAULT: colors.gray[200],
81 | },
82 | content: {
83 | subtle: colors.gray[400],
84 | DEFAULT: colors.gray[500],
85 | emphasis: colors.gray[700],
86 | strong: colors.gray[900],
87 | inverted: colors.white,
88 | teal: colors.teal[500],
89 | },
90 | },
91 | // dark mode
92 | "dark-tremor": {
93 | brand: {
94 | faint: "#0B1229",
95 | muted: colors.blue[950],
96 | subtle: colors.blue[800],
97 | DEFAULT: colors.blue[500],
98 | emphasis: colors.blue[400],
99 | inverted: colors.blue[950],
100 | green: colors.green,
101 | },
102 | background: {
103 | muted: "#131A2B",
104 | subtle: colors.gray[800],
105 | DEFAULT: colors.gray[900],
106 | emphasis: colors.gray[300],
107 | },
108 | border: {
109 | DEFAULT: colors.gray[800],
110 | },
111 | ring: {
112 | DEFAULT: colors.gray[800],
113 | },
114 | content: {
115 | subtle: colors.gray[600],
116 | DEFAULT: colors.gray[500],
117 | emphasis: colors.gray[200],
118 | strong: colors.gray[50],
119 | inverted: colors.gray[950],
120 | },
121 | },
122 | },
123 | boxShadow: {
124 | // light
125 | "tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
126 | "tremor-card":
127 | "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
128 | "tremor-dropdown":
129 | "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
130 | // dark
131 | "dark-tremor-input": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
132 | "dark-tremor-card":
133 | "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
134 | "dark-tremor-dropdown":
135 | "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
136 | },
137 | borderRadius: {
138 | lg: "var(--radius)",
139 | md: "calc(var(--radius) - 2px)",
140 | sm: "calc(var(--radius) - 4px)",
141 | "tremor-small": "0.375rem",
142 | "tremor-default": "0.5rem",
143 | "tremor-full": "9999px",
144 | },
145 | fontSize: {
146 | "tremor-label": ["0.75rem", { lineHeight: "1rem" }],
147 | "tremor-default": ["0.875rem", { lineHeight: "1.25rem" }],
148 | "tremor-title": ["1.125rem", { lineHeight: "1.75rem" }],
149 | "tremor-metric": ["1.875rem", { lineHeight: "2.25rem" }],
150 | },
151 | keyframes: {
152 | "accordion-down": {
153 | from: { height: "0" },
154 | to: { height: "var(--radix-accordion-content-height)" },
155 | },
156 | "accordion-up": {
157 | from: { height: "var(--radix-accordion-content-height)" },
158 | to: { height: "0" },
159 | },
160 | },
161 | animation: {
162 | "accordion-down": "accordion-down 0.2s ease-out",
163 | "accordion-up": "accordion-up 0.2s ease-out",
164 | },
165 | },
166 | },
167 | safelist: [
168 | {
169 | pattern:
170 | /^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
171 | variants: ["hover", "ui-selected"],
172 | },
173 | {
174 | pattern:
175 | /^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
176 | variants: ["hover", "ui-selected"],
177 | },
178 | {
179 | pattern:
180 | /^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
181 | variants: ["hover", "ui-selected"],
182 | },
183 | {
184 | pattern:
185 | /^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
186 | },
187 | {
188 | pattern:
189 | /^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
190 | },
191 | {
192 | pattern:
193 | /^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/,
194 | },
195 | ],
196 | plugins: [
197 | require("tailwindcss-animate"),
198 | require("@headlessui/tailwindcss"),
199 | require("@tailwindcss/forms"),
200 | ],
201 | } satisfies Config;
202 |
203 | export default config;
204 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/types/global.d.ts:
--------------------------------------------------------------------------------
1 | import type { Database as DB } from "./database.types";
2 |
3 | declare global {
4 | type Database = DB;
5 | type subscribers = DB["public"]["Tables"]["subscriber"]["Row"];
6 | type campaigns = DB["public"]["Tables"]["campaign"]["Row"];
7 | }
8 |
--------------------------------------------------------------------------------