├── public ├── favicon.ico ├── img │ ├── full-logo.png │ ├── charts │ │ ├── area-chart.png │ │ ├── bar-chart.png │ │ ├── pie-chart.png │ │ ├── table-chart.png │ │ └── counter-chart.png │ ├── databases │ │ ├── planetscale.svg │ │ ├── supabase.svg │ │ └── mysql.svg │ └── supaboard.svg └── favicon │ ├── favicon.ico │ ├── favicon.png │ └── favicon.svg ├── src ├── app │ ├── favicon.ico │ ├── opengraph-image.png │ ├── (app) │ │ ├── dashboards │ │ │ ├── [id] │ │ │ │ └── loading.js │ │ │ ├── loading.js │ │ │ ├── actions.js │ │ │ └── page.js │ │ ├── segments │ │ │ ├── new │ │ │ │ └── layout.js │ │ │ ├── [id] │ │ │ │ └── page.js │ │ │ ├── page.js │ │ │ └── actions.js │ │ ├── setup │ │ │ ├── state │ │ │ │ └── route.js │ │ │ └── page.js │ │ ├── settings │ │ │ ├── layout.js │ │ │ ├── workspace │ │ │ │ ├── new │ │ │ │ │ ├── actions.js │ │ │ │ │ └── page.js │ │ │ │ └── [account_id] │ │ │ │ │ └── actions.js │ │ │ ├── page.js │ │ │ └── billing │ │ │ │ └── page.js │ │ ├── layout.js │ │ ├── thank-you │ │ │ └── page.js │ │ ├── contacts │ │ │ ├── action.js │ │ │ └── import │ │ │ │ └── page.js │ │ ├── workflows │ │ │ └── page.js │ │ └── databases │ │ │ ├── new │ │ │ └── page.js │ │ │ ├── page.js │ │ │ ├── actions.js │ │ │ └── [id] │ │ │ ├── loading.js │ │ │ └── page.js │ ├── (public) │ │ └── public │ │ │ └── layout.js │ ├── (auth) │ │ ├── auth │ │ │ ├── signout │ │ │ │ └── route.js │ │ │ └── callback │ │ │ │ └── route.js │ │ ├── login │ │ │ └── page.js │ │ ├── register │ │ │ └── page.js │ │ └── layout.js │ ├── api │ │ ├── dashboards │ │ │ ├── hash │ │ │ │ └── [hash] │ │ │ │ │ └── route.js │ │ │ ├── route.js │ │ │ ├── data │ │ │ │ └── route.js │ │ │ └── [uuid] │ │ │ │ ├── route.js │ │ │ │ └── charts │ │ │ │ └── [chart_id] │ │ │ │ └── route.js │ │ ├── settings │ │ │ └── route.js │ │ ├── databases │ │ │ ├── route.js │ │ │ └── [id] │ │ │ │ └── route.js │ │ ├── segments │ │ │ ├── route.js │ │ │ └── [id] │ │ │ │ └── route.js │ │ ├── contacts │ │ │ ├── segments │ │ │ │ └── route.js │ │ │ └── route.js │ │ ├── counts │ │ │ └── route.js │ │ ├── webhooks │ │ │ └── stripe │ │ │ │ └── route.js │ │ └── externaldb │ │ │ └── [id] │ │ │ └── route.js │ ├── layout.js │ └── globals.css ├── lib │ ├── stripe │ │ ├── stripe.js │ │ └── stripe-client.js │ ├── operators.js │ ├── database.js │ ├── crypto.js │ ├── adapters │ │ ├── postgres │ │ │ └── querybuilder.js │ │ └── mysql │ │ │ └── querybuilder.js │ └── auth.js ├── components │ ├── util │ │ ├── index.js │ │ └── Modal.js │ ├── Skeleton.js │ ├── dashboards │ │ ├── charts │ │ │ ├── ChartError.js │ │ │ ├── ChartErrorBoundary.js │ │ │ ├── CounterChart.js │ │ │ ├── PieChart.js │ │ │ ├── AreaChart.js │ │ │ ├── TableChart.js │ │ │ └── ChartHeader.js │ │ ├── NoDashboards.js │ │ ├── steps │ │ │ ├── ChartOptions.js │ │ │ └── ChartDatabase.js │ │ ├── DashboardDeleteModal.js │ │ ├── DashboardNewModal.js │ │ └── DashboardEditModal.js │ ├── brand │ │ └── Logo.js │ ├── img │ │ ├── Bitbucket.js │ │ ├── Gitlab.js │ │ ├── Github.js │ │ └── Slack.js │ ├── Loading.js │ ├── auth │ │ └── AuthForm.js │ ├── settings │ │ ├── SettingsNav.js │ │ └── SubscriptionSettings.js │ ├── segments │ │ ├── SegmentSidebar.js │ │ ├── SegmentDeleteModal.js │ │ ├── ContactResetModal.js │ │ └── SegmentEditModal.js │ ├── databases │ │ ├── DatabaseDeleteModal.js │ │ └── steps │ │ │ ├── DatabaseTypes.js │ │ │ └── DatabaseDetails.js │ ├── UpgradeModal.js │ ├── workflows │ │ └── nodes │ │ │ ├── util.js │ │ │ └── DefaultNode.js │ ├── contacts │ │ └── steps │ │ │ └── SelectContacts.js │ ├── emails │ │ └── invite.jsx │ └── WorkspaceSwitcher.js ├── instrumentation.js ├── store │ └── index.js ├── providers │ └── Telemetry.js ├── config │ ├── permissions.js │ └── pricing.js ├── middleware.js └── util │ └── index.js ├── jsconfig.json ├── postcss.config.js ├── .gitmodules ├── .eslintrc.json ├── .editorconfig ├── next.config.js ├── .gitignore ├── tailwind.config.js ├── .env.example ├── package.json └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /public/img/full-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/HEAD/public/img/full-logo.png -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/HEAD/public/favicon/favicon.ico -------------------------------------------------------------------------------- /public/favicon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/HEAD/public/favicon/favicon.png -------------------------------------------------------------------------------- /src/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/HEAD/src/app/opengraph-image.png -------------------------------------------------------------------------------- /public/img/charts/area-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/HEAD/public/img/charts/area-chart.png -------------------------------------------------------------------------------- /public/img/charts/bar-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/HEAD/public/img/charts/bar-chart.png -------------------------------------------------------------------------------- /public/img/charts/pie-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/HEAD/public/img/charts/pie-chart.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /public/img/charts/table-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/HEAD/public/img/charts/table-chart.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/img/charts/counter-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/supaboard/app/HEAD/public/img/charts/counter-chart.png -------------------------------------------------------------------------------- /src/app/(app)/dashboards/[id]/loading.js: -------------------------------------------------------------------------------- 1 | export default function Dashboard({ params }) { 2 | return ( 3 | <> 4 | ) 5 | } -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/app/api/payments-api"] 2 | path = src/app/api/payments-api 3 | url = https://github.com/supaboard/payments-api.git 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "eqeqeq": "off", 5 | "quotes": ["error", "double"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/(app)/segments/new/layout.js: -------------------------------------------------------------------------------- 1 | export default async function SegmentsLayout({ children }) { 2 | return ( 3 |
4 | {children} 5 |
6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = tab 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/app/(app)/setup/state/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { createClient } from "@supabase/supabase-js" 3 | 4 | 5 | export async function GET(req) { 6 | return NextResponse.json({ null: [] }) 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/stripe/stripe.js: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe"; 2 | 3 | export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? "", { 4 | apiVersion: "2020-08-27", 5 | appInfo: { 6 | name: "supaboard", 7 | version: "0.1.0" 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/util/index.js: -------------------------------------------------------------------------------- 1 | export function classNames(...classes) { 2 | return classes.filter(Boolean).join(" ") 3 | } 4 | 5 | export function formatDate(input) { 6 | const date = new Date(input) 7 | return date.toLocaleDateString("en-US", { 8 | year: "numeric", 9 | month: "long", 10 | day: "numeric" 11 | }) 12 | } -------------------------------------------------------------------------------- /src/app/(public)/public/layout.js: -------------------------------------------------------------------------------- 1 | import "@/app/globals.css" 2 | 3 | export default function RootLayout({ children }) { 4 | return ( 5 | 6 | 7 |
8 | {children} 9 |
10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/instrumentation.js: -------------------------------------------------------------------------------- 1 | export async function register() { 2 | if (process.env.NEXT_RUNTIME == "nodejs" && process.env.IS_PLATFORM == true && process.env.NEXT_PUBLIC_ENV != "dev") { 3 | const { registerHighlight } = await import("@highlight-run/next/server") 4 | 5 | registerHighlight({ 6 | projectID: process.env.NEXT_PUBLIC_HIGHLIGHT_KEY, 7 | }) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/stripe/stripe-client.js: -------------------------------------------------------------------------------- 1 | import { loadStripe, Stripe } from "@stripe/stripe-js" 2 | 3 | let stripePromise 4 | 5 | export const getStripe = () => { 6 | if (!stripePromise) { 7 | stripePromise = loadStripe( 8 | process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_LIVE ?? process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? "" 9 | ); 10 | } 11 | 12 | return stripePromise 13 | } -------------------------------------------------------------------------------- /src/components/Skeleton.js: -------------------------------------------------------------------------------- 1 | const Skeleton = ({width, height , className, darkness = 100}) => { 2 | return ( 3 | 4 | 5 | 6 | ) 7 | } 8 | 9 | export default Skeleton 10 | -------------------------------------------------------------------------------- /src/app/(app)/settings/layout.js: -------------------------------------------------------------------------------- 1 | import { SettingsNav } from "@/components/settings/SettingsNav" 2 | 3 | export default async function SettingsLayout({ children }) { 4 | 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | {children} 12 |
13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/app/(app)/setup/page.js: -------------------------------------------------------------------------------- 1 | 2 | async function getData() { 3 | const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/setup/state`) 4 | if (!res.ok) { 5 | throw new Error("Failed to fetch data") 6 | } 7 | 8 | return res.json() 9 | } 10 | 11 | export default async function Setup() { 12 | const data = await getData() 13 | 14 | return ( 15 |
16 | 17 |
18 | ) 19 | } -------------------------------------------------------------------------------- /src/lib/operators.js: -------------------------------------------------------------------------------- 1 | export const operatorMap = { 2 | "eq": "=", 3 | "neq": "!=", 4 | "gt": ">", 5 | "gte": ">=", 6 | "lt": "<", 7 | "lte": "<=", 8 | "like": "LIKE", 9 | "not_like": "NOT LIKE", 10 | "is": "IS", 11 | "is_not": "IS NOT", 12 | "in": "IN", 13 | "not_in": "NOT IN", 14 | "between": "BETWEEN", 15 | "not_between": "NOT BETWEEN", 16 | "contains": "@>", 17 | "contained_by": "<@", 18 | "has_key": "?", 19 | "has_keys_any": "?|", 20 | "has_keys_all": "?&", 21 | } 22 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const nextBuildId = require("next-build-id") 2 | 3 | const nextConfig = { 4 | generateBuildId: () => nextBuildId({ dir: __dirname }), 5 | experimental: { 6 | appDir: true, 7 | serverActions: true, 8 | esmExternals: "loose", 9 | instrumentationHook: true 10 | }, 11 | productionBrowserSourceMaps: false, 12 | async redirects() { 13 | return [ 14 | { 15 | source: "/", 16 | destination: "/overview", 17 | permanent: true 18 | } 19 | ] 20 | }, 21 | } 22 | 23 | module.exports = nextConfig 24 | -------------------------------------------------------------------------------- /src/components/dashboards/charts/ChartError.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { ExclamationTriangleIcon } from "@heroicons/react/24/outline" 3 | 4 | export function ChartError() { 5 | return ( 6 |
7 |
8 | 9 |
Error loading chart
10 |
11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .vscode 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 | -------------------------------------------------------------------------------- /src/app/(auth)/auth/signout/route.js: -------------------------------------------------------------------------------- 1 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 2 | import { cookies } from "next/headers" 3 | import { NextResponse } from "next/server" 4 | 5 | export async function POST(req) { 6 | const supabase = createRouteHandlerClient({ cookies }) 7 | 8 | // Check if we have a session 9 | const { 10 | data: { session }, 11 | } = await supabase.auth.getSession() 12 | 13 | if (session) { 14 | await supabase.auth.signOut() 15 | } 16 | 17 | return NextResponse.redirect(new URL("/", req.url), { 18 | status: 302, 19 | }) 20 | } -------------------------------------------------------------------------------- /src/app/(app)/layout.js: -------------------------------------------------------------------------------- 1 | import { Toaster } from "sonner" 2 | import "tippy.js/dist/tippy.css" 3 | import "@/app/globals.css" 4 | 5 | import { Sidebar } from "@/components/Sidebar" 6 | import { UserMenu } from "@/components/UserMenu" 7 | 8 | export default async function RootLayout({ children }) { 9 | return ( 10 | <> 11 |
12 | 13 |
14 | 15 |
16 |
17 | {children} 18 |
19 |
20 |
21 |
22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/app/(auth)/auth/callback/route.js: -------------------------------------------------------------------------------- 1 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 2 | import { cookies } from "next/headers" 3 | import { NextResponse } from "next/server" 4 | 5 | export async function GET(request) { 6 | const requestUrl = new URL(request.url) 7 | const code = requestUrl.searchParams.get("code") 8 | 9 | if (code) { 10 | const supabase = createRouteHandlerClient({ cookies }) 11 | await supabase.auth.exchangeCodeForSession(code) 12 | } 13 | 14 | // URL to redirect to after sign in process completes 15 | return NextResponse.redirect(requestUrl.origin, { status: 302 }) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/brand/Logo.js: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import Link from "next/link" 3 | 4 | const Logo = () => { 5 | return ( 6 | <> 7 | 8 |
9 |
10 | Supaboard 11 |
12 |
13 | Supaboard 14 |
15 |
16 | 17 | 18 | ) 19 | } 20 | 21 | 22 | export default Logo -------------------------------------------------------------------------------- /src/app/(auth)/login/page.js: -------------------------------------------------------------------------------- 1 | import AuthForm from "@/components/auth/AuthForm" 2 | import Logo from "@/components/brand/Logo" 3 | 4 | export default function Login() { 5 | return ( 6 |
7 |
8 |
9 | 10 |
11 |

12 | Sign in to continue. 13 |

14 |
15 |
16 | 17 |
18 |
19 | ) 20 | } -------------------------------------------------------------------------------- /src/components/dashboards/charts/ChartErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { ChartError } from "./ChartError" 3 | 4 | 5 | class ChartErrorBoundary extends React.Component { 6 | constructor(props) { 7 | super(props) 8 | this.state = { hasError: false } 9 | } 10 | 11 | static getDerivedStateFromError(error) { 12 | return { hasError: true } 13 | } 14 | 15 | componentDidCatch(error, errorInfo) { 16 | console.log({ error, errorInfo }) 17 | } 18 | 19 | render() { 20 | if (this.state.hasError) { 21 | return ( 22 | 23 | ) 24 | } 25 | 26 | return this.props.children 27 | } 28 | } 29 | 30 | export default ChartErrorBoundary 31 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 12 | "gradient-conic": 13 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 14 | }, 15 | colors: { 16 | "dark": "#444", 17 | "highlight": "#31dc8e", 18 | } 19 | }, 20 | }, 21 | plugins: [], 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/database.js: -------------------------------------------------------------------------------- 1 | import { decrypt } from "./crypto" 2 | 3 | export const getConnectionDetails = async (supabase, account_id, tabase_uuid) => { 4 | let query = supabase 5 | .from("databases") 6 | .select() 7 | .eq("uuid", tabase_uuid) 8 | 9 | if (process.env.IS_PLATFORM) { 10 | query = query.eq("account_id", account_id) 11 | } 12 | 13 | query = query.single() 14 | const { data: database, error } = await query 15 | 16 | if (error) { 17 | console.log(error) 18 | throw new Error("Internal Server Error") 19 | } 20 | 21 | let connectionDetails = JSON.parse(decrypt(database.connection)) 22 | connectionDetails.type = database.type 23 | 24 | return connectionDetails 25 | } 26 | -------------------------------------------------------------------------------- /public/img/databases/planetscale.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_APP_URL=http://localhost:3000 2 | NEXT_PUBLIC_APP_LANDING=/overview 3 | NEXT_PUBLIC_ENV=dev 4 | 5 | # Differentiate between cloud and self-hosted 6 | IS_PLATFORM=true 7 | NEXT_PUBLIC_IS_PLATFORM=true 8 | NEXT_PUBLIC_SIGNUP_CLOSED=false 9 | 10 | # Supabase / database 11 | NEXT_PUBLIC_SUPABASE_URL=https://XYZ.supabase.co 12 | NEXT_PUBLIC_SUPABASE_ANON_KEY=XYZ 13 | SUPABASE_SERVICE_ROLE_KEY=XYZ 14 | SUPABASE_JWT_SECRET=XYZ 15 | 16 | # We encrypt all database connection details before storing them in the database 17 | # This is the key used to encrypt the connection details 18 | CRYPTO_SECRET_KEY=XYZ 19 | CRYPRO_SECRET_IV=XYZ 20 | CRYPTO_ENCRYPTION_METHOD=aes-256-cbc 21 | 22 | # Resend / email 23 | RESEND_API_KEY=re_XYZ 24 | -------------------------------------------------------------------------------- /src/app/(app)/thank-you/page.js: -------------------------------------------------------------------------------- 1 | import Logo from "@/components/brand/Logo" 2 | 3 | export default async function ThankYou() { 4 | return ( 5 |
6 |
7 | 8 |
9 |

Thank you!

10 |

11 | You're awesome. Thank you very much for signing up for Supaboard! If 12 | you need help with anything, don't hesitate to contact us. 13 |

14 |
15 | 16 | Go Home 17 | 18 |
19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { create } from "zustand" 2 | 3 | const useStore = create((set) => ({ 4 | nodes: [], 5 | edges: [], 6 | dashboard: {}, 7 | showUpgradeModal: false, 8 | showSidebar: false, 9 | 10 | setShowSidebar: (show) => { 11 | set((state) => ({ showSidebar: show })) 12 | }, 13 | 14 | setShowUpgradeModal: (show) => { 15 | set((state) => ({ showUpgradeModal: show })) 16 | }, 17 | 18 | setDashboard: (dash) => { 19 | set((state) => ({ dashboard: dash })) 20 | }, 21 | 22 | setNodes: (nds) => { 23 | set((state) => ({ nodes: nds })) 24 | }, 25 | 26 | setEdges: (eds) => { 27 | set((state) => ({ edges: eds })) 28 | }, 29 | })) 30 | 31 | export default useStore 32 | -------------------------------------------------------------------------------- /src/lib/crypto.js: -------------------------------------------------------------------------------- 1 | import crypto from "crypto" 2 | const key = crypto.createHash("sha512").update(process.env.CRYPTO_SECRET_KEY).digest("hex").substring(0, 32) 3 | const encryptionIV = crypto.createHash("sha512").update(process.env.CRYPRO_SECRET_IV).digest("hex").substring(0, 16) 4 | 5 | export function encrypt(data) { 6 | const cipher = crypto.createCipheriv(process.env.CRYPTO_ENCRYPTION_METHOD, key, encryptionIV) 7 | return Buffer.from(cipher.update(data, "utf8", "hex") + cipher.final("hex")).toString("base64") 8 | } 9 | 10 | export function decrypt(encryptedData) { 11 | const buff = Buffer.from(encryptedData, "base64") 12 | const decipher = crypto.createDecipheriv(process.env.CRYPTO_ENCRYPTION_METHOD, key, encryptionIV) 13 | return (decipher.update(buff.toString("utf8"), "hex", "utf8") + decipher.final("utf8")) 14 | } 15 | -------------------------------------------------------------------------------- /src/app/api/dashboards/hash/[hash]/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | 4 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 5 | 6 | 7 | export async function GET(req, { params }) { 8 | const hash = params.hash 9 | 10 | const supabase = createRouteHandlerClient({ cookies }, { 11 | options: { 12 | db: { schema: "supaboard" } 13 | } 14 | }) 15 | 16 | const { data: dashboard, error } = await supabase 17 | .from("dashboards") 18 | .select() 19 | .eq("public_hash", hash) 20 | .eq("is_public", true) 21 | .single() 22 | 23 | if (error || !dashboard) { 24 | return new NextResponse("Not found", { status: 404 }) 25 | } 26 | 27 | return NextResponse.json(dashboard) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/img/Bitbucket.js: -------------------------------------------------------------------------------- 1 | const Bitbucket = () => { 2 | return ( 3 | 4 | ) 5 | } 6 | 7 | 8 | export default Bitbucket 9 | -------------------------------------------------------------------------------- /src/app/(auth)/register/page.js: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | import AuthForm from "@/components/auth/AuthForm" 3 | import Logo from "@/components/brand/Logo" 4 | 5 | export default async function Register() { 6 | if (process.env.NEXT_PUBLIC_SIGNUP_CLOSED === "true") { 7 | redirect("/login") 8 | } 9 | 10 | return ( 11 |
12 |
13 |
14 | 15 |
16 |

17 | Create a new account to get started. 18 |

19 |
20 |
21 | 22 |
23 |
24 | ) 25 | } -------------------------------------------------------------------------------- /src/components/img/Gitlab.js: -------------------------------------------------------------------------------- 1 | const Gitlab = () => { 2 | return ( 3 | 4 | ) 5 | } 6 | 7 | 8 | export default Gitlab 9 | -------------------------------------------------------------------------------- /src/app/layout.js: -------------------------------------------------------------------------------- 1 | import "@/app/globals.css" 2 | import { UpgradeModal } from "@/components/UpgradeModal" 3 | import { Telemetry } from "@/providers/Telemetry" 4 | 5 | export const metadata = { 6 | metadataBase: new URL("https://app.supaboard.co"), 7 | title: "Supaboard — Supabase and Postgres dashboards", 8 | description: "Create reporting dashboards on top of Supabase with ease.", 9 | icons: { 10 | icon: "/favicon/favicon.png", 11 | }, 12 | } 13 | 14 | 15 | export default function RootLayout({ children }) { 16 | if (process.env.IS_PLATFORM && process.env.NEXT_PUBLIC_ENV != "dev") { 17 | return ( 18 | 19 | {children} 20 | 21 | 22 | ) 23 | } else { 24 | return ( 25 | 26 | 27 | {children} 28 | 29 | 30 | 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/(app)/dashboards/loading.js: -------------------------------------------------------------------------------- 1 | import Loading from "@/components/Loading" 2 | 3 | export default async function LoadingDashboards() { 4 | return ( 5 | <> 6 |
7 |
8 |
9 | 10 |
11 |
12 | 13 | Loading dashboards... 14 | 15 |
16 |
17 |
18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/app/(app)/settings/workspace/new/actions.js: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { cookies } from "next/headers" 4 | import { createServerActionClient } from "@supabase/auth-helpers-nextjs" 5 | import { redirect } from "next/navigation" 6 | 7 | 8 | export const createWorkspace = async (formData) => { 9 | const supabase = createServerActionClient({ cookies }, { 10 | options: { 11 | db: { schema: "supaboard" } 12 | } 13 | }) 14 | const { data: { session } } = await supabase.auth.getSession() 15 | if (!session) { 16 | throw new Error("Not authenticated") 17 | } 18 | 19 | const teamName = formData.get("name") 20 | 21 | const { data, error } = await supabase 22 | .from("accounts") 23 | .insert({ 24 | team_name: teamName 25 | }) 26 | .select() 27 | 28 | if (error) { 29 | console.log(error) 30 | throw Error(error) 31 | } 32 | 33 | redirect("/settings/workspace") 34 | } 35 | -------------------------------------------------------------------------------- /public/favicon/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/(auth)/layout.js: -------------------------------------------------------------------------------- 1 | import "@/app/globals.css" 2 | 3 | export const metadata = { 4 | title: "Supaboard — Supabase and Postgres dashboards", 5 | description: "Powerful reporting dashboards on top of Supabase.", 6 | openGraph: { 7 | type: "website", 8 | locale: "en_US", 9 | url: "https://app.supaboard.co", 10 | title: "Supaboard — Supabase and Postgres dashboards", 11 | description: "Powerful reporting dashboards on top of Supabase.", 12 | siteName: "Supaboard", 13 | }, 14 | twitter: { 15 | card: "summary_large_image", 16 | title: "Supaboard — Supabase and Postgres dashboards", 17 | description: "Powerful reporting dashboards on top of Supabase.", 18 | } 19 | } 20 | 21 | export default function AuthLayout({ children }) { 22 | return ( 23 | 24 | 25 |
26 | {children} 27 |
28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Loading.js: -------------------------------------------------------------------------------- 1 | const Loading = ({width = 25, height = 25, className}) => { 2 | return ( 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | ) 12 | } 13 | 14 | export default Loading -------------------------------------------------------------------------------- /src/components/img/Github.js: -------------------------------------------------------------------------------- 1 | const GitHub = ({ size, className }) => { 2 | return ( 3 | 4 | ) 5 | } 6 | 7 | 8 | export default GitHub 9 | -------------------------------------------------------------------------------- /src/app/(app)/contacts/action.js: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { cookies } from "next/headers" 4 | import { createServerActionClient } from "@supabase/auth-helpers-nextjs" 5 | 6 | 7 | export const createContactConnection = async (contactConnection) => { 8 | const cookieStore = cookies() 9 | const account_id = cookieStore.get("account_id").value 10 | const supabase = createServerActionClient({ cookies }, { 11 | options: { 12 | db: { schema: "supaboard" } 13 | } 14 | }) 15 | const { data: { session } } = await supabase.auth.getSession() 16 | if (!session) { 17 | throw new Error("Not authenticated") 18 | } 19 | 20 | contactConnection.account_id = account_id 21 | 22 | try { 23 | const { data, error } = await supabase 24 | .from("contacts") 25 | .insert([ 26 | contactConnection 27 | ]) 28 | .select() 29 | .single() 30 | 31 | if (error) { 32 | console.log(error) 33 | throw Error(error) 34 | } 35 | 36 | return data 37 | } catch (error) { 38 | throw Error(error) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/api/settings/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { checkUserAllowed } from "@/lib/auth" 5 | 6 | export async function GET(req, { params }) { 7 | const cookieStore = cookies() 8 | const account_id = cookieStore.get("account_id").value 9 | const supabase = createRouteHandlerClient({ cookies }, { 10 | options: { 11 | db: { schema: "supaboard" } 12 | } 13 | }) 14 | 15 | const { data: { session } } = await supabase.auth.getSession() 16 | if (!session) throw new Error("Not authenticated") 17 | 18 | const accountUser = await checkUserAllowed(supabase, session, account_id) 19 | let query = supabase 20 | .from("accounts") 21 | .select() 22 | 23 | if (process.env.IS_PLATFORM) { 24 | query = query.eq("account_id", account_id) 25 | } 26 | 27 | query = query.single() 28 | const { data: database, error } = await query 29 | 30 | return NextResponse.json(tables || []) 31 | } 32 | -------------------------------------------------------------------------------- /src/app/(app)/settings/workspace/new/page.js: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { redirect } from "next/navigation" 4 | import { createWorkspace } from "./actions" 5 | 6 | export default async function NewWorkspace() { 7 | 8 | return ( 9 | <> 10 |

11 | Create a new workspace 12 |

13 | 14 |
15 |
16 | 17 | Workspace name 18 | 19 |
20 |
21 |
22 | 23 |
24 | 30 |
31 |
32 |
33 |
34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/components/auth/AuthForm.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { Auth } from "@supabase/auth-ui-react" 3 | import { ThemeSupa } from "@supabase/auth-ui-shared" 4 | import { createClientComponentClient } from "@supabase/auth-helpers-nextjs" 5 | import { useEffect } from "react" 6 | 7 | export default function AuthForm({ view }) { 8 | const supabase = createClientComponentClient() 9 | 10 | useEffect(() => { 11 | const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => { 12 | if (session) { 13 | window.location.href = `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback` 14 | } 15 | }) 16 | }, []) 17 | 18 | return ( 19 |
20 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/app/api/databases/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { checkUserAllowed } from "@/lib/auth" 5 | 6 | 7 | export async function GET(req) { 8 | const cookieStore = cookies() 9 | const account_id = cookieStore.get("account_id").value 10 | const supabase = createRouteHandlerClient({ cookies }, { 11 | options: { 12 | db: { schema: "supaboard" } 13 | } 14 | }) 15 | 16 | const { data: { session } } = await supabase.auth.getSession() 17 | if (!session) throw new Error("Not authenticated") 18 | 19 | const accountUser = await checkUserAllowed(supabase, session, account_id) 20 | 21 | let query = supabase 22 | .from("databases") 23 | .select() 24 | 25 | if (process.env.IS_PLATFORM) { 26 | query = query.eq("account_id", account_id) 27 | } 28 | 29 | query.order("created_at", { ascending: false }) 30 | 31 | const { data: databases, error } = await query 32 | 33 | return NextResponse.json(databases || []) 34 | } 35 | -------------------------------------------------------------------------------- /src/app/api/dashboards/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies, headers } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { checkUserAllowed } from "@/lib/auth" 5 | 6 | 7 | export async function GET(req) { 8 | const cookieStore = cookies() 9 | const account_id = cookieStore.get("account_id").value 10 | 11 | const supabase = createRouteHandlerClient({ cookies }, { 12 | options: { 13 | db: { schema: "supaboard" } 14 | } 15 | }) 16 | 17 | const { data: { session } } = await supabase.auth.getSession() 18 | if (!session) throw new Error("Not authenticated") 19 | 20 | const accountUser = await checkUserAllowed(supabase, session, account_id) 21 | 22 | let query = supabase 23 | .from("dashboards") 24 | .select() 25 | 26 | if (process.env.IS_PLATFORM) { 27 | query = query.eq("account_id", account_id) 28 | } 29 | 30 | query.order("created_at", { ascending: false }) 31 | 32 | const { data: dashboards, error } = await query 33 | 34 | 35 | return NextResponse.json(dashboards || []) 36 | } 37 | -------------------------------------------------------------------------------- /src/app/api/databases/[id]/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { checkUserAllowed } from "@/lib/auth" 5 | 6 | 7 | export async function GET(req, { params }) { 8 | const id = params.id 9 | const cookieStore = cookies() 10 | const account_id = cookieStore.get("account_id").value 11 | const supabase = createRouteHandlerClient({ cookies }, { 12 | options: { 13 | db: { schema: "supaboard" } 14 | } 15 | }) 16 | 17 | const { data: { session } } = await supabase.auth.getSession() 18 | if (!session) throw new Error("Not authenticated") 19 | 20 | const accountUser = await checkUserAllowed(supabase, session, account_id) 21 | 22 | let query = supabase 23 | .from("databases") 24 | .select() 25 | .eq("uuid", id) 26 | 27 | if (process.env.IS_PLATFORM) { 28 | query = query.eq("account_id", account_id) 29 | } 30 | 31 | query = query.single() 32 | 33 | 34 | const { data: database, error } = await query 35 | 36 | return NextResponse.json(database || []) 37 | } 38 | -------------------------------------------------------------------------------- /public/img/databases/supabase.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/app/api/segments/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { checkUserAllowed } from "@/lib/auth" 5 | 6 | export async function GET(req, { params }) { 7 | const cookieStore = cookies() 8 | const account_id = cookieStore.get("account_id").value 9 | const supabase = createRouteHandlerClient({ cookies }, { 10 | options: { 11 | db: { schema: "supaboard" } 12 | } 13 | }) 14 | 15 | const { data: { session } } = await supabase.auth.getSession() 16 | if (!session) throw new Error("Not authenticated") 17 | 18 | const accountUser = await checkUserAllowed(supabase, session, account_id) 19 | 20 | let query = supabase 21 | .from("segments") 22 | .select() 23 | 24 | if (process.env.IS_PLATFORM) { 25 | query = query.eq("account_id", account_id) 26 | } 27 | 28 | query.order("created_at", { ascending: false }) 29 | 30 | const { data: segments, error } = await query 31 | 32 | if (error) { 33 | console.log(error) 34 | throw Error(error) 35 | } 36 | 37 | return NextResponse.json(segments) 38 | } 39 | -------------------------------------------------------------------------------- /src/app/(app)/settings/page.js: -------------------------------------------------------------------------------- 1 | import { cookies } from "next/headers" 2 | import { createServerComponentClient } from "@supabase/auth-helpers-nextjs" 3 | 4 | 5 | // async function getData() { 6 | // const res = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/settings`) 7 | // if (!res.ok) { 8 | // throw new Error('Failed to fetch data') 9 | // } 10 | 11 | // return res.json() 12 | // } 13 | 14 | export default async function Settings() { 15 | const supabase = createServerComponentClient({ cookies }) 16 | const { data: { session } } = await supabase.auth.getSession() 17 | 18 | // const data = await getData() 19 | 20 | return ( 21 | <> 22 |

23 | Manage your personal account information 24 |

25 |
26 |
27 | 28 | Email 29 | 30 |
31 |
32 | 33 |
34 |
35 | 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /src/components/settings/SettingsNav.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import { useSelectedLayoutSegment, usePathname } from "next/navigation" 5 | import { classNames } from "@/components/util" 6 | 7 | const navigation = [ 8 | { name: "Personal details", href: "/settings", current: true }, 9 | { name: "Workspaces", href: "/settings/workspace", current: false }, 10 | { name: "Billing", href: "/settings/billing", current: false }, 11 | ] 12 | 13 | 14 | export function SettingsNav({ children }) { 15 | const segment = useSelectedLayoutSegment() 16 | 17 | return ( 18 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/components/img/Slack.js: -------------------------------------------------------------------------------- 1 | const Slack = ({ size, className }) => { 2 | return ( 3 | 4 | ) 5 | } 6 | 7 | 8 | export default Slack 9 | -------------------------------------------------------------------------------- /src/app/api/contacts/segments/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | import { Client } from "pg" 5 | import { checkUserAllowed } from "@/lib/auth" 6 | import { getConnectionDetails } from "@/lib/database" 7 | 8 | 9 | export async function GET(req) { 10 | const cookieStore = cookies() 11 | const account_id = cookieStore.get("account_id").value 12 | 13 | const supabase = createRouteHandlerClient({ cookies }, { 14 | options: { 15 | db: { schema: "supaboard" } 16 | } 17 | }) 18 | 19 | const { data: { session } } = await supabase.auth.getSession() 20 | if (!session) throw new Error("Not authenticated") 21 | 22 | const accountUser = await checkUserAllowed(supabase, session, account_id) 23 | 24 | let query = supabase.from("segments").select() 25 | 26 | if (process.env.IS_PLATFORM) { 27 | query = query.eq("account_id", account_id) 28 | } 29 | 30 | const { data: segments, error } = await query 31 | 32 | if (error) { 33 | console.log(error) 34 | throw new Error("Internal Server Error") 35 | } 36 | 37 | if (!segments || segments.length === 0) { 38 | return NextResponse.json(null) 39 | } 40 | 41 | return NextResponse.json(segments) 42 | } 43 | -------------------------------------------------------------------------------- /src/app/api/dashboards/data/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | 5 | import { getConnectionDetails } from "@/lib/database" 6 | import { fetchPostgresData } from "@/lib/adapters/postgres/querybuilder" 7 | import { fetchMySQLData } from "@/lib/adapters/mysql/querybuilder" 8 | 9 | 10 | export async function POST(req) { 11 | try { 12 | const data = await req.json() 13 | const cookieStore = cookies() 14 | const account_id = cookieStore.get("account_id").value 15 | 16 | const supabase = createRouteHandlerClient({ cookies }, { 17 | options: { 18 | db: { schema: "supaboard" } 19 | } 20 | }) 21 | 22 | const connectionDetails = await getConnectionDetails(supabase, account_id, data.database) 23 | let result 24 | 25 | if (connectionDetails.type == "postgres" || connectionDetails.type == "supabase") { 26 | result = await fetchPostgresData(data, connectionDetails) 27 | } 28 | 29 | if (connectionDetails.type == "mysql" || connectionDetails.type == "planetscale") { 30 | result = await fetchMySQLData(data, connectionDetails) 31 | } 32 | 33 | return NextResponse.json(result || []) 34 | } catch (error) { 35 | console.log(error) 36 | return NextResponse.json([]) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app/api/dashboards/[uuid]/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 4 | 5 | export async function GET(req, { params }) { 6 | const supabase = createRouteHandlerClient({ cookies }, { 7 | options: { 8 | db: { schema: "supaboard" } 9 | } 10 | }) 11 | 12 | const uuid = params.uuid 13 | 14 | const { data: dashboard, error } = await supabase 15 | .from("dashboards") 16 | .select() 17 | .eq("uuid", uuid) 18 | .single() 19 | 20 | if (error) { 21 | console.log(error) 22 | } 23 | 24 | return NextResponse.json(dashboard || []) 25 | } 26 | 27 | 28 | export async function PUT(req, { params }) { 29 | const supabase = createRouteHandlerClient({ cookies }, { 30 | options: { 31 | db: { schema: "supaboard" } 32 | } 33 | }) 34 | 35 | const uuid = params.uuid 36 | const data = await req.json() 37 | 38 | const { data: dashboard, error } = await supabase 39 | .from("dashboards") 40 | .update({config: data.config }) 41 | .eq("uuid", uuid) 42 | .select() 43 | .single() 44 | 45 | if (error) { 46 | console.log(error) 47 | } 48 | 49 | return NextResponse.json(dashboard || []) 50 | } 51 | -------------------------------------------------------------------------------- /src/app/api/dashboards/[uuid]/charts/[chart_id]/route.js: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import { cookies } from "next/headers" 3 | 4 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs" 5 | 6 | 7 | export async function PUT(req, { params }) { 8 | const uuid = params.uuid 9 | const chart_id = params.chart_id 10 | const data = await req.json() 11 | let newConfig 12 | 13 | const supabase = createRouteHandlerClient({ cookies }, { 14 | options: { 15 | db: { schema: "supaboard" } 16 | } 17 | }) 18 | 19 | if (data.duplicate) { 20 | const chart = data.dashboard.config.charts.find((chart) => parseInt(chart.id) == parseInt(chart_id)) 21 | const newChart = { ...chart, id: new Date().getTime() } 22 | newConfig = { ...data.dashboard.config, charts: [...data.dashboard.config.charts, newChart] } 23 | } else { 24 | const charts = data.dashboard.config.charts.filter((chart) => parseInt(chart.id) != parseInt(chart_id)) 25 | newConfig = { ...data.dashboard.config, charts } 26 | } 27 | 28 | const { data: dashboard, error } = await supabase 29 | .from("dashboards") 30 | .update({config: newConfig }) 31 | .eq("uuid", uuid) 32 | .select() 33 | .single() 34 | 35 | if (error) { 36 | console.log(error) 37 | } 38 | 39 | return NextResponse.json(dashboard || []) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/dashboards/NoDashboards.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useState } from "react" 4 | import { PlusIcon } from "@heroicons/react/20/solid" 5 | import { Toaster } from "sonner" 6 | 7 | import { DashboardNewModal } from "@/components/dashboards/DashboardNewModal" 8 | 9 | 10 | export function NoDashboards() { 11 | const [showModal, setShowModal] = useState(false) 12 | 13 | return ( 14 | <> 15 |
16 |
17 | 18 |
19 | 20 | You don't have any dashboards yet.
Create a new dashboard to get started. 21 |
22 | 28 |
29 | 30 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/providers/Telemetry.js: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Script from "next/script" 4 | import posthog from "posthog-js" 5 | import { PostHogProvider } from "posthog-js/react" 6 | import { HighlightInit, ErrorBoundary } from "@highlight-run/next/client" 7 | import { useEffect } from "react" 8 | import { Crisp } from "crisp-sdk-web" 9 | 10 | if (typeof window !== "undefined") { 11 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { 12 | api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST 13 | }) 14 | } 15 | 16 | export function Telemetry({ children }) { 17 | useEffect(() => { 18 | Crisp.configure(process.env.NEXT_PUBLIC_CRISP_ID) 19 | }, []) 20 | 21 | return ( 22 | <> 23 | 32 | 33 | 34 | 35 | {children} 36 | 37 | 38 |